@mui/internal-babel-plugin-minify-errors 2.0.8-canary.2 → 2.0.8-canary.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +223 -146
- package/index.test.js +161 -59
- package/package.json +9 -9
package/index.js
CHANGED
|
@@ -26,136 +26,203 @@ function pathToNodeImportSpecifier(importPath) {
|
|
|
26
26
|
|
|
27
27
|
const COMMENT_OPT_IN_MARKER = 'minify-error';
|
|
28
28
|
const COMMENT_OPT_OUT_MARKER = 'minify-error-disabled';
|
|
29
|
+
const SUPPORTED_ERROR_CONSTRUCTORS = new Set(['Error', 'TypeError']);
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* @typedef {import('@babel/core')} babel
|
|
32
33
|
*/
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* @typedef {babel.PluginPass & {updatedErrorCodes?: boolean, formatErrorMessageIdentifier?: babel.types.Identifier}} PluginState
|
|
36
36
|
* @typedef {'annotate' | 'throw' | 'write'} MissingError
|
|
37
|
+
* @typedef {babel.PluginPass & {formatErrorMessageIdentifier?: babel.types.Identifier, processedNodes?: WeakSet<babel.types.Node>}} PluginState
|
|
37
38
|
* @typedef {{
|
|
38
|
-
* errorCodesPath
|
|
39
|
-
* missingError: MissingError,
|
|
39
|
+
* errorCodesPath?: string,
|
|
40
40
|
* runtimeModule?: string,
|
|
41
41
|
* detection?: 'opt-in' | 'opt-out',
|
|
42
|
-
* outExtension?: string
|
|
42
|
+
* outExtension?: string,
|
|
43
|
+
* collectErrors?: Set<string | Error>
|
|
43
44
|
* }} Options
|
|
44
45
|
*/
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
+
* `collectErrors` - When provided, the plugin collects error messages into this Set
|
|
49
|
+
* instead of transforming the code. The caller typically passes the same Set instance
|
|
50
|
+
* across multiple plugin invocations (e.g., when processing multiple files), and the
|
|
51
|
+
* plugin is expected to mutate the Set by adding entries during traversal.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a node is `process.env.NODE_ENV` using Babel types.
|
|
48
56
|
* @param {babel.types} t
|
|
49
57
|
* @param {babel.types.Node} node
|
|
50
|
-
* @returns {
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function isProcessEnvNodeEnv(t, node) {
|
|
61
|
+
return (
|
|
62
|
+
t.isMemberExpression(node) &&
|
|
63
|
+
t.isMemberExpression(node.object) &&
|
|
64
|
+
t.isIdentifier(node.object.object, { name: 'process' }) &&
|
|
65
|
+
t.isIdentifier(node.object.property, { name: 'env' }) &&
|
|
66
|
+
t.isIdentifier(node.property, { name: 'NODE_ENV' })
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks if a binary expression compares `process.env.NODE_ENV` with a value using the given operator.
|
|
72
|
+
* Handles both `process.env.NODE_ENV op value` and `value op process.env.NODE_ENV`.
|
|
73
|
+
* @param {babel.types} t
|
|
74
|
+
* @param {babel.types.BinaryExpression} node
|
|
75
|
+
* @param {string} operator
|
|
76
|
+
* @param {string} value
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
function isNodeEnvComparison(t, node, operator, value) {
|
|
80
|
+
if (node.operator !== operator) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
(isProcessEnvNodeEnv(t, node.left) && t.isStringLiteral(node.right, { value })) ||
|
|
85
|
+
(t.isStringLiteral(node.left, { value }) && isProcessEnvNodeEnv(t, node.right))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks if the given path is inside a dev-only branch
|
|
91
|
+
* (e.g. `if (process.env.NODE_ENV !== 'production') { ... }`).
|
|
92
|
+
* Errors inside such branches are already stripped in production,
|
|
93
|
+
* so minification is unnecessary.
|
|
94
|
+
* @param {babel.types} t
|
|
95
|
+
* @param {babel.NodePath} path
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
function isInsideDevOnlyBranch(t, path) {
|
|
99
|
+
let current = path;
|
|
100
|
+
while (current.parentPath) {
|
|
101
|
+
const parent = current.parentPath;
|
|
102
|
+
if (parent.isIfStatement()) {
|
|
103
|
+
const isInConsequent = current.key === 'consequent';
|
|
104
|
+
const isInAlternate = current.key === 'alternate';
|
|
105
|
+
if ((isInConsequent || isInAlternate) && t.isBinaryExpression(parent.node.test)) {
|
|
106
|
+
const operator = isInConsequent ? '!==' : '===';
|
|
107
|
+
if (isNodeEnvComparison(t, parent.node.test, operator, 'production')) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
current = current.parentPath;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @typedef {{ path: babel.NodePath<babel.types.Expression>, message: string, expressions: babel.types.Expression[] }} ExtractedMessage
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extracts the message and expressions from a path.
|
|
123
|
+
* @param {babel.types} t
|
|
124
|
+
* @param {babel.NodePath<babel.types.ArgumentPlaceholder | babel.types.SpreadElement | babel.types.Expression>} path
|
|
125
|
+
* @returns {ExtractedMessage | null}
|
|
51
126
|
*/
|
|
52
|
-
function extractMessage(t,
|
|
53
|
-
if (
|
|
127
|
+
function extractMessage(t, path) {
|
|
128
|
+
if (path.isSpreadElement() || path.isArgumentPlaceholder()) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (path.isTemplateLiteral()) {
|
|
54
132
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
133
|
+
path,
|
|
134
|
+
message: path.node.quasis.map((quasi) => quasi.value.cooked).join('%s'),
|
|
135
|
+
expressions: path.node.expressions.map((expression) => {
|
|
57
136
|
if (t.isExpression(expression)) {
|
|
58
137
|
return expression;
|
|
59
138
|
}
|
|
60
|
-
throw
|
|
139
|
+
throw path.buildCodeFrameError('Can only evaluate javascript template literals.');
|
|
61
140
|
}),
|
|
62
141
|
};
|
|
63
142
|
}
|
|
64
|
-
if (
|
|
65
|
-
return { message: node.value, expressions: [] };
|
|
143
|
+
if (path.isStringLiteral()) {
|
|
144
|
+
return { path, message: path.node.value, expressions: [] };
|
|
66
145
|
}
|
|
67
|
-
if (
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
146
|
+
if (path.isBinaryExpression() && path.node.operator === '+') {
|
|
147
|
+
const leftPath = path.get('left');
|
|
148
|
+
if (leftPath.isExpression()) {
|
|
149
|
+
const left = extractMessage(t, leftPath);
|
|
150
|
+
const right = extractMessage(t, path.get('right'));
|
|
151
|
+
if (!left || !right) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
path,
|
|
156
|
+
message: left.message + right.message,
|
|
157
|
+
expressions: [...left.expressions, ...right.expressions],
|
|
158
|
+
};
|
|
72
159
|
}
|
|
73
|
-
return {
|
|
74
|
-
message: left.message + right.message,
|
|
75
|
-
expressions: [...left.expressions, ...right.expressions],
|
|
76
|
-
};
|
|
77
160
|
}
|
|
78
161
|
return null;
|
|
79
162
|
}
|
|
80
163
|
|
|
81
164
|
/**
|
|
82
|
-
*
|
|
83
|
-
* @param {
|
|
84
|
-
* @param {
|
|
165
|
+
* @param {babel.types} t
|
|
166
|
+
* @param {babel.NodePath<babel.types.NewExpression>} newExpressionPath
|
|
167
|
+
* @param {'opt-in' | 'opt-out'} detection
|
|
168
|
+
* @returns {null | babel.NodePath<babel.types.ArgumentPlaceholder | babel.types.SpreadElement | babel.types.Expression>}
|
|
85
169
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
170
|
+
function findMessageNode(t, newExpressionPath, detection) {
|
|
171
|
+
const callee = newExpressionPath.get('callee');
|
|
172
|
+
if (!callee.isIdentifier() || !SUPPORTED_ERROR_CONSTRUCTORS.has(callee.node.name)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isInsideDevOnlyBranch(t, newExpressionPath)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
switch (detection) {
|
|
181
|
+
case 'opt-in': {
|
|
182
|
+
if (
|
|
183
|
+
!newExpressionPath.node.leadingComments?.some((comment) =>
|
|
184
|
+
comment.value.includes(COMMENT_OPT_IN_MARKER),
|
|
185
|
+
)
|
|
186
|
+
) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
93
189
|
break;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
190
|
+
}
|
|
191
|
+
case 'opt-out': {
|
|
192
|
+
if (
|
|
193
|
+
newExpressionPath.node.leadingComments?.some((comment) =>
|
|
194
|
+
comment.value.includes(COMMENT_OPT_OUT_MARKER),
|
|
195
|
+
)
|
|
196
|
+
) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
99
200
|
break;
|
|
100
|
-
|
|
101
|
-
|
|
201
|
+
}
|
|
202
|
+
default: {
|
|
203
|
+
throw new Error(`Unknown detection option: ${detection}`);
|
|
204
|
+
}
|
|
102
205
|
}
|
|
206
|
+
|
|
207
|
+
const messagePath = newExpressionPath.get('arguments')[0];
|
|
208
|
+
|
|
209
|
+
return messagePath ?? null;
|
|
103
210
|
}
|
|
104
211
|
|
|
105
212
|
/**
|
|
106
213
|
* Transforms the error message node.
|
|
107
214
|
* @param {babel.types} t
|
|
108
|
-
* @param {
|
|
109
|
-
* @param {
|
|
215
|
+
* @param {ExtractedMessage} extracted
|
|
216
|
+
* @param {number} errorCode
|
|
110
217
|
* @param {PluginState} state
|
|
111
|
-
* @param {Map<string, number>} errorCodesLookup
|
|
112
|
-
* @param {MissingError} missingError
|
|
113
218
|
* @param {string} runtimeModule
|
|
114
219
|
* @param {string} outExtension
|
|
115
|
-
* @returns {babel.types.Expression
|
|
220
|
+
* @returns {babel.types.Expression}
|
|
116
221
|
*/
|
|
117
|
-
function transformMessage(
|
|
118
|
-
t,
|
|
119
|
-
path,
|
|
120
|
-
messageNode,
|
|
121
|
-
state,
|
|
122
|
-
errorCodesLookup,
|
|
123
|
-
missingError,
|
|
124
|
-
runtimeModule,
|
|
125
|
-
outExtension,
|
|
126
|
-
) {
|
|
127
|
-
const message = extractMessage(t, messageNode);
|
|
128
|
-
if (!message) {
|
|
129
|
-
handleUnminifyableError(missingError, path);
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let errorCode = errorCodesLookup.get(message.message);
|
|
134
|
-
if (errorCode === undefined) {
|
|
135
|
-
switch (missingError) {
|
|
136
|
-
case 'annotate':
|
|
137
|
-
path.addComment(
|
|
138
|
-
'leading',
|
|
139
|
-
' FIXME (minify-errors-in-prod): Unminified error message in production build! ',
|
|
140
|
-
);
|
|
141
|
-
return null;
|
|
142
|
-
case 'throw':
|
|
143
|
-
throw new Error(
|
|
144
|
-
`Missing error code for message '${message.message}'. Did you forget to run \`pnpm extract-error-codes\` first?`,
|
|
145
|
-
);
|
|
146
|
-
case 'write':
|
|
147
|
-
errorCode = errorCodesLookup.size + 1;
|
|
148
|
-
errorCodesLookup.set(message.message, errorCode);
|
|
149
|
-
state.updatedErrorCodes = true;
|
|
150
|
-
break;
|
|
151
|
-
default:
|
|
152
|
-
throw new Error(`Unknown missingError option: ${missingError}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
222
|
+
function transformMessage(t, extracted, errorCode, state, runtimeModule, outExtension) {
|
|
156
223
|
if (!state.formatErrorMessageIdentifier) {
|
|
157
224
|
state.formatErrorMessageIdentifier = helperModuleImports.addDefault(
|
|
158
|
-
path,
|
|
225
|
+
extracted.path,
|
|
159
226
|
transformExtension(resolveRuntimeModule(runtimeModule, state), outExtension),
|
|
160
227
|
{ nameHint: '_formatErrorMessage' },
|
|
161
228
|
);
|
|
@@ -170,10 +237,10 @@ function transformMessage(
|
|
|
170
237
|
),
|
|
171
238
|
t.stringLiteral('production'),
|
|
172
239
|
),
|
|
173
|
-
|
|
240
|
+
extracted.path.node,
|
|
174
241
|
t.callExpression(t.cloneNode(state.formatErrorMessageIdentifier, true), [
|
|
175
242
|
t.numericLiteral(errorCode),
|
|
176
|
-
...
|
|
243
|
+
...extracted.expressions,
|
|
177
244
|
]),
|
|
178
245
|
);
|
|
179
246
|
}
|
|
@@ -240,99 +307,109 @@ module.exports = function plugin(
|
|
|
240
307
|
{ types: t },
|
|
241
308
|
{
|
|
242
309
|
errorCodesPath,
|
|
243
|
-
missingError = 'annotate',
|
|
244
310
|
runtimeModule = '#formatErrorMessage',
|
|
245
311
|
detection = 'opt-in',
|
|
246
312
|
outExtension = '.js',
|
|
313
|
+
collectErrors,
|
|
247
314
|
},
|
|
248
315
|
) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
316
|
+
/** @type {Map<string, number>} */
|
|
317
|
+
let errorCodesLookup;
|
|
252
318
|
|
|
253
|
-
|
|
254
|
-
|
|
319
|
+
if (collectErrors) {
|
|
320
|
+
errorCodesLookup = new Map();
|
|
321
|
+
} else {
|
|
322
|
+
if (!errorCodesPath) {
|
|
323
|
+
throw new Error('errorCodesPath is required.');
|
|
324
|
+
}
|
|
255
325
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
326
|
+
const errorCodesContent = fs.readFileSync(errorCodesPath, 'utf8');
|
|
327
|
+
const errorCodes = JSON.parse(errorCodesContent);
|
|
328
|
+
|
|
329
|
+
errorCodesLookup = new Map(
|
|
330
|
+
Object.entries(errorCodes).map(([key, value]) => [value, Number(key)]),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
259
333
|
|
|
260
334
|
return {
|
|
261
335
|
name: '@mui/internal-babel-plugin-minify-errors',
|
|
262
336
|
visitor: {
|
|
263
337
|
NewExpression(newExpressionPath, state) {
|
|
264
|
-
|
|
338
|
+
// Initialize the WeakSet lazily to track processed nodes
|
|
339
|
+
state.processedNodes ??= new WeakSet();
|
|
340
|
+
|
|
341
|
+
// Skip if we've already processed this node. This can happen when Babel
|
|
342
|
+
// visits the same node multiple times due to configuration or plugin
|
|
343
|
+
// interactions (e.g., @babel/preset-env with modules: 'commonjs' combined
|
|
344
|
+
// with React.forwardRef causes double visitation).
|
|
345
|
+
if (state.processedNodes.has(newExpressionPath.node)) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Mark this node as processed before transforming
|
|
350
|
+
state.processedNodes.add(newExpressionPath.node);
|
|
351
|
+
|
|
352
|
+
const messagePath = findMessageNode(t, newExpressionPath, detection);
|
|
353
|
+
|
|
354
|
+
if (!messagePath) {
|
|
355
|
+
// Not an error, or not eligible for minification
|
|
265
356
|
return;
|
|
266
357
|
}
|
|
267
358
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
!
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
359
|
+
if (!collectErrors && newExpressionPath.node.leadingComments) {
|
|
360
|
+
newExpressionPath.node.leadingComments = newExpressionPath.node.leadingComments.filter(
|
|
361
|
+
(comment) =>
|
|
362
|
+
!comment.value.includes(COMMENT_OPT_IN_MARKER) &&
|
|
363
|
+
!comment.value.includes(COMMENT_OPT_OUT_MARKER),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const extracted = extractMessage(t, messagePath);
|
|
368
|
+
|
|
369
|
+
if (!extracted) {
|
|
370
|
+
if (collectErrors) {
|
|
371
|
+
// Mutates the caller's Set
|
|
372
|
+
collectErrors.add(
|
|
373
|
+
messagePath.buildCodeFrameError(
|
|
374
|
+
'Unminifyable error. You can only use literal strings and template strings as error messages.',
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
} else {
|
|
378
|
+
newExpressionPath.addComment(
|
|
379
|
+
'leading',
|
|
380
|
+
' FIXME (minify-errors-in-prod): Unminifyable error in production! ',
|
|
279
381
|
);
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case 'opt-out': {
|
|
283
|
-
if (
|
|
284
|
-
newExpressionPath.node.leadingComments?.some((comment) =>
|
|
285
|
-
comment.value.includes(COMMENT_OPT_OUT_MARKER),
|
|
286
|
-
)
|
|
287
|
-
) {
|
|
288
|
-
newExpressionPath.node.leadingComments =
|
|
289
|
-
newExpressionPath.node.leadingComments.filter(
|
|
290
|
-
(comment) => !comment.value.includes(COMMENT_OPT_OUT_MARKER),
|
|
291
|
-
);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
default: {
|
|
298
|
-
throw new Error(`Unknown detection option: ${detection}`);
|
|
299
382
|
}
|
|
383
|
+
return;
|
|
300
384
|
}
|
|
301
385
|
|
|
302
|
-
const
|
|
303
|
-
|
|
386
|
+
const errorCode = errorCodesLookup.get(extracted.message);
|
|
387
|
+
|
|
388
|
+
if (collectErrors) {
|
|
389
|
+
// Mutates the caller's Set
|
|
390
|
+
collectErrors.add(extracted.message);
|
|
304
391
|
return;
|
|
305
392
|
}
|
|
306
393
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
394
|
+
if (errorCode === undefined) {
|
|
395
|
+
newExpressionPath.addComment(
|
|
396
|
+
'leading',
|
|
397
|
+
' FIXME (minify-errors-in-prod): Unminified error message in production build! ',
|
|
398
|
+
);
|
|
310
399
|
return;
|
|
311
400
|
}
|
|
312
401
|
|
|
313
402
|
const transformedMessage = transformMessage(
|
|
314
403
|
t,
|
|
315
|
-
|
|
316
|
-
|
|
404
|
+
extracted,
|
|
405
|
+
errorCode,
|
|
317
406
|
state,
|
|
318
|
-
errorCodesLookup,
|
|
319
|
-
missingError,
|
|
320
407
|
runtimeModule,
|
|
321
408
|
outExtension,
|
|
322
409
|
);
|
|
323
410
|
|
|
324
|
-
|
|
325
|
-
messagePath.replaceWith(transformedMessage);
|
|
326
|
-
}
|
|
411
|
+
messagePath.replaceWith(transformedMessage);
|
|
327
412
|
},
|
|
328
413
|
},
|
|
329
|
-
post() {
|
|
330
|
-
if (missingError === 'write' && this.updatedErrorCodes) {
|
|
331
|
-
const invertedErrorCodes = Object.fromEntries(
|
|
332
|
-
Array.from(errorCodesLookup, ([key, value]) => [value, key]),
|
|
333
|
-
);
|
|
334
|
-
fs.writeFileSync(errorCodesPath, `${JSON.stringify(invertedErrorCodes, null, 2)}\n`);
|
|
335
|
-
}
|
|
336
|
-
},
|
|
337
414
|
};
|
|
338
415
|
};
|
package/index.test.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import { transformSync } from '@babel/core';
|
|
4
4
|
import { pluginTester } from 'babel-plugin-tester';
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { expect, describe, it } from 'vitest';
|
|
6
|
+
import * as babel from '@babel/core';
|
|
6
7
|
import plugin from './index';
|
|
7
8
|
|
|
8
|
-
const temporaryErrorCodesPath = path.join(os.tmpdir(), 'error-codes.json');
|
|
9
9
|
const fixturePath = path.resolve(__dirname, './__fixtures__');
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -35,6 +35,15 @@ pluginTester({
|
|
|
35
35
|
fixture: path.join(fixturePath, 'literal', 'input.js'),
|
|
36
36
|
output: readOutputFixtureSync('literal', 'output.js'),
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
title: 'type-error',
|
|
40
|
+
pluginOptions: {
|
|
41
|
+
errorCodesPath: path.join(fixturePath, 'type-error', 'error-codes.json'),
|
|
42
|
+
runtimeModule: '@mui/utils/formatMuiErrorMessage',
|
|
43
|
+
},
|
|
44
|
+
fixture: path.join(fixturePath, 'type-error', 'input.js'),
|
|
45
|
+
output: readOutputFixtureSync('type-error', 'output.js'),
|
|
46
|
+
},
|
|
38
47
|
{
|
|
39
48
|
title: 'interpolation',
|
|
40
49
|
pluginOptions: {
|
|
@@ -53,19 +62,6 @@ pluginTester({
|
|
|
53
62
|
fixture: path.join(fixturePath, 'no-error-code-annotation', 'input.js'),
|
|
54
63
|
output: readOutputFixtureSync('no-error-code-annotation', 'output.js'),
|
|
55
64
|
},
|
|
56
|
-
{
|
|
57
|
-
title: 'can throw on missing error codes',
|
|
58
|
-
// babel prefixes with filename.
|
|
59
|
-
// We're only interested in the message.
|
|
60
|
-
error:
|
|
61
|
-
/: Missing error code for message 'missing'. Did you forget to run `pnpm extract-error-codes` first\?/,
|
|
62
|
-
fixture: path.join(fixturePath, 'no-error-code-throw', 'input.js'),
|
|
63
|
-
pluginOptions: {
|
|
64
|
-
errorCodesPath: path.join(fixturePath, 'no-error-code-throw', 'error-codes.json'),
|
|
65
|
-
missingError: 'throw',
|
|
66
|
-
runtimeModule: '@mui/utils/formatMuiErrorMessage',
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
65
|
{
|
|
70
66
|
title: 'annotates unminifyable errors',
|
|
71
67
|
pluginOptions: {
|
|
@@ -76,50 +72,10 @@ pluginTester({
|
|
|
76
72
|
output: readOutputFixtureSync('unminifyable-annotation', 'output.js'),
|
|
77
73
|
},
|
|
78
74
|
{
|
|
79
|
-
title: '
|
|
80
|
-
|
|
81
|
-
// We're only interested in the message.
|
|
82
|
-
error:
|
|
83
|
-
/: Unminifyable error. You can only use literal strings and template strings as error messages./,
|
|
84
|
-
fixture: path.join(fixturePath, 'unminifyable-throw', 'input.js'),
|
|
85
|
-
pluginOptions: {
|
|
86
|
-
errorCodesPath: path.join(fixturePath, 'unminifyable-throw', 'error-codes.json'),
|
|
87
|
-
missingError: 'throw',
|
|
88
|
-
runtimeModule: '@mui/utils/formatMuiErrorMessage',
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
title: 'can extract errors',
|
|
93
|
-
fixture: path.join(fixturePath, 'error-code-extraction', 'input.js'),
|
|
75
|
+
title: 'collects unminifyable errors as Error objects without throwing',
|
|
76
|
+
fixture: path.join(fixturePath, 'unminifyable-collect', 'input.js'),
|
|
94
77
|
pluginOptions: {
|
|
95
|
-
|
|
96
|
-
missingError: 'write',
|
|
97
|
-
runtimeModule: '@mui/utils/formatMuiErrorMessage',
|
|
98
|
-
},
|
|
99
|
-
output: readOutputFixtureSync('error-code-extraction', 'output.js'),
|
|
100
|
-
setup() {
|
|
101
|
-
fs.copyFileSync(
|
|
102
|
-
path.join(fixturePath, 'error-code-extraction', 'error-codes.before.json'),
|
|
103
|
-
temporaryErrorCodesPath,
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
return function teardown() {
|
|
107
|
-
try {
|
|
108
|
-
const actualErrorCodes = JSON.parse(
|
|
109
|
-
fs.readFileSync(temporaryErrorCodesPath, { encoding: 'utf8' }),
|
|
110
|
-
);
|
|
111
|
-
const expectedErrorCodes = JSON.parse(
|
|
112
|
-
fs.readFileSync(
|
|
113
|
-
path.join(fixturePath, 'error-code-extraction', 'error-codes.after.json'),
|
|
114
|
-
'utf-8',
|
|
115
|
-
),
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
expect(actualErrorCodes).to.deep.equal(expectedErrorCodes);
|
|
119
|
-
} finally {
|
|
120
|
-
fs.unlinkSync(temporaryErrorCodesPath);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
78
|
+
collectErrors: new Set(),
|
|
123
79
|
},
|
|
124
80
|
},
|
|
125
81
|
{
|
|
@@ -166,5 +122,151 @@ pluginTester({
|
|
|
166
122
|
fixture: path.join(fixturePath, 'custom-runtime-imports-recursive', 'input.js'),
|
|
167
123
|
output: readOutputFixtureSync('custom-runtime-imports-recursive', 'output.js'),
|
|
168
124
|
},
|
|
125
|
+
{
|
|
126
|
+
title: 'skips errors inside dev-only branches',
|
|
127
|
+
pluginOptions: {
|
|
128
|
+
errorCodesPath: path.join(fixturePath, 'dev-only-branch', 'error-codes.json'),
|
|
129
|
+
runtimeModule: '@mui/utils/formatMuiErrorMessage',
|
|
130
|
+
detection: 'opt-out',
|
|
131
|
+
},
|
|
132
|
+
fixture: path.join(fixturePath, 'dev-only-branch', 'input.js'),
|
|
133
|
+
output: readOutputFixtureSync('dev-only-branch', 'output.js'),
|
|
134
|
+
},
|
|
169
135
|
],
|
|
170
136
|
});
|
|
137
|
+
|
|
138
|
+
describe('collectErrors', () => {
|
|
139
|
+
it('collects error messages into the provided Set without transforming code', () => {
|
|
140
|
+
const errors = new Set();
|
|
141
|
+
const code = [
|
|
142
|
+
'throw /* minify-error */ new Error("first error");',
|
|
143
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
144
|
+
'throw /* minify-error */ new Error(`second ${x} error`);',
|
|
145
|
+
].join('\n');
|
|
146
|
+
|
|
147
|
+
transformSync(code, {
|
|
148
|
+
filename: '/test/file.js',
|
|
149
|
+
plugins: [[plugin, { collectErrors: errors }]],
|
|
150
|
+
configFile: false,
|
|
151
|
+
babelrc: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(errors).toEqual(new Set(['first error', 'second %s error']));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('collects Error objects for unminifyable errors', () => {
|
|
158
|
+
const errors = new Set();
|
|
159
|
+
const code = [
|
|
160
|
+
'throw /* minify-error */ new Error(foo);',
|
|
161
|
+
'throw /* minify-error */ new Error(...bar);',
|
|
162
|
+
].join('\n');
|
|
163
|
+
|
|
164
|
+
transformSync(code, {
|
|
165
|
+
filename: '/test/file.js',
|
|
166
|
+
plugins: [[plugin, { collectErrors: errors }]],
|
|
167
|
+
configFile: false,
|
|
168
|
+
babelrc: false,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const collected = Array.from(errors);
|
|
172
|
+
expect(collected).toHaveLength(2);
|
|
173
|
+
expect(collected[0]).toBeInstanceOf(Error);
|
|
174
|
+
expect(collected[0].message).toMatch(
|
|
175
|
+
/Unminifyable error. You can only use literal strings and template strings as error messages./,
|
|
176
|
+
);
|
|
177
|
+
expect(collected[1]).toBeInstanceOf(Error);
|
|
178
|
+
expect(collected[1].message).toMatch(
|
|
179
|
+
/Unminifyable error. You can only use literal strings and template strings as error messages./,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('continues collection past unminifyable errors', () => {
|
|
184
|
+
const errors = new Set();
|
|
185
|
+
const code = [
|
|
186
|
+
'throw /* minify-error */ new Error(foo);',
|
|
187
|
+
'throw /* minify-error */ new Error("valid error message");',
|
|
188
|
+
].join('\n');
|
|
189
|
+
|
|
190
|
+
transformSync(code, {
|
|
191
|
+
filename: '/test/file.js',
|
|
192
|
+
plugins: [[plugin, { collectErrors: errors }]],
|
|
193
|
+
configFile: false,
|
|
194
|
+
babelrc: false,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const collected = Array.from(errors);
|
|
198
|
+
expect(collected).toHaveLength(2);
|
|
199
|
+
expect(collected[0]).toBeInstanceOf(Error);
|
|
200
|
+
expect(collected[1]).toBe('valid error message');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('respects detection option when collecting errors', () => {
|
|
204
|
+
const errors = new Set();
|
|
205
|
+
const code = ['throw new Error("opted-in error");', 'throw new Error("not collected");'].join(
|
|
206
|
+
'\n',
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
transformSync(code, {
|
|
210
|
+
filename: '/test/file.js',
|
|
211
|
+
plugins: [[plugin, { collectErrors: errors, detection: 'opt-out' }]],
|
|
212
|
+
configFile: false,
|
|
213
|
+
babelrc: false,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(errors).toEqual(new Set(['opted-in error', 'not collected']));
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('minify-errors double-visitation fix', () => {
|
|
221
|
+
// Separate test for the double-visitation bug fix
|
|
222
|
+
// This test uses @babel/core directly because it requires specific preset configuration
|
|
223
|
+
// that triggers the double-visitation issue (preset-env with modules: 'commonjs' + React.forwardRef)
|
|
224
|
+
it('handles double visitation with preset-env commonjs modules', () => {
|
|
225
|
+
// This pattern (React.forwardRef) combined with @babel/preset-env modules: 'commonjs'
|
|
226
|
+
// causes Babel to visit the same NewExpression node multiple times.
|
|
227
|
+
// Without the fix, this would result in both a FIXME annotation AND a minified error.
|
|
228
|
+
//
|
|
229
|
+
// NOTE: We use detection: 'opt-out' to properly trigger the bug, because with opt-in,
|
|
230
|
+
// the /* minify-error */ comment gets removed on the first pass, which masks the issue.
|
|
231
|
+
const input = `
|
|
232
|
+
import * as React from 'react';
|
|
233
|
+
|
|
234
|
+
export const Component = React.forwardRef(function Component(props, ref) {
|
|
235
|
+
if (!props.store) {
|
|
236
|
+
throw new Error('Component requires a store prop');
|
|
237
|
+
}
|
|
238
|
+
return <div ref={ref}>{props.children}</div>;
|
|
239
|
+
});
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
const result = babel.transformSync(input, {
|
|
243
|
+
filename: path.join(fixturePath, 'commonjs-double-visit', 'test.js'),
|
|
244
|
+
configFile: false,
|
|
245
|
+
babelrc: false,
|
|
246
|
+
presets: [
|
|
247
|
+
['@babel/preset-env', { modules: 'commonjs' }],
|
|
248
|
+
['@babel/preset-react', { runtime: 'automatic' }],
|
|
249
|
+
],
|
|
250
|
+
plugins: [
|
|
251
|
+
[
|
|
252
|
+
plugin,
|
|
253
|
+
{
|
|
254
|
+
errorCodesPath: path.join(fixturePath, 'commonjs-double-visit', 'error-codes.json'),
|
|
255
|
+
runtimeModule: '@mui/utils/formatMuiErrorMessage',
|
|
256
|
+
detection: 'opt-out', // Use opt-out to properly trigger the bug
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Key assertions:
|
|
263
|
+
// 1. Output should NOT contain FIXME annotation (which would indicate improper double processing)
|
|
264
|
+
expect(result?.code).not.toContain('FIXME');
|
|
265
|
+
|
|
266
|
+
// 2. Output should contain the properly minified error with NODE_ENV conditional
|
|
267
|
+
expect(result?.code).toContain('process.env.NODE_ENV !== "production"');
|
|
268
|
+
|
|
269
|
+
// 3. Output should contain the error code call (the import name varies based on babel helper)
|
|
270
|
+
expect(result?.code).toMatch(/_formatMuiErrorMessage.*\(1\)/);
|
|
271
|
+
});
|
|
272
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-babel-plugin-minify-errors",
|
|
3
|
-
"version": "2.0.8-canary.
|
|
3
|
+
"version": "2.0.8-canary.20",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "This is an internal package not meant for general use.",
|
|
6
6
|
"repository": {
|
|
@@ -17,22 +17,22 @@
|
|
|
17
17
|
"url": "https://opencollective.com/mui-org"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@babel/helper-module-imports": "^7.
|
|
20
|
+
"@babel/helper-module-imports": "^7.28.6",
|
|
21
21
|
"find-package-json": "^1.2.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@babel/core": "
|
|
25
|
-
"@types/babel__core": "
|
|
26
|
-
"@types/babel__helper-module-imports": "
|
|
27
|
-
"@types/find-package-json": "
|
|
28
|
-
"babel-plugin-tester": "
|
|
29
|
-
"chai": "^4.5.0"
|
|
24
|
+
"@babel/core": "7.28.6",
|
|
25
|
+
"@types/babel__core": "7.20.5",
|
|
26
|
+
"@types/babel__helper-module-imports": "7.18.3",
|
|
27
|
+
"@types/find-package-json": "1.2.7",
|
|
28
|
+
"babel-plugin-tester": "12.0.0"
|
|
30
29
|
},
|
|
31
30
|
"peerDependencies": {
|
|
32
31
|
"@babel/core": "7"
|
|
33
32
|
},
|
|
34
33
|
"sideEffects": false,
|
|
35
34
|
"type": "commonjs",
|
|
35
|
+
"main": "./index.js",
|
|
36
36
|
"exports": {
|
|
37
37
|
".": "./index.js"
|
|
38
38
|
},
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
|
-
"gitSha": "
|
|
45
|
+
"gitSha": "2836960cc7e128ab529942129c49aeffb8a0b76d",
|
|
46
46
|
"scripts": {}
|
|
47
47
|
}
|