fix: build and serve the app with Turbopack - #2025
Merged
Merged
Conversation
OliverDudgeon
force-pushed
the
feat/2020-turbopack-migration
branch
from
August 28, 2026 08:56
ed5ff18 to
69f048e
Compare
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
force-pushed
the
feat/2020-turbopack-migration
branch
from
August 28, 2026 09:03
69f048e to
ae2539d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes the Turbopack migration tracked in #2020.
--webpackcomes offdev,dev:debugandbuild;analyze*keeps it, because@next/bundle-analyzeris webpack-only (and, separately, is not currently wired intonext.config.mjsat all).The paper patch cleared the original blocker
The
paper@0.12.18patch from #2024 is load-bearing here and does its job. With noresolveAliasand nosrc/stubs/jsdom-utils.stub.jsanywhere in the tree:next buildunder Turbopack compiles clean and generates all 38 pages.next devunder Turbopack boots and serves.Sketcher— it is otherwise auth-gated and has no story — with Ketcher mounting and drawing benzene fromc1ccccc1, in bothnext devand a productionnext 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 whatbundlePagesRouterDependenciesexists 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-nextjsis externalised, so the cacheAppCacheProviderwrites lands in a different emotion React context than the bundled MUI components read. They fall back to emotion's defaultcsscache instead of themuione, and every server-rendered class name mismatched at hydration (css-zf0iqhserver againstmui-zf0iqhclient), 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.jsand no emotion external at all. SobundlePagesRouterDependencies: truerestores exactly what webpack was already doing.Comparing the
data-emotionkeys in the SSR HTML:mui,mui-globalcss×35,css-global×2mui,mui-globalAfter the fix the hydrated DOM under Turbopack matches the webpack production baseline exactly — 259
mui-and 60css-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-nextjsTranspiling 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-stateand@squonk/mui-themeall sit on the far side of it as well. A page rendering a@mui/labTimelinestill emittedcss-i6popu-MuiTimeline-rootfrom the server with only@mui/material-nextjstranspiled; underbundlePagesRouterDependenciesit emitsmui-i6popu-MuiTimeline-rootand hydrates clean. Since@mui/laband@rjsf/muiare 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:
emotion-react.cjs.mjs.compiler: { emotion: true }reported as having no effect.Each of these was tested and each left the mismatch in place:
bundlePagesRouterDependencies: truetranspilePackages: ["@mui/material-nextjs"]@mui/laband friends still mismatch@mui/material-nextjs9.0.1 → 9.4.0serverExternalPackagesfor@emotion/{react,cache,styled}turbopack.resolveAlias@emotion/react→dist/emotion-react.cjs.mjsjsx-runtimeandjsx-dev-runtimesubpathsThe 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-nextjsversion 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 lintandpnpm test(808 passed) all pass on Turbopack with dependency bundling on. A productionnext startwas checked by hand as well: SSR emitsmuikeys, the sketcher renders, and neither the home page, a@mui/labpage nor the sketcher page reports a hydration mismatch.Note for #2015
That branch restores the
jsdomalias andsrc/stubs/jsdom-utils.stub.js. Neither is needed once this lands, and both should come out of it.Closes #2020
🤖 Generated with Claude Code