@mui/internal-babel-plugin-minify-errors 1.0.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Call-Em-All
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ rules: {
3
+ // keeps test simple
4
+ 'no-unreachable': 'off',
5
+ // keeps test simple
6
+ 'no-useless-concat': 'off',
7
+
8
+ // Babel import helpers do not add one.
9
+ // Since this is auto generated code we don't care about code style.
10
+ 'import/newline-after-import': 'off',
11
+ },
12
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "1": "exists",
3
+ "2": "will be created"
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "1": "exists"
3
+ }
@@ -0,0 +1,2 @@
1
+ throw /* minify-error */ new Error('exists');
2
+ throw /* minify-error */ new Error('will be created');
@@ -0,0 +1,5 @@
1
+ import _formatMuiErrorMessage from '@mui/utils/formatMuiErrorMessage';
2
+ throw new Error(process.env.NODE_ENV !== 'production' ? 'exists' : _formatMuiErrorMessage(1));
3
+ throw new Error(
4
+ process.env.NODE_ENV !== 'production' ? 'will be created' : _formatMuiErrorMessage(2),
5
+ );
@@ -0,0 +1,3 @@
1
+ {
2
+ "1": "MUI: %s, %s"
3
+ }
@@ -0,0 +1,5 @@
1
+ const foo = 'foo';
2
+ const bar = 'bar';
3
+ throw /* minify-error */ new Error(`MUI: ${foo}, ${bar}`);
4
+ throw /* minify-error */ new Error(`MUI: ${foo}` + `, ${bar}`);
5
+ throw /* minify-error */ new Error('MUI: ' + `${foo}, ${bar}`);
@@ -0,0 +1,18 @@
1
+ import _formatMuiErrorMessage from '@mui/utils/formatMuiErrorMessage';
2
+ const foo = 'foo';
3
+ const bar = 'bar';
4
+ throw new Error(
5
+ process.env.NODE_ENV !== 'production'
6
+ ? `MUI: ${foo}, ${bar}`
7
+ : _formatMuiErrorMessage(1, foo, bar),
8
+ );
9
+ throw new Error(
10
+ process.env.NODE_ENV !== 'production'
11
+ ? `MUI: ${foo}` + `, ${bar}`
12
+ : _formatMuiErrorMessage(1, foo, bar),
13
+ );
14
+ throw new Error(
15
+ process.env.NODE_ENV !== 'production'
16
+ ? 'MUI: ' + `${foo}, ${bar}`
17
+ : _formatMuiErrorMessage(1, foo, bar),
18
+ );
@@ -0,0 +1,3 @@
1
+ {
2
+ "1": "MUI: Expected valid input target.\nDid you use `inputComponent`"
3
+ }
@@ -0,0 +1,6 @@
1
+ throw /* minify-error */ new Error(
2
+ 'MUI: Expected valid input target.\n' + 'Did you use `inputComponent`',
3
+ );
4
+ throw /* minify-error */ new Error(
5
+ `MUI: Expected valid input target.\n` + `Did you use \`inputComponent\``,
6
+ );
@@ -0,0 +1,11 @@
1
+ import _formatMuiErrorMessage from '@mui/utils/formatMuiErrorMessage';
2
+ throw new Error(
3
+ process.env.NODE_ENV !== 'production'
4
+ ? 'MUI: Expected valid input target.\n' + 'Did you use `inputComponent`'
5
+ : _formatMuiErrorMessage(1),
6
+ );
7
+ throw new Error(
8
+ process.env.NODE_ENV !== 'production'
9
+ ? `MUI: Expected valid input target.\n` + `Did you use \`inputComponent\``
10
+ : _formatMuiErrorMessage(1),
11
+ );
@@ -0,0 +1,3 @@
1
+ throw /* minify-error */ new Error(
2
+ 'MUI: Expected valid input target.\n' + 'Did you use inputComponent',
3
+ );
@@ -0,0 +1,3 @@
1
+ throw /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ new Error(
2
+ 'MUI: Expected valid input target.\n' + 'Did you use inputComponent',
3
+ );
@@ -0,0 +1 @@
1
+ throw /* minify-error */ new Error('missing');
@@ -0,0 +1,4 @@
1
+ const foo = 'foo';
2
+ const bar = ['bar'];
3
+ throw /* minify-error */ new Error(foo);
4
+ throw /* minify-error */ new Error(...bar);
@@ -0,0 +1,4 @@
1
+ const foo = 'foo';
2
+ const bar = ['bar'];
3
+ throw /* FIXME (minify-errors-in-prod): Unminifyable error in production! */ new Error(foo);
4
+ throw /* FIXME (minify-errors-in-prod): Unminifyable error in production! */ new Error(...bar);
@@ -0,0 +1,4 @@
1
+ const foo = 'foo';
2
+ const bar = ['bar'];
3
+ throw /* minify-error */ new Error(foo);
4
+ throw /* minify-error */ new Error(...bar);
package/index.js ADDED
@@ -0,0 +1,225 @@
1
+ // @ts-check
2
+
3
+ const helperModuleImports = require('@babel/helper-module-imports');
4
+ const fs = require('fs');
5
+
6
+ const COMMENT_MARKER = 'minify-error';
7
+
8
+ /**
9
+ * @typedef {import('@babel/core')} babel
10
+ */
11
+
12
+ /**
13
+ * @typedef {{updatedErrorCodes?: boolean, formatMuiErrorMessageIdentifier?: babel.types.Identifier}} PluginState
14
+ * @typedef {'annotate' | 'throw' | 'write'} MissingError
15
+ * @typedef {{ errorCodesPath: string, missingError: MissingError }} Options
16
+ */
17
+
18
+ /**
19
+ *
20
+ * @param {babel.types} t
21
+ * @param {babel.types.Node} node
22
+ * @returns {{ message: string, expressions: babel.types.Expression[] } | null}
23
+ */
24
+ function extractMessageFromExpression(t, node) {
25
+ if (t.isTemplateLiteral(node)) {
26
+ return {
27
+ message: node.quasis.map((quasi) => quasi.value.cooked).join('%s'),
28
+ expressions: node.expressions.map((expression) => {
29
+ if (t.isExpression(expression)) {
30
+ return expression;
31
+ }
32
+ throw new Error('Can only evaluate javascript template literals.');
33
+ }),
34
+ };
35
+ }
36
+ if (t.isStringLiteral(node)) {
37
+ return { message: node.value, expressions: [] };
38
+ }
39
+ if (t.isBinaryExpression(node) && node.operator === '+') {
40
+ if (t.isPrivateName(node.left)) {
41
+ // This is only psossible with `in` expressions, e.g. `#foo in {}`
42
+ throw new Error('Unreachable');
43
+ }
44
+ const left = extractMessageFromExpression(t, node.left);
45
+ const right = extractMessageFromExpression(t, node.right);
46
+ if (!left || !right) {
47
+ return null;
48
+ }
49
+ return {
50
+ message: left.message + right.message,
51
+ expressions: [...left.expressions, ...right.expressions],
52
+ };
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ *
59
+ * @param {MissingError} missingError
60
+ * @param {babel.NodePath} path
61
+ * @returns
62
+ */
63
+ function handleUnminifyable(missingError, path) {
64
+ switch (missingError) {
65
+ case 'annotate': {
66
+ // Outputs:
67
+ // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
68
+ // throw new Error(foo)
69
+ path.addComment(
70
+ 'leading',
71
+ ' FIXME (minify-errors-in-prod): Unminifyable error in production! ',
72
+ );
73
+ return;
74
+ }
75
+ case 'throw': {
76
+ throw new Error(
77
+ `Unminifyable error. You can only use literal strings and template strings as error messages.`,
78
+ );
79
+ }
80
+ case 'write': {
81
+ return;
82
+ }
83
+ default: {
84
+ throw new Error(`Unknown missingError option: ${missingError}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {babel} file
91
+ * @param {Options} options
92
+ * @returns {babel.PluginObj<PluginState>}
93
+ */
94
+ module.exports = function plugin({ types: t }, { errorCodesPath, missingError = 'annotate' }) {
95
+ if (!errorCodesPath) {
96
+ throw new Error('errorCodesPath is required.');
97
+ }
98
+
99
+ const errorCodesContent = fs.readFileSync(errorCodesPath, 'utf8');
100
+ const errorCodes = JSON.parse(errorCodesContent);
101
+
102
+ const errorCodesLookup = new Map(
103
+ Object.entries(errorCodes).map(([key, value]) => [value, Number(key)]),
104
+ );
105
+
106
+ return {
107
+ visitor: {
108
+ NewExpression(newExpressionPath, state) {
109
+ if (!newExpressionPath.get('callee').isIdentifier({ name: 'Error' })) {
110
+ return;
111
+ }
112
+
113
+ if (
114
+ !newExpressionPath.node.leadingComments?.some((comment) =>
115
+ comment.value.includes(COMMENT_MARKER),
116
+ )
117
+ ) {
118
+ return;
119
+ }
120
+
121
+ newExpressionPath.node.leadingComments = newExpressionPath.node.leadingComments.filter(
122
+ (comment) => !comment.value.includes(COMMENT_MARKER),
123
+ );
124
+
125
+ const messagePath = newExpressionPath.get('arguments')[0];
126
+ if (!messagePath) {
127
+ return;
128
+ }
129
+
130
+ const messageNode = messagePath.node;
131
+ if (t.isSpreadElement(messageNode) || t.isArgumentPlaceholder(messageNode)) {
132
+ handleUnminifyable(missingError, newExpressionPath);
133
+ return;
134
+ }
135
+
136
+ const message = extractMessageFromExpression(t, messageNode);
137
+
138
+ if (!message) {
139
+ handleUnminifyable(missingError, newExpressionPath);
140
+ return;
141
+ }
142
+
143
+ let errorCode = errorCodesLookup.get(message.message);
144
+ if (errorCode === undefined) {
145
+ switch (missingError) {
146
+ case 'annotate': {
147
+ // Outputs:
148
+ // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
149
+ // throw new Error(`A message with ${interpolation}`)
150
+ newExpressionPath.addComment(
151
+ 'leading',
152
+ ' FIXME (minify-errors-in-prod): Unminified error message in production build! ',
153
+ );
154
+ return;
155
+ }
156
+ case 'throw': {
157
+ throw new Error(
158
+ `Missing error code for message '${message.message}'. Did you forget to run \`pnpm extract-error-codes\` first?`,
159
+ );
160
+ }
161
+ case 'write': {
162
+ errorCode = errorCodesLookup.size + 1;
163
+ errorCodesLookup.set(message.message, errorCode);
164
+ state.updatedErrorCodes = true;
165
+ break;
166
+ }
167
+ default: {
168
+ throw new Error(`Unknown missingError option: ${missingError}`);
169
+ }
170
+ }
171
+ }
172
+
173
+ if (!state.formatMuiErrorMessageIdentifier) {
174
+ // Outputs:
175
+ // import { formatMuiErrorMessage } from '@mui/utils';
176
+ state.formatMuiErrorMessageIdentifier = helperModuleImports.addDefault(
177
+ newExpressionPath,
178
+ '@mui/utils/formatMuiErrorMessage',
179
+ { nameHint: '_formatMuiErrorMessage' },
180
+ );
181
+ }
182
+
183
+ // Outputs:
184
+ // `A ${adj} message that contains ${noun}`;
185
+ const devMessage = messageNode;
186
+
187
+ // Outputs:
188
+ // formatMuiErrorMessage(ERROR_CODE, adj, noun)
189
+ const prodMessage = t.callExpression(
190
+ t.cloneNode(state.formatMuiErrorMessageIdentifier, true),
191
+ [t.numericLiteral(errorCode), ...message.expressions],
192
+ );
193
+
194
+ // Outputs:
195
+ // new Error(
196
+ // process.env.NODE_ENV !== "production"
197
+ // ? `A message with ${interpolation}`
198
+ // : formatProdError('A message with %s', interpolation)
199
+ // )
200
+ messagePath.replaceWith(
201
+ t.conditionalExpression(
202
+ t.binaryExpression(
203
+ '!==',
204
+ t.memberExpression(
205
+ t.memberExpression(t.identifier('process'), t.identifier('env')),
206
+ t.identifier('NODE_ENV'),
207
+ ),
208
+ t.stringLiteral('production'),
209
+ ),
210
+ devMessage,
211
+ prodMessage,
212
+ ),
213
+ );
214
+ },
215
+ },
216
+ post() {
217
+ if (missingError === 'write' && this.updatedErrorCodes) {
218
+ const invertedErrorCodes = Object.fromEntries(
219
+ Array.from(errorCodesLookup, ([key, value]) => [value, key]),
220
+ );
221
+ fs.writeFileSync(errorCodesPath, `${JSON.stringify(invertedErrorCodes, null, 2)}\n`);
222
+ }
223
+ },
224
+ };
225
+ };
package/index.test.js ADDED
@@ -0,0 +1,112 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { pluginTester } from 'babel-plugin-tester';
5
+ import { expect } from 'chai';
6
+ import plugin from './index';
7
+
8
+ const temporaryErrorCodesPath = path.join(os.tmpdir(), 'error-codes.json');
9
+ const fixturePath = path.resolve(__dirname, './__fixtures__');
10
+
11
+ function readOutputFixtureSync(fixture, file) {
12
+ // babel hardcodes the linefeed to \n
13
+ return fs
14
+ .readFileSync(path.join(fixturePath, fixture, file), { encoding: 'utf8' })
15
+ .replace(/\r?\n/g, '\n');
16
+ }
17
+
18
+ pluginTester({
19
+ plugin,
20
+ filepath: __filename,
21
+ tests: [
22
+ {
23
+ title: 'literal',
24
+ pluginOptions: {
25
+ errorCodesPath: path.join(fixturePath, 'literal', 'error-codes.json'),
26
+ },
27
+ fixture: path.join(fixturePath, 'literal', 'input.js'),
28
+ output: readOutputFixtureSync('literal', 'output.js'),
29
+ },
30
+ {
31
+ title: 'interpolation',
32
+ pluginOptions: {
33
+ errorCodesPath: path.join(fixturePath, 'interpolation', 'error-codes.json'),
34
+ },
35
+ fixture: path.join(fixturePath, 'interpolation', 'input.js'),
36
+ output: readOutputFixtureSync('interpolation', 'output.js'),
37
+ },
38
+ {
39
+ title: 'annotates missing error codes',
40
+ pluginOptions: {
41
+ errorCodesPath: path.join(fixturePath, 'no-error-code-annotation', 'error-codes.json'),
42
+ },
43
+ fixture: path.join(fixturePath, 'no-error-code-annotation', 'input.js'),
44
+ output: readOutputFixtureSync('no-error-code-annotation', 'output.js'),
45
+ },
46
+ {
47
+ title: 'can throw on missing error codes',
48
+ // babel prefixes with filename.
49
+ // We're only interested in the message.
50
+ error:
51
+ /: Missing error code for message 'missing'. Did you forget to run `pnpm extract-error-codes` first?/,
52
+ fixture: path.join(fixturePath, 'no-error-code-throw', 'input.js'),
53
+ pluginOptions: {
54
+ errorCodesPath: path.join(fixturePath, 'no-error-code-throw', 'error-codes.json'),
55
+ missingError: 'throw',
56
+ },
57
+ },
58
+ {
59
+ title: 'annotates unminifyable errors',
60
+ pluginOptions: {
61
+ errorCodesPath: path.join(fixturePath, 'unminifyable-annotation', 'error-codes.json'),
62
+ },
63
+ fixture: path.join(fixturePath, 'unminifyable-annotation', 'input.js'),
64
+ output: readOutputFixtureSync('unminifyable-annotation', 'output.js'),
65
+ },
66
+ {
67
+ title: 'can throw on unminifyable errors',
68
+ // babel prefixes with filename.
69
+ // We're only interested in the message.
70
+ error:
71
+ /: Unminifyable error. You can only use literal strings and template strings as error messages.?/,
72
+ fixture: path.join(fixturePath, 'unminifyable-throw', 'input.js'),
73
+ pluginOptions: {
74
+ errorCodesPath: path.join(fixturePath, 'unminifyable-throw', 'error-codes.json'),
75
+ missingError: 'throw',
76
+ },
77
+ },
78
+ {
79
+ title: 'can extract errors',
80
+
81
+ fixture: path.join(fixturePath, 'error-code-extraction', 'input.js'),
82
+ pluginOptions: {
83
+ errorCodesPath: temporaryErrorCodesPath,
84
+ missingError: 'write',
85
+ },
86
+ output: readOutputFixtureSync('error-code-extraction', 'output.js'),
87
+ setup() {
88
+ fs.copyFileSync(
89
+ path.join(fixturePath, 'error-code-extraction', 'error-codes.before.json'),
90
+ temporaryErrorCodesPath,
91
+ );
92
+
93
+ return function teardown() {
94
+ try {
95
+ const actualErrorCodes = JSON.parse(
96
+ fs.readFileSync(temporaryErrorCodesPath, { encoding: 'utf8' }),
97
+ );
98
+ const expectedErrorCodes = JSON.parse(
99
+ fs.readFileSync(
100
+ path.join(fixturePath, 'error-code-extraction', 'error-codes.after.json'),
101
+ ),
102
+ );
103
+
104
+ expect(actualErrorCodes).to.deep.equal(expectedErrorCodes);
105
+ } finally {
106
+ fs.unlinkSync(temporaryErrorCodesPath);
107
+ }
108
+ };
109
+ },
110
+ },
111
+ ],
112
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@mui/internal-babel-plugin-minify-errors",
3
+ "version": "1.0.10",
4
+ "author": "MUI Team",
5
+ "description": "This is an internal package not meant for general use.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mui/material-ui.git",
9
+ "directory": "packages/mui-babel-plugin-minify-errors"
10
+ },
11
+ "license": "MIT",
12
+ "bugs": {
13
+ "url": "https://github.com/mui/material-ui/issues"
14
+ },
15
+ "funding": {
16
+ "type": "opencollective",
17
+ "url": "https://opencollective.com/mui-org"
18
+ },
19
+ "dependencies": {
20
+ "@babel/helper-module-imports": "^7.24.7"
21
+ },
22
+ "devDependencies": {
23
+ "@types/babel__helper-module-imports": "^7.18.3",
24
+ "babel-plugin-tester": "^11.0.4",
25
+ "chai": "^4.5.0"
26
+ },
27
+ "sideEffects": false,
28
+ "type": "commonjs",
29
+ "exports": {
30
+ ".": "./index.js"
31
+ },
32
+ "engines": {
33
+ "node": ">=14.0.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "test": "cd ../../ && cross-env NODE_ENV=test mocha 'packages-internal/babel-plugin-minify-errors/**/*.test.{js,ts,tsx}'"
40
+ }
41
+ }