Skip to content
Draft
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
4 changes: 4 additions & 0 deletions examples/nextjs-node-esm-ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
.next
next-env.d.ts
*.tsbuildinfo
3 changes: 3 additions & 0 deletions examples/nextjs-node-esm-ssr/.stackblitzrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"startCommand": "npm run build && npm run start"
}
24 changes: 24 additions & 0 deletions examples/nextjs-node-esm-ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Material UI - Next.js Node ESM SSR example

Check failure on line 1 in examples/nextjs-node-esm-ssr/README.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [MUI.MuiBrandName] Use a non-breaking space (option+space on Mac, Alt+0160 on Windows or AltGr+Space on Linux, instead of space) for brand name ('Material UI' instead of 'Material UI') Raw Output: {"message": "[MUI.MuiBrandName] Use a non-breaking space (option+space on Mac, Alt+0160 on Windows or AltGr+Space on Linux, instead of space) for brand name ('Material UI' instead of 'Material UI')", "location": {"path": "examples/nextjs-node-esm-ssr/README.md", "range": {"start": {"line": 1, "column": 3}}}, "severity": "ERROR"}

Minimal Next.js Pages Router app for checking issue 48636. The server render
loads `@mui/material` through `@mui/x-date-pickers`, using Node's ESM package
resolution path.

## How to use

From the repository root:

```bash
pnpm --dir examples/nextjs-node-esm-ssr install --ignore-workspace
pnpm --dir examples/nextjs-node-esm-ssr build
pnpm --dir examples/nextjs-node-esm-ssr start
```

or:

[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mj12albert/material-ui/tree/examples-rtg-esm-fix/examples/nextjs-node-esm-ssr)

Open http://localhost:3000. The build and page render should complete without
`ERR_UNSUPPORTED_DIR_IMPORT`.

This example installs `@mui/material` from the PR 48645 package preview.
21 changes: 21 additions & 0 deletions examples/nextjs-node-esm-ssr/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// No configuration is needed: the Pages Router server build keeps
// `node_modules` packages external, so at render time Node's own resolver
// loads `@mui/x-date-pickers` and, transitively, `@mui/material`'s published
// files. On current Node versions that resolution uses the package's `import`
// conditions — the exact path that failed with ERR_UNSUPPORTED_DIR_IMPORT in
// https://github.com/mui/material-ui/issues/48636.
//
// Why this example does not externalize `@mui/material` directly: Next.js
// includes it in the built-in `experimental.optimizePackageImports` list, so
// direct imports of it are always bundled, and listing it in
// `serverExternalPackages` is rejected with a `transpilePackages` conflict.
// In the App Router, ESM externals also load without Next.js's React
// require-hook aliasing, so React component libraries cannot be
// server-external there at all. The Pages Router with a server-external
// dependent package, like `@mui/x-date-pickers` here, is the setup that still
// reaches `@mui/material` through Node.

/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;
23 changes: 23 additions & 0 deletions examples/nextjs-node-esm-ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "nextjs-node-esm-ssr",
"private": true,
"scripts": {
"build": "next build",
"start": "next start"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "https://pkg.pr.new/mui/material-ui/@mui/material@90aef5f59695faaed739e8a394281a03c5741aed",
"@mui/x-date-pickers": "^9.5.0",
"dayjs": "^1.11.13",
"next": "^16.0.7",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.2.14",
"typescript": "^5.9.3"
}
}
30 changes: 30 additions & 0 deletions examples/nextjs-node-esm-ssr/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';

// Server-render the page on every request so `next start` exercises the same
// Node module resolution path on each reload.
export function getServerSideProps() {
return { props: {} };
}

export default function Home() {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Stack spacing={2} sx={{ maxWidth: 480, mx: 'auto', p: 4 }}>
<Typography variant="h5" component="h1">
Next.js Node ESM SSR with Material UI
</Typography>
<Typography>
The date picker reaches Material UI transition internals through the server-external
@mui/x-date-pickers package.
</Typography>
<DatePicker label="Pick a date" defaultValue={dayjs('2026-06-13')} />
</Stack>
</LocalizationProvider>
);
}
30 changes: 30 additions & 0 deletions examples/nextjs-node-esm-ssr/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
1 change: 1 addition & 0 deletions examples/node-esm-ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
3 changes: 3 additions & 0 deletions examples/node-esm-ssr/.stackblitzrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"startCommand": "npm run start"
}
28 changes: 28 additions & 0 deletions examples/node-esm-ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Material UI - Node ESM SSR example

Check failure on line 1 in examples/node-esm-ssr/README.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [MUI.MuiBrandName] Use a non-breaking space (option+space on Mac, Alt+0160 on Windows or AltGr+Space on Linux, instead of space) for brand name ('Material UI' instead of 'Material UI') Raw Output: {"message": "[MUI.MuiBrandName] Use a non-breaking space (option+space on Mac, Alt+0160 on Windows or AltGr+Space on Linux, instead of space) for brand name ('Material UI' instead of 'Material UI')", "location": {"path": "examples/node-esm-ssr/README.md", "range": {"start": {"line": 1, "column": 3}}}, "severity": "ERROR"}

Minimal Node ESM server-rendered React app for checking issue 48636. It renders
Material UI transition components without a bundler rewriting package imports.

Check failure on line 4 in examples/node-esm-ssr/README.md

View workflow job for this annotation

GitHub Actions / test-dev (ubuntu-latest)

[vale] reported by reviewdog 🐶 [MUI.MuiBrandName] Use a non-breaking space (option+space on Mac, Alt+0160 on Windows or AltGr+Space on Linux, instead of space) for brand name ('Material UI' instead of 'Material UI') Raw Output: {"message": "[MUI.MuiBrandName] Use a non-breaking space (option+space on Mac, Alt+0160 on Windows or AltGr+Space on Linux, instead of space) for brand name ('Material UI' instead of 'Material UI')", "location": {"path": "examples/node-esm-ssr/README.md", "range": {"start": {"line": 4, "column": 1}}}, "severity": "ERROR"}

## How to use

From the repository root:

```bash
pnpm --dir examples/node-esm-ssr install --ignore-workspace
pnpm --dir examples/node-esm-ssr render
```

or:

[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mj12albert/material-ui/tree/examples-rtg-esm-fix/examples/node-esm-ssr)

To run the HTTP server:

```bash
pnpm --dir examples/node-esm-ssr start
```

Open http://localhost:3000. Rendering should complete without
`ERR_UNSUPPORTED_DIR_IMPORT`.

This example installs `@mui/material` from the PR 48645 package preview.
17 changes: 17 additions & 0 deletions examples/node-esm-ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "node-esm-ssr",
"private": true,
"type": "module",
"scripts": {
"render": "node ./server.mjs --once",
"start": "node ./server.mjs"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "https://pkg.pr.new/mui/material-ui/@mui/material@90aef5f59695faaed739e8a394281a03c5741aed",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-transition-group": "^4.4.5"
}
}
22 changes: 22 additions & 0 deletions examples/node-esm-ssr/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createServer } from 'node:http';
import { renderPage } from './src/renderPage.mjs';

const port = Number(process.env.PORT || 3000);
const renderOnce = process.argv.includes('--once');

if (renderOnce) {
console.log(renderPage());
} else {
createServer((request, response) => {
if (request.url !== '/') {
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
return;
}

response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(renderPage());
}).listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
}
158 changes: 158 additions & 0 deletions examples/node-esm-ssr/src/App.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Collapse from '@mui/material/Collapse';
import CssBaseline from '@mui/material/CssBaseline';
import Fade from '@mui/material/Fade';
import Grow from '@mui/material/Grow';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Slide from '@mui/material/Slide';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Zoom from '@mui/material/Zoom';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { TransitionGroup } from 'react-transition-group';

const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#0057b8',
},
secondary: {
main: '#0f766e',
},
background: {
default: '#f7f8fa',
},
},
shape: {
borderRadius: 8,
},
});

const transitions = [
['Fade', Fade],
['Grow', Grow],
['Collapse', Collapse],
['Slide', Slide, { direction: 'up' }],
['Zoom', Zoom],
];

const serverItems = ['Primary navigation', 'Account menu', 'Settings panel'];

function TransitionPanel({ name, TransitionComponent, transitionProps = {} }) {
return React.createElement(
TransitionComponent,
{ in: true, timeout: 0, ...transitionProps },
React.createElement(
Box,
{
component: 'section',
sx: {
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
bgcolor: 'background.paper',
px: 2,
py: 1.5,
},
},
React.createElement(
Typography,
{ component: 'h2', variant: 'h6' },
`${name} rendered on the server`,
),
React.createElement(
Typography,
{ color: 'text.secondary', variant: 'body2' },
'This component imports Material UI transition internals through the published ESM build.',
),
),
);
}

function TransitionGroupPanel() {
return React.createElement(
Box,
{
component: 'section',
sx: {
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
bgcolor: 'background.paper',
px: 2,
py: 1.5,
},
},
React.createElement(
Typography,
{ component: 'h2', variant: 'h6' },
'TransitionGroup rendered on the server',
),
React.createElement(
List,
{ dense: true, sx: { mt: 1 } },
React.createElement(
TransitionGroup,
null,
serverItems.map((item) =>
React.createElement(
Collapse,
{ key: item, timeout: 0 },
React.createElement(
ListItem,
{ disablePadding: true },
React.createElement(ListItemText, { primary: item }),
),
),
),
),
),
);
}

export default function App() {
return React.createElement(
ThemeProvider,
{ theme },
React.createElement(CssBaseline),
React.createElement(
Box,
{
component: 'main',
sx: {
maxWidth: 760,
mx: 'auto',
px: 3,
py: 5,
},
},
React.createElement(
Typography,
{ component: 'h1', variant: 'h4', gutterBottom: true },
'Node ESM SSR with Material UI',
),
React.createElement(
Typography,
{ color: 'text.secondary', sx: { mb: 3 } },
'A minimal server-rendered app that runs MUI transition components through Node native ESM.',
),
React.createElement(
Stack,
{ spacing: 2 },
transitions.map(([name, TransitionComponent, transitionProps]) =>
React.createElement(TransitionPanel, {
key: name,
name,
TransitionComponent,
transitionProps,
}),
),
React.createElement(TransitionGroupPanel, { key: 'TransitionGroup' }),
),
),
);
}
19 changes: 19 additions & 0 deletions examples/node-esm-ssr/src/renderPage.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import App from './App.mjs';

export function renderPage() {
const appHtml = ReactDOMServer.renderToString(React.createElement(App));

return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<title>Material UI Node ESM SSR</title>
</head>
<body>
<div id="root">${appHtml}</div>
</body>
</html>`;
}
Loading
Loading