Skip to content

Commit

Permalink
Add reactProdInvariant and corresponding babel rewrite pass (facebook…
Browse files Browse the repository at this point in the history
  • Loading branch information
keyz committed Jun 8, 2016
1 parent ccd26ee commit 1abce16
Show file tree
Hide file tree
Showing 8 changed files with 448 additions and 4 deletions.
1 change: 0 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"presets": ["react"],
"ignore": ["third_party"],
"plugins": [
"fbjs-scripts/babel-6/dev-expression",
"syntax-trailing-function-commas",
"babel-plugin-transform-object-rest-spread",
"transform-es2015-template-literals",
Expand Down
2 changes: 2 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var del = require('del');

var babelPluginModules = require('fbjs-scripts/babel-6/rewrite-modules');
var extractErrors = require('./scripts/error-codes/gulp-extract-errors');
var devExpressionWithCodes = require('./scripts/error-codes/dev-expression-with-codes');

var paths = {
react: {
Expand Down Expand Up @@ -53,6 +54,7 @@ var errorCodeOpts = {

var babelOpts = {
plugins: [
devExpressionWithCodes, // this pass has to run before `rewrite-modules`
[babelPluginModules, {map: moduleMap}],
],
};
Expand Down
145 changes: 145 additions & 0 deletions scripts/error-codes/__tests__/dev-expression-with-codes-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* 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 devExpressionWithCodes = require('../dev-expression-with-codes');

function transform(input) {
return babel.transform(input, {
plugins: [devExpressionWithCodes],
}).code;
}

function compare(input, output) {
var compiled = transform(input);
expect(compiled).toEqual(output);
}

var oldEnv;

describe('dev-expression', function() {
beforeEach(() => {
oldEnv = process.env.NODE_ENV;
process.env.NODE_ENV = '';
});

afterEach(() => {
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');",

`var _prodInvariant = require('reactProdInvariant');
var invariant = require('invariant');`
);
});

it('should replace simple invariant calls', () => {
compare(
"invariant(condition, 'Do not override existing functions.');",
"var _prodInvariant = require('reactProdInvariant');\n\n" +
"!condition ? " +
"process.env.NODE_ENV !== 'production' ? " +
"invariant(false, 'Do not override existing functions.') : " +
`_prodInvariant('16') : void 0;`
);
});

it("should only add `reactProdInvariant` once", () => {
var expectedInvariantTransformResult = (
"!condition ? " +
"process.env.NODE_ENV !== 'production' ? " +
"invariant(false, 'Do not override existing functions.') : " +
`_prodInvariant('16') : void 0;`
);

compare(
`var invariant = require('invariant');
invariant(condition, 'Do not override existing functions.');
invariant(condition, 'Do not override existing functions.');`,

`var _prodInvariant = require('reactProdInvariant');
var invariant = require('invariant');
${expectedInvariantTransformResult}
${expectedInvariantTransformResult}`
);
});

it('should support invariant calls with args', () => {
compare(
"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' ? " +
"invariant(false, 'Expected %s target to be an array; got %s', 'foo', 'bar') : " +
`_prodInvariant('7', 'foo', 'bar') : void 0;`
);
});

it('should support invariant calls with a concatenated template string and args', () => {
compare(
"invariant(condition, 'Expected a component class, ' + 'got %s.' + '%s', 'Foo', 'Bar');",
"var _prodInvariant = require('reactProdInvariant');\n\n" +
"!condition ? " +
"process.env.NODE_ENV !== 'production' ? " +
"invariant(false, 'Expected a component class, got %s.%s', 'Foo', 'Bar') : " +
`_prodInvariant('18', 'Foo', 'Bar') : void 0;`
);
});

it('should warn in non-test envs if the error message cannot be found', () => {
spyOn(console, 'warn');
transform("invariant(condition, 'a %s b', 'c');");

expect(console.warn.calls.count()).toBe(1);
expect(console.warn.calls.argsFor(0)[0]).toBe(
'Error message "a %s b" ' +
'cannot be found. The current React version ' +
'and the error map are probably out of sync. ' +
'Please run `gulp react:extract-errors` before building React.'
);
});

it('should not warn in test env if the error message cannot be found', () => {
process.env.NODE_ENV = 'test';

spyOn(console, 'warn');
transform("invariant(condition, 'a %s b', 'c');");

expect(console.warn.calls.count()).toBe(0);

process.env.NODE_ENV = '';
});
});
190 changes: 190 additions & 0 deletions scripts/error-codes/dev-expression-with-codes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* 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';

var evalToString = require('./evalToString');
var existingErrorMap = require('./codes.json');
var invertObject = require('./invertObject');

var errorMap = invertObject(existingErrorMap);

module.exports = function(babel) {
var t = babel.types;

var SEEN_SYMBOL = Symbol('dev-expression-with-codes.seen');

// Generate a hygienic identifier
function getProdInvariantIdentifier(path, localState) {
if (!localState.prodInvariantIdentifier) {
localState.prodInvariantIdentifier = path.scope.generateUidIdentifier('prodInvariant');
path.scope.getProgramParent().push({
id: localState.prodInvariantIdentifier,
init: t.callExpression(
t.identifier('require'),
[t.stringLiteral('reactProdInvariant')]
),
});
}
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')
);

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;
// Ignore if it's already been processed
if (node[SEEN_SYMBOL]) {
return;
}
// Insert `var PROD_INVARIANT = require('reactProdInvariant');`
// before all `require('invariant')`s.
// NOTE it doesn't support ES6 imports yet.
if (
path.get('callee').isIdentifier({name: 'require'}) &&
path.get('arguments')[0] &&
path.get('arguments')[0].isStringLiteral({value: 'invariant'})
) {
node[SEEN_SYMBOL] = true;
getProdInvariantIdentifier(path, this);
} else if (path.get('callee').isIdentifier({name: 'invariant'})) {
// Turns this code:
//
// invariant(condition, argument, 'foo', 'bar');
//
// into this:
//
// if (!condition) {
// if ("production" !== process.env.NODE_ENV) {
// invariant(false, argument, 'foo', 'bar');
// } else {
// PROD_INVARIANT('XYZ', 'foo', 'bar');
// }
// }
//
// where
// - `XYZ` is an error code: a unique identifier (a number string)
// that references a verbose error message.
// The mapping is stored in `scripts/error-codes/codes.json`.
// - `PROD_INVARIANT` is the `reactProdInvariant` function that always throws with an error URL like
// http://facebook.github.io/react/docs/error-decoder.html?invariant=XYZ&args[]=foo&args[]=bar
//
// Specifically this does 3 things:
// 1. Checks the condition first, preventing an extra function call.
// 2. Adds an environment check so that verbose error messages aren't
// shipped to production.
// 3. Rewrites the call to `invariant` in production to `reactProdInvariant`
// - `reactProdInvariant` is always renamed to avoid shadowing
// The generated code is longer than the original code but will dead
// code removal in a minifier will strip that out.
var condition = node.arguments[0];
var errorMsgLiteral = evalToString(node.arguments[1]);

var prodErrorId = errorMap[errorMsgLiteral];
if (prodErrorId === undefined) {
// The error cannot be found in the map.
node[SEEN_SYMBOL] = true;
if (process.env.NODE_ENV !== 'test') {
console.warn(
'Error message "' + errorMsgLiteral +
'" cannot be found. The current React version ' +
'and the error map are probably out of sync. ' +
'Please run `gulp react:extract-errors` before building React.'
);
}
return;
}

var devInvariant = t.callExpression(node.callee, [
t.booleanLiteral(false),
t.stringLiteral(errorMsgLiteral),
].concat(node.arguments.slice(2)));

devInvariant[SEEN_SYMBOL] = true;

var localInvariantId = getProdInvariantIdentifier(path, this);
var prodInvariant = t.callExpression(localInvariantId, [
t.stringLiteral(prodErrorId),
].concat(node.arguments.slice(2)));

prodInvariant[SEEN_SYMBOL] = true;
path.replaceWith(t.ifStatement(
t.unaryExpression('!', condition),
t.blockStatement([
t.ifStatement(
DEV_EXPRESSION,
t.blockStatement([
t.expressionStatement(devInvariant),
]),
t.blockStatement([
t.expressionStatement(prodInvariant),
])
),
])
));
} 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
),
])
));
}
},
},
},
};
};
5 changes: 3 additions & 2 deletions scripts/jest/preprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ var createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction')
// Use require.resolve to be resilient to file moves, npm updates, etc
var pathToBabel = path.join(require.resolve('babel-core'), '..', 'package.json');
var pathToModuleMap = require.resolve('fbjs/module-map');
var pathToBabelPluginDev = require.resolve('fbjs-scripts/babel-6/dev-expression');
var pathToBabelPluginDevWithCode = require.resolve('../error-codes/dev-expression-with-codes');
var pathToBabelPluginModules = require.resolve('fbjs-scripts/babel-6/rewrite-modules');
var pathToBabelrc = path.join(__dirname, '..', '..', '.babelrc');

// TODO: make sure this stays in sync with gulpfile
var babelOptions = {
plugins: [
pathToBabelPluginDevWithCode, // this pass has to run before `rewrite-modules`
[babelPluginModules, {
map: Object.assign(
{},
Expand Down Expand Up @@ -68,7 +69,7 @@ module.exports = {
pathToBabel,
pathToBabelrc,
pathToModuleMap,
pathToBabelPluginDev,
pathToBabelPluginDevWithCode,
pathToBabelPluginModules,
]),
};
Loading

0 comments on commit 1abce16

Please sign in to comment.