@samitouri / QOS-React-2 / commits / 98454371a9

Construct Error at invariant call site for clearer stack traces (#15877)

Moti Zilberman committed Jun 18, 2019 at 19:38 UTC 98454371a995d73a6cb2f3f4770c13945fb78ea4
6 files changed +80 -37
packages/shared/ReactError.js
+2 -3
@@ -6,12 +6,11 @@
6 *
7 */
8
9 -// Do not require this module directly! Use a normal error constructor with
9 +// Do not require this module directly! Use normal `invariant` calls with
10 // template literal strings. The messages will be converted to ReactError during
11 // build, and in production they will be minified.
12
13 -function ReactError(message) {
14 - const error = new Error(message);
13 +function ReactError(error) {
14 error.name = 'Invariant Violation';
15 return error;
16 }
packages/shared/ReactErrorProd.js
+7 -6
@@ -6,20 +6,21 @@
6 *
7 */
8
9 -// Do not require this module directly! Use a normal error constructor with
9 +// Do not require this module directly! Use normal `invariant` calls with
10 // template literal strings. The messages will be converted to ReactError during
11 // build, and in production they will be minified.
12
13 -function ReactErrorProd(code) {
13 +function ReactErrorProd(error) {
14 + const code = error.message;
15 let url = 'https://reactjs.org/docs/error-decoder.html?invariant=' + code;
16 for (let i = 1; i < arguments.length; i++) {
17 url += '&args[]=' + encodeURIComponent(arguments[i]);
18 }
18 - return new Error(
19 + error.message =
20 `Minified React error #${code}; visit ${url} for the full message or ` +
20 - 'use the non-minified dev environment for full errors and additional ' +
21 - 'helpful warnings. ',
22 - );
21 + 'use the non-minified dev environment for full errors and additional ' +
22 + 'helpful warnings. ';
23 + return error;
24 }
25
26 export default ReactErrorProd;
packages/shared/__tests__/ReactErrorProd-test.internal.js
+3 -3
@@ -36,7 +36,7 @@ describe('ReactErrorProd', () => {
36
37 it('should throw with the correct number of `%s`s in the URL', () => {
38 expect(function() {
39 - throw ReactErrorProd(124, 'foo', 'bar');
39 + throw ReactErrorProd(Error(124), 'foo', 'bar');
40 }).toThrowError(
41 'Minified React error #124; visit ' +
42 'https://reactjs.org/docs/error-decoder.html?invariant=124&args[]=foo&args[]=bar' +
@@ -45,7 +45,7 @@ describe('ReactErrorProd', () => {
45 );
46
47 expect(function() {
48 - throw ReactErrorProd(20);
48 + throw ReactErrorProd(Error(20));
49 }).toThrowError(
50 'Minified React error #20; visit ' +
51 'https://reactjs.org/docs/error-decoder.html?invariant=20' +
@@ -54,7 +54,7 @@ describe('ReactErrorProd', () => {
54 );
55
56 expect(function() {
57 - throw ReactErrorProd(77, '<div>', '&?bar');
57 + throw ReactErrorProd(Error(77), '<div>', '&?bar');
58 }).toThrowError(
59 'Minified React error #77; visit ' +
60 'https://reactjs.org/docs/error-decoder.html?invariant=77&args[]=%3Cdiv%3E&args[]=%26%3Fbar' +
scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap
+13 -13
@@ -6,7 +6,7 @@ exports[`error transform should correctly transform invariants that are not in t
6 import invariant from 'shared/invariant';
7 /*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () {
8 if (!condition) {
9 - throw _ReactError(\`This is not a real error message.\`);
9 + throw _ReactError(Error(\`This is not a real error message.\`));
10 }
11 })();"
12 `;
@@ -17,7 +17,7 @@ exports[`error transform should handle escaped characters 1`] = `
17 import invariant from 'shared/invariant';
18 /*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () {
19 if (!condition) {
20 - throw _ReactError(\`What's up?\`);
20 + throw _ReactError(Error(\`What's up?\`));
21 }
22 })();"
23 `;
@@ -30,18 +30,18 @@ import invariant from 'shared/invariant';
30 (function () {
31 if (!condition) {
32 if (__DEV__) {
33 - throw _ReactError(\`Do not override existing functions.\`);
33 + throw _ReactError(Error(\`Do not override existing functions.\`));
34 } else {
35 - throw _ReactErrorProd(16);
35 + throw _ReactErrorProd(Error(16));
36 }
37 }
38 })();
39 (function () {
40 if (!condition) {
41 if (__DEV__) {
42 - throw _ReactError(\`Do not override existing functions.\`);
42 + throw _ReactError(Error(\`Do not override existing functions.\`));
43 } else {
44 - throw _ReactErrorProd(16);
44 + throw _ReactErrorProd(Error(16));
45 }
46 }
47 })();"
@@ -55,9 +55,9 @@ import invariant from 'shared/invariant';
55 (function () {
56 if (!condition) {
57 if (__DEV__) {
58 - throw _ReactError(\`Do not override existing functions.\`);
58 + throw _ReactError(Error(\`Do not override existing functions.\`));
59 } else {
60 - throw _ReactErrorProd(16);
60 + throw _ReactErrorProd(Error(16));
61 }
62 }
63 })();"
@@ -71,9 +71,9 @@ import invariant from 'shared/invariant';
71 (function () {
72 if (!condition) {
73 if (__DEV__) {
74 - throw _ReactError(\`Expected a component class, got \${Foo}.\${Bar}\`);
74 + throw _ReactError(Error(\`Expected a component class, got \${Foo}.\${Bar}\`));
75 } else {
76 - throw _ReactErrorProd(18, Foo, Bar);
76 + throw _ReactErrorProd(Error(18), Foo, Bar);
77 }
78 }
79 })();"
@@ -87,9 +87,9 @@ import invariant from 'shared/invariant';
87 (function () {
88 if (!condition) {
89 if (__DEV__) {
90 - throw _ReactError(\`Expected \${foo} target to be an array; got \${bar}\`);
90 + throw _ReactError(Error(\`Expected \${foo} target to be an array; got \${bar}\`));
91 } else {
92 - throw _ReactErrorProd(7, foo, bar);
92 + throw _ReactErrorProd(Error(7), foo, bar);
93 }
94 }
95 })();"
@@ -101,7 +101,7 @@ exports[`error transform should support noMinify option 1`] = `
101 import invariant from 'shared/invariant';
102 (function () {
103 if (!condition) {
104 - throw _ReactError(\`Do not override existing functions.\`);
104 + throw _ReactError(Error(\`Do not override existing functions.\`));
105 }
106 })();"
107 `;
scripts/error-codes/transform-error-messages.js
+14 -10
@@ -29,9 +29,9 @@ module.exports = function(babel) {
29 //
30 // if (!condition) {
31 // if (__DEV__) {
32 - // throw ReactError(`A ${adj} message that contains ${noun}`);
32 + // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
33 // } else {
34 - // throw ReactErrorProd(ERR_CODE, adj, noun);
34 + // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
35 // }
36 // }
37 //
@@ -53,10 +53,12 @@ module.exports = function(babel) {
53 );
54
55 // Outputs:
56 - // throw ReactError(`A ${adj} message that contains ${noun}`);
56 + // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
57 const devThrow = t.throwStatement(
58 t.callExpression(reactErrorIdentfier, [
59 - t.templateLiteral(errorMsgQuasis, errorMsgExpressions),
59 + t.callExpression(t.identifier('Error'), [
60 + t.templateLiteral(errorMsgQuasis, errorMsgExpressions),
61 + ]),
62 ])
63 );
64
@@ -65,7 +67,7 @@ module.exports = function(babel) {
67 //
68 // Outputs:
69 // if (!condition) {
68 - // throw ReactError(`A ${adj} message that contains ${noun}`);
70 + // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
71 // }
72 path.replaceWith(
73 t.ifStatement(
@@ -92,7 +94,7 @@ module.exports = function(babel) {
94 // Outputs:
95 // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
96 // if (!condition) {
95 - // throw ReactError(`A ${adj} message that contains ${noun}`);
97 + // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
98 // }
99 path.replaceWith(
100 t.ifStatement(
@@ -116,10 +118,12 @@ module.exports = function(babel) {
118 );
119
120 // Outputs:
119 - // throw ReactErrorProd(ERR_CODE, adj, noun);
121 + // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
122 const prodThrow = t.throwStatement(
123 t.callExpression(reactErrorProdIdentfier, [
122 - t.numericLiteral(prodErrorId),
124 + t.callExpression(t.identifier('Error'), [
125 + t.numericLiteral(prodErrorId),
126 + ]),
127 ...errorMsgExpressions,
128 ])
129 );
@@ -127,9 +131,9 @@ module.exports = function(babel) {
131 // Outputs:
132 // if (!condition) {
133 // if (__DEV__) {
130 - // throw ReactError(`A ${adj} message that contains ${noun}`);
134 + // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
135 // } else {
132 - // throw ReactErrorProd(ERR_CODE, adj, noun);
136 + // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
137 // }
138 // }
139 path.replaceWith(
scripts/jest/setupTests.js
+41 -2
@@ -130,6 +130,10 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
130 if (process.env.NODE_ENV === 'production') {
131 // In production, we strip error messages and turn them into codes.
132 // This decodes them back so that the test assertions on them work.
133 + // 1. `ErrorProxy` decodes error messages at Error construction time and
134 + // also proxies error instances with `proxyErrorInstance`.
135 + // 2. `proxyErrorInstance` decodes error messages when the `message`
136 + // property is changed.
137 const decodeErrorMessage = function(message) {
138 if (!message) {
139 return message;
@@ -150,16 +154,51 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
154 return format.replace(/%s/g, () => args[argIndex++]);
155 };
156 const OriginalError = global.Error;
157 + // V8's Error.captureStackTrace (used in Jest) fails if the error object is
158 + // a Proxy, so we need to pass it the unproxied instance.
159 + const originalErrorInstances = new WeakMap();
160 + const captureStackTrace = function(error, ...args) {
161 + return OriginalError.captureStackTrace.call(
162 + this,
163 + originalErrorInstances.get(error) ||
164 + // Sometimes this wrapper receives an already-unproxied instance.
165 + error,
166 + ...args
167 + );
168 + };
169 + const proxyErrorInstance = error => {
170 + const proxy = new Proxy(error, {
171 + set(target, key, value, receiver) {
172 + if (key === 'message') {
173 + return Reflect.set(
174 + target,
175 + key,
176 + decodeErrorMessage(value),
177 + receiver
178 + );
179 + }
180 + return Reflect.set(target, key, value, receiver);
181 + },
182 + });
183 + originalErrorInstances.set(proxy, error);
184 + return proxy;
185 + };
186 const ErrorProxy = new Proxy(OriginalError, {
187 apply(target, thisArg, argumentsList) {
188 const error = Reflect.apply(target, thisArg, argumentsList);
189 error.message = decodeErrorMessage(error.message);
157 - return error;
190 + return proxyErrorInstance(error);
191 },
192 construct(target, argumentsList, newTarget) {
193 const error = Reflect.construct(target, argumentsList, newTarget);
194 error.message = decodeErrorMessage(error.message);
162 - return error;
195 + return proxyErrorInstance(error);
196 + },
197 + get(target, key, receiver) {
198 + if (key === 'captureStackTrace') {
199 + return captureStackTrace;
200 + }
201 + return Reflect.get(target, key, receiver);
202 },
203 });
204 ErrorProxy.OriginalError = OriginalError;