7
'use strict';
8
9
const fs = require('fs');
10
-const {
11
- evalStringConcat,
12
- evalStringAndTemplateConcat,
13
-} = require('../shared/evalToString');
10
+const {evalStringAndTemplateConcat} = require('../shared/evalToString');
11
const invertObject = require('./invertObject');
12
const helperModuleImports = require('@babel/helper-module-imports');
13
20
module.exports = function(babel) {
21
const t = babel.types;
22
26
- // TODO: Instead of outputting __DEV__ conditions, only apply this transform
27
- // in production.
28
- const DEV_EXPRESSION = t.identifier('__DEV__');
29
-
30
- function CallOrNewExpression(path, file) {
23
+ function ErrorCallExpression(path, file) {
24
// Turns this code:
25
//
26
// new Error(`A ${adj} message that contains ${noun}`);
31
//
32
// into this:
33
//
41
- // Error(
42
- // __DEV__
43
- // ? `A ${adj} message that contains ${noun}`
44
- // : formatProdErrorMessage(ERR_CODE, adj, noun)
45
- // );
34
+ // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
35
const node = path.node;
36
if (node[SEEN_SYMBOL]) {
37
return;
51
52
let prodErrorId = errorMap[errorMsgLiteral];
53
if (prodErrorId === undefined) {
65
- // There is no error code for this message. We use a lint rule to
66
- // enforce that messages can be minified, so assume this is
67
- // intentional and exit gracefully.
54
+ // There is no error code for this message. Add an inline comment
55
+ // that flags this as an unminified error. This allows the build
56
+ // to proceed, while also allowing a post-build linter to detect it.
57
+ //
58
+ // Outputs:
59
+ // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
60
+ // /* <expected-error-format>"A % message that contains %"</expected-error-format> */
61
+ // if (!condition) {
62
+ // throw Error(`A ${adj} message that contains ${noun}`);
63
+ // }
64
+
65
+ const statementParent = path.getStatementParent();
66
+ const leadingComments = statementParent.node.leadingComments;
67
+ if (leadingComments !== undefined) {
68
+ for (let i = 0; i < leadingComments.length; i++) {
69
+ // TODO: Since this only detects one of many ways to disable a lint
70
+ // rule, we should instead search for a custom directive (like
71
+ // no-minify-errors) instead of ESLint. Will need to update our lint
72
+ // rule to recognize the same directive.
73
+ const commentText = leadingComments[i].value;
74
+ if (
75
+ commentText.includes(
76
+ 'eslint-disable-next-line react-internal/prod-error-codes'
77
+ )
78
+ ) {
79
+ return;
80
+ }
81
+ }
82
+ }
83
+
84
+ statementParent.addComment(
85
+ 'leading',
86
+ `! <expected-error-format>"${errorMsgLiteral}"</expected-error-format>`
87
+ );
88
+ statementParent.addComment(
89
+ 'leading',
90
+ '! FIXME (minify-errors-in-prod): Unminified error message in production build!'
91
+ );
92
return;
93
}
94
prodErrorId = parseInt(prodErrorId, 10);
108
]);
109
110
// Outputs:
87
- // Error(
88
- // __DEV__
89
- // ? `A ${adj} message that contains ${noun}`
90
- // : formatProdErrorMessage(ERR_CODE, adj, noun)
91
- // );
92
- path.replaceWith(t.callExpression(t.identifier('Error'), [prodMessage]));
93
- path.replaceWith(
94
- t.callExpression(t.identifier('Error'), [
95
- t.conditionalExpression(DEV_EXPRESSION, errorMsgNode, prodMessage),
96
- ])
97
- );
111
+ // Error(formatProdErrorMessage(ERR_CODE, adj, noun));
112
+ const newErrorCall = t.callExpression(t.identifier('Error'), [prodMessage]);
113
+ newErrorCall[SEEN_SYMBOL] = true;
114
+ path.replaceWith(newErrorCall);
115
}
116
117
return {
118
visitor: {
119
NewExpression(path, file) {
103
- const noMinify = file.opts.noMinify;
104
- if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) {
105
- CallOrNewExpression(path, file);
120
+ if (path.get('callee').isIdentifier({name: 'Error'})) {
121
+ ErrorCallExpression(path, file);
122
}
123
},
124
125
CallExpression(path, file) {
110
- const node = path.node;
111
- const noMinify = file.opts.noMinify;
112
-
113
- if (!noMinify && path.get('callee').isIdentifier({name: 'Error'})) {
114
- CallOrNewExpression(path, file);
126
+ if (path.get('callee').isIdentifier({name: 'Error'})) {
127
+ ErrorCallExpression(path, file);
128
return;
129
}
117
-
118
- if (path.get('callee').isIdentifier({name: 'invariant'})) {
119
- // Turns this code:
120
- //
121
- // invariant(condition, 'A %s message that contains %s', adj, noun);
122
- //
123
- // into this:
124
- //
125
- // if (!condition) {
126
- // throw Error(
127
- // __DEV__
128
- // ? `A ${adj} message that contains ${noun}`
129
- // : formatProdErrorMessage(ERR_CODE, adj, noun)
130
- // );
131
- // }
132
- //
133
- // where ERR_CODE is an error code: a unique identifier (a number
134
- // string) that references a verbose error message. The mapping is
135
- // stored in `scripts/error-codes/codes.json`.
136
- const condition = node.arguments[0];
137
- const errorMsgLiteral = evalStringConcat(node.arguments[1]);
138
- const errorMsgExpressions = Array.from(node.arguments.slice(2));
139
- const errorMsgQuasis = errorMsgLiteral
140
- .split('%s')
141
- .map(raw => t.templateElement({raw, cooked: String.raw({raw})}));
142
-
143
- // Outputs:
144
- // `A ${adj} message that contains ${noun}`;
145
- const devMessage = t.templateLiteral(
146
- errorMsgQuasis,
147
- errorMsgExpressions
148
- );
149
-
150
- const parentStatementPath = path.parentPath;
151
- if (parentStatementPath.type !== 'ExpressionStatement') {
152
- throw path.buildCodeFrameError(
153
- 'invariant() cannot be called from expression context. Move ' +
154
- 'the call to its own statement.'
155
- );
156
- }
157
-
158
- if (noMinify) {
159
- // Error minification is disabled for this build.
160
- //
161
- // Outputs:
162
- // if (!condition) {
163
- // throw Error(`A ${adj} message that contains ${noun}`);
164
- // }
165
- parentStatementPath.replaceWith(
166
- t.ifStatement(
167
- t.unaryExpression('!', condition),
168
- t.blockStatement([
169
- t.throwStatement(
170
- t.callExpression(t.identifier('Error'), [devMessage])
171
- ),
172
- ])
173
- )
174
- );
175
- return;
176
- }
177
-
178
- let prodErrorId = errorMap[errorMsgLiteral];
179
-
180
- if (prodErrorId === undefined) {
181
- // There is no error code for this message. Add an inline comment
182
- // that flags this as an unminified error. This allows the build
183
- // to proceed, while also allowing a post-build linter to detect it.
184
- //
185
- // Outputs:
186
- // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
187
- // if (!condition) {
188
- // throw Error(`A ${adj} message that contains ${noun}`);
189
- // }
190
- parentStatementPath.replaceWith(
191
- t.ifStatement(
192
- t.unaryExpression('!', condition),
193
- t.blockStatement([
194
- t.throwStatement(
195
- t.callExpression(t.identifier('Error'), [devMessage])
196
- ),
197
- ])
198
- )
199
- );
200
- parentStatementPath.addComment(
201
- 'leading',
202
- 'FIXME (minify-errors-in-prod): Unminified error message in production build!'
203
- );
204
- return;
205
- }
206
- prodErrorId = parseInt(prodErrorId, 10);
207
-
208
- // Import formatProdErrorMessage
209
- const formatProdErrorMessageIdentifier = helperModuleImports.addDefault(
210
- path,
211
- 'shared/formatProdErrorMessage',
212
- {nameHint: 'formatProdErrorMessage'}
213
- );
214
-
215
- // Outputs:
216
- // formatProdErrorMessage(ERR_CODE, adj, noun);
217
- const prodMessage = t.callExpression(
218
- formatProdErrorMessageIdentifier,
219
- [t.numericLiteral(prodErrorId), ...errorMsgExpressions]
220
- );
221
-
222
- // Outputs:
223
- // if (!condition) {
224
- // throw Error(
225
- // __DEV__
226
- // ? `A ${adj} message that contains ${noun}`
227
- // : formatProdErrorMessage(ERR_CODE, adj, noun)
228
- // );
229
- // }
230
- parentStatementPath.replaceWith(
231
- t.ifStatement(
232
- t.unaryExpression('!', condition),
233
- t.blockStatement([
234
- t.blockStatement([
235
- t.throwStatement(
236
- t.callExpression(t.identifier('Error'), [
237
- t.conditionalExpression(
238
- DEV_EXPRESSION,
239
- devMessage,
240
- prodMessage
241
- ),
242
- ])
243
- ),
244
- ]),
245
- ])
246
- )
247
- );
248
- }
130
},
131
},
132
};