Add reactProdInvariant and corresponding babel rewrite pass (#6948)
Keyan Zhang committed
Jun 7, 2016 at 17:11 UTC
1abce1630c60b501ff187187d0bf2b5eb0d95dd7
8 files changed
+448
-4
.babelrc
-1
@@ -2,7 +2,6 @@
2
"presets": ["react"],
3
"ignore": ["third_party"],
4
"plugins": [
5
- "fbjs-scripts/babel-6/dev-expression",
5
"syntax-trailing-function-commas",
6
"babel-plugin-transform-object-rest-spread",
7
"transform-es2015-template-literals",
gulpfile.js
+2
@@ -16,6 +16,7 @@ var del = require('del');
16
17
var babelPluginModules = require('fbjs-scripts/babel-6/rewrite-modules');
18
var extractErrors = require('./scripts/error-codes/gulp-extract-errors');
19
+var devExpressionWithCodes = require('./scripts/error-codes/dev-expression-with-codes');
20
21
var paths = {
22
react: {
@@ -53,6 +54,7 @@ var errorCodeOpts = {
54
55
var babelOpts = {
56
plugins: [
57
+ devExpressionWithCodes, // this pass has to run before `rewrite-modules`
58
[babelPluginModules, {map: moduleMap}],
59
],
60
};
scripts/error-codes/__tests__/dev-expression-with-codes-test.js
new
+145
@@ -0,0 +1,145 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ */
9
+/* eslint-disable quotes */
10
+'use strict';
11
+
12
+let babel = require('babel-core');
13
+let devExpressionWithCodes = require('../dev-expression-with-codes');
14
+
15
+function transform(input) {
16
+ return babel.transform(input, {
17
+ plugins: [devExpressionWithCodes],
18
+ }).code;
19
+}
20
+
21
+function compare(input, output) {
22
+ var compiled = transform(input);
23
+ expect(compiled).toEqual(output);
24
+}
25
+
26
+var oldEnv;
27
+
28
+describe('dev-expression', function() {
29
+ beforeEach(() => {
30
+ oldEnv = process.env.NODE_ENV;
31
+ process.env.NODE_ENV = '';
32
+ });
33
+
34
+ afterEach(() => {
35
+ process.env.NODE_ENV = oldEnv;
36
+ });
37
+
38
+ it('should replace __DEV__ in if', () => {
39
+ compare(
40
+`
41
+if (__DEV__) {
42
+ console.log('foo')
43
+}`,
44
+`
45
+if (process.env.NODE_ENV !== 'production') {
46
+ console.log('foo');
47
+}`
48
+ );
49
+ });
50
+
51
+ it('should replace warning calls', () => {
52
+ compare(
53
+ "warning(condition, 'a %s b', 'c');",
54
+ "process.env.NODE_ENV !== 'production' ? warning(condition, 'a %s b', 'c') : void 0;"
55
+ );
56
+ });
57
+
58
+ it("should add `reactProdInvariant` when it finds `require('invariant')`", () => {
59
+ compare(
60
+"var invariant = require('invariant');",
61
+
62
+`var _prodInvariant = require('reactProdInvariant');
63
+
64
+var invariant = require('invariant');`
65
+ );
66
+ });
67
+
68
+ it('should replace simple invariant calls', () => {
69
+ compare(
70
+ "invariant(condition, 'Do not override existing functions.');",
71
+ "var _prodInvariant = require('reactProdInvariant');\n\n" +
72
+ "!condition ? " +
73
+ "process.env.NODE_ENV !== 'production' ? " +
74
+ "invariant(false, 'Do not override existing functions.') : " +
75
+ `_prodInvariant('16') : void 0;`
76
+ );
77
+ });
78
+
79
+ it("should only add `reactProdInvariant` once", () => {
80
+ var expectedInvariantTransformResult = (
81
+ "!condition ? " +
82
+ "process.env.NODE_ENV !== 'production' ? " +
83
+ "invariant(false, 'Do not override existing functions.') : " +
84
+ `_prodInvariant('16') : void 0;`
85
+ );
86
+
87
+ compare(
88
+`var invariant = require('invariant');
89
+invariant(condition, 'Do not override existing functions.');
90
+invariant(condition, 'Do not override existing functions.');`,
91
+
92
+`var _prodInvariant = require('reactProdInvariant');
93
+
94
+var invariant = require('invariant');
95
+${expectedInvariantTransformResult}
96
+${expectedInvariantTransformResult}`
97
+ );
98
+ });
99
+
100
+ it('should support invariant calls with args', () => {
101
+ compare(
102
+ "invariant(condition, 'Expected %s target to be an array; got %s', 'foo', 'bar');",
103
+ "var _prodInvariant = require('reactProdInvariant');\n\n" +
104
+ "!condition ? " +
105
+ "process.env.NODE_ENV !== 'production' ? " +
106
+ "invariant(false, 'Expected %s target to be an array; got %s', 'foo', 'bar') : " +
107
+ `_prodInvariant('7', 'foo', 'bar') : void 0;`
108
+ );
109
+ });
110
+
111
+ it('should support invariant calls with a concatenated template string and args', () => {
112
+ compare(
113
+ "invariant(condition, 'Expected a component class, ' + 'got %s.' + '%s', 'Foo', 'Bar');",
114
+ "var _prodInvariant = require('reactProdInvariant');\n\n" +
115
+ "!condition ? " +
116
+ "process.env.NODE_ENV !== 'production' ? " +
117
+ "invariant(false, 'Expected a component class, got %s.%s', 'Foo', 'Bar') : " +
118
+ `_prodInvariant('18', 'Foo', 'Bar') : void 0;`
119
+ );
120
+ });
121
+
122
+ it('should warn in non-test envs if the error message cannot be found', () => {
123
+ spyOn(console, 'warn');
124
+ transform("invariant(condition, 'a %s b', 'c');");
125
+
126
+ expect(console.warn.calls.count()).toBe(1);
127
+ expect(console.warn.calls.argsFor(0)[0]).toBe(
128
+ 'Error message "a %s b" ' +
129
+ 'cannot be found. The current React version ' +
130
+ 'and the error map are probably out of sync. ' +
131
+ 'Please run `gulp react:extract-errors` before building React.'
132
+ );
133
+ });
134
+
135
+ it('should not warn in test env if the error message cannot be found', () => {
136
+ process.env.NODE_ENV = 'test';
137
+
138
+ spyOn(console, 'warn');
139
+ transform("invariant(condition, 'a %s b', 'c');");
140
+
141
+ expect(console.warn.calls.count()).toBe(0);
142
+
143
+ process.env.NODE_ENV = '';
144
+ });
145
+});
scripts/error-codes/dev-expression-with-codes.js
new
+190
@@ -0,0 +1,190 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ */
9
+'use strict';
10
+
11
+var evalToString = require('./evalToString');
12
+var existingErrorMap = require('./codes.json');
13
+var invertObject = require('./invertObject');
14
+
15
+var errorMap = invertObject(existingErrorMap);
16
+
17
+module.exports = function(babel) {
18
+ var t = babel.types;
19
+
20
+ var SEEN_SYMBOL = Symbol('dev-expression-with-codes.seen');
21
+
22
+ // Generate a hygienic identifier
23
+ function getProdInvariantIdentifier(path, localState) {
24
+ if (!localState.prodInvariantIdentifier) {
25
+ localState.prodInvariantIdentifier = path.scope.generateUidIdentifier('prodInvariant');
26
+ path.scope.getProgramParent().push({
27
+ id: localState.prodInvariantIdentifier,
28
+ init: t.callExpression(
29
+ t.identifier('require'),
30
+ [t.stringLiteral('reactProdInvariant')]
31
+ ),
32
+ });
33
+ }
34
+ return localState.prodInvariantIdentifier;
35
+ }
36
+
37
+ var DEV_EXPRESSION = t.binaryExpression(
38
+ '!==',
39
+ t.memberExpression(
40
+ t.memberExpression(
41
+ t.identifier('process'),
42
+ t.identifier('env'),
43
+ false
44
+ ),
45
+ t.identifier('NODE_ENV'),
46
+ false
47
+ ),
48
+ t.stringLiteral('production')
49
+ );
50
+
51
+ return {
52
+ pre: function() {
53
+ this.prodInvariantIdentifier = null;
54
+ },
55
+
56
+ visitor: {
57
+ Identifier: {
58
+ enter: function(path) {
59
+ // Do nothing when testing
60
+ if (process.env.NODE_ENV === 'test') {
61
+ return;
62
+ }
63
+ // Replace __DEV__ with process.env.NODE_ENV !== 'production'
64
+ if (path.isIdentifier({name: '__DEV__'})) {
65
+ path.replaceWith(DEV_EXPRESSION);
66
+ }
67
+ },
68
+ },
69
+ CallExpression: {
70
+ exit: function(path) {
71
+ var node = path.node;
72
+ // Ignore if it's already been processed
73
+ if (node[SEEN_SYMBOL]) {
74
+ return;
75
+ }
76
+ // Insert `var PROD_INVARIANT = require('reactProdInvariant');`
77
+ // before all `require('invariant')`s.
78
+ // NOTE it doesn't support ES6 imports yet.
79
+ if (
80
+ path.get('callee').isIdentifier({name: 'require'}) &&
81
+ path.get('arguments')[0] &&
82
+ path.get('arguments')[0].isStringLiteral({value: 'invariant'})
83
+ ) {
84
+ node[SEEN_SYMBOL] = true;
85
+ getProdInvariantIdentifier(path, this);
86
+ } else if (path.get('callee').isIdentifier({name: 'invariant'})) {
87
+ // Turns this code:
88
+ //
89
+ // invariant(condition, argument, 'foo', 'bar');
90
+ //
91
+ // into this:
92
+ //
93
+ // if (!condition) {
94
+ // if ("production" !== process.env.NODE_ENV) {
95
+ // invariant(false, argument, 'foo', 'bar');
96
+ // } else {
97
+ // PROD_INVARIANT('XYZ', 'foo', 'bar');
98
+ // }
99
+ // }
100
+ //
101
+ // where
102
+ // - `XYZ` is an error code: a unique identifier (a number string)
103
+ // that references a verbose error message.
104
+ // The mapping is stored in `scripts/error-codes/codes.json`.
105
+ // - `PROD_INVARIANT` is the `reactProdInvariant` function that always throws with an error URL like
106
+ // http://facebook.github.io/react/docs/error-decoder.html?invariant=XYZ&args[]=foo&args[]=bar
107
+ //
108
+ // Specifically this does 3 things:
109
+ // 1. Checks the condition first, preventing an extra function call.
110
+ // 2. Adds an environment check so that verbose error messages aren't
111
+ // shipped to production.
112
+ // 3. Rewrites the call to `invariant` in production to `reactProdInvariant`
113
+ // - `reactProdInvariant` is always renamed to avoid shadowing
114
+ // The generated code is longer than the original code but will dead
115
+ // code removal in a minifier will strip that out.
116
+ var condition = node.arguments[0];
117
+ var errorMsgLiteral = evalToString(node.arguments[1]);
118
+
119
+ var prodErrorId = errorMap[errorMsgLiteral];
120
+ if (prodErrorId === undefined) {
121
+ // The error cannot be found in the map.
122
+ node[SEEN_SYMBOL] = true;
123
+ if (process.env.NODE_ENV !== 'test') {
124
+ console.warn(
125
+ 'Error message "' + errorMsgLiteral +
126
+ '" cannot be found. The current React version ' +
127
+ 'and the error map are probably out of sync. ' +
128
+ 'Please run `gulp react:extract-errors` before building React.'
129
+ );
130
+ }
131
+ return;
132
+ }
133
+
134
+ var devInvariant = t.callExpression(node.callee, [
135
+ t.booleanLiteral(false),
136
+ t.stringLiteral(errorMsgLiteral),
137
+ ].concat(node.arguments.slice(2)));
138
+
139
+ devInvariant[SEEN_SYMBOL] = true;
140
+
141
+ var localInvariantId = getProdInvariantIdentifier(path, this);
142
+ var prodInvariant = t.callExpression(localInvariantId, [
143
+ t.stringLiteral(prodErrorId),
144
+ ].concat(node.arguments.slice(2)));
145
+
146
+ prodInvariant[SEEN_SYMBOL] = true;
147
+ path.replaceWith(t.ifStatement(
148
+ t.unaryExpression('!', condition),
149
+ t.blockStatement([
150
+ t.ifStatement(
151
+ DEV_EXPRESSION,
152
+ t.blockStatement([
153
+ t.expressionStatement(devInvariant),
154
+ ]),
155
+ t.blockStatement([
156
+ t.expressionStatement(prodInvariant),
157
+ ])
158
+ ),
159
+ ])
160
+ ));
161
+ } else if (path.get('callee').isIdentifier({name: 'warning'})) {
162
+ // Turns this code:
163
+ //
164
+ // warning(condition, argument, argument);
165
+ //
166
+ // into this:
167
+ //
168
+ // if ("production" !== process.env.NODE_ENV) {
169
+ // warning(condition, argument, argument);
170
+ // }
171
+ //
172
+ // The goal is to strip out warning calls entirely in production. We
173
+ // don't need the same optimizations for conditions that we use for
174
+ // invariant because we don't care about an extra call in __DEV__
175
+
176
+ node[SEEN_SYMBOL] = true;
177
+ path.replaceWith(t.ifStatement(
178
+ DEV_EXPRESSION,
179
+ t.blockStatement([
180
+ t.expressionStatement(
181
+ node
182
+ ),
183
+ ])
184
+ ));
185
+ }
186
+ },
187
+ },
188
+ },
189
+ };
190
+};
scripts/jest/preprocessor.js
+3
-2
@@ -20,13 +20,14 @@ var createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction')
20
// Use require.resolve to be resilient to file moves, npm updates, etc
21
var pathToBabel = path.join(require.resolve('babel-core'), '..', 'package.json');
22
var pathToModuleMap = require.resolve('fbjs/module-map');
23
-var pathToBabelPluginDev = require.resolve('fbjs-scripts/babel-6/dev-expression');
23
+var pathToBabelPluginDevWithCode = require.resolve('../error-codes/dev-expression-with-codes');
24
var pathToBabelPluginModules = require.resolve('fbjs-scripts/babel-6/rewrite-modules');
25
var pathToBabelrc = path.join(__dirname, '..', '..', '.babelrc');
26
27
// TODO: make sure this stays in sync with gulpfile
28
var babelOptions = {
29
plugins: [
30
+ pathToBabelPluginDevWithCode, // this pass has to run before `rewrite-modules`
31
[babelPluginModules, {
32
map: Object.assign(
33
{},
@@ -68,7 +69,7 @@ module.exports = {
69
pathToBabel,
70
pathToBabelrc,
71
pathToModuleMap,
71
- pathToBabelPluginDev,
72
+ pathToBabelPluginDevWithCode,
73
pathToBabelPluginModules,
74
]),
75
};
src/renderers/dom/__tests__/ReactDOMProduction-test.js
+16
-1
@@ -8,7 +8,6 @@
8
*
9
* @emails react-core
10
*/
11
-
11
'use strict';
12
13
describe('ReactDOMProduction', function() {
@@ -87,4 +86,20 @@ describe('ReactDOMProduction', function() {
86
expect(container.childNodes.length).toBe(0);
87
});
88
89
+ it('should throw with an error code in production', function() {
90
+ expect(function() {
91
+ var Component = React.createClass({
92
+ render: function() {
93
+ return ['this is wrong'];
94
+ },
95
+ });
96
+ var container = document.createElement('div');
97
+ ReactDOM.render(<Component />, container);
98
+ }).toThrowError(
99
+ 'Minified React error #109; visit ' +
100
+ 'http://facebook.github.io/react/docs/error-decoder.html?invariant=109&args[]=Component' +
101
+ ' for the full message or use the non-minified dev environment' +
102
+ ' for full errors and additional helpful warnings.'
103
+ );
104
+ });
105
});
src/shared/utils/__tests__/reactProdInvariant-test.js
new
+49
@@ -0,0 +1,49 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+'use strict';
12
+
13
+var reactProdInvariant;
14
+
15
+describe('reactProdInvariant', function() {
16
+ beforeEach(function() {
17
+ jest.resetModuleRegistry();
18
+ reactProdInvariant = require('reactProdInvariant');
19
+ });
20
+
21
+ it('should throw with the correct number of `%s`s in the URL', function() {
22
+ expect(function() {
23
+ reactProdInvariant(124, 'foo', 'bar');
24
+ }).toThrowError(
25
+ 'Minified React error #124; visit ' +
26
+ 'http://facebook.github.io/react/docs/error-decoder.html?invariant=124&args[]=foo&args[]=bar' +
27
+ ' for the full message or use the non-minified dev environment' +
28
+ ' for full errors and additional helpful warnings.'
29
+ );
30
+
31
+ expect(function() {
32
+ reactProdInvariant(20);
33
+ }).toThrowError(
34
+ 'Minified React error #20; visit ' +
35
+ 'http://facebook.github.io/react/docs/error-decoder.html?invariant=20' +
36
+ ' for the full message or use the non-minified dev environment' +
37
+ ' for full errors and additional helpful warnings.'
38
+ );
39
+
40
+ expect(function() {
41
+ reactProdInvariant(77, '<div>', '&?bar');
42
+ }).toThrowError(
43
+ 'Minified React error #77; visit ' +
44
+ 'http://facebook.github.io/react/docs/error-decoder.html?invariant=77&args[]=%3Cdiv%3E&args[]=%26%3Fbar' +
45
+ ' for the full message or use the non-minified dev environment' +
46
+ ' for full errors and additional helpful warnings.'
47
+ );
48
+ });
49
+});
src/shared/utils/reactProdInvariant.js
new
+43
@@ -0,0 +1,43 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule reactProdInvariant
10
+ */
11
+'use strict';
12
+
13
+/**
14
+ * WARNING: DO NOT manually require this module.
15
+ * This is a replacement for `invariant(...)` used by the error code system
16
+ * and will _only_ be required by the corresponding babel pass.
17
+ * It always throws.
18
+ */
19
+function reactProdInvariant(code) {
20
+ var argCount = arguments.length - 1;
21
+
22
+ var message = (
23
+ 'Minified React error #' + code + '; visit ' +
24
+ 'http://facebook.github.io/react/docs/error-decoder.html?invariant=' + code
25
+ );
26
+
27
+ for (var argIdx = 0; argIdx < argCount; argIdx++) {
28
+ message += '&args[]=' + encodeURIComponent(arguments[argIdx + 1]);
29
+ }
30
+
31
+ message += (
32
+ ' for the full message or use the non-minified dev environment' +
33
+ ' for full errors and additional helpful warnings.'
34
+ );
35
+
36
+ var error = new Error(message);
37
+ error.name = 'Invariant Violation';
38
+ error.framesToPop = 1; // we don't care about reactProdInvariant's own frame
39
+
40
+ throw error;
41
+}
42
+
43
+module.exports = reactProdInvariant;