Skip to content

fix(pages): preserve module identity with deployment ids - #2012

Merged
james-elicx merged 1 commit into
mainfrom
codex/fix-middleware-client-transition-final
Jun 14, 2026
Merged

fix(pages): preserve module identity with deployment ids#2012
james-elicx merged 1 commit into
mainfrom
codex/fix-middleware-client-transition-final

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • keep Pages Router JavaScript bootstrap, preload, page, and _app URLs on one native ESM identity when a deployment ID is configured
  • retain ?dpl= tagging for managed non-JavaScript assets such as CSS
  • fix the pinned Next.js middleware-general client transition where a second queried client-entry module created a second React root and restored the old page after the destination committed

Root cause

Instrumentation showed the destination /blog/[slug] component rendered and committed successfully on the original root. About 6 ms later, the Pages client entry evaluated again as the queried URL index-*.js?dpl=..., called hydrateRoot a second time on the same #__next container, overwrote window.__VINEXT_ROOT__, and rendered the original /[id] page.

The initial document bootstrap was unqueried while Pages page/module URLs were deployment-queried. Native ESM does not inherit query strings across relative imports, so queried and unqueried URLs formed duplicate module graphs. This applies PR #2005's existing module-identity rule to Pages Router asset tags.

Validation

  • pinned Next.js v16.2.6 middleware-general/test/index.test.ts, exact assertion only: with i18n PASS; without i18n PASS
  • pinned Next.js v16.2.6 middleware-general/test/node-runtime.test.ts, exact assertion only: with i18n PASS; without i18n PASS
  • vp test run tests/pages-asset-tags.test.ts tests/build-optimization.test.ts tests/asset-prefix.test.ts
  • vp test run tests/shims.test.ts -t "shares RouterContext across duplicated next/router module instances"
  • focused PR fix(router): restore pages scroll traversal state #1999 scroll traversal tests
  • vp run vinext#build
  • scoped vp check

@pkg-pr-new

pkg-pr-new Bot commented Jun 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2012
npm i https://pkg.pr.new/vinext@2012

commit: 3c4c452

@github-actions

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

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx marked this pull request as ready for review June 14, 2026 18:15

@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.

Review: fix(pages): preserve module identity with deployment ids

Verdict: LGTM. The fix is correct, well-scoped, and consistent with the existing codebase architecture. No blocking issues found.

What the change does

Stops appending ?dpl=<deploymentId> to Pages Router JavaScript URLs (bootstrap, modulepreload, page/_app import URLs) while retaining ?dpl= tagging for managed non-JS assets (CSS). This applies the same native-ESM module-identity rule already established in utils/built-asset-url.ts:24 (PR #2005) to Pages Router asset tags.

Verification I performed

  1. Root cause is real and the fix addresses it. shims/router.ts:1772 does await import(pageModuleUrl) on the value stored in __NEXT_DATA__.__vinext.pageModuleUrl. When that URL carried ?dpl=... but the unqueried bootstrap imported the same chunk relatively, native ESM produced two distinct module identities → a second hydrateRoot on #__next → the duplicate React root described in the PR body. Removing the query from JS URLs collapses them to one identity.

  2. Consistent with the established pattern. built-asset-url.ts:20-24 already does hostType === "js" ? url : appendDeploymentIdQuery(...) for the exact same architectural reason. This PR mirrors it (value.endsWith(".js") ? url : appendDeploymentIdQuery(...)), and the inline comment accurately explains the native-ESM rationale.

  3. No regression risk for other deployment-id paths. appendAssetDeploymentIdQuery (fonts/stylesheets in pages-page-response.ts, app-ssr-entry.ts, and the font Link header at pages-page-handler.ts:498) is only used for CSS/font preloads — never for import()ed JS modules — so the skew-protection query stays where it is safe. Data-fetch skew protection still rides the x-deployment-id header (shims/internal/pages-data-target.ts:133, shims/router.ts:1501), unaffected by this change.

  4. href() input is always a raw .js/.css filename (tf, entry) with no pre-existing query at the point value.endsWith(".js") runs, so the suffix check is reliable. The __VINEXT_CLIENT_ENTRY__ bootstrap also routes through href() and correctly stays unqueried, which is precisely what keeps the bootstrap and page modules on one graph.

  5. Deliberate, documented Next.js divergence. Upstream Next.js does tag JS script/preload URLs with ?dpl= (its webpack runtime rewrites chunk URLs so identities stay aligned). vinext cannot rely on that under native ESM, so this divergence is intentional — matching the precedent in PR #2005. This is the correct call for vinext's architecture.

  6. Tests pass locally:

    • vp test run tests/pages-asset-tags.test.ts → 34 passed
    • vp test run tests/build-optimization.test.ts tests/asset-prefix.test.ts → 156 passed, including the e2e deployment ID: keeps native ESM URLs on one module identity test which asserts no ?dpl= appears in any built .js or in served JS asset URLs.
    • vp check on both changed files → clean (format, lint, types).

Minor (non-blocking) nit

resolveClientModuleUrl's 5th parameter is now _deploymentId (unused), but both call sites in pages-page-handler.ts:463-476 still pass process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID. Keeping the param preserves the signature but the now-dead argument at the call sites is slightly misleading. Optionally drop the param and the two call-site arguments to make it obvious that JS module URLs are intentionally never deployment-tagged. Not required for merge.

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2012. Summary of my findings:

Verdict: LGTM — no blocking issues

The PR removes ?dpl= from Pages Router JavaScript URLs while keeping it on CSS, fixing the duplicate-React-root bug in the pinned Next.js middleware-general client transition.

Why it's correct:

  • Root cause confirmed: shims/router.ts:1772 does await import(pageModuleUrl). A queried JS URL gets a different native-ESM module identity than the unqueried bootstrap → duplicate module graph → second hydrateRoot. Removing the query collapses them to one identity.
  • Consistent with the codebase: mirrors the identical hostType === "js" ? url : append(...) rule already in utils/built-asset-url.ts:24 from PR fix(deploy): preserve app module identity with deployment ids #2005.
  • No collateral risk: appendAssetDeploymentIdQuery (fonts/CSS) and the x-deployment-id data-fetch header are untouched, so skew protection remains where it's safe.
  • Deliberate Next.js divergence: upstream tags JS via its webpack runtime; vinext can't under native ESM, so this is the right architectural call (matching PR fix(deploy): preserve app module identity with deployment ids #2005 precedent).

Verification done:

  • tests/pages-asset-tags.test.ts → 34 passed
  • tests/build-optimization.test.ts + tests/asset-prefix.test.ts → 156 passed (incl. the e2e "keeps native ESM URLs on one module identity" test)
  • vp check on both files → clean

One non-blocking nit raised: resolveClientModuleUrl's 5th param is now _deploymentId (unused), but both call sites in pages-page-handler.ts still pass the deployment ID. Optionally drop the param and the dead arguments for clarity.

github run

@james-elicx
james-elicx merged commit 4821d24 into main Jun 14, 2026
55 checks passed
@james-elicx
james-elicx deleted the codex/fix-middleware-client-transition-final branch June 14, 2026 18:45
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