From cc24142ed32a1f6958831c17f9292e15b55e9db6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Oct 2021 09:06:30 -0400 Subject: [PATCH 1/7] 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. --- .../transform-error-messages.js.snap | 10 +++ .../__tests__/transform-error-messages.js | 25 ++++++ .../error-codes/transform-error-messages.js | 85 ++++++++++++------- 3 files changed, 90 insertions(+), 30 deletions(-) diff --git a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap index bfb80ab375562..7b55c12b5d71e 100644 --- a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap +++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap @@ -28,6 +28,16 @@ exports[`error transform should not touch other calls or new expressions 1`] = ` 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!*/ +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`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; Error(__DEV__ ? 'Do not override existing functions.' : _formatProdErrorMessage(16));" diff --git a/scripts/error-codes/__tests__/transform-error-messages.js b/scripts/error-codes/__tests__/transform-error-messages.js index 2bca7366321e2..b940914c240ff 100644 --- a/scripts/error-codes/__tests__/transform-error-messages.js +++ b/scripts/error-codes/__tests__/transform-error-messages.js @@ -110,6 +110,31 @@ Error('Do not override existing functions.'); ).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', () => { expect( transform(` diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index 2baf4baa1c1a7..045798ed17af1 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -62,9 +62,39 @@ module.exports = function(babel) { let prodErrorId = errorMap[errorMsgLiteral]; if (prodErrorId === undefined) { - // There is no error code for this message. We use a lint rule to - // enforce that messages can be minified, so assume this is - // intentional and exit gracefully. + // 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}`); + // } + + 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', + 'FIXME (minify-errors-in-prod): Unminified error message in production build!' + ); return; } prodErrorId = parseInt(prodErrorId, 10); @@ -89,12 +119,11 @@ module.exports = function(babel) { // ? `A ${adj} message that contains ${noun}` // : formatProdErrorMessage(ERR_CODE, adj, noun) // ); - path.replaceWith(t.callExpression(t.identifier('Error'), [prodMessage])); - path.replaceWith( - t.callExpression(t.identifier('Error'), [ - t.conditionalExpression(DEV_EXPRESSION, errorMsgNode, prodMessage), - ]) - ); + const newErrorCall = t.callExpression(t.identifier('Error'), [ + t.conditionalExpression(DEV_EXPRESSION, errorMsgNode, prodMessage), + ]); + newErrorCall[SEEN_SYMBOL] = true; + path.replaceWith(newErrorCall); } return { @@ -162,16 +191,17 @@ module.exports = function(babel) { // if (!condition) { // throw Error(`A ${adj} message that contains ${noun}`); // } + const errorCallNode = t.callExpression(t.identifier('Error'), [ + devMessage, + ]); + errorCallNode[SEEN_SYMBOL] = true; parentStatementPath.replaceWith( t.ifStatement( t.unaryExpression('!', condition), - t.blockStatement([ - t.throwStatement( - t.callExpression(t.identifier('Error'), [devMessage]) - ), - ]) + t.blockStatement([t.throwStatement(errorCallNode)]) ) ); + return; } @@ -187,14 +217,14 @@ module.exports = function(babel) { // if (!condition) { // throw Error(`A ${adj} message that contains ${noun}`); // } + const errorCall = t.callExpression(t.identifier('Error'), [ + devMessage, + ]); + errorCall[SEEN_SYMBOL] = true; parentStatementPath.replaceWith( t.ifStatement( t.unaryExpression('!', condition), - t.blockStatement([ - t.throwStatement( - t.callExpression(t.identifier('Error'), [devMessage]) - ), - ]) + t.blockStatement([t.throwStatement(errorCall)]) ) ); parentStatementPath.addComment( @@ -219,6 +249,11 @@ module.exports = function(babel) { [t.numericLiteral(prodErrorId), ...errorMsgExpressions] ); + const errorCall = t.callExpression(t.identifier('Error'), [ + t.conditionalExpression(DEV_EXPRESSION, devMessage, prodMessage), + ]); + errorCall[SEEN_SYMBOL] = true; + // Outputs: // if (!condition) { // throw Error( @@ -231,17 +266,7 @@ module.exports = function(babel) { t.ifStatement( t.unaryExpression('!', condition), t.blockStatement([ - t.blockStatement([ - t.throwStatement( - t.callExpression(t.identifier('Error'), [ - t.conditionalExpression( - DEV_EXPRESSION, - devMessage, - prodMessage - ), - ]) - ), - ]), + t.blockStatement([t.throwStatement(errorCall)]), ]) ) ); From 5a290b14f8ec4dc82e2f75b35440131ba1c27b05 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Oct 2021 09:44:48 -0400 Subject: [PATCH 2/7] 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. --- .../__tests__/ReactError-test.internal.js | 1 + .../transform-error-messages.js.snap | 73 +------- .../__tests__/transform-error-messages.js | 66 ------- .../error-codes/transform-error-messages.js | 172 ++---------------- scripts/jest/preprocessor.js | 6 - scripts/rollup/build.js | 27 +-- 6 files changed, 29 insertions(+), 316 deletions(-) diff --git a/packages/shared/__tests__/ReactError-test.internal.js b/packages/shared/__tests__/ReactError-test.internal.js index c40e62c6f391e..5576c72bf683e 100644 --- a/packages/shared/__tests__/ReactError-test.internal.js +++ b/packages/shared/__tests__/ReactError-test.internal.js @@ -37,6 +37,7 @@ describe('ReactError', () => { }); // @gate build === "production" + // @gate !source it('should error with minified error code', () => { expect(() => ReactDOM.render('Hi', null)).toThrowError( 'Minified React error #200; visit ' + diff --git a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap index 7b55c12b5d71e..c9c6ba8500f52 100644 --- a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap +++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap @@ -2,25 +2,7 @@ exports[`error transform handles escaped backticks in template string 1`] = ` "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));" -`; - -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?\\"); -}" +Error(_formatProdErrorMessage(231, listener, type));" `; exports[`error transform should not touch other calls or new expressions 1`] = ` @@ -30,6 +12,8 @@ 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!*/ + +/*Add to error code map: This is not a real error message.*/ Error('This is not a real error message.');" `; @@ -40,66 +24,25 @@ Error('This is not a real error message.');" exports[`error transform should replace error constructors (no new) 1`] = ` "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`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? 'Do not override existing functions.' : _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)); - } -}" +Error(_formatProdErrorMessage(16));" `; exports[`error transform should support error constructors with concatenated messages 1`] = ` "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`] = ` "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`] = ` "import _formatProdErrorMessage from \\"shared/formatProdErrorMessage\\"; -Error(__DEV__ ? \\"Expected \\" + foo + \\" target to be an array; got \\" + bar : _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.\\"); -}" +Error(_formatProdErrorMessage(7, foo, bar));" `; diff --git a/scripts/error-codes/__tests__/transform-error-messages.js b/scripts/error-codes/__tests__/transform-error-messages.js index b940914c240ff..3cf08b69e848f 100644 --- a/scripts/error-codes/__tests__/transform-error-messages.js +++ b/scripts/error-codes/__tests__/transform-error-messages.js @@ -28,72 +28,6 @@ describe('error transform', () => { 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', () => { expect( transform(` diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index 045798ed17af1..eaaf6df497726 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -7,10 +7,7 @@ 'use strict'; const fs = require('fs'); -const { - evalStringConcat, - evalStringAndTemplateConcat, -} = require('../shared/evalToString'); +const {evalStringAndTemplateConcat} = require('../shared/evalToString'); const invertObject = require('./invertObject'); const helperModuleImports = require('@babel/helper-module-imports'); @@ -23,11 +20,7 @@ const SEEN_SYMBOL = Symbol('transform-error-messages.seen'); module.exports = function(babel) { const t = babel.types; - // TODO: Instead of outputting __DEV__ conditions, only apply this transform - // in production. - const DEV_EXPRESSION = t.identifier('__DEV__'); - - function CallOrNewExpression(path, file) { + function ErrorCallExpression(path, file) { // Turns this code: // // new Error(`A ${adj} message that contains ${noun}`); @@ -38,11 +31,7 @@ module.exports = function(babel) { // // into this: // - // Error( - // __DEV__ - // ? `A ${adj} message that contains ${noun}` - // : formatProdErrorMessage(ERR_CODE, adj, noun) - // ); + // Error(formatProdErrorMessage(ERR_CODE, adj, noun)); const node = path.node; if (node[SEEN_SYMBOL]) { return; @@ -68,6 +57,7 @@ module.exports = function(babel) { // // Outputs: // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ + // /* Add to error code map: "A % message that contains %" */ // if (!condition) { // throw Error(`A ${adj} message that contains ${noun}`); // } @@ -91,6 +81,10 @@ module.exports = function(babel) { } } + statementParent.addComment( + 'leading', + 'Add to error code map: ' + errorMsgLiteral + ); statementParent.addComment( 'leading', 'FIXME (minify-errors-in-prod): Unminified error message in production build!' @@ -114,14 +108,8 @@ module.exports = function(babel) { ]); // Outputs: - // Error( - // __DEV__ - // ? `A ${adj} message that contains ${noun}` - // : formatProdErrorMessage(ERR_CODE, adj, noun) - // ); - const newErrorCall = t.callExpression(t.identifier('Error'), [ - t.conditionalExpression(DEV_EXPRESSION, errorMsgNode, prodMessage), - ]); + // Error(formatProdErrorMessage(ERR_CODE, adj, noun)); + const newErrorCall = t.callExpression(t.identifier('Error'), [prodMessage]); newErrorCall[SEEN_SYMBOL] = true; path.replaceWith(newErrorCall); } @@ -129,148 +117,16 @@ module.exports = function(babel) { return { visitor: { NewExpression(path, file) { - const noMinify = file.opts.noMinify; - if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) { - CallOrNewExpression(path, file); + if (path.get('callee').isIdentifier({name: 'Error'})) { + ErrorCallExpression(path, file); } }, CallExpression(path, file) { - const node = path.node; - const noMinify = file.opts.noMinify; - - if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) { - CallOrNewExpression(path, file); + if (path.get('callee').isIdentifier({name: 'Error'})) { + ErrorCallExpression(path, file); 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}`); - // } - const errorCallNode = t.callExpression(t.identifier('Error'), [ - devMessage, - ]); - errorCallNode[SEEN_SYMBOL] = true; - parentStatementPath.replaceWith( - t.ifStatement( - t.unaryExpression('!', condition), - t.blockStatement([t.throwStatement(errorCallNode)]) - ) - ); - - 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}`); - // } - const errorCall = t.callExpression(t.identifier('Error'), [ - devMessage, - ]); - errorCall[SEEN_SYMBOL] = true; - parentStatementPath.replaceWith( - t.ifStatement( - t.unaryExpression('!', condition), - t.blockStatement([t.throwStatement(errorCall)]) - ) - ); - 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] - ); - - const errorCall = t.callExpression(t.identifier('Error'), [ - t.conditionalExpression(DEV_EXPRESSION, devMessage, prodMessage), - ]); - errorCall[SEEN_SYMBOL] = true; - - // 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(errorCall)]), - ]) - ) - ); - } }, }, }; diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index 072071be07fbd..d7a5a2cdab6da 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -13,9 +13,6 @@ const pathToBabel = path.join( '../..', 'package.json' ); -const pathToBabelPluginDevWithCode = require.resolve( - '../error-codes/transform-error-messages' -); const pathToBabelPluginReplaceConsoleCalls = require.resolve( '../babel/transform-replace-console-calls' ); @@ -36,8 +33,6 @@ const babelOptions = { // For Node environment only. For builds, Rollup takes care of ESM. require.resolve('@babel/plugin-transform-modules-commonjs'), - pathToBabelPluginDevWithCode, - // Keep stacks detailed in tests. // Don't put this in .babelrc so that we don't embed filenames // into ReactART builds that include JSX. @@ -105,7 +100,6 @@ module.exports = { __filename, pathToBabel, pathToBabelrc, - pathToBabelPluginDevWithCode, pathToTransformInfiniteLoops, pathToTransformTestGatePragma, pathToErrorCodes, diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 7e29c0e8c54d3..ea797cb2d8d0d 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -176,26 +176,13 @@ function getBabelConfig( if (updateBabelOptions) { 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) { - 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_PROD: case UMD_PROFILING: @@ -206,8 +193,6 @@ function getBabelConfig( plugins: options.plugins.concat([ // Use object-assign polyfill in open source path.resolve('./scripts/babel/transform-object-assign-require'), - // Minify invariant messages - require('../error-codes/transform-error-messages'), ]), }); default: From 9e61bbb0b259a14a2e596f1c2c2e2fbe7fceff8d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Oct 2021 10:08:08 -0400 Subject: [PATCH 3/7] 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. --- .../__tests__/__snapshots__/transform-error-messages.js.snap | 4 ++-- scripts/error-codes/transform-error-messages.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap index c9c6ba8500f52..1eb0c4598311c 100644 --- a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap +++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap @@ -11,9 +11,9 @@ 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!*/ +"/*! FIXME (minify-errors-in-prod): Unminified error message in production build!*/ -/*Add to error code map: This is not a real error message.*/ +/*! Add to error code map: This is not a real error message.*/ Error('This is not a real error message.');" `; diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index eaaf6df497726..56f19e9dbd070 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -83,11 +83,11 @@ module.exports = function(babel) { statementParent.addComment( 'leading', - 'Add to error code map: ' + errorMsgLiteral + '! Add to error code map: ' + errorMsgLiteral ); statementParent.addComment( 'leading', - 'FIXME (minify-errors-in-prod): Unminified error message in production build!' + '! FIXME (minify-errors-in-prod): Unminified error message in production build!' ); return; } From 7bb6b696d18debe3d2bf60aa578b505093934d82 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Oct 2021 11:55:53 -0400 Subject: [PATCH 4/7] 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. --- package.json | 1 + .../transform-error-messages.js.snap | 2 +- scripts/error-codes/extract-errors2.js | 74 +++++++++++++++++++ .../error-codes/transform-error-messages.js | 4 +- 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 scripts/error-codes/extract-errors2.js diff --git a/package.json b/package.json index e7471a686ce88..7065775e249ee 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "lint": "node ./scripts/tasks/eslint.js", "lint-build": "node ./scripts/rollup/validate/index.js", "extract-errors": "yarn build --type=dev --extract-errors", + "extract-errors2": "node scripts/error-codes/extract-errors2.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'", "test": "node ./scripts/jest/jest-cli.js", diff --git a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap index 1eb0c4598311c..97870a4b31b8f 100644 --- a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap +++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap @@ -13,7 +13,7 @@ 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!*/ -/*! Add to error code map: This is not a real error message.*/ +/*! \\"This is not a real error message.\\"*/ Error('This is not a real error message.');" `; diff --git a/scripts/error-codes/extract-errors2.js b/scripts/error-codes/extract-errors2.js new file mode 100644 index 0000000000000..addb095b4ca1f --- /dev/null +++ b/scripts/error-codes/extract-errors2.js @@ -0,0 +1,74 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const {execSync} = require('child_process'); + +async function main() { + const originalJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '../error-codes/codes.json')) + ); + const existingMessages = new Set(); + const codes = Object.keys(originalJSON); + let nextCode = 0; + for (let i = 0; i < codes.length; i++) { + const codeStr = codes[i]; + const message = originalJSON[codeStr]; + const code = parseInt(codeStr, 10); + existingMessages.add(message); + if (code >= nextCode) { + nextCode = code + 1; + } + } + + console.log('Searching `build` directory for unminified errors...\n'); + + let out; + try { + out = execSync( + "git --no-pager grep -n --untracked --no-exclude-standard '/*! ' -- build" + ).toString(); + } catch (e) { + if (e.status === 1 && e.stdout.toString() === '') { + // No unminified errors found. + return; + } + throw e; + } + + let newJSON = null; + const regex = /\"(.+?)"\<\/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( + path.resolve(__dirname, '../error-codes/codes.json'), + JSON.stringify(newJSON, null, 2) + ); + } +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index 56f19e9dbd070..a429ed4008b68 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -57,7 +57,7 @@ module.exports = function(babel) { // // Outputs: // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ - // /* Add to error code map: "A % message that contains %" */ + // /* "A % message that contains %" */ // if (!condition) { // throw Error(`A ${adj} message that contains ${noun}`); // } @@ -83,7 +83,7 @@ module.exports = function(babel) { statementParent.addComment( 'leading', - '! Add to error code map: ' + errorMsgLiteral + `! "${errorMsgLiteral}"` ); statementParent.addComment( 'leading', From bf07c910b3cbc60ee9ff3dea09e303f3d9a26baa Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Oct 2021 13:01:29 -0400 Subject: [PATCH 5/7] Disable error minification in more bundles Not worth it because the number of errors does not outweight the size of the formatProdErrorMessage runtime. --- scripts/rollup/bundles.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index af7b30b7feb99..40ae43ae7cbc0 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -141,7 +141,7 @@ const bundles = [ moduleType: ISOMORPHIC, entry: 'react-fetch/index.browser', global: 'ReactFetch', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -163,7 +163,7 @@ const bundles = [ moduleType: ISOMORPHIC, entry: 'react-fs/index.browser.server', global: 'ReactFilesystem', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: [], }, @@ -185,7 +185,7 @@ const bundles = [ moduleType: ISOMORPHIC, entry: 'react-pg/index.browser.server', global: 'ReactPostgres', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: [], }, @@ -349,7 +349,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-server-dom-webpack', global: 'ReactServerDOMReader', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react'], }, @@ -594,7 +594,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-noop-renderer', global: 'ReactNoopRenderer', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'scheduler/unstable_mock', 'expect'], }, @@ -605,7 +605,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-noop-renderer/persistent', global: 'ReactNoopRendererPersistent', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'expect'], }, @@ -616,7 +616,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-noop-renderer/server', global: 'ReactNoopRendererServer', - minifyWithProdErrorCodes: true, + minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: ['react', 'scheduler', 'expect'], }, From 24feb6368885468008269fe1ececdfcc3b9017c3 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Oct 2021 17:56:35 -0400 Subject: [PATCH 6/7] 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. --- .circleci/config.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d1eb1e8b3381..f3a2d6e9b9f66 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -209,7 +209,20 @@ jobs: - run: yarn workspaces info | head -n -1 > workspace_info.txt - *restore_node_modules - 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-errors2 + 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: docker: *docker @@ -414,6 +427,9 @@ workflows: - yarn_lint_build: requires: - yarn_build_combined + - check_error_codes: + requires: + - yarn_build_combined - RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: requires: - yarn_build_combined From b2e38a94937dd653d95620caadc9ceb687daa8df Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Oct 2021 18:00:32 -0400 Subject: [PATCH 7/7] Replace old extract-errors script with new one Deletes the old extract-errors in favor of extract-errors2 --- .circleci/config.yml | 2 +- package.json | 3 +- scripts/error-codes/README.md | 5 +- scripts/error-codes/extract-errors.js | 143 ++++++++++--------------- scripts/error-codes/extract-errors2.js | 74 ------------- scripts/rollup/build.js | 23 +--- 6 files changed, 63 insertions(+), 187 deletions(-) delete mode 100644 scripts/error-codes/extract-errors2.js diff --git a/.circleci/config.yml b/.circleci/config.yml index f3a2d6e9b9f66..8c847fc05f39e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -221,7 +221,7 @@ jobs: - run: name: Search build artifacts for unminified errors command: | - yarn extract-errors2 + 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: diff --git a/package.json b/package.json index 7065775e249ee..e4ea1c83be801 100644 --- a/package.json +++ b/package.json @@ -114,8 +114,7 @@ "linc": "node ./scripts/tasks/linc.js", "lint": "node ./scripts/tasks/eslint.js", "lint-build": "node ./scripts/rollup/validate/index.js", - "extract-errors": "yarn build --type=dev --extract-errors", - "extract-errors2": "node scripts/error-codes/extract-errors2.js", + "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", "debug-test": "yarn test --deprecated 'yarn test --debug'", "test": "node ./scripts/jest/jest-cli.js", diff --git a/scripts/error-codes/README.md b/scripts/error-codes/README.md index 9933e9903d1ea..38918bd42a52e 100644 --- a/scripts/error-codes/README.md +++ b/scripts/error-codes/README.md @@ -9,7 +9,10 @@ provide a better debugging support in production. Check out the blog post the file will never be changed/removed. - [`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 - 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) is a Babel pass that rewrites error messages to IDs for a production (minified) build. diff --git a/scripts/error-codes/extract-errors.js b/scripts/error-codes/extract-errors.js index d60ffe308cdbe..addb095b4ca1f 100644 --- a/scripts/error-codes/extract-errors.js +++ b/scripts/error-codes/extract-errors.js @@ -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'; -const parser = require('@babel/parser'); const fs = require('fs'); const path = require('path'); -const traverse = require('@babel/traverse').default; -const {evalStringConcat} = require('../shared/evalToString'); -const invertObject = require('./invertObject'); +const {execSync} = require('child_process'); -const babylonOptions = { - sourceType: 'module', - // As a parser, babylon has its own options and we can't directly - // import/require a babel preset. It should be kept **the same** as - // the `babel-plugin-syntax-*` ones specified in - // https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js - plugins: [ - 'classProperties', - 'flow', - 'jsx', - 'trailingFunctionCommas', - 'objectRestSpread', - ], -}; - -module.exports = function(opts) { - if (!opts || !('errorMapFilePath' in opts)) { - throw new Error( - 'Missing options. Ensure you pass an object with `errorMapFilePath`.' - ); +async function main() { + const originalJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '../error-codes/codes.json')) + ); + const existingMessages = new Set(); + const codes = Object.keys(originalJSON); + let nextCode = 0; + for (let i = 0; i < codes.length; i++) { + const codeStr = codes[i]; + const message = originalJSON[codeStr]; + const code = parseInt(codeStr, 10); + existingMessages.add(message); + if (code >= nextCode) { + nextCode = code + 1; + } } - const errorMapFilePath = opts.errorMapFilePath; - let existingErrorMap; + console.log('Searching `build` directory for unminified errors...\n'); + + let out; try { - // Using `fs.readFileSync` instead of `require` here, because `require()` - // calls are cached, and the cache map is not properly invalidated after - // file changes. - existingErrorMap = JSON.parse( - fs.readFileSync( - path.join(__dirname, path.basename(errorMapFilePath)), - 'utf8' - ) - ); + out = execSync( + "git --no-pager grep -n --untracked --no-exclude-standard '/*! ' -- build" + ).toString(); } catch (e) { - existingErrorMap = {}; - } - - const allErrorIDs = Object.keys(existingErrorMap); - let currentID; - - if (allErrorIDs.length === 0) { - // Map is empty - currentID = 0; - } else { - currentID = Math.max.apply(null, allErrorIDs) + 1; + if (e.status === 1 && e.stdout.toString() === '') { + // No unminified errors found. + return; + } + throw e; } - // 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; + let newJSON = null; + const regex = /\"(.+?)"\<\/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); - // 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; + // Add to json map + if (newJSON === null) { + newJSON = Object.assign({}, originalJSON); + } + console.log(`"${nextCode}": "${message}"`); + newJSON[nextCode] = message; + nextCode += 1; } - existingErrorMap[errorMsgLiteral] = '' + currentID++; - } + } while (true); - function flush(cb) { + if (newJSON) { fs.writeFileSync( - errorMapFilePath, - JSON.stringify(invertObject(existingErrorMap), null, 2) + '\n', - 'utf-8' + path.resolve(__dirname, '../error-codes/codes.json'), + JSON.stringify(newJSON, null, 2) ); } +} - return function extractErrors(source) { - transform(source); - flush(); - }; -}; +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/error-codes/extract-errors2.js b/scripts/error-codes/extract-errors2.js deleted file mode 100644 index addb095b4ca1f..0000000000000 --- a/scripts/error-codes/extract-errors2.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const {execSync} = require('child_process'); - -async function main() { - const originalJSON = JSON.parse( - fs.readFileSync(path.resolve(__dirname, '../error-codes/codes.json')) - ); - const existingMessages = new Set(); - const codes = Object.keys(originalJSON); - let nextCode = 0; - for (let i = 0; i < codes.length; i++) { - const codeStr = codes[i]; - const message = originalJSON[codeStr]; - const code = parseInt(codeStr, 10); - existingMessages.add(message); - if (code >= nextCode) { - nextCode = code + 1; - } - } - - console.log('Searching `build` directory for unminified errors...\n'); - - let out; - try { - out = execSync( - "git --no-pager grep -n --untracked --no-exclude-standard '/*! ' -- build" - ).toString(); - } catch (e) { - if (e.status === 1 && e.stdout.toString() === '') { - // No unminified errors found. - return; - } - throw e; - } - - let newJSON = null; - const regex = /\"(.+?)"\<\/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( - path.resolve(__dirname, '../error-codes/codes.json'), - JSON.stringify(newJSON, null, 2) - ); - } -} - -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index ea797cb2d8d0d..48401c002b431 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -19,7 +19,6 @@ const Sync = require('./sync'); const sizes = require('./plugins/sizes-plugin'); const useForks = require('./plugins/use-forks-plugin'); const stripUnusedImports = require('./plugins/strip-unused-imports'); -const extractErrorCodes = require('../error-codes/extract-errors'); const Packaging = require('./packaging'); const {asyncRimRaf} = require('./utils'); const codeFrame = require('babel-code-frame'); @@ -94,10 +93,6 @@ const forcePrettyOutput = argv.pretty; const isWatchMode = argv.watch; const syncFBSourcePath = argv['sync-fbsource']; const syncWWWPath = argv['sync-www']; -const shouldExtractErrors = argv['extract-errors']; -const errorCodeOpts = { - errorMapFilePath: 'scripts/error-codes/codes.json', -}; const closureOptions = { compilation_level: 'SIMPLE', @@ -324,7 +319,6 @@ function getPlugins( pureExternalModules, bundle ) { - const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts); const forks = Modules.getForks(bundleType, entry, moduleType, bundle); const isProduction = isProductionBundleType(bundleType); const isProfiling = isProfilingBundleType(bundleType); @@ -345,13 +339,6 @@ function getPlugins( bundleType === RN_FB_PROFILING; const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput; 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. useForks(forks), // Ensure we don't try to bundle any fbjs modules. @@ -747,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. const nodeTotal = parseInt(process.env.CIRCLE_NODE_TOTAL, 10); const nodeIndex = parseInt(process.env.CIRCLE_NODE_INDEX, 10); @@ -772,14 +759,6 @@ async function buildEverything() { if (!forcePrettyOutput) { 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();