Skip to content

fix: build and serve the app with Turbopack - #2025

Merged
OliverDudgeon merged 1 commit into
devfrom
feat/2020-turbopack-migration
Aug 28, 2026
Merged

fix: build and serve the app with Turbopack#2025
OliverDudgeon merged 1 commit into
devfrom
feat/2020-turbopack-migration

Conversation

@OliverDudgeon

@OliverDudgeon OliverDudgeon commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Completes the Turbopack migration tracked in #2020. --webpack comes off dev, dev:debug and build; analyze* keeps it, because @next/bundle-analyzer is webpack-only (and, separately, is not currently wired into next.config.mjs at all).

The paper patch cleared the original blocker

The paper@0.12.18 patch from #2024 is load-bearing here and does its job. With no resolveAlias and no src/stubs/jsdom-utils.stub.js anywhere in the tree:

  • next build under Turbopack compiles clean and generates all 38 pages.
  • next dev under Turbopack boots and serves.
  • The sketcher itself runs. Verified through a temporary public page rendering Sketcher — it is otherwise auth-gated and has no story — with Ketcher mounting and drawing benzene from c1ccccc1, in both next dev and a production next start. No jsdom or paper errors in the server log or the browser console. The temporary page is not part of this branch.

That satisfies the "done when" of #2020.

Switching bundlers exposed a second defect

Unrelated to paper, and it would have shipped silently with the migration.

The Pages Router does not bundle server-side node_modules — that is what bundlePagesRouterDependencies exists to opt into, and the App Router does it by default. Left unbundled, Turbopack's SSR build ends up holding two copies of @emotion/react: the one Node loads at runtime for an externalised package, and the one bundled into the SSR graph. @mui/material-nextjs is externalised, so the cache AppCacheProvider writes lands in a different emotion React context than the bundled MUI components read. They fall back to emotion's default css cache instead of the mui one, and every server-rendered class name mismatched at hydration (css-zf0iqh server against mui-zf0iqh client), on every page, in development and production alike.

Webpack never split them because it bundles the server dependencies itself — its server output contains emotion-react.cjs.js and no emotion external at all. So bundlePagesRouterDependencies: true restores exactly what webpack was already doing.

Comparing the data-emotion keys in the SSR HTML:

keys emitted
webpack, dev and production mui, mui-global
Turbopack, before css ×35, css-global ×2
Turbopack, after mui, mui-global

After the fix the hydrated DOM under Turbopack matches the webpack production baseline exactly — 259 mui- and 60 css- class names, the latter being ketcher-react's own emotion instance, present under webpack too — and React logs no mismatch.

Why bundling, and not just transpiling @mui/material-nextjs

Transpiling that one package also un-externalises it and does fix the pages I first checked, but it treats the symptom. The externalisation boundary is what splits emotion, and @mui/lab, @rjsf/mui, material-ui-popup-state and @squonk/mui-theme all sit on the far side of it as well. A page rendering a @mui/lab Timeline still emitted css-i6popu-MuiTimeline-root from the server with only @mui/material-nextjs transpiled; under bundlePagesRouterDependencies it emits mui-i6popu-MuiTimeline-root and hydrates clean. Since @mui/lab and @rjsf/mui are used on the tasks, workflows and job-run screens, the narrow fix would have left the defect live on exactly the pages that matter most.

Upstream status, and what else was tried

Open upstream with no fix released and no workaround recorded:

  • vercel/next.js#82607 — the exact symptom, and specifically Turbopack plus Pages Router; the App Router is unaffected, which fits, since it bundles server dependencies by default.
  • vercel/next.js#91411 — why the two copies are not the same file: Turbopack takes emotion's ESM build where Node's runtime resolution of the same specifier lands on the CJS one through emotion-react.cjs.mjs.
  • mui/material-ui#46385 — closed with no fix; compiler: { emotion: true } reported as having no effect.

Each of these was tested and each left the mismatch in place:

Approach Result
bundlePagesRouterDependencies: true works, everywhere
transpilePackages: ["@mui/material-nextjs"] fixes MUI core only; @mui/lab and friends still mismatch
Upgrade @mui/material-nextjs 9.0.1 → 9.4.0 no change
serverExternalPackages for @emotion/{react,cache,styled} no change
turbopack.resolveAlias @emotion/reactdist/emotion-react.cjs.mjs no change
The same alias plus the jsx-runtime and jsx-dev-runtime subpaths no change

The alias failing is the informative one: forcing every bare emotion specifier onto webpack's collapsed build does nothing when the package reaching emotion is externalised, because that resolution happens in Node at runtime, outside the bundler's aliasing entirely.

The @mui/material-nextjs version bump is worth doing on its own merits — the monorepo is uniformly on 9.0.1 against 9.4.0 latest, and Renovate's #1857 covers it — but it fixes nothing here, so it is left out of this branch.

Checks

pnpm build, pnpm tsc, pnpm lint and pnpm test (808 passed) all pass on Turbopack with dependency bundling on. A production next start was checked by hand as well: SSR emits mui keys, the sketcher renders, and neither the home page, a @mui/lab page nor the sketcher page reports a hydration mismatch.

Note for #2015

That branch restores the jsdom alias and src/stubs/jsdom-utils.stub.js. Neither is needed once this lands, and both should come out of it.

Closes #2020

🤖 Generated with Claude Code

@OliverDudgeon
OliverDudgeon force-pushed the feat/2020-turbopack-migration branch from ed5ff18 to 69f048e Compare August 28, 2026 08:56
The paper patch in #2024 removed the last thing keeping the sketcher out of
a Turbopack server graph, so `--webpack` can come off `dev`, `dev:debug` and
`build`. `analyze*` keeps it, since @next/bundle-analyzer is webpack-only.

Switching bundlers exposed a second defect, unrelated to paper. The Pages
Router leaves server-side node_modules unbundled, so Turbopack ends up with two
copies of @emotion/react: the one Node loads at runtime for an externalised
package, and the one bundled into the SSR graph. @mui/material-nextjs is
externalised, so the cache AppCacheProvider writes lands in a different emotion
context than the bundled MUI components read; they fall back to emotion's
default "css" cache instead of the "mui" one, and every server-rendered class
name mismatched on hydration, on every page, in development and production
alike. Webpack never split them because it bundles the server dependencies
itself, so bundlePagesRouterDependencies restores what it was doing — and what
the App Router does by default. SSR emits only "mui" keys again and the
hydrated DOM matches the webpack production baseline exactly.

Bundling rather than transpiling @mui/material-nextjs alone, because the
externalisation boundary is what splits emotion, and @mui/lab, @rjsf/mui,
material-ui-popup-state and @squonk/mui-theme sit on the far side of it too: a
page rendering a @mui/lab timeline still emitted "css" class names with only
that package transpiled.

This is open upstream as vercel/next.js#82607 and vercel/next.js#91411, with no
fix released and no workaround recorded. Upgrading @mui/material-nextjs to
9.4.0, externalising emotion with serverExternalPackages, and aliasing emotion
onto its collapsed cjs.mjs build with turbopack.resolveAlias were each tested
and each left the mismatch in place.

Closes #2020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@OliverDudgeon
OliverDudgeon force-pushed the feat/2020-turbopack-migration branch from 69f048e to ae2539d Compare August 28, 2026 09:03
@OliverDudgeon OliverDudgeon changed the title feat: build and serve the app with Turbopack fix: build and serve the app with Turbopack Aug 28, 2026
@OliverDudgeon
OliverDudgeon merged commit 98343f5 into dev Aug 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant