@samitouri / QOS-React / commits / 1abce1630c

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;