Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[code-infra] Setup error message minification #1463

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const path = require('path');

const errorCodesPath = path.resolve(__dirname, './docs/public/static/error-codes.json');
const errorCodesPath = path.resolve(__dirname, './docs/src/error-codes.json');
const missingError = process.env.MUI_EXTRACT_ERROR_CODES === 'true' ? 'write' : 'annotate';

function resolveAliasPath(relativeToBabelConf) {
Expand Down Expand Up @@ -39,15 +39,6 @@ module.exports = function getBabelConfig(api) {
];

const plugins = [
[
'babel-plugin-macros',
{
muiError: {
errorCodesPath,
missingError,
},
},
],
'babel-plugin-optimize-clsx',
[
'@babel/plugin-transform-runtime',
Expand All @@ -63,6 +54,15 @@ module.exports = function getBabelConfig(api) {
mode: 'unsafe-wrap',
},
],
[
'@mui/internal-babel-plugin-minify-errors',
{
errorCodesPath,
missingError,
runtimeModule: '#formatErrorMessage',
detection: 'opt-out',
},
],
];

const devPlugins = [
Expand Down
1 change: 0 additions & 1 deletion docs/public/static/error-codes.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';
import * as React from 'react';
import { useSearchParams } from 'next/navigation';

export interface ErrorDisplayProps {
msg: string;
}

function ErrorMessageWithArgs({ msg }: ErrorDisplayProps) {
const searchParams = useSearchParams();
return React.useMemo(() => {
const args = searchParams.getAll('args[]');
let index = 0;
return msg.replace(/%s/g, () => {
const replacement = args[index];
index += 1;
return replacement === undefined ? '[missing argument]' : replacement;
});
}, [msg, searchParams]);
}

/**
* Client component that interpollates arguments in an error message. Must be
* a client component because it reads the search params.
*/
export default function ErrorDisplay({ msg }: ErrorDisplayProps) {
const fallbackMsg = React.useMemo(() => msg.replace(/%s/g, '…'), [msg]);

return (
<code>
<React.Suspense fallback={fallbackMsg}>
<ErrorMessageWithArgs msg={msg} />
</React.Suspense>
</code>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Production error

{/**
@typedef Props
@property {string} msg The raw error message.
**/}

<Subtitle>Explanation for minified production error message.</Subtitle>
<Meta
name="description"
content="In the production build, error messages are minified to keep your application lightweight."
/>

For debugging, we recommend using the development build, which provides additional warnings about potential issues. If an exception occurs in the production build, this page will reconstruct the original error message.

The original error you encountered:

<ErrorDisplay msg={props.msg} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import { notFound } from 'next/navigation';
import PageContent from './PageContent.mdx';
import codes from '../../../../../error-codes.json';
import ErrorDisplay from './ErrorDisplay';

export const dynamicParams = false;

export async function generateStaticParams() {
return Object.keys(codes).map((code) => ({ code }));
}

export default async function ProductionError(props: {
params: Promise<{ code: string }>;
}) {
const params = await props.params;
const code = Number(params.code);

if (Number.isNaN(code)) {
notFound();
}

const msg = (codes as Partial<Record<string, string>>)[code];

if (!msg) {
notFound();
}

return <PageContent components={{ ErrorDisplay }} msg={msg} />;
}
52 changes: 52 additions & 0 deletions docs/src/error-codes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"1": "Base UI: <AlertDialog.Portal> is missing.",
"2": "Base UI: AccordionItemContext is missing. Accordion parts must be placed within <Accordion.Item>.",
"3": "Base UI: AccordionRootContext is missing. Accordion parts must be placed within <Accordion.Root>.",
"4": "Base UI: AlertDialogRootContext is missing. AlertDialog parts must be placed within <AlertDialog.Root>.",
"5": "Base UI: AvatarRootContext is missing. Avatar parts must be placed within <Avatar.Root>.",
"6": "Base UI: CheckboxRootContext is missing. Checkbox parts must be placed within <Checkbox.Root>.",
"7": "Base UI: CheckboxGroupContext is missing. CheckboxGroup parts must be placed within <CheckboxGroup>.",
"8": "Base UI: CollapsibleRootContext is missing. Collapsible parts must be placed within <Collapsible.Root>.",
"9": "[Base UI]: Invalid grid - item width at index %s is greater than grid columns",
"10": "Base UI: CompositeRootContext is missing. Composite parts must be placed within <Composite.Root>.",
"11": "Base UI: <Dialog.Portal> is missing.",
"12": "Base UI: DialogRootContext is missing. Dialog parts must be placed within <Dialog.Root>.",
"13": "Base UI: DirectionContext is missing.",
"14": "Base UI: FieldRootContext is missing. Field parts must be placed within <Field.Root>.",
"15": "Base UI: MenuCheckboxItemContext is missing. MenuCheckboxItem parts must be placed within <Menu.CheckboxItem>.",
"16": "Base UI: Missing MenuGroupRootContext provider",
"17": "Base UI: <Menu.Portal> is missing.",
"18": "Base UI: MenuPositionerContext is missing. MenuPositioner parts must be placed within <Menu.Positioner>.",
"19": "Base UI: MenuRadioGroupContext is missing. MenuRadioGroup parts must be placed within <Menu.RadioGroup>.",
"20": "Base UI: MenuRadioItemContext is missing. MenuRadioItem parts must be placed within <Menu.RadioItem>.",
"21": "Base UI: MenuRootContext is missing. Menu parts must be placed within <Menu.Root>.",
"22": "Base UI: ItemTrigger must be placed in a nested Menu.",
"23": "Base UI: NumberFieldRootContext is missing. NumberField parts must be placed within <NumberField.Root>.",
"24": "Base UI: <Popover.Portal> is missing.",
"25": "Base UI: PopoverPositionerContext is missing. PopoverPositioner parts must be placed within <Popover.Positioner>.",
"26": "Base UI: PopoverRootContext is missing. Popover parts must be placed within <Popover.Root>.",
"27": "Base UI: <PreviewCard.Portal> is missing.",
"28": "Base UI: <PreviewCard.Popup> and <PreviewCard.Arrow> must be used within the <PreviewCard.Positioner> component",
"29": "Base UI: PreviewCardRootContext is missing. PreviewCard parts must be placed within <PreviewCard.Root>.",
"30": "Base UI: ProgressRootContext is missing. Progress parts must be placed within <Progress.Root>.",
"31": "Base UI: RadioRootContext is missing. Radio parts must be placed within <Radio.Root>.",
"32": "Base UI: ScrollAreaRootContext is missing. ScrollArea parts must be placed within <ScrollArea.Root>.",
"33": "Base UI: ScrollAreaScrollbarContext is missing. ScrollAreaScrollbar parts must be placed within <ScrollArea.Scrollbar>.",
"34": "Base UI: SelectGroupContext is missing. SelectGroup parts must be placed within <Select.Group>.",
"35": "Base UI: SelectItemContext is missing. SelectItem parts must be placed within <Select.Item>.",
"36": "Expected prop '%s' to be of type Element",
"37": "Base UI: <Select.Portal> is missing.",
"38": "Base UI: SelectPositionerContext is missing. SelectPositioner parts must be placed within <Select.Positioner>.",
"39": "Base UI: SelectIndexContext is missing. Select parts must be placed within <Select.Root>.",
"40": "useSelectRootContext must be used within a SelectRoot",
"41": "Base UI: SliderRootContext is missing. Slider parts must be placed within <Slider.Root>.",
"42": "Base UI: SwitchRootContext is missing. Switch parts must be placed within <Switch.Root>.",
"43": "Base UI: TabsListContext is missing. TabsList parts must be placed within <Tabs.List>.",
"44": "Base UI: TabsRootContext is missing. Tabs parts must be placed within <Tabs.Root>.",
"45": "Base UI: ToggleGroupContext is missing. ToggleGroup parts must be placed within <ToggleGroup>.",
"46": "Base UI: <Tooltip.Portal> is missing.",
"47": "Base UI: TooltipPositionerContext is missing. TooltipPositioner parts must be placed within <Tooltip.Positioner>.",
"48": "Base UI: TooltipRootContext is missing. Tooltip parts must be placed within <Tooltip.Root>.",
"49": "Invalid %s `%s` supplied to `%s`. Expected an HTMLElement.",
"50": "The following props are not supported: %s. Please remove them."
}
5 changes: 5 additions & 0 deletions docs/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@

declare module 'gtag.js';
declare module '@mui/monorepo/docs/nextConfigDocsInfra.js';

declare module '*.mdx' {
const MDXComponent: (props) => JSX.Element;
export default MDXComponent;
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"docs:size-why": "cross-env DOCS_STATS_ENABLED=true pnpm docs:build",
"docs:start": "pnpm --filter docs serve",
"docs:link-check": "pnpm --filter docs link-check",
"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:stable",
"install:codesandbox": "pnpm install --no-frozen-lockfile",
"jsonlint": "node ./scripts/jsonlint.mjs",
"eslint": "eslint . --cache --report-unused-disable-directives --ext .js,.ts,.tsx --max-warnings 0",
Expand Down Expand Up @@ -69,6 +69,7 @@
"@mui/internal-markdown": "^1.0.25",
"@mui/internal-scripts": "^1.0.33",
"@mui/internal-test-utils": "^1.0.26",
"@mui/internal-babel-plugin-minify-errors": "https://pkg.csb.dev/mui/material-ui/commit/6c569b3e/@mui/internal-babel-plugin-minify-errors",
"@mui/monorepo": "github:mui/material-ui#v6.4.2",
"@next/eslint-plugin-next": "^14.2.23",
"@octokit/rest": "^21.1.0",
Expand All @@ -88,7 +89,6 @@
"@vitest/ui": "3.0.5",
"babel-loader": "^9.2.1",
"babel-plugin-add-import-extension": "1.6.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",
Expand Down
3 changes: 2 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"./utils": "./src/utils/index.ts"
},
"imports": {
"#test-utils": "./test/index.ts"
"#test-utils": "./test/index.ts",
"#formatErrorMessage": "./src/utils/formatErrorMessage.ts"
},
"type": "commonjs",
"scripts": {
Expand Down
15 changes: 15 additions & 0 deletions packages/react/src/utils/formatErrorMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* 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 formatErrorMessage(code: number, ...args: string[]): string {
const url = new URL(`https://base-ui.com/production-error/${code}`);
args.forEach((arg) => url.searchParams.append('args[]', arg));
return `Minified MUI error #${code}; visit ${url} for the full message.`;
}
23 changes: 20 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading