@samitouri / QOS-React-2 / commits / 7034408ff7

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

Andrew Clark committed Oct 31, 2021 at 18:37 UTC 7034408ff762b52a39f3a3145a37f4526d0a95cf
11 files changed +179 -439
.circleci/config.yml
+17 -1
@@ -209,7 +209,20 @@ jobs:
209 - run: yarn workspaces info | head -n -1 > workspace_info.txt
210 - *restore_node_modules
211 - 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)
226
227 yarn_test:
228 docker: *docker
@@ -414,6 +427,9 @@ workflows:
427 - yarn_lint_build:
428 requires:
429 - yarn_build_combined
430 + - check_error_codes:
431 + requires:
432 + - yarn_build_combined
433 - RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
434 requires:
435 - yarn_build_combined
package.json
+1 -1
@@ -114,7 +114,7 @@
114 "linc": "node ./scripts/tasks/linc.js",
115 "lint": "node ./scripts/tasks/eslint.js",
116 "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",
118 "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
119 "debug-test": "yarn test --deprecated 'yarn test --debug'",
120 "test": "node ./scripts/jest/jest-cli.js",
packages/shared/__tests__/ReactError-test.internal.js
+1
@@ -37,6 +37,7 @@ describe('ReactError', () => {
37 });
38
39 // @gate build === "production"
40 + // @gate !source
41 it('should error with minified error code', () => {
42 expect(() => ReactDOM.render('Hi', null)).toThrowError(
43 'Minified React error #200; visit ' +
scripts/error-codes/README.md
+4 -1
@@ -9,7 +9,10 @@ provide a better debugging support in production. Check out the blog post
9 the file will never be changed/removed.
10 - [`extract-errors.js`](https://github.com/facebook/react/blob/main/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 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.
16 - [`transform-error-messages`](https://github.com/facebook/react/blob/main/scripts/error-codes/transform-error-messages.js)
17 is a Babel pass that rewrites error messages to IDs for a production
18 (minified) build.
scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap
+16 -63
@@ -2,94 +2,47 @@
2
3 exports[`error transform handles escaped backticks in template string 1`] = `
4 "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));"
6 `;
7
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();"
11 `;
12
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!*/
15
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.');"
18 `;
19
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.');"
23 `;
24
25 exports[`error transform should replace error constructors (no new) 1`] = `
26 "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
33 -Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));"
27 +Error(_formatProdErrorMessage(16));"
28 `;
29
30 exports[`error transform should replace error constructors 1`] = `
31 "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));"
33 `;
34
35 exports[`error transform should support error constructors with concatenated messages 1`] = `
36 "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));"
38 `;
39
40 exports[`error transform should support interpolating arguments with concatenation 1`] = `
41 "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));"
43 `;
44
45 exports[`error transform should support interpolating arguments with template strings 1`] = `
46 "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));"
48 `;
scripts/error-codes/__tests__/transform-error-messages.js
+21 -62
@@ -28,87 +28,46 @@ describe('error transform', () => {
28 process.env.NODE_ENV = oldEnv;
29 });
30
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', () => {
32 expect(
33 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.');
35 `)
36 ).toMatchSnapshot();
37 });
38
76 - it('should handle escaped characters', () => {
39 + it('should replace error constructors (no new)', () => {
40 expect(
41 transform(`
79 -import invariant from 'shared/invariant';
80 -invariant(condition, 'What\\'s up?');
42 +Error('Do not override existing functions.');
43 `)
44 ).toMatchSnapshot();
45 });
46
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", () => {
48 expect(
49 transform(`
100 -new Error('Do not override existing functions.');
50 +Error('This is not a real error message.');
51 `)
52 ).toMatchSnapshot();
53 });
54
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.');
67 `)
110 - ).toMatchSnapshot();
111 - });
68 + ).toMatchSnapshot();
69 + }
70 + );
71
72 it('should not touch other calls or new expressions', () => {
73 expect(
scripts/error-codes/extract-errors.js
+56 -87
@@ -1,105 +1,74 @@
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 - */
1 'use strict';
2
9 -const parser = require('@babel/parser');
3 const fs = require('fs');
4 const path = require('path');
12 -const traverse = require('@babel/traverse').default;
13 -const {evalStringConcat} = require('../shared/evalToString');
14 -const invertObject = require('./invertObject');
5 +const {execSync} = require('child_process');
6
16 -const babylonOptions = {
17 - sourceType: 'module',
18 - // As a parser, babylon has its own options and we can't directly
19 - // import/require a babel preset. It should be kept **the same** as
20 - // the `babel-plugin-syntax-*` ones specified in
21 - // https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js
22 - plugins: [
23 - 'classProperties',
24 - 'flow',
25 - 'jsx',
26 - 'trailingFunctionCommas',
27 - 'objectRestSpread',
28 - ],
29 -};
30 -
31 -module.exports = function(opts) {
32 - if (!opts || !('errorMapFilePath' in opts)) {
33 - throw new Error(
34 - 'Missing options. Ensure you pass an object with `errorMapFilePath`.'
35 - );
7 +async function main() {
8 + const originalJSON = JSON.parse(
9 + fs.readFileSync(path.resolve(__dirname, '../error-codes/codes.json'))
10 + );
11 + const existingMessages = new Set();
12 + const codes = Object.keys(originalJSON);
13 + let nextCode = 0;
14 + for (let i = 0; i < codes.length; i++) {
15 + const codeStr = codes[i];
16 + const message = originalJSON[codeStr];
17 + const code = parseInt(codeStr, 10);
18 + existingMessages.add(message);
19 + if (code >= nextCode) {
20 + nextCode = code + 1;
21 + }
22 }
23
38 - const errorMapFilePath = opts.errorMapFilePath;
39 - let existingErrorMap;
24 + console.log('Searching `build` directory for unminified errors...\n');
25 +
26 + let out;
27 try {
41 - // Using `fs.readFileSync` instead of `require` here, because `require()`
42 - // calls are cached, and the cache map is not properly invalidated after
43 - // file changes.
44 - existingErrorMap = JSON.parse(
45 - fs.readFileSync(
46 - path.join(__dirname, path.basename(errorMapFilePath)),
47 - 'utf8'
48 - )
49 - );
28 + out = execSync(
29 + "git --no-pager grep -n --untracked --no-exclude-standard '/*! <expected-error-format>' -- build"
30 + ).toString();
31 } catch (e) {
51 - existingErrorMap = {};
52 - }
53 -
54 - const allErrorIDs = Object.keys(existingErrorMap);
55 - let currentID;
56 -
57 - if (allErrorIDs.length === 0) {
58 - // Map is empty
59 - currentID = 0;
60 - } else {
61 - currentID = Math.max.apply(null, allErrorIDs) + 1;
32 + if (e.status === 1 && e.stdout.toString() === '') {
33 + // No unminified errors found.
34 + return;
35 + }
36 + throw e;
37 }
38
64 - // Here we invert the map object in memory for faster error code lookup
65 - existingErrorMap = invertObject(existingErrorMap);
66 -
67 - function transform(source) {
68 - const ast = parser.parse(source, babylonOptions);
69 -
70 - traverse(ast, {
71 - CallExpression: {
72 - exit(astPath) {
73 - if (astPath.get('callee').isIdentifier({name: 'invariant'})) {
74 - const node = astPath.node;
39 + let newJSON = null;
40 + const regex = /\<expected-error-format\>"(.+?)"\<\/expected-error-format\>/g;
41 + do {
42 + const match = regex.exec(out);
43 + if (match === null) {
44 + break;
45 + } else {
46 + const message = match[1].trim();
47 + if (existingMessages.has(message)) {
48 + // This probably means you ran the script twice.
49 + continue;
50 + }
51 + existingMessages.add(message);
52
76 - // error messages can be concatenated (`+`) at runtime, so here's a
77 - // trivial partial evaluator that interprets the literal value
78 - const errorMsgLiteral = evalStringConcat(node.arguments[1]);
79 - addToErrorMap(errorMsgLiteral);
80 - }
81 - },
82 - },
83 - });
84 - }
85 -
86 - function addToErrorMap(errorMsgLiteral) {
87 - if (existingErrorMap.hasOwnProperty(errorMsgLiteral)) {
88 - return;
53 + // Add to json map
54 + if (newJSON === null) {
55 + newJSON = Object.assign({}, originalJSON);
56 + }
57 + console.log(`"${nextCode}": "${message}"`);
58 + newJSON[nextCode] = message;
59 + nextCode += 1;
60 }
90 - existingErrorMap[errorMsgLiteral] = '' + currentID++;
91 - }
61 + } while (true);
62
93 - function flush(cb) {
63 + if (newJSON) {
64 fs.writeFileSync(
95 - errorMapFilePath,
96 - JSON.stringify(invertObject(existingErrorMap), null, 2) + '\n',
97 - 'utf-8'
65 + path.resolve(__dirname, '../error-codes/codes.json'),
66 + JSON.stringify(newJSON, null, 2)
67 );
68 }
69 +}
70
101 - return function extractErrors(source) {
102 - transform(source);
103 - flush();
104 - };
105 -};
71 +main().catch(error => {
72 + console.error(error);
73 + process.exit(1);
74 +});
scripts/error-codes/transform-error-messages.js
+49 -168
@@ -7,10 +7,7 @@
7 'use strict';
8
9 const fs = require('fs');
10 -const {
11 - evalStringConcat,
12 - evalStringAndTemplateConcat,
13 -} = require('../shared/evalToString');
10 +const {evalStringAndTemplateConcat} = require('../shared/evalToString');
11 const invertObject = require('./invertObject');
12 const helperModuleImports = require('@babel/helper-module-imports');
13
@@ -23,11 +20,7 @@ const SEEN_SYMBOL = Symbol('transform-error-messages.seen');
20 module.exports = function(babel) {
21 const t = babel.types;
22
26 - // TODO: Instead of outputting __DEV__ conditions, only apply this transform
27 - // in production.
28 - const DEV_EXPRESSION = t.identifier('__DEV__');
29 -
30 - function CallOrNewExpression(path, file) {
23 + function ErrorCallExpression(path, file) {
24 // Turns this code:
25 //
26 // new Error(`A ${adj} message that contains ${noun}`);
@@ -38,11 +31,7 @@ module.exports = function(babel) {
31 //
32 // into this:
33 //
41 - // Error(
42 - // __DEV__
43 - // ? `A ${adj} message that contains ${noun}`
44 - // : formatProdErrorMessage(ERR_CODE, adj, noun)
45 - // );
34 + // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
35 const node = path.node;
36 if (node[SEEN_SYMBOL]) {
37 return;
@@ -62,9 +51,44 @@ module.exports = function(babel) {
51
52 let prodErrorId = errorMap[errorMsgLiteral];
53 if (prodErrorId === undefined) {
65 - // There is no error code for this message. We use a lint rule to
66 - // enforce that messages can be minified, so assume this is
67 - // intentional and exit gracefully.
54 + // There is no error code for this message. Add an inline comment
55 + // that flags this as an unminified error. This allows the build
56 + // to proceed, while also allowing a post-build linter to detect it.
57 + //
58 + // Outputs:
59 + // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
60 + // /* <expected-error-format>"A % message that contains %"</expected-error-format> */
61 + // if (!condition) {
62 + // throw Error(`A ${adj} message that contains ${noun}`);
63 + // }
64 +
65 + const statementParent = path.getStatementParent();
66 + const leadingComments = statementParent.node.leadingComments;
67 + if (leadingComments !== undefined) {
68 + for (let i = 0; i < leadingComments.length; i++) {
69 + // TODO: Since this only detects one of many ways to disable a lint
70 + // rule, we should instead search for a custom directive (like
71 + // no-minify-errors) instead of ESLint. Will need to update our lint
72 + // rule to recognize the same directive.
73 + const commentText = leadingComments[i].value;
74 + if (
75 + commentText.includes(
76 + 'eslint-disable-next-line react-internal/prod-error-codes'
77 + )
78 + ) {
79 + return;
80 + }
81 + }
82 + }
83 +
84 + statementParent.addComment(
85 + 'leading',
86 + `! <expected-error-format>"${errorMsgLiteral}"</expected-error-format>`
87 + );
88 + statementParent.addComment(
89 + 'leading',
90 + '! FIXME (minify-errors-in-prod): Unminified error message in production build!'
91 + );
92 return;
93 }
94 prodErrorId = parseInt(prodErrorId, 10);
@@ -84,168 +108,25 @@ module.exports = function(babel) {
108 ]);
109
110 // Outputs:
87 - // Error(
88 - // __DEV__
89 - // ? `A ${adj} message that contains ${noun}`
90 - // : formatProdErrorMessage(ERR_CODE, adj, noun)
91 - // );
92 - path.replaceWith(t.callExpression(t.identifier('Error'), [prodMessage]));
93 - path.replaceWith(
94 - t.callExpression(t.identifier('Error'), [
95 - t.conditionalExpression(DEV_EXPRESSION, errorMsgNode, prodMessage),
96 - ])
97 - );
111 + // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
112 + const newErrorCall = t.callExpression(t.identifier('Error'), [prodMessage]);
113 + newErrorCall[SEEN_SYMBOL] = true;
114 + path.replaceWith(newErrorCall);
115 }
116
117 return {
118 visitor: {
119 NewExpression(path, file) {
103 - const noMinify = file.opts.noMinify;
104 - if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) {
105 - CallOrNewExpression(path, file);
120 + if (path.get('callee').isIdentifier({name: 'Error'})) {
121 + ErrorCallExpression(path, file);
122 }
123 },
124
125 CallExpression(path, file) {
110 - const node = path.node;
111 - const noMinify = file.opts.noMinify;
112 -
113 - if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) {
114 - CallOrNewExpression(path, file);
126 + if (path.get('callee').isIdentifier({name: 'Error'})) {
127 + ErrorCallExpression(path, file);
128 return;
129 }
117 -
118 - if (path.get('callee').isIdentifier({name: 'invariant'})) {
119 - // Turns this code:
120 - //
121 - // invariant(condition, 'A %s message that contains %s', adj, noun);
122 - //
123 - // into this:
124 - //
125 - // if (!condition) {
126 - // throw Error(
127 - // __DEV__
128 - // ? `A ${adj} message that contains ${noun}`
129 - // : formatProdErrorMessage(ERR_CODE, adj, noun)
130 - // );
131 - // }
132 - //
133 - // where ERR_CODE is an error code: a unique identifier (a number
134 - // string) that references a verbose error message. The mapping is
135 - // stored in `scripts/error-codes/codes.json`.
136 - const condition = node.arguments[0];
137 - const errorMsgLiteral = evalStringConcat(node.arguments[1]);
138 - const errorMsgExpressions = Array.from(node.arguments.slice(2));
139 - const errorMsgQuasis = errorMsgLiteral
140 - .split('%s')
141 - .map(raw => t.templateElement({raw, cooked: String.raw({raw})}));
142 -
143 - // Outputs:
144 - // `A ${adj} message that contains ${noun}`;
145 - const devMessage = t.templateLiteral(
146 - errorMsgQuasis,
147 - errorMsgExpressions
148 - );
149 -
150 - const parentStatementPath = path.parentPath;
151 - if (parentStatementPath.type !== 'ExpressionStatement') {
152 - throw path.buildCodeFrameError(
153 - 'invariant() cannot be called from expression context. Move ' +
154 - 'the call to its own statement.'
155 - );
156 - }
157 -
158 - if (noMinify) {
159 - // Error minification is disabled for this build.
160 - //
161 - // Outputs:
162 - // if (!condition) {
163 - // throw Error(`A ${adj} message that contains ${noun}`);
164 - // }
165 - parentStatementPath.replaceWith(
166 - t.ifStatement(
167 - t.unaryExpression('!', condition),
168 - t.blockStatement([
169 - t.throwStatement(
170 - t.callExpression(t.identifier('Error'), [devMessage])
171 - ),
172 - ])
173 - )
174 - );
175 - return;
176 - }
177 -
178 - let prodErrorId = errorMap[errorMsgLiteral];
179 -
180 - if (prodErrorId === undefined) {
181 - // There is no error code for this message. Add an inline comment
182 - // that flags this as an unminified error. This allows the build
183 - // to proceed, while also allowing a post-build linter to detect it.
184 - //
185 - // Outputs:
186 - // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
187 - // if (!condition) {
188 - // throw Error(`A ${adj} message that contains ${noun}`);
189 - // }
190 - parentStatementPath.replaceWith(
191 - t.ifStatement(
192 - t.unaryExpression('!', condition),
193 - t.blockStatement([
194 - t.throwStatement(
195 - t.callExpression(t.identifier('Error'), [devMessage])
196 - ),
197 - ])
198 - )
199 - );
200 - parentStatementPath.addComment(
201 - 'leading',
202 - 'FIXME (minify-errors-in-prod): Unminified error message in production build!'
203 - );
204 - return;
205 - }
206 - prodErrorId = parseInt(prodErrorId, 10);
207 -
208 - // Import formatProdErrorMessage
209 - const formatProdErrorMessageIdentifier = helperModuleImports.addDefault(
210 - path,
211 - 'shared/formatProdErrorMessage',
212 - {nameHint: 'formatProdErrorMessage'}
213 - );
214 -
215 - // Outputs:
216 - // formatProdErrorMessage(ERR_CODE, adj, noun);
217 - const prodMessage = t.callExpression(
218 - formatProdErrorMessageIdentifier,
219 - [t.numericLiteral(prodErrorId), ...errorMsgExpressions]
220 - );
221 -
222 - // Outputs:
223 - // if (!condition) {
224 - // throw Error(
225 - // __DEV__
226 - // ? `A ${adj} message that contains ${noun}`
227 - // : formatProdErrorMessage(ERR_CODE, adj, noun)
228 - // );
229 - // }
230 - parentStatementPath.replaceWith(
231 - t.ifStatement(
232 - t.unaryExpression('!', condition),
233 - t.blockStatement([
234 - t.blockStatement([
235 - t.throwStatement(
236 - t.callExpression(t.identifier('Error'), [
237 - t.conditionalExpression(
238 - DEV_EXPRESSION,
239 - devMessage,
240 - prodMessage
241 - ),
242 - ])
243 - ),
244 - ]),
245 - ])
246 - )
247 - );
248 - }
130 },
131 },
132 };
scripts/jest/preprocessor.js
-6
@@ -13,9 +13,6 @@ const pathToBabel = path.join(
13 '../..',
14 'package.json'
15 );
16 -const pathToBabelPluginDevWithCode = require.resolve(
17 - '../error-codes/transform-error-messages'
18 -);
16 const pathToBabelPluginReplaceConsoleCalls = require.resolve(
17 '../babel/transform-replace-console-calls'
18 );
@@ -36,8 +33,6 @@ const babelOptions = {
33 // For Node environment only. For builds, Rollup takes care of ESM.
34 require.resolve('@babel/plugin-transform-modules-commonjs'),
35
39 - pathToBabelPluginDevWithCode,
40 -
36 // Keep stacks detailed in tests.
37 // Don't put this in .babelrc so that we don't embed filenames
38 // into ReactART builds that include JSX.
@@ -105,7 +100,6 @@ module.exports = {
100 __filename,
101 pathToBabel,
102 pathToBabelrc,
108 - pathToBabelPluginDevWithCode,
103 pathToTransformInfiniteLoops,
104 pathToTransformTestGatePragma,
105 pathToErrorCodes,
scripts/rollup/build.js
+7 -43
@@ -19,7 +19,6 @@ const Sync = require('./sync');
19 const sizes = require('./plugins/sizes-plugin');
20 const useForks = require('./plugins/use-forks-plugin');
21 const stripUnusedImports = require('./plugins/strip-unused-imports');
22 -const extractErrorCodes = require('../error-codes/extract-errors');
22 const Packaging = require('./packaging');
23 const {asyncRimRaf} = require('./utils');
24 const codeFrame = require('babel-code-frame');
@@ -94,10 +93,6 @@ const forcePrettyOutput = argv.pretty;
93 const isWatchMode = argv.watch;
94 const syncFBSourcePath = argv['sync-fbsource'];
95 const syncWWWPath = argv['sync-www'];
97 -const shouldExtractErrors = argv['extract-errors'];
98 -const errorCodeOpts = {
99 - errorMapFilePath: 'scripts/error-codes/codes.json',
100 -};
96
97 const closureOptions = {
98 compilation_level: 'SIMPLE',
@@ -176,26 +171,13 @@ function getBabelConfig(
171 if (updateBabelOptions) {
172 options = updateBabelOptions(options);
173 }
174 + // Controls whether to replace error messages with error codes in production.
175 + // By default, error messages are replaced in production.
176 + if (!isDevelopment && bundle.minifyWithProdErrorCodes !== false) {
177 + options.plugins.push(require('../error-codes/transform-error-messages'));
178 + }
179 +
180 switch (bundleType) {
180 - case FB_WWW_DEV:
181 - case FB_WWW_PROD:
182 - case FB_WWW_PROFILING:
183 - case RN_OSS_DEV:
184 - case RN_OSS_PROD:
185 - case RN_OSS_PROFILING:
186 - case RN_FB_DEV:
187 - case RN_FB_PROD:
188 - case RN_FB_PROFILING:
189 - return Object.assign({}, options, {
190 - plugins: options.plugins.concat([
191 - [
192 - require('../error-codes/transform-error-messages'),
193 - // Controls whether to replace error messages with error codes
194 - // in production. By default, error messages are replaced.
195 - {noMinify: bundle.minifyWithProdErrorCodes === false},
196 - ],
197 - ]),
198 - });
181 case UMD_DEV:
182 case UMD_PROD:
183 case UMD_PROFILING:
@@ -206,8 +188,6 @@ function getBabelConfig(
188 plugins: options.plugins.concat([
189 // Use object-assign polyfill in open source
190 path.resolve('./scripts/babel/transform-object-assign-require'),
209 - // Minify invariant messages
210 - require('../error-codes/transform-error-messages'),
191 ]),
192 });
193 default:
@@ -339,7 +319,6 @@ function getPlugins(
319 pureExternalModules,
320 bundle
321 ) {
342 - const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts);
322 const forks = Modules.getForks(bundleType, entry, moduleType, bundle);
323 const isProduction = isProductionBundleType(bundleType);
324 const isProfiling = isProfilingBundleType(bundleType);
@@ -360,13 +339,6 @@ function getPlugins(
339 bundleType === RN_FB_PROFILING;
340 const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;
341 return [
363 - // Extract error codes from invariant() messages into a file.
364 - shouldExtractErrors && {
365 - transform(source) {
366 - findAndRecordErrorCodes(source);
367 - return source;
368 - },
369 - },
342 // Shim any modules that need forking in this environment.
343 useForks(forks),
344 // Ensure we don't try to bundle any fbjs modules.
@@ -762,7 +734,7 @@ async function buildEverything() {
734 );
735 }
736
765 - if (!shouldExtractErrors && process.env.CIRCLE_NODE_TOTAL) {
737 + if (process.env.CIRCLE_NODE_TOTAL) {
738 // In CI, parallelize bundles across multiple tasks.
739 const nodeTotal = parseInt(process.env.CIRCLE_NODE_TOTAL, 10);
740 const nodeIndex = parseInt(process.env.CIRCLE_NODE_INDEX, 10);
@@ -787,14 +759,6 @@ async function buildEverything() {
759 if (!forcePrettyOutput) {
760 Stats.saveResults();
761 }
790 -
791 - if (shouldExtractErrors) {
792 - console.warn(
793 - '\nWarning: this build was created with --extract-errors enabled.\n' +
794 - 'this will result in extremely slow builds and should only be\n' +
795 - 'used when the error map needs to be rebuilt.\n'
796 - );
797 - }
762 }
763
764 buildEverything();
scripts/rollup/bundles.js
+7 -7
@@ -141,7 +141,7 @@ const bundles = [
141 moduleType: ISOMORPHIC,
142 entry: 'react-fetch/index.browser',
143 global: 'ReactFetch',
144 - minifyWithProdErrorCodes: true,
144 + minifyWithProdErrorCodes: false,
145 wrapWithModuleBoundaries: false,
146 externals: ['react'],
147 },
@@ -163,7 +163,7 @@ const bundles = [
163 moduleType: ISOMORPHIC,
164 entry: 'react-fs/index.browser.server',
165 global: 'ReactFilesystem',
166 - minifyWithProdErrorCodes: true,
166 + minifyWithProdErrorCodes: false,
167 wrapWithModuleBoundaries: false,
168 externals: [],
169 },
@@ -185,7 +185,7 @@ const bundles = [
185 moduleType: ISOMORPHIC,
186 entry: 'react-pg/index.browser.server',
187 global: 'ReactPostgres',
188 - minifyWithProdErrorCodes: true,
188 + minifyWithProdErrorCodes: false,
189 wrapWithModuleBoundaries: false,
190 externals: [],
191 },
@@ -349,7 +349,7 @@ const bundles = [
349 moduleType: RENDERER,
350 entry: 'react-server-dom-webpack',
351 global: 'ReactServerDOMReader',
352 - minifyWithProdErrorCodes: true,
352 + minifyWithProdErrorCodes: false,
353 wrapWithModuleBoundaries: false,
354 externals: ['react'],
355 },
@@ -594,7 +594,7 @@ const bundles = [
594 moduleType: RENDERER,
595 entry: 'react-noop-renderer',
596 global: 'ReactNoopRenderer',
597 - minifyWithProdErrorCodes: true,
597 + minifyWithProdErrorCodes: false,
598 wrapWithModuleBoundaries: false,
599 externals: ['react', 'scheduler', 'scheduler/unstable_mock', 'expect'],
600 },
@@ -605,7 +605,7 @@ const bundles = [
605 moduleType: RENDERER,
606 entry: 'react-noop-renderer/persistent',
607 global: 'ReactNoopRendererPersistent',
608 - minifyWithProdErrorCodes: true,
608 + minifyWithProdErrorCodes: false,
609 wrapWithModuleBoundaries: false,
610 externals: ['react', 'scheduler', 'expect'],
611 },
@@ -616,7 +616,7 @@ const bundles = [
616 moduleType: RENDERER,
617 entry: 'react-noop-renderer/server',
618 global: 'ReactNoopRendererServer',
619 - minifyWithProdErrorCodes: true,
619 + minifyWithProdErrorCodes: false,
620 wrapWithModuleBoundaries: false,
621 externals: ['react', 'scheduler', 'expect'],
622 },