-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add reactProdInvariant and corresponding babel rewrite pass #6948
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
c8588ba
added reactProdInvariant and the corresponding babel pass
60aa80a
added stack info to the query params
ef1f5d6
fixed actual module names
cfcd0a1
WIP added a prod test (failing)
81bf266
error.stack null check
6bfaa6d
fixed failing reactProdInvariant stack-related tests
a6d4858
moved pass from .babelrc to gulpfile and jest/preprocessor
889416b
make invariant hygienic again
84365a3
changed URL format and removed stack info
d855ba3
improved message format and babel code
f69268f
changed url to /error-decoder.html
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh btw,
reactProdInvariant
is hard-coded now. Do you think I should make it an option to the babel plugin?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@spicyj
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably okay like this.