diff --git a/.circleci/config.yml b/.circleci/config.yml index c7f54af7723a54..3f573f62f82b06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,6 +42,7 @@ default-job: &default-job TEST_GATE: << parameters.test-gate >> AWS_REGION_ARTIFACTS: eu-central-1 COREPACK_ENABLE_DOWNLOAD_PROMPT: '0' + DANGER_DISABLE_TRANSPILATION: 'true' working_directory: /tmp/material-ui docker: - image: cimg/node:20.17 diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 437b8c3d5eb5ef..2881171258056b 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -4,7 +4,6 @@ "node": "20", "packages": [ "packages/markdown", - "packages/mui-babel-macros", "packages/mui-base", "packages/mui-codemod", "packages/mui-core-downloads-tracker", @@ -33,7 +32,6 @@ "@mui/docs": "packages/mui-docs/build", "@mui/icons-material": "packages/mui-icons-material/build", "@mui/internal-test-utils": "packages-internal/test-utils", - "@mui/internal-babel-macros": "packages/mui-babel-macros", "@mui/internal-docs-utils": "packages-internal/docs-utils", "@mui/internal-markdown": "packages/markdown", "@mui/internal-scripts": "packages-internal/scripts", diff --git a/babel.config.js b/babel.config.js index cbadb0c6a75a97..de9a6cdea126ae 100644 --- a/babel.config.js +++ b/babel.config.js @@ -78,15 +78,6 @@ module.exports = function getBabelConfig(api) { /** @type {babel.PluginItem[]} */ const plugins = [ - [ - 'babel-plugin-macros', - { - muiError: { - errorCodesPath, - missingError, - }, - }, - ], 'babel-plugin-optimize-clsx', [ '@babel/plugin-transform-runtime', @@ -114,6 +105,13 @@ module.exports = function getBabelConfig(api) { ], }, ], + [ + '@mui/internal-babel-plugin-minify-errors', + { + missingError, + errorCodesPath, + }, + ], ...(useESModules ? [ [ diff --git a/dangerfile.ts b/dangerFileContent.ts similarity index 98% rename from dangerfile.ts rename to dangerFileContent.ts index 3b17317e6ba195..ee34fafdc9d622 100644 --- a/dangerfile.ts +++ b/dangerFileContent.ts @@ -1,9 +1,11 @@ -// danger has to be the first thing required! -import { danger, markdown } from 'danger'; import { exec } from 'child_process'; +import type * as dangerModule from 'danger'; import { loadComparison } from './scripts/sizeSnapshot'; import replaceUrl from './packages/api-docs-builder/utils/replaceUrl'; +declare const danger: (typeof dangerModule)['danger']; +declare const markdown: (typeof dangerModule)['markdown']; + const circleCIBuildNumber = process.env.CIRCLE_BUILD_NUM; const circleCIBuildUrl = `https://app.circleci.com/pipelines/github/mui/material-ui/jobs/${circleCIBuildNumber}`; const dangerCommand = process.env.DANGER_COMMAND; diff --git a/dangerfile.js b/dangerfile.js new file mode 100644 index 00000000000000..5862e174a8ab7a --- /dev/null +++ b/dangerfile.js @@ -0,0 +1,7 @@ +// danger uses babelify under the hood. The implementation is buggy and fails on our +// custom plugins in our codebase. We'll just disable it and do our own compilation. +// Danger must always be run with envirnonent variable `DANGER_DISABLE_TRANSPILATION="true"`. +require('@babel/register')({ + extensions: ['.js', '.ts', '.tsx'], +}); +require('./dangerFileContent'); diff --git a/package.json b/package.json index c40e741686ed72..b9d7e1f969f72b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "docs:typescript:formatted": "tsx ./docs/scripts/formattedTSDemos", "docs:mdicons:synonyms": "cross-env BABEL_ENV=development babel-node --extensions \".tsx,.ts,.js,.mjs\" ./docs/scripts/updateIconSynonyms && pnpm prettier", "docs:zipRules": "cd docs && rm mui-vale.zip && zip -r mui-vale.zip mui-vale && cd ../ && vale sync", - "extract-error-codes": "cross-env MUI_EXTRACT_ERROR_CODES=true lerna run --concurrency 8 build:modern", + "extract-error-codes": "cross-env MUI_EXTRACT_ERROR_CODES=true lerna run --concurrency 1 build:modern", "rsc:build": "tsx ./packages/rsc-builder/buildRsc.ts", "template:screenshot": "cross-env BABEL_ENV=development babel-node --extensions \".tsx,.ts,.js\" ./docs/scripts/generateTemplateScreenshots", "template:update-theme": "cross-env BABEL_ENV=development babel-node --extensions \".tsx,.ts,.js\" ./docs/scripts/updateTemplatesTheme", @@ -139,7 +139,6 @@ "@typescript-eslint/parser": "^7.18.0", "babel-loader": "^9.2.1", "babel-plugin-istanbul": "^7.0.0", - "babel-plugin-macros": "^3.1.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-optimize-clsx": "^2.6.2", "babel-plugin-react-remove-properties": "^0.3.0", diff --git a/packages/mui-babel-macros/__fixtures__/.eslintrc.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/.eslintrc.js similarity index 100% rename from packages/mui-babel-macros/__fixtures__/.eslintrc.js rename to packages-internal/babel-plugin-minify-errors/__fixtures__/.eslintrc.js diff --git a/packages/mui-babel-macros/__fixtures__/error-code-extraction/error-codes.after.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/error-codes.after.json similarity index 100% rename from packages/mui-babel-macros/__fixtures__/error-code-extraction/error-codes.after.json rename to packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/error-codes.after.json diff --git a/packages/mui-babel-macros/__fixtures__/error-code-extraction/error-codes.before.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/error-codes.before.json similarity index 100% rename from packages/mui-babel-macros/__fixtures__/error-code-extraction/error-codes.before.json rename to packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/error-codes.before.json diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/input.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/input.js new file mode 100644 index 00000000000000..0cc2982ad5f3a3 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/input.js @@ -0,0 +1,2 @@ +throw /* minify-error */ new Error('exists'); +throw /* minify-error */ new Error('will be created'); diff --git a/packages/mui-babel-macros/__fixtures__/error-code-extraction/output.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/output.js similarity index 53% rename from packages/mui-babel-macros/__fixtures__/error-code-extraction/output.js rename to packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/output.js index 7a1d72c6a355cf..41342d27e3b230 100644 --- a/packages/mui-babel-macros/__fixtures__/error-code-extraction/output.js +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/error-code-extraction/output.js @@ -1,5 +1,5 @@ import _formatMuiErrorMessage from '@mui/utils/formatMuiErrorMessage'; -throw new Error(process.env.NODE_ENV !== 'production' ? `exists` : _formatMuiErrorMessage(1)); +throw new Error(process.env.NODE_ENV !== 'production' ? 'exists' : _formatMuiErrorMessage(1)); throw new Error( - process.env.NODE_ENV !== 'production' ? `will be created` : _formatMuiErrorMessage(2), + process.env.NODE_ENV !== 'production' ? 'will be created' : _formatMuiErrorMessage(2), ); diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/error-codes.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/error-codes.json new file mode 100644 index 00000000000000..17bea7ce743eec --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/error-codes.json @@ -0,0 +1,3 @@ +{ + "1": "MUI: %s, %s" +} diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/input.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/input.js new file mode 100644 index 00000000000000..bf99cee19dae5c --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/input.js @@ -0,0 +1,5 @@ +const foo = 'foo'; +const bar = 'bar'; +throw /* minify-error */ new Error(`MUI: ${foo}, ${bar}`); +throw /* minify-error */ new Error(`MUI: ${foo}` + `, ${bar}`); +throw /* minify-error */ new Error('MUI: ' + `${foo}, ${bar}`); diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/output.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/output.js new file mode 100644 index 00000000000000..7d5887fa7657fd --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/interpolation/output.js @@ -0,0 +1,18 @@ +import _formatMuiErrorMessage from '@mui/utils/formatMuiErrorMessage'; +const foo = 'foo'; +const bar = 'bar'; +throw new Error( + process.env.NODE_ENV !== 'production' + ? `MUI: ${foo}, ${bar}` + : _formatMuiErrorMessage(1, foo, bar), +); +throw new Error( + process.env.NODE_ENV !== 'production' + ? `MUI: ${foo}` + `, ${bar}` + : _formatMuiErrorMessage(1, foo, bar), +); +throw new Error( + process.env.NODE_ENV !== 'production' + ? 'MUI: ' + `${foo}, ${bar}` + : _formatMuiErrorMessage(1, foo, bar), +); diff --git a/packages/mui-babel-macros/__fixtures__/literal/error-codes.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/literal/error-codes.json similarity index 100% rename from packages/mui-babel-macros/__fixtures__/literal/error-codes.json rename to packages-internal/babel-plugin-minify-errors/__fixtures__/literal/error-codes.json diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/literal/input.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/literal/input.js new file mode 100644 index 00000000000000..63567b147abee5 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/literal/input.js @@ -0,0 +1,6 @@ +throw /* minify-error */ new Error( + 'MUI: Expected valid input target.\n' + 'Did you use `inputComponent`', +); +throw /* minify-error */ new Error( + `MUI: Expected valid input target.\n` + `Did you use \`inputComponent\``, +); diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/literal/output.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/literal/output.js new file mode 100644 index 00000000000000..e0705efb04a30e --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/literal/output.js @@ -0,0 +1,11 @@ +import _formatMuiErrorMessage from '@mui/utils/formatMuiErrorMessage'; +throw new Error( + process.env.NODE_ENV !== 'production' + ? 'MUI: Expected valid input target.\n' + 'Did you use `inputComponent`' + : _formatMuiErrorMessage(1), +); +throw new Error( + process.env.NODE_ENV !== 'production' + ? `MUI: Expected valid input target.\n` + `Did you use \`inputComponent\`` + : _formatMuiErrorMessage(1), +); diff --git a/packages/mui-babel-macros/__fixtures__/factory-call/error-codes.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/error-codes.json similarity index 100% rename from packages/mui-babel-macros/__fixtures__/factory-call/error-codes.json rename to packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/error-codes.json diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/input.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/input.js new file mode 100644 index 00000000000000..be815c1b3427b2 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/input.js @@ -0,0 +1,3 @@ +throw /* minify-error */ new Error( + 'MUI: Expected valid input target.\n' + 'Did you use inputComponent', +); diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/output.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/output.js new file mode 100644 index 00000000000000..069754e40df18f --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-annotation/output.js @@ -0,0 +1,3 @@ +throw /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ new Error( + 'MUI: Expected valid input target.\n' + 'Did you use inputComponent', +); diff --git a/packages/mui-babel-macros/__fixtures__/no-error-code-annotation/error-codes.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-throw/error-codes.json similarity index 100% rename from packages/mui-babel-macros/__fixtures__/no-error-code-annotation/error-codes.json rename to packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-throw/error-codes.json diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-throw/input.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-throw/input.js new file mode 100644 index 00000000000000..3c8d1a793d474b --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/no-error-code-throw/input.js @@ -0,0 +1 @@ +throw /* minify-error */ new Error('missing'); diff --git a/packages/mui-babel-macros/__fixtures__/no-error-code-throw/error-codes.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/error-codes.json similarity index 100% rename from packages/mui-babel-macros/__fixtures__/no-error-code-throw/error-codes.json rename to packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/error-codes.json diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/input.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/input.js new file mode 100644 index 00000000000000..865fba71e2aa10 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/input.js @@ -0,0 +1,4 @@ +const foo = 'foo'; +const bar = ['bar']; +throw /* minify-error */ new Error(foo); +throw /* minify-error */ new Error(...bar); diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/output.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/output.js new file mode 100644 index 00000000000000..0d425ce67ef0d8 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-annotation/output.js @@ -0,0 +1,4 @@ +const foo = 'foo'; +const bar = ['bar']; +throw /* FIXME (minify-errors-in-prod): Unminifyable error in production! */ new Error(foo); +throw /* FIXME (minify-errors-in-prod): Unminifyable error in production! */ new Error(...bar); diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-throw/error-codes.json b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-throw/error-codes.json new file mode 100644 index 00000000000000..0967ef424bce67 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-throw/error-codes.json @@ -0,0 +1 @@ +{} diff --git a/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-throw/input.js b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-throw/input.js new file mode 100644 index 00000000000000..865fba71e2aa10 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/__fixtures__/unminifyable-throw/input.js @@ -0,0 +1,4 @@ +const foo = 'foo'; +const bar = ['bar']; +throw /* minify-error */ new Error(foo); +throw /* minify-error */ new Error(...bar); diff --git a/packages-internal/babel-plugin-minify-errors/index.js b/packages-internal/babel-plugin-minify-errors/index.js new file mode 100644 index 00000000000000..390c70a545d0e0 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/index.js @@ -0,0 +1,225 @@ +// @ts-check + +const helperModuleImports = require('@babel/helper-module-imports'); +const fs = require('fs'); + +const COMMENT_MARKER = 'minify-error'; + +/** + * @typedef {import('@babel/core')} babel + */ + +/** + * @typedef {{updatedErrorCodes?: boolean, formatMuiErrorMessageIdentifier?: babel.types.Identifier}} PluginState + * @typedef {'annotate' | 'throw' | 'write'} MissingError + * @typedef {{ errorCodesPath: string, missingError: MissingError }} Options + */ + +/** + * + * @param {babel.types} t + * @param {babel.types.Node} node + * @returns {{ message: string, expressions: babel.types.Expression[] } | null} + */ +function extractMessageFromExpression(t, node) { + if (t.isTemplateLiteral(node)) { + return { + message: node.quasis.map((quasi) => quasi.value.cooked).join('%s'), + expressions: node.expressions.map((expression) => { + if (t.isExpression(expression)) { + return expression; + } + throw new Error('Can only evaluate javascript template literals.'); + }), + }; + } + if (t.isStringLiteral(node)) { + return { message: node.value, expressions: [] }; + } + if (t.isBinaryExpression(node) && node.operator === '+') { + if (t.isPrivateName(node.left)) { + // This is only psossible with `in` expressions, e.g. `#foo in {}` + throw new Error('Unreachable'); + } + const left = extractMessageFromExpression(t, node.left); + const right = extractMessageFromExpression(t, node.right); + if (!left || !right) { + return null; + } + return { + message: left.message + right.message, + expressions: [...left.expressions, ...right.expressions], + }; + } + return null; +} + +/** + * + * @param {MissingError} missingError + * @param {babel.NodePath} path + * @returns + */ +function handleUnminifyable(missingError, path) { + switch (missingError) { + case 'annotate': { + // Outputs: + // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ + // throw new Error(foo) + path.addComment( + 'leading', + ' FIXME (minify-errors-in-prod): Unminifyable error in production! ', + ); + return; + } + case 'throw': { + throw new Error( + `Unminifyable error. You can only use literal strings and template strings as error messages.`, + ); + } + case 'write': { + return; + } + default: { + throw new Error(`Unknown missingError option: ${missingError}`); + } + } +} + +/** + * @param {babel} file + * @param {Options} options + * @returns {babel.PluginObj} + */ +module.exports = function plugin({ types: t }, { errorCodesPath, missingError = 'annotate' }) { + if (!errorCodesPath) { + throw new Error('errorCodesPath is required.'); + } + + const errorCodesContent = fs.readFileSync(errorCodesPath, 'utf8'); + const errorCodes = JSON.parse(errorCodesContent); + + const errorCodesLookup = new Map( + Object.entries(errorCodes).map(([key, value]) => [value, Number(key)]), + ); + + return { + visitor: { + NewExpression(newExpressionPath, state) { + if (!newExpressionPath.get('callee').isIdentifier({ name: 'Error' })) { + return; + } + + if ( + !newExpressionPath.node.leadingComments?.some((comment) => + comment.value.includes(COMMENT_MARKER), + ) + ) { + return; + } + + newExpressionPath.node.leadingComments = newExpressionPath.node.leadingComments.filter( + (comment) => !comment.value.includes(COMMENT_MARKER), + ); + + const messagePath = newExpressionPath.get('arguments')[0]; + if (!messagePath) { + return; + } + + const messageNode = messagePath.node; + if (t.isSpreadElement(messageNode) || t.isArgumentPlaceholder(messageNode)) { + handleUnminifyable(missingError, newExpressionPath); + return; + } + + const message = extractMessageFromExpression(t, messageNode); + + if (!message) { + handleUnminifyable(missingError, newExpressionPath); + return; + } + + let errorCode = errorCodesLookup.get(message.message); + if (errorCode === undefined) { + switch (missingError) { + case 'annotate': { + // Outputs: + // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ + // throw new Error(`A message with ${interpolation}`) + newExpressionPath.addComment( + 'leading', + ' FIXME (minify-errors-in-prod): Unminified error message in production build! ', + ); + return; + } + case 'throw': { + throw new Error( + `Missing error code for message '${message.message}'. Did you forget to run \`pnpm extract-error-codes\` first?`, + ); + } + case 'write': { + errorCode = errorCodesLookup.size + 1; + errorCodesLookup.set(message.message, errorCode); + state.updatedErrorCodes = true; + break; + } + default: { + throw new Error(`Unknown missingError option: ${missingError}`); + } + } + } + + if (!state.formatMuiErrorMessageIdentifier) { + // Outputs: + // import { formatMuiErrorMessage } from '@mui/utils'; + state.formatMuiErrorMessageIdentifier = helperModuleImports.addDefault( + newExpressionPath, + '@mui/utils/formatMuiErrorMessage', + { nameHint: '_formatMuiErrorMessage' }, + ); + } + + // Outputs: + // `A ${adj} message that contains ${noun}`; + const devMessage = messageNode; + + // Outputs: + // formatMuiErrorMessage(ERROR_CODE, adj, noun) + const prodMessage = t.callExpression( + t.cloneNode(state.formatMuiErrorMessageIdentifier, true), + [t.numericLiteral(errorCode), ...message.expressions], + ); + + // Outputs: + // new Error( + // process.env.NODE_ENV !== "production" + // ? `A message with ${interpolation}` + // : formatProdError('A message with %s', interpolation) + // ) + messagePath.replaceWith( + t.conditionalExpression( + t.binaryExpression( + '!==', + t.memberExpression( + t.memberExpression(t.identifier('process'), t.identifier('env')), + t.identifier('NODE_ENV'), + ), + t.stringLiteral('production'), + ), + devMessage, + prodMessage, + ), + ); + }, + }, + post() { + if (missingError === 'write' && this.updatedErrorCodes) { + const invertedErrorCodes = Object.fromEntries( + Array.from(errorCodesLookup, ([key, value]) => [value, key]), + ); + fs.writeFileSync(errorCodesPath, `${JSON.stringify(invertedErrorCodes, null, 2)}\n`); + } + }, + }; +}; diff --git a/packages/mui-babel-macros/MuiError.macro.test.js b/packages-internal/babel-plugin-minify-errors/index.test.js similarity index 62% rename from packages/mui-babel-macros/MuiError.macro.test.js rename to packages-internal/babel-plugin-minify-errors/index.test.js index cf2cbb3aee402d..4d8b36c5c68882 100644 --- a/packages/mui-babel-macros/MuiError.macro.test.js +++ b/packages-internal/babel-plugin-minify-errors/index.test.js @@ -2,8 +2,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { pluginTester } from 'babel-plugin-tester'; -import plugin from 'babel-plugin-macros'; import { expect } from 'chai'; +import plugin from './index'; const temporaryErrorCodesPath = path.join(os.tmpdir(), 'error-codes.json'); const fixturePath = path.resolve(__dirname, './__fixtures__'); @@ -22,17 +22,23 @@ pluginTester({ { title: 'literal', pluginOptions: { - muiError: { errorCodesPath: path.join(fixturePath, 'literal', 'error-codes.json') }, + errorCodesPath: path.join(fixturePath, 'literal', 'error-codes.json'), }, fixture: path.join(fixturePath, 'literal', 'input.js'), output: readOutputFixtureSync('literal', 'output.js'), }, + { + title: 'interpolation', + pluginOptions: { + errorCodesPath: path.join(fixturePath, 'interpolation', 'error-codes.json'), + }, + fixture: path.join(fixturePath, 'interpolation', 'input.js'), + output: readOutputFixtureSync('interpolation', 'output.js'), + }, { title: 'annotates missing error codes', pluginOptions: { - muiError: { - errorCodesPath: path.join(fixturePath, 'no-error-code-annotation', 'error-codes.json'), - }, + errorCodesPath: path.join(fixturePath, 'no-error-code-annotation', 'error-codes.json'), }, fixture: path.join(fixturePath, 'no-error-code-annotation', 'input.js'), output: readOutputFixtureSync('no-error-code-annotation', 'output.js'), @@ -45,10 +51,28 @@ pluginTester({ /: Missing error code for message 'missing'. Did you forget to run `pnpm extract-error-codes` first?/, fixture: path.join(fixturePath, 'no-error-code-throw', 'input.js'), pluginOptions: { - muiError: { - errorCodesPath: path.join(fixturePath, 'no-error-code-throw', 'error-codes.json'), - missingError: 'throw', - }, + errorCodesPath: path.join(fixturePath, 'no-error-code-throw', 'error-codes.json'), + missingError: 'throw', + }, + }, + { + title: 'annotates unminifyable errors', + pluginOptions: { + errorCodesPath: path.join(fixturePath, 'unminifyable-annotation', 'error-codes.json'), + }, + fixture: path.join(fixturePath, 'unminifyable-annotation', 'input.js'), + output: readOutputFixtureSync('unminifyable-annotation', 'output.js'), + }, + { + title: 'can throw on unminifyable errors', + // babel prefixes with filename. + // We're only interested in the message. + error: + /: Unminifyable error. You can only use literal strings and template strings as error messages.?/, + fixture: path.join(fixturePath, 'unminifyable-throw', 'input.js'), + pluginOptions: { + errorCodesPath: path.join(fixturePath, 'unminifyable-throw', 'error-codes.json'), + missingError: 'throw', }, }, { @@ -56,10 +80,8 @@ pluginTester({ fixture: path.join(fixturePath, 'error-code-extraction', 'input.js'), pluginOptions: { - muiError: { - errorCodesPath: temporaryErrorCodesPath, - missingError: 'write', - }, + errorCodesPath: temporaryErrorCodesPath, + missingError: 'write', }, output: readOutputFixtureSync('error-code-extraction', 'output.js'), setup() { @@ -86,16 +108,5 @@ pluginTester({ }; }, }, - { - title: 'throws if not called as a constructor', - error: - /: Encountered `MuiError` outside of a "new expression" i\.e\. `new MuiError\(\)`\. Use `throw new MuiError\(message\)` over `throw MuiError\(message\)`\./, - fixture: path.join(fixturePath, 'factory-call', 'input.js'), - pluginOptions: { - muiError: { - errorCodesPath: path.join(fixturePath, 'factory-call', 'error-codes.json'), - }, - }, - }, ], }); diff --git a/packages-internal/babel-plugin-minify-errors/package.json b/packages-internal/babel-plugin-minify-errors/package.json new file mode 100644 index 00000000000000..07fb856bb99d85 --- /dev/null +++ b/packages-internal/babel-plugin-minify-errors/package.json @@ -0,0 +1,41 @@ +{ + "name": "@mui/internal-babel-plugin-minify-errors", + "version": "1.0.9", + "author": "MUI Team", + "description": "This is an internal package not meant for general use.", + "repository": { + "type": "git", + "url": "git+https://github.com/mui/material-ui.git", + "directory": "packages/mui-babel-plugin-minify-errors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/mui/material-ui/issues" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "scripts": { + "test": "cd ../../ && cross-env NODE_ENV=test mocha 'packages-internal/babel-plugin-minify-errors/**/*.test.{js,ts,tsx}'" + }, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7" + }, + "devDependencies": { + "@types/babel__helper-module-imports": "^7.18.3", + "babel-plugin-tester": "^11.0.4", + "chai": "^4.5.0" + }, + "sideEffects": false, + "type": "commonjs", + "exports": { + ".": "./index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/mui-babel-macros/CHANGELOG.md b/packages/mui-babel-macros/CHANGELOG.md deleted file mode 100644 index a6c93f9bf457a6..00000000000000 --- a/packages/mui-babel-macros/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# Changelog - -## 1.0.0 - -Initial release as an npm package. diff --git a/packages/mui-babel-macros/MuiError.macro.d.ts b/packages/mui-babel-macros/MuiError.macro.d.ts deleted file mode 100644 index 54293408e59b1a..00000000000000 --- a/packages/mui-babel-macros/MuiError.macro.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default class MuiError { - constructor(message: string, ...args: string[]); -} diff --git a/packages/mui-babel-macros/MuiError.macro.js b/packages/mui-babel-macros/MuiError.macro.js deleted file mode 100644 index 70a77e3595f5cd..00000000000000 --- a/packages/mui-babel-macros/MuiError.macro.js +++ /dev/null @@ -1,188 +0,0 @@ -const fs = require('fs'); -const { createMacro, MacroError } = require('babel-plugin-macros'); -const helperModuleImports = require('@babel/helper-module-imports'); - -function invertObject(object) { - const inverted = {}; - Object.keys(object).forEach((key) => { - inverted[object[key]] = key; - }); - return inverted; -} - -/** - * Supported import: - * Bare specifier `'@mui/internal-babel-macros/MuiError.macro'` - * @param {import('babel-plugin-macros').MacroParams} param0 - */ -function muiError({ references, babel, config, source }) { - const { errorCodesPath = {}, missingError = 'annotate' } = config; - const errorCodes = JSON.parse(fs.readFileSync(errorCodesPath, { encoding: 'utf8' })); - const errorCodesLookup = invertObject(errorCodes); - let updatedErrorCodes = false; - - let handleMissingErrorCode; - switch (missingError) { - case 'annotate': - handleMissingErrorCode = ({ devMessage, newExpressionPath }) => { - // Outputs: - // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ - // throw new Error(`A message with ${interpolation}`) - newExpressionPath.replaceWith( - babel.types.newExpression(babel.types.identifier('Error'), [devMessage]), - ); - newExpressionPath.addComment( - 'leading', - ' FIXME (minify-errors-in-prod): Unminified error message in production build! ', - ); - }; - break; - case 'throw': - handleMissingErrorCode = ({ errorMessageLiteral }) => { - throw new MacroError( - `Missing error code for message '${errorMessageLiteral}'. Did you forget to run \`pnpm extract-error-codes\` first?`, - ); - }; - break; - case 'write': - handleMissingErrorCode = ({ errorMessageLiteral }) => { - updatedErrorCodes = true; - // error codes are 1-based - const newErrorCode = Object.keys(errorCodesLookup).length + 1; - errorCodesLookup[errorMessageLiteral] = newErrorCode; - return newErrorCode; - }; - break; - default: - throw new MacroError( - `Unknown missing error behavior '${missingError}'. Can only handle 'annotate', 'throw' and 'write'.`, - ); - } - - /** - * Evaluates a babel node as a string. - * - * Supported nodes - * - `'just a literal'` - * - `'a literal' + 'concatenated' + 'with +'` - * Cannot evaluate template literals or Array.prototype.join etc. - * - * @param {import('@babel/core').types.Node} node - */ - function evaluateMessage(node) { - if (babel.types.isBinaryExpression(node)) { - if (node.operator !== '+') { - throw new Error(`Unsupported binary operator '${node.operator}'. Can only evaluate '+'.`); - } - return `${evaluateMessage(node.left, babel)}${evaluateMessage(node.right, babel)}`; - } - if (babel.types.isStringLiteral(node)) { - return node.value; - } - throw new Error('Can only evaluate strings that are concatenated with `+` or string literals.'); - } - - /** - * The identifier for the callee in `formatMuiErrorMessage()` - * Creating an identifier per MuiError reference would create duplicate imports. - * It's not obvious that these will be deduplicated by bundlers. - * We can already do this at transpile-time - * - * @type {import('@babel/core').NodePath | null} - */ - let formatMuiErrorMessageIdentifier = null; - - references.default.forEach((babelPath) => { - const newExpressionPath = babelPath.parentPath; - if (!newExpressionPath.isNewExpression()) { - throw new MacroError( - 'Encountered `MuiError` outside of a "new expression" i.e. `new MuiError()`. Use `throw new MuiError(message)` over `throw MuiError(message)`.', - ); - } - - const errorMessageLiteral = evaluateMessage(newExpressionPath.node.arguments[0]); - const errorMessageExpressions = newExpressionPath.node.arguments.slice(1); - const errorMessageQuasis = errorMessageLiteral - .split('%s') - // Providing `cooked` here is important. - // Otherwise babel will generate "" with NODE_ENV=test - // - // Original code used `cooked: String.raw({ raw: cooked })` - // Thought it's unclear what for. - // 'One line\nNext line' will end up as `One line - // Next line` which is what you'd want from that literal. - .map((cooked) => babel.types.templateElement({ raw: cooked.replace(/`/g, '\\`'), cooked })); - - // Outputs: - // `A ${adj} message that contains ${noun}`; - const devMessage = babel.types.templateLiteral(errorMessageQuasis, errorMessageExpressions); - - let errorCode = errorCodesLookup[errorMessageLiteral]; - if (errorCode === undefined) { - errorCode = handleMissingErrorCode({ devMessage, errorMessageLiteral, newExpressionPath }); - if (errorCode === undefined) { - return; - } - } - errorCode = parseInt(errorCode, 10); - - if (formatMuiErrorMessageIdentifier === null) { - const isBareImportSourceIdentifier = source.startsWith('@mui/internal-babel-macros'); - if (isBareImportSourceIdentifier) { - // Input: import MuiError from '@mui/internal-babel-macros/MuiError.macro' - // Outputs: - // import { formatMuiErrorMessage } from '@mui/utils'; - formatMuiErrorMessageIdentifier = helperModuleImports.addDefault( - babelPath, - '@mui/utils/formatMuiErrorMessage', - { nameHint: '_formatMuiErrorMessage' }, - ); - } else { - throw new Error('Only package imports from @mui/internal-babel-macros are supported'); - } - } - - // Outputs: - // formatMuiErrorMessage(ERROR_CODE, adj, noun) - const prodMessage = babel.types.callExpression( - babel.types.cloneDeep(formatMuiErrorMessageIdentifier), - [babel.types.numericLiteral(errorCode), ...errorMessageExpressions], - ); - - // Outputs: - // new Error( - // process.env.NODE_ENV !== "production" - // ? `A message with ${interpolation}` - // : formatProdError('A message with %s', interpolation) - // ) - newExpressionPath.replaceWith( - babel.types.newExpression(babel.types.identifier('Error'), [ - babel.types.conditionalExpression( - babel.types.binaryExpression( - '!==', - babel.types.memberExpression( - babel.types.memberExpression( - babel.types.identifier('process'), - babel.types.identifier('env'), - ), - babel.types.identifier('NODE_ENV'), - ), - babel.types.stringLiteral('production'), - ), - devMessage, - prodMessage, - ), - ]), - ); - }); - - if (missingError === 'write' && updatedErrorCodes) { - fs.writeFileSync(errorCodesPath, JSON.stringify(invertObject(errorCodesLookup), null, 2)); - } - - return { keepImports: false }; -} - -module.exports = createMacro(muiError, { - configName: 'muiError', -}); diff --git a/packages/mui-babel-macros/README.md b/packages/mui-babel-macros/README.md deleted file mode 100644 index 4fcfb319018975..00000000000000 --- a/packages/mui-babel-macros/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @mui/internal-babel-macros - -This package contains the error macro used in MUI projects. -This is an internal package not meant for general use. - -## Release - -There is no build step. -To publish the package to npm, run: `pnpm release:publish` diff --git a/packages/mui-babel-macros/__fixtures__/error-code-extraction/input.js b/packages/mui-babel-macros/__fixtures__/error-code-extraction/input.js deleted file mode 100644 index 69a30d073b7136..00000000000000 --- a/packages/mui-babel-macros/__fixtures__/error-code-extraction/input.js +++ /dev/null @@ -1,4 +0,0 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - -throw new MuiError('exists'); -throw new MuiError('will be created'); diff --git a/packages/mui-babel-macros/__fixtures__/factory-call/input.js b/packages/mui-babel-macros/__fixtures__/factory-call/input.js deleted file mode 100644 index c4957eb663baad..00000000000000 --- a/packages/mui-babel-macros/__fixtures__/factory-call/input.js +++ /dev/null @@ -1,4 +0,0 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - -// `throw Error(message)` is valid JS but we limit error construction to a single syntax. -throw MuiError('my message'); diff --git a/packages/mui-babel-macros/__fixtures__/literal/input.js b/packages/mui-babel-macros/__fixtures__/literal/input.js deleted file mode 100644 index 2ed3398ceb8021..00000000000000 --- a/packages/mui-babel-macros/__fixtures__/literal/input.js +++ /dev/null @@ -1,3 +0,0 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - -throw new MuiError('MUI: Expected valid input target.\n' + 'Did you use `inputComponent`'); diff --git a/packages/mui-babel-macros/__fixtures__/literal/output.js b/packages/mui-babel-macros/__fixtures__/literal/output.js deleted file mode 100644 index 9ca62d0007b57c..00000000000000 --- a/packages/mui-babel-macros/__fixtures__/literal/output.js +++ /dev/null @@ -1,7 +0,0 @@ -import _formatMuiErrorMessage from '@mui/utils/formatMuiErrorMessage'; -throw new Error( - process.env.NODE_ENV !== 'production' - ? `MUI: Expected valid input target. -Did you use \`inputComponent\`` - : _formatMuiErrorMessage(1), -); diff --git a/packages/mui-babel-macros/__fixtures__/no-error-code-annotation/input.js b/packages/mui-babel-macros/__fixtures__/no-error-code-annotation/input.js deleted file mode 100644 index 922dbfba609578..00000000000000 --- a/packages/mui-babel-macros/__fixtures__/no-error-code-annotation/input.js +++ /dev/null @@ -1,3 +0,0 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - -throw new MuiError('MUI: Expected valid input target.\n' + 'Did you use inputComponent'); diff --git a/packages/mui-babel-macros/__fixtures__/no-error-code-annotation/output.js b/packages/mui-babel-macros/__fixtures__/no-error-code-annotation/output.js deleted file mode 100644 index 0d694b13d4eef7..00000000000000 --- a/packages/mui-babel-macros/__fixtures__/no-error-code-annotation/output.js +++ /dev/null @@ -1,2 +0,0 @@ -throw /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ new Error(`MUI: Expected valid input target. -Did you use inputComponent`); diff --git a/packages/mui-babel-macros/__fixtures__/no-error-code-throw/input.js b/packages/mui-babel-macros/__fixtures__/no-error-code-throw/input.js deleted file mode 100644 index d804f33d59c04d..00000000000000 --- a/packages/mui-babel-macros/__fixtures__/no-error-code-throw/input.js +++ /dev/null @@ -1,3 +0,0 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - -throw new MuiError('missing'); diff --git a/packages/mui-babel-macros/package.json b/packages/mui-babel-macros/package.json deleted file mode 100644 index f10caf897c1008..00000000000000 --- a/packages/mui-babel-macros/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@mui/internal-babel-macros", - "version": "1.0.9", - "author": "MUI Team", - "description": "MUI Babel macros. This is an internal package not meant for general use.", - "main": "./MuiError.macro.js", - "repository": { - "type": "git", - "url": "git+https://github.com/mui/material-ui.git", - "directory": "packages/mui-babel-macros" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/mui/material-ui/issues" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "scripts": { - "release:publish": "pnpm publish --tag latest", - "release:publish:dry-run": "pnpm publish --tag latest --registry=\"http://localhost:4873/\"", - "test": "cd ../../ && cross-env NODE_ENV=test mocha 'packages/mui-babel-macros/**/*.test.{js,ts,tsx}'" - }, - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/runtime": "^7.25.6", - "babel-plugin-macros": "^3.1.0" - }, - "devDependencies": { - "@mui/internal-babel-macros": "workspace:*", - "@types/babel-plugin-macros": "^3.1.3", - "babel-plugin-tester": "^11.0.4", - "chai": "^4.5.0" - }, - "peerDependencies": { - "@mui/utils": "^5.0.0 || ^6.0.0" - }, - "sideEffects": false, - "engines": { - "node": ">=14.0.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/mui-base/package.json b/packages/mui-base/package.json index 798139cc45b2ee..39d5865ce565d1 100644 --- a/packages/mui-base/package.json +++ b/packages/mui-base/package.json @@ -49,7 +49,6 @@ "prop-types": "^15.8.1" }, "devDependencies": { - "@mui/internal-babel-macros": "workspace:^", "@mui/internal-test-utils": "workspace:^", "@mui/types": "workspace:^", "@testing-library/react": "^16.0.1", diff --git a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts index 1129b7013fd1e7..3c3dc249904d5e 100644 --- a/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts +++ b/packages/mui-base/src/unstable_useNumberInput/useNumberInput.ts @@ -1,6 +1,5 @@ 'use client'; import * as React from 'react'; -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import { unstable_useForkRef as useForkRef, unstable_useId as useId } from '@mui/utils'; import { extractEventHandlers } from '../utils/extractEventHandlers'; import { MuiCancellableEvent } from '../utils/MuiCancellableEvent'; @@ -185,7 +184,7 @@ export function useNumberInput(parameters: UseNumberInputParameters): UseNumberI (otherHandlers: Partial) => (event: React.ChangeEvent & MuiCancellableEvent) => { if (!isControlled && event.target === null) { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: Expected valid input target. ' + 'Did you use a custom `slots.input` and forget to forward refs? ' + 'See https://mui.com/r/input-component-ref-interface for more info.', diff --git a/packages/mui-base/src/useInput/useInput.ts b/packages/mui-base/src/useInput/useInput.ts index 146d7f0882232d..a7bf95138a5b6c 100644 --- a/packages/mui-base/src/useInput/useInput.ts +++ b/packages/mui-base/src/useInput/useInput.ts @@ -1,6 +1,5 @@ 'use client'; import * as React from 'react'; -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; import { FormControlState, useFormControlContext } from '../FormControl'; import { extractEventHandlers } from '../utils/extractEventHandlers'; @@ -140,7 +139,7 @@ export function useInput(parameters: UseInputParameters = {}): UseInputReturnVal if (!isControlled) { const element = event.target || inputRef.current; if (element == null) { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: Expected valid input target. ' + 'Did you use a custom `slots.input` and forget to forward refs? ' + 'See https://mui.com/r/input-component-ref-interface for more info.', diff --git a/packages/mui-material/package.json b/packages/mui-material/package.json index b277c097237c9f..390a42b4a1a60e 100644 --- a/packages/mui-material/package.json +++ b/packages/mui-material/package.json @@ -54,7 +54,6 @@ "react-transition-group": "^4.4.5" }, "devDependencies": { - "@mui/internal-babel-macros": "workspace:^", "@mui/internal-test-utils": "workspace:^", "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.5.2", diff --git a/packages/mui-material/src/InputBase/InputBase.js b/packages/mui-material/src/InputBase/InputBase.js index 7333c1c4f32cf5..54e1c8babea2a2 100644 --- a/packages/mui-material/src/InputBase/InputBase.js +++ b/packages/mui-material/src/InputBase/InputBase.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import elementTypeAcceptingRef from '@mui/utils/elementTypeAcceptingRef'; import refType from '@mui/utils/refType'; -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import composeClasses from '@mui/utils/composeClasses'; import TextareaAutosize from '../TextareaAutosize'; import isHostComponent from '../utils/isHostComponent'; @@ -425,7 +424,7 @@ const InputBase = React.forwardRef(function InputBase(inProps, ref) { if (!isControlled) { const element = event.target || inputRef.current; if (element == null) { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: Expected valid input target. ' + 'Did you use a custom `inputComponent` and forget to forward refs? ' + 'See https://mui.com/r/input-component-ref-interface for more info.', diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js index 27bf3ede4f1818..e8e7dc989c6808 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -3,7 +3,6 @@ import * as React from 'react'; import { isFragment } from 'react-is'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import composeClasses from '@mui/utils/composeClasses'; import useId from '@mui/utils/useId'; import refType from '@mui/utils/refType'; @@ -372,7 +371,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { if (multiple) { if (!Array.isArray(value)) { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: The `value` prop must be an array ' + 'when using the `Select` component with `multiple`.', ); diff --git a/packages/mui-material/src/styles/createPalette.js b/packages/mui-material/src/styles/createPalette.js index 949c41e7a8d6e2..d3c47d9dfa9b32 100644 --- a/packages/mui-material/src/styles/createPalette.js +++ b/packages/mui-material/src/styles/createPalette.js @@ -1,5 +1,4 @@ import deepmerge from '@mui/utils/deepmerge'; -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import { darken, getContrastRatio, lighten } from '@mui/system/colorManipulator'; import common from '../colors/common'; import grey from '../colors/grey'; @@ -223,18 +222,16 @@ export default function createPalette(palette) { } if (!color.hasOwnProperty('main')) { - throw new MuiError( - 'MUI: The color%s provided to augmentColor(color) is invalid.\n' + - 'The color object needs to have a `main` property or a `%s` property.', - name ? ` (${name})` : '', - mainShade, + throw /* minify-error */ new Error( + `MUI: The color${name ? ` (${name})` : ''} provided to augmentColor(color) is invalid.\n` + + `The color object needs to have a \`main\` property or a \`${mainShade}\` property.`, ); } if (typeof color.main !== 'string') { - throw new MuiError( - 'MUI: The color%s provided to augmentColor(color) is invalid.\n' + - '`color.main` should be a string, but `%s` was provided instead.\n' + + throw /* minify-error */ new Error( + `MUI: The color${name ? ` (${name})` : ''} provided to augmentColor(color) is invalid.\n` + + `\`color.main\` should be a string, but \`${JSON.stringify(color.main)}\` was provided instead.\n` + '\n' + 'Did you intend to use one of the following approaches?\n' + '\n' + @@ -247,8 +244,6 @@ export default function createPalette(palette) { 'const theme2 = createTheme({ palette: {\n' + ' primary: { main: green[500] },\n' + '} });', - name ? ` (${name})` : '', - JSON.stringify(color.main), ); } diff --git a/packages/mui-material/src/styles/createThemeNoVars.js b/packages/mui-material/src/styles/createThemeNoVars.js index 77e5abdeafba79..713c3db3f57a76 100644 --- a/packages/mui-material/src/styles/createThemeNoVars.js +++ b/packages/mui-material/src/styles/createThemeNoVars.js @@ -3,7 +3,6 @@ import styleFunctionSx, { unstable_defaultSxConfig as defaultSxConfig, } from '@mui/system/styleFunctionSx'; import systemCreateTheme from '@mui/system/createTheme'; -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import generateUtilityClass from '@mui/utils/generateUtilityClass'; import createMixins from './createMixins'; import createPalette from './createPalette'; @@ -25,7 +24,7 @@ function createThemeNoVars(options = {}, ...args) { } = options; if (options.vars) { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: `vars` is a private field used for CSS variables support.\n' + 'Please use another name.', ); diff --git a/packages/mui-material/src/styles/createThemeWithVars.js b/packages/mui-material/src/styles/createThemeWithVars.js index 1da4352a2315d1..9e1fd9f76707e9 100644 --- a/packages/mui-material/src/styles/createThemeWithVars.js +++ b/packages/mui-material/src/styles/createThemeWithVars.js @@ -1,4 +1,3 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import deepmerge from '@mui/utils/deepmerge'; import { unstable_createGetCssVar as systemCreateGetCssVar, createSpacing } from '@mui/system'; import { createUnarySpacing } from '@mui/system/spacing'; @@ -158,9 +157,8 @@ export default function createThemeWithVars(options = {}, ...args) { } if (!defaultScheme) { - throw new MuiError( - 'MUI: The `colorSchemes.%s` option is either missing or invalid.', - defaultColorScheme, + throw /* minify-error */ new Error( + `MUI: The \`colorSchemes.${defaultColorScheme}\` option is either missing or invalid.`, ); } diff --git a/packages/mui-material/src/styles/index.js b/packages/mui-material/src/styles/index.js index e856d1d5d2fa43..d98971ae31a471 100644 --- a/packages/mui-material/src/styles/index.js +++ b/packages/mui-material/src/styles/index.js @@ -1,5 +1,3 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - export { default as THEME_ID } from './identifier'; export { default as adaptV4Theme } from './adaptV4Theme'; export { @@ -21,7 +19,7 @@ export { unstable_createBreakpoints } from '@mui/system/createBreakpoints'; // TODO: Remove this function in v6. // eslint-disable-next-line @typescript-eslint/naming-convention export function experimental_sx() { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: The `experimental_sx` has been moved to `theme.unstable_sx`.' + 'For more details, see https://github.com/mui/material-ui/pull/35150.', ); diff --git a/packages/mui-material/src/styles/makeStyles.js b/packages/mui-material/src/styles/makeStyles.js index 380bbc39f5a8c8..7a22383ce7537a 100644 --- a/packages/mui-material/src/styles/makeStyles.js +++ b/packages/mui-material/src/styles/makeStyles.js @@ -1,7 +1,5 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - export default function makeStyles() { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: makeStyles is no longer exported from @mui/material/styles.\n' + 'You have to import it from @mui/styles.\n' + 'See https://mui.com/r/migration-v4/#mui-material-styles for more details.', diff --git a/packages/mui-material/src/styles/responsiveFontSizes.js b/packages/mui-material/src/styles/responsiveFontSizes.js index e01b0a3e186c5c..63d343fd8e328b 100644 --- a/packages/mui-material/src/styles/responsiveFontSizes.js +++ b/packages/mui-material/src/styles/responsiveFontSizes.js @@ -1,4 +1,3 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import { isUnitless, convertLength, responsiveProperty, alignProperty, fontGrid } from './cssUtils'; export default function responsiveFontSizes(themeInput, options = {}) { @@ -51,7 +50,7 @@ export default function responsiveFontSizes(themeInput, options = {}) { let { lineHeight } = style; if (!isUnitless(lineHeight) && !disableAlign) { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: Unsupported non-unitless line height with grid alignment.\n' + 'Use unitless line heights instead.', ); diff --git a/packages/mui-material/src/styles/withStyles.js b/packages/mui-material/src/styles/withStyles.js index b5064fae0a4d51..b8ca2dcb5df811 100644 --- a/packages/mui-material/src/styles/withStyles.js +++ b/packages/mui-material/src/styles/withStyles.js @@ -1,7 +1,5 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - export default function withStyles() { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: withStyles is no longer exported from @mui/material/styles.\n' + 'You have to import it from @mui/styles.\n' + 'See https://mui.com/r/migration-v4/#mui-material-styles for more details.', diff --git a/packages/mui-material/src/styles/withTheme.js b/packages/mui-material/src/styles/withTheme.js index 1b24aa0c8feeb8..3c5d8f5ae62a1e 100644 --- a/packages/mui-material/src/styles/withTheme.js +++ b/packages/mui-material/src/styles/withTheme.js @@ -1,7 +1,5 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - export default function withTheme() { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: withTheme is no longer exported from @mui/material/styles.\n' + 'You have to import it from @mui/styles.\n' + 'See https://mui.com/r/migration-v4/#mui-material-styles for more details.', diff --git a/packages/mui-system/package.json b/packages/mui-system/package.json index 25c12aca617eee..f197586386bf1a 100644 --- a/packages/mui-system/package.json +++ b/packages/mui-system/package.json @@ -51,7 +51,6 @@ "devDependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@mui/internal-babel-macros": "workspace:^", "@mui/internal-test-utils": "workspace:^", "@mui/system": "workspace:*", "@types/chai": "^4.3.20", diff --git a/packages/mui-system/src/colorManipulator/colorManipulator.js b/packages/mui-system/src/colorManipulator/colorManipulator.js index 721949fb2a296e..3bc8b02359ced3 100644 --- a/packages/mui-system/src/colorManipulator/colorManipulator.js +++ b/packages/mui-system/src/colorManipulator/colorManipulator.js @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ import clamp from '@mui/utils/clamp'; -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; /** * Returns a number whose value is limited to the given range. @@ -69,10 +68,9 @@ export function decomposeColor(color) { const type = color.substring(0, marker); if (!['rgb', 'rgba', 'hsl', 'hsla', 'color'].includes(type)) { - throw new MuiError( - 'MUI: Unsupported `%s` color.\n' + + throw /* minify-error */ new Error( + `MUI: Unsupported \`${color}\` color.\n` + 'The following formats are supported: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color().', - color, ); } @@ -86,10 +84,9 @@ export function decomposeColor(color) { values[3] = values[3].slice(1); } if (!['srgb', 'display-p3', 'a98-rgb', 'prophoto-rgb', 'rec-2020'].includes(colorSpace)) { - throw new MuiError( - 'MUI: unsupported `%s` color space.\n' + + throw /* minify-error */ new Error( + `MUI: unsupported \`${colorSpace}\` color space.\n` + 'The following color spaces are supported: srgb, display-p3, a98-rgb, prophoto-rgb, rec-2020.', - colorSpace, ); } } else { diff --git a/packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts b/packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts index c2e3f284f92be0..532ca044dda7ed 100644 --- a/packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts +++ b/packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts @@ -1,4 +1,3 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; import { Breakpoints, Breakpoint } from '../createBreakpoints/createBreakpoints'; interface ContainerQueries { @@ -58,10 +57,9 @@ export function getContainerQuery(theme: CssContainerQueries, shorthand: string) const matches = shorthand.match(/^@([^/]+)?\/?(.+)?$/); if (!matches) { if (process.env.NODE_ENV !== 'production') { - throw new MuiError( - 'MUI: The provided shorthand %s is invalid. The format should be `@` or `@/`.\n' + + throw /* minify-error */ new Error( + `MUI: The provided shorthand ${`(${shorthand})`} is invalid. The format should be \`@\` or \`@/\`.\n` + 'For example, `@sm` or `@600` or `@40rem/sidebar`.', - `(${shorthand})`, ); } return null; diff --git a/packages/mui-system/src/index.js b/packages/mui-system/src/index.js index 67691e7d7e3a4f..4a4bb930f387fd 100644 --- a/packages/mui-system/src/index.js +++ b/packages/mui-system/src/index.js @@ -1,5 +1,3 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - export { css, keyframes, StyledEngineProvider } from '@mui/styled-engine'; export { default as GlobalStyles } from './GlobalStyles'; export { default as borders } from './borders'; @@ -38,7 +36,7 @@ export { // TODO: Remove this function in v6 // eslint-disable-next-line @typescript-eslint/naming-convention export function experimental_sx() { - throw new MuiError( + throw /* minify-error */ new Error( 'MUI: The `experimental_sx` has been moved to `theme.unstable_sx`.' + 'For more details, see https://github.com/mui/material-ui/pull/35150.', ); diff --git a/packages/mui-utils/package.json b/packages/mui-utils/package.json index cc3e94c9f8a7de..f10ff6c15193a4 100644 --- a/packages/mui-utils/package.json +++ b/packages/mui-utils/package.json @@ -46,7 +46,6 @@ "react-is": "^18.3.1" }, "devDependencies": { - "@mui/internal-babel-macros": "workspace:^", "@mui/internal-test-utils": "workspace:^", "@mui/types": "workspace:^", "@types/chai": "^4.3.20", diff --git a/packages/mui-utils/src/capitalize/capitalize.ts b/packages/mui-utils/src/capitalize/capitalize.ts index 6b3407ffb374ec..b4496bbbbd7def 100644 --- a/packages/mui-utils/src/capitalize/capitalize.ts +++ b/packages/mui-utils/src/capitalize/capitalize.ts @@ -1,12 +1,10 @@ -import MuiError from '@mui/internal-babel-macros/MuiError.macro'; - // It should to be noted that this function isn't equivalent to `text-transform: capitalize`. // // A strict capitalization should uppercase the first letter of each word in the sentence. // We only handle the first word. export default function capitalize(string: string): string { if (typeof string !== 'string') { - throw new MuiError('MUI: `capitalize(string)` expects a string argument.'); + throw /* minify-error */ new Error('MUI: `capitalize(string)` expects a string argument.'); } return string.charAt(0).toUpperCase() + string.slice(1); diff --git a/packages/mui-utils/src/formatMuiErrorMessage/formatMuiErrorMessage.ts b/packages/mui-utils/src/formatMuiErrorMessage/formatMuiErrorMessage.ts index 3118870f40e511..c4cdc505d7bbbe 100644 --- a/packages/mui-utils/src/formatMuiErrorMessage/formatMuiErrorMessage.ts +++ b/packages/mui-utils/src/formatMuiErrorMessage/formatMuiErrorMessage.ts @@ -1,19 +1,15 @@ /** - * WARNING: Don't import this directly. - * Use `MuiError` from `@mui/internal-babel-macros/MuiError.macro` instead. + * WARNING: Don't import this directly. It's imported by the code generated by + * `@mui/interal-babel-plugin-minify-errors`. Make sure to always use string literals in `Error` + * constructors to ensure the plugin works as expected. Supported patterns include: + * throw new Error('My message'); + * throw new Error(`My message: ${foo}`); + * throw new Error(`My message: ${foo}` + 'another string'); + * ... * @param {number} code */ -export default function formatMuiErrorMessage(code: number): string { - // Apply babel-plugin-transform-template-literals in loose mode - // loose mode is safe if we're concatenating primitives - // see https://babeljs.io/docs/en/babel-plugin-transform-template-literals#loose - /* eslint-disable prefer-template */ - let url = 'https://mui.com/production-error/?code=' + code; - for (let i = 1; i < arguments.length; i += 1) { - // rest params over-transpile for this case - // eslint-disable-next-line prefer-rest-params - url += '&args[]=' + encodeURIComponent(arguments[i]); - } - return 'Minified MUI error #' + code + '; visit ' + url + ' for the full message.'; - /* eslint-enable prefer-template */ +export default function formatMuiErrorMessage(code: number, ...args: string[]): string { + const url = new URL(`https://mui.com/production-error/?code=${code}`); + args.forEach((arg) => url.searchParams.append('args[]', arg)); + return `Minified MUI error #${code}; visit ${url} for the full message.`; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 977b254d71505b..fefd0d1dff6458 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,6 @@ importers: babel-plugin-istanbul: specifier: ^7.0.0 version: 7.0.0 - babel-plugin-macros: - specifier: ^3.1.0 - version: 3.1.0 babel-plugin-module-resolver: specifier: ^5.0.2 version: 5.0.2 @@ -907,6 +904,22 @@ importers: specifier: ^17.7.2 version: 17.7.2 + packages-internal/babel-plugin-minify-errors: + dependencies: + '@babel/helper-module-imports': + specifier: ^7.24.7 + version: 7.24.7 + devDependencies: + '@types/babel__helper-module-imports': + specifier: ^7.18.3 + version: 7.18.3 + babel-plugin-tester: + specifier: ^11.0.4 + version: 11.0.4(@babel/core@7.25.2) + chai: + specifier: ^4.5.0 + version: 4.5.0 + packages-internal/babel-plugin-resolve-imports: dependencies: '@babel/core': @@ -1273,34 +1286,6 @@ importers: specifier: ^4.5.0 version: 4.5.0 - packages/mui-babel-macros: - dependencies: - '@babel/helper-module-imports': - specifier: ^7.24.7 - version: 7.24.7 - '@babel/runtime': - specifier: ^7.25.6 - version: 7.25.6 - '@mui/utils': - specifier: ^5.0.0 || ^6.0.0 - version: 6.1.2(@types/react@18.3.10)(react@18.3.1) - babel-plugin-macros: - specifier: ^3.1.0 - version: 3.1.0 - devDependencies: - '@mui/internal-babel-macros': - specifier: workspace:* - version: 'link:' - '@types/babel-plugin-macros': - specifier: ^3.1.3 - version: 3.1.3 - babel-plugin-tester: - specifier: ^11.0.4 - version: 11.0.4(@babel/core@7.25.2) - chai: - specifier: ^4.5.0 - version: 4.5.0 - packages/mui-base: dependencies: '@babel/runtime': @@ -1325,9 +1310,6 @@ importers: specifier: ^15.8.1 version: 15.8.1 devDependencies: - '@mui/internal-babel-macros': - specifier: workspace:^ - version: link:../mui-babel-macros '@mui/internal-test-utils': specifier: workspace:^ version: link:../../packages-internal/test-utils @@ -1768,9 +1750,6 @@ importers: specifier: ^4.4.5 version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: - '@mui/internal-babel-macros': - specifier: workspace:^ - version: link:../mui-babel-macros '@mui/internal-test-utils': specifier: workspace:^ version: link:../../packages-internal/test-utils @@ -2105,9 +2084,6 @@ importers: '@emotion/styled': specifier: ^11.13.0 version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.10)(react@18.3.1))(@types/react@18.3.10)(react@18.3.1) - '@mui/internal-babel-macros': - specifier: workspace:^ - version: link:../mui-babel-macros '@mui/internal-test-utils': specifier: workspace:^ version: link:../../packages-internal/test-utils @@ -2177,9 +2153,6 @@ importers: specifier: ^18.3.1 version: 18.3.1 devDependencies: - '@mui/internal-babel-macros': - specifier: workspace:^ - version: link:../mui-babel-macros '@mui/internal-test-utils': specifier: workspace:^ version: link:../../packages-internal/test-utils @@ -5465,15 +5438,15 @@ packages: '@types/autosuggest-highlight@3.2.3': resolution: {integrity: sha512-8Mb21KWtpn6PvRQXjsKhrXIcxbSloGqNH50RntwGeJsGPW4xvNhfml+3kKulaKpO/7pgZfOmzsJz7VbepArlGQ==} - '@types/babel-plugin-macros@3.1.3': - resolution: {integrity: sha512-JU+MgpsHK3taY18mBETy5XlwY6LVngte7QXYzUuXEaaX0CN8dBqbjXtADe+gJmkSQE1FJHufzPj++OWZlhRmGw==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} '@types/babel__generator@7.6.4': resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} + '@types/babel__helper-module-imports@7.18.3': + resolution: {integrity: sha512-2pyr9Vlriessj2KI85SEF7qma8vA3vzquQMw3wn6kL5lsfjH/YxJ1Noytk4/FJElpYybUbyaC37CVfEgfyme9A==} + '@types/babel__template@7.4.1': resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} @@ -16804,10 +16777,6 @@ snapshots: '@types/autosuggest-highlight@3.2.3': {} - '@types/babel-plugin-macros@3.1.3': - dependencies: - '@types/babel__core': 7.20.5 - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.25.6 @@ -16820,6 +16789,11 @@ snapshots: dependencies: '@babel/types': 7.25.6 + '@types/babel__helper-module-imports@7.18.3': + dependencies: + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + '@types/babel__template@7.4.1': dependencies: '@babel/parser': 7.25.6