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
This commit is contained in:
Andrew Clark 2021-10-31 18:37:32 -04:00 committed by GitHub
parent 9c8161ba81
commit 7034408ff7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 446 deletions

View File

@ -209,7 +209,20 @@ jobs:
- run: yarn workspaces info | head -n -1 > workspace_info.txt - run: yarn workspaces info | head -n -1 > workspace_info.txt
- *restore_node_modules - *restore_node_modules
- run: yarn lint-build - run: yarn lint-build
- run: scripts/circleci/check_minified_errors.sh
check_error_codes:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace: *attach_workspace
- run: yarn workspaces info | head -n -1 > workspace_info.txt
- *restore_node_modules
- run:
name: Search build artifacts for unminified errors
command: |
yarn extract-errors
git diff || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
yarn_test: yarn_test:
docker: *docker docker: *docker
@ -414,6 +427,9 @@ workflows:
- yarn_lint_build: - yarn_lint_build:
requires: requires:
- yarn_build_combined - yarn_build_combined
- check_error_codes:
requires:
- yarn_build_combined
- RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: - RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
requires: requires:
- yarn_build_combined - yarn_build_combined

View File

@ -114,7 +114,7 @@
"linc": "node ./scripts/tasks/linc.js", "linc": "node ./scripts/tasks/linc.js",
"lint": "node ./scripts/tasks/eslint.js", "lint": "node ./scripts/tasks/eslint.js",
"lint-build": "node ./scripts/rollup/validate/index.js", "lint-build": "node ./scripts/rollup/validate/index.js",
"extract-errors": "yarn build --type=dev --extract-errors", "extract-errors": "node scripts/error-codes/extract-errors.js",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js", "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
"debug-test": "yarn test --deprecated 'yarn test --debug'", "debug-test": "yarn test --deprecated 'yarn test --debug'",
"test": "node ./scripts/jest/jest-cli.js", "test": "node ./scripts/jest/jest-cli.js",

View File

@ -37,6 +37,7 @@ describe('ReactError', () => {
}); });
// @gate build === "production" // @gate build === "production"
// @gate !source
it('should error with minified error code', () => { it('should error with minified error code', () => {
expect(() => ReactDOM.render('Hi', null)).toThrowError( expect(() => ReactDOM.render('Hi', null)).toThrowError(
'Minified React error #200; visit ' + 'Minified React error #200; visit ' +

View File

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

View File

@ -2,25 +2,7 @@
exports[`error transform handles escaped backticks in template string 1`] = ` exports[`error transform handles escaped backticks in template string 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
Error(__DEV__ ? \\"Expected \`\\" + listener + \\"\` listener to be a function, instead got a value of \`\\" + type + \\"\` type.\\" : _formatProdErrorMessage(231, listener, type));" Error(_formatProdErrorMessage(231, listener, type));"
`;
exports[`error transform should correctly transform invariants that are not in the error codes map 1`] = `
"import invariant from 'shared/invariant';
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/
if (!condition) {
throw Error(\\"This is not a real error message.\\");
}"
`;
exports[`error transform should handle escaped characters 1`] = `
"import invariant from 'shared/invariant';
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/
if (!condition) {
throw Error(\\"What's up?\\");
}"
`; `;
exports[`error transform should not touch other calls or new expressions 1`] = ` exports[`error transform should not touch other calls or new expressions 1`] = `
@ -28,68 +10,39 @@ exports[`error transform should not touch other calls or new expressions 1`] = `
NotAnError();" NotAnError();"
`; `;
exports[`error transform should output FIXME for errors that don't have a matching error code 1`] = `
"/*! FIXME (minify-errors-in-prod): Unminified error message in production build!*/
/*! <expected-error-format>\\"This is not a real error message.\\"</expected-error-format>*/
Error('This is not a real error message.');"
`;
exports[`error transform should output FIXME for errors that don't have a matching error code, unless opted out with a comment 1`] = `
"// eslint-disable-next-line react-internal/prod-error-codes
Error('This is not a real error message.');"
`;
exports[`error transform should replace error constructors (no new) 1`] = ` exports[`error transform should replace error constructors (no new) 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));" Error(_formatProdErrorMessage(16));"
`; `;
exports[`error transform should replace error constructors 1`] = ` exports[`error transform should replace error constructors 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));" Error(_formatProdErrorMessage(16));"
`;
exports[`error transform should replace simple invariant calls 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
import invariant from 'shared/invariant';
if (!condition) {
{
throw Error(__DEV__ ? \\"Do not override existing functions.\\" : _formatProdErrorMessage(16));
}
}"
`; `;
exports[`error transform should support error constructors with concatenated messages 1`] = ` exports[`error transform should support error constructors with concatenated messages 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
Error(__DEV__ ? \\"Expected \\" + foo + \\" target to \\" + (\\"be an array; got \\" + bar) : _formatProdErrorMessage(7, foo, bar));" Error(_formatProdErrorMessage(7, foo, bar));"
`; `;
exports[`error transform should support interpolating arguments with concatenation 1`] = ` exports[`error transform should support interpolating arguments with concatenation 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
Error(__DEV__ ? 'Expected ' + foo + ' target to be an array; got ' + bar : _formatProdErrorMessage(7, foo, bar));" Error(_formatProdErrorMessage(7, foo, bar));"
`; `;
exports[`error transform should support interpolating arguments with template strings 1`] = ` exports[`error transform should support interpolating arguments with template strings 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
Error(__DEV__ ? \\"Expected \\" + foo + \\" target to be an array; got \\" + bar : _formatProdErrorMessage(7, foo, bar));" Error(_formatProdErrorMessage(7, foo, bar));"
`;
exports[`error transform should support invariant calls with a concatenated template string and args 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
import invariant from 'shared/invariant';
if (!condition) {
{
throw Error(__DEV__ ? \\"Expected a component class, got \\" + Foo + \\".\\" + Bar : _formatProdErrorMessage(18, Foo, Bar));
}
}"
`;
exports[`error transform should support invariant calls with args 1`] = `
"import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\";
import invariant from 'shared/invariant';
if (!condition) {
{
throw Error(__DEV__ ? \\"Expected \\" + foo + \\" target to be an array; got \\" + bar : _formatProdErrorMessage(7, foo, bar));
}
}"
`;
exports[`error transform should support noMinify option 1`] = `
"import invariant from 'shared/invariant';
if (!condition) {
throw Error(\\"Do not override existing functions.\\");
}"
`; `;

View File

@ -28,72 +28,6 @@ describe('error transform', () => {
process.env.NODE_ENV = oldEnv; process.env.NODE_ENV = oldEnv;
}); });
it('should replace simple invariant calls', () => {
expect(
transform(`
import invariant from 'shared/invariant';
invariant(condition, 'Do not override existing functions.');
`)
).toMatchSnapshot();
});
it('should throw if invariant is not in an expression statement', () => {
expect(() => {
transform(`
import invariant from 'shared/invariant';
cond && invariant(condition, 'Do not override existing functions.');
`);
}).toThrow('invariant() cannot be called from expression context');
});
it('should support invariant calls with args', () => {
expect(
transform(`
import invariant from 'shared/invariant';
invariant(condition, 'Expected %s target to be an array; got %s', foo, bar);
`)
).toMatchSnapshot();
});
it('should support invariant calls with a concatenated template string and args', () => {
expect(
transform(`
import invariant from 'shared/invariant';
invariant(condition, 'Expected a component class, ' + 'got %s.' + '%s', Foo, Bar);
`)
).toMatchSnapshot();
});
it('should correctly transform invariants that are not in the error codes map', () => {
expect(
transform(`
import invariant from 'shared/invariant';
invariant(condition, 'This is not a real error message.');
`)
).toMatchSnapshot();
});
it('should handle escaped characters', () => {
expect(
transform(`
import invariant from 'shared/invariant';
invariant(condition, 'What\\'s up?');
`)
).toMatchSnapshot();
});
it('should support noMinify option', () => {
expect(
transform(
`
import invariant from 'shared/invariant';
invariant(condition, 'Do not override existing functions.');
`,
{noMinify: true}
)
).toMatchSnapshot();
});
it('should replace error constructors', () => { it('should replace error constructors', () => {
expect( expect(
transform(` transform(`
@ -110,6 +44,31 @@ Error('Do not override existing functions.');
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it("should output FIXME for errors that don't have a matching error code", () => {
expect(
transform(`
Error('This is not a real error message.');
`)
).toMatchSnapshot();
});
it(
"should output FIXME for errors that don't have a matching error " +
'code, unless opted out with a comment',
() => {
// TODO: Since this only detects one of many ways to disable a lint
// rule, we should instead search for a custom directive (like
// no-minify-errors) instead of ESLint. Will need to update our lint
// rule to recognize the same directive.
expect(
transform(`
// eslint-disable-next-line react-internal/prod-error-codes
Error('This is not a real error message.');
`)
).toMatchSnapshot();
}
);
it('should not touch other calls or new expressions', () => { it('should not touch other calls or new expressions', () => {
expect( expect(
transform(` transform(`

View File

@ -1,105 +1,74 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict'; 'use strict';
const parser = require('@babel/parser');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const traverse = require('@babel/traverse').default; const {execSync} = require('child_process');
const {evalStringConcat} = require('../shared/evalToString');
const invertObject = require('./invertObject');
const babylonOptions = { async function main() {
sourceType: 'module', const originalJSON = JSON.parse(
// As a parser, babylon has its own options and we can't directly fs.readFileSync(path.resolve(__dirname, '../error-codes/codes.json'))
// import/require a babel preset. It should be kept **the same** as );
// the `babel-plugin-syntax-*` ones specified in const existingMessages = new Set();
// https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js const codes = Object.keys(originalJSON);
plugins: [ let nextCode = 0;
'classProperties', for (let i = 0; i < codes.length; i++) {
'flow', const codeStr = codes[i];
'jsx', const message = originalJSON[codeStr];
'trailingFunctionCommas', const code = parseInt(codeStr, 10);
'objectRestSpread', existingMessages.add(message);
], if (code >= nextCode) {
}; nextCode = code + 1;
}
module.exports = function(opts) {
if (!opts || !('errorMapFilePath' in opts)) {
throw new Error(
'Missing options. Ensure you pass an object with `errorMapFilePath`.'
);
} }
const errorMapFilePath = opts.errorMapFilePath; console.log('Searching `build` directory for unminified errors...\n');
let existingErrorMap;
let out;
try { try {
// Using `fs.readFileSync` instead of `require` here, because `require()` out = execSync(
// calls are cached, and the cache map is not properly invalidated after "git --no-pager grep -n --untracked --no-exclude-standard '/*! <expected-error-format>' -- build"
// file changes. ).toString();
existingErrorMap = JSON.parse(
fs.readFileSync(
path.join(__dirname, path.basename(errorMapFilePath)),
'utf8'
)
);
} catch (e) { } catch (e) {
existingErrorMap = {}; if (e.status === 1 && e.stdout.toString() === '') {
} // No unminified errors found.
const allErrorIDs = Object.keys(existingErrorMap);
let currentID;
if (allErrorIDs.length === 0) {
// Map is empty
currentID = 0;
} else {
currentID = Math.max.apply(null, allErrorIDs) + 1;
}
// Here we invert the map object in memory for faster error code lookup
existingErrorMap = invertObject(existingErrorMap);
function transform(source) {
const ast = parser.parse(source, babylonOptions);
traverse(ast, {
CallExpression: {
exit(astPath) {
if (astPath.get('callee').isIdentifier({name: 'invariant'})) {
const node = astPath.node;
// error messages can be concatenated (`+`) at runtime, so here's a
// trivial partial evaluator that interprets the literal value
const errorMsgLiteral = evalStringConcat(node.arguments[1]);
addToErrorMap(errorMsgLiteral);
}
},
},
});
}
function addToErrorMap(errorMsgLiteral) {
if (existingErrorMap.hasOwnProperty(errorMsgLiteral)) {
return; return;
} }
existingErrorMap[errorMsgLiteral] = '' + currentID++; throw e;
} }
function flush(cb) { let newJSON = null;
const regex = /\<expected-error-format\>"(.+?)"\<\/expected-error-format\>/g;
do {
const match = regex.exec(out);
if (match === null) {
break;
} else {
const message = match[1].trim();
if (existingMessages.has(message)) {
// This probably means you ran the script twice.
continue;
}
existingMessages.add(message);
// Add to json map
if (newJSON === null) {
newJSON = Object.assign({}, originalJSON);
}
console.log(`"${nextCode}": "${message}"`);
newJSON[nextCode] = message;
nextCode += 1;
}
} while (true);
if (newJSON) {
fs.writeFileSync( fs.writeFileSync(
errorMapFilePath, path.resolve(__dirname, '../error-codes/codes.json'),
JSON.stringify(invertObject(existingErrorMap), null, 2) + '\n', JSON.stringify(newJSON, null, 2)
'utf-8'
); );
} }
}
return function extractErrors(source) { main().catch(error => {
transform(source); console.error(error);
flush(); process.exit(1);
}; });
};

View File

@ -7,10 +7,7 @@
'use strict'; 'use strict';
const fs = require('fs'); const fs = require('fs');
const { const {evalStringAndTemplateConcat} = require('../shared/evalToString');
evalStringConcat,
evalStringAndTemplateConcat,
} = require('../shared/evalToString');
const invertObject = require('./invertObject'); const invertObject = require('./invertObject');
const helperModuleImports = require('@babel/helper-module-imports'); const helperModuleImports = require('@babel/helper-module-imports');
@ -23,11 +20,7 @@ const SEEN_SYMBOL = Symbol('transform-error-messages.seen');
module.exports = function(babel) { module.exports = function(babel) {
const t = babel.types; const t = babel.types;
// TODO: Instead of outputting __DEV__ conditions, only apply this transform function ErrorCallExpression(path, file) {
// in production.
const DEV_EXPRESSION = t.identifier('__DEV__');
function CallOrNewExpression(path, file) {
// Turns this code: // Turns this code:
// //
// new Error(`A ${adj} message that contains ${noun}`); // new Error(`A ${adj} message that contains ${noun}`);
@ -38,11 +31,7 @@ module.exports = function(babel) {
// //
// into this: // into this:
// //
// Error( // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
// __DEV__
// ? `A ${adj} message that contains ${noun}`
// : formatProdErrorMessage(ERR_CODE, adj, noun)
// );
const node = path.node; const node = path.node;
if (node[SEEN_SYMBOL]) { if (node[SEEN_SYMBOL]) {
return; return;
@ -62,9 +51,44 @@ module.exports = function(babel) {
let prodErrorId = errorMap[errorMsgLiteral]; let prodErrorId = errorMap[errorMsgLiteral];
if (prodErrorId === undefined) { if (prodErrorId === undefined) {
// There is no error code for this message. We use a lint rule to // There is no error code for this message. Add an inline comment
// enforce that messages can be minified, so assume this is // that flags this as an unminified error. This allows the build
// intentional and exit gracefully. // to proceed, while also allowing a post-build linter to detect it.
//
// Outputs:
// /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
// /* <expected-error-format>"A % message that contains %"</expected-error-format> */
// if (!condition) {
// throw Error(`A ${adj} message that contains ${noun}`);
// }
const statementParent = path.getStatementParent();
const leadingComments = statementParent.node.leadingComments;
if (leadingComments !== undefined) {
for (let i = 0; i < leadingComments.length; i++) {
// TODO: Since this only detects one of many ways to disable a lint
// rule, we should instead search for a custom directive (like
// no-minify-errors) instead of ESLint. Will need to update our lint
// rule to recognize the same directive.
const commentText = leadingComments[i].value;
if (
commentText.includes(
'eslint-disable-next-line react-internal/prod-error-codes'
)
) {
return;
}
}
}
statementParent.addComment(
'leading',
`! <expected-error-format>"${errorMsgLiteral}"</expected-error-format>`
);
statementParent.addComment(
'leading',
'! FIXME (minify-errors-in-prod): Unminified error message in production build!'
);
return; return;
} }
prodErrorId = parseInt(prodErrorId, 10); prodErrorId = parseInt(prodErrorId, 10);
@ -84,168 +108,25 @@ module.exports = function(babel) {
]); ]);
// Outputs: // Outputs:
// Error( // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
// __DEV__ const newErrorCall = t.callExpression(t.identifier('Error'), [prodMessage]);
// ? `A ${adj} message that contains ${noun}` newErrorCall[SEEN_SYMBOL] = true;
// : formatProdErrorMessage(ERR_CODE, adj, noun) path.replaceWith(newErrorCall);
// );
path.replaceWith(t.callExpression(t.identifier('Error'), [prodMessage]));
path.replaceWith(
t.callExpression(t.identifier('Error'), [
t.conditionalExpression(DEV_EXPRESSION, errorMsgNode, prodMessage),
])
);
} }
return { return {
visitor: { visitor: {
NewExpression(path, file) { NewExpression(path, file) {
const noMinify = file.opts.noMinify; if (path.get('callee').isIdentifier({name: 'Error'})) {
if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) { ErrorCallExpression(path, file);
CallOrNewExpression(path, file);
} }
}, },
CallExpression(path, file) { CallExpression(path, file) {
const node = path.node; if (path.get('callee').isIdentifier({name: 'Error'})) {
const noMinify = file.opts.noMinify; ErrorCallExpression(path, file);
if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) {
CallOrNewExpression(path, file);
return; return;
} }
if (path.get('callee').isIdentifier({name: 'invariant'})) {
// Turns this code:
//
// invariant(condition, 'A %s message that contains %s', adj, noun);
//
// into this:
//
// if (!condition) {
// throw Error(
// __DEV__
// ? `A ${adj} message that contains ${noun}`
// : formatProdErrorMessage(ERR_CODE, adj, noun)
// );
// }
//
// where ERR_CODE is an error code: a unique identifier (a number
// string) that references a verbose error message. The mapping is
// stored in `scripts/error-codes/codes.json`.
const condition = node.arguments[0];
const errorMsgLiteral = evalStringConcat(node.arguments[1]);
const errorMsgExpressions = Array.from(node.arguments.slice(2));
const errorMsgQuasis = errorMsgLiteral
.split('%s')
.map(raw => t.templateElement({raw, cooked: String.raw({raw})}));
// Outputs:
// `A ${adj} message that contains ${noun}`;
const devMessage = t.templateLiteral(
errorMsgQuasis,
errorMsgExpressions
);
const parentStatementPath = path.parentPath;
if (parentStatementPath.type !== 'ExpressionStatement') {
throw path.buildCodeFrameError(
'invariant() cannot be called from expression context. Move ' +
'the call to its own statement.'
);
}
if (noMinify) {
// Error minification is disabled for this build.
//
// Outputs:
// if (!condition) {
// throw Error(`A ${adj} message that contains ${noun}`);
// }
parentStatementPath.replaceWith(
t.ifStatement(
t.unaryExpression('!', condition),
t.blockStatement([
t.throwStatement(
t.callExpression(t.identifier('Error'), [devMessage])
),
])
)
);
return;
}
let prodErrorId = errorMap[errorMsgLiteral];
if (prodErrorId === undefined) {
// There is no error code for this message. Add an inline comment
// that flags this as an unminified error. This allows the build
// to proceed, while also allowing a post-build linter to detect it.
//
// Outputs:
// /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
// if (!condition) {
// throw Error(`A ${adj} message that contains ${noun}`);
// }
parentStatementPath.replaceWith(
t.ifStatement(
t.unaryExpression('!', condition),
t.blockStatement([
t.throwStatement(
t.callExpression(t.identifier('Error'), [devMessage])
),
])
)
);
parentStatementPath.addComment(
'leading',
'FIXME (minify-errors-in-prod): Unminified error message in production build!'
);
return;
}
prodErrorId = parseInt(prodErrorId, 10);
// Import formatProdErrorMessage
const formatProdErrorMessageIdentifier = helperModuleImports.addDefault(
path,
'shared/formatProdErrorMessage',
{nameHint: 'formatProdErrorMessage'}
);
// Outputs:
// formatProdErrorMessage(ERR_CODE, adj, noun);
const prodMessage = t.callExpression(
formatProdErrorMessageIdentifier,
[t.numericLiteral(prodErrorId), ...errorMsgExpressions]
);
// Outputs:
// if (!condition) {
// throw Error(
// __DEV__
// ? `A ${adj} message that contains ${noun}`
// : formatProdErrorMessage(ERR_CODE, adj, noun)
// );
// }
parentStatementPath.replaceWith(
t.ifStatement(
t.unaryExpression('!', condition),
t.blockStatement([
t.blockStatement([
t.throwStatement(
t.callExpression(t.identifier('Error'), [
t.conditionalExpression(
DEV_EXPRESSION,
devMessage,
prodMessage
),
])
),
]),
])
)
);
}
}, },
}, },
}; };

View File

@ -13,9 +13,6 @@ const pathToBabel = path.join(
'../..', '../..',
'package.json' 'package.json'
); );
const pathToBabelPluginDevWithCode = require.resolve(
'../error-codes/transform-error-messages'
);
const pathToBabelPluginReplaceConsoleCalls = require.resolve( const pathToBabelPluginReplaceConsoleCalls = require.resolve(
'../babel/transform-replace-console-calls' '../babel/transform-replace-console-calls'
); );
@ -36,8 +33,6 @@ const babelOptions = {
// For Node environment only. For builds, Rollup takes care of ESM. // For Node environment only. For builds, Rollup takes care of ESM.
require.resolve('@babel/plugin-transform-modules-commonjs'), require.resolve('@babel/plugin-transform-modules-commonjs'),
pathToBabelPluginDevWithCode,
// Keep stacks detailed in tests. // Keep stacks detailed in tests.
// Don't put this in .babelrc so that we don't embed filenames // Don't put this in .babelrc so that we don't embed filenames
// into ReactART builds that include JSX. // into ReactART builds that include JSX.
@ -105,7 +100,6 @@ module.exports = {
__filename, __filename,
pathToBabel, pathToBabel,
pathToBabelrc, pathToBabelrc,
pathToBabelPluginDevWithCode,
pathToTransformInfiniteLoops, pathToTransformInfiniteLoops,
pathToTransformTestGatePragma, pathToTransformTestGatePragma,
pathToErrorCodes, pathToErrorCodes,

View File

@ -19,7 +19,6 @@ const Sync = require('./sync');
const sizes = require('./plugins/sizes-plugin'); const sizes = require('./plugins/sizes-plugin');
const useForks = require('./plugins/use-forks-plugin'); const useForks = require('./plugins/use-forks-plugin');
const stripUnusedImports = require('./plugins/strip-unused-imports'); const stripUnusedImports = require('./plugins/strip-unused-imports');
const extractErrorCodes = require('../error-codes/extract-errors');
const Packaging = require('./packaging'); const Packaging = require('./packaging');
const {asyncRimRaf} = require('./utils'); const {asyncRimRaf} = require('./utils');
const codeFrame = require('babel-code-frame'); const codeFrame = require('babel-code-frame');
@ -94,10 +93,6 @@ const forcePrettyOutput = argv.pretty;
const isWatchMode = argv.watch; const isWatchMode = argv.watch;
const syncFBSourcePath = argv['sync-fbsource']; const syncFBSourcePath = argv['sync-fbsource'];
const syncWWWPath = argv['sync-www']; const syncWWWPath = argv['sync-www'];
const shouldExtractErrors = argv['extract-errors'];
const errorCodeOpts = {
errorMapFilePath: 'scripts/error-codes/codes.json',
};
const closureOptions = { const closureOptions = {
compilation_level: 'SIMPLE', compilation_level: 'SIMPLE',
@ -176,26 +171,13 @@ function getBabelConfig(
if (updateBabelOptions) { if (updateBabelOptions) {
options = updateBabelOptions(options); options = updateBabelOptions(options);
} }
// Controls whether to replace error messages with error codes in production.
// By default, error messages are replaced in production.
if (!isDevelopment && bundle.minifyWithProdErrorCodes !== false) {
options.plugins.push(require('../error-codes/transform-error-messages'));
}
switch (bundleType) { switch (bundleType) {
case FB_WWW_DEV:
case FB_WWW_PROD:
case FB_WWW_PROFILING:
case RN_OSS_DEV:
case RN_OSS_PROD:
case RN_OSS_PROFILING:
case RN_FB_DEV:
case RN_FB_PROD:
case RN_FB_PROFILING:
return Object.assign({}, options, {
plugins: options.plugins.concat([
[
require('../error-codes/transform-error-messages'),
// Controls whether to replace error messages with error codes
// in production. By default, error messages are replaced.
{noMinify: bundle.minifyWithProdErrorCodes === false},
],
]),
});
case UMD_DEV: case UMD_DEV:
case UMD_PROD: case UMD_PROD:
case UMD_PROFILING: case UMD_PROFILING:
@ -206,8 +188,6 @@ function getBabelConfig(
plugins: options.plugins.concat([ plugins: options.plugins.concat([
// Use object-assign polyfill in open source // Use object-assign polyfill in open source
path.resolve('./scripts/babel/transform-object-assign-require'), path.resolve('./scripts/babel/transform-object-assign-require'),
// Minify invariant messages
require('../error-codes/transform-error-messages'),
]), ]),
}); });
default: default:
@ -339,7 +319,6 @@ function getPlugins(
pureExternalModules, pureExternalModules,
bundle bundle
) { ) {
const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts);
const forks = Modules.getForks(bundleType, entry, moduleType, bundle); const forks = Modules.getForks(bundleType, entry, moduleType, bundle);
const isProduction = isProductionBundleType(bundleType); const isProduction = isProductionBundleType(bundleType);
const isProfiling = isProfilingBundleType(bundleType); const isProfiling = isProfilingBundleType(bundleType);
@ -360,13 +339,6 @@ function getPlugins(
bundleType === RN_FB_PROFILING; bundleType === RN_FB_PROFILING;
const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput; const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;
return [ return [
// Extract error codes from invariant() messages into a file.
shouldExtractErrors && {
transform(source) {
findAndRecordErrorCodes(source);
return source;
},
},
// Shim any modules that need forking in this environment. // Shim any modules that need forking in this environment.
useForks(forks), useForks(forks),
// Ensure we don't try to bundle any fbjs modules. // Ensure we don't try to bundle any fbjs modules.
@ -762,7 +734,7 @@ async function buildEverything() {
); );
} }
if (!shouldExtractErrors && process.env.CIRCLE_NODE_TOTAL) { if (process.env.CIRCLE_NODE_TOTAL) {
// In CI, parallelize bundles across multiple tasks. // In CI, parallelize bundles across multiple tasks.
const nodeTotal = parseInt(process.env.CIRCLE_NODE_TOTAL, 10); const nodeTotal = parseInt(process.env.CIRCLE_NODE_TOTAL, 10);
const nodeIndex = parseInt(process.env.CIRCLE_NODE_INDEX, 10); const nodeIndex = parseInt(process.env.CIRCLE_NODE_INDEX, 10);
@ -787,14 +759,6 @@ async function buildEverything() {
if (!forcePrettyOutput) { if (!forcePrettyOutput) {
Stats.saveResults(); Stats.saveResults();
} }
if (shouldExtractErrors) {
console.warn(
'\nWarning: this build was created with --extract-errors enabled.\n' +
'this will result in extremely slow builds and should only be\n' +
'used when the error map needs to be rebuilt.\n'
);
}
} }
buildEverything(); buildEverything();

View File

@ -141,7 +141,7 @@ const bundles = [
moduleType: ISOMORPHIC, moduleType: ISOMORPHIC,
entry: 'react-fetch/index.browser', entry: 'react-fetch/index.browser',
global: 'ReactFetch', global: 'ReactFetch',
minifyWithProdErrorCodes: true, minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false, wrapWithModuleBoundaries: false,
externals: ['react'], externals: ['react'],
}, },
@ -163,7 +163,7 @@ const bundles = [
moduleType: ISOMORPHIC, moduleType: ISOMORPHIC,
entry: 'react-fs/index.browser.server', entry: 'react-fs/index.browser.server',
global: 'ReactFilesystem', global: 'ReactFilesystem',
minifyWithProdErrorCodes: true, minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false, wrapWithModuleBoundaries: false,
externals: [], externals: [],
}, },
@ -185,7 +185,7 @@ const bundles = [
moduleType: ISOMORPHIC, moduleType: ISOMORPHIC,
entry: 'react-pg/index.browser.server', entry: 'react-pg/index.browser.server',
global: 'ReactPostgres', global: 'ReactPostgres',
minifyWithProdErrorCodes: true, minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false, wrapWithModuleBoundaries: false,
externals: [], externals: [],
}, },
@ -349,7 +349,7 @@ const bundles = [
moduleType: RENDERER, moduleType: RENDERER,
entry: 'react-server-dom-webpack', entry: 'react-server-dom-webpack',
global: 'ReactServerDOMReader', global: 'ReactServerDOMReader',
minifyWithProdErrorCodes: true, minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false, wrapWithModuleBoundaries: false,
externals: ['react'], externals: ['react'],
}, },
@ -594,7 +594,7 @@ const bundles = [
moduleType: RENDERER, moduleType: RENDERER,
entry: 'react-noop-renderer', entry: 'react-noop-renderer',
global: 'ReactNoopRenderer', global: 'ReactNoopRenderer',
minifyWithProdErrorCodes: true, minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false, wrapWithModuleBoundaries: false,
externals: ['react', 'scheduler', 'scheduler/unstable_mock', 'expect'], externals: ['react', 'scheduler', 'scheduler/unstable_mock', 'expect'],
}, },
@ -605,7 +605,7 @@ const bundles = [
moduleType: RENDERER, moduleType: RENDERER,
entry: 'react-noop-renderer/persistent', entry: 'react-noop-renderer/persistent',
global: 'ReactNoopRendererPersistent', global: 'ReactNoopRendererPersistent',
minifyWithProdErrorCodes: true, minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false, wrapWithModuleBoundaries: false,
externals: ['react', 'scheduler', 'expect'], externals: ['react', 'scheduler', 'expect'],
}, },
@ -616,7 +616,7 @@ const bundles = [
moduleType: RENDERER, moduleType: RENDERER,
entry: 'react-noop-renderer/server', entry: 'react-noop-renderer/server',
global: 'ReactNoopRendererServer', global: 'ReactNoopRendererServer',
minifyWithProdErrorCodes: true, minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false, wrapWithModuleBoundaries: false,
externals: ['react', 'scheduler', 'expect'], externals: ['react', 'scheduler', 'expect'],
}, },