From c6ba36b1bcde275fb41d2ed8504236fc3687c30e Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 10 Jun 2026 00:05:42 +0800 Subject: [PATCH 1/4] fix RTG import in ESM --- babel.config.mjs | 37 ++++ examples/nextjs-node-esm-ssr/.gitignore | 4 + examples/nextjs-node-esm-ssr/README.md | 50 ++++++ examples/nextjs-node-esm-ssr/next.config.mjs | 21 +++ examples/nextjs-node-esm-ssr/package.json | 33 ++++ examples/nextjs-node-esm-ssr/pages/index.tsx | 30 ++++ examples/nextjs-node-esm-ssr/tsconfig.json | 30 ++++ examples/node-esm-ssr/.gitignore | 1 + examples/node-esm-ssr/README.md | 38 +++++ examples/node-esm-ssr/package.json | 27 +++ examples/node-esm-ssr/server.mjs | 22 +++ examples/node-esm-ssr/src/App.mjs | 158 ++++++++++++++++++ examples/node-esm-ssr/src/renderPage.mjs | 19 +++ examples/react-router-node-esm-ssr/.gitignore | 4 + examples/react-router-node-esm-ssr/README.md | 46 +++++ .../react-router-node-esm-ssr/app/root.tsx | 24 +++ .../react-router-node-esm-ssr/app/routes.ts | 3 + .../app/routes/home.tsx | 72 ++++++++ .../react-router-node-esm-ssr/package.json | 41 +++++ .../react-router.config.ts | 12 ++ .../react-router-node-esm-ssr/tsconfig.json | 18 ++ .../react-router-node-esm-ssr/vite.config.ts | 9 + examples/vitest-node-esm/.gitignore | 1 + examples/vitest-node-esm/README.md | 35 ++++ examples/vitest-node-esm/package.json | 32 ++++ examples/vitest-node-esm/src/App.jsx | 44 +++++ examples/vitest-node-esm/src/App.test.jsx | 55 ++++++ examples/vitest-node-esm/vitest.config.mjs | 15 ++ packages/mui-material/package.json | 7 + .../src/internal/Transition.test.tsx | 48 ++++++ .../TransitionGroupContext.browser.ts | 5 + .../src/internal/TransitionGroupContext.ts | 5 + .../src/internal/react-transition-group.d.ts | 22 +++ 33 files changed, 968 insertions(+) create mode 100644 examples/nextjs-node-esm-ssr/.gitignore create mode 100644 examples/nextjs-node-esm-ssr/README.md create mode 100644 examples/nextjs-node-esm-ssr/next.config.mjs create mode 100644 examples/nextjs-node-esm-ssr/package.json create mode 100644 examples/nextjs-node-esm-ssr/pages/index.tsx create mode 100644 examples/nextjs-node-esm-ssr/tsconfig.json create mode 100644 examples/node-esm-ssr/.gitignore create mode 100644 examples/node-esm-ssr/README.md create mode 100644 examples/node-esm-ssr/package.json create mode 100644 examples/node-esm-ssr/server.mjs create mode 100644 examples/node-esm-ssr/src/App.mjs create mode 100644 examples/node-esm-ssr/src/renderPage.mjs create mode 100644 examples/react-router-node-esm-ssr/.gitignore create mode 100644 examples/react-router-node-esm-ssr/README.md create mode 100644 examples/react-router-node-esm-ssr/app/root.tsx create mode 100644 examples/react-router-node-esm-ssr/app/routes.ts create mode 100644 examples/react-router-node-esm-ssr/app/routes/home.tsx create mode 100644 examples/react-router-node-esm-ssr/package.json create mode 100644 examples/react-router-node-esm-ssr/react-router.config.ts create mode 100644 examples/react-router-node-esm-ssr/tsconfig.json create mode 100644 examples/react-router-node-esm-ssr/vite.config.ts create mode 100644 examples/vitest-node-esm/.gitignore create mode 100644 examples/vitest-node-esm/README.md create mode 100644 examples/vitest-node-esm/package.json create mode 100644 examples/vitest-node-esm/src/App.jsx create mode 100644 examples/vitest-node-esm/src/App.test.jsx create mode 100644 examples/vitest-node-esm/vitest.config.mjs create mode 100644 packages/mui-material/src/internal/TransitionGroupContext.browser.ts create mode 100644 packages/mui-material/src/internal/TransitionGroupContext.ts diff --git a/babel.config.mjs b/babel.config.mjs index 1448b5a1584d22..febfd67f81c95c 100644 --- a/babel.config.mjs +++ b/babel.config.mjs @@ -21,6 +21,38 @@ function resolveAliasPath(relativeToBabelConf) { return `./${resolvedPath.replace('\\', '/')}`; } +function rewriteTransitionGroupContextImport() { + const transitionSourcePath = path.join( + 'packages', + 'mui-material', + 'src', + 'internal', + 'Transition.tsx', + ); + + return { + name: 'rewrite-transition-group-context-import', + visitor: { + ImportDeclaration(babelPath) { + const sourceFilename = babelPath.hub.file.opts.filename; + const isMaterialTransition = + typeof sourceFilename === 'string' && + path.normalize(sourceFilename).endsWith(transitionSourcePath); + + if ( + isMaterialTransition && + babelPath.node.source.value === 'react-transition-group/TransitionGroupContext' + ) { + // Package-private `#` imports are supported in Node 14.6.0+. + // Keep @mui/material's broader engine range unchanged here; changing it is a + // separate support-policy decision for the small Node 14.0-14.5 window. + babelPath.node.source.value = '#mui/TransitionGroupContext'; + } + }, + }, + }; +} + /** @type {babel.ConfigFunction} */ export default function getBabelConfig(api) { const baseConfig = getBaseConfig(api); @@ -28,6 +60,7 @@ export default function getBabelConfig(api) { // Covers: docs prod build (NODE_ENV=production), package esm build (BABEL_ENV=stable), // package cjs build (BABEL_ENV=node). Excludes docs dev, tests, coverage. const isProductionBuild = api.env(['production', 'stable', 'node']); + const isPackageBuild = api.env(['stable', 'node']); const defaultAlias = { '@mui/material': resolveAliasPath('./packages/mui-material/src'), @@ -71,6 +104,10 @@ export default function getBabelConfig(api) { !excludedBasePlugins.has(pluginName), ); + if (isPackageBuild) { + basePlugins.push(rewriteTransitionGroupContextImport()); + } + if (isProductionBuild) { basePlugins.push(...prodOnlyPlugins); } diff --git a/examples/nextjs-node-esm-ssr/.gitignore b/examples/nextjs-node-esm-ssr/.gitignore new file mode 100644 index 00000000000000..4b805353335b81 --- /dev/null +++ b/examples/nextjs-node-esm-ssr/.gitignore @@ -0,0 +1,4 @@ +node_modules +.next +next-env.d.ts +*.tsbuildinfo diff --git a/examples/nextjs-node-esm-ssr/README.md b/examples/nextjs-node-esm-ssr/README.md new file mode 100644 index 00000000000000..19a0d053a12c8e --- /dev/null +++ b/examples/nextjs-node-esm-ssr/README.md @@ -0,0 +1,50 @@ +# Material UI - Next.js Node ESM SSR example + +This is a minimal Next.js Pages Router app that server-renders an +`@mui/x-date-pickers` date picker. + +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 same module resolution path as the +Next.js reports in https://github.com/mui/material-ui/issues/48636, without the +`transpilePackages` workaround. With `@mui/material@9.1.1` this example fails +to build with `ERR_UNSUPPORTED_DIR_IMPORT`. + +Next.js keeps `@mui/material` itself in the built-in +`experimental.optimizePackageImports` list, so direct imports of it are always +bundled and it cannot be listed in `serverExternalPackages`. Node therefore +only resolves `@mui/material` when a server-external package, like +`@mui/x-date-pickers` here, depends on it. + +## How to use + +From the repository root: + +```bash +pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build +pnpm --dir examples/nextjs-node-esm-ssr install --ignore-workspace +pnpm --dir examples/nextjs-node-esm-ssr build +``` + +To run the production server: + +```bash +pnpm --dir examples/nextjs-node-esm-ssr start +``` + +Then open http://localhost:3000. The page is rendered on every request, and the +important signal is that the build and the page render complete without +`ERR_UNSUPPORTED_DIR_IMPORT`. + +## Why this example uses local package paths + +The app depends directly on `@mui/material`, `@mui/x-date-pickers`, React, and +Emotion. `@mui/material` points at `../../packages/mui-material/build` so this +example can test the current checkout before the package is published. + +The `pnpm.overrides` entries are all required for local development: the +unpublished `@mui/material` and `@mui/system` builds declare their MUI +dependencies with the `workspace:` protocol, which only resolves inside this +repository. Each override points one of those dependencies at this checkout's +built packages so the standalone install can resolve them. diff --git a/examples/nextjs-node-esm-ssr/next.config.mjs b/examples/nextjs-node-esm-ssr/next.config.mjs new file mode 100644 index 00000000000000..d97a3ac2772fff --- /dev/null +++ b/examples/nextjs-node-esm-ssr/next.config.mjs @@ -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; diff --git a/examples/nextjs-node-esm-ssr/package.json b/examples/nextjs-node-esm-ssr/package.json new file mode 100644 index 00000000000000..6daa928a0e51c9 --- /dev/null +++ b/examples/nextjs-node-esm-ssr/package.json @@ -0,0 +1,33 @@ +{ + "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": "file:../../packages/mui-material/build", + "@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" + }, + "pnpm": { + "overrides": { + "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", + "@mui/private-theming": "file:../../packages/mui-private-theming/build", + "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", + "@mui/system": "file:../../packages/mui-system/build", + "@mui/types": "file:../../packages/mui-types/build", + "@mui/utils": "file:../../packages/mui-utils/build" + } + } +} diff --git a/examples/nextjs-node-esm-ssr/pages/index.tsx b/examples/nextjs-node-esm-ssr/pages/index.tsx new file mode 100644 index 00000000000000..201ab84e68c2e9 --- /dev/null +++ b/examples/nextjs-node-esm-ssr/pages/index.tsx @@ -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 ( + + + + Next.js Node ESM SSR with Material UI + + + The date picker reaches Material UI transition internals through the server-external + @mui/x-date-pickers package. + + + + + ); +} diff --git a/examples/nextjs-node-esm-ssr/tsconfig.json b/examples/nextjs-node-esm-ssr/tsconfig.json new file mode 100644 index 00000000000000..d73edcaea03078 --- /dev/null +++ b/examples/nextjs-node-esm-ssr/tsconfig.json @@ -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"] +} diff --git a/examples/node-esm-ssr/.gitignore b/examples/node-esm-ssr/.gitignore new file mode 100644 index 00000000000000..3c3629e647f5dd --- /dev/null +++ b/examples/node-esm-ssr/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/node-esm-ssr/README.md b/examples/node-esm-ssr/README.md new file mode 100644 index 00000000000000..c2add38205db3f --- /dev/null +++ b/examples/node-esm-ssr/README.md @@ -0,0 +1,38 @@ +# Material UI - Node ESM SSR example + +This is a minimal server-rendered React app that runs in Node's native ESM mode. +It imports transition components from `@mui/material` and renders them on the +server without a bundler rewriting package imports first. It also imports +`TransitionGroup` from `react-transition-group`, like the Material UI transition +docs demo. + +This setup is useful for locally checking the package shape involved in +https://github.com/mui/material-ui/issues/48636. + +## How to use + +From the repository root: + +```bash +pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build +pnpm --dir examples/node-esm-ssr install --ignore-workspace +pnpm --dir examples/node-esm-ssr render +``` + +To run it as a small HTTP server: + +```bash +pnpm --dir examples/node-esm-ssr start +``` + +Then open http://localhost:3000. + +## Why this example uses local package paths + +The app depends directly on `@mui/material`, `react-transition-group`, React, +and Emotion. `@mui/material` points at `../../packages/mui-material/build` so +this example can test the current checkout before the package is published. + +The `pnpm.overrides` entries are only for local development. They make +`@mui/material`'s workspace dependencies resolve to this checkout's built +packages inside the standalone example. diff --git a/examples/node-esm-ssr/package.json b/examples/node-esm-ssr/package.json new file mode 100644 index 00000000000000..331d76f3ed21a7 --- /dev/null +++ b/examples/node-esm-ssr/package.json @@ -0,0 +1,27 @@ +{ + "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": "file:../../packages/mui-material/build", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-transition-group": "^4.4.5" + }, + "pnpm": { + "overrides": { + "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", + "@mui/private-theming": "file:../../packages/mui-private-theming/build", + "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", + "@mui/system": "file:../../packages/mui-system/build", + "@mui/types": "file:../../packages/mui-types/build", + "@mui/utils": "file:../../packages/mui-utils/build" + } + } +} diff --git a/examples/node-esm-ssr/server.mjs b/examples/node-esm-ssr/server.mjs new file mode 100644 index 00000000000000..cfa4833d5b78fe --- /dev/null +++ b/examples/node-esm-ssr/server.mjs @@ -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}`); + }); +} diff --git a/examples/node-esm-ssr/src/App.mjs b/examples/node-esm-ssr/src/App.mjs new file mode 100644 index 00000000000000..db9db282aeeadd --- /dev/null +++ b/examples/node-esm-ssr/src/App.mjs @@ -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' }), + ), + ), + ); +} diff --git a/examples/node-esm-ssr/src/renderPage.mjs b/examples/node-esm-ssr/src/renderPage.mjs new file mode 100644 index 00000000000000..fda0ef28ed4a01 --- /dev/null +++ b/examples/node-esm-ssr/src/renderPage.mjs @@ -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 ` + + + + + Material UI Node ESM SSR + + +
${appHtml}
+ +`; +} diff --git a/examples/react-router-node-esm-ssr/.gitignore b/examples/react-router-node-esm-ssr/.gitignore new file mode 100644 index 00000000000000..be40ab16504fee --- /dev/null +++ b/examples/react-router-node-esm-ssr/.gitignore @@ -0,0 +1,4 @@ +build +node_modules +.react-router +tsconfig.tsbuildinfo diff --git a/examples/react-router-node-esm-ssr/README.md b/examples/react-router-node-esm-ssr/README.md new file mode 100644 index 00000000000000..b3bcc51ef9db7d --- /dev/null +++ b/examples/react-router-node-esm-ssr/README.md @@ -0,0 +1,46 @@ +# Material UI - React Router Node ESM SSR example + +This is a minimal React Router SSR app that imports Material UI from the local +built package and uses `TransitionGroup` from `react-transition-group`, like the +Material UI transition docs demo. It mirrors the package-resolution path involved in +https://github.com/mui/material-ui/issues/48636 without the extra template code +from larger React Router examples. + +## How to use + +From the repository root, build the local Material UI packages first: + +```bash +pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build +``` + +Then from this example directory: + +```bash +pnpm install --ignore-workspace +pnpm build +pnpm start +``` + +Open http://localhost:3000. + +For a non-server smoke test after `pnpm build`, import the server build: + +```bash +node -e "import('./build/server/index.js').then(() => console.log('server build import ok'))" +``` + +Before the fix for issue 48636, that import path can fail when Node resolves +Material UI's built ESM transition module and reaches +`react-transition-group/TransitionGroupContext`. + +## Why this example uses local package paths + +The app depends directly on `@mui/material`, `react-transition-group`, React, +Emotion, and React Router. `@mui/material` points at +`../../packages/mui-material/build` so this example can test the current checkout +before the package is published. + +The `pnpm.overrides` entries are only for local development. They make +`@mui/material`'s workspace dependencies resolve to this checkout's built +packages inside the standalone example. diff --git a/examples/react-router-node-esm-ssr/app/root.tsx b/examples/react-router-node-esm-ssr/app/root.tsx new file mode 100644 index 00000000000000..429e6c3f8b1c38 --- /dev/null +++ b/examples/react-router-node-esm-ssr/app/root.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/examples/react-router-node-esm-ssr/app/routes.ts b/examples/react-router-node-esm-ssr/app/routes.ts new file mode 100644 index 00000000000000..205ff3ccb9fd4e --- /dev/null +++ b/examples/react-router-node-esm-ssr/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from '@react-router/dev/routes'; + +export default [index('routes/home.tsx')] satisfies RouteConfig; diff --git a/examples/react-router-node-esm-ssr/app/routes/home.tsx b/examples/react-router-node-esm-ssr/app/routes/home.tsx new file mode 100644 index 00000000000000..829747865c392b --- /dev/null +++ b/examples/react-router-node-esm-ssr/app/routes/home.tsx @@ -0,0 +1,72 @@ +import Box from '@mui/material/Box'; +import Collapse from '@mui/material/Collapse'; +import Fade from '@mui/material/Fade'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { TransitionGroup } from 'react-transition-group'; + +const routeItems = ['Dashboard shell', 'Details route', 'Settings route']; + +export function meta() { + return [ + { title: 'React Router Node ESM SSR' }, + { + name: 'description', + content: 'Minimal React Router SSR app using Material UI transitions.', + }, + ]; +} + +export default function Home() { + return ( + + + + React Router SSR with Material UI + + + This route renders through React Router's server build and imports MUI transition + components from the installed package. + + + + + Fade rendered during SSR + + + + + + {routeItems.map((item) => ( + + + + + + ))} + + + + + ); +} diff --git a/examples/react-router-node-esm-ssr/package.json b/examples/react-router-node-esm-ssr/package.json new file mode 100644 index 00000000000000..a2755d0bbd5388 --- /dev/null +++ b/examples/react-router-node-esm-ssr/package.json @@ -0,0 +1,41 @@ +{ + "name": "react-router-node-esm-ssr", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "file:../../packages/mui-material/build", + "@react-router/node": "7.16.0", + "@react-router/serve": "7.16.0", + "isbot": "^5", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router": "7.16.0", + "react-transition-group": "^4.4.5" + }, + "devDependencies": { + "@react-router/dev": "7.16.0", + "@types/node": "^22.0.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/react-transition-group": "^4.4.12", + "typescript": "^5.9.3", + "vite": "^8.0.3" + }, + "pnpm": { + "overrides": { + "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", + "@mui/private-theming": "file:../../packages/mui-private-theming/build", + "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", + "@mui/system": "file:../../packages/mui-system/build", + "@mui/types": "file:../../packages/mui-types/build", + "@mui/utils": "file:../../packages/mui-utils/build" + } + } +} diff --git a/examples/react-router-node-esm-ssr/react-router.config.ts b/examples/react-router-node-esm-ssr/react-router.config.ts new file mode 100644 index 00000000000000..9d8b4134caa567 --- /dev/null +++ b/examples/react-router-node-esm-ssr/react-router.config.ts @@ -0,0 +1,12 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + ssr: true, + future: { + v8_middleware: true, + v8_passThroughRequests: true, + v8_splitRouteModules: true, + v8_trailingSlashAwareDataRequests: true, + v8_viteEnvironmentApi: true, + }, +} satisfies Config; diff --git a/examples/react-router-node-esm-ssr/tsconfig.json b/examples/react-router-node-esm-ssr/tsconfig.json new file mode 100644 index 00000000000000..90aefcd00b4a53 --- /dev/null +++ b/examples/react-router-node-esm-ssr/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": [".react-router/types/**/*", "app/**/*"], + "compilerOptions": { + "composite": true, + "strict": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/examples/react-router-node-esm-ssr/vite.config.ts b/examples/react-router-node-esm-ssr/vite.config.ts new file mode 100644 index 00000000000000..263bb8e25747ed --- /dev/null +++ b/examples/react-router-node-esm-ssr/vite.config.ts @@ -0,0 +1,9 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [reactRouter()], + ssr: { + external: ['@mui/material'], + }, +}); diff --git a/examples/vitest-node-esm/.gitignore b/examples/vitest-node-esm/.gitignore new file mode 100644 index 00000000000000..3c3629e647f5dd --- /dev/null +++ b/examples/vitest-node-esm/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/vitest-node-esm/README.md b/examples/vitest-node-esm/README.md new file mode 100644 index 00000000000000..4054a045209829 --- /dev/null +++ b/examples/vitest-node-esm/README.md @@ -0,0 +1,35 @@ +# Material UI - Vitest Node ESM example + +This is a minimal Vitest + jsdom test setup for an app that renders Material UI +transition components inside `TransitionGroup` from `react-transition-group`, +like the Material UI transition docs demo. + +Vitest externalizes `node_modules` dependencies by default, so these tests load +`@mui/material` through Node's native ESM resolver — the same module resolution +path that the test-runner reports in +https://github.com/mui/material-ui/issues/48636 hit. The Vitest config +intentionally has no `server.deps.inline` workaround for `@mui/material` or +`react-transition-group`. + +## How to use + +From the repository root: + +```bash +pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build +pnpm --dir examples/vitest-node-esm install --ignore-workspace +pnpm --dir examples/vitest-node-esm test +``` + +The important signal is that the tests pass without `ERR_UNSUPPORTED_DIR_IMPORT` +and without inlining `@mui/material` into the Vite transform pipeline. + +## Why this example uses local package paths + +The app depends directly on `@mui/material`, `react-transition-group`, React, +and Emotion. `@mui/material` points at `../../packages/mui-material/build` so +this example can test the current checkout before the package is published. + +The `pnpm.overrides` entries are only for local development. They make +`@mui/material`'s workspace dependencies resolve to this checkout's built +packages inside the standalone example. diff --git a/examples/vitest-node-esm/package.json b/examples/vitest-node-esm/package.json new file mode 100644 index 00000000000000..c8523d59393f19 --- /dev/null +++ b/examples/vitest-node-esm/package.json @@ -0,0 +1,32 @@ +{ + "name": "vitest-node-esm", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "file:../../packages/mui-material/build", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-transition-group": "^4.4.5" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.3.0", + "jsdom": "^26.1.0", + "vitest": "^4.1.0" + }, + "pnpm": { + "overrides": { + "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", + "@mui/private-theming": "file:../../packages/mui-private-theming/build", + "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", + "@mui/system": "file:../../packages/mui-system/build", + "@mui/types": "file:../../packages/mui-types/build", + "@mui/utils": "file:../../packages/mui-utils/build" + } + } +} diff --git a/examples/vitest-node-esm/src/App.jsx b/examples/vitest-node-esm/src/App.jsx new file mode 100644 index 00000000000000..5c2a0cf772fa12 --- /dev/null +++ b/examples/vitest-node-esm/src/App.jsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; +import Fade from '@mui/material/Fade'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { TransitionGroup } from 'react-transition-group'; + +const initialItems = ['Item 1', 'Item 2', 'Item 3']; + +export default function App() { + const [items, setItems] = React.useState(initialItems); + + return ( + + + Vitest with Material UI transitions + + + Faded in on first render + + + + + {items.map((item) => ( + + + + + + ))} + + + + ); +} diff --git a/examples/vitest-node-esm/src/App.test.jsx b/examples/vitest-node-esm/src/App.test.jsx new file mode 100644 index 00000000000000..10decf380aaf13 --- /dev/null +++ b/examples/vitest-node-esm/src/App.test.jsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import Collapse from '@mui/material/Collapse'; +import { TransitionGroup } from 'react-transition-group'; +import { expect, test, vi } from 'vitest'; +import App from './App.jsx'; + +test('renders the app through the published @mui/material package layout', () => { + render(); + + expect(screen.getByText('Faded in on first render')).toBeDefined(); + expect(screen.getByText('Item 1')).toBeDefined(); +}); + +test('adds a list item to an already-mounted TransitionGroup', async () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Add item' })); + + expect(await screen.findByText('Item 4')).toBeDefined(); +}); + +test('Collapse added to a mounted TransitionGroup enters with isAppearing=false', async () => { + const handleEntered = vi.fn(); + + function Harness() { + const [open, setOpen] = React.useState(false); + return ( + + + + {open ? ( + +

Collapse content

+
+ ) : null} +
+
+ ); + } + + render(); + fireEvent.click(screen.getByRole('button', { name: 'Open' })); + + expect(await screen.findByText('Collapse content')).toBeDefined(); + await waitFor(() => expect(handleEntered).toHaveBeenCalledTimes(1)); + + // Material UI and react-transition-group must share one TransitionGroupContext + // instance for this to hold: a child added after the group mounted reports + // isAppearing=false (the last callback argument). + const enteredArguments = handleEntered.mock.calls[0]; + expect(enteredArguments[enteredArguments.length - 1]).toBe(false); +}); diff --git a/examples/vitest-node-esm/vitest.config.mjs b/examples/vitest-node-esm/vitest.config.mjs new file mode 100644 index 00000000000000..e40eceafff368e --- /dev/null +++ b/examples/vitest-node-esm/vitest.config.mjs @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + // Expose global test hooks so @testing-library/react registers its + // automatic cleanup. + globals: true, + // Intentionally no `server.deps.inline` entries for `@mui/material` or + // `react-transition-group`. Vitest externalizes `node_modules` by default, + // so the tests load `@mui/material` through Node's own ESM resolver — the + // module resolution path from + // https://github.com/mui/material-ui/issues/48636. + }, +}); diff --git a/packages/mui-material/package.json b/packages/mui-material/package.json index 3692d62f304084..bf0ef6b31b42ba 100644 --- a/packages/mui-material/package.json +++ b/packages/mui-material/package.json @@ -91,6 +91,13 @@ "engines": { "node": ">=14.0.0" }, + "imports": { + "#mui/TransitionGroupContext": { + "node": "./src/internal/TransitionGroupContext.ts", + "browser": "./src/internal/TransitionGroupContext.browser.ts", + "default": "./src/internal/TransitionGroupContext.browser.ts" + } + }, "exports": { ".": "./src/index.js", "./ButtonBase/TouchRipple": "./src/ButtonBase/TouchRipple.js", diff --git a/packages/mui-material/src/internal/Transition.test.tsx b/packages/mui-material/src/internal/Transition.test.tsx index f792e5fc2c58af..087cef5b8da907 100644 --- a/packages/mui-material/src/internal/Transition.test.tsx +++ b/packages/mui-material/src/internal/Transition.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; +import { TransitionGroup } from 'react-transition-group'; import TransitionGroupContext from 'react-transition-group/TransitionGroupContext'; import { act, createRenderer, screen } from '@mui/internal-test-utils'; import Transition from './Transition'; @@ -990,6 +991,53 @@ describe('', () => { }); }); + describe('react-transition-group public TransitionGroup interop', () => { + it('child added to an already-mounted TransitionGroup enters with isAppearing=false', async () => { + const handlers = { onEnter: spy(), onEntered: spy() }; + let done: (() => void) | null = null; + const addEndListener = (_node: HTMLElement, next: () => void) => { + done = next; + }; + + function ChildWrapper() { + const [shouldRender, setShouldRender] = React.useState(false); + return ( + + + + {shouldRender ? ( + + ) : null} + + + ); + } + + const { user } = render(); + await user.click(screen.getByRole('button', { name: 'add' })); + + expect(screen.getByTestId('target')).to.have.attribute('data-status', 'entering'); + expect(handlers.onEnter.callCount).to.equal(1); + expect(handlers.onEnter.args[0][0]).to.equal(false); + + act(() => { + done!(); + }); + + expect(screen.getByTestId('target')).to.have.attribute('data-status', 'entered'); + expect(handlers.onEntered.callCount).to.equal(1); + expect(handlers.onEntered.args[0][0]).to.equal(false); + }); + }); + describe('react-transition-group TransitionGroupContext user interactions', () => { const mountedGroup = { isMounting: false }; diff --git a/packages/mui-material/src/internal/TransitionGroupContext.browser.ts b/packages/mui-material/src/internal/TransitionGroupContext.browser.ts new file mode 100644 index 00000000000000..813f6e39d00364 --- /dev/null +++ b/packages/mui-material/src/internal/TransitionGroupContext.browser.ts @@ -0,0 +1,5 @@ +/// +// eslint-disable-next-line import/extensions -- Browser builds need the explicit .js extension. +import TransitionGroupContext from 'react-transition-group/esm/TransitionGroupContext.js'; + +export default TransitionGroupContext; diff --git a/packages/mui-material/src/internal/TransitionGroupContext.ts b/packages/mui-material/src/internal/TransitionGroupContext.ts new file mode 100644 index 00000000000000..d41671623487b7 --- /dev/null +++ b/packages/mui-material/src/internal/TransitionGroupContext.ts @@ -0,0 +1,5 @@ +/// +// eslint-disable-next-line import/extensions -- Node ESM needs the explicit .js extension. +import TransitionGroupContext from 'react-transition-group/cjs/TransitionGroupContext.js'; + +export default TransitionGroupContext; diff --git a/packages/mui-material/src/internal/react-transition-group.d.ts b/packages/mui-material/src/internal/react-transition-group.d.ts index ef20f1c36ce0cd..2c9651e43894ad 100644 --- a/packages/mui-material/src/internal/react-transition-group.d.ts +++ b/packages/mui-material/src/internal/react-transition-group.d.ts @@ -8,3 +8,25 @@ declare module 'react-transition-group/TransitionGroupContext' { const TransitionGroupContext: React.Context; export default TransitionGroupContext; } + +declare module 'react-transition-group/cjs/TransitionGroupContext.js' { + import * as React from 'react'; + + interface TransitionGroupContextValue { + isMounting: boolean; + } + + const TransitionGroupContext: React.Context; + export default TransitionGroupContext; +} + +declare module 'react-transition-group/esm/TransitionGroupContext.js' { + import * as React from 'react'; + + interface TransitionGroupContextValue { + isMounting: boolean; + } + + const TransitionGroupContext: React.Context; + export default TransitionGroupContext; +} From 8752430764fdb87b9acc295bea4c65aa39709ce9 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 17 Jun 2026 09:48:02 +0800 Subject: [PATCH 2/4] alt fix without package import --- babel.config.mjs | 7 +++--- packages/mui-material/package.json | 8 ++----- .../TransitionGroupContext.browser.ts | 5 ----- .../src/internal/TransitionGroupContext.ts | 5 ----- .../src/internal/react-transition-group.d.ts | 22 ------------------- 5 files changed, 5 insertions(+), 42 deletions(-) delete mode 100644 packages/mui-material/src/internal/TransitionGroupContext.browser.ts delete mode 100644 packages/mui-material/src/internal/TransitionGroupContext.ts diff --git a/babel.config.mjs b/babel.config.mjs index febfd67f81c95c..f923c7b1310187 100644 --- a/babel.config.mjs +++ b/babel.config.mjs @@ -43,10 +43,9 @@ function rewriteTransitionGroupContextImport() { isMaterialTransition && babelPath.node.source.value === 'react-transition-group/TransitionGroupContext' ) { - // Package-private `#` imports are supported in Node 14.6.0+. - // Keep @mui/material's broader engine range unchanged here; changing it is a - // separate support-policy decision for the small Node 14.0-14.5 window. - babelPath.node.source.value = '#mui/TransitionGroupContext'; + // Use the explicit CJS file for Node builds; package.json's `browser` + // field redirects this request to RTG's ESM file in browser bundlers. + babelPath.node.source.value = 'react-transition-group/cjs/TransitionGroupContext.js'; } }, }, diff --git a/packages/mui-material/package.json b/packages/mui-material/package.json index bf0ef6b31b42ba..0716eb5e553135 100644 --- a/packages/mui-material/package.json +++ b/packages/mui-material/package.json @@ -91,12 +91,8 @@ "engines": { "node": ">=14.0.0" }, - "imports": { - "#mui/TransitionGroupContext": { - "node": "./src/internal/TransitionGroupContext.ts", - "browser": "./src/internal/TransitionGroupContext.browser.ts", - "default": "./src/internal/TransitionGroupContext.browser.ts" - } + "browser": { + "react-transition-group/cjs/TransitionGroupContext.js": "react-transition-group/esm/TransitionGroupContext.js" }, "exports": { ".": "./src/index.js", diff --git a/packages/mui-material/src/internal/TransitionGroupContext.browser.ts b/packages/mui-material/src/internal/TransitionGroupContext.browser.ts deleted file mode 100644 index 813f6e39d00364..00000000000000 --- a/packages/mui-material/src/internal/TransitionGroupContext.browser.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -// eslint-disable-next-line import/extensions -- Browser builds need the explicit .js extension. -import TransitionGroupContext from 'react-transition-group/esm/TransitionGroupContext.js'; - -export default TransitionGroupContext; diff --git a/packages/mui-material/src/internal/TransitionGroupContext.ts b/packages/mui-material/src/internal/TransitionGroupContext.ts deleted file mode 100644 index d41671623487b7..00000000000000 --- a/packages/mui-material/src/internal/TransitionGroupContext.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -// eslint-disable-next-line import/extensions -- Node ESM needs the explicit .js extension. -import TransitionGroupContext from 'react-transition-group/cjs/TransitionGroupContext.js'; - -export default TransitionGroupContext; diff --git a/packages/mui-material/src/internal/react-transition-group.d.ts b/packages/mui-material/src/internal/react-transition-group.d.ts index 2c9651e43894ad..ef20f1c36ce0cd 100644 --- a/packages/mui-material/src/internal/react-transition-group.d.ts +++ b/packages/mui-material/src/internal/react-transition-group.d.ts @@ -8,25 +8,3 @@ declare module 'react-transition-group/TransitionGroupContext' { const TransitionGroupContext: React.Context; export default TransitionGroupContext; } - -declare module 'react-transition-group/cjs/TransitionGroupContext.js' { - import * as React from 'react'; - - interface TransitionGroupContextValue { - isMounting: boolean; - } - - const TransitionGroupContext: React.Context; - export default TransitionGroupContext; -} - -declare module 'react-transition-group/esm/TransitionGroupContext.js' { - import * as React from 'react'; - - interface TransitionGroupContextValue { - isMounting: boolean; - } - - const TransitionGroupContext: React.Context; - export default TransitionGroupContext; -} From 90aef5f59695faaed739e8a394281a03c5741aed Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 18 Jun 2026 02:38:25 +0800 Subject: [PATCH 3/4] remove temp examples --- examples/nextjs-node-esm-ssr/.gitignore | 4 - examples/nextjs-node-esm-ssr/README.md | 50 ------ examples/nextjs-node-esm-ssr/next.config.mjs | 21 --- examples/nextjs-node-esm-ssr/package.json | 33 ---- examples/nextjs-node-esm-ssr/pages/index.tsx | 30 ---- examples/nextjs-node-esm-ssr/tsconfig.json | 30 ---- examples/node-esm-ssr/.gitignore | 1 - examples/node-esm-ssr/README.md | 38 ----- examples/node-esm-ssr/package.json | 27 --- examples/node-esm-ssr/server.mjs | 22 --- examples/node-esm-ssr/src/App.mjs | 158 ------------------ examples/node-esm-ssr/src/renderPage.mjs | 19 --- examples/react-router-node-esm-ssr/.gitignore | 4 - examples/react-router-node-esm-ssr/README.md | 46 ----- .../react-router-node-esm-ssr/app/root.tsx | 24 --- .../react-router-node-esm-ssr/app/routes.ts | 3 - .../app/routes/home.tsx | 72 -------- .../react-router-node-esm-ssr/package.json | 41 ----- .../react-router.config.ts | 12 -- .../react-router-node-esm-ssr/tsconfig.json | 18 -- .../react-router-node-esm-ssr/vite.config.ts | 9 - examples/vitest-node-esm/.gitignore | 1 - examples/vitest-node-esm/README.md | 35 ---- examples/vitest-node-esm/package.json | 32 ---- examples/vitest-node-esm/src/App.jsx | 44 ----- examples/vitest-node-esm/src/App.test.jsx | 55 ------ examples/vitest-node-esm/vitest.config.mjs | 15 -- 27 files changed, 844 deletions(-) delete mode 100644 examples/nextjs-node-esm-ssr/.gitignore delete mode 100644 examples/nextjs-node-esm-ssr/README.md delete mode 100644 examples/nextjs-node-esm-ssr/next.config.mjs delete mode 100644 examples/nextjs-node-esm-ssr/package.json delete mode 100644 examples/nextjs-node-esm-ssr/pages/index.tsx delete mode 100644 examples/nextjs-node-esm-ssr/tsconfig.json delete mode 100644 examples/node-esm-ssr/.gitignore delete mode 100644 examples/node-esm-ssr/README.md delete mode 100644 examples/node-esm-ssr/package.json delete mode 100644 examples/node-esm-ssr/server.mjs delete mode 100644 examples/node-esm-ssr/src/App.mjs delete mode 100644 examples/node-esm-ssr/src/renderPage.mjs delete mode 100644 examples/react-router-node-esm-ssr/.gitignore delete mode 100644 examples/react-router-node-esm-ssr/README.md delete mode 100644 examples/react-router-node-esm-ssr/app/root.tsx delete mode 100644 examples/react-router-node-esm-ssr/app/routes.ts delete mode 100644 examples/react-router-node-esm-ssr/app/routes/home.tsx delete mode 100644 examples/react-router-node-esm-ssr/package.json delete mode 100644 examples/react-router-node-esm-ssr/react-router.config.ts delete mode 100644 examples/react-router-node-esm-ssr/tsconfig.json delete mode 100644 examples/react-router-node-esm-ssr/vite.config.ts delete mode 100644 examples/vitest-node-esm/.gitignore delete mode 100644 examples/vitest-node-esm/README.md delete mode 100644 examples/vitest-node-esm/package.json delete mode 100644 examples/vitest-node-esm/src/App.jsx delete mode 100644 examples/vitest-node-esm/src/App.test.jsx delete mode 100644 examples/vitest-node-esm/vitest.config.mjs diff --git a/examples/nextjs-node-esm-ssr/.gitignore b/examples/nextjs-node-esm-ssr/.gitignore deleted file mode 100644 index 4b805353335b81..00000000000000 --- a/examples/nextjs-node-esm-ssr/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -.next -next-env.d.ts -*.tsbuildinfo diff --git a/examples/nextjs-node-esm-ssr/README.md b/examples/nextjs-node-esm-ssr/README.md deleted file mode 100644 index 19a0d053a12c8e..00000000000000 --- a/examples/nextjs-node-esm-ssr/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Material UI - Next.js Node ESM SSR example - -This is a minimal Next.js Pages Router app that server-renders an -`@mui/x-date-pickers` date picker. - -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 same module resolution path as the -Next.js reports in https://github.com/mui/material-ui/issues/48636, without the -`transpilePackages` workaround. With `@mui/material@9.1.1` this example fails -to build with `ERR_UNSUPPORTED_DIR_IMPORT`. - -Next.js keeps `@mui/material` itself in the built-in -`experimental.optimizePackageImports` list, so direct imports of it are always -bundled and it cannot be listed in `serverExternalPackages`. Node therefore -only resolves `@mui/material` when a server-external package, like -`@mui/x-date-pickers` here, depends on it. - -## How to use - -From the repository root: - -```bash -pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build -pnpm --dir examples/nextjs-node-esm-ssr install --ignore-workspace -pnpm --dir examples/nextjs-node-esm-ssr build -``` - -To run the production server: - -```bash -pnpm --dir examples/nextjs-node-esm-ssr start -``` - -Then open http://localhost:3000. The page is rendered on every request, and the -important signal is that the build and the page render complete without -`ERR_UNSUPPORTED_DIR_IMPORT`. - -## Why this example uses local package paths - -The app depends directly on `@mui/material`, `@mui/x-date-pickers`, React, and -Emotion. `@mui/material` points at `../../packages/mui-material/build` so this -example can test the current checkout before the package is published. - -The `pnpm.overrides` entries are all required for local development: the -unpublished `@mui/material` and `@mui/system` builds declare their MUI -dependencies with the `workspace:` protocol, which only resolves inside this -repository. Each override points one of those dependencies at this checkout's -built packages so the standalone install can resolve them. diff --git a/examples/nextjs-node-esm-ssr/next.config.mjs b/examples/nextjs-node-esm-ssr/next.config.mjs deleted file mode 100644 index d97a3ac2772fff..00000000000000 --- a/examples/nextjs-node-esm-ssr/next.config.mjs +++ /dev/null @@ -1,21 +0,0 @@ -// 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; diff --git a/examples/nextjs-node-esm-ssr/package.json b/examples/nextjs-node-esm-ssr/package.json deleted file mode 100644 index 6daa928a0e51c9..00000000000000 --- a/examples/nextjs-node-esm-ssr/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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": "file:../../packages/mui-material/build", - "@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" - }, - "pnpm": { - "overrides": { - "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", - "@mui/private-theming": "file:../../packages/mui-private-theming/build", - "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", - "@mui/system": "file:../../packages/mui-system/build", - "@mui/types": "file:../../packages/mui-types/build", - "@mui/utils": "file:../../packages/mui-utils/build" - } - } -} diff --git a/examples/nextjs-node-esm-ssr/pages/index.tsx b/examples/nextjs-node-esm-ssr/pages/index.tsx deleted file mode 100644 index 201ab84e68c2e9..00000000000000 --- a/examples/nextjs-node-esm-ssr/pages/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -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 ( - - - - Next.js Node ESM SSR with Material UI - - - The date picker reaches Material UI transition internals through the server-external - @mui/x-date-pickers package. - - - - - ); -} diff --git a/examples/nextjs-node-esm-ssr/tsconfig.json b/examples/nextjs-node-esm-ssr/tsconfig.json deleted file mode 100644 index d73edcaea03078..00000000000000 --- a/examples/nextjs-node-esm-ssr/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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"] -} diff --git a/examples/node-esm-ssr/.gitignore b/examples/node-esm-ssr/.gitignore deleted file mode 100644 index 3c3629e647f5dd..00000000000000 --- a/examples/node-esm-ssr/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/examples/node-esm-ssr/README.md b/examples/node-esm-ssr/README.md deleted file mode 100644 index c2add38205db3f..00000000000000 --- a/examples/node-esm-ssr/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Material UI - Node ESM SSR example - -This is a minimal server-rendered React app that runs in Node's native ESM mode. -It imports transition components from `@mui/material` and renders them on the -server without a bundler rewriting package imports first. It also imports -`TransitionGroup` from `react-transition-group`, like the Material UI transition -docs demo. - -This setup is useful for locally checking the package shape involved in -https://github.com/mui/material-ui/issues/48636. - -## How to use - -From the repository root: - -```bash -pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build -pnpm --dir examples/node-esm-ssr install --ignore-workspace -pnpm --dir examples/node-esm-ssr render -``` - -To run it as a small HTTP server: - -```bash -pnpm --dir examples/node-esm-ssr start -``` - -Then open http://localhost:3000. - -## Why this example uses local package paths - -The app depends directly on `@mui/material`, `react-transition-group`, React, -and Emotion. `@mui/material` points at `../../packages/mui-material/build` so -this example can test the current checkout before the package is published. - -The `pnpm.overrides` entries are only for local development. They make -`@mui/material`'s workspace dependencies resolve to this checkout's built -packages inside the standalone example. diff --git a/examples/node-esm-ssr/package.json b/examples/node-esm-ssr/package.json deleted file mode 100644 index 331d76f3ed21a7..00000000000000 --- a/examples/node-esm-ssr/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "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": "file:../../packages/mui-material/build", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-transition-group": "^4.4.5" - }, - "pnpm": { - "overrides": { - "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", - "@mui/private-theming": "file:../../packages/mui-private-theming/build", - "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", - "@mui/system": "file:../../packages/mui-system/build", - "@mui/types": "file:../../packages/mui-types/build", - "@mui/utils": "file:../../packages/mui-utils/build" - } - } -} diff --git a/examples/node-esm-ssr/server.mjs b/examples/node-esm-ssr/server.mjs deleted file mode 100644 index cfa4833d5b78fe..00000000000000 --- a/examples/node-esm-ssr/server.mjs +++ /dev/null @@ -1,22 +0,0 @@ -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}`); - }); -} diff --git a/examples/node-esm-ssr/src/App.mjs b/examples/node-esm-ssr/src/App.mjs deleted file mode 100644 index db9db282aeeadd..00000000000000 --- a/examples/node-esm-ssr/src/App.mjs +++ /dev/null @@ -1,158 +0,0 @@ -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' }), - ), - ), - ); -} diff --git a/examples/node-esm-ssr/src/renderPage.mjs b/examples/node-esm-ssr/src/renderPage.mjs deleted file mode 100644 index fda0ef28ed4a01..00000000000000 --- a/examples/node-esm-ssr/src/renderPage.mjs +++ /dev/null @@ -1,19 +0,0 @@ -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 ` - - - - - Material UI Node ESM SSR - - -
${appHtml}
- -`; -} diff --git a/examples/react-router-node-esm-ssr/.gitignore b/examples/react-router-node-esm-ssr/.gitignore deleted file mode 100644 index be40ab16504fee..00000000000000 --- a/examples/react-router-node-esm-ssr/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -build -node_modules -.react-router -tsconfig.tsbuildinfo diff --git a/examples/react-router-node-esm-ssr/README.md b/examples/react-router-node-esm-ssr/README.md deleted file mode 100644 index b3bcc51ef9db7d..00000000000000 --- a/examples/react-router-node-esm-ssr/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Material UI - React Router Node ESM SSR example - -This is a minimal React Router SSR app that imports Material UI from the local -built package and uses `TransitionGroup` from `react-transition-group`, like the -Material UI transition docs demo. It mirrors the package-resolution path involved in -https://github.com/mui/material-ui/issues/48636 without the extra template code -from larger React Router examples. - -## How to use - -From the repository root, build the local Material UI packages first: - -```bash -pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build -``` - -Then from this example directory: - -```bash -pnpm install --ignore-workspace -pnpm build -pnpm start -``` - -Open http://localhost:3000. - -For a non-server smoke test after `pnpm build`, import the server build: - -```bash -node -e "import('./build/server/index.js').then(() => console.log('server build import ok'))" -``` - -Before the fix for issue 48636, that import path can fail when Node resolves -Material UI's built ESM transition module and reaches -`react-transition-group/TransitionGroupContext`. - -## Why this example uses local package paths - -The app depends directly on `@mui/material`, `react-transition-group`, React, -Emotion, and React Router. `@mui/material` points at -`../../packages/mui-material/build` so this example can test the current checkout -before the package is published. - -The `pnpm.overrides` entries are only for local development. They make -`@mui/material`'s workspace dependencies resolve to this checkout's built -packages inside the standalone example. diff --git a/examples/react-router-node-esm-ssr/app/root.tsx b/examples/react-router-node-esm-ssr/app/root.tsx deleted file mode 100644 index 429e6c3f8b1c38..00000000000000 --- a/examples/react-router-node-esm-ssr/app/root.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} diff --git a/examples/react-router-node-esm-ssr/app/routes.ts b/examples/react-router-node-esm-ssr/app/routes.ts deleted file mode 100644 index 205ff3ccb9fd4e..00000000000000 --- a/examples/react-router-node-esm-ssr/app/routes.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type RouteConfig, index } from '@react-router/dev/routes'; - -export default [index('routes/home.tsx')] satisfies RouteConfig; diff --git a/examples/react-router-node-esm-ssr/app/routes/home.tsx b/examples/react-router-node-esm-ssr/app/routes/home.tsx deleted file mode 100644 index 829747865c392b..00000000000000 --- a/examples/react-router-node-esm-ssr/app/routes/home.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import Box from '@mui/material/Box'; -import Collapse from '@mui/material/Collapse'; -import Fade from '@mui/material/Fade'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { TransitionGroup } from 'react-transition-group'; - -const routeItems = ['Dashboard shell', 'Details route', 'Settings route']; - -export function meta() { - return [ - { title: 'React Router Node ESM SSR' }, - { - name: 'description', - content: 'Minimal React Router SSR app using Material UI transitions.', - }, - ]; -} - -export default function Home() { - return ( - - - - React Router SSR with Material UI - - - This route renders through React Router's server build and imports MUI transition - components from the installed package. - - - - - Fade rendered during SSR - - - - - - {routeItems.map((item) => ( - - - - - - ))} - - - - - ); -} diff --git a/examples/react-router-node-esm-ssr/package.json b/examples/react-router-node-esm-ssr/package.json deleted file mode 100644 index a2755d0bbd5388..00000000000000 --- a/examples/react-router-node-esm-ssr/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "react-router-node-esm-ssr", - "private": true, - "type": "module", - "scripts": { - "build": "react-router build", - "start": "react-router-serve ./build/server/index.js", - "typecheck": "react-router typegen && tsc" - }, - "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/material": "file:../../packages/mui-material/build", - "@react-router/node": "7.16.0", - "@react-router/serve": "7.16.0", - "isbot": "^5", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-router": "7.16.0", - "react-transition-group": "^4.4.5" - }, - "devDependencies": { - "@react-router/dev": "7.16.0", - "@types/node": "^22.0.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@types/react-transition-group": "^4.4.12", - "typescript": "^5.9.3", - "vite": "^8.0.3" - }, - "pnpm": { - "overrides": { - "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", - "@mui/private-theming": "file:../../packages/mui-private-theming/build", - "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", - "@mui/system": "file:../../packages/mui-system/build", - "@mui/types": "file:../../packages/mui-types/build", - "@mui/utils": "file:../../packages/mui-utils/build" - } - } -} diff --git a/examples/react-router-node-esm-ssr/react-router.config.ts b/examples/react-router-node-esm-ssr/react-router.config.ts deleted file mode 100644 index 9d8b4134caa567..00000000000000 --- a/examples/react-router-node-esm-ssr/react-router.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Config } from '@react-router/dev/config'; - -export default { - ssr: true, - future: { - v8_middleware: true, - v8_passThroughRequests: true, - v8_splitRouteModules: true, - v8_trailingSlashAwareDataRequests: true, - v8_viteEnvironmentApi: true, - }, -} satisfies Config; diff --git a/examples/react-router-node-esm-ssr/tsconfig.json b/examples/react-router-node-esm-ssr/tsconfig.json deleted file mode 100644 index 90aefcd00b4a53..00000000000000 --- a/examples/react-router-node-esm-ssr/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "include": [".react-router/types/**/*", "app/**/*"], - "compilerOptions": { - "composite": true, - "strict": true, - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["vite/client"], - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "rootDirs": [".", "./.react-router/types"], - "skipLibCheck": true, - "noEmit": true, - "esModuleInterop": true, - "resolveJsonModule": true - } -} diff --git a/examples/react-router-node-esm-ssr/vite.config.ts b/examples/react-router-node-esm-ssr/vite.config.ts deleted file mode 100644 index 263bb8e25747ed..00000000000000 --- a/examples/react-router-node-esm-ssr/vite.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { reactRouter } from '@react-router/dev/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [reactRouter()], - ssr: { - external: ['@mui/material'], - }, -}); diff --git a/examples/vitest-node-esm/.gitignore b/examples/vitest-node-esm/.gitignore deleted file mode 100644 index 3c3629e647f5dd..00000000000000 --- a/examples/vitest-node-esm/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/examples/vitest-node-esm/README.md b/examples/vitest-node-esm/README.md deleted file mode 100644 index 4054a045209829..00000000000000 --- a/examples/vitest-node-esm/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Material UI - Vitest Node ESM example - -This is a minimal Vitest + jsdom test setup for an app that renders Material UI -transition components inside `TransitionGroup` from `react-transition-group`, -like the Material UI transition docs demo. - -Vitest externalizes `node_modules` dependencies by default, so these tests load -`@mui/material` through Node's native ESM resolver — the same module resolution -path that the test-runner reports in -https://github.com/mui/material-ui/issues/48636 hit. The Vitest config -intentionally has no `server.deps.inline` workaround for `@mui/material` or -`react-transition-group`. - -## How to use - -From the repository root: - -```bash -pnpm -F @mui/types -F @mui/utils -F @mui/private-theming -F @mui/styled-engine -F @mui/system -F @mui/material build -pnpm --dir examples/vitest-node-esm install --ignore-workspace -pnpm --dir examples/vitest-node-esm test -``` - -The important signal is that the tests pass without `ERR_UNSUPPORTED_DIR_IMPORT` -and without inlining `@mui/material` into the Vite transform pipeline. - -## Why this example uses local package paths - -The app depends directly on `@mui/material`, `react-transition-group`, React, -and Emotion. `@mui/material` points at `../../packages/mui-material/build` so -this example can test the current checkout before the package is published. - -The `pnpm.overrides` entries are only for local development. They make -`@mui/material`'s workspace dependencies resolve to this checkout's built -packages inside the standalone example. diff --git a/examples/vitest-node-esm/package.json b/examples/vitest-node-esm/package.json deleted file mode 100644 index c8523d59393f19..00000000000000 --- a/examples/vitest-node-esm/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "vitest-node-esm", - "private": true, - "type": "module", - "scripts": { - "test": "vitest run" - }, - "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/material": "file:../../packages/mui-material/build", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-transition-group": "^4.4.5" - }, - "devDependencies": { - "@testing-library/dom": "^10.4.0", - "@testing-library/react": "^16.3.0", - "jsdom": "^26.1.0", - "vitest": "^4.1.0" - }, - "pnpm": { - "overrides": { - "@mui/core-downloads-tracker": "file:../../packages/mui-core-downloads-tracker", - "@mui/private-theming": "file:../../packages/mui-private-theming/build", - "@mui/styled-engine": "file:../../packages/mui-styled-engine/build", - "@mui/system": "file:../../packages/mui-system/build", - "@mui/types": "file:../../packages/mui-types/build", - "@mui/utils": "file:../../packages/mui-utils/build" - } - } -} diff --git a/examples/vitest-node-esm/src/App.jsx b/examples/vitest-node-esm/src/App.jsx deleted file mode 100644 index 5c2a0cf772fa12..00000000000000 --- a/examples/vitest-node-esm/src/App.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import Collapse from '@mui/material/Collapse'; -import Fade from '@mui/material/Fade'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { TransitionGroup } from 'react-transition-group'; - -const initialItems = ['Item 1', 'Item 2', 'Item 3']; - -export default function App() { - const [items, setItems] = React.useState(initialItems); - - return ( - - - Vitest with Material UI transitions - - - Faded in on first render - - - - - {items.map((item) => ( - - - - - - ))} - - - - ); -} diff --git a/examples/vitest-node-esm/src/App.test.jsx b/examples/vitest-node-esm/src/App.test.jsx deleted file mode 100644 index 10decf380aaf13..00000000000000 --- a/examples/vitest-node-esm/src/App.test.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import Collapse from '@mui/material/Collapse'; -import { TransitionGroup } from 'react-transition-group'; -import { expect, test, vi } from 'vitest'; -import App from './App.jsx'; - -test('renders the app through the published @mui/material package layout', () => { - render(); - - expect(screen.getByText('Faded in on first render')).toBeDefined(); - expect(screen.getByText('Item 1')).toBeDefined(); -}); - -test('adds a list item to an already-mounted TransitionGroup', async () => { - render(); - - fireEvent.click(screen.getByRole('button', { name: 'Add item' })); - - expect(await screen.findByText('Item 4')).toBeDefined(); -}); - -test('Collapse added to a mounted TransitionGroup enters with isAppearing=false', async () => { - const handleEntered = vi.fn(); - - function Harness() { - const [open, setOpen] = React.useState(false); - return ( - - - - {open ? ( - -

Collapse content

-
- ) : null} -
-
- ); - } - - render(); - fireEvent.click(screen.getByRole('button', { name: 'Open' })); - - expect(await screen.findByText('Collapse content')).toBeDefined(); - await waitFor(() => expect(handleEntered).toHaveBeenCalledTimes(1)); - - // Material UI and react-transition-group must share one TransitionGroupContext - // instance for this to hold: a child added after the group mounted reports - // isAppearing=false (the last callback argument). - const enteredArguments = handleEntered.mock.calls[0]; - expect(enteredArguments[enteredArguments.length - 1]).toBe(false); -}); diff --git a/examples/vitest-node-esm/vitest.config.mjs b/examples/vitest-node-esm/vitest.config.mjs deleted file mode 100644 index e40eceafff368e..00000000000000 --- a/examples/vitest-node-esm/vitest.config.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - environment: 'jsdom', - // Expose global test hooks so @testing-library/react registers its - // automatic cleanup. - globals: true, - // Intentionally no `server.deps.inline` entries for `@mui/material` or - // `react-transition-group`. Vitest externalizes `node_modules` by default, - // so the tests load `@mui/material` through Node's own ESM resolver — the - // module resolution path from - // https://github.com/mui/material-ui/issues/48636. - }, -}); From ab7f810c436d7405bdcc564f53b2d9b615107135 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 18 Jun 2026 19:58:49 +0800 Subject: [PATCH 4/4] try direct import from RTG cjs path --- babel.config.mjs | 36 ------------------- .../mui-material/src/internal/Transition.tsx | 5 ++- .../src/internal/react-transition-group.d.ts | 11 ++++++ 3 files changed, 15 insertions(+), 37 deletions(-) diff --git a/babel.config.mjs b/babel.config.mjs index f923c7b1310187..1448b5a1584d22 100644 --- a/babel.config.mjs +++ b/babel.config.mjs @@ -21,37 +21,6 @@ function resolveAliasPath(relativeToBabelConf) { return `./${resolvedPath.replace('\\', '/')}`; } -function rewriteTransitionGroupContextImport() { - const transitionSourcePath = path.join( - 'packages', - 'mui-material', - 'src', - 'internal', - 'Transition.tsx', - ); - - return { - name: 'rewrite-transition-group-context-import', - visitor: { - ImportDeclaration(babelPath) { - const sourceFilename = babelPath.hub.file.opts.filename; - const isMaterialTransition = - typeof sourceFilename === 'string' && - path.normalize(sourceFilename).endsWith(transitionSourcePath); - - if ( - isMaterialTransition && - babelPath.node.source.value === 'react-transition-group/TransitionGroupContext' - ) { - // Use the explicit CJS file for Node builds; package.json's `browser` - // field redirects this request to RTG's ESM file in browser bundlers. - babelPath.node.source.value = 'react-transition-group/cjs/TransitionGroupContext.js'; - } - }, - }, - }; -} - /** @type {babel.ConfigFunction} */ export default function getBabelConfig(api) { const baseConfig = getBaseConfig(api); @@ -59,7 +28,6 @@ export default function getBabelConfig(api) { // Covers: docs prod build (NODE_ENV=production), package esm build (BABEL_ENV=stable), // package cjs build (BABEL_ENV=node). Excludes docs dev, tests, coverage. const isProductionBuild = api.env(['production', 'stable', 'node']); - const isPackageBuild = api.env(['stable', 'node']); const defaultAlias = { '@mui/material': resolveAliasPath('./packages/mui-material/src'), @@ -103,10 +71,6 @@ export default function getBabelConfig(api) { !excludedBasePlugins.has(pluginName), ); - if (isPackageBuild) { - basePlugins.push(rewriteTransitionGroupContextImport()); - } - if (isProductionBuild) { basePlugins.push(...prodOnlyPlugins); } diff --git a/packages/mui-material/src/internal/Transition.tsx b/packages/mui-material/src/internal/Transition.tsx index bde3801241b60e..0b57c0c76f6e80 100644 --- a/packages/mui-material/src/internal/Transition.tsx +++ b/packages/mui-material/src/internal/Transition.tsx @@ -6,7 +6,10 @@ import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useValueAsRef from '@mui/utils/useValueAsRef'; // Material UI transitions must still work inside react-transition-group's TransitionGroup. // Import only its context module; do not import its Transition or TransitionGroup components. -import TransitionGroupContext from 'react-transition-group/TransitionGroupContext'; +// Use RTG's explicit CJS file for Node ESM/SSR; package.json's `browser` field redirects +// browser bundles to RTG's ESM file. +// eslint-disable-next-line import/extensions -- Node ESM needs the explicit .js extension. +import TransitionGroupContext from 'react-transition-group/cjs/TransitionGroupContext.js'; import { reflow } from '../transitions/utils'; type RenderedTransitionStatus = 'entering' | 'entered' | 'exiting' | 'exited'; diff --git a/packages/mui-material/src/internal/react-transition-group.d.ts b/packages/mui-material/src/internal/react-transition-group.d.ts index ef20f1c36ce0cd..bca21db721ead7 100644 --- a/packages/mui-material/src/internal/react-transition-group.d.ts +++ b/packages/mui-material/src/internal/react-transition-group.d.ts @@ -8,3 +8,14 @@ declare module 'react-transition-group/TransitionGroupContext' { const TransitionGroupContext: React.Context; export default TransitionGroupContext; } + +declare module 'react-transition-group/cjs/TransitionGroupContext.js' { + import * as React from 'react'; + + interface TransitionGroupContextValue { + isMounting: boolean; + } + + const TransitionGroupContext: React.Context; + export default TransitionGroupContext; +}