diff --git a/packages/shared/ReactError.js b/packages/shared/ReactError.js index 2c493f7d81cd..0a5b1ed222d8 100644 --- a/packages/shared/ReactError.js +++ b/packages/shared/ReactError.js @@ -6,12 +6,11 @@ * */ -// Do not require this module directly! Use a normal error constructor with +// Do not require this module directly! Use normal `invariant` calls with // template literal strings. The messages will be converted to ReactError during // build, and in production they will be minified. -function ReactError(message) { - const error = new Error(message); +function ReactError(error) { error.name = 'Invariant Violation'; return error; } diff --git a/packages/shared/ReactErrorProd.js b/packages/shared/ReactErrorProd.js index 3b3996e1366d..b909e2180809 100644 --- a/packages/shared/ReactErrorProd.js +++ b/packages/shared/ReactErrorProd.js @@ -6,20 +6,21 @@ * */ -// Do not require this module directly! Use a normal error constructor with +// Do not require this module directly! Use normal `invariant` calls with // template literal strings. The messages will be converted to ReactError during // build, and in production they will be minified. -function ReactErrorProd(code) { +function ReactErrorProd(error) { + const code = error.message; let url = 'https://reactjs.org/docs/error-decoder.html?invariant=' + code; for (let i = 1; i < arguments.length; i++) { url += '&args[]=' + encodeURIComponent(arguments[i]); } - return new Error( + error.message = `Minified React error #${code}; visit ${url} for the full message or ` + - 'use the non-minified dev environment for full errors and additional ' + - 'helpful warnings. ', - ); + 'use the non-minified dev environment for full errors and additional ' + + 'helpful warnings. '; + return error; } export default ReactErrorProd; diff --git a/packages/shared/__tests__/ReactErrorProd-test.internal.js b/packages/shared/__tests__/ReactErrorProd-test.internal.js index b8984d13d52a..62d6da32c5c2 100644 --- a/packages/shared/__tests__/ReactErrorProd-test.internal.js +++ b/packages/shared/__tests__/ReactErrorProd-test.internal.js @@ -36,7 +36,7 @@ describe('ReactErrorProd', () => { it('should throw with the correct number of `%s`s in the URL', () => { expect(function() { - throw ReactErrorProd(124, 'foo', 'bar'); + throw ReactErrorProd(Error(124), 'foo', 'bar'); }).toThrowError( 'Minified React error #124; visit ' + 'https://reactjs.org/docs/error-decoder.html?invariant=124&args[]=foo&args[]=bar' + @@ -45,7 +45,7 @@ describe('ReactErrorProd', () => { ); expect(function() { - throw ReactErrorProd(20); + throw ReactErrorProd(Error(20)); }).toThrowError( 'Minified React error #20; visit ' + 'https://reactjs.org/docs/error-decoder.html?invariant=20' + @@ -54,7 +54,7 @@ describe('ReactErrorProd', () => { ); expect(function() { - throw ReactErrorProd(77, '
', '&?bar'); + throw ReactErrorProd(Error(77), '
', '&?bar'); }).toThrowError( 'Minified React error #77; visit ' + 'https://reactjs.org/docs/error-decoder.html?invariant=77&args[]=%3Cdiv%3E&args[]=%26%3Fbar' + 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 3887fcc15bc8..7819a3147469 100644 --- a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap +++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap @@ -6,7 +6,7 @@ exports[`error transform should correctly transform invariants that are not in t import invariant from 'shared/invariant'; /*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () { if (!condition) { - throw _ReactError(\`This is not a real error message.\`); + throw _ReactError(Error(\`This is not a real error message.\`)); } })();" `; @@ -17,7 +17,7 @@ exports[`error transform should handle escaped characters 1`] = ` import invariant from 'shared/invariant'; /*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () { if (!condition) { - throw _ReactError(\`What's up?\`); + throw _ReactError(Error(\`What's up?\`)); } })();" `; @@ -30,18 +30,18 @@ import invariant from 'shared/invariant'; (function () { if (!condition) { if (__DEV__) { - throw _ReactError(\`Do not override existing functions.\`); + throw _ReactError(Error(\`Do not override existing functions.\`)); } else { - throw _ReactErrorProd(16); + throw _ReactErrorProd(Error(16)); } } })(); (function () { if (!condition) { if (__DEV__) { - throw _ReactError(\`Do not override existing functions.\`); + throw _ReactError(Error(\`Do not override existing functions.\`)); } else { - throw _ReactErrorProd(16); + throw _ReactErrorProd(Error(16)); } } })();" @@ -55,9 +55,9 @@ import invariant from 'shared/invariant'; (function () { if (!condition) { if (__DEV__) { - throw _ReactError(\`Do not override existing functions.\`); + throw _ReactError(Error(\`Do not override existing functions.\`)); } else { - throw _ReactErrorProd(16); + throw _ReactErrorProd(Error(16)); } } })();" @@ -71,9 +71,9 @@ import invariant from 'shared/invariant'; (function () { if (!condition) { if (__DEV__) { - throw _ReactError(\`Expected a component class, got \${Foo}.\${Bar}\`); + throw _ReactError(Error(\`Expected a component class, got \${Foo}.\${Bar}\`)); } else { - throw _ReactErrorProd(18, Foo, Bar); + throw _ReactErrorProd(Error(18), Foo, Bar); } } })();" @@ -87,9 +87,9 @@ import invariant from 'shared/invariant'; (function () { if (!condition) { if (__DEV__) { - throw _ReactError(\`Expected \${foo} target to be an array; got \${bar}\`); + throw _ReactError(Error(\`Expected \${foo} target to be an array; got \${bar}\`)); } else { - throw _ReactErrorProd(7, foo, bar); + throw _ReactErrorProd(Error(7), foo, bar); } } })();" @@ -101,7 +101,7 @@ exports[`error transform should support noMinify option 1`] = ` import invariant from 'shared/invariant'; (function () { if (!condition) { - throw _ReactError(\`Do not override existing functions.\`); + throw _ReactError(Error(\`Do not override existing functions.\`)); } })();" `; diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index f0331579270e..f18ba3f6ffd4 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -29,9 +29,9 @@ module.exports = function(babel) { // // if (!condition) { // if (__DEV__) { - // throw ReactError(`A ${adj} message that contains ${noun}`); + // throw ReactError(Error(`A ${adj} message that contains ${noun}`)); // } else { - // throw ReactErrorProd(ERR_CODE, adj, noun); + // throw ReactErrorProd(Error(ERR_CODE), adj, noun); // } // } // @@ -53,10 +53,12 @@ module.exports = function(babel) { ); // Outputs: - // throw ReactError(`A ${adj} message that contains ${noun}`); + // throw ReactError(Error(`A ${adj} message that contains ${noun}`)); const devThrow = t.throwStatement( t.callExpression(reactErrorIdentfier, [ - t.templateLiteral(errorMsgQuasis, errorMsgExpressions), + t.callExpression(t.identifier('Error'), [ + t.templateLiteral(errorMsgQuasis, errorMsgExpressions), + ]), ]) ); @@ -65,7 +67,7 @@ module.exports = function(babel) { // // Outputs: // if (!condition) { - // throw ReactError(`A ${adj} message that contains ${noun}`); + // throw ReactError(Error(`A ${adj} message that contains ${noun}`)); // } path.replaceWith( t.ifStatement( @@ -92,7 +94,7 @@ module.exports = function(babel) { // Outputs: // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ // if (!condition) { - // throw ReactError(`A ${adj} message that contains ${noun}`); + // throw ReactError(Error(`A ${adj} message that contains ${noun}`)); // } path.replaceWith( t.ifStatement( @@ -116,10 +118,12 @@ module.exports = function(babel) { ); // Outputs: - // throw ReactErrorProd(ERR_CODE, adj, noun); + // throw ReactErrorProd(Error(ERR_CODE), adj, noun); const prodThrow = t.throwStatement( t.callExpression(reactErrorProdIdentfier, [ - t.numericLiteral(prodErrorId), + t.callExpression(t.identifier('Error'), [ + t.numericLiteral(prodErrorId), + ]), ...errorMsgExpressions, ]) ); @@ -127,9 +131,9 @@ module.exports = function(babel) { // Outputs: // if (!condition) { // if (__DEV__) { - // throw ReactError(`A ${adj} message that contains ${noun}`); + // throw ReactError(Error(`A ${adj} message that contains ${noun}`)); // } else { - // throw ReactErrorProd(ERR_CODE, adj, noun); + // throw ReactErrorProd(Error(ERR_CODE), adj, noun); // } // } path.replaceWith( diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index 92ef339f6728..b2933bc5fba5 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -130,6 +130,10 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { if (process.env.NODE_ENV === 'production') { // In production, we strip error messages and turn them into codes. // This decodes them back so that the test assertions on them work. + // 1. `ErrorProxy` decodes error messages at Error construction time and + // also proxies error instances with `proxyErrorInstance`. + // 2. `proxyErrorInstance` decodes error messages when the `message` + // property is changed. const decodeErrorMessage = function(message) { if (!message) { return message; @@ -150,16 +154,51 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { return format.replace(/%s/g, () => args[argIndex++]); }; const OriginalError = global.Error; + // V8's Error.captureStackTrace (used in Jest) fails if the error object is + // a Proxy, so we need to pass it the unproxied instance. + const originalErrorInstances = new WeakMap(); + const captureStackTrace = function(error, ...args) { + return OriginalError.captureStackTrace.call( + this, + originalErrorInstances.get(error) || + // Sometimes this wrapper receives an already-unproxied instance. + error, + ...args + ); + }; + const proxyErrorInstance = error => { + const proxy = new Proxy(error, { + set(target, key, value, receiver) { + if (key === 'message') { + return Reflect.set( + target, + key, + decodeErrorMessage(value), + receiver + ); + } + return Reflect.set(target, key, value, receiver); + }, + }); + originalErrorInstances.set(proxy, error); + return proxy; + }; const ErrorProxy = new Proxy(OriginalError, { apply(target, thisArg, argumentsList) { const error = Reflect.apply(target, thisArg, argumentsList); error.message = decodeErrorMessage(error.message); - return error; + return proxyErrorInstance(error); }, construct(target, argumentsList, newTarget) { const error = Reflect.construct(target, argumentsList, newTarget); error.message = decodeErrorMessage(error.message); - return error; + return proxyErrorInstance(error); + }, + get(target, key, receiver) { + if (key === 'captureStackTrace') { + return captureStackTrace; + } + return Reflect.get(target, key, receiver); }, }); ErrorProxy.OriginalError = OriginalError;