Add reactProdInvariant and corresponding babel rewrite pass (#6948)
This commit is contained in:
parent
ccd26ee020
commit
1abce1630c
1
.babelrc
1
.babelrc
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
@ -53,6 +54,7 @@ var errorCodeOpts = {
|
|||
|
||||
var babelOpts = {
|
||||
plugins: [
|
||||
devExpressionWithCodes, // this pass has to run before `rewrite-modules`
|
||||
[babelPluginModules, {map: moduleMap}],
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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 = '';
|
||||
});
|
||||
});
|
|
@ -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
|
||||
),
|
||||
])
|
||||
));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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(
|
||||
{},
|
||||
|
@ -68,7 +69,7 @@ module.exports = {
|
|||
pathToBabel,
|
||||
pathToBabelrc,
|
||||
pathToModuleMap,
|
||||
pathToBabelPluginDev,
|
||||
pathToBabelPluginDevWithCode,
|
||||
pathToBabelPluginModules,
|
||||
]),
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('ReactDOMProduction', function() {
|
||||
|
@ -87,4 +86,20 @@ describe('ReactDOMProduction', function() {
|
|||
expect(container.childNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw with an error code in production', function() {
|
||||
expect(function() {
|
||||
var Component = React.createClass({
|
||||
render: function() {
|
||||
return ['this is wrong'];
|
||||
},
|
||||
});
|
||||
var container = document.createElement('div');
|
||||
ReactDOM.render(<Component />, container);
|
||||
}).toThrowError(
|
||||
'Minified React error #109; visit ' +
|
||||
'http://facebook.github.io/react/docs/error-decoder.html?invariant=109&args[]=Component' +
|
||||
' for the full message or use the non-minified dev environment' +
|
||||
' for full errors and additional helpful warnings.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Copyright 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var reactProdInvariant;
|
||||
|
||||
describe('reactProdInvariant', function() {
|
||||
beforeEach(function() {
|
||||
jest.resetModuleRegistry();
|
||||
reactProdInvariant = require('reactProdInvariant');
|
||||
});
|
||||
|
||||
it('should throw with the correct number of `%s`s in the URL', function() {
|
||||
expect(function() {
|
||||
reactProdInvariant(124, 'foo', 'bar');
|
||||
}).toThrowError(
|
||||
'Minified React error #124; visit ' +
|
||||
'http://facebook.github.io/react/docs/error-decoder.html?invariant=124&args[]=foo&args[]=bar' +
|
||||
' for the full message or use the non-minified dev environment' +
|
||||
' for full errors and additional helpful warnings.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
reactProdInvariant(20);
|
||||
}).toThrowError(
|
||||
'Minified React error #20; visit ' +
|
||||
'http://facebook.github.io/react/docs/error-decoder.html?invariant=20' +
|
||||
' for the full message or use the non-minified dev environment' +
|
||||
' for full errors and additional helpful warnings.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
reactProdInvariant(77, '<div>', '&?bar');
|
||||
}).toThrowError(
|
||||
'Minified React error #77; visit ' +
|
||||
'http://facebook.github.io/react/docs/error-decoder.html?invariant=77&args[]=%3Cdiv%3E&args[]=%26%3Fbar' +
|
||||
' for the full message or use the non-minified dev environment' +
|
||||
' for full errors and additional helpful warnings.'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @providesModule reactProdInvariant
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* WARNING: DO NOT manually require this module.
|
||||
* This is a replacement for `invariant(...)` used by the error code system
|
||||
* and will _only_ be required by the corresponding babel pass.
|
||||
* It always throws.
|
||||
*/
|
||||
function reactProdInvariant(code) {
|
||||
var argCount = arguments.length - 1;
|
||||
|
||||
var message = (
|
||||
'Minified React error #' + code + '; visit ' +
|
||||
'http://facebook.github.io/react/docs/error-decoder.html?invariant=' + code
|
||||
);
|
||||
|
||||
for (var argIdx = 0; argIdx < argCount; argIdx++) {
|
||||
message += '&args[]=' + encodeURIComponent(arguments[argIdx + 1]);
|
||||
}
|
||||
|
||||
message += (
|
||||
' for the full message or use the non-minified dev environment' +
|
||||
' for full errors and additional helpful warnings.'
|
||||
);
|
||||
|
||||
var error = new Error(message);
|
||||
error.name = 'Invariant Violation';
|
||||
error.framesToPop = 1; // we don't care about reactProdInvariant's own frame
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
module.exports = reactProdInvariant;
|
Loading…
Reference in New Issue