main
js 154 lines 4.51 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7 'use strict';
8
9 const fs = require('fs');
10 const {evalStringAndTemplateConcat} = require('../shared/evalToString');
11 const invertObject = require('./invertObject');
12 const helperModuleImports = require('@babel/helper-module-imports');
13
14 const errorMap = invertObject(
15 JSON.parse(fs.readFileSync(__dirname + '/codes.json', 'utf-8'))
16 );
17
18 const SEEN_SYMBOL = Symbol('transform-error-messages.seen');
19
20 module.exports = function (babel) {
21 const t = babel.types;
22
23 function ErrorCallExpression(path, file) {
24 // Turns this code:
25 //
26 // new Error(`A ${adj} message that contains ${noun}`);
27 //
28 // or this code (no constructor):
29 //
30 // Error(`A ${adj} message that contains ${noun}`);
31 //
32 // into this:
33 //
34 // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
35 const node = path.node;
36 if (node[SEEN_SYMBOL]) {
37 return;
38 }
39 node[SEEN_SYMBOL] = true;
40
41 const errorMsgNode = node.arguments[0];
42 if (errorMsgNode === undefined) {
43 return;
44 }
45
46 const errorMsgExpressions = [];
47 const errorMsgLiteral = evalStringAndTemplateConcat(
48 errorMsgNode,
49 errorMsgExpressions
50 );
51
52 if (errorMsgLiteral === 'react-stack-top-frame') {
53 // This is a special case for generating stack traces.
54 return;
55 }
56
57 let prodErrorId = errorMap[errorMsgLiteral];
58 if (prodErrorId === undefined) {
59 // There is no error code for this message. Add an inline comment
60 // that flags this as an unminified error. This allows the build
61 // to proceed, while also allowing a post-build linter to detect it.
62 //
63 // Outputs:
64 // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
65 // /* <expected-error-format>"A % message that contains %"</expected-error-format> */
66 // if (!condition) {
67 // throw Error(`A ${adj} message that contains ${noun}`);
68 // }
69
70 let leadingComments = [];
71
72 const statementParent = path.getStatementParent();
73 let nextPath = path;
74 while (true) {
75 let nextNode = nextPath.node;
76 if (nextNode.leadingComments) {
77 leadingComments.push(...nextNode.leadingComments);
78 }
79 if (nextPath === statementParent) {
80 break;
81 }
82 nextPath = nextPath.parentPath;
83 }
84
85 if (leadingComments !== undefined) {
86 for (let i = 0; i < leadingComments.length; i++) {
87 // TODO: Since this only detects one of many ways to disable a lint
88 // rule, we should instead search for a custom directive (like
89 // no-minify-errors) instead of ESLint. Will need to update our lint
90 // rule to recognize the same directive.
91 const commentText = leadingComments[i].value;
92 if (
93 commentText.includes(
94 'eslint-disable-next-line react-internal/prod-error-codes'
95 )
96 ) {
97 return;
98 }
99 }
100 }
101
102 statementParent.addComment(
103 'leading',
104 `! <expected-error-format>"${errorMsgLiteral}"</expected-error-format>`
105 );
106 statementParent.addComment(
107 'leading',
108 '! FIXME (minify-errors-in-prod): Unminified error message in production build!'
109 );
110 return;
111 }
112 prodErrorId = parseInt(prodErrorId, 10);
113
114 // Import formatProdErrorMessage
115 const formatProdErrorMessageIdentifier = helperModuleImports.addDefault(
116 path,
117 'shared/formatProdErrorMessage',
118 {nameHint: 'formatProdErrorMessage'}
119 );
120
121 // Outputs:
122 // formatProdErrorMessage(ERR_CODE, adj, noun);
123 const prodMessage = t.callExpression(formatProdErrorMessageIdentifier, [
124 t.numericLiteral(prodErrorId),
125 ...errorMsgExpressions,
126 ]);
127
128 // Outputs:
129 // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
130 const newErrorCall = t.callExpression(t.identifier('Error'), [
131 prodMessage,
132 ...node.arguments.slice(1),
133 ]);
134 newErrorCall[SEEN_SYMBOL] = true;
135 path.replaceWith(newErrorCall);
136 }
137
138 return {
139 visitor: {
140 NewExpression(path, file) {
141 if (path.get('callee').isIdentifier({name: 'Error'})) {
142 ErrorCallExpression(path, file);
143 }
144 },
145
146 CallExpression(path, file) {
147 if (path.get('callee').isIdentifier({name: 'Error'})) {
148 ErrorCallExpression(path, file);
149 return;
150 }
151 },
152 },
153 };
154 };