From 8175debde79c64694ff14f73ab153cdc9e10f120 Mon Sep 17 00:00:00 2001 From: Haz Date: Wed, 15 Jan 2020 11:23:21 -0300 Subject: [PATCH] Add warning package (#19317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new warning package * Add babel-plugin-dev-expression * Remove babel-plugin-dev-expression * Add babel-plugin to the warning pacakge * Include @wordpress/warning/babel-plugin into @wordpress/babel-preset-default * Add warning to webpack.config.js * Check the presence of process.env * Check presence of process.env in the code transformed by the babel plugin * Move babel-plugin.js and its test file to the directory top-level * Improve README with information about dead code removal * Turn sideEffects into true in package.json * Revert sideEffects change * npm run docs:build * Update packages/warning/babel-plugin.js Co-Authored-By: Grzegorz (Greg) Ziółkowski * warning: Add types checking for warning package * Avoid indentation on the warning function with an early return * Fix TS error on babel-plugin.js * Accept a single string as message instead of multiple messages Co-authored-by: Grzegorz (Greg) Ziółkowski Co-authored-by: Andrew Duthie --- bin/api-docs/packages.js | 1 + docs/manifest-devhub.json | 6 ++ package-lock.json | 4 + package.json | 1 + packages/babel-preset-default/index.js | 1 + packages/babel-preset-default/package.json | 1 + packages/warning/.npmrc | 1 + packages/warning/CHANGELOG.md | 0 packages/warning/README.md | 56 +++++++++++ packages/warning/babel-plugin.js | 88 ++++++++++++++++++ packages/warning/package.json | 27 ++++++ packages/warning/src/index.js | 45 +++++++++ packages/warning/src/test/index.js | 30 ++++++ packages/warning/test/babel-plugin.js | 102 +++++++++++++++++++++ tsconfig.json | 3 +- webpack.config.js | 1 + 16 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 packages/warning/.npmrc create mode 100644 packages/warning/CHANGELOG.md create mode 100644 packages/warning/README.md create mode 100644 packages/warning/babel-plugin.js create mode 100644 packages/warning/package.json create mode 100644 packages/warning/src/index.js create mode 100644 packages/warning/src/test/index.js create mode 100644 packages/warning/test/babel-plugin.js diff --git a/bin/api-docs/packages.js b/bin/api-docs/packages.js index ff96517799bf9f..dcc583fe65d0e2 100644 --- a/bin/api-docs/packages.js +++ b/bin/api-docs/packages.js @@ -32,6 +32,7 @@ const packages = [ 'shortcode', 'url', 'viewport', + 'warning', 'wordcount', ]; diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json index 29ca54d6084946..50d6f02c8b9f16 100644 --- a/docs/manifest-devhub.json +++ b/docs/manifest-devhub.json @@ -1487,6 +1487,12 @@ "markdown_source": "../packages/viewport/README.md", "parent": "packages" }, + { + "title": "@wordpress/warning", + "slug": "packages-warning", + "markdown_source": "../packages/warning/README.md", + "parent": "packages" + }, { "title": "@wordpress/wordcount", "slug": "packages-wordcount", diff --git a/package-lock.json b/package-lock.json index 4532e846ec98e6..ad3e6931f83d37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8308,6 +8308,7 @@ "@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma", "@wordpress/browserslist-config": "file:packages/browserslist-config", "@wordpress/element": "file:packages/element", + "@wordpress/warning": "file:packages/warning", "core-js": "^3.1.4" } }, @@ -9111,6 +9112,9 @@ "lodash": "^4.17.15" } }, + "@wordpress/warning": { + "version": "file:packages/warning" + }, "@wordpress/wordcount": { "version": "file:packages/wordcount", "requires": { diff --git a/package.json b/package.json index 71b5fda8a7635b..c1413bd2eced7d 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", + "@wordpress/warning": "file:packages/warning", "@wordpress/wordcount": "file:packages/wordcount" }, "devDependencies": { diff --git a/packages/babel-preset-default/index.js b/packages/babel-preset-default/index.js index a1343706b03b8b..6d368c70f0d00b 100644 --- a/packages/babel-preset-default/index.js +++ b/packages/babel-preset-default/index.js @@ -56,6 +56,7 @@ module.exports = function( api ) { presets: [ getPresetEnv() ], plugins: [ require.resolve( '@babel/plugin-proposal-object-rest-spread' ), + require.resolve( '@wordpress/warning/babel-plugin' ), [ require.resolve( '@wordpress/babel-plugin-import-jsx-pragma' ), { diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 5ae690b47350a2..061e4528eba757 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -37,6 +37,7 @@ "@wordpress/babel-plugin-import-jsx-pragma": "file:../babel-plugin-import-jsx-pragma", "@wordpress/browserslist-config": "file:../browserslist-config", "@wordpress/element": "file:../element", + "@wordpress/warning": "file:../warning", "core-js": "^3.1.4" }, "publishConfig": { diff --git a/packages/warning/.npmrc b/packages/warning/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/warning/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/warning/CHANGELOG.md b/packages/warning/CHANGELOG.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/warning/README.md b/packages/warning/README.md new file mode 100644 index 00000000000000..74d5d2d7f5cd34 --- /dev/null +++ b/packages/warning/README.md @@ -0,0 +1,56 @@ +# Warning + +Utility for warning messages to the console based on a passed condition. + +## Installation + +Install the module + +```bash +npm install @wordpress/warning --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ + +## Reducing bundle size + +Literal strings aren't minified. Keeping them in your production bundle may increase the bundle size significantly. + +To prevent that, you should: + +1. Put `@wordpress/warning/babel-plugin` into your [babel config](https://babeljs.io/docs/en/plugins#plugin-options) or use [`@wordpress/babel-preset-default`](https://www.npmjs.com/package/@wordpress/babel-preset-default), which already includes the babel plugin. + + This will make sure your `warning` calls are wrapped within a condition that checks if `process.env.NODE_ENV !== 'production'`. + +2. Use [UglifyJS](https://github.com/mishoo/UglifyJS2), [Terser](https://github.com/terser/terser) or any other JavaScript parser that performs [dead code elimination](https://en.wikipedia.org/wiki/Dead_code_elimination). This is usually used in conjunction with JavaScript bundlers, such as [webpack](https://github.com/webpack/webpack). + + When parsing the code in `production` mode, the `warning` call will be removed altogether. + +## API + + + +# **default** + +Shows a warning with `message` if `condition` passes and environment is not `production`. + +_Usage_ + +```js +import warning from '@wordpress/warning'; + +function MyComponent( props ) { + warning( ! props.title, '`props.title` was not passed' ); + ... +} +``` + +_Parameters_ + +- _condition_ `boolean`: Whether the warning will be triggered or not. +- _message_ `string`: Message to show in the warning. + + + + +

Code is Poetry.

diff --git a/packages/warning/babel-plugin.js b/packages/warning/babel-plugin.js new file mode 100644 index 00000000000000..2266fe0a0d416e --- /dev/null +++ b/packages/warning/babel-plugin.js @@ -0,0 +1,88 @@ +/** + * Internal dependencies + */ +const pkg = require( './package.json' ); + +/** + * Babel plugin which transforms `warning` function calls to wrap within a + * condition that checks if `process.env.NODE_ENV !== 'production'`. + * + * @param {import('@babel/core')} babel Current Babel object. + * + * @return {import('@babel/core').PluginObj} Babel plugin object. + */ +function babelPlugin( { types: t } ) { + const seen = Symbol(); + + const typeofProcessExpression = t.binaryExpression( + '!==', + t.unaryExpression( 'typeof', t.identifier( 'process' ), false ), + t.stringLiteral( 'undefined' ) + ); + + const processEnvExpression = t.memberExpression( + t.identifier( 'process' ), + t.identifier( 'env' ), + false + ); + + const nodeEnvCheckExpression = t.binaryExpression( + '!==', + t.memberExpression( processEnvExpression, t.identifier( 'NODE_ENV' ), false ), + t.stringLiteral( 'production' ) + ); + + const logicalExpression = t.logicalExpression( + '&&', + t.logicalExpression( '&&', typeofProcessExpression, processEnvExpression ), + nodeEnvCheckExpression + ); + + return { + visitor: { + ImportDeclaration( path, state ) { + const { node } = path; + const isThisPackageImport = node.source.value.indexOf( pkg.name ) !== -1; + + if ( ! isThisPackageImport ) { + return; + } + + const defaultSpecifier = node.specifiers.find( + ( specifier ) => specifier.type === 'ImportDefaultSpecifier' + ); + + if ( defaultSpecifier && defaultSpecifier.local ) { + const { name } = defaultSpecifier.local; + state.callee = name; + } + }, + CallExpression( path, state ) { + const { node } = path; + + // Ignore if it's already been processed + if ( node[ seen ] ) { + return; + } + + const name = state.callee || state.opts.callee; + + if ( path.get( 'callee' ).isIdentifier( { name } ) ) { + // Turns this code: + // warning(condition, argument, argument); + // into this: + // typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warning(condition, argument, argument) : void 0; + node[ seen ] = true; + path.replaceWith( + t.ifStatement( + logicalExpression, + t.blockStatement( [ t.expressionStatement( node ) ] ) + ) + ); + } + }, + }, + }; +} + +module.exports = babelPlugin; diff --git a/packages/warning/package.json b/packages/warning/package.json new file mode 100644 index 00000000000000..076f28d16150ed --- /dev/null +++ b/packages/warning/package.json @@ -0,0 +1,27 @@ +{ + "name": "@wordpress/warning", + "version": "0.0.1", + "description": "Warning utility for WordPress.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "warning" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/warning/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/warning" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "sideEffects": false, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/warning/src/index.js b/packages/warning/src/index.js new file mode 100644 index 00000000000000..9774020697b7a4 --- /dev/null +++ b/packages/warning/src/index.js @@ -0,0 +1,45 @@ +function isDev() { + return ( + typeof process !== 'undefined' && + process.env && + process.env.NODE_ENV !== 'production' + ); +} + +/** + * Shows a warning with `message` if `condition` passes and environment is not `production`. + * + * @param {boolean} condition Whether the warning will be triggered or not. + * @param {string} message Message to show in the warning. + * + * @example + * ```js + * import warning from '@wordpress/warning'; + * + * function MyComponent( props ) { + * warning( ! props.title, '`props.title` was not passed' ); + * ... + * } + * ``` + */ +export default function warning( condition, message ) { + if ( ! isDev() ) { + return; + } + + if ( ! condition ) { + return; + } + + // eslint-disable-next-line no-console + console.warn( message ); + + // Throwing an error and catching it immediately to improve debugging + // A consumer can use 'pause on caught exceptions' + // https://github.com/facebook/react/issues/4216 + try { + throw Error( message ); + } catch ( x ) { + // do nothing + } +} diff --git a/packages/warning/src/test/index.js b/packages/warning/src/test/index.js new file mode 100644 index 00000000000000..b5e83253ab6086 --- /dev/null +++ b/packages/warning/src/test/index.js @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import warning from '..'; + +const initialNodeEnv = process.env.NODE_ENV; + +describe( 'warning', () => { + afterEach( () => { + process.env.NODE_ENV = initialNodeEnv; + } ); + + it( 'logs to console.warn when NODE_ENV is not "production"', () => { + process.env.NODE_ENV = 'development'; + warning( true, 'warning' ); + expect( console ).toHaveWarnedWith( 'warning' ); + } ); + + it( 'does not log to console.warn if NODE_ENV is "production"', () => { + process.env.NODE_ENV = 'production'; + warning( true, 'warning' ); + expect( console ).not.toHaveWarned(); + } ); + + it( 'does not log to console.warn if condition is falsy', () => { + process.env.NODE_ENV = 'development'; + warning( false, 'warning' ); + expect( console ).not.toHaveWarned(); + } ); +} ); diff --git a/packages/warning/test/babel-plugin.js b/packages/warning/test/babel-plugin.js new file mode 100644 index 00000000000000..7758af5a96b9c4 --- /dev/null +++ b/packages/warning/test/babel-plugin.js @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import { transform } from '@babel/core'; + +/** + * Internal dependencies + */ +import babelPlugin from '../babel-plugin'; + +function join( ...strings ) { + return strings.join( '\n' ); +} + +function compare( input, output, options = {} ) { + const { code } = transform( input, { + configFile: false, + plugins: [ [ babelPlugin, options ] ], + } ); + expect( code ).toEqual( output ); +} + +describe( 'babel-plugin', function() { + it( 'should replace warning calls with import declaration', () => { + compare( + join( + 'import warning from "@wordpress/warning";', + 'warning(true, "a", "b");' + ), + join( + 'import warning from "@wordpress/warning";', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warning(true, "a", "b") : void 0;' + ) + ); + } ); + + it( 'should not replace warning calls without import declaration', () => { + compare( + 'warning(true, "a", "b");', + 'warning(true, "a", "b");' + ); + } ); + + it( 'should replace warning calls without import declaration with plugin options', () => { + compare( + 'warning(true, "a", "b");', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warning(true, "a", "b") : void 0;', + { callee: 'warning' } + ); + } ); + + it( 'should replace multiple warning calls', () => { + compare( + join( + 'import warning from "@wordpress/warning";', + 'warning(true, "a", "b");', + 'warning(false, "b", "a");', + 'warning(cond, "c");', + ), + join( + 'import warning from "@wordpress/warning";', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warning(true, "a", "b") : void 0;', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warning(false, "b", "a") : void 0;', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warning(cond, "c") : void 0;' + ) + ); + } ); + + it( 'should identify warning callee name', () => { + compare( + join( + 'import warn from "@wordpress/warning";', + 'warn(true, "a", "b");', + 'warn(false, "b", "a");', + 'warn(cond, "c");', + ), + join( + 'import warn from "@wordpress/warning";', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warn(true, "a", "b") : void 0;', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warn(false, "b", "a") : void 0;', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warn(cond, "c") : void 0;' + ) + ); + } ); + + it( 'should identify warning callee name by ', () => { + compare( + join( + 'import warn from "@wordpress/warning";', + 'warn(true, "a", "b");', + 'warn(false, "b", "a");', + 'warn(cond, "c");', + ), + join( + 'import warn from "@wordpress/warning";', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warn(true, "a", "b") : void 0;', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warn(false, "b", "a") : void 0;', + 'typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" ? warn(cond, "c") : void 0;' + ) + ); + } ); +} ); diff --git a/tsconfig.json b/tsconfig.json index a1854349a9d95d..6e74bf45066430 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,7 +38,8 @@ "./packages/is-shallow-equal/**/*.js", "./packages/priority-queue/**/*.js", "./packages/token-list/**/*.js", - "./packages/url/**/*.js" + "./packages/url/**/*.js", + "./packages/warning/**/*.js", ], "exclude": [ "./packages/*/benchmark", diff --git a/webpack.config.js b/webpack.config.js index f1b9c578192eba..586a28df6d5001 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -90,6 +90,7 @@ module.exports = { 'token-list', 'server-side-render', 'shortcode', + 'warning', ].map( camelCaseDash ) ), new CopyWebpackPlugin( gutenbergPackages.map( ( packageName ) => ( {