mirror of
https://github.com/zebrajr/react.git
synced 2025-12-07 00:20:28 +01:00
We basically have four kinds of recoverable errors: - Hydration mismatches. - Server errored but client didn't. - Hydration render errored but client render didn't (in Root or Suspense boundary). - Concurrent render errored but synchronous render didn't. For the first three we log an additional error that the root or Suspense boundary didn't error. This provides some context about what happened. However, the problem is that for hydration mismatches that's unnecessary extra context that is confusing. We also don't log any additional context for concurrent render errors that could recover. This used to be the only recoverable error so it didn't need extra context but now we need to distinguish them. When we log these to `reportError` it's confusing to just see the error because you didn't see anything error on the page. It's also hard to group them together as one. In this PR, I remove the unnecessary context for hydration mismatches. For hydration and concurrent errors, I now wrap them in an error that describes that what happened but then use the new `cause` field to link the original error so we can keep that as the cause. The error that happened was that hydration client rendered or you deopted to sync render, the cause of that error is some other error. For server errors, we control the Error object so I already had added some context to that error object's message. Since we hide the message in prod, it's nice not to have the raw message in DEV neither. We could potentially split these into two errors for parity though.
150 lines
4.4 KiB
JavaScript
150 lines
4.4 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 fs = require('fs');
|
|
const {evalStringAndTemplateConcat} = require('../shared/evalToString');
|
|
const invertObject = require('./invertObject');
|
|
const helperModuleImports = require('@babel/helper-module-imports');
|
|
|
|
const errorMap = invertObject(
|
|
JSON.parse(fs.readFileSync(__dirname + '/codes.json', 'utf-8'))
|
|
);
|
|
|
|
const SEEN_SYMBOL = Symbol('transform-error-messages.seen');
|
|
|
|
module.exports = function (babel) {
|
|
const t = babel.types;
|
|
|
|
function ErrorCallExpression(path, file) {
|
|
// Turns this code:
|
|
//
|
|
// new Error(`A ${adj} message that contains ${noun}`);
|
|
//
|
|
// or this code (no constructor):
|
|
//
|
|
// Error(`A ${adj} message that contains ${noun}`);
|
|
//
|
|
// into this:
|
|
//
|
|
// Error(formatProdErrorMessage(ERR_CODE, adj, noun));
|
|
const node = path.node;
|
|
if (node[SEEN_SYMBOL]) {
|
|
return;
|
|
}
|
|
node[SEEN_SYMBOL] = true;
|
|
|
|
const errorMsgNode = node.arguments[0];
|
|
if (errorMsgNode === undefined) {
|
|
return;
|
|
}
|
|
|
|
const errorMsgExpressions = [];
|
|
const errorMsgLiteral = evalStringAndTemplateConcat(
|
|
errorMsgNode,
|
|
errorMsgExpressions
|
|
);
|
|
|
|
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! */
|
|
// /* <expected-error-format>"A % message that contains %"</expected-error-format> */
|
|
// if (!condition) {
|
|
// throw Error(`A ${adj} message that contains ${noun}`);
|
|
// }
|
|
|
|
let leadingComments = [];
|
|
|
|
const statementParent = path.getStatementParent();
|
|
let nextPath = path;
|
|
while (true) {
|
|
let nextNode = nextPath.node;
|
|
if (nextNode.leadingComments) {
|
|
leadingComments.push(...nextNode.leadingComments);
|
|
}
|
|
if (nextPath === statementParent) {
|
|
break;
|
|
}
|
|
nextPath = nextPath.parentPath;
|
|
}
|
|
|
|
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;
|
|
}
|
|
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:
|
|
// Error(formatProdErrorMessage(ERR_CODE, adj, noun));
|
|
const newErrorCall = t.callExpression(t.identifier('Error'), [
|
|
prodMessage,
|
|
...node.arguments.slice(1),
|
|
]);
|
|
newErrorCall[SEEN_SYMBOL] = true;
|
|
path.replaceWith(newErrorCall);
|
|
}
|
|
|
|
return {
|
|
visitor: {
|
|
NewExpression(path, file) {
|
|
if (path.get('callee').isIdentifier({name: 'Error'})) {
|
|
ErrorCallExpression(path, file);
|
|
}
|
|
},
|
|
|
|
CallExpression(path, file) {
|
|
if (path.get('callee').isIdentifier({name: 'Error'})) {
|
|
ErrorCallExpression(path, file);
|
|
return;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
};
|