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/.stackblitzrc b/examples/nextjs-node-esm-ssr/.stackblitzrc
new file mode 100644
index 00000000000000..8812c06a591915
--- /dev/null
+++ b/examples/nextjs-node-esm-ssr/.stackblitzrc
@@ -0,0 +1,3 @@
+{
+ "startCommand": "npm run build && npm run start"
+}
diff --git a/examples/nextjs-node-esm-ssr/README.md b/examples/nextjs-node-esm-ssr/README.md
new file mode 100644
index 00000000000000..84e375bac3bfbf
--- /dev/null
+++ b/examples/nextjs-node-esm-ssr/README.md
@@ -0,0 +1,24 @@
+# Material UI - Next.js Node ESM SSR example
+
+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:
+
+[](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.
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..3a464101cb22fa
--- /dev/null
+++ b/examples/nextjs-node-esm-ssr/package.json
@@ -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"
+ }
+}
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/.stackblitzrc b/examples/node-esm-ssr/.stackblitzrc
new file mode 100644
index 00000000000000..ccc731a8edfc40
--- /dev/null
+++ b/examples/node-esm-ssr/.stackblitzrc
@@ -0,0 +1,3 @@
+{
+ "startCommand": "npm run start"
+}
diff --git a/examples/node-esm-ssr/README.md b/examples/node-esm-ssr/README.md
new file mode 100644
index 00000000000000..cf0d6f7e7d82c7
--- /dev/null
+++ b/examples/node-esm-ssr/README.md
@@ -0,0 +1,28 @@
+# Material UI - Node ESM SSR example
+
+Minimal Node ESM server-rendered React app for checking issue 48636. It renders
+Material UI transition components without a bundler rewriting package imports.
+
+## 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:
+
+[](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.
diff --git a/examples/node-esm-ssr/package.json b/examples/node-esm-ssr/package.json
new file mode 100644
index 00000000000000..0e3390d8e4c4df
--- /dev/null
+++ b/examples/node-esm-ssr/package.json
@@ -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"
+ }
+}
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/.stackblitzrc b/examples/react-router-node-esm-ssr/.stackblitzrc
new file mode 100644
index 00000000000000..8812c06a591915
--- /dev/null
+++ b/examples/react-router-node-esm-ssr/.stackblitzrc
@@ -0,0 +1,3 @@
+{
+ "startCommand": "npm run build && npm run start"
+}
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..c327337b9dcae6
--- /dev/null
+++ b/examples/react-router-node-esm-ssr/README.md
@@ -0,0 +1,24 @@
+# Material UI - React Router Node ESM SSR example
+
+Minimal React Router SSR app for checking issue 48636. It keeps
+`@mui/material` external in the server build so Node resolves the package at
+runtime.
+
+## How to use
+
+From the repository root:
+
+```bash
+pnpm --dir examples/react-router-node-esm-ssr install --ignore-workspace
+pnpm --dir examples/react-router-node-esm-ssr build
+pnpm --dir examples/react-router-node-esm-ssr start
+```
+
+or:
+
+[](https://stackblitz.com/github/mj12albert/material-ui/tree/examples-rtg-esm-fix/examples/react-router-node-esm-ssr)
+
+Open http://localhost:3000. The server build should import and render without
+`ERR_UNSUPPORTED_DIR_IMPORT`.
+
+This example installs `@mui/material` from the PR 48645 package preview.
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..da8dc0288681ba
--- /dev/null
+++ b/examples/react-router-node-esm-ssr/package.json
@@ -0,0 +1,31 @@
+{
+ "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": "https://pkg.pr.new/mui/material-ui/@mui/material@90aef5f59695faaed739e8a394281a03c5741aed",
+ "@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"
+ }
+}
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..a8a2c3aa14f02b
--- /dev/null
+++ b/examples/vitest-node-esm/README.md
@@ -0,0 +1,18 @@
+# Material UI - Vitest Node ESM example
+
+Minimal Vitest + jsdom setup for checking issue 48636. Vitest externalizes
+dependencies, so the test loads `@mui/material` through Node's ESM resolver.
+
+## How to use
+
+From the repository root:
+
+```bash
+pnpm --dir examples/vitest-node-esm install --ignore-workspace
+pnpm --dir examples/vitest-node-esm test
+```
+
+The tests should pass without `ERR_UNSUPPORTED_DIR_IMPORT` and without inlining
+`@mui/material` into Vite.
+
+This example installs `@mui/material` from the PR 48645 package preview.
diff --git a/examples/vitest-node-esm/package.json b/examples/vitest-node-esm/package.json
new file mode 100644
index 00000000000000..3a7ee53edb6902
--- /dev/null
+++ b/examples/vitest-node-esm/package.json
@@ -0,0 +1,22 @@
+{
+ "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": "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"
+ },
+ "devDependencies": {
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/react": "^16.3.0",
+ "jsdom": "^26.1.0",
+ "vitest": "^4.1.0"
+ }
+}
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/examples/webpack-cjs-browser/.gitignore b/examples/webpack-cjs-browser/.gitignore
new file mode 100644
index 00000000000000..de4d1f007dd195
--- /dev/null
+++ b/examples/webpack-cjs-browser/.gitignore
@@ -0,0 +1,2 @@
+dist
+node_modules
diff --git a/examples/webpack-cjs-browser/.stackblitzrc b/examples/webpack-cjs-browser/.stackblitzrc
new file mode 100644
index 00000000000000..09e22c0da2fc3a
--- /dev/null
+++ b/examples/webpack-cjs-browser/.stackblitzrc
@@ -0,0 +1,3 @@
+{
+ "startCommand": "npm run dev"
+}
diff --git a/examples/webpack-cjs-browser/README.md b/examples/webpack-cjs-browser/README.md
new file mode 100644
index 00000000000000..effcb39f872b9f
--- /dev/null
+++ b/examples/webpack-cjs-browser/README.md
@@ -0,0 +1,26 @@
+# Material UI - Webpack CJS browser example
+
+Minimal Webpack browser app for checking issue 48636. The app uses CommonJS
+`require()` calls and verifies that Material UI and `react-transition-group`
+share the same transition context in the browser bundle.
+
+## How to use
+
+From the repository root:
+
+```bash
+pnpm --dir examples/webpack-cjs-browser install --ignore-workspace
+pnpm --dir examples/webpack-cjs-browser dev
+```
+
+or:
+
+[](https://stackblitz.com/github/mj12albert/material-ui/tree/examples-rtg-esm-fix/examples/webpack-cjs-browser)
+
+Open http://127.0.0.1:3005, click "Add item", and confirm:
+
+```text
+Last isAppearing: false
+```
+
+This example installs `@mui/material` from the PR 48645 package preview.
diff --git a/examples/webpack-cjs-browser/index.html b/examples/webpack-cjs-browser/index.html
new file mode 100644
index 00000000000000..5028f327139f22
--- /dev/null
+++ b/examples/webpack-cjs-browser/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Webpack CJS browser smoke test
+
+
+
+
+
+
diff --git a/examples/webpack-cjs-browser/package.json b/examples/webpack-cjs-browser/package.json
new file mode 100644
index 00000000000000..0b18ea125a4097
--- /dev/null
+++ b/examples/webpack-cjs-browser/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "webpack-cjs-browser",
+ "private": true,
+ "scripts": {
+ "dev": "webpack serve --mode=development",
+ "build": "webpack --mode=production",
+ "build:dev": "webpack --mode=development"
+ },
+ "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"
+ },
+ "devDependencies": {
+ "webpack": "latest",
+ "webpack-cli": "latest",
+ "webpack-dev-server": "latest"
+ }
+}
diff --git a/examples/webpack-cjs-browser/src/index.cjs b/examples/webpack-cjs-browser/src/index.cjs
new file mode 100644
index 00000000000000..fa954fd91b416e
--- /dev/null
+++ b/examples/webpack-cjs-browser/src/index.cjs
@@ -0,0 +1,6 @@
+const React = require('react');
+const ReactDOM = require('react-dom/client');
+const { ListWithTransitions } = require('./internal-ui.cjs');
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(React.createElement(ListWithTransitions));
diff --git a/examples/webpack-cjs-browser/src/internal-ui.cjs b/examples/webpack-cjs-browser/src/internal-ui.cjs
new file mode 100644
index 00000000000000..5e6e6b1c7e6700
--- /dev/null
+++ b/examples/webpack-cjs-browser/src/internal-ui.cjs
@@ -0,0 +1,81 @@
+const React = require('react');
+const Button = require('@mui/material/Button').default;
+const Collapse = require('@mui/material/Collapse').default;
+const CssBaseline = require('@mui/material/CssBaseline').default;
+const List = require('@mui/material/List').default;
+const ListItem = require('@mui/material/ListItem').default;
+const ListItemText = require('@mui/material/ListItemText').default;
+const Stack = require('@mui/material/Stack').default;
+const Typography = require('@mui/material/Typography').default;
+const { TransitionGroup } = require('react-transition-group');
+
+const h = React.createElement;
+
+function ListWithTransitions() {
+ const [items, setItems] = React.useState([
+ { id: 1, label: 'Initial item' },
+ { id: 2, label: 'Second item' },
+ ]);
+ const [lastIsAppearing, setLastIsAppearing] = React.useState('pending');
+
+ const handleAdd = () => {
+ setItems((currentItems) => [
+ ...currentItems,
+ { id: currentItems.length + 1, label: `Added item ${currentItems.length + 1}` },
+ ]);
+ };
+
+ const handleEntered = React.useCallback((node, isAppearing) => {
+ window.__MUI_WEBPACK_CJS_BROWSER_SMOKE__ = { lastIsAppearing: isAppearing };
+ setLastIsAppearing(String(isAppearing));
+ }, []);
+
+ return h(
+ React.Fragment,
+ null,
+ h(CssBaseline),
+ h(
+ 'main',
+ {
+ style: {
+ maxWidth: 640,
+ margin: '0 auto',
+ padding: 32,
+ fontFamily: 'system-ui, sans-serif',
+ },
+ },
+ h(
+ Stack,
+ { spacing: 2 },
+ h(Typography, { component: 'h1', variant: 'h4' }, 'Webpack CJS browser smoke test'),
+ h(
+ Typography,
+ { color: 'text.secondary' },
+ 'This app uses CommonJS require() calls and is bundled for the browser.',
+ ),
+ h(Button, { onClick: handleAdd, variant: 'contained' }, 'Add item'),
+ h(Typography, { component: 'p' }, `Last isAppearing: ${lastIsAppearing}`),
+ h(
+ List,
+ {
+ dense: true,
+ sx: { border: '1px solid', borderColor: 'divider', borderRadius: 1 },
+ },
+ h(
+ TransitionGroup,
+ null,
+ items.map((item) =>
+ h(
+ Collapse,
+ { key: item.id, timeout: 0, onEntered: handleEntered },
+ h(ListItem, null, h(ListItemText, { primary: item.label })),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+}
+
+module.exports = { ListWithTransitions };
diff --git a/examples/webpack-cjs-browser/webpack.config.cjs b/examples/webpack-cjs-browser/webpack.config.cjs
new file mode 100644
index 00000000000000..04d027189e04f1
--- /dev/null
+++ b/examples/webpack-cjs-browser/webpack.config.cjs
@@ -0,0 +1,28 @@
+const path = require('node:path');
+
+module.exports = {
+ entry: './src/index.cjs',
+ output: {
+ path: path.resolve(__dirname, 'dist'),
+ filename: 'main.js',
+ publicPath: '/dist/',
+ clean: true,
+ },
+ devServer: {
+ static: {
+ directory: __dirname,
+ },
+ devMiddleware: {
+ publicPath: '/dist/',
+ },
+ host: '127.0.0.1',
+ port: 3005,
+ },
+ target: 'web',
+ devtool: false,
+ resolve: {
+ extensions: ['.cjs', '.js'],
+ aliasFields: ['browser'],
+ mainFields: ['browser', 'module', 'main'],
+ },
+};