Skip to content

Commit 42c3c96

Browse files
authored
Compile invariant directly to throw expressions (#15071)
* Transform invariant to custom error type This transforms calls to the invariant module: ```js invariant(condition, 'A %s message that contains %s', adj, noun); ``` Into throw statements: ```js if (!condition) { if (__DEV__) { throw ReactError(`A ${adj} message that contains ${noun}`); } else { throw ReactErrorProd(ERR_CODE, adj, noun); } } ``` The only thing ReactError does is return an error whose name is set to "Invariant Violation" to match the existing behavior. ReactErrorProd is a special version used in production that throws a minified error code, with a link to see to expanded form. This replaces the reactProdInvariant module. As a next step, I would like to replace our use of the invariant module for user facing errors by transforming normal Error constructors to ReactError and ReactErrorProd. (We can continue using invariant for internal React errors that are meant to be unreachable, which was the original purpose of invariant.) * Use numbers instead of strings for error codes * Use arguments instead of an array I wasn't sure about this part so I asked Sebastian, and his rationale was that using arguments will make ReactErrorProd slightly slower, but using an array will likely make all the functions that throw slightly slower to compile, so it's hard to say which way is better. But since ReactErrorProd is in an error path, and fewer bytes is generally better, no array is good. * Casing nit
1 parent df7b87d commit 42c3c96

17 files changed

+462
-360
lines changed

packages/shared/ReactError.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
// Do not require this module directly! Use a normal error constructor with
10+
// template literal strings. The messages will be converted to ReactError during
11+
// build, and in production they will be minified.
12+
13+
function ReactError(message) {
14+
const error = new Error(message);
15+
error.name = 'Invariant Violation';
16+
return error;
17+
}
18+
19+
export default ReactError;

packages/shared/ReactErrorProd.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
// Do not require this module directly! Use a normal error constructor with
10+
// template literal strings. The messages will be converted to ReactError during
11+
// build, and in production they will be minified.
12+
13+
function ReactErrorProd(code) {
14+
let url = 'https://reactjs.org/docs/error-decoder.html?invariant=' + code;
15+
for (let i = 1; i < arguments.length; i++) {
16+
url += '&args[]=' + encodeURIComponent(arguments[i]);
17+
}
18+
return new Error(
19+
`Minified React error #${code}; visit ${url} for the full message or ` +
20+
'use the non-minified dev environment for full errors and additional ' +
21+
'helpful warnings. ',
22+
);
23+
}
24+
25+
export default ReactErrorProd;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
'use strict';
10+
11+
let React;
12+
let ReactDOM;
13+
14+
describe('ReactError', () => {
15+
let globalErrorMock;
16+
17+
beforeEach(() => {
18+
if (!__DEV__) {
19+
// In production, our Jest environment overrides the global Error
20+
// class in order to decode error messages automatically. However
21+
// this is a single test where we actually *don't* want to decode
22+
// them. So we assert that the OriginalError exists, and temporarily
23+
// set the global Error object back to it.
24+
globalErrorMock = global.Error;
25+
global.Error = globalErrorMock.OriginalError;
26+
expect(typeof global.Error).toBe('function');
27+
}
28+
jest.resetModules();
29+
React = require('react');
30+
ReactDOM = require('react-dom');
31+
});
32+
33+
afterEach(() => {
34+
if (!__DEV__) {
35+
global.Error = globalErrorMock;
36+
}
37+
});
38+
39+
if (__DEV__) {
40+
it('should throw errors whose name is "Invariant Violation"', () => {
41+
let error;
42+
try {
43+
React.useState();
44+
} catch (e) {
45+
error = e;
46+
}
47+
expect(error.name).toEqual('Invariant Violation');
48+
});
49+
} else {
50+
it('should error with minified error code', () => {
51+
expect(() => ReactDOM.render('Hi', null)).toThrowError(
52+
'Minified React error #200; visit ' +
53+
'https://reactjs.org/docs/error-decoder.html?invariant=200' +
54+
' for the full message or use the non-minified dev environment' +
55+
' for full errors and additional helpful warnings.',
56+
);
57+
});
58+
it('should serialize arguments', () => {
59+
function Oops() {
60+
return;
61+
}
62+
Oops.displayName = '#wtf';
63+
const container = document.createElement('div');
64+
expect(() => ReactDOM.render(<Oops />, container)).toThrowError(
65+
'Minified React error #152; visit ' +
66+
'https://reactjs.org/docs/error-decoder.html?invariant=152&args[]=%23wtf' +
67+
' for the full message or use the non-minified dev environment' +
68+
' for full errors and additional helpful warnings.',
69+
);
70+
});
71+
}
72+
});

packages/shared/__tests__/reactProdInvariant-test.internal.js renamed to packages/shared/__tests__/ReactErrorProd-test.internal.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
*/
99
'use strict';
1010

11-
let reactProdInvariant;
11+
let ReactErrorProd;
1212

13-
describe('reactProdInvariant', () => {
13+
describe('ReactErrorProd', () => {
1414
let globalErrorMock;
1515

1616
beforeEach(() => {
@@ -25,7 +25,7 @@ describe('reactProdInvariant', () => {
2525
expect(typeof global.Error).toBe('function');
2626
}
2727
jest.resetModules();
28-
reactProdInvariant = require('shared/reactProdInvariant').default;
28+
ReactErrorProd = require('shared/ReactErrorProd').default;
2929
});
3030

3131
afterEach(() => {
@@ -36,7 +36,7 @@ describe('reactProdInvariant', () => {
3636

3737
it('should throw with the correct number of `%s`s in the URL', () => {
3838
expect(function() {
39-
reactProdInvariant(124, 'foo', 'bar');
39+
throw ReactErrorProd(124, 'foo', 'bar');
4040
}).toThrowError(
4141
'Minified React error #124; visit ' +
4242
'https://reactjs.org/docs/error-decoder.html?invariant=124&args[]=foo&args[]=bar' +
@@ -45,7 +45,7 @@ describe('reactProdInvariant', () => {
4545
);
4646

4747
expect(function() {
48-
reactProdInvariant(20);
48+
throw ReactErrorProd(20);
4949
}).toThrowError(
5050
'Minified React error #20; visit ' +
5151
'https://reactjs.org/docs/error-decoder.html?invariant=20' +
@@ -54,7 +54,7 @@ describe('reactProdInvariant', () => {
5454
);
5555

5656
expect(function() {
57-
reactProdInvariant(77, '<div>', '&?bar');
57+
throw ReactErrorProd(77, '<div>', '&?bar');
5858
}).toThrowError(
5959
'Minified React error #77; visit ' +
6060
'https://reactjs.org/docs/error-decoder.html?invariant=77&args[]=%3Cdiv%3E&args[]=%26%3Fbar' +

packages/shared/forks/invariant.www.js

-8
This file was deleted.

packages/shared/invariant.js

+4-33
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,9 @@
1717
* will remain to ensure logic does not differ in production.
1818
*/
1919

20-
let validateFormat = () => {};
21-
22-
if (__DEV__) {
23-
validateFormat = function(format) {
24-
if (format === undefined) {
25-
throw new Error('invariant requires an error message argument');
26-
}
27-
};
28-
}
29-
3020
export default function invariant(condition, format, a, b, c, d, e, f) {
31-
validateFormat(format);
32-
33-
if (!condition) {
34-
let error;
35-
if (format === undefined) {
36-
error = new Error(
37-
'Minified exception occurred; use the non-minified dev environment ' +
38-
'for the full error message and additional helpful warnings.',
39-
);
40-
} else {
41-
const args = [a, b, c, d, e, f];
42-
let argIndex = 0;
43-
error = new Error(
44-
format.replace(/%s/g, function() {
45-
return args[argIndex++];
46-
}),
47-
);
48-
error.name = 'Invariant Violation';
49-
}
50-
51-
error.framesToPop = 1; // we don't care about invariant's own frame
52-
throw error;
53-
}
21+
throw new Error(
22+
'Internal React error: invariant() is meant to be replaced at compile ' +
23+
'time. There is no runtime version.',
24+
);
5425
}

packages/shared/reactProdInvariant.js

-43
This file was deleted.

scripts/error-codes/README.md

+16-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
The error code system substitutes React's invariant error messages with error IDs to provide a better debugging support in production. Check out the blog post [here](https://reactjs.org/blog/2016/07/11/introducing-reacts-error-code-system.html).
1+
The error code system substitutes React's error messages with error IDs to
2+
provide a better debugging support in production. Check out the blog post
3+
[here](https://reactjs.org/blog/2016/07/11/introducing-reacts-error-code-system.html).
24

3-
## Note for cutting a new React release
4-
1. For each release, we run `yarn build -- --extract-errors` to update the error codes before calling `yarn build`. The build step uses `codes.json` for a production (minified) build; there should be no warning like `Error message "foo" cannot be found` for a successful release.
5-
2. The updated `codes.json` file should be synced back to the master branch. The error decoder page in our documentation site uses `codes.json` from master; if the json file has been updated, the docs site should also be rebuilt (`rake copy_error_codes` is included in the default `rake release` task).
6-
3. Be certain to run `yarn build -- --extract-errors` directly in the release branch (if not master) to ensure the correct error codes are generated. These error messages might be changed/removed before cutting a new release, and we don't want to add intermediate/temporary error messages to `codes.json`. However, if a PR changes an existing error message and there's a specific production test (which is rare), it's ok to update `codes.json` for that. Please use `yarn build -- --extract-errors` and don't edit the file manually.
7-
8-
## Structure
9-
The error code system consists of 5 parts:
10-
- [`codes.json`](https://github.com/facebook/react/blob/master/scripts/error-codes/codes.json) contains the mapping from IDs to error messages. This file is generated by the Gulp plugin and is used by both the Babel plugin and the error decoder page in our documentation. This file is append-only, which means an existing code in the file will never be changed/removed.
11-
- [`extract-errors.js`](https://github.com/facebook/react/blob/master/scripts/error-codes/extract-errors.js) is an node script that traverses our codebase and updates `codes.json`. Use it by calling `yarn build -- --extract-errors`.
12-
- [`replace-invariant-error-codes.js`](https://github.com/facebook/react/blob/master/scripts/error-codes/replace-invariant-error-codes.js) is a Babel pass that rewrites error messages to IDs for a production (minified) build.
13-
- [`reactProdInvariant.js`](https://github.com/facebook/react/blob/master/src/shared/utils/reactProdInvariant.js) is the replacement for `invariant` in production. This file gets imported by the Babel plugin and should _not_ be used manually.
14-
- [`ErrorDecoderComponent`](https://github.com/facebook/react/blob/master/docs/_js/ErrorDecoderComponent.js) is a React component that lives at https://reactjs.org/docs/error-decoder.html. This page takes parameters like `?invariant=109&args[]=Foo` and displays a corresponding error message. Our documentation site's [`Rakefile`](https://github.com/facebook/react/blob/master/docs/Rakefile#L64-L69) has a task (`bundle exec rake copy_error_codes`) for adding the latest `codes.json` to the error decoder page. This task is included in the default `bundle exec rake release` task.
5+
- [`codes.json`](https://github.com/facebook/react/blob/master/scripts/error-codes/codes.json)
6+
contains the mapping from IDs to error messages. This file is generated by the
7+
Gulp plugin and is used by both the Babel plugin and the error decoder page in
8+
our documentation. This file is append-only, which means an existing code in
9+
the file will never be changed/removed.
10+
- [`extract-errors.js`](https://github.com/facebook/react/blob/master/scripts/error-codes/extract-errors.js)
11+
is an node script that traverses our codebase and updates `codes.json`. You
12+
can test it by running `yarn build -- --extract-errors`, but you should only
13+
commit changes to this file when running a release. (The release tool will
14+
perform this step automatically.)
15+
- [`minify-error-codes`](https://github.com/facebook/react/blob/master/scripts/error-codes/minify-error-codes)
16+
is a Babel pass that rewrites error messages to IDs for a production
17+
(minified) build.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`error transform should correctly transform invariants that are not in the error codes map 1`] = `
4+
"import _ReactError from 'shared/ReactError';
5+
6+
import invariant from 'shared/invariant';
7+
(function () {
8+
if (!condition) {
9+
throw _ReactError(\`This is not a real error message.\`);
10+
}
11+
})();"
12+
`;
13+
14+
exports[`error transform should handle escaped characters 1`] = `
15+
"import _ReactError from 'shared/ReactError';
16+
17+
import invariant from 'shared/invariant';
18+
(function () {
19+
if (!condition) {
20+
throw _ReactError(\`What's up?\`);
21+
}
22+
})();"
23+
`;
24+
25+
exports[`error transform should only add \`ReactError\` and \`ReactErrorProd\` once each 1`] = `
26+
"import _ReactErrorProd from 'shared/ReactErrorProd';
27+
import _ReactError from 'shared/ReactError';
28+
29+
import invariant from 'shared/invariant';
30+
(function () {
31+
if (!condition) {
32+
if (__DEV__) {
33+
throw _ReactError(\`Do not override existing functions.\`);
34+
} else {
35+
throw _ReactErrorProd(16);
36+
}
37+
}
38+
})();
39+
(function () {
40+
if (!condition) {
41+
if (__DEV__) {
42+
throw _ReactError(\`Do not override existing functions.\`);
43+
} else {
44+
throw _ReactErrorProd(16);
45+
}
46+
}
47+
})();"
48+
`;
49+
50+
exports[`error transform should replace simple invariant calls 1`] = `
51+
"import _ReactErrorProd from 'shared/ReactErrorProd';
52+
import _ReactError from 'shared/ReactError';
53+
54+
import invariant from 'shared/invariant';
55+
(function () {
56+
if (!condition) {
57+
if (__DEV__) {
58+
throw _ReactError(\`Do not override existing functions.\`);
59+
} else {
60+
throw _ReactErrorProd(16);
61+
}
62+
}
63+
})();"
64+
`;
65+
66+
exports[`error transform should support invariant calls with a concatenated template string and args 1`] = `
67+
"import _ReactErrorProd from 'shared/ReactErrorProd';
68+
import _ReactError from 'shared/ReactError';
69+
70+
import invariant from 'shared/invariant';
71+
(function () {
72+
if (!condition) {
73+
if (__DEV__) {
74+
throw _ReactError(\`Expected a component class, got \${Foo}.\${Bar}\`);
75+
} else {
76+
throw _ReactErrorProd(18, Foo, Bar);
77+
}
78+
}
79+
})();"
80+
`;
81+
82+
exports[`error transform should support invariant calls with args 1`] = `
83+
"import _ReactErrorProd from 'shared/ReactErrorProd';
84+
import _ReactError from 'shared/ReactError';
85+
86+
import invariant from 'shared/invariant';
87+
(function () {
88+
if (!condition) {
89+
if (__DEV__) {
90+
throw _ReactError(\`Expected \${foo} target to be an array; got \${bar}\`);
91+
} else {
92+
throw _ReactErrorProd(7, foo, bar);
93+
}
94+
}
95+
})();"
96+
`;

0 commit comments

Comments
 (0)