FB bundles wrap warning() calls in __DEV__ (#10314)

FB bundles wrap warning() calls in __DEV__

Split dev-mode transforms into separate parts:
1) umd+cjs+fb: Wrap warning calls with process.env checks
2) umd+cjs: Replace error messages with minified codes

Also updated transforms to use __DEV__ since it transforms to smaller code after stripEnvVariables is run.

Also renamed 'scripts/error-codes/dev-expression-with-codes.js' -> 'scripts/error-codes/replace-invariant-error-codes.js'
This commit is contained in:
Brian Vaughn 2017-08-02 14:14:26 -07:00 committed by GitHub
parent 8890db707b
commit f3e502c613
9 changed files with 130 additions and 75 deletions

View File

@ -9,7 +9,7 @@ Prior to this release, we stripped out error messages at build-time and this is
> Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.
In order to make debugging in production easier, we're introducing an Error Code System in [15.2.0](https://github.com/facebook/react/releases/tag/v15.2.0). We developed a [gulp script](https://github.com/facebook/react/blob/master/scripts/error-codes/gulp-extract-errors.js) that collects all of our `invariant` error messages and folds them to a [JSON file](https://github.com/facebook/react/blob/master/scripts/error-codes/codes.json), and at build-time Babel uses the JSON to [rewrite](https://github.com/facebook/react/blob/master/scripts/error-codes/dev-expression-with-codes.js) our `invariant` calls in production to reference the corresponding error IDs. Now when things go wrong in production, the error that React throws will contain a URL with an error ID and relevant information. The URL will point you to a page in our documentation where the original error message gets reassembled.
In order to make debugging in production easier, we're introducing an Error Code System in [15.2.0](https://github.com/facebook/react/releases/tag/v15.2.0). We developed a [gulp script](https://github.com/facebook/react/blob/master/scripts/error-codes/gulp-extract-errors.js) that collects all of our `invariant` error messages and folds them to a [JSON file](https://github.com/facebook/react/blob/master/scripts/error-codes/codes.json), and at build-time Babel uses the JSON to [rewrite](https://github.com/facebook/react/blob/master/scripts/error-codes/replace-invariant-error-codes.js) our `invariant` calls in production to reference the corresponding error IDs. Now when things go wrong in production, the error that React throws will contain a URL with an error ID and relevant information. The URL will point you to a page in our documentation where the original error message gets reassembled.
While we hope you don't see errors often, you can see how it works [here](/react/docs/error-decoder.html?invariant=109&args[]=Foo). This is what the same error from above will look like:

View File

@ -9,6 +9,6 @@ The error code system substitutes React's invariant error messages with error ID
The error code system consists of 5 parts:
- [`codes.json`](https://github.com/facebook/react/blob/master/scripts/error-codes/codes.json) contains the mapping from IDs to error messages. This file is generated by the Gulp plugin and is used by both the Babel plugin and the error decoder page in our documentation. This file is append-only, which means an existing code in the file will never be changed/removed.
- [`extract-errors.js`](https://github.com/facebook/react/blob/master/scripts/error-codes/extract-errors.js) is an node script that traverses our codebase and updates `codes.json`. Use it by calling `yarn build -- --extract-errors`.
- [`dev-expression-with-codes.js`](https://github.com/facebook/react/blob/master/scripts/error-codes/dev-expression-with-codes.js) is a Babel pass that rewrites error messages to IDs for a production (minified) build.
- [`replace-invariant-error-codes.js`](https://github.com/facebook/react/blob/master/scripts/error-codes/replace-invariant-error-codes.js) is a Babel pass that rewrites error messages to IDs for a production (minified) build.
- [`reactProdInvariant.js`](https://github.com/facebook/react/blob/master/src/shared/utils/reactProdInvariant.js) is the replacement for `invariant` in production. This file gets imported by the Babel plugin and should _not_ be used manually.
- [`ErrorDecoderComponent`](https://github.com/facebook/react/blob/master/docs/_js/ErrorDecoderComponent.js) is a React component that lives at https://facebook.github.io/react/docs/error-decoder.html. This page takes parameters like `?invariant=109&args[]=Foo` and displays a corresponding error message. Our documentation site's [`Rakefile`](https://github.com/facebook/react/blob/master/docs/Rakefile#L64-L69) has a task (`bundle exec rake copy_error_codes`) for adding the latest `codes.json` to the error decoder page. This task is included in the default `bundle exec rake release` task.

View File

@ -10,7 +10,7 @@
'use strict';
let babel = require('babel-core');
let devExpressionWithCodes = require('../dev-expression-with-codes');
let devExpressionWithCodes = require('../replace-invariant-error-codes');
function transform(input) {
return babel.transform(input, {
@ -35,26 +35,6 @@ describe('dev-expression', () => {
process.env.NODE_ENV = oldEnv;
});
it('should replace __DEV__ in if', () => {
compare(
`
if (__DEV__) {
console.log('foo')
}`,
`
if (process.env.NODE_ENV !== 'production') {
console.log('foo');
}`
);
});
it('should replace warning calls', () => {
compare(
"warning(condition, 'a %s b', 'c');",
"process.env.NODE_ENV !== 'production' ? warning(condition, 'a %s b', 'c') : void 0;"
);
});
it("should add `reactProdInvariant` when it finds `require('invariant')`", () => {
compare(
"var invariant = require('invariant');",
@ -69,7 +49,7 @@ var invariant = require('invariant');`
"invariant(condition, 'Do not override existing functions.');",
"var _prodInvariant = require('reactProdInvariant');\n\n" +
'!condition ? ' +
"process.env.NODE_ENV !== 'production' ? " +
'__DEV__ ? ' +
"invariant(false, 'Do not override existing functions.') : " +
`_prodInvariant('16') : void 0;`
);
@ -78,7 +58,7 @@ var invariant = require('invariant');`
it('should only add `reactProdInvariant` once', () => {
var expectedInvariantTransformResult =
'!condition ? ' +
"process.env.NODE_ENV !== 'production' ? " +
'__DEV__ ? ' +
"invariant(false, 'Do not override existing functions.') : " +
`_prodInvariant('16') : void 0;`;
@ -99,7 +79,7 @@ ${expectedInvariantTransformResult}`
"invariant(condition, 'Expected %s target to be an array; got %s', 'foo', 'bar');",
"var _prodInvariant = require('reactProdInvariant');\n\n" +
'!condition ? ' +
"process.env.NODE_ENV !== 'production' ? " +
'__DEV__ ? ' +
"invariant(false, 'Expected %s target to be an array; got %s', 'foo', 'bar') : " +
`_prodInvariant('7', 'foo', 'bar') : void 0;`
);
@ -110,7 +90,7 @@ ${expectedInvariantTransformResult}`
"invariant(condition, 'Expected a component class, ' + 'got %s.' + '%s', 'Foo', 'Bar');",
"var _prodInvariant = require('reactProdInvariant');\n\n" +
'!condition ? ' +
"process.env.NODE_ENV !== 'production' ? " +
'__DEV__ ? ' +
"invariant(false, 'Expected a component class, got %s.%s', 'Foo', 'Bar') : " +
`_prodInvariant('18', 'Foo', 'Bar') : void 0;`
);

View File

@ -17,7 +17,7 @@ var errorMap = invertObject(existingErrorMap);
module.exports = function(babel) {
var t = babel.types;
var SEEN_SYMBOL = Symbol('dev-expression-with-codes.seen');
var SEEN_SYMBOL = Symbol('replace-invariant-error-codes.seen');
// Generate a hygienic identifier
function getProdInvariantIdentifier(path, localState) {
@ -35,15 +35,7 @@ module.exports = function(babel) {
return localState.prodInvariantIdentifier;
}
var DEV_EXPRESSION = t.binaryExpression(
'!==',
t.memberExpression(
t.memberExpression(t.identifier('process'), t.identifier('env'), false),
t.identifier('NODE_ENV'),
false
),
t.stringLiteral('production')
);
var DEV_EXPRESSION = t.identifier('__DEV__');
return {
pre: function() {
@ -51,18 +43,6 @@ module.exports = function(babel) {
},
visitor: {
Identifier: {
enter: function(path) {
// Do nothing when testing
if (process.env.NODE_ENV === 'test') {
return;
}
// Replace __DEV__ with process.env.NODE_ENV !== 'production'
if (path.isIdentifier({name: '__DEV__'})) {
path.replaceWith(DEV_EXPRESSION);
}
},
},
CallExpression: {
exit: function(path) {
var node = path.node;
@ -149,28 +129,6 @@ module.exports = function(babel) {
])
)
);
} else if (path.get('callee').isIdentifier({name: 'warning'})) {
// Turns this code:
//
// warning(condition, argument, argument);
//
// into this:
//
// if ("production" !== process.env.NODE_ENV) {
// warning(condition, argument, argument);
// }
//
// The goal is to strip out warning calls entirely in production. We
// don't need the same optimizations for conditions that we use for
// invariant because we don't care about an extra call in __DEV__
node[SEEN_SYMBOL] = true;
path.replaceWith(
t.ifStatement(
DEV_EXPRESSION,
t.blockStatement([t.expressionStatement(node)])
)
);
}
},
},

View File

@ -20,7 +20,7 @@ eslint-rules/__tests__/warning-and-invariant-args-test.js
* warning(true, 'error!');
* warning(true, '%s %s, %s %s: %s (%s)', 1, 2, 3, 4, 5, 6);
scripts/error-codes/__tests__/dev-expression-with-codes-test.js
scripts/error-codes/__tests__/replace-invariant-error-codes-test.js
* should replace __DEV__ in if
* should replace warning calls
* should add `reactProdInvariant` when it finds `require('invariant')`

View File

@ -25,7 +25,7 @@ var pathToBabel = path.join(
);
var pathToModuleMap = require.resolve('fbjs/module-map');
var pathToBabelPluginDevWithCode = require.resolve(
'../error-codes/dev-expression-with-codes'
'../error-codes/replace-invariant-error-codes'
);
var pathToBabelPluginModules = require.resolve(
'fbjs-scripts/babel-6/rewrite-modules'

View File

@ -118,6 +118,14 @@ function getFooter(bundleType) {
function updateBabelConfig(babelOpts, bundleType) {
switch (bundleType) {
case FB_DEV:
case FB_PROD:
return Object.assign({}, babelOpts, {
plugins: babelOpts.plugins.concat([
// Wrap warning() calls in a __DEV__ check so they are stripped from production.
require('./plugins/wrap-warning-with-env-check'),
]),
});
case UMD_DEV:
case UMD_PROD:
case NODE_DEV:
@ -126,8 +134,12 @@ function updateBabelConfig(babelOpts, bundleType) {
plugins: babelOpts.plugins.concat([
// Use object-assign polyfill in open source
resolve('./scripts/babel/transform-object-assign-require'),
// Replace __DEV__ with process.env.NODE_ENV and minify invariant messages
require('../error-codes/dev-expression-with-codes'),
// Minify invariant messages
require('../error-codes/replace-invariant-error-codes'),
// Wrap warning() calls in a __DEV__ check so they are stripped from production.
require('./plugins/wrap-warning-with-env-check'),
]),
});
default:

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* eslint-disable quotes */
'use strict';
let babel = require('babel-core');
let wrapWarningWithEnvCheck = require('../wrap-warning-with-env-check');
function transform(input) {
return babel.transform(input, {
plugins: [wrapWarningWithEnvCheck],
}).code;
}
function compare(input, output) {
var compiled = transform(input);
expect(compiled).toEqual(output);
}
var oldEnv;
describe('wrap-warning-with-env-check', () => {
beforeEach(() => {
oldEnv = process.env.NODE_ENV;
process.env.NODE_ENV = '';
});
afterEach(() => {
process.env.NODE_ENV = oldEnv;
});
it('should wrap warning calls', () => {
compare(
"warning(condition, 'a %s b', 'c');",
"__DEV__ ? warning(condition, 'a %s b', 'c') : void 0;"
);
});
it('should not wrap invariant calls', () => {
compare(
"invariant(condition, 'a %s b', 'c');",
"invariant(condition, 'a %s b', 'c');"
);
});
});

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
module.exports = function(babel, options) {
var t = babel.types;
var DEV_EXPRESSION = t.identifier('__DEV__');
var SEEN_SYMBOL = Symbol('expression.seen');
return {
visitor: {
CallExpression: {
exit: function(path) {
var node = path.node;
// Ignore if it's already been processed
if (node[SEEN_SYMBOL]) {
return;
}
if (path.get('callee').isIdentifier({name: 'warning'})) {
node[SEEN_SYMBOL] = true;
// Turns this code:
//
// warning(condition, argument, argument);
//
// into this:
//
// if (__DEV__) {
// warning(condition, argument, argument);
// }
//
// The goal is to strip out warning calls entirely in production.
path.replaceWith(
t.ifStatement(
DEV_EXPRESSION,
t.blockStatement([t.expressionStatement(node)])
)
);
}
},
},
},
};
};