forked from facebook/react
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add reactProdInvariant and corresponding babel rewrite pass (facebook…
- Loading branch information
Showing
8 changed files
with
448 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
145 changes: 145 additions & 0 deletions
145
scripts/error-codes/__tests__/dev-expression-with-codes-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = ''; | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
), | ||
]) | ||
)); | ||
} | ||
}, | ||
}, | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.