fix: patch paper so bundlers stop resolving its undeclared jsdom require - #2024
Merged
Conversation
`next dev` writes this block into AGENTS.md on every run (node_modules/next/dist/server/lib/generate-agent-files.js), so leaving it out of the tree only re-creates the same uncommitted change each time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file has been in the tree since 795e23c but has never been wired to anything: there is no `patchedDependencies` entry for it and this repository does not use patch-package, so it has not been applied. It is also no longer needed. It added a `types` condition to ketcher-standalone's `exports["."]` map, and ketcher-standalone@3.12.0 — the version installed here — already ships that condition upstream. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
paper's published manifest declares no dependencies, optionalDependencies or peerDependencies at all — jsdom and canvas are devDependencies, which consumers never install. `dist/node/canvas.js` requires both anyway. The canvas require sits in a try/catch; the jsdom one does not, so bundlers resolve `jsdom/lib/jsdom/living/generated/utils` statically and nothing in this tree provides it. pnpm is behaving correctly. Flat hoisting would let any unrelated jsdom in the tree silently satisfy the unqualified require; pnpm's isolated node_modules cannot, so the upstream bug is unmaskable. The bug is latent under the current webpack build, because the sketcher never reaches the server compilation. It becomes fatal under Turbopack, which keeps it in the SSR graph — every failing import trace is an SSR one, never the browser graph. Landing the patch first means the Turbopack migration (#2015) never has to introduce the `turbopack.resolveAlias` workaround it currently carries. Patch the require with a `webpackIgnore: true` magic comment, which both webpack and Turbopack honour on `require()` expressions, so the specifier is left for Node to resolve at runtime instead of being bundled. Runtime behaviour is unchanged: the line is only reached once `require('canvas')` has succeeded, and installing canvas is what brings jsdom into the tree. Scoping is what makes this better than the alias. `resolveAlias` redirects the specifier for every importer in the graph, so a genuine jsdom consumer would silently receive a no-op stub. The patch touches only paper. Also delete `src/stubs/jsdom-utils.stub.js`, which the alias used to point at. It has been dead code here since 84a9309 removed the alias that referenced it, and the patch means nothing needs it again. Considered and rejected: declaring jsdom as a devDependency, and repairing paper's manifest via pnpm.packageExtensions. Both work, and both install a large tree and pin it to <=27 — the target is a jsdom internal, removed in 28 with no semver obligation — to satisfy a require that never executes. If a ketcher bump moves paper off 0.12.18, pnpm fails the install with ERR_PNPM_UNUSED_PATCH rather than silently reverting to a broken build. Worth fixing upstream regardless: paper should declare jsdom as an optionalDependency next to canvas, or not require it statically. Closes #2020 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OliverDudgeon
force-pushed
the
fix/2020-paper-patch
branch
from
August 27, 2026 21:42
20e61df to
35f7e84
Compare
OliverDudgeon
changed the base branch from
t3code/enable-react-compiler-eslint
to
dev
August 27, 2026 21:42
OliverDudgeon
marked this pull request as ready for review
August 27, 2026 21:44
OliverDudgeon
added a commit
that referenced
this pull request
Aug 28, 2026
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
added a commit
that referenced
this pull request
Aug 28, 2026
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>
im-semantic-release Bot
pushed a commit
that referenced
this pull request
Aug 28, 2026
# [7.0.0-dev.19](7.0.0-dev.18...7.0.0-dev.19) (2026-08-28) ### Bug Fixes * build and serve the app with Turbopack ([ae2539d](ae2539d)), closes [#2024](#2024) [vercel/next.js#82607](vercel/next.js#82607) [vercel/next.js#91411](vercel/next.js#91411) [#2020](#2020)
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.
Closes #2020. Targets
devand lands before #2015 — the React Compiler / Turbopack migration should be rebased on top of this once it merges.Why this goes first
#2015 currently has to carry a
turbopack.resolveAliasworkaround pointing a real module specifier at a hand-written no-op stub, and #2020 records that as unacceptable as a permanent state. Landing the real fix first means #2015 never has to introduce the workaround at all — it just drops those lines from its own diff.Root cause
paperrequires a package it does not depend on. Its published manifest declares nodependencies,optionalDependenciesorpeerDependenciesat all —jsdomandcanvasare devDependencies, which consumers never install.dist/node/canvas.jsrequires both anyway; thecanvasrequire sits in atry/catch, the jsdom one does not, so bundlers resolvejsdom/lib/jsdom/living/generated/utilsstatically and nothing in this tree provides it.ketcher-coredepends onpaper, which is how it reaches us. The code never runs — it is guarded oncanvas, which is not installed — but the specifier is still resolved at build time.pnpm is behaving correctly. Flat hoisting would let an unrelated jsdom anywhere in the tree silently satisfy the unqualified require; pnpm's isolated
node_modulescannot, so the upstream bug is unmaskable.On
devtoday the bug is latent, because webpack never pulls the sketcher into the server compilation. It becomes fatal under Turbopack, which keeps it in the SSR graph — every failing import trace is an SSR one, never the browser graph.The fix
patches/paper@0.12.18.patch, one changed line:webpackIgnoreis honoured by both webpack and Turbopack onrequire()expressions (Turbopack API reference), so the specifier is left for Node to resolve at runtime rather than bundled. Runtime behaviour is unchanged: the line is only reached oncerequire('canvas')has succeeded, and installingcanvasis what brings jsdom into the tree.Scoping is what makes this better than the alias #2015 carries.
resolveAliasredirects the specifier for every importer in the graph, so a genuine jsdom consumer would silently receive a no-op stub. The patch touches only paper.If a ketcher bump moves paper off
0.12.18, pnpm fails the install withERR_PNPM_UNUSED_PATCHrather than silently reverting to a broken build. CI installs withpnpm ci(clean+install --frozen-lockfile), so that guard is genuinely exercised.Considered and rejected
'use client'marks a boundary; it does not remove a module from the server graph.jsdomas a devDependency, or repair paper's manifest viapnpm.packageExtensions— both work, and both install a large tree and pin it to<=27(the target is a jsdom internal, dropped in 28 with no semver obligation) to satisfy a require that never executes.Also here
src/stubs/jsdom-utils.stub.jsdeleted. It is already dead code ondev: 84a9309 removed the alias that referenced it, and nothing has pointed at it since. The patch means nothing needs it again.patches/ketcher-standalone.patchremoved. In the tree since 795e23c but wired to nothing — nopatchedDependenciesentry, no patch-package — and obsolete anyway:ketcher-standalone@3.12.0already ships thetypesexport condition it added.nextjs-agent-rulesblock inAGENTS.md, whichnext devrewrites on every run, committed separately so the tree stops showing it as a pending change.Verification
Verified twice — once on the Turbopack base this was originally written against, and again after rebasing onto
dev, where the build is still webpack:pnpm buildpasses with the patch and no alias, emitting no bundler warning about the require.dev, this branch):pnpm buildpasses with no jsdom/paper warnings and nocritical dependencywarning.pnpm tscandpnpm lintclean on both.pnpm test808 passed on both bases.pnpm install --frozen-lockfileclean, and the file in thepatch_hash=…store directory carries the patched line.COPY patches ./patches/alongsidepnpm-workspace.yaml, so the image build picks the patch up.Follow-up, not in this PR
Sketcher.tsx/SMILESInput.tsxhave no story and no acceptance journey, so nothing would catch a runtime regression in the one component this change concerns. Tracked as the main pre-merge question in Complete the Turbopack migration and remove the paper.js/jsdom resolveAlias workaround #2020.ketcher-standalone.patchsat dead in the tree for months and nothing reported it. A contract test asserting every file inpatches/has apatchedDependenciesentry would prevent a repeat.docs/testing.mdis stale — two references to "the webpack production build" that feat: compile the application with the React Compiler #2015 makes false.jsdomas anoptionalDependencynext tocanvas, or not require it statically. Reporting that is what gives this patch an exit route.🤖 Generated with Claude Code