Skip to content

Commit 7034408

Browse files
authored
Follow-up improvements to error code extraction infra (#22516)
* Output FIXME during build for unminified errors The invariant Babel transform used to output a FIXME comment if it could not find a matching error code. This could happen if there were a configuration mistake that caused an unminified message to slip through. Linting the compiled bundles is the most reliable way to do it because there's not a one-to-one mapping between source modules and bundles. For example, the same source module may appear in multiple bundles, some which are minified and others which aren't. This updates the transform to output the same messages for Error calls. The source lint rule is still useful for catching mistakes during development, to prompt you to update the error codes map before pushing the PR to CI. * Don't run error transform in development We used to run the error transform in both production and development, because in development it was used to convert `invariant` calls into throw statements. Now that don't use `invariant` anymore, we only have to run the transform for production builds. * Add ! to FIXME comment so Closure doesn't strip it Don't love this solution because Closure could change this heuristic, or we could switch to a differnt compiler that doesn't support it. But it works. Could add a bundle that contains an unminified error solely for the purpose of testing it, but that seems like overkill. * Alternate extract-errors that scrapes artifacts The build script outputs a special FIXME comment when it fails to minify an error message. CI will detect these comments and fail the workflow. The comments also include the expected error message. So I added an alternate extract-errors that scrapes unminified messages from the build artifacts and updates `codes.json`. This is nice because it works on partial builds. And you can also run it after the fact, instead of needing build all over again. * Disable error minification in more bundles Not worth it because the number of errors does not outweight the size of the formatProdErrorMessage runtime. * Run extract-errors script in CI The lint_build job already checks for unminified errors, but the output isn't super helpful. Instead I've added a new job that runs the extract-errors script and fails the build if `codes.json` changes. It also outputs the expected diff so you can easily see which messages were missing from the map. * Replace old extract-errors script with new one Deletes the old extract-errors in favor of extract-errors2
1 parent 9c8161b commit 7034408

File tree

11 files changed

+179
-439
lines changed

11 files changed

+179
-439
lines changed

.circleci/config.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,20 @@ jobs:
209209
- run: yarn workspaces info | head -n -1 > workspace_info.txt
210210
- *restore_node_modules
211211
- run: yarn lint-build
212-
- run: scripts/circleci/check_minified_errors.sh
212+
213+
check_error_codes:
214+
docker: *docker
215+
environment: *environment
216+
steps:
217+
- checkout
218+
- attach_workspace: *attach_workspace
219+
- run: yarn workspaces info | head -n -1 > workspace_info.txt
220+
- *restore_node_modules
221+
- run:
222+
name: Search build artifacts for unminified errors
223+
command: |
224+
yarn extract-errors
225+
git diff || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
213226
214227
yarn_test:
215228
docker: *docker
@@ -414,6 +427,9 @@ workflows:
414427
- yarn_lint_build:
415428
requires:
416429
- yarn_build_combined
430+
- check_error_codes:
431+
requires:
432+
- yarn_build_combined
417433
- RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
418434
requires:
419435
- yarn_build_combined

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"linc": "node ./scripts/tasks/linc.js",
115115
"lint": "node ./scripts/tasks/eslint.js",
116116
"lint-build": "node ./scripts/rollup/validate/index.js",
117-
"extract-errors": "yarn build --type=dev --extract-errors",
117+
"extract-errors": "node scripts/error-codes/extract-errors.js",
118118
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
119119
"debug-test": "yarn test --deprecated 'yarn test --debug'",
120120
"test": "node ./scripts/jest/jest-cli.js",

packages/shared/__tests__/ReactError-test.internal.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('ReactError', () => {
3737
});
3838

3939
// @gate build === "production"
40+
// @gate !source
4041
it('should error with minified error code', () => {
4142
expect(() => ReactDOM.render('Hi', null)).toThrowError(
4243
'Minified React error #200; visit ' +

scripts/error-codes/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ provide a better debugging support in production. Check out the blog post
99
the file will never be changed/removed.
1010
- [`extract-errors.js`](https://github.com/facebook/react/blob/main/scripts/error-codes/extract-errors.js)
1111
is an node script that traverses our codebase and updates `codes.json`. You
12-
can test it by running `yarn extract-errors`.
12+
can test it by running `yarn extract-errors`. It works by crawling the build
13+
artifacts directory, so you need to have either run the build script or
14+
downloaded pre-built artifacts (e.g. with `yarn download build`). It works
15+
with partial builds, too.
1316
- [`transform-error-messages`](https://github.com/facebook/react/blob/main/scripts/error-codes/transform-error-messages.js)
1417
is a Babel pass that rewrites error messages to IDs for a production
1518
(minified) build.

scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap

Lines changed: 16 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,94 +2,47 @@
22

33
exports[`error transform handles escaped backticks in template string 1`] = `
44
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
5-
Error(__DEV__ ? \\"Expected \`\\" + listener + \\"\` listener to be a function, instead got a value of \`\\" + type + \\"\` type.\\" : _formatProdErrorMessage(231, listener, type));"
5+
Error(_formatProdErrorMessage(231, listener, type));"
66
`;
77

8-
exports[`error transform should correctly transform invariants that are not in the error codes map 1`] = `
9-
"import invariant from 'shared/invariant';
10-
11-
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/
12-
if (!condition) {
13-
throw Error(\\"This is not a real error message.\\");
14-
}"
8+
exports[`error transform should not touch other calls or new expressions 1`] = `
9+
"new NotAnError();
10+
NotAnError();"
1511
`;
1612

17-
exports[`error transform should handle escaped characters 1`] = `
18-
"import invariant from 'shared/invariant';
13+
exports[`error transform should output FIXME for errors that don't have a matching error code 1`] = `
14+
"/*! FIXME (minify-errors-in-prod): Unminified error message in production build!*/
1915
20-
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/
21-
if (!condition) {
22-
throw Error(\\"What's up?\\");
23-
}"
16+
/*! <expected-error-format>\\"This is not a real error message.\\"</expected-error-format>*/
17+
Error('This is not a real error message.');"
2418
`;
2519

26-
exports[`error transform should not touch other calls or new expressions 1`] = `
27-
"new NotAnError();
28-
NotAnError();"
20+
exports[`error transform should output FIXME for errors that don't have a matching error code, unless opted out with a comment 1`] = `
21+
"// eslint-disable-next-line react-internal/prod-error-codes
22+
Error('This is not a real error message.');"
2923
`;
3024

3125
exports[`error transform should replace error constructors (no new) 1`] = `
3226
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
33-
Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));"
27+
Error(_formatProdErrorMessage(16));"
3428
`;
3529

3630
exports[`error transform should replace error constructors 1`] = `
3731
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
38-
Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));"
39-
`;
40-
41-
exports[`error transform should replace simple invariant calls 1`] = `
42-
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
43-
import invariant from 'shared/invariant';
44-
45-
if (!condition) {
46-
{
47-
throw Error(__DEV__ ? \\"Do not override existing functions.\\" : _formatProdErrorMessage(16));
48-
}
49-
}"
32+
Error(_formatProdErrorMessage(16));"
5033
`;
5134

5235
exports[`error transform should support error constructors with concatenated messages 1`] = `
5336
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
54-
Error(__DEV__ ? \\"Expected \\" + foo + \\" target to \\" + (\\"be an array; got \\" + bar) : _formatProdErrorMessage(7, foo, bar));"
37+
Error(_formatProdErrorMessage(7, foo, bar));"
5538
`;
5639

5740
exports[`error transform should support interpolating arguments with concatenation 1`] = `
5841
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
59-
Error(__DEV__ ? 'Expected ' + foo + ' target to be an array; got ' + bar : _formatProdErrorMessage(7, foo, bar));"
42+
Error(_formatProdErrorMessage(7, foo, bar));"
6043
`;
6144

6245
exports[`error transform should support interpolating arguments with template strings 1`] = `
6346
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
64-
Error(__DEV__ ? \\"Expected \\" + foo + \\" target to be an array; got \\" + bar : _formatProdErrorMessage(7, foo, bar));"
65-
`;
66-
67-
exports[`error transform should support invariant calls with a concatenated template string and args 1`] = `
68-
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
69-
import invariant from 'shared/invariant';
70-
71-
if (!condition) {
72-
{
73-
throw Error(__DEV__ ? \\"Expected a component class, got \\" + Foo + \\".\\" + Bar : _formatProdErrorMessage(18, Foo, Bar));
74-
}
75-
}"
76-
`;
77-
78-
exports[`error transform should support invariant calls with args 1`] = `
79-
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
80-
import invariant from 'shared/invariant';
81-
82-
if (!condition) {
83-
{
84-
throw Error(__DEV__ ? \\"Expected \\" + foo + \\" target to be an array; got \\" + bar : _formatProdErrorMessage(7, foo, bar));
85-
}
86-
}"
87-
`;
88-
89-
exports[`error transform should support noMinify option 1`] = `
90-
"import invariant from 'shared/invariant';
91-
92-
if (!condition) {
93-
throw Error(\\"Do not override existing functions.\\");
94-
}"
47+
Error(_formatProdErrorMessage(7, foo, bar));"
9548
`;

scripts/error-codes/__tests__/transform-error-messages.js

Lines changed: 21 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -28,87 +28,46 @@ describe('error transform', () => {
2828
process.env.NODE_ENV = oldEnv;
2929
});
3030

31-
it('should replace simple invariant calls', () => {
32-
expect(
33-
transform(`
34-
import invariant from 'shared/invariant';
35-
invariant(condition, 'Do not override existing functions.');
36-
`)
37-
).toMatchSnapshot();
38-
});
39-
40-
it('should throw if invariant is not in an expression statement', () => {
41-
expect(() => {
42-
transform(`
43-
import invariant from 'shared/invariant';
44-
cond && invariant(condition, 'Do not override existing functions.');
45-
`);
46-
}).toThrow('invariant() cannot be called from expression context');
47-
});
48-
49-
it('should support invariant calls with args', () => {
50-
expect(
51-
transform(`
52-
import invariant from 'shared/invariant';
53-
invariant(condition, 'Expected %s target to be an array; got %s', foo, bar);
54-
`)
55-
).toMatchSnapshot();
56-
});
57-
58-
it('should support invariant calls with a concatenated template string and args', () => {
59-
expect(
60-
transform(`
61-
import invariant from 'shared/invariant';
62-
invariant(condition, 'Expected a component class, ' + 'got %s.' + '%s', Foo, Bar);
63-
`)
64-
).toMatchSnapshot();
65-
});
66-
67-
it('should correctly transform invariants that are not in the error codes map', () => {
31+
it('should replace error constructors', () => {
6832
expect(
6933
transform(`
70-
import invariant from 'shared/invariant';
71-
invariant(condition, 'This is not a real error message.');
34+
new Error('Do not override existing functions.');
7235
`)
7336
).toMatchSnapshot();
7437
});
7538

76-
it('should handle escaped characters', () => {
39+
it('should replace error constructors (no new)', () => {
7740
expect(
7841
transform(`
79-
import invariant from 'shared/invariant';
80-
invariant(condition, 'What\\'s up?');
42+
Error('Do not override existing functions.');
8143
`)
8244
).toMatchSnapshot();
8345
});
8446

85-
it('should support noMinify option', () => {
86-
expect(
87-
transform(
88-
`
89-
import invariant from 'shared/invariant';
90-
invariant(condition, 'Do not override existing functions.');
91-
`,
92-
{noMinify: true}
93-
)
94-
).toMatchSnapshot();
95-
});
96-
97-
it('should replace error constructors', () => {
47+
it("should output FIXME for errors that don't have a matching error code", () => {
9848
expect(
9949
transform(`
100-
new Error('Do not override existing functions.');
50+
Error('This is not a real error message.');
10151
`)
10252
).toMatchSnapshot();
10353
});
10454

105-
it('should replace error constructors (no new)', () => {
106-
expect(
107-
transform(`
108-
Error('Do not override existing functions.');
55+
it(
56+
"should output FIXME for errors that don't have a matching error " +
57+
'code, unless opted out with a comment',
58+
() => {
59+
// TODO: Since this only detects one of many ways to disable a lint
60+
// rule, we should instead search for a custom directive (like
61+
// no-minify-errors) instead of ESLint. Will need to update our lint
62+
// rule to recognize the same directive.
63+
expect(
64+
transform(`
65+
// eslint-disable-next-line react-internal/prod-error-codes
66+
Error('This is not a real error message.');
10967
`)
110-
).toMatchSnapshot();
111-
});
68+
).toMatchSnapshot();
69+
}
70+
);
11271

11372
it('should not touch other calls or new expressions', () => {
11473
expect(

0 commit comments

Comments
 (0)