Skip to content

fix(pages): preserve CJS globals in bundled dependencies - #2826

Draft
james-elicx wants to merge 2 commits into
mainfrom
codex/fix-pages-bundled-cjs-globals
Draft

fix(pages): preserve CJS globals in bundled dependencies#2826
james-elicx wants to merge 2 commits into
mainfrom
codex/fix-pages-bundled-cjs-globals

Conversation

@james-elicx

@james-elicx james-elicx commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

  • consolidate project-source, bundled-dependency, and emitted-chunk __dirname / __filename handling into the existing module-identity capability
  • retain only a thin optimizer adapter because Vite dependency optimization is an independent Rolldown pipeline
  • select identity by actual runtime: source identity in Node development, relocatable emitted-chunk identity in Node/Nitro production, and Next Edge-compatible logical identity in Workers
  • support plain Pages and hybrid App+Pages applications without project-shape gates
  • keep the hot path filtered and bounded: native id/code filters, cheap early exits, one CommonJS plugin hook, bounded transform/package caches, and watch invalidation

Regression coverage

  • real Pages route importing a CommonJS dependency that reads both globals
  • plain Pages development and relocated Node production
  • hybrid App+Pages development and production
  • real nitro/vite build executed through .output/server/index.mjs
  • real Cloudflare build executed under Wrangler/workerd
  • no build-host dependency or project paths leak into deployable Worker/Nitro bundles
  • CommonJS-to-ESM package-scope changes and symlink retargets invalidate cached classification
  • client and ESM dependencies remain untouched

Upstream behaviour

Next.js bundles dependencies per module layer; the presence of an App Router does not disable Pages dependency bundling. Node-target webpack exposes emitted-chunk identity. Next's Edge webpack/Rspack configuration uses logical /index.js and / values because no filesystem module identity exists.

Relevant Next.js sources:

  • packages/next/src/build/handle-externals.ts
  • packages/next/src/build/webpack/config/blocks/base.ts
  • packages/next/src/build/webpack-config.ts
  • test/e2e/externals-pages-bundle/externals-pages-bundle.test.ts

Validation

  • vp test run tests/import-meta-url.test.ts tests/build-optimization.test.ts — 203 passed, 2 skipped
  • focused plain Pages dev + relocated production — 3 passed
  • focused hybrid App+Pages dev and production — passed
  • vp test run tests/cjs-globals-runtime.test.ts — real Nitro and workerd runtimes passed
  • exact combined batch — 438/438 passed in 38.56s
  • touched-file vp check and git diff --check — clean
  • independent cumulative-diff review — clean after cache invalidation, runtime isolation, and Worker project-source findings were addressed

@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2826
npm i https://pkg.pr.new/create-vinext-app@2826
npm i https://pkg.pr.new/@vinext/types@2826
npm i https://pkg.pr.new/vinext@2826

commit: f12d38d

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared f12d38d against base a01075a using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 134.7 KB 134.7 KB ⚫ -0.0%
Client entry size (gzip) vinext 122.2 KB 122.2 KB ⚫ -0.0%
Dev server cold start vinext 2.78 s 2.74 s ⚫ -1.4%
Production build time vinext 2.95 s 2.97 s ⚫ +0.6%
RSC entry closure size (gzip) vinext 113.8 KB 113.8 KB ⚫ +0.0%
Server bundle size (gzip) vinext 191.4 KB 191.4 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx
james-elicx force-pushed the codex/fix-pages-bundled-cjs-globals branch 2 times, most recently from 0a4cfb8 to cb39205 Compare August 6, 2026 10:38
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@ask-bonk

ask-bonk Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review — PR #2826 (fix(pages): preserve CJS globals in bundled dependencies)

Reviewed the full diff, ran the touched tests, and traced the dev vs build code paths. This looks solid and well-scoped. Summary and a few observations below.

What I verified

  • tests/import-meta-url.test.ts — 50 passed
  • tests/pages-router.test.ts (bundled-CJS + relocated-prod) — passed; the relocated build asserts import.meta.dirname in the server chunk and that the runtime path resolves relative to the copied output (.not.toContain("/node_modules/")), which is the key relocatability guarantee.
  • tests/build-optimization.test.ts — 149 passed
  • vp check on import-meta-url.ts + index.ts — clean

Design is correct and consistent

  • Dev/serve path bakes canonical absolute __filename/__dirname at transform time (rewriteBundledDependencyCjsGlobals) — fine for a running dev server.
  • Build path deliberately does not bake absolute paths: the serve plugin is gated enabled: () => isServeCommand, so in build only renderChunk injects var __dirname = import.meta.dirname, keeping the emitted chunk relocatable. This matches Next's webpack Pages behavior (chunk-relative), and the relocated-prod test proves it.
  • The commonjs filter change is correct against vite-plugin-commonjs 0.10.4 semantics (false = skip, true = force even for node_modules, undefined = default). Returning true only for isBundledCommonJsDependencyId + isCommonjs(code) is appropriately narrow.
  • analyzeServerCjsGlobals guards against a duplicate top-level var __dirname collision in a mixed chunk via moduleBindings, so the renderChunk injection won't emit a redeclaration.
  • Cloudflare/Nitro/App-Router/client are all gated off. This is a deliberate, documented scope (__dirname in a Worker has no meaningful filesystem value), consistent with the PR description.

Minor observations (non-blocking)

  1. E2E host inconsistency. tests/e2e/pages-router-prod/cjs-dependency-globals.browser.spec.ts uses http://127.0.0.1:4175, while every sibling spec in that dir uses http://localhost:4175. Functionally equivalent, but worth aligning for consistency (the PAGES_ROUTER_PROD_BASE_URL env fallback is otherwise unused by the other specs).

  2. mergeOptimizeDepsExclude broadening is scope-adjacent. The SSR exclude grew from ["ipaddr.js"] to include react, react-dom, react-dom/server, and all nextShimMap keys. This is coherent with the "don't enable env-wide discovery" goal and is asserted by the new test, but it's a broader optimizer behavior change than the CJS-globals fix strictly needs. Fine as-is; just flagging that it's the highest-surface part of the diff.

  3. isCommonJsDependency does sync fs package.json walks per matching dependency module. Gated behind the native code: /__filename|__dirname/ + node_modules id filter and Vite's per-module transform caching, so this only fires for the rare dep that actually reads these globals — acceptable, no change needed.

No correctness issues found

Dev/prod parity holds (build path goes through renderChunk for all Pages server builds via buildApp), the relocatability contract is tested, and the fix matches Next.js Pages-bundle behavior. LGTM.

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2826.

Summary

I reviewed PR #2826 (fix(pages): preserve CJS globals in bundled dependencies) — traced the dev vs. build code paths and ran the touched test suites (all green: import-meta-url.test.ts 50 passed, the new pages-router bundled-CJS + relocated-prod tests passed, build-optimization.test.ts 149 passed, vp check clean).

Verdict: LGTM — no correctness issues found.

Key findings:

  • The core design is correct: dev/serve bakes canonical absolute paths (fine for a running server), while build deliberately keeps chunks relocatable by injecting import.meta.dirname via renderChunk instead of baking absolute paths. The relocated-prod test proves the runtime path resolves relative to the copied output and rejects /node_modules/.
  • The commonjs filter change matches vite-plugin-commonjs semantics and is narrowly scoped.
  • Duplicate var __dirname collisions in mixed chunks are guarded by moduleBindings.
  • Cloudflare/Nitro/App-Router/client gating is a deliberate, documented scope.

Three non-blocking observations flagged: an E2E host inconsistency (127.0.0.1 vs localhost in sibling specs), the mergeOptimizeDepsExclude broadening being the highest-surface part of the diff, and the sync fs package.json walk (acceptable given the native filter + Vite transform caching).

github run

@james-elicx
james-elicx force-pushed the codex/fix-pages-bundled-cjs-globals branch from cb39205 to d1bab88 Compare August 7, 2026 11:12
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

1 similar comment
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review — PR #2826 (fix(pages): preserve CJS globals in bundled dependencies)

Traced the dev/build/optimizer code paths, verified the vite-plugin-commonjs internals the wrapper depends on, and ran the touched suites. Verdict: LGTM — no correctness issues found. A couple of minor/latent-fragility notes below, none blocking.

What I verified

  • tests/import-meta-url.test.ts — 55 passed
  • tests/build-optimization.test.ts — 149 passed (2 skipped)
  • tests/pages-router.test.ts -t "bundled" — 3 passed (real dev route + relocated-prod build asserting import.meta.dirname in the server chunk and .not.toContain("/node_modules/"))
  • vp check on import-meta-url.ts + index.ts — clean

Design is correct

  • The sync-filter bridge is sound. I confirmed against vite-plugin-commonjs@0.10.4 that transform returns transformCommonjs(...), whose first await (dynaimcRequire.generateRuntime) comes after the options.filter?.call(id) invocation. So transformBundledCommonJsDependencies is guaranteed to still be set when the filter reads it, and the finally reset after the sync prefix is safe. The "do not await here" comment is accurate.
  • Dev vs build split is right. Dev node_modules transform bakes source-path (or Edge) globals; in build the node_modules branch bails on mode !== "dev" and renderChunk injects chunk-relative import.meta.dirname (Node/Nitro) or /index.js/// (Worker). The relocated-prod and real Nitro/workerd tests prove each variant.
  • Idempotency / no double-injection. analyzeServerCjsGlobals records module-scope bindings and skips injection when __dirname/__filename is already bound, so a prebundled dep already rewritten by the optimizer plugin, or a chunk where rolldown's CJS interop declared the global, won't get a redeclaration. Function-param __dirname in a CJS wrapper is correctly not treated as a module binding (a harmless unused top-level var results, shadowed inside the wrapper).
  • Runtime selection. getServerRuntime reads hasCloudflarePlugin lazily; it's set in the config hook, which runs before configEnvironment (optimizer registration) and well before any transform/renderChunk, so Worker→Edge vs Node→chunk identity is always resolved by the time it's read.
  • Optimizer exclude broadening is coherent. react/react-dom/react-dom/server are already in this Pages SSR env's resolve.external, so excluding them from the dep optimizer matches the existing SSR_EXTERNAL_REACT_ENTRIES rationale (avoid duplicate React records → React.H null). Adding Object.keys(nextShimMap) is consistent with those being alias-resolved.

Minor / non-blocking

  1. E2E host inconsistency. tests/e2e/pages-router-prod/cjs-dependency-globals.browser.spec.ts uses http://127.0.0.1:4175 while every sibling spec (production.spec.ts, interactive.spec.ts, gssp-data-dedup.spec.ts, javascript-urls.spec.ts) uses http://localhost:4175. Functionally equivalent; worth aligning. The PAGES_ROUTER_PROD_BASE_URL env fallback is otherwise unused by siblings.

  2. commonJsPlugin.transform reassignment assumes the function form. The wrapper is guarded by typeof commonJsTransform === "function". vite-plugin-commonjs 0.10.4 exposes transform as a plain function, so this works today, but if it ever migrates to the Vite 8 object form ({ filter, handler }) the wrapper silently no-ops and environment-awareness is lost (bundled deps would stop being transformed in dev). A latent fragility, not a bug — consider a comment or a defensive assertion pinning the expected shape.

  3. Test fixture couples to a Next.js internal. The fixtures import next/dist/compiled/regenerator-runtime/path, which happens to read __dirname. Convenient and real, but tied to Next's compiled layout; if Next drops/relocates that file the tests break for an unrelated reason. Acceptable given it's a genuine bundled-CJS-reads-__dirname case, just flagging the coupling.

No correctness issues found

Dev/prod parity holds, the relocatability contract is tested against real Nitro and workerd runtimes, and the Node/Edge identity split matches Next.js Pages-bundle behavior. LGTM.

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

To https://github.com/cloudflare/vinext.git
! [rejected] codex/fix-pages-bundled-cjs-globals -> codex/fix-pages-bundled-cjs-globals (fetch first)
error: failed to push some refs to 'https://github.com/cloudflare/vinext.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

github run

@james-elicx
james-elicx force-pushed the codex/fix-pages-bundled-cjs-globals branch from b17f08c to 0338ffe Compare August 7, 2026 11:33
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review — PR #2826 (fix(pages): preserve CJS globals in bundled dependencies)

Traced the dev/build/optimizer code paths, verified the vite-plugin-commonjs@0.10.4 internals the wrapper depends on, checked runtime selection timing, and ran the touched suites.

Verdict: LGTM — no blocking correctness issues found. A few observations below, none blocking.

What I verified

  • tests/import-meta-url.test.ts — 55 passed
  • tests/pages-router.test.ts -t "bundled" — 3 passed (real dev route + relocated-prod build asserting import.meta.dirname in the server chunk and .not.toContain("/node_modules/"))
  • vp check on import-meta-url.ts + index.ts — clean

Design is correct

  • The sync-filter bridge is sound. Confirmed against vite-plugin-commonjs@0.10.4: transform returns transformCommonjs(...), which runs synchronously through options.filter?.call(id) (index.mjs L408) before the first await (dynaimcRequire.generateRuntime, L422). The wrapper's try { return transform(...) } finally { reset } resets the flag when the promise is returned (synchronously), which is after the filter has already been consulted. So transformBundledCommonJsDependencies is guaranteed set when the filter reads it, and concurrent transforms cannot cross-talk. The "do not await here" comment is accurate.
  • vite-plugin-commonjs's own optimizer path doesn't apply under Vite 8. It pushes an optimizeDeps.esbuildOptions pre-bundle plugin (index.mjs L356-358), but this repo resolves vite to Vite 8 / Rolldown (optimizeDeps.rolldownOptions), so that esbuild hook is inert. Registering the separate optimizeDepsPlugin via rolldownOptions.plugins in configEnvironment is the right way to cover the Rolldown pre-bundle pipeline.
  • Runtime selection timing is safe. getServerRuntime reads hasCloudflarePlugin, which is assigned inside the config hook body (index.ts L2650) — this completes before configEnvironment (optimizer registration) and well before any transform/renderChunk. So Worker→Edge vs Node→chunk identity is always resolved by read time.
  • Idempotency / no double-injection. analyzeServerCjsGlobals records module-scope bindings and skips injection when __dirname/__filename is already bound. This correctly prevents renderChunk from redeclaring a global already emitted by the optimizer rewrite or by rolldown's CJS interop (verified by the not.toContain("var __dirname = void 0") assertion in cjs-globals-runtime.test.ts).
  • Node/Nitro/Worker runtime split matches Next.js. Chunk-relative import.meta.dirname for Node/Nitro, Edge-logical /index.js/// for Workers — each proved against real Nitro and workerd runtimes in cjs-globals-runtime.test.ts.

Observations (non-blocking)

  1. Project-source __dirname is not relocatable in Node/Nitro production — pre-existing, not introduced here. For project source (non-node_modules), the build transform bakes the absolute build-host source path (cjs-globals-runtime.test.ts L269-273 / L415-416 assert project-runtime-path = canonicalRoot/pages/..., i.e. the original source root, even in relocated Nitro output). Only bundled-dependency __dirname gets the relocatable chunk-relative treatment via renderChunk. This asymmetry is confirmed to exist on main already (no renderChunk there; project source was baked in both dev and build), so this PR does not regress it — but the PR body's "relocatable emitted-chunk identity in Node/Nitro production" claim only holds for dependencies, not project source. Worth a follow-up: Next's Node-target webpack makes project-source __dirname the emitted-chunk directory too, so this is a latent parity gap.

  2. Chunk-wide binding guard can under-inject in a rare mixed chunk. analyzeServerCjsGlobals runs over the whole emitted chunk. If module A in a chunk already has a top-level var __dirname (e.g. optimizer-injected) and module B in the same chunk reads __dirname freely, the chunk-wide moduleBindings guard skips renderChunk injection entirely, and B's read resolves to A's chunk-scoped value. Requires two bundled CJS deps reading __dirname in one chunk with divergent expected values — very rare, and no test exercises it, but it's a real edge of the chunk-level (vs module-level) analysis.

  3. commonJsPlugin.transform reassignment assumes the function form. Guarded by typeof commonJsTransform === "function". Works today (0.10.4 exposes a plain function), but if it ever migrates to the Vite 8 object form ({ filter, handler }) the wrapper silently no-ops and bundled deps stop being environment-scoped in dev. Consider a defensive assertion or comment pinning the expected shape.

  4. E2E host inconsistency. tests/e2e/pages-router-prod/cjs-dependency-globals.browser.spec.ts uses http://127.0.0.1:4175 while every sibling spec (production.spec.ts, interactive.spec.ts, gssp-data-dedup.spec.ts, javascript-urls.spec.ts) uses http://localhost:4175. Functionally equivalent; worth aligning. The PAGES_ROUTER_PROD_BASE_URL env fallback is otherwise unused by siblings.

  5. mergeOptimizeDepsExclude broadening is the highest-surface change. SSR exclude grew from ["ipaddr.js"] to add react, react-dom, react-dom/server, and all nextShimMap keys. Coherent — those React entries already sit in this env's resolve.external, and nextShimMap keys are alias-resolved — but it's a broader optimizer behavior change than the CJS-globals fix strictly needs. Fine as-is; flagging as the biggest behavioral surface.

Bottom line

Dev/prod parity holds for dependencies, the relocatability contract is tested against real Nitro and workerd runtimes, and the Node/Edge identity split matches Next.js Pages-bundle behavior. The concurrency reasoning in the commonjs wrapper is correct against the pinned plugin version. Non-blocking notes above.

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2826.

Summary

I reviewed PR #2826 (fix(pages): preserve CJS globals in bundled dependencies) by tracing the dev/build/optimizer code paths, verifying the vite-plugin-commonjs@0.10.4 internals the wrapper depends on, and running the touched suites (import-meta-url.test.ts 55 passed, pages-router.test.ts -t "bundled" 3 passed, vp check clean).

Verdict: LGTM — no blocking correctness issues found.

Key things I confirmed as correct:

  • The sync-filter concurrency bridge is sound. Verified against the pinned plugin source that options.filter (index.mjs L408) is invoked synchronously before the first await (L422), so the try/finally flag reset (which fires when the promise is returned, not resolved) cannot race concurrent transforms. The "do not await here" comment is accurate.
  • vite-plugin-commonjs's own optimizer hook is inert under Vite 8 (it uses esbuildOptions, repo uses Rolldown), justifying the separate optimizeDepsPlugin registration.
  • Runtime selection timing is safehasCloudflarePlugin is set in the config hook, before configEnvironment and any transform/renderChunk.
  • No double-injection — the moduleBindings guard prevents redeclaration.
  • Node/Nitro/Worker split matches Next.js and is proven against real Nitro + workerd runtimes.

Five non-blocking observations flagged:

  1. Project-source __dirname bakes the absolute build-host path in Node/Nitro production (a pre-existing parity gap, not introduced by this PR, but the PR body's "relocatable in production" claim only holds for dependencies).
  2. A rare mixed-chunk under-injection edge in the chunk-wide binding analysis.
  3. The commonJsPlugin.transform reassignment assumes the function form (latent fragility if the plugin migrates to Vite 8 object-hook form).
  4. E2E host inconsistency (127.0.0.1 vs sibling localhost).
  5. The mergeOptimizeDepsExclude broadening being the highest-surface change (coherent but larger than the fix strictly needs).

github run

ask-bonk Bot added a commit that referenced this pull request Aug 7, 2026
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
@james-elicx
james-elicx force-pushed the codex/fix-pages-bundled-cjs-globals branch from 841c9a1 to 0338ffe Compare August 7, 2026 11:40
@james-elicx
james-elicx force-pushed the codex/fix-pages-bundled-cjs-globals branch from 0338ffe to f12d38d Compare August 7, 2026 21:08
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review — PR #2826 (fix(pages): preserve CJS globals in bundled dependencies)

Independent trace of the dev/build/optimizer paths, verified vite-plugin-commonjs@0.10.4 internals against the pinned source, checked runtime-selection timing, and ran the touched suites (tests/import-meta-url.test.ts 55 passed; tests/pages-router.test.ts -t "bundled" 3 passed).

Verdict: LGTM — no blocking correctness issues found. The design is sound and the runtime coverage (real Nitro + workerd + relocated Node prod) is exactly what this class of fix needs. Findings below are non-blocking; several corroborate the earlier reviews.

Verified correct

  • Sync-filter concurrency bridge is safe. Confirmed against dist/index.mjs: transform(code, id) returns transformCommonjs(...), which invokes options.filter?.call(options, id) (L408) synchronously before the first await (generateRuntime, L422). The wrapper's try { return … } finally { reset } resets the flag when the promise is returned (synchronous, no await in the wrapper), so the flag is guaranteed live when the filter reads it and concurrent transforms cannot cross-talk. The "do not await here" comment is accurate. The early !isCommonjs(code) bail (L407) runs before the filter, so non-CJS inputs never consult it — fine.
  • Optimizer split is justified. commonjs() pushes an optimizeDeps.esbuildOptions prebundle plugin; this repo is Vite 8 / Rolldown (rolldownOptions), so that hook is inert. Registering optimizeDepsPlugin via rolldownOptions.plugins in configEnvironment is the correct way to cover the Rolldown prebundle.
  • Runtime-selection timing. getServerRuntime reads hasCloudflarePlugin, set in the config hook (index.ts:2650), which completes before configEnvironment and any transform/renderChunk. Worker→Edge vs Node→chunk identity is always resolved by read time.
  • Idempotency. analyzeServerCjsGlobals skips injection when a global is already module-bound, preventing renderChunk from redeclaring a global emitted by the optimizer rewrite or rolldown's CJS interop (asserted by not.toContain("var __dirname = void 0")).
  • Node/Nitro/Worker split matches Next.js and is proven end-to-end against real runtimes.

Non-blocking

  1. Chunk-wide binding guard can under-inject in a rare mixed chunk. analyzeServerCjsGlobals runs over the whole emitted chunk. If module A already has a top-level var __dirname and module B in the same chunk reads a free __dirname, the chunk-wide moduleBindings guard suppresses renderChunk injection and B resolves to A's value. Requires two bundled CJS deps reading __dirname in one chunk — very rare, no test exercises it, but it's a real edge of chunk-level (vs module-level) analysis. Consider a follow-up test if this becomes a concern.

  2. commonJsPlugin.transform reassignment assumes the function form. Guarded by typeof commonJsTransform === "function". Works on 0.10.4 (plain function), but if the plugin ever migrates to the Vite 8 object hook form ({ filter, handler }) the wrapper silently no-ops and bundled deps stop being environment-scoped in dev — no error, just a behavioral regression. Worth a defensive assertion or a comment pinning the expected shape so a dependency bump surfaces it loudly.

  3. Project-source __dirname is not relocatable in Node/Nitro production. Only bundled dependency __dirname gets the chunk-relative import.meta.dirname treatment via renderChunk; project source still bakes the absolute build-host path (cjs-globals-runtime.test.ts:262-273 assert project-runtime-path = the original source root even in relocated Nitro output). This is pre-existing on main (no renderChunk there), so the PR does not regress it, but the PR body's "relocatable emitted-chunk identity in Node/Nitro production" claim holds only for dependencies, not project source. Next's Node-target webpack makes project-source __dirname chunk-relative too — a latent parity gap worth a follow-up.

  4. E2E host inconsistency. tests/e2e/pages-router-prod/cjs-dependency-globals.browser.spec.ts:3 uses http://127.0.0.1:4175 while every sibling (production.spec.ts, interactive.spec.ts, gssp-data-dedup.spec.ts, javascript-urls.spec.ts) uses http://localhost:4175. Functionally equivalent; align for consistency. The PAGES_ROUTER_PROD_BASE_URL fallback is otherwise unused by siblings.

  5. mergeOptimizeDepsExclude broadening is the highest-surface change. The SSR exclude grew from ["ipaddr.js"] to add react, react-dom, react-dom/server, and all nextShimMap keys. Coherent — those React entries already sit in this env's resolve.external, and nextShimMap keys are alias-resolved — and the new tests/pages-router.test.ts test locks the contract. Fine as-is; flagging as the biggest behavioral surface beyond the strict CJS-globals fix.

Bottom line

Dev/prod parity holds for dependencies, the relocatability contract is tested against real Nitro and workerd, and the Node/Edge identity split matches Next.js Pages-bundle behavior. The concurrency reasoning in the commonjs wrapper is correct against the pinned plugin version. Ship it; the two items I'd most want addressed before/after merge are #2 (defensive shape assertion) and #4 (host alignment).

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2826.

Summary

I independently reviewed PR #2826 (fix(pages): preserve CJS globals in bundled dependencies) by tracing the dev/build/optimizer code paths, verifying vite-plugin-commonjs@0.10.4 internals against the pinned source in node_modules, checking runtime-selection timing in index.ts, and running the touched suites (import-meta-url.test.ts 55 passed, pages-router.test.ts -t "bundled" 3 passed).

Verdict: LGTM — no blocking correctness issues found.

Things I verified as correct:

  • The sync-filter concurrency bridge is sound. Confirmed in dist/index.mjs that options.filter is called synchronously (L408) before the first await (L422), so the wrapper's try/finally flag reset cannot race concurrent transforms.
  • Optimizer split is justified — the plugin's own esbuild optimizeDeps hook is inert under Vite 8/Rolldown, so the separate optimizeDepsPlugin registration is the right coverage.
  • Runtime-selection timing is safehasCloudflarePlugin is set in config before configEnvironment and any transform/renderChunk.
  • Idempotency guard prevents double-injection of __dirname/__filename.
  • Node/Nitro/Worker identity split is proven against real Nitro + workerd + relocated Node prod runtimes.

Five non-blocking findings flagged:

  1. Rare mixed-chunk under-injection edge in the chunk-wide binding analysis.
  2. commonJsPlugin.transform reassignment assumes the function form — latent fragility if the plugin migrates to the Vite 8 object hook shape (silent no-op). Recommended a defensive assertion.
  3. Project-source __dirname isn't relocatable in Node/Nitro prod (pre-existing, not a regression, but the PR body's claim only holds for dependencies).
  4. E2E host inconsistency (127.0.0.1 vs sibling localhost).
  5. mergeOptimizeDepsExclude broadening is the highest-surface change (coherent, test-locked).

My findings corroborate the earlier reviews. The two items I'd most want addressed are #2 and #4.

github run

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