Skip to content

Commit

Permalink
FB bundles wrap warning() calls in __DEV__ (#10314)
Browse files Browse the repository at this point in the history
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'
  • Loading branch information
bvaughn authored Aug 2, 2017
1 parent 8890db7 commit f3e502c
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion scripts/error-codes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
30 changes: 5 additions & 25 deletions scripts/error-codes/__tests__/dev-expression-with-codes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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');",
Expand All @@ -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;`
);
Expand All @@ -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;`;

Expand All @@ -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;`
);
Expand All @@ -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;`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -35,34 +35,14 @@ 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() {
this.prodInvariantIdentifier = null;
},

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;
Expand Down Expand Up @@ -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)])
)
);
}
},
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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')`
Expand Down
2 changes: 1 addition & 1 deletion scripts/jest/preprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
16 changes: 14 additions & 2 deletions scripts/rollup/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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');"
);
});
});
54 changes: 54 additions & 0 deletions scripts/rollup/plugins/wrap-warning-with-env-check.js
Original file line number Diff line number Diff line change
@@ -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)])
)
);
}
},
},
},
};
};

0 comments on commit f3e502c

Please sign in to comment.