mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.8
103 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
85d49ec814 |
perf(dev): externalize SSR React runtime (#2191)
* perf(dev): externalize SSR React runtime * fix(test): pass Vite config environment |
||
|
|
680cbafabc | perf(mdx): gate the MDX transform handler behind a native id filter (#2200) | ||
|
|
b2ea860284 |
fix(proxy): match Next.js file conventions (#2155)
* fix(proxy): match Next.js file conventions * test(proxy): keep convention fixture out of workspace * test(proxy): provide middleware path in handler fixture |
||
|
|
7487886b38 |
fix(prod-server): send headers-only for HEAD in sendCompressed (#1980) (#2058)
sendCompressed wrote the full body for every request. HEAD requests routed through it (bot/crawler-buffered Pages HTML and Pages API routes) still compressed and emitted a payload that Node then discards at the socket level. Short-circuit with res.end() after writeHead when req.method is HEAD, mirroring sendWebResponse. This skips spinning up a compressor for a body that would be thrown away and keeps the buffered sender consistent with the streamed and static-file senders, which already handle HEAD. Co-authored-by: James <james@eli.cx> |
||
|
|
2eb394f0a4 |
perf(dev): skip typeof window folding for prebundles (#2118)
* perf(dev): skip typeof window folding for prebundles * perf(dev): move prebundle skip into hook filter * test: provide cacheDir in resolved config mock * fix(dev): handle reused typeof-window plugins * refactor(dev): share cache directory normalization --------- Co-authored-by: James <james@eli.cx> |
||
|
|
bacee9ef52 |
fix(pages): emit canonical __NEXT_DATA__ JSON (#2043)
* fix(pages): emit canonical next data script * test(pages): expect canonical next data markup * fix(pages): initialize canonical next data before routing * test(pages): parse canonical next data markup * fix(pages): preserve router bootstrap ordering * test(pages): expect readiness bootstrap import * fix(pages): guard readiness bootstrap without next data |
||
|
|
51d0208d3d |
fix(pages): re-enter filesystem routes after rewrites (#2041)
* fix(dev): defer rewrite conditions until after middleware * docs(dev): clarify rewritten static handling |
||
|
|
b58adb3cfa | fix(middleware): classify Pages data requests by URL (#2039) | ||
|
|
0fb9bd32bf |
fix(pages): apply Document renderPage enhancers (#2034)
* fix(pages): apply document renderPage enhancers * fix(pages): propagate document render errors * fix(pages): delegate document initial props * fix(pages): render dev errors through document * fix(pages): preserve document-provided html * fix(pages): keep render page enhancer type internal * fix(pages): buffer custom dev error documents |
||
|
|
c3dcdba1fe |
fix(pages): honor basePath in dev module loaders (#2033)
* fix(pages): honor basePath in dev module loaders * fix(pages): preserve basePath in ISR loaders * style(pages): format dev module loader changes * fix(pages): preserve dynamic route module paths * test(pages): run basePath dev coverage in CI |
||
|
|
a3d2f92152 |
fix(router): honor hybrid pages route priority (#1997)
* fix(router): honor hybrid pages route priority * fix(router): share hybrid owner decision with client navigation PR #1997 fixed the server-side route ownership for direct document loads, but the same invariant broke for client-side soft navigations and the matching prefetch path: - navigateClientSide() always delegated to the App runtime's RSC fetch, even when the Pages route had higher priority. renderPagesFallback() short-circuits RSC requests with null, so the App catch-all won. - prefetchUrl() prefetched an RSC stream for any URL that matched an App route, again ignoring Pages ownership. - The Pages entry was loaded on every hybrid request, even when a static App route had already matched (Pages can never win in that case). Expose the Pages route manifest on the client via a new __VINEXT_PAGES_LINK_PREFETCH_ROUTES__ window global emitted by both the App and Pages browser entries. The link shim consults a shared resolveHybridClientRouteOwner helper that mirrors the server-side pagesRouteHasPriorityOverAppRoute comparison. When Pages owns the URL, the click handler issues a window.location navigation and the prefetch path returns early. Gate the renderPagesFallback call behind a static-App-route check in handleAppRscRequest: when a static App route matches, the bridge cannot win, so skip the eager Pages entry load. Centralise the comparison in a new resolveHybridRouteOwner helper so server and client reach the same answer for the same (URL, route pair). Adds the missing client-navigation coverage to the use-params e2e fixture (Link from /app/ to /pages-dir/foobar) and unit tests for the shared owner decision. * fix(router): mirror server hybrid owner decision on the client PR review flagged two split-brain bugs in the previous hybrid ownership fix: a hand-copied client comparator and a Link-only ownership gate that left programmatic App Router navigations on the wrong path. The hand-copied routePrecedence in hybrid-client-route-owner.ts omitted the static-prefix reduction that lives in routing/utils.ts#routePrecedence, and used a strict-less-than comparison that returned App for identical dynamic patterns. The server returns Pages for both cases (Pages providers sort ahead of App providers, and routePrecedence subtracts 50 per static prefix segment). The split produced a real ownership disagreement on overlapping patterns like /_sites/:slug* (Pages) vs /:slug* (App). Add a shared compareHybridRoutePatterns to routing/utils.ts as the single source of truth: the static/dynamic short-circuits plus a sortRoutes call (which carries the static-prefix reduction and the Pages-first equal-pattern tiebreak). The server pagesRouteHasPriorityOverAppRoute and the client resolveHybridClientRouteOwner both delegate to it, so they cannot diverge. Drop the hand-copied routePrecedence entirely. Wire the ownership check at the App navigation runtime boundary so useRouter().push, useRouter().replace, gesturePush, and form submits all get the same hard-nav contract as the Link click handler. Specifically: - navigateClientSide: after same-origin normalization, if Pages owns the URL, hard-navigate via window.location and return (matching the existing external-URL branch). - _appRouter.prefetch: short-circuit RSC URL construction for Pages-owned targets so we do not warm an unusable cache entry. Centralise the existing two inline hard-nav branches in navigateClientSide into a hardNavigateTo helper for clarity. Tests: - Direct unit tests for compareHybridRoutePatterns covering identical-dynamic tiebreak, static-prefix dynamic overlap, static-prefix catch-all overlap, and infix-static bonus. - Direct unit tests for resolveHybridClientRouteOwner mirroring the server assertions plus a basePath-stripping test. - e2e: useRouter().push('/pages-dir/foobar') from an App page resolves to the Pages document. - e2e: useRouter().prefetch('/pages-dir/foobar') issues zero RSC requests for the target URL. * docs(router): fix stale score in static-prefix catch-all test comment The hand-copied comparator note quoted the dynamic-segment scores (51 / 1000) for the optional-catch-all example (/_sites/:slug* vs /:slug*). The current optional-catch-all scoring actually produces 1951 vs 2000. The test assertion was correct; only the comment was stale. * test(use-params): fix direct-load single dynamic param to use /a instead of /a/b * fix(router): compare hybrid routes structurally * fix(router): reject hybrid route conflicts * fix(router): refresh hybrid route ownership * fix(router): preserve hybrid routing lifecycle * fix(router): recheck pages routes after rewrites * fix(router): preserve rewritten pages queries * fix(router): preserve rewritten route ownership * fix(router): resolve client rewrites sequentially * fix(router): apply rewrite phases sequentially * fix(router): preserve rewrite params and endpoints * fix(router): hand off endpoint navigations * fix(router): preserve rewrite fragment params * test(e2e): avoid hybrid fixture route conflicts * test(pages): await async config validation * test(hybrid): avoid duplicate page fixtures * chore(router): clarify hybrid priority semantics * refactor(router): remove duplicate app route matcher * docs(router): clarify client hybrid comparator --------- Co-authored-by: James <james@eli.cx> |
||
|
|
b7ef466742 |
fix(pages): preserve app props for GSSP requests (#1996)
* fix(pages): preserve app props for gssp requests
* docs(pages): update _next/data comment to reflect full props envelope
* fix(pages-router): preserve _app.getInitialProps during stale ISR regeneration
Stale ISR background regeneration called getStaticProps directly and rendered
HTML and __NEXT_DATA__ with only the page's pageProps, dropping app-level
props from _app.getInitialProps. The foreground path already loads
_app.getInitialProps first and merges its props, but the background
regeneration path skipped that step.
Extract a shared loadPagesAppInitialRenderProps helper from the production
foreground path and reuse it during background regeneration in both
pages-page-data.ts and dev-server.ts. This ensures regenerated pages carry the
same full props envelope as the original render.
Add a regression test in tests/pages-page-data.test.ts.
* fix(pages-router): skip _app.getInitialProps on ISR cache hits
The foreground render path ran _app.getInitialProps before consulting the
ISR cache, so fresh and stale cache hits executed userland App data code
even though the cached HTML was returned verbatim. That wasted work on the
hot path and ran side-effecting App code whose result the cached response
never reflected.
The eager App initial-props load happened up front, ahead of the HIT and
STALE short-circuits. Defer it until the request will actually render:
load it lazily inside the getServerSideProps / getStaticProps /
getInitialProps branches, after the cache short-circuits return.
Stale-while-revalidate already re-runs it inside the regeneration closure,
so regenerated HTML still carries app-level props. The dev SSR handler and
the production page-data resolver are updated in lockstep to preserve
dev/prod parity.
Tests cover the fresh-HIT skip and the stale foreground-skip vs
regeneration-run split at the page-data resolver boundary.
* refactor(pages-router): extract dev App getInitialProps loader into a tested helper
The dev SSR handler ran _app.getInitialProps inside an inline closure that
mutated captured pageProps/renderProps and read res directly, so its lazy
load-only-on-render behavior could only be covered by a heavyweight
integration test that vi.doMock'd the isr-cache and file-matcher modules —
a mock cascade with little signal.
Move the decision into loadDevAppInitialProps in pages-get-initial-props.ts.
It takes the App component, req/res, and an injected AppTree builder (keeping
React out of the module) and returns a discriminated result —
skip / response-sent / render — that the caller applies. The dev-server
closure is now thin glue: build the React tree, delegate, apply the result.
Adds focused unit tests that drive the helper through its real boundary
(genuine getInitialProps functions, plain req/res inputs) — no module mocks:
the skip path, App/page prop merging, pageProps normalization, the
response-sent short-circuit, and the router/ctx wiring passed to userland.
* fix(pages): preserve app props across data requests
* fix(pages): preserve fallback cache expiry
* fix(pages): preserve dev app context during ISR
* fix(pages): hydrate middleware rewrite fallbacks
* fix(pages): retarget middleware data rewrites
* fix(pages): preserve rewrite query on navigation
* fix(pages): finalize rewrite navigation state
* fix(pages): resolve rewrite data targets consistently
* fix(pages): await static props during regeneration
* fix(pages): preserve original query in resolved url
* Revert "fix(pages): await static props during regeneration"
This reverts commit
|
||
|
|
3253aafd77 |
fix(pages-router): pass rewrite URL to edge API requests (#1998)
* fix(pages-router): pass rewrite URL to edge API requests * fix(pages-router): preserve edge API request URL parity * fix(pages-router): configure edge API NextRequest * fix(pages-router): preserve edge API locale config * fix(pages-router): preserve edge request metadata * fix(pages-router): align edge API URL formatting * fix(pages-router): match API locale casing * fix(pages-router): match domain locales by locale * docs(pages-router): clarify edge API basePath parity * test(deploy): expect metadata-preserving URL clone * docs(pages-router): note basePath rewrite parity --------- Co-authored-by: James <james@eli.cx> |
||
|
|
eef22e07a8 |
fix(pages-router): block fallback shell for crawler UAs on prerender (#1543) (#1663)
* fix(pages-router): block fallback shell for crawler UAs on prerender (#1543) When `getStaticPaths` returns `fallback: true` and an unlisted path is hit by a known bot/crawler, vinext was serving the `Loading...` shell so the crawler indexed the fallback HTML instead of the real page. Mirror Next.js `pages-handler.ts` and flip `isFallback` to false for bot User-Agents so the request renders synchronously with real props. - Add `isBotUserAgent` helper (DOM + HTML bot regex) in `utils/html-limited-bots.ts`, matching Next.js `shared/lib/router/utils/is-bot.ts`. - Plumb `userAgent` + `htmlLimitedBots` into `resolvePagesPageData` and flip the fallback bit when the UA matches. - Apply the same flip in the dev `createSSRHandler` for parity. - Targeted unit + integration regression tests covering Googlebot, Bingbot, DuckDuckBot, YandexBot, facebookexternalhit, and a normal Chrome UA control. Ported from `.nextjs-ref/test/e2e/prerender-crawler.test.ts`. Refs #1543. * fix(pages-router): preserve array user-agent bot detection |
||
|
|
f5355964aa |
fix(pages-router): set x-nextjs-deployment-id on SSG _next/data responses (#1863)
* fix(pages-router): set x-nextjs-deployment-id on SSG _next/data responses Pages Router `/_next/data/<buildId>/<page>.json` responses were missing the `x-nextjs-deployment-id` response header that Next.js sets on these routes when a deployment ID is configured. The client router reads this header to detect when a new deployment has been rolled out and triggers a hard navigation for deployment-skew protection. Root cause: the `isDataReq` short-circuit path in `pages-page-handler.ts` that builds the JSON envelope (`buildNextDataJsonResponse`) never injected the header. Next.js's `pages-handler.ts` sets `NEXT_NAV_DEPLOYMENT_ID_HEADER` on every `isNextDataRequest` response (success, redirect, and notFound cases). Fix: read `process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID` (the runtime-available deployment ID, inlined at build time by Vite's define transform) and set `x-nextjs-deployment-id` on the `_next/data` response headers when a deployment ID is configured. Maps to test: `pages ssg data deployment skew - hard navigate when a new deployment occurs > header with deployment id > header is set on data routes`. Fixes #1829 * fix(pages-router): complete x-nextjs-deployment-id parity for _next/data (bonk #1829) Address all three ask-bonk findings on PR #1863: 1. Incomplete parity — add x-nextjs-deployment-id to redirect and notFound _next/data exits in pages-page-data.ts (buildPagesRedirectResponse, buildPagesDataNotFoundResponse). Thread deploymentId through ResolvePagesPageDataOptions and pass it from pages-page-handler.ts, matching the success-path pattern already in that file. 2. Dev/prod parity — patch dev-server.ts to set x-nextjs-deployment-id on all three _next/data exits: the writeGsspRedirect isDataReq path, the getStaticPaths/ getServerSideProps/getStaticProps notFound isDataReq paths, and the success JSON envelope short-circuit (isDataReq block). Mirrors the prod handler. 3. Add regression tests asserting x-nextjs-deployment-id is present on _next/data success / redirect / notFound responses in both pages-page-data.test.ts (unit, via deploymentId option) and pages-page-handler.test.ts (integration, via process.env.__VINEXT_DEPLOYMENT_ID), and absent when no env var is set. * fix(pages-router): add x-nextjs-deployment-id to wrong-buildId/route-miss _next/data 404s (#1829) Fixes the deployment-skew gap identified in review: `buildNextDataNotFoundResponse` now includes the `x-nextjs-deployment-id` header so all three wrong-buildId/route-miss exits (normalizePagesDataRequest, prod-server Node path, pages-page-handler route-miss) surface the deployment ID to the client router. Adds unit tests for the new header on all relevant exits. * fix(pages-router): set deployment-id header on remaining two dev-mode _next/data 404 exits (#1829) Patches the stale-buildId 404 in the vite-dev middleware (index.ts) and the route-miss data 404 in createSSRHandler (dev-server.ts) to set x-nextjs-deployment-id, matching prod parity via buildNextDataNotFoundResponse(). * test(pages-router): cover dev-mode _next/data deployment-id header exits Drive the two dev-only exits flagged in review through the real fixture dev server: the wrong-buildId data 404 in the plugin middleware (index.ts) and createSSRHandler's route-miss data 404 (dev-server.ts), plus the dev success path and the no-deployment-id case. * fix(pages-router): skip deployment-id header on /_error and /500 data success responses Next.js pages-handler.ts guards the success-path x-nextjs-deployment-id injection with `!isErrorPage && !is500Page`. Mirror that exclusion in both the prod success short-circuit (pages-page-handler.ts) and the dev one (dev-server.ts), and lock it in with a prod unit test (/_error and /500) plus a dev integration test (/500). |
||
|
|
08504a7745 |
fix(app-router): preload next/dynamic chunks with CSP nonce (#1594)
* fix(app-router): preload next/dynamic chunks with CSP nonce
Rendered next/dynamic boundaries produced no SSR preload links for their client chunks. That diverged from Next.js and broke nonce-based CSP tests that inspect preload tags before hydration.
The runtime assumed React.lazy alone was enough for dynamic imports. It never carried the dynamic module IDs from the source call into SSR, and client JS chunk URLs did not use Next.js's static/chunks path shape.
Add a focused dynamic metadata transform, resolve boundary files from the client build manifest, emit nonce-bearing preload hints during SSR, and keep client JS assets under _next/static/chunks. Ported regression coverage verifies the nonce and the emitted path shape.
* fix: honor assetPrefix for dynamic preload globals
* test: cover asset-prefixed dynamic preload nonces
App Router production only covered the default dynamic preload URL path. That left assetPrefix regressions at the manifest-to-runtime emission boundary unguarded.
Add a startProdServer regression that builds app-basic with assetPrefix "/cdn" and asserts nonce-bearing next/dynamic chunk preloads render under /cdn/_next/static/chunks/.
* test: isolate asset-prefixed prod server globals
The assetPrefix regression starts a second production server in the same Vitest process. startProdServer and the generated RSC runtime install global manifest and module-loader state, so leaving the temporary build's globals in place breaks later production requests in the shared server.
Snapshot and restore the relevant globals around the temporary /cdn server while keeping the integration assertion at the real runtime boundary.
* refactor: reuse shared record guard in dynamic preload metadata
The dynamic preload metadata transform carried a local object guard even though the codebase already has a shared record predicate. That made the AST helper drift from the existing non-array record invariant used elsewhere.
Reuse isUnknownRecord from utils/record and keep the plugin-specific AST alias local to the transform.
* fix: make dynamic preload metadata binding-aware
The next/dynamic metadata transform matched calls by imported identifier text. Shadowed parameters and block bindings with the same name were therefore mutated even when they no longer referred to the next/dynamic import.
Walk the parsed module with lexical binding state so shadowed names are excluded, and collect object-form imports only from loader/modules rather than every property on the options object.
* fix: model switch and class scopes in dynamic preload transform
The dynamic preload metadata transform still treated imported next/dynamic names as active inside switch case lexical scopes and named class expression bodies. That could inject loadableGenerated metadata into calls that resolve to local bindings rather than the import.
The scope walker now removes switch case-block bindings while traversing case tests and consequents, leaves the switch discriminant in the outer scope, and scopes named class declarations and expressions through their children. Regression tests cover switch case shadowing and named class expressions.
* fix: address PR review feedback for next/dynamic CSP nonce
- Drop `as="style"` from `<link rel="stylesheet">` — `as` is a
preload attribute per the HTML spec, semantically incorrect on
stylesheet links. Browsers ignore it but it produces bad HTML.
- Remove brittle `import(` string gate from the transform — it could
miss `import ("./x")` (whitespace between import and paren). The
Vite filter already gates on "next/dynamic" and the AST parse is the
real validation.
- Add comment explaining MagicString mutation safety in Promise.all.
- Add comment explaining AST end-exclusive assumption on closeParen.
- Add test for whitespace-separated dynamic import syntax.
* fix: update global.d.ts comment to mention asset prefix, fix formatting
* refactor: extract computeClientRuntimeMetadata shared helper
Extract the duplicated client-manifest runtime metadata computation
(lazy chunks, dynamic preloads, client entry) into a single shared
helper in utils/client-runtime-metadata.ts.
Both prod-server.ts (Pages Router Node.js production server) and
index.ts (Cloudflare closeBundle hook) now call the same helper,
eliminating the highest-risk seam in the PR where basePath/
assetPrefix handling produced bugs.
* test: add TSX-generic and object-form metadata regression tests
- Add post-type-strip generic call test verifying multi-line
dynamic() calls work after TS/JSX stripping by esbuild.
- Add object-form existing loadableGenerated preservation test
to guard against duplicate injection in the object overload.
* chore: remove unused ClientRuntimeMetadata type export
* revert: discard unrelated lockfile change from previous commit
* test: replace deploy test simulators with computeClientRuntimeMetadata
simulateCloseBundleAppRouter and simulateCloseBundlePagesRouter
no longer hand-roll lazy chunk and dynamic preload computation.
They call the shared computeClientRuntimeMetadata helper directly,
matching production wiring.
Also:
- Update helper doc comment to mention Node server startup
- Remove unused imports (manifestFileWithAssetPrefix,
manifestFileWithBase, computeDynamicImportPreloads,
dynamicImportPreloadsWithBase) from deploy.test.ts
* test: add absolute assetPrefix CSP nonce tests and clean up redundant globals
* fix(app-router): keep CSP nonce on next/dynamic preloads from Server Component call sites
DynamicPreloadChunks rendered in the environment of the dynamic() call site. For a Server Component call site it ran in the RSC environment, where the script-nonce React context is unavailable, so the emitted dynamic preload <link>s dropped the request nonce — a CSP violation under `script-src 'nonce-…' 'strict-dynamic'`. The existing tests missed this because every fixture called dynamic() from "use client" modules, which render in the SSR pass where the nonce is present.
Extract DynamicPreloadChunks into its own "use client" module (mirroring Next.js's PreloadChunks and vinext's next/script shim) so it always renders in the SSR pass where withScriptNonce() installs the provider, regardless of whether dynamic() is called from a Server or Client Component.
Add fixture regression tests: server-call-site JS and CSS preloads carry the nonce, an ssr:false boundary emits no server preload (Next.js loadable.tsx parity), and preloads are still emitted without a nonce when no CSP is set.
* test(app-router): add browser CSP-nonce e2e for Server Component next/dynamic call site
The existing csp.spec.ts browser test only exercises the client call-site page (/nextjs-compat/dynamic). Add a Playwright test for the Server Component call site (/nextjs-compat/dynamic/rsc-imports-client) asserting the lazily-loaded client widget hydrates under script-src 'nonce-…' 'strict-dynamic' with no CSP violations in the console.
* fix(app-router): correct next/dynamic preload key-spaces and harden the transform
Addresses review findings on the next/dynamic CSP-nonce feature.
- Lazy chunks must stay in the SSR-manifest key-space (basePath only); only dynamic preloads take the assetPrefix. Applying an absolute-URL assetPrefix to lazy chunks broke the Pages Router modulepreload-exclusion membership test and leaked lazy chunks into <link rel=modulepreload>.
- Resolve symlinks on both sides when computing a dynamic boundary's module ID (reusing relativeWithinRoot/tryRealpathSync), so the key matches the client manifest under pnpm/Cloudflare symlinked layouts instead of silently dropping the preload.
- Throw on dynamic() calls with >2 args (Next.js react-loadable parity).
- Insert the generated options arg right after the first arg, preserving comments/whitespace (the previous substring-comma scan ate commas inside comments).
- Recurse into _next/static/chunks/ in the on-disk client-entry fallback (client JS now lands under chunks/).
- Add typeof-window guard to DynamicPreloadChunks and document the deliberate as="style" omission for stylesheet links.
- Extract buildRuntimeGlobalsScript so the Cloudflare closeBundle injection shares one source of truth with the deploy tests (which now mirror includeClientEntry + the mixed app+pages branch).
Tests: lazy/dynamic key-space split incl. absolute-assetPrefix leak regression and a realistic already-prefixed manifest; mixed app+pages client-entry injection; symlinked module-id resolution; nested dynamic() calls; >2-arg throw; comment-safe insertion; ssr:false positive content assertion; honest dev-mode E2E naming (preload-nonce coverage lives in the prod-server Vitest suite).
* fix(app-router): fix parenthesized next/dynamic loader corruption + harden re-review findings
Follow-up to the re-review of
|
||
|
|
3a1e56748f |
fix(app-router): match Pages navigation params in hybrid builds (#1741)
* fix(app-router): emit pages client entry for hybrid builds * fix pages router navigation params readiness * fix(pages-router): track hashed fallback client entry * fix(pages-router): honor navigation readiness in hooks * fix(pages-router): derive server router.isReady from SSR readiness Address review feedback on #1741: - getRouterSnapshot() now reads navigationIsReady from the SSR context on the server instead of unconditionally returning true. For pre-ready Pages routes (auto-export dynamic / query-string / rewrite-capable builds) this keeps useRouter().isReady consistent between SSR and client hydration, avoiding a hydration mismatch for components that read it in JSX. Mirrors Next.js render.tsx's server readiness rule. - Drop the redundant guard branch in getClientParamsSnapshot. - Add a regression test asserting server useRouter().isReady reflects the SSR navigationIsReady context. * fix(pages-router): align param resolution and dev/prod readiness parity Address review feedback (ask-bonk + self-review) on #1741: - Align client Pages navigation params to query-first, matching the server snapshot and Next.js's adaptForPathParams (derives path params from router.query). __NEXT_DATA__.query is kept current on client navigation, so the two snapshots are now provably identical and avoid a latent post- hydration mismatch. - Populate window.__VINEXT_PAGE_PATTERNS__ in the dev server (own script tag) so the next/navigation compat hooks can resolve a dynamic pattern from a resolved path in dev exactly as in production. Closes a dev/prod parity gap (prod set this via the client entry; dev never did). - Drop the redundant pre-readiness setSSRContext in dev; the single post- module-load call now publishes navigationIsReady before rendering, matching the prod handler. - Document that installPagesClientAssetGlobals is intentionally inert for App-only builds, and that getReadonlyPagesSearchParams' string-keyed cache is safe to share across concurrent SSR requests (immutable wrappers). - Add a dev regression test asserting __VINEXT_PAGE_PATTERNS__ exposure. * fix(pages-router): prefer URL params for client snapshots * fix(app-router): gate pages asset globals * test(app-router): add production test for hybrid basePath + assetPrefix * fix(navigation): widen usePathname return type to string | null * refactor(pages-router): dedupe navigation readiness modeling and cache pattern scan Addresses code-review cleanup findings on the Pages Router navigation readiness work: - Extract buildPagesReadinessNextData() so the dev SSR handler and the production Pages page handler derive the gssp/gsp/gip/appGip/autoExport __NEXT_DATA__ readiness flags from one place, instead of two hand-kept copies that could drift and produce a dev/prod hydration mismatch. - Extract resolvePagesNavigationPathname() shared by the client and SSR branches of getPagesNavigationContext, so the pathname null-ness rule (fallback / pre-ready auto-export dynamic) cannot diverge between them. - Memoize resolvePagesRoutePatternForPath()'s pattern scan, which ran an uncached O(routes) loop on every useSyncExternalStore snapshot read. - In getServerSearchParamsSnapshot, serialize the source only when the object-identity fast path misses, avoiding a toString() on every read. * refactor(pages-router): consistent ISR readiness data + robustness cleanups Addresses further code-review findings: - Thread the readiness flags (gssp/gsp/gip/appGip/autoExport) through the ISR background-regeneration path: renderPagesIsrHtml now serializes the same __NEXT_DATA__ readiness state the initial render emits, so a regenerated, re-cached page hydrates with the identical initial router.isReady the server computed. Previously these flags were dropped on that path — benign today (ISR-eligible getStaticProps routes never trigger the flag-gated early-ready branches) but a latent dev/prod hydration-readiness drift. Adds a shared PagesNextDataExtras type so every render path stays in lockstep. - readSsrManifest: degrade a malformed/unparseable SSR manifest to "no SSR manifest" with a warning instead of throwing and aborting server startup, restoring the previous tolerant behavior. - pages-readiness: project PagesReadinessNextData from the canonical VinextNextData and accept PagesPageModule, instead of bespoke types that could drift from the __NEXT_DATA__ shape. - getReadonlyPagesSearchParams: drop the redundant string-keyed cache slot; the per-object WeakMap is authoritative and concurrency-safe on its own. - isPagesRouterReady: drop the redundant server-side window check. * fix(pages-router): restore string-keyed readonly search-params cache Reverts the cache simplification from the previous commit. The string-keyed slot is load-bearing, not redundant: across the Pages Router pre-ready → ready transition the navigation context swaps in a new URLSearchParams instance even when the query string is unchanged, so the per-object WeakMap alone returns a fresh ReadonlyURLSearchParams wrapper and useSearchParams() loses Object.is stability — re-firing a [searchParams] effect. Caught by the rewrite-driven pre-ready static page e2e (search-params-snapshots saw ['',''] not ['']). Keeps the improved comment explaining why both cache levels are needed. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
494e8d2d5b |
fix(pages): render custom errors for notFound results (#1737)
* fix(pages): render custom errors for notFound results Pages getStaticProps and getServerSideProps notFound HTML results returned the built-in 404 response directly. That skipped the configured pages/404 or pages/_error render path, so _error.getInitialProps never received ctx.req.url or ctx.asPath for the original request. The page-data resolver now returns a typed notFound signal for HTML requests while preserving JSON-shaped 404 data responses. The generated Pages entry rerenders the configured not-found route with the original asPath, and Pages getInitialProps is loaded through a shared typed helper for prod and dev parity. * fix(pages): address PR review feedback on getInitialProps binding, err context, and response-sent handling - Call getInitialProps with component as this receiver - Thread caught errors through to _error.getInitialProps(ctx.err) - Check headersSent || writableEnded in dev server after getInitialProps - Add tests: gSSP notFound, explicit 404, data JSON 404, err on throw, this binding * refactor(pages): use isResponseSent helper in resolvePagesPageData for sent-check consistency The prod block in resolvePagesPageData checked only res.headersSent, while the helper's isResponseSent and the dev server check both headersSent and writableEnded. The prod PagesReqResResponse only declares headersSent (its getter is backed by the ended flag), so this is functionally correct in prod, but the asymmetry is hard to read next to the helper and a future change to the prod response shape would silently break sent-detection. Export isResponseSent from pages-get-initial-props and use it at both gSSP and gIP call sites so the prod resolver reuses the same sent-detection logic as the helper. Also adds tests for the writableEnded-only path and a focused unit test for isResponseSent covering both prod-shaped and node-shaped res mocks. * perf(pages): gate getInitialProps req/res allocation behind presence check The getInitialProps prod block in resolvePagesPageData unconditionally called createGsspReqRes() for every static page without gSP/gSSP, even when the page had no getInitialProps either. This allocated a req/res pair and an unresolved responsePromise that was immediately discarded. Export hasPagesGetInitialProps from pages-get-initial-props.ts and gate the prod block on it, keeping the hot path for plain static pages allocation-free. |
||
|
|
fb233b0a34 |
feat: improve dev error overlay source frames (#1746)
* Improve dev overlay stack source mapping * Polish dev error overlay stack display * Add dev overlay code frames * Align dev overlay message spacing * Add dev overlay error copy action * Fix dev overlay knip exports * Polish dev overlay layout * Polish dev error overlay spacing * Fix dev overlay coverage and source frame metadata * Fix dev overlay recovery for RSC HMR errors * Handle Vite HMR build errors in dev overlay * Fix knip * Use Shadow DOM for dev error overlay |
||
|
|
51690038d8 |
fix(pages-router): cancel in-flight nav on gSSP/gSP data redirect (#1465) (#1691)
* fix(pages-router): return __N_REDIRECT JSON for gSSP/gSP data redirects (#1465) When getServerSideProps/getStaticProps returns `{ redirect }` on a client-side `_next/data` navigation, vinext emitted a real HTTP redirect. The client's fetch transparently followed it to the destination's HTML (not a valid data envelope), forcing a hard reload with console errors and leaving navigation on the wrong page. Mirror Next.js: for data requests, reply 200 with `__N_REDIRECT` / `__N_REDIRECT_STATUS` in pageProps. The client router now detects these markers and re-dispatches a fresh client navigation to the destination, which increments the navigation id and supersedes (cancels) the in-flight navigation so the intermediate page never commits. HTML requests still emit a real HTTP redirect. Fixed in both server paths to keep dev/prod parity (see AGENTS.md): - prod: buildPagesRedirectResponse() in pages-page-data.ts branches on isDataReq - dev: writeGsspRedirect() in dev-server.ts branches on isDataReq - client: navigateClientData() handles pageProps.__N_REDIRECT via handleDataRedirect(), preserving push/replace mode - tests: data-endpoint redirect shape (vitest) + client cancellation (e2e) * fix(pages-router): hard-navigate to external __N_REDIRECT destination (#1465) Address review: handleDataRedirect() passed the originating page URL to the hard-navigation fallback for external/non-absolute __N_REDIRECT destinations, which would loop back to the redirecting page (e.g. an OAuth callback at https://example.com/...). Hard-navigate to the destination instead, matching Next.js's handleHardNavigation({ url: destination }). Drop the now-unused fallback parameter and add a server test asserting an external destination is preserved verbatim in the data envelope. |
||
|
|
b1f2e57bc4 |
fix(pages): preserve import.meta.url source paths (#1598)
* fix(pages): preserve import.meta.url source paths
Pages Router production bundles previously left direct import.meta.url expressions for the bundler to evaluate from generated output. Server renders could expose dist/server/entry.js, and prerendered Pages output could miss the client entry needed for hydration to replace the server value with the Turbopack-style browser source URL.
The fix rewrites direct import.meta.url reads in user modules to source-file URLs at transform time, keeps new URL asset bases intact, and loads the Pages client entry from the build manifest for Node production and prerender servers.
Regression coverage ports the Next.js import-meta deploy test, including the prerender and hydrated browser path.
* fix(tests): run import.meta browser coverage in e2e
The import-meta regression test launched Playwright from a Vitest integration shard. That shard does not install Playwright browser binaries in CI, so it failed before exercising the runtime behavior.
Move the browser assertion into the existing pages-router-prod Playwright project, keep the lower-boundary server and client chunk assertions in Vitest, and cache the import-meta plugin root paths after an early source-text bailout to address review feedback.
* refactor(import-meta-url): canonicalize each module once per transform
The transform path canonicalized the same module id twice: once in the
eligibility check and again when computing the replacement value, so every
rewritten module paid for two fs.realpathSync.native() syscalls.
The eligibility check now returns the canonical id and threads it through to
the value computation, leaving a single realpath per module.
Also widens the extension match to .cts/.mts/.cjs and documents that the
new URL() skip only covers the bare URL identifier, matching Vite's own
asset-detection scope.
* test(pages): drop unrelated prerender from top-level-await setup
The top-level-await production test ran runPrerender in its setup, but the
assertions pass identically whether the routes are prerendered or rendered on
demand. The step added unrelated build work to a test that is not about
prerendering or client-entry injection.
* fix(pages): share client entry fallback
* fix(import-meta-url): address PR review feedback
- Handle ChainExpression (import.meta?.url) in new URL() asset-detection
skip so the optional-chain variant preserves Vite asset resolution
- Add explicit ChainExpression check in visit() to avoid fragile
reliance on coincident inner-node start/end ranges
- Add test for new URL('./file', import.meta?.url) preservation
- Fix decodeHtmlText entity ordering (& before ") in both
unit and E2E test helpers
- Add doc comments: excluded prefixes rationale, node_modules
early-exit optimization intent
* fix(pages): select client entry by marker in build manifest
findClientEntryFileFromManifest returned the first chunk flagged
isEntry, but a client build can emit several entry chunks (client
entry plus instrumentation/middleware) and manifest order is not
guaranteed. The on-disk fallback already disambiguates by the
vinext-client-entry / vinext-app-browser-entry marker; apply the
same check to the manifest path so both lookups agree, falling back
to the first entry only when nothing is marked.
* refactor(pages): address review feedback on client-entry/import-meta-url
- prod-server: hoist findClientEntryFile out of the manifest if/else
since it already handles a missing manifest via the on-disk fallback
- import-meta-url: document the two known limitations (destructured
access, aliased new URL() base) that match Vite's own behavior
- import-meta-url: drop the dead visit(value.callee) for new URL() —
the callee is always the bare URL identifier
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
5d9548dc92 |
fix(pages-router): invoke Document.getInitialProps so docProps reach SSR (#1633)
* fix(pages-router): invoke Document.getInitialProps so docProps reach SSR (#1361) `pages/_document.tsx` files that override `static async getInitialProps` to inject extra props were rendered with an empty `this.props`, because both the dev-server and the production pages response builder created the Document element with `React.createElement(DocumentComponent)` and never called the hook. The Next.js async-modules e2e fixture (`test/e2e/async-modules/pages/_document.jsx`) uses exactly that pattern to surface a `docValue` from `await Document.getInitialProps(ctx)` plus a top-level `await` constant. PR #1381 (issue #1361) fixed the class construction crash, but the deploy-suite continued to fail "csr async page modules" because `<div id="doc-value">` rendered empty. This change adds a small `loadUserDocumentInitialProps` helper used by both the Pages Router dev-server (`streamPageToResponse` shell and the custom-error-page shell) and the production response builder (`buildPagesShellHtml`). It invokes the override (if any) and spreads the resolved props on the Document element, mirroring Next.js's `render.tsx` `<Document {...htmlProps} {...docProps} />` call. The helper short-circuits when the user did not override the base shim, so the existing fast path keeps the same number of awaits. Reference: vercel/next.js packages/next/src/server/render.tsx (`loadDocumentInitialProps`, `documentElement`). Tests: - Adds a new pages-router production e2e case that builds a `_document.tsx` with `static async getInitialProps` + top-level `await`, plus a custom `404.tsx` that uses top-level `await`, and asserts the resolved values reach the rendered HTML. - Unit-tests `loadUserDocumentInitialProps` directly for the override, no-override, and throwing-override cases. Refs #1361 * fix(pages-router): address bonk review on Document.getInitialProps helper Two changes from review feedback on PR #1633: 1. Cache the base getInitialProps reference at module level. The previous helper did `await import("vinext/shims/document")` on every request to compare identity — unnecessary per-request work and an extra await on the fast path where the user has no override. Switched to a static import + module-scoped constant so the identity check is a synchronous reference compare. No-override callers now resolve with one less await. 2. Let user `getInitialProps` errors propagate. Next.js's `loadGetInitialProps` does not catch — a throw surfaces as a 500 to the caller. The previous version logged-and-swallowed the error and returned null, which silently erased docProps on every failing render and made user bugs harder to find. vinext now matches Next.js's contract. Updated the corresponding unit test to assert the throw propagates rather than that the helper resolves to null. Refs #1361, #1633 |
||
|
|
76fe5d8b05 |
fix(pages-router): render custom pages/500 on SSR throw (#1676)
When getServerSideProps (or any other render-time code) threw in the production server entry, vinext collapsed the response into a plain "Internal Server Error" text body. Next.js instead renders the user's custom pages/500.tsx (with pages/_error.tsx as a fallback). Match that behaviour so frameworks that rely on custom error pages — and the Next.js getserversideprops deploy suite — see the expected output. The catch block in `_renderPage` now re-renders through the same entry using `pages/500.tsx` (or the `_error` route) with status 500. A new `__isInternalErrorRender` guard prevents infinite recursion if the error page itself throws; data (`/_next/data/...`) requests still short-circuit to the plain text body since they can't return HTML. Mirrors Next.js base-server.ts `renderToResponseImpl` 500 fallback. The dev server already renders custom 500/error pages via `renderErrorPage()` — this aligns prod with that behaviour and adds explicit regression coverage for both paths via a new gssp-throw fixture. Refs #1458 |
||
|
|
15fbe66c47 |
test(pages-router): regression coverage for edge runtime & OG image API routes (#1338) (#1648)
Adds Pages Router fixtures and dev + production integration tests for the
two scenarios reported as failing in the Next.js Deploy Suite:
- `export const config = { runtime: 'edge' }` API route returns a Web
Response (issue #1338 case 1 — edge API routes were observed as 500).
- `next/og` `ImageResponse` with `runtime: 'edge'` inside a Pages Router
API route returns `image/png` (issue #1338 case 2 — OG routes were
observed as 404).
Ports route handlers from `next.js/test/e2e/edge-pages-support/app/pages/api/hello.js`
and `next.js/test/e2e/og-api/app/pages/api/og.js`. The existing
implementation already wires both code paths correctly via
`handlePagesApiRoute` and the `vinext:og-inline-fetch-assets` plugin; this
PR locks in coverage so a Pages Router-only build cannot silently regress
either path.
Refs #1338
|
||
|
|
f75845ef14 |
fix(pages-router): emit page scripts with defer in <head> by default (#1636)
* fix(pages-router): emit page scripts with defer in <head> by default Next.js' Pages Router emits page scripts with the `defer` attribute in `<head>` when `experimental.disableOptimizedLoading` is false (the default). vinext emits `<script type="module">` (which already defers implicitly), but upstream tests assert the literal `defer` attribute and were failing against the deploy suite. Add `defer` to module scripts emitted by `collectAssetTags` when `experimental.disableOptimizedLoading` is not enabled. Surface the flag in `ResolvedNextConfig` so prod-server respects an explicit opt-out. Browser behaviour is unchanged (module scripts already defer), but parity tests now pass. Fixes #1519. * test(e2e): update hydration regex to allow defer attribute The script tag regex in `tests/e2e/cloudflare-pages-router/hydration.spec.ts` required `type="module"` to be immediately followed by `src="…"`, but the optimized-loading fix inserts a `defer` attribute between them. Relax the regex to allow any whitespace-separated attributes between `type="module"` and `src` so future attribute additions don't break it either. |
||
|
|
a20c49ead2 |
fix(middleware): stop merging original query into middleware rewrite target (#1632)
* fix(middleware): stop merging original query into middleware rewrite target
Next.js middleware constructs the rewrite-target URL itself — by mutating
`request.nextUrl` or by passing a fresh URL — and whatever search params
that URL carries IS the final query. vinext was silently re-merging the
original request's query on top, which resurrected keys that middleware
had explicitly removed via `searchParams.delete(...)`.
Tests:
- Add `mw-clear-query-params` Pages Router fixture + dev test ported from
Next.js `test/e2e/middleware-rewrites/test/index.test.ts` ("should
clear query parameters").
- Add prod-server tests for the original-query-preservation paths
(middleware now propagates query via `request.nextUrl.clone()` in the
fixture, mirroring Next.js `app/middleware.js`).
- Add App Router parity test for original-query-preservation.
Closes #1342
* test(middleware): address bonk review nits for #1342
- Fix misleading comment in app-router.test.ts that referenced Object.assign
merge semantics — the PR's premise is that vinext should NOT auto-merge.
- Add prod-server regression test for searchParams.delete on rewrite target,
mirroring the dev coverage so both code paths are exercised explicitly.
|
||
|
|
35bb1a2250 |
fix(pages-router): return 405 with Allow: GET, HEAD on POST to static pages (#1631)
Pages Router previously rendered HTML for any HTTP method, including POST/PUT/DELETE, on static (no `getServerSideProps`) pages — masking client bugs that Next.js surfaces as 405. Mirrors Next.js's behavior in `base-server.ts` (the `(typeof components.Component === 'string' || isSSG)` gate), which rejects non-GET/HEAD methods on static pages with `Allow: GET, HEAD` and a `Method Not Allowed` body. Pages with `getServerSideProps` continue to accept any method, since user code may legitimately read `req.method` inside gSSP. The new `resolvePagesPageMethodResponse` helper is wired into both the dev-server SSR handler and the prod-server pages-server-entry, and skips `/_error`, `/404`, `/500`, data requests, and explicit status overrides — matching the upstream carve-outs. Refs #1463. The `res.revalidate()` portion of #1463 is left for a follow-up. |
||
|
|
b6ac0c9abd |
test(pages-router): regression test for literal form action=action:foo (#1476) (#1613)
* test(pages-router): regression test for literal form action=action:foo (#1476) Adds a Pages Router fixture page with a literal action:foo string in a form action attribute, plus an integration test asserting the attribute survives SSR serialization intact. This guards against future regressions where React 19 or the RSC plugin might strip the literal string because it resembles a server action ID. Ported from Next.js test/e2e/app-dir/action-in-pages-router. * test: clarify action:foo regression test comment Update the comment to accurately describe that this test verifies React 19 SSR preserves literal string action attributes, not server action invocation. This addresses review feedback on the PR adding the regression test for #1476. |
||
|
|
5b633b0284 |
fix(pages): render masked basePath error routes (#1441)
* fix(pages): render masked basePath error routes
Pages Router route misses under basePath returned generic HTML and masked client navigations to /404 or /_error could fetch the visible as-path instead of the error route. This diverged from Next.js when applications used next/router to preserve a friendly URL while rendering the error component.
The implementation assumed the browser URL and the component route could always collapse to one fetch target. Preserve the masked history URL, fetch the Pages error route as the HTML target, allow its 404 response to hydrate, and render pages/404 for production route misses with the correct status and client module metadata.
* test(pages): keep error navigation coverage out of vitest browser
Vitest integration CI does not install Playwright browsers, so launching Chromium from tests/pages-router.test.ts made the PR fail deterministically even though the upstream deploy harness passed.
Keep the browser-level verification in the upstream deploy-suite run and cover vinext locally at the lower boundaries: production 404 rendering through HTTP and Pages Router masked error-route navigation through the router shim. The generated Pages entry now also shares one SSR manifest module lookup helper between asset tag collection and client module URL resolution.
* fix(prerender): keep custom 404 out of generic page loop
* fix(pages): fall back to _error on route misses
* fix(pages): document the catch-all guard on matchRoute("/404")
The pattern check prevents a dynamic catch-all route (e.g. [...slug]) from
being used as the 404 fallback. Without it, matchRoute can return a
catch-all match and silently hijack the error rendering path.
* fix(pages): preserve error navigation rewrite order
* fix(pages): limit fallback rewrites to route misses
* fix(deploy): skip deferred error rendering for data requests
* fix(pages): preserve ISR response status
* fix(pages): keep module metadata in ISR regeneration
|
||
|
|
61cf2e78f5 |
fix(image): emit /_next/image URLs (#1562)
* fix(image): emit /_next/image URLs to match Next.js Closes #1513 The default image loader and optimization endpoint switched from the vinext-specific /_vinext/image path to Next.js's canonical /_next/image. This unblocks the deploy suite tests that import Next.js's expected URL shape (/_next/image?url=...&w=...&q=...). * refactor(image): use IMAGE_OPTIMIZATION_PATH constant at remaining call sites Replace hardcoded "/_next/image" strings in index.ts, app-rsc-handler.ts, and the generated worker entry templates in deploy.ts with the IMAGE_OPTIMIZATION_PATH constant from server/image-optimization, matching the pattern already used in prod-server.ts. Prevents future drift if the path ever changes again. * feat(image): accept both /_next/image and /_vinext/image at the optimizer Add a VINEXT_IMAGE_OPTIMIZATION_PATH constant and an isImageOptimizationPath() helper, then route through every match site (prod-server, dev server passthrough, app RSC handler, generated worker templates, and shipped example workers). Apps that wire image URLs to either prefix now hit the same handler; new URLs are still emitted via IMAGE_OPTIMIZATION_PATH. |
||
|
|
e5d9c4ca33 |
fix(pages-router): await Promise-shaped gssp props (#1568)
* fix(pages-router): await Promise-shaped gssp props
Next.js explicitly supports a Promise value for `getServerSideProps`
`props`. Pages returning `{ props: Promise.resolve(...) }` previously
rendered empty pageProps because vinext passed the unresolved Promise
straight through to the serializer. Match Next.js' `render.tsx` by
detecting a thenable `props` and awaiting it before continuing.
The fix is applied symmetrically to the dev server gssp dispatch
(`dev-server.ts`) and the prod page-data dispatch (`pages-page-data.ts`)
so the two stay in sync per AGENTS.md. Other return shapes (`notFound`,
`redirect`, `revalidate`, fallback) are untouched.
Closes #1459
* refactor: simplify Promise-shaped props handling
await Promise.resolve(x) already handles both thenables and plain
values, so the .then duck-typing check is unnecessary.
|
||
|
|
37e077dd06 |
fix(pages-router): preserve query string in Link and router.push (#1572)
`parseQueryString` and the parseQuery template embedded in the Pages
Router worker entry both used `url.split("?")[1]`, which silently drops
everything after the second `?`. URLs like `/linker?href=/about?hello=world`
have query `href=/about?hello=world` per RFC 3986, so the previous
implementation truncated `query.href` to `/about` and `<Link>` rendered
without the embedded query string. Switching to an `indexOf`-based slice
keeps the full query value intact.
Closes #1471
|
||
|
|
7ffa2b7f3c |
fix(pages-router): default Cache-Control on gssp responses (#1574)
getServerSideProps responses now ship the Next.js default
`private, no-cache, no-store, max-age=0, must-revalidate` Cache-Control
header in both dev and prod, and forward `res.setHeader('Cache-Control', ...)`
overrides set inside gssp through to the final response.
Mirrors next/src/server/route-modules/pages/pages-handler.ts where a
gssp page resolves to `revalidate: 0` and getCacheControlHeader emits
the same string, gated by `if (!res.getHeader('Cache-Control'))` to
preserve user overrides.
Closes #1461
|
||
|
|
3ebc28f0e1 |
fix: preserve Next.js cache headers for prerendered app pages (#1424)
* fix: preserve Next.js cache headers for prerendered app pages * fix: address cache header review feedback |
||
|
|
81bdd6acd3 |
fix(pages-router): render loading shell for unlisted fallback: true paths (#1414)
* fix(pages-router): render loading shell for unlisted `fallback: true` paths PR #1394 fixed the post-rewrite static-vs-dynamic priority sub-problem from #1331 and explicitly tracked `fallback: true` rewrite behaviour separately. When `getStaticPaths` returns `fallback: true` and a request hits a path not in the pre-rendered list, vinext was SSRing the page fully — running `getStaticProps` and emitting `isFallback: false` in `__NEXT_DATA__`. Next.js instead ships a loading shell with empty props and `useRouter().isFallback === true`, then the client hydrates by fetching the data URL. The upstream regression test `should rewrite to fallback: true page successfully` in `test/e2e/middleware-rewrites/test/index.test.ts` asserts "Loading..." in the SSR HTML, which vinext could not produce. Thread an `isFallback` signal through `resolvePagesPageData`, `buildPagesNextDataScript`, the Pages Router SSR context, and both the prod (`pages-server-entry.ts`) and dev (`dev-server.ts`) renderers: - `getStaticProps` and `getServerSideProps` are skipped on the fallback render (matches Next.js `render.tsx`'s `if (isSSG && !isFallback)` gate around `getStaticProps`). - Data requests (`/_next/data/<buildId>/<page>.json`) still resolve real props so the client can swap in after the shell ships. - `useRouter().isFallback` reads from the SSR context server-side and from `__NEXT_DATA__.isFallback` client-side — both flip to true on the shell render. The dev-server comment that claimed Next.js dev "also renders fully" was incorrect; Next.js's `pages-handler.ts` invokes `doRender` with `isFallback: true` for dev fallback responses, so dev now serves the shell too. Tests: - Updated `tests/pages-router.test.ts` "SSR renders unlisted path with getStaticPaths fallback: true" — previously documented the deviation; now asserts the loading shell, empty props, and `isFallback: true`. - Added a counterpart test that the data URL for the same unlisted path still returns real props from `getStaticProps`. - Added a production-side regression test mirroring the upstream "should rewrite to fallback: true page successfully" expectation. Refs #1331 * test(pages-page-data): include isFallback: false in normalized render assertion The render branch of `resolvePagesPageData` now returns `isFallback` alongside the other fields (added in the parent commit for the `fallback: true` loading shell). Update the strict-shape `toEqual` in the cache-miss unit test so it matches the new return shape. |
||
|
|
512be7f25c |
fix(middleware): preserve route priority on rewrite target resolution (#1394)
* fix(middleware): preserve route priority on rewrite target resolution When `trailingSlash: true` is set in next.config.js, a request like `/rewrite-1/` was matched against the next.config.js rewrite source `/rewrite-1` literally, including the trailing slash. The match failed, so the request fell through to the dynamic `[id]` route and rendered the dynamic page with `id="rewrite-1"` instead of being rewritten to the static target. Strip a single trailing slash from the incoming pathname before matching against config rewrite/redirect/header sources, matching Next.js's `source + '(/)?'` behavior under `trailingSlash: true`. The source pattern itself is preserved so catch-all (`:param*`, `:param+`) and root (`/`) semantics are unaffected. Refs #1331 * address bonk feedback: locale-static path, comment wording, tighter tests - Extract `stripTrailingSlashForConfigMatch` and call it from `matchRedirect` so the locale-static fast path (Map.get on the pathname) also honors trailing slashes — previously a pure locale-static redirect rule could miss when `trailingSlash: true`. - Clarify the comment to reflect that the strip is unconditional defense-in-depth rather than a faithful port of Next.js's conditional `(/)?` suffix. - Disambiguate the pages-router integration tests by asserting on `id="home"` / `id="ssr"` so the assertion can't pass on the wrong page (both pages render "Hello World"). - Add direct unit tests for `matchConfigPattern` trailing-slash behavior covering exact, :param, regex-group, root, and catch-all cases. * extend trailing-slash normalization to matchHeaders Bonk flagged that matchHeaders runs its own compiled regex path that doesn't go through matchConfigPattern, so a header source like `/api/:path*` would miss `/api/foo/` under `trailingSlash: true`. Normalize once at the top of matchHeaders the same way matchRedirect already does, and add a regression test. |
||
|
|
d504f022b0 |
fix(assets): default assetsDir to _next/static (Next.js parity) (#1411)
* fix(assets): default assetsDir to _next/static (Next.js parity) Closes #1337. Supersedes #1383. vinext's default `assetsDir` was Vite's historical `assets/`, so URL emission (`/_next/static/...` via `resolveAssetUrlPrefix("")`) and on-disk layout (`dist/client/assets/...`) disagreed in the empty-`assetPrefix` case. The `build.assetsDir` and `experimental.renderBuiltUrl` Vite overrides only applied when `assetPrefix` was configured, leaving the no-prefix branch serving from a different path than the URL contract Next.js's client runtime and test harness assert against. This commit flips the default to Next.js's canonical layout: - `resolveAssetsDir("")` returns `_next/static` (was `assets`) - `build.assetsDir` is now set unconditionally from `resolveAssetsDir` - Vite's default `base + assetsDir` composition produces correct URLs in the no-prefix case; `renderBuiltUrl` stays gated on `assetPrefix` because it's only needed for the configured cases - Drops the legacy `/assets/` branches in prod-server (3 sites) and static-file-cache (hard cutover — see PR description for rationale) - Updates `_headers` generation, precompress default, fonts plugin `DEFAULT_ASSETS_DIR` to match the new default With the layout aligned, invalid `_next/static/*` requests naturally return plain-text `404 + "Not Found"` from the static-file layer instead of falling through to the page renderer (which would produce an HTML 404 with bootstrap scripts and CSS). This replaces #1383's parity short-circuit with the natural code path: - Node prod-server (App + Pages branches): missing asset under `resolveAppRouterAssetPath` returns `text/plain; charset=utf-8` 404 - Cloudflare worker entry: `isNextStaticPath` recognises asset-shape after ASSETS-binding misses; returns `notFoundStaticAssetResponse` Mirrors Next.js: packages/next/src/server/lib/router-server.ts. Adds `tests/invalid-static-asset-404.test.ts` (3 App Router + 3 Pages Router cases — no prefix / basePath / assetPrefix) ported from Next.js e2e suites. Updates 11 source files + 12 test files; 914+ tests pass in the touched suites (asset-prefix, app-router, pages-router, deploy, font-google, static-file-cache, serve-static, precompress, features, routing, isr-cache, build-optimization, app-rsc/ssr, standalone, middleware, shims). Ported from Next.js: - test/e2e/invalid-static-asset-404-app/*.test.ts - test/e2e/invalid-static-asset-404-pages/*.test.ts * fix(e2e): update cloudflare-pages-router hydration spec for _next/static * PR #1411 approved. Clean fix, 2 nits. Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> * refactor(assets): derive isHashed checks from ASSET_PREFIX_URL_DIR Replace hardcoded '_next/static' string literals in runtime code paths with the ASSET_PREFIX_URL_DIR constant so the asset URL convention has a single source of truth. - server/prod-server.ts isHashed: pathname.includes(`/${ASSET_PREFIX_URL_DIR}/`) - server/prod-server.ts public-dir guard: startsWith(`/${ASSET_PREFIX_URL_DIR}/`) - server/static-file-cache.ts isHashed: both startsWith + includes derived - plugins/fonts.ts DEFAULT_ASSETS_DIR = ASSET_PREFIX_URL_DIR Behavioural no-op — the constant is '_next/static' so the resolved strings are identical. Centralises the URL contract so future changes to ASSET_PREFIX_URL_DIR (or a refactor that derives it from build config) flow through to every consumer. Doc-comment references to '_next/static' left in place — they describe the canonical value for human readers and are not code paths. --------- Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com> Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> |
||
|
|
576c55d7db |
feat(pages-router): implement _next/data JSON endpoint (#1384)
* feat(pages-router): implement _next/data JSON endpoint
Recognize `/_next/data/<buildId>/<page>.json` in dev and prod request
pipelines. The URL is normalized to the page path BEFORE middleware
runs (matching Next.js' `handleNextDataRequest`), and the server emits
`{ pageProps }` as JSON with `Content-Type: application/json` instead
of the rendered HTML.
Closes #1330
* fix(pages-router): address bigbonk findings on _next/data endpoint
- Delegate JSON envelope construction from the generated entry to the
typed `buildNextDataJsonResponse` helper so the envelope shape lives
in one place (entries stay thin per AGENTS.md).
- Return a JSON 404 from `resolvePagesPageData` when `getStaticPaths`
with `fallback: false` rejects the path for a data request. Without
this, client-side navigations to unlisted slugs got an HTML 404 body
which fails to parse as JSON in the Next.js client router.
- Apply the same JSON-404 gating in the dev server for
`getStaticPaths fallback:false`, `getServerSideProps.notFound`, and
`getStaticProps.notFound` paths.
- Read the dev buildId from the plugin's resolved `nextConfig.buildId`
first so a user-supplied `generateBuildId` is honored, matching the
value embedded into the prod server entry.
- Add coverage for `getStaticProps` data requests and the
`fallback:false` JSON-404 path.
* chore: drop unused NextDataMatch export (knip)
* fix(pages-router): JSON 404 for dev stale buildId; HTML 404 for gSSP/gSP notFound on HTML nav
Follow-ups from bigbonk round 2:
- Dev request pipeline now returns a JSON 404 when `_next/data/<wrong>` is
hit, mirroring the prod-server path. Previously the dev fall-through
served Vite's HTML 404, breaking client routers that try to parse the
body as JSON.
- `resolvePagesPageData` now gates the gSSP/gSP `notFound: true` 404 body
on `isDataReq` so HTML navigations get the HTML 404 page and data
requests get the JSON envelope. Previously both shapes always returned
the JSON body, which surfaced text-content-type mismatches on HTML
navigations through gSSP/gSP `notFound`.
|
||
|
|
2d035d47bd |
fix: await top-level await module imports (#1381)
* fix: make next/document default export a class Next.js's `next/document` default export is `class Document extends React.Component`. vinext's shim exported a function, so user `_document.tsx` files that use the class-based form (the common Next.js idiom: `class MyDocument extends Document`) ended up extending a function. React refuses to call class constructors without `new`, which caused SSR to throw "Class constructor MyDocument cannot be invoked without 'new'" — surfacing in e2e as 500 / empty responses. This is the actual root cause of the "async modules render empty pages" report (issue #1361): the Next.js `test/e2e/async-modules` fixture's `pages/_document.jsx` uses `class extends Document`, and once it crashes the whole page renders empty. Top-level await itself propagates correctly through Rolldown's static-import graph already. Adds a class-based Document shim that mirrors Next.js's signature (static `getInitialProps`, instance `render()`), plus production-build e2e coverage for pages, `_app.tsx`, `getStaticProps`, `getServerSideProps`, API routes, and class-based `_document.tsx`, all using top-level `await`. Closes #1361 Ports from Next.js: test/e2e/async-modules/index.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/async-modules/index.test.ts * fix(next/document): align Document base class with Next.js's typed contract Addresses Bonk review feedback on PR #1381: - Export DocumentContext / DocumentInitialProps types matching Next.js, so subclasses that delegate via `await Document.getInitialProps(ctx)` receive typed objects. The runtime path is still a stub (the Pages Router renderPage/defaultGetInitialProps chain is not wired up), but the signatures pin the contract. - Declare the default Document class export in next-shims.d.ts so strict-mode consumers don't see `any` for `import Document from "next/document"`. - Add a class-extends regression test in tests/document.test.ts — this is the contract that broke in issue #1361 (the original Next.js `pages/_document.jsx` uses `class extends Document`). - Switch the class generic default from `unknown` to `{}` to match Next.js's `class Document<P = {}>`. - Clarify App Router fixture comment: the upstream Next.js async-modules suite is Pages-Router-only, so this is adapted, not ported verbatim. |
||
|
|
bc586788a2 |
fix(middleware): preserve query params on rewrite (#1375)
* fix(middleware): preserve query params on rewrite When middleware (or a `next.config.js` rewrite) sends a request to a new target, vinext was throwing away the original request's query string, breaking `getServerSideProps`/route-handler `query` for any URL of the form `/rewrite-me?key=value`. Match Next.js's behaviour: `Object.assign(parsedUrl.query, rewrittenParsedUrl.query)` — keep the original query first, let the rewrite-target's query win on key conflicts. Applied to: - middleware rewrites (middleware-runtime.ts) - beforeFiles / afterFiles / fallback config rewrites (prod-server.ts, index.ts, deploy.ts) Closes #1342 References: - https://github.com/vercel/next.js/blob/canary/packages/next/src/server/route-modules/route-module.ts (Object.assign merge) - https://github.com/vercel/next.js/blob/canary/test/e2e/edge-pages-support/index.test.ts (rewrite query expectations) * test(query): document URL-encoded space normalization on rewrite merge Addresses Bonk review observations on #1375: - Add test confirming `%20` → `+` normalization through URLSearchParams is intentional and lossless (downstream consumers re-parse). - Drop unreachable `?? ""` fallback in mergeRewriteQuery; `String#split` always yields a string. |
||
|
|
517c2bde19 |
feat(pages-router): expose window.next.router on hydration (#1400)
* feat(pages-router): expose window.next.router on hydration
Promote next/router from a lazy dynamic import inside hydrate() to a
static side-effect import at the top of the generated client entry, so
window.next = { version, router, ... } is published before any test
harness or third-party script reads it.
Without this, the Next.js deploy test suite — which drives client-side
navigations via `browser.eval('next.router.push(...)')` — crashes with
`TypeError: Cannot read properties of undefined (reading 'router')` (~80
test failures across middleware-general, middleware-rewrites,
trailing-slashes, basepath, with-router, and ignore-invalid-popstateevent).
Mirrors Next.js's bootstrap order in
.nextjs-ref/packages/next/src/client/next.ts (line 5–13), which
statically imports the router from './' and sets window.next as a
top-level side effect before initialize/hydrate.
Closes #1329
* feat(router): expose state fields as live getters on Router singleton
window.next.router must expose `pathname`, `route`, `query`, `asPath`,
`basePath`, `locale`, `locales`, `defaultLocale`, `domainLocales`,
`isReady`, `isPreview`, and `isFallback` as live properties — not just
the navigation methods. The Next.js deploy test suite drives navigations
via `browser.eval('next.router.push(...)')` and then asserts on
`browser.eval('next.router.pathname')` to verify the navigation
completed; without the field getters the assertion reads undefined.
Mirrors Next.js's `singletonRouter` shape in
.nextjs-ref/packages/next/src/client/router.ts (lines 32-47), which
uses Object.defineProperty for urlPropertyFields so reads forward to
the active router state.
Strengthens the existing shims.test.ts coverage so future regressions
of the field surface fail at unit-test time.
|
||
|
|
b01fe032fa |
fix(pages-router): avoid getServerSideProps identifier collision in build (#1370)
When a page declared `getServerSideProps` (or `getStaticProps` /
`getStaticPaths`) as a local binding and re-exported it via
`export { getServerSideProps }`, the client-bundle transform overwrote
the `export { ... }` statement with `export const getServerSideProps =
undefined;`. The original local `const getServerSideProps = ...` was
left in place, producing two declarations of the same identifier in
module scope. Rolldown/OXC rejects this with `Identifier
'getServerSideProps' has already been declared`, breaking the entire
production build (and cascading to 48 test failures in Next.js's
`test/e2e/getserversideprops` suite).
Drop the export specifier without emitting a stub, matching Next.js's
`next-ssg-transform` behaviour. The now-unused local declaration becomes
dead code and is tree-shaken later.
Closes #1354
|
||
|
|
9415a238f4 |
feat(config): implement assetPrefix (#1311)
* feat(config): parse and apply next.config assetPrefix at build time Wire `assetPrefix` from next.config through to the Vite build so every emitted JS/CSS/image/static asset URL is prefixed accordingly. Mirrors Next.js semantics: `assetPrefix` controls asset URLs only, independent of `basePath` (which still controls route URLs). * Add `assetPrefix` to NextConfig and ResolvedNextConfig, normalised via a new `normalizeAssetPrefix` helper that trims trailing slashes, accepts absolute URLs (`https://cdn.example.com[/sub]`) and path prefixes (`/custom-asset-prefix`), and rejects non-string / unparseable values. * New `packages/vinext/src/utils/asset-prefix.ts` module collects the shared helpers: `resolveAssetsDir` (on-disk layout), `resolveAssetUrlPrefix` (emitted URL prefix), `assetPrefixPathname` (path portion for runtime matching), and the canonical `_next/static` directory constant. * In the Vite plugin, set `build.assetsDir` so on-disk layout matches the URL (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs). Use `experimental.renderBuiltUrl` to override Vite's default `base + url` URL emission so `assetPrefix` and `basePath` stay independent — routes continue to anchor at `base = basePath + '/'`. * Persist `assetPrefix` in the embedded `vinextConfig` for the Pages Router server and in a new `vinext-runtime-config.json` sibling for the App Router prod server (the RSC entry only embeds `__basePath`). @see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix * feat(server): serve assetPrefix-prefixed assets at runtime Update the production server to resolve incoming asset requests under `<assetPrefix>/_next/static/<filename>` (path-prefix form) and `/_next/static/<filename>` (absolute-URL form) in addition to the historical `/assets/` URLs. Both routers (Pages and App) share a new `resolveAppRouterAssetPath` helper that returns the lookup path on disk or `null` to fall through. * App Router prod server loads `assetPrefix` from the new `vinext-runtime-config.json` sibling written by the Vite plugin. * Pages Router prod server reads `assetPrefix` from the embedded `vinextConfig` and threads it through the existing `stripBasePath` lookup. * StaticFileCache marks any path under `_next/static/` as content-hashed (alongside the existing `assets/` rule) so emitted bundles get the long-lived `immutable` cache headers. Same change in the slow-path branch of `tryServeStatic`. Cloudflare Workers needs no code change: the ASSETS binding serves `<prefix>/_next/static/...` directly from `dist/client/<prefix>/_next/static/...` because the on-disk layout already mirrors the URL. Absolute-URL prefixes are expected to be served by the CDN, so they don't reach the worker. * test(asset-prefix): add unit + e2e build tests for next.config assetPrefix Covers the surface area added in the previous two commits: * Unit tests on the new helpers: `normalizeAssetPrefix` (path vs URL, trailing-slash trimming, validation), `resolveAssetsDir` / `resolveAssetUrlPrefix` (on-disk vs emitted-URL layout), `assetPrefixPathname` (runtime matching), and `resolveAppRouterAssetPath` (request → on-disk lookup translation). * End-to-end build tests that exercise `vinext build` against the `tests/fixtures/app-basic` app patched with each `assetPrefix` shape: - Path prefix (`/custom-asset-prefix`): SSR HTML references `/custom-asset-prefix/_next/static/` URLs, on-disk layout matches, and fetching the bootstrap URL from the prod server returns 200. - Absolute URL (`https://cdn.example.com`): emitted asset URLs are fully qualified; no `/assets/` leaks. - Combined `basePath: '/app'` + `assetPrefix: '/cdn-prefix'`: routes stay under `/app/` while asset URLs live under `/cdn-prefix/` — proving the two configs stay independent. - Unset (default): historical `/assets/` layout is preserved. Ported from Next.js: test/e2e/app-dir/asset-prefix/asset-prefix.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/asset-prefix/asset-prefix.test.ts * test(next-config): add assetPrefix field to makeResolved helper ResolvedNextConfig requires assetPrefix; the test helper was missing it which broke the type check. * fix(config): replace polynomial regex with loop (CodeQL) CodeQL flagged trim().replace(/\/+$/, "") and replace(/^\/+/, "") as polynomial-time backtracking on uncontrolled input. Swap both for explicit while loops with linear-time semantics. Same fix applied to the URL pathname trim in assetPrefixPathname(). * refactor(prod-server): drop dead null fallback; tighten hashed-asset detection - Remove the `?? null` after resolveAppRouterAssetPath; the helper already returns null on miss, so the fallback (and its obsolete comment) were no-ops. - Add explicit startsWith("_next/static/") / startsWith("/_next/static/") prefix checks before the substring fallback in both static-file-cache and prod-server. The substring check stays so assetPrefix-prefixed layouts (e.g. "cdn/_next/static/") are still recognised. * fix(build/precompress): use resolved assetsDir so assetPrefix builds get compressed precompressAssets() hardcoded `<clientDir>/assets/`. When `assetPrefix` is configured the build writes hashed assets to a different subdirectory (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs) and the precompression step walked an empty directory, silently emitting zero .br/.gz/.zst files. Cloudflare Workers builds with assetPrefix would then serve uncompressed assets. Thread the resolved subdirectory through from resolveAssetsDir(assetPrefix) at the plugin call site, default to `"assets"` for backward compat, and add focused tests for both assetPrefix layouts. * refactor: drop redundant isHashed startsWith; harden test cleanup against build failures - prod-server.ts: drop the redundant `pathname.startsWith("/_next/static/")` check; `pathname` always has a leading slash so the existing `includes("/_next/static/")` already covers it. - static-file-cache.ts: keep both `startsWith("_next/static/")` and `includes("/_next/static/")` because `relativePath` has no leading slash — the prefix and substring checks cover different layouts (absolute-URL prefix vs path-prefix). Clarify the comment. - tests/asset-prefix.test.ts: register the tmp-dir cleanup with the shared `cleanups` array synchronously inside `buildFixtureWithConfig`, right after `mkdtempSync`. Previously the cleanup was only pushed after the build returned, so a thrown `createBuilder`/`buildApp` would leak the tmp dir. * refactor(build): embed assetPrefix in App Router entry instead of sidecar JSON Mirrors the existing __basePath embed pattern (see entries/app-rsc-entry.ts) and how Pages Router exposes `vinextConfig.assetPrefix` from its own entry. The runtime-config.json sidecar was unnecessary — the App Router entry template already inlines config values as compile-time constants, so assetPrefix should be inlined the same way. Changes: - Add `assetPrefix` to `AppRouterConfig` in entries/app-rsc-entry.ts. - Emit `export const __assetPrefix = <json>` from the generated RSC entry alongside the existing `__basePath` / `__configHeaders` / `__expireTime` constants. Single-line declaration to stay within the "thin generated entry" guideline. - Thread `nextConfig.assetPrefix` through the generateRscEntry call site in index.ts. - App Router prod-server reads `rscModule.__assetPrefix` (with a string fallback for older builds) instead of `vinext-runtime-config.json`. - Delete the vinext-runtime-config.json write in index.ts. Nothing else in the repo reads this file (verified via rg on packages/ tests/ examples/). Pages Router behaviour is unchanged: it already gets assetPrefix from the embedded `vinextConfig` exported by pages-server-entry.ts. Addresses james-elicx review feedback on #1311. * fix(ssr): emit bootstrap as <script src> via bootstrapModules The App Router SSR pipeline previously passed only `bootstrapScriptContent` to React's renderToReadableStream, producing inline `<script>import("/path/to/entry.js")</script>` tags. The URL was therefore inaccessible to anything that reads `script.attribs.src` — including the Next.js asset-prefix fixture test "bundles should return 200 on served assetPrefix" which iterates script tags looking for a `src` attribute prefixed by the configured assetPrefix. Extract the bootstrap URL from the plugin-rsc-generated content and pass it as `bootstrapModules: [url]` so React emits a real `<script type="module" src="url">` tag. The existing `extractModulePreloadHtml` helper already did the same extraction for the modulepreload <link>, so the URL was already exposed (just renamed `extractBootstrapModuleUrl` and lifted out of the head-injection helper to share between the bootstrap and the preload tags). The inline `bootstrapScriptContent` is no longer needed — it was only carrying the dynamic-import call that bootstrapModules now performs natively. Passing both would load the bootstrap module twice. `bootstrapModules` works in both dev and prod: in dev, plugin-rsc emits a Vite dev-server URL (e.g. `/@id/__x00__virtual:vinext-app-browser-entry`) that the browser fetches as a module from the dev server; in prod it's the hashed bundle URL, optionally prefixed by `assetPrefix`. Mirrors Next.js's app-render path which combines both `bootstrapScriptContent` (for session state) and `bootstrapScripts` (for the actual chunk URL). See .nextjs-ref/packages/next/src/server/app-render/required-scripts.tsx. We use `bootstrapModules` instead of `bootstrapScripts` because vinext's chunks are native ES modules (Vite output) — a `type="module"` tag is the correct loader. Tests updated where they asserted on the old `import("URL")` shape: tests/asset-prefix.test.ts, tests/app-router.test.ts, tests/e2e/cloudflare-workers/navigation.spec.ts. Verified all related suites pass locally (asset-prefix, app-router, script-nonce, script-preload, use-client-page-pathname, document, prerender, ISR, fetch-cache, entry-templates, precompress). * fix(build): use resolveAssetsDir for Pages Router client entry fallback scan The Pages Router Cloudflare Workers post-build step scans the on-disk assets directory for the client entry chunk when the SSR manifest lookup doesn't surface one. The directory path was hardcoded to `<clientDir>/assets/`, which is the Vite default — but when `assetPrefix` is configured, the chunk lives under `<prefix>/_next/static/` (path-prefix) or `_next/static/` (absolute-URL prefix). The fallback would silently miss the entry and the worker would ship without its client bootstrap reference. Use the shared `resolveAssetsDir(assetPrefix)` helper that already drives `build.assetsDir` and the prod-server lookup, so this scan works for every supported asset layout. Addresses round-4 reviewer feedback on #1311 (low-priority follow-up item — only fires when the SSR manifest lookup fails). * feat(config): mirror Next.js basePath → assetPrefix fallback Next.js auto-sets `assetPrefix = basePath` when `basePath` is configured and `assetPrefix` is left unset (packages/next/src/server/config.ts:528-531). Without this, an app deployed under a basePath serves its routes correctly but emits its assets from `<basePath>/assets/...` (Vite's default `base + assetsDir` composition) rather than from the Next.js- canonical `<basePath>/_next/static/...`. Add the same fallback in resolveNextConfig, gated by Next.js's own conditions: - basePath !== "" (skip when basePath is unset) - basePath !== "/" (Next.js rejects this earlier; we mirror the guard so we never produce assetPrefix === "/") - assetPrefix === "" (user did not opt out) The fallback feeds the rest of the asset-prefix pipeline (Vite's `build.assetsDir`, `experimental.renderBuiltUrl`, and the prod-server URL→file resolver) so no other code paths need to change. Tests: - tests/next-config.test.ts: 5 unit tests for the four edge cases of the fallback condition. - tests/asset-prefix.test.ts: new end-to-end test that builds with `basePath: "/app"` (no assetPrefix), starts the prod-server, and asserts the bootstrap <script src> lands at /app/_next/static/... (and that the legacy /app/assets/... layout is gone). - tests/pages-router.test.ts: updated the "preserves basePath on backfilled SSR manifest entries" test to expect the new /docs/_next/static/ layout (was /docs/assets/). The manifest assertion is relaxed to startsWith("docs/") because Vite's raw SSR manifest now emits some duplicate-prefixed entries ("docs/docs/_next/static/...") alongside the backfilled ones; both forms anchor under basePath so the prod-server's URL→file lookup remains correct — and the user-visible HTML assertions in the same test are the source of truth. Addresses round-4 reviewer feedback on #1311. * fix(server): Pages Router must not strip basePath before asset lookup The Pages Router prod-server stripped `basePath` from the request pathname BEFORE handing it to `resolveAppRouterAssetPath`, which broke the asset lookup whenever the assetPrefix path-prefix branch was active with a path that happened to equal basePath. Concrete failure: with the round-4 commit `bac8425` mirroring Next.js's `basePath → assetPrefix` fallback, an app configured with `basePath: "/docs"` (and no explicit `assetPrefix`) now resolves `assetPrefix = "/docs"`. Emitted asset URLs are `/docs/_next/static/…` (correct). But the Pages Router branch did: const staticLookupPath = stripBasePath(pathname, basePath); // "/docs/_next/static/x.js" → "/_next/static/x.js" resolveAppRouterAssetPath(staticLookupPath, "/docs", "/docs"); // path-prefix branch needs pathname to start with "/docs/" → null → 404 The App Router branch already passes the un-stripped `pathname` to the same helper (see line ~1140). Match that behaviour. `staticLookupPath` is still computed and used downstream by non-asset paths (image-optimization endpoint, page routing), where the basePath-stripped form is correct. Test coverage: - tests/pages-router.test.ts: extended the existing "preserves basePath on backfilled SSR manifest entries" test to FETCH every emitted asset URL and assert HTTP 200 (previously it only inspected HTML for the URL string). This is the missing assertion that would have caught the bug the first time. - tests/asset-prefix.test.ts: added a parallel Pages Router e2e test that builds an inline pages/ fixture with `basePath: "/docs"`, starts the prod-server, collects every script/link URL from the SSR HTML, and asserts every URL serves a 200 + correct content-type. Confirmed to fail on `bac8425` before this fix and pass after. Addresses round-5 reviewer feedback on #1311. * refactor(ssr): accept either quote style in bootstrap URL regex plugin-rsc currently emits the bootstrap as `import("<url>")` with double quotes, but the contract isn't documented and a future version could switch to single quotes. Tighten the URL extraction regex to `import(["'])([^"']+)\1` so either form works. Addresses non-blocking round-5 review feedback on #1311. * docs(ssr): note CSP implications of bootstrapModules switch The round-3 commit `3a2c886` switched the App Router SSR bootstrap from inline `bootstrapScriptContent` (`<script>import(…)</script>`) to React's `bootstrapModules` option (`<script type="module" src=…>`). That has user-visible CSP consequences worth documenting at the call site so app authors hit them in code search rather than at runtime: - Apps no longer need `script-src 'unsafe-inline'` for the bootstrap (improvement — inline imports previously required it). - Apps that restrict script sources need `'self'` for same-origin, or the CDN origin when assetPrefix is an absolute URL. - React still threads nonce through to the emitted <script> tag, so nonce-based CSP (`script-src 'nonce-…' 'strict-dynamic'`) keeps working unchanged. Addresses non-blocking round-5 review feedback on #1311. |
||
|
|
aed79d46b0 |
test: regression tests for issue #1196 (catch-all route params with basePath + rewrites + middleware) (#1291)
* test: add regression tests for issue #1196 (catch-all route params with basePath + rewrites + middleware) Adds integration and unit tests to verify that catch-all route params are preserved when basePath, rewrites, and middleware are combined. - Integration test in pages-router.test.ts: checks server-rendered __NEXT_DATA__ contains correct page and query for a catch-all route. - Unit test in shims.test.ts: verifies client-side router.query stays intact for a catch-all page under basePath. Both tests are ported from the Next.js test suite and target the regression described in GitHub issue #1196. * test: address review feedback on #1196 regression tests - Use {.*?} regex for consistency with other __NEXT_DATA__ extractions - Drop unused { appRouter: false } option (vinext auto-detects) - Rename shims test to describe what it actually verifies (initial state) - Move vi.resetModules() after window restore for clean teardown ordering * test: add real client-nav test for #1196 catch-all router.query Adds a Router.push() test that mocks fetch and exercises the actual client-side navigation code path: performNavigation → pushState → navigateClient → fetch. The mocked fetch handler captures router state mid-navigation via a Probe component, then asserts router.query.path reflects the destination URL segments after basePath is stripped. This addresses the bonk review feedback that the previous tests only verified initial hydration state and didn't exercise client navigation. The new test also uses the previously-unused buildNavHtml `query` parameter, addressing the dead-signature concern. vinext's navigateClient doesn't construct _next/data/... URLs the way Next.js does, so it doesn't have the architectural pattern that causes #1196 in Next.js. But the route-param extraction path that ultimately produces router.query must still strip basePath from the resolved pathname before matching the catch-all pattern — and that's what this test verifies. --------- Co-authored-by: opencode <noreply@opencode.ai> |
||
|
|
329b89e3c3 |
fix(navigation): populate useParams/useSearchParams under Pages Router (#1303)
Under the Pages Router, hooks from `next/navigation` returned empty
objects:
expect(pagesParamsJSON).toEqual({ slug: 'foobar' }) // got {}
expect(pagesSearchparamsJSON).toEqual({ q: 'pages' }) // got {}
Next.js treats `next/navigation` as a cross-router compat shim. The
Pages Router server (render.tsx) and client (client/index.tsx) wrap
each page with SearchParamsContext / PathParamsContext / PathnameContext
providers populated from the router via `adaptForSearchParams` and
`adaptForPathParams` in
.nextjs-ref/packages/next/src/shared/lib/router/adapters.tsx.
vinext drives those hooks from a module-level navigation context that is
only set for App Router renders. For Pages Router renders the context is
null, so useParams / useSearchParams / usePathname fell through to empty
fallbacks.
This change adds a Pages Router compat source published by next/router
through a Symbol.for global handle. next/navigation reads that handle
when the App Router context is null and derives:
- pathname from window.location / SSR asPath
- searchParams from the URL query string only (not merged route params)
- params from the route pattern matched against the resolved path
The publisher is exposed via globalThis[Symbol.for(...)] rather than a
direct module import so navigation.ts does not become a transitive
dependency of every next/router consumer (which would pull in the
window.history.pushState patch at module-load time and break unit tests
that only need next/router).
Ports the behaviour covered by these Next.js tests:
- test/e2e/app-dir/params-hooks-compat/index.test.ts
- test/e2e/app-dir/use-params/use-params.test.ts ("should work on pages
router")
Adds vitest coverage at tests/pages-router.test.ts ("next/navigation
useParams returns only dynamic route params under Pages Router" and
"next/navigation useSearchParams returns only query string under
Pages Router") via a new fixture page at
tests/fixtures/pages-basic/pages/nav-compat/[slug].tsx that mirrors
.nextjs-ref/test/e2e/app-dir/params-hooks-compat/shared/params-component.js.
|
||
|
|
4aa7663084 |
fix(app-router): hard navigate stale RSC build payloads (#1178)
* fix(app-router): hard navigate stale RSC build payloads App Router soft navigation currently accepts any valid RSC payload after deploy. That lets stale tabs decode RSC responses produced by a different build, which can break client navigation when the server and browser artifacts no longer match. The missing invariant was that every RSC response consumed by the browser must identify the build that produced it, and the browser must reject payloads from a different build before handing them to React. Emit a vinext-owned build ID header on App Router RSC responses, preserve it in prefetched and visited response snapshots, and hard navigate when a response is missing or mismatches the current client build ID. * chore: fix e2e fixture hook lint suppression * chore: avoid hook-like fixture callback name * fix(app-router): preserve build IDs for cached RSC payloads * fix(app-router): use public RSC compatibility IDs |
||
|
|
07be32258f |
fix(router): preserve filesystem routes before afterFiles rewrites (#1166)
* fix(router): preserve filesystem routes before afterFiles rewrites afterFiles rewrites were evaluated before App and Pages filesystem route matches in several runtime paths. That let a rewrite override an existing non-dynamic page, which diverges from Next.js route ordering. The fix checks the page/app match first and only applies afterFiles rewrites when no non-dynamic route wins, while still allowing afterFiles to run before dynamic routes. Regression coverage exercises App RSC handler, Pages dev/prod, and generated Worker wiring. * test(router): align chained afterFiles rewrite expectations The chained rewrite fixture expected an afterFiles rewrite to override a concrete /intermediate page. That is the route-ordering behavior this branch fixes. Update the fixture to cover both intended contracts: middleware can chain into afterFiles when no page file wins, and concrete page files are not overridden by afterFiles rewrites. * refactor(router): remove route-ordering type assertions Validate generated Pages route metadata at the boundary instead of asserting its shape, and make the app handler test fixture route matching explicit. * test(router): cover afterFiles order in Pages Worker Add a built Cloudflare Pages Router regression where a concrete page route must win before an afterFiles rewrite. Align custom Pages Worker entries with the generated worker route-match gate. |
||
|
|
3adf2da4c4 |
chore(lint): enable stricter promise and error rules (#1148)
* chore(lint): enable stricter promise and error rules The lint config did not enforce several promise, exhaustive switch, template expression, self-import, and error-construction checks on source files. That let void callback promise boundaries and empty Error fixtures slip through review. Enable the rules for vinext source files, apply the useful test subset, and make the existing async callback boundaries explicit. Also remove the redundant unknown union and close response bodies in the affected Pages Router tests so server teardown remains deterministic. * chore(lint): require exhaustive test switches Test files already enforce strict async callback and error construction checks, but exhaustive switch checking was source-only. Enable switch exhaustiveness for tests and make the Pages Router response decoder handle the undefined encoding case explicitly. Tighten the allowedDevOrigins suite cleanup so the temporary dev server closes deterministically. |
||
|
|
0e62dc24c7 | Prefer Pages Router route params over same-key query params (#1119) | ||
|
|
634808bea4 |
perf(pages): cache ISR misses from streamed render (#1068)
* perf(pages): cache ISR misses from streamed render Pages Router ISR cache misses currently render the page once for the live response and then render a second React tree to produce cache HTML. That doubles userland render, data reads, suspense work, and render-scoped side effects on every cold cache fill. The cache writer now tees the already-rendered body stream, records the cache branch with streaming UTF-8 decoding, and writes the same shell and body that the client receives. Stream failures report through the Pages Router render error path and never persist partial HTML. Tests cover the single-render cache fill, split UTF-8 chunks, failed stream writes, and the Pages Router fixture state contract. * fix(pages): keep ISR cache error reporting non-fatal * chore: rerun CI |