fix(pages-router): collapse doubled basePath in client asset URLs#1730
Conversation
commit: |
A Pages Router app built with a `basePath` emits its page and `_app` chunk URLs with the basePath applied twice, e.g. `/docs/docs/_next/static/_slug_.js`. Those URLs 404, so the page never hydrates, `next.router` never initializes, and any client-side navigation (router.push to /404, /_error, etc.) silently does nothing. Vite+ bakes the configured `base` into emitted chunk fileNames on disk (`dist/client/docs/_next/static/...`) so the Cloudflare ASSETS binding can serve them directly. Vite then prepends `base` a second time when it writes `ssr-manifest.json`, producing `docs/docs/_next/static/...` entries. `augmentSsrManifestFromBundle` preserved those doubled entries verbatim (it only stripped a leading slash), while its own bundle backfill produced the correct single-prefix path. The merged manifest held both, and `resolveClientModuleUrl` returned the doubled one. The on-disk chunk fileName carries the basePath exactly once, so the public URL must too. Collapse an exact `<base>/<base>/` prefix when normalizing the Vite-sourced manifest entries so they match the single-base paths the backfill produces. A correctly single-prefixed entry is left untouched. Regression covered with a focused unit test on the manifest builder and verified end to end against the Next.js basepath/error-pages deploy suite (previously 2 failing client-navigation cases, now all green).
c35b200 to
aecfec4
Compare
|
/bigbonk review |
There was a problem hiding this comment.
Review: collapse doubled basePath in client asset URLs
Clean, well-scoped fix. The root-cause analysis is accurate and the fix is placed at the single boundary (augmentSsrManifestFromBundle) that owns the SSR manifest shape, so every downstream reader (resolveClientModuleUrl in pages-server-entry.ts:565, modulepreload tags, __vinext.pageModuleUrl, CSS links) is corrected at once.
Verified
- Bug reproduction is real.
resolveClientModuleUrlreturns the first.jsentry in the manifest file list (pages-server-entry.ts:568-573). The oldnormalizeManifestFileonly stripped a leading slash, so the Vite-doubleddocs/docs/_next/static/...entry was preserved verbatim and returned first → 404 → no hydration. The regression test would fail on the old implementation. - Dedup works. The collapsed ssr-manifest entry (
docs/_next/static/_slug_.js) and the bundle-backfill entry (manifestFileWithBase(chunk.fileName, base), which leaves the already-bakeddocs/prefix untouched permanifest-paths.ts:13) produce the same string, so theSet<string>dedups them. No leftover doubled entry. - App Router parity / no regression.
augmentSsrManifestFromBundleis shared (index.ts:4278), but collapse only triggers on an exact<base>/<base>/prefix, so it is a no-op for App Router and for builds without a basePath. The over-collapse guard test confirms this. - Dev/prod parity. The doubling is build-only (dev uses Vite's runtime module resolution, not this static manifest), so the build-time fix is the correct and complete location. No matching prod-server-only path was left unfixed.
vp checkpasses on both changed source files; theaugmentSsrManifestFromBundletest block passes (8 tests).
One minor note (non-blocking)
The collapse keys on an exact <base>/<base>/ prefix. There is a theoretical over-collapse if a real (post-base-strip) asset path itself begins with the base segment, e.g. base /a with a genuine asset at a/a/x.js. For Next.js this can't happen in practice since asset paths always start with _next/static/, and the doc comment already calls out the exact-prefix-only behavior, so this is fine as-is — just worth being aware of if the helper is ever reused for non-_next assets.
LGTM.
|
fatal: could not read Username for 'https://github.com': No such device or address |
|
@james-elicx Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
What this changes
Pages Router apps built with a
basePathnow emit their page and_appclient chunk URLs with the basePath applied exactly once
(
/docs/_next/static/_slug_.js) instead of twice(
/docs/docs/_next/static/_slug_.js).Why
With a
basePath(e.g./docs), the rendered HTML pointed every page and_appchunk at a doubled path:Those URLs 404, so the client bundle never loads, the page never hydrates,
next.routernever initializes, and any client-side navigation silently doesnothing.
<div id="__next">stays empty.This surfaced as two failing cases in the upstream Next.js suite
test/e2e/basepath/error-pages.test.ts:basePath › client-side navigation › should navigate to /404 correctly client-sidebasePath › client-side navigation › should navigate to /_error correctly client-sideBoth call
next.router.push("/404", "/slug-2")and time out waiting for the404 page, because the router that would handle the push never came online.
Approach
Root cause is a base-applied-twice interaction in the build:
baseinto emitted chunk fileNames on disk(
dist/client/docs/_next/static/...). vinext relies on that layout so theCloudflare ASSETS binding serves
/docs/_next/static/...directly.baseagain when it writesssr-manifest.json,producing
docs/docs/_next/static/...entries.augmentSsrManifestFromBundlepreserved those doubled entries verbatim (it only stripped a leading slash),
while its own bundle backfill keyed off
chunk.fileNameand produced thecorrect single-prefix path. The merged manifest held both, and
resolveClientModuleUrlreturned the first (doubled) entry.The on-disk chunk fileName carries the basePath exactly once, so the public URL
must too. New
collapseDuplicateBasehelper collapses an exact<base>/<base>/prefix when normalizing the Vite-sourced manifest entries, sothey line up with the single-base paths the backfill already produces. A
correctly single-prefixed entry is left untouched (no over-collapse).
The fix lives at the single boundary that owns the manifest shape, so every
downstream reader (modulepreload tags,
__vinext.pageModuleUrl, CSS links) iscorrected at once.
Non-goals
plugin's Vite runtime base resolution, which already emitted single-base URLs
even in the broken output.
Validation
augmentSsrManifestFromBundle:old implementation, passes with the fix)
pages-i18n-domains-basepathfixture and confirmed the emittedssr-manifest.jsonno longer contains anyapp/app/...entries.test/e2e/basepath/error-pages.test.tswent from 2 failed / 6 passed to8 passed / 1 skipped.
vp checkand the fulltests/build-optimization.test.ts+tests/deploy.test.tssuites pass.Risks / follow-ups
<base>/<base>/prefix, so it is ano-op for builds where the bundler does not bake
baseinto fileNames (thepaths are already single-prefixed). No behavior change for apps without a
basePath.