* feat(server): add waitForAllReady forkpoint for prerender/ISR parity
Mirrors Next.js PR #93376: adds a waitForAllReady option that defers
emitting the HTML stream until React's onAllReady fires, ensuring the
full tree (including resolved Suspense boundaries) is present before
serialization.
Changes:
- app-ssr-entry.ts: accept waitForAllReady and await stream.allReady
- app-page-stream.ts: thread waitForAllReady through the pipeline
- app-page-render.ts: pass waitForAllReady: isPrerender to the HTML render
Closes#1011
* test(app-page-stream): use toHaveBeenCalledWith for waitForAllReady forwarding
Replaces the positional-tuple type cast with expect.objectContaining,
which reads more clearly and stays robust if handleSsr's signature
gains new positional args.
---------
Co-authored-by: James <james@eli.cx>
## Summary
- Skip static generation context setup for draft mode requests in app-page-dispatch.ts, matching Next.js behavior where draft mode opts into dynamic HTML rendering.
- Port test fixture and streaming test from Next.js PR #93417 to verify draft-mode pages stream their loading.tsx / Suspense fallbacks.
## Test plan
- Added streaming test to tests/nextjs-compat/draft-mode.test.ts
- Added fixture pages at tests/fixtures/app-basic/app/nextjs-compat/draftmode/streaming/
- Verified draft-mode requests show Suspense fallback text in HTML response
Fixes#1009
* 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.
* fix(fetch-cache): dedupe identical render fetches
Uncached and no-store fetches currently bypass the persistent fetch cache by calling the original fetch directly. That diverges from Next.js, where the patched fetch wraps the original fetch in a request-scoped dedupe layer before persistent cache policy runs, so repeated GET and HEAD fetches during a render share one network response.
Add request-scoped fetch dedupe state to the fetch cache context and route network misses and cache-bypass fetches through it. The dedupe key follows the Next.js method, header, mode, redirect, credentials, referrer, referrerPolicy, and integrity semantics while still opting out for abort signals, keepalive, and side-effecting methods.
Update fetch-cache coverage for uncached render dedupe, independent response bodies, request-scope isolation, trace header exclusion, and the changed no-store/no-cache interaction.
* fix(fetch-cache): give each cloned dedupe response its own Headers
Pass `new Headers(response.headers)` per clone instead of sharing the same
Headers object across both branches and the dedupe entry. Matches Next.js'
`cloneResponse` and avoids a future-correctness trap if any code path mutates
response headers post-fetch.
Also drops two redundant `runWithFetchDedupe` calls inside `dispatchAppPage`
(the ISR revalidation render path and the intercept stream render path) — both
sites are inline anonymous functions that already inherit the dedupe scope from
the outer `dispatchAppPage` / `runAppPageRevalidationContext` wrap, so the
inner calls were no-ops and just obscured that inheritance.
* test(fetch-cache): cover Request input dedupe + always clone bodyless responses
Always construct fresh Response objects in cloneDedupeResponse, including the
bodyless path that previously returned the same `response` reference for both
tuple slots. Mirrors Next.js' cloneResponse and avoids any "disturbed response"
surprise if a runtime tracks consumption state on the shared reference. Factor
the per-clone construction into buildDedupeClone() so bodied and bodyless
clones share the headers-copy + url-restore + finalizer-register treatment.
Adds a comment near entry.response assignment noting that the unconsumed tee
branch is bounded by the render scope: when runWithFetchDedupe exits, the
dedupe map becomes unreachable and the FinalizationRegistry cancels any
still-unconsumed branch.
Also adds two tests covering the Request-object input path of
createFetchDedupeCandidate — one verifying dedupe applies for identical
Request inputs, one verifying differing non-trace headers on Request inputs
prevent dedupe.
* fix(fetch-cache): drop failed dedupe entries so later callers can retry
When the original fetch rejects, the dedupe entry stayed in the map with
response: null. Subsequent callers within the same render scope chained on
the rejected promise — propagation was correct, but they could never retry,
because the failed entry blocked fresh attempts for the rest of the scope.
React.cache() (which Next.js uses) avoids this because each call site
naturally retries on failure.
Splice the entry out of the URL bucket on rejection so a later fetch to the
same URL within the same render scope creates a fresh entry and re-issues
the upstream request.
Also align the fetch-dedupe-metadata fixture with fetch-dedupe-isr-metadata
by replacing the `as CountBody` cast with a runtime assertCountBody check.
* docs(fetch-cache): document scope inheritance + harden ISR test stabilization
Rename `dispatchAppPageWithDedupe` to `dispatchAppPageInner` so the name
reflects that it runs *inside* the dedupe scope rather than activating it.
Add comments at the four `runWithFetchDedupe` / `renderToReadableStream`
sites explaining how each one relates to the surrounding dedupe scope:
- app-page-render and app-page-boundary: defensive wrap, no-op under
dispatch; standalone callers must keep an outer scope alive across async
stream consumption since `runWithFetchDedupe` of a synchronous fn only
covers the synchronous portion.
- ISR revalidation render and intercept render in dispatch: explicitly note
why no inner wrap is needed (the outer revalidation context / dispatch
wrapper already activated dedupe).
- runWithFetchDedupe doc: document the ALS-scope-vs-async-consumption
caveat that ties this together.
Replace the fixed 200ms post-condition sleep in the ISR background dedupe
test with a 500ms count-stabilization poll. A stray third upstream fetch
(which would betray dedupe leaking across the metadata + page render
boundary) is now caught regardless of when it fires.
---------
Co-authored-by: James <james@eli.cx>
* fix(app-router): schedule link navigations as transitions
App Router link clicks could commit cross-route payloads synchronously because the browser entry passed route path equality into the navigation controller as a transition toggle. That diverged from Next.js, where app-router push and replace enter React.startTransition for every app navigation.
Remove the transition override from normal navigation payload commits, keep synchronous commits private for HMR and same-URL server-action handling, and start RSC Link navigations through the same programmatic transition path used by useRouter().
* test(app-router): cover link transition scheduling behavior
* docs(navigation): drop stale isSameRoute references in comments
The isSameRoute concept was removed from app-browser-entry.ts in this PR;
update two comments in navigation.ts that still referenced it.
---------
Co-authored-by: James <james@eli.cx>
* fix(cache): scope use cache keys by deployment id
Shared "use cache" entries only used the vinext build ID as their cache-key seed. That diverges from the upstream Next.js change for cacheHandlers and can let a Cloudflare Worker deployment reuse entries from another deployment when the deployment identity changes independently of the build fallback.
The cache runtime now resolves a deployment-aware key seed at execution time, with request-scoped Worker version metadata or NEXT_DEPLOYMENT_ID taking precedence over the build ID. Generated Wrangler config includes the CF_VERSION_METADATA binding, and the App Router Worker entry threads it into the runtime without process-global request races.
Tests cover deployment ID precedence, concurrent deployment scope isolation, build ID fallback, and generated Worker config wiring.
* fix(config): resolve deployment id for cache seeds
Deployment-aware "use cache" keys had runtime Worker and NEXT_DEPLOYMENT_ID support, but vinext did not resolve the Next.js deploymentId config field. That left config-defined deployment identity out of the build-time cache seed.
Resolve deploymentId from next.config.js before NEXT_DEPLOYMENT_ID, validate the same string shape Next.js documents, and feed the resolved value into the __VINEXT_DEPLOYMENT_ID define. The process-wide cache-runtime setter is also documented as a fallback rather than the per-request API.
Tests cover config precedence, env fallback, empty config values, and invalid deploymentId shapes.
* refactor(cache): carry deployment id through request context
The Worker version metadata path still needs request-scoped state, but cache-runtime should not own a cache-specific deployment ALS or public-looking setter.
Move runtime deployment ID storage into the unified request-context layer. The Worker boundary now writes deploymentId into request runtime context, cache-runtime reads that generic request metadata before falling back to inlined deployment/build IDs, and the tests cover direct runtime scopes plus inheritance into unified request contexts.
* refactor(cache): reuse request context for deployment id
* test(app-elements): expect artifact compatibility metadata
* refactor(cache): inline deployment id seed
* chore: rerun ci
Tags with non-ASCII characters (Hebrew, Arabic, CJK, emoji) can crash ISR
responses with ERR_INVALID_CHAR when written to x-next-cache-tags, and
silently break invalidation when storage form diverges from the form
revalidateTag/revalidatePath produce.
Add encodeCacheTag/encodeCacheTags helper (matches behavior of
vercel/next.js#93601) and apply it at every public boundary: cacheTag,
revalidateTag, revalidatePath, updateTag, unstable_cache options.tags,
fetch next.tags, and the path-derived _N_T_ tag builders.
Closes#1138
* feat(cache): export stable `io` from next/cache, deprecate `unstable_io`
Next.js stabilized `unstable_io` as `io` in vercel/next.js#93621 without a
deprecation alias. vinext shipped the unstable name longer than upstream
canaries (since #805), so keep `unstable_io` as a transitional alias that
emits a one-time deprecation warning.
Closes#1139
* chore: apply formatter
* fix(app-router): emit action revalidation headers
Server Actions that call revalidatePath, revalidateTag, updateTag, refresh, or mutate cookies currently return RSC responses without x-action-revalidated. The client router therefore keeps stale cached payloads after an action invalidates data.
The action execution path assumed cache invalidation only needed to touch the server-side cache handler. Record action-scoped revalidation in the cache shim, consume it when building action responses, and preserve Next.js' static-and-dynamic versus dynamic-only header values.
Tests cover normal action rerenders, redirecting actions, and refresh dynamic-only invalidation.
* fix(app-router): match profiled tag action revalidation
Server Actions using revalidateTag with a cacheLife profile should not trigger x-action-revalidated. Next.js treats profiled tag revalidation as stale-while-revalidate and does not force immediate client router cache invalidation.
Only record action revalidation for the no-profile revalidateTag form, keep the action marker cleanup symmetric on error, and cover the profiled tag case in server action tests.
* fix(app-router): notify actions for immediate tag expiry
Server Actions that call revalidateTag with an inline profile of expire zero immediately expire server cache data, but the client router still needs x-action-revalidated to drop stale RSC payloads.
Resolve revalidateTag profiles before deciding whether to mark action revalidation, matching Next.js' no-profile or expire-zero condition. Also clear the action marker on progressive action errors for symmetry with the RSC action path.
* fix(app-router): notify on revalidateTag with unknown profile
Address review feedback:
- revalidateTag with an unrecognized profile string resolves to no
durations and the handler treats it as a full invalidation, but the
client router was not being notified. Mark action revalidation when
durations is missing too.
- Document the cookie override path in resolveActionRevalidationKind so
future readers see why it bypasses the max-escalation pattern used in
markActionRevalidation.
---------
Co-authored-by: James <james@eli.cx>
* perf(prerender): reuse embedded RSC payload
* fix(prerender): address RSC extraction review
* fix(test): include artifact compatibility metadata
* fix(prerender): document RSC extraction invariants
* refactor(prerender): simplify RSC extractor and remove dead fallback
Cleanups on top of #1097:
1. Drop the legacy `__VINEXT_RSC__` extractor. The browser entry still
recognizes that format for in-the-wild cached HTML, but prerender
parses HTML it just emitted in this build, where the writer always
emits the chunked form. The legacy branch is unreachable here.
2. Remove the silent fallback to a second `RSC: 1` invocation. The
writer is in-tree, so a missing-marker case is a vinext-internal
regression we want to surface loudly rather than mask by
double-rendering. `extractRscPayloadFromPrerenderedHtml` now
returns `string` and throws on any anomaly.
3. Replace the manual JSON-string tokenizer (parseEmbeddedJsonString +
assertOnlyTrailingSemicolon + isWhitespace, ~80 lines) with a
~12-line slice + JSON.parse. Relies on the same safeJsonStringify
invariant the original parser relied on; tampering with trailing
code now surfaces as 'invalid chunk JSON' instead of a separate
diagnostic, which is fine for an internal protocol.
4. Remove the obsolete TODO marker ("Extract RSC payload from the
first response instead of invoking the handler twice") — the PR
resolves it.
7. Tighten the chunk-presence assertion: empty HTML or HTML with only
the done marker now throw 'no chunk scripts found in HTML' instead
of returning the fallback sentinel.
Test updates mirror these:
- Drop the two legacy-format tests.
- Drop the "falls back to a separate RSC request" integration test
(the fallback no longer exists).
- Update error-message assertions where the diagnostic text changed.
* fix(prerender): fall back to second invocation for non-render HTML responses
The previous cleanup made `extractRscPayloadFromPrerenderedHtml` throw
on any HTML lacking RSC embed chunks. That's wrong for one legitimate
case: middleware that short-circuits the App Router pipeline with a
custom 200 HTML response — that HTML never went through
createRscEmbedTransform, so it has no chunks and no done marker.
Distinguish two cases at the extractor level:
- No chunks AND no done marker → middleware short-circuit. Return null
so the caller falls back to a second invocation with `RSC: 1`.
- Done marker present without chunks → real bug, throw.
- Chunks present without done marker → real bug, throw.
- Malformed/tampered chunk JSON → real bug, throw.
The driver checks for null and recovers by issuing a second handler
invocation with RSC headers, writing whatever that returns as the
.rsc file.
Adds an integration test alongside the existing dedup test:
- Middleware short-circuit: server returns 200 HTML without chunks for
page requests, valid Flight for RSC: 1 → driver writes the fallback
payload as .rsc, with exactly one of each request kind.
* fix(prerender): fail non-ok RSC fallback responses
---------
Co-authored-by: James <james@eli.cx>
* fix: add `shell: true` on Windows for execFileSync calls
On Windows, `execFileSync` cannot execute `.bin/wrangler` (a shell
script) or package manager commands like `npm`/`npx` (which are `.cmd`
wrappers) without enabling the shell. This causes `ENOENT` errors
during `vinext deploy`, `vinext build`, and `vinext lint`.
Fix by adding `shell: process.platform === "win32"` to all
`execFileSync` options. This only enables the shell on Windows where
it's required, preserving the no-shell-injection guarantee on other
platforms.
Fixes `spawnSync ENOENT` on Windows for:
- `runWranglerDeploy` (deploy.ts)
- `installDeps` (deploy.ts)
- React upgrade in `deploy()` (deploy.ts)
- React upgrade in `buildApp()` (cli.ts)
- Lint commands in `lint()` (cli.ts)
* style: break long execFileSync lines to fit printWidth: 100
* fix: use ExecFileSyncOptions for execFileSync opts type
ExecSyncOptions.shell is typed as `string | undefined`, while
ExecFileSyncOptions.shell is `boolean | string | undefined`. The
explicit annotation rejected the new `shell: process.platform === "win32"`
boolean. Switching to the matching options type resolves the TS2322
error reported by `vp check` in CI.
* docs: update execFileSync comment for cross-platform shell behavior
Reflects shell: true on win32 — the array form still prevents trivial
injection, but the older comment implied shell-free execution everywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Middleware could consume the same ReadableStream that downstream App Router handling later needed to read. This broke POST route handlers when middleware inspected request.json() and then returned NextResponse.next().
The root cause was constructing middleware NextRequest instances from the caller-owned Request body. Clone the request body branch before wrapping or pathname-normalizing middleware requests, and cover both the constructor and middleware pipeline behavior with regression tests.
* fix(app-router): read indefinite app page cache entries
App pages with revalidate=false currently normalize to Infinity, but the app-page cache read gate excludes Infinity. Pre-rendered static app pages can be seeded into the ISR cache and still miss that cache on every production request.
The cache read policy now treats positive Infinity as read-eligible while preserving force-dynamic, production, RSC, and nonce guards. Cached indefinite responses are normalized to the same static one-year Cache-Control header used by fresh revalidate=false responses, so cache hits do not emit s-maxage=Infinity.
Tests cover the dispatch cache hit path and the shared cached Cache-Control helper.
* chore: rerun ci
* chore: rerun ci
The create-next-app job was failing with ERR_PNPM_UNEXPECTED_STORE.
create-next-app's bundled pnpm (10.x) installs node_modules linked to
the v10 store; vp's bundled pnpm recently bumped to v11, which then
refuses to add packages to the v10-linked node_modules.
Anchor the scaffolded project to vinext's own pinned pnpm version
(read from root package.json's packageManager field) and invoke that
pnpm directly via npx for the install step. This:
- keeps store layouts aligned between create-next-app's install and
the subsequent vinext add
- stays in sync with future pnpm bumps without further CI edits
- bypasses vp's bundled pnpm only for this layering step; the rest of
the job (vp exec, vp dlx) is unchanged
Middleware request-header overrides could rebuild forwarded external rewrite requests from only the override set. That drops cookie and authorization when middleware reports a partial override, so proxied upstream requests lose credentials.
Keep Next.js-compatible deletion semantics by default, and make credential preservation an explicit opt-in for external proxy boundaries. Cover both partial preservation and explicit credential deletion.
App Page ISR could serve a cached production response to a request carrying a valid __prerender_bypass cookie. That violates Next.js draft-mode semantics because preview users must render dynamically and must not populate shared page cache entries.
The dispatch layer now identifies draft-mode requests with the same cookie parser and secret used by draftMode(), skips App Page cache reads, and passes that state into render response policy. HTML and RSC render paths now treat draft mode as no-store and avoid cache capture/write work.
Tests cover valid draft cookies bypassing cached HTML, arbitrary cookie values preserving cache hits, and response-policy no-store behavior.
RSC fetch actions that threw notFound() or another HTTP access fallback still returned a Flight payload with status 200. That diverged from Next.js, which sets the response status to the access fallback status before generating Flight for fetch actions.
The server action path now extracts the fallback status while preserving the action error in the Flight return value. The regression test covers legacy NEXT_NOT_FOUND, modern NEXT_HTTP_ERROR_FALLBACK;404, and another fallback status.
* feat(router): expose RouteManifest semantic facts
RouteManifest only exposed minimal route, layout, template, and slot lookup maps. That left later #726 planner and cache-proof work without stable graph-owned facts for slot ownership, slot defaults, template reset scope, or boundary outcomes.
The graph builder now derives declaration-level slot facts, per-route slot bindings, template remount facts, and layout-boundary outcome facts from existing App Router metadata. The manifest graphVersion includes the new maps so downstream compatibility checks can detect semantic topology changes.
Tests cover route groups, slots, defaults, templates, and boundaries in one fixture.
* test(router): align CI fixtures with graph facts
Full CI checks more App Router fixtures than the targeted RouteManifest test run. The new ParallelSlot declaration fields must be present in hand-built fixtures, otherwise typecheck fails even though production graph scanning populates them.
The app browser trace tests also still asserted the stale reason at entry zero, but no-commit transaction tracing is prepended before the reason entry. Update those assertions to inspect the stale reason entry without changing runtime behavior.
* fix(app-router): guard repeated hard navigation targets
Root-boundary changes currently trigger a hard navigation without remembering the target. If an old client keeps receiving an incompatible new-server payload for the same URL, the recovery path can re-enter the same hard navigation instead of stopping the loop.
Persist the attempted hard-navigation target in sessionStorage, block a repeated same-target attempt once the browser is already on that URL, and clear the guard after a successful initial RSC bootstrap. Add old-client/new-server future compatibility coverage so unknown metadata remains a render-fresh fallback, not reuse proof.
* test(app-router): cover cross-page hard navigation guard
Bonk noted that the hard-navigation guard allows cross-URL recovery even when sessionStorage cannot persist the guard, and that same-URL server-action hard navigation intentionally ignores the guard result.
Document those invariants in the controller and add coverage proving a stored guard target does not block a hard navigation when the browser is currently on a different URL.
NextResponse.next({ status }) currently continues routing but drops the middleware response status. That diverges from Next.js middleware handling and makes successful route rendering mask middleware-selected statuses such as 404.
The middleware runtime now exposes a status override for continue responses, and App Router plus Pages Router dev/prod consumers feed that value into their existing middleware response contexts. Rewrite status behavior remains covered by the same path.
Tests cover the middleware runtime contract, App Router context propagation, and a Pages Router dev integration case with a real matched page.
Stale App Router RSC cache entries currently schedule background regeneration with only the clean pathname. That violates the cache-key invariant because RSC app-page entries include the mounted slot variant, so concurrent stale variants for one pathname can collapse into one pending regeneration.
HTML ISR cache writes also read late dynamic usage from the cache branch after the client branch can clear request context. When dynamic APIs are discovered while streaming, the cache writer can miss that signal and persist personalized output.
Use the actual app-page ISR cache key as the regeneration dedup key, and snapshot late dynamic usage immediately before request context cleanup so the HTML cache writer can still skip writes after stream teeing. Add focused regression coverage for slot-specific stale regeneration and pre-cleanup dynamic usage.
* fix(config): exclude React from ssr optimizeDeps when ssr.external: true
When users set `ssr.external: true` in vite.config.ts (the only practical
way to keep CJS-quirky packages like pg, protobufjs, keyv,
@launchdarkly/js-server-sdk-common, @opentelemetry/otlp-transformer
working in dev), `'use client'` components calling React.useContext from
external packages (nuqs, next-themes, launchdarkly-react, etc.) crashed
in dev with `Cannot read properties of null (reading 'useContext')`.
Root cause: @vitejs/plugin-rsc populates
environments.ssr.optimizeDeps.include with React entries via
crawlFrameworkPkgs. With external: true, the SSR env loads React via
Node's resolver, but Vite still pre-bundles a duplicate React copy into
deps_ssr/ because optimizeDeps.include overrides external: true.
react-dom-server.edge then sets the dispatcher on its bundled React
while externalized callers (vinext's runtime) see a different React,
leaving React.H null and crashing every hook call from a 'use client'
module SSR'd through it.
Fix: when userSsrExternal === true, add React entries to
environments.ssr.optimizeDeps.exclude so plugin-rsc's auto-includes
cannot pre-bundle a duplicate copy. The renderer and the runtime then
share one Node-loaded React.
The RSC env is unaffected (it still pre-bundles React with the
react-server condition), and the default mode (`noExternal: true`) is
unaffected since external: true gating the strip.
Closes#1103
* fix(config): also skip top-level ssr.noExternal: true when ssr.external: true
The previous fix only stripped React entries from
environments.ssr.optimizeDeps.exclude, but Vite uses top-level `ssr.*`
as defaults for `environments.ssr.*`. With vinext setting top-level
`ssr.noExternal: true` unconditionally (when not Cloudflare/Nitro),
that `true` leaked into `environments.ssr.resolve.noExternal` and
@vitejs/plugin-rsc's later expansion to an array (or the user's own
configResolved mirror) brought React back into the bundled list,
recreating the duplicate-React crash.
Two changes:
1. When `config.ssr.external === true`, vinext only sets the top-level
`ssr: { external: true }` block — no `noExternal: true` to leak into
the per-env config.
2. Defensive configResolved hook that strips React entries from
`environments.ssr.resolve.noExternal` if a later plugin (or
plugin-rsc itself) added them back. The strip is gated on
`external === true` so it only applies in the all-external mode.
Verified against a real-world App Router app (~80 deps including nuqs,
next-themes, launchdarkly-react, blocknote, opentelemetry, pg,
protobufjs, keyv, etc.): with ssr.external: true and these fixes
applied, 'use client' routes now render correctly in dev — the
`Cannot read properties of null (reading 'useContext')` /
`Invalid hook call` crashes are gone.
Refs #1103
App Router middleware currently receives internal Flight request headers, and external middleware rewrite proxy responses can return upstream x-middleware-* control headers when no middleware header context exists. That exposes internal protocol state at request and response boundaries.
The middleware entry now clones requests with the Next.js Flight header set removed before middleware execution or middleware-owned external rewrites. External middleware rewrite proxy responses are always sanitized with the existing middleware header scrubber before any middleware headers are merged.
Regression coverage asserts middleware cannot observe Flight headers and that upstream proxy x-middleware-* headers are stripped without context.
Visible router state currently has an approved commit path, but the raw reducer remains an exported mutation escape hatch and approval traces only explain low-level reasons. That makes it harder to audit whether a payload became visible, was discarded, or forced a hard navigation.
The violated invariant is that visibleCommitVersion should have one owner behind ApprovedVisibleCommit, with every approval decision carrying an operator-visible transaction outcome.
Move the reducer implementation behind applyApprovedVisibleCommit, keep visibleCommitVersion advancement in that boundary, and prepend transaction trace codes for visible commits, no-commit decisions, and hard navigations.
* fix(router): gate refresh and traverse outcomes
Implements #726-CORE-05/06A and #726-CORE-06B from issue #726.
Compare pending operation visible commit versions against the latest visible state before applying refresh, traverse, or same-URL navigation payloads. Return explicit navigation payload outcomes so stale and hard-navigation results settle without seeding visited-response cache entries.
Make router.prefetch entries expose a cache-seeded outcome only after the snapshot is ready and pending work is cleared, and keep the ambient next/navigation declarations aligned with the runtime cache shape.
* chore: retrigger ci
* feat(router): expose route graph manifest read model
The App Router graph now exposes root-boundary identity and a stable RouteManifest read model instead of leaving these facts implicit inside per-route arrays. Future #726 planner and compatibility work needs a semantic lookup surface for routes, layouts, pages, templates, slots, and root boundaries before it can stop reading meaning from transport keys.
This adds RootBoundaryId, graphVersion, StaticSegmentGraph maps, and fail-fast invariant checks for mismatched layout/template positions while preserving the existing routes array for current callers.
* chore: retrigger ci
Request-time dynamicParams=false validation swallowed thrown and rejected generateStaticParams failures. That made application errors look like either successful validation or a 404 when a later source excluded the params.
The validation helper now lets generateStaticParams failures reject, matching the Next.js static params builder contract. The dispatch caller no longer passes a logging callback whose only recovery path was to continue after the failure.
Regression tests cover thrown, rejected, and partially failing generateStaticParams sources.
Fetch server actions accepted every value returned by decodeReply and spread that array into action.apply. A hostile action payload could force an oversized argument list and hit the same stack-overflow class Next.js guards against.
Add the Next.js-compatible 1000 argument limit immediately before invoking the action, preserving the existing action error packaging path. Cover the regression with a focused fetch-action test that proves the action is not called for 1001 decoded arguments.
App RSC route matching trusted callers to pass already-normalized pathname parts. That let encoded static segments miss and caused interception source params to be dropped when source paths still contained URL-encoded parts.
The matcher now reuses the existing route pathname normalizer before splitting both direct and interception source paths. Regression coverage verifies encoded static segments, exactly-once dynamic param decoding, and merged interception params.
* fix(router): reject stale same-url server action commits
Same-URL server action payloads could commit visible router state after newer visible work had already won. URL equality and activeNavigationId alone did not distinguish two different visible worlds at the same href.
The lifecycle gate now compares a pending operation's started visibleCommitVersion against the current router state's visibleCommitVersion at approval time. Stale results keep returning their action value, but they no longer mutate visible state or trigger stale hard-navigation recovery.
Tests cover out-of-order same-URL server actions and the visible-version stale path taking priority over root-boundary hard navigation.
* fix(router): preserve server action initiation version
Server action POST handling captured the router state before fetch but the same-URL commit helper previously rebased the pending operation when the response was decoded. If a newer visible commit landed while the POST was in flight, the old action could still look current.
Thread the initiation state through same-URL action commit handling so startedVisibleCommitVersion reflects the world that submitted the action. Approval still compares against the live router state after payload decoding, so stale results keep their action value without mutating visible state.
Add a hostile race test where the action response is processed after a newer same-URL visible commit.
* fix(deploy): resolve wrangler .CMD shim on Windows (#1095)
`runWranglerDeploy` invoked the bare-name `node_modules/.bin/wrangler`
file, which on Windows is a Unix shebang script that CreateProcess()
cannot execute, surfacing as a misleading `spawnSync wrangler ENOENT`.
Prefer the `.CMD` shim on win32, fall back to the bare name on other
platforms (and as an error-message fallback when nothing is found).
Extracted bin resolution into an exported, platform-injected
`resolveWranglerBin` helper so the behavior is unit-testable without
mutating `process.platform`.
Fixes#1095
* Apply suggestion from @ask-bonk[bot]
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
---------
Co-authored-by: James Anderson <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* feat(server): add disabled cache proof model
Route-scoped cache work currently lacks a typed proof boundary for representing render observations, variants, and breaker fallback decisions. That makes future cache reuse work easy to wire before proof completeness is explicit.
The missing invariant is that cache proof data must be canonical, redacted, bounded, and incapable of authorizing runtime reuse while the model is disabled.
Add a v0 cache proof model with RenderObservation, CacheVariant, BoundaryOutcomeCompatibility, variant budgets, and breaker fallbacks. The disabled decision always returns canReuse false, and tests cover canonical redacted dimensions, budget breaker paths, negative request API proof completeness, exact boundary outcome matching, and the disabled reuse gate.
Refs #726
* fix(server): use distinct invalid cache budget code
Invalid cache proof budget fields currently report the route variant ceiling reason code, even when the failing field is unrelated to route cardinality.
That conflates malformed configuration with an actual per-route variant ceiling breach.
Add CP_INVALID_VARIANT_BUDGET for invalid budget fields while leaving CP_ROUTE_VARIANT_CEILING_EXCEEDED for real route variant count failures. Cover maxEncodedLength as the regression case.
Refs #726
* fix(server): harden disabled cache proof invariants
Disabled cache proof variants currently collapse null and empty-string output scope fields into the same encoded value. That makes a future empty-string field addition able to collide silently with absent values.
The matcher also leaves two defensive contracts implicit: breaker fallback detection depends on the generic kind field, and the private boundary matcher would treat unknown as a match if called directly.
Preserve null through JSON output encoding, use the fallback-specific code field for the internal breaker guard, and make unknown outcomes non-matching at the matcher level. Add coverage for null versus empty-string scope keys and for missing request API observations not counting as negative proof.
Refs #726
Layout-only App Router routes with parallel slot content were omitted from prerender because the collection loop treated route.pagePath as the only renderable UI entry. That silently skipped static export output for routes whose visible content comes from slot page or default modules.
Use a shared App Router render-entry helper for prerender and build-report classification. It keeps route handlers API-only, preserves normal children page priority, and falls back to parallel slot page/default modules for layout-only UI routes.
Covers the regression with prerender output assertions for /parallel-nested/home, /parallel-nested/home/nested, and /slot-collision, plus build-report classification coverage.
* refactor(app-router): gate browser state writes with approved visible commits
Browser router commits currently mutate visible state from raw pending navigation actions after separate disposition checks. That leaves lifecycle approval implicit at each writer, which makes later stale-operation and planner work easier to bypass by accident.
Introduce CommitDecision and ApprovedVisibleCommit as the proof object for visible browser commits. Route navigation, same-URL action, and HMR state writes through the approved commit path while preserving the legacy dispatch, skip, and hard-navigate dispositions for current callers.
Tests cover approved navigate, replace, traverse, stale no-commit, and hard-navigation decisions, alongside the existing browser lifecycle regression coverage.
* refactor(app-router): close visible commit approval escape hatch
* ci: rerun checks
* refactor(app-router): isolate visible commit approval authority
* refactor(app-router): remove legacy commit disposition mirror
Visible commit classification still exposed a translated legacy disposition beside the authoritative decision. That left same-URL server action handling able to read two equivalent fields and made the approval boundary less clear than it needs to be.
Use the commit decision directly, remove the translation helper, keep the old state-level disposition private, and assert the non-visible trace reason codes in the approval tests.
* refactor(app-router): fence AppElements wire-key construction
AppElements transport keys could still be constructed or recognized through scattered raw string prefixes outside the wire codec. That kept the flat payload format acting as an informal semantic API and left legacy compatibility behavior covered only by indirect route tests.
Make AppElementsWire the exported constructor and parser boundary for route, page, layout, template, slot, cache, metadata, and unmatched-slot wire values. Route graph semantic ID minting stays separate, while render wiring, layout classification, browser slot merging, and prefetch cache tests now consume the codec boundary.
Tests cover canonical key construction and parsing, legacy metadata read/write behavior, unmatched-slot marker compatibility, and a source-boundary check that rejects new raw AppElements wire-key construction outside the codec and route graph.
* refactor(app-router): address AppElements wire review
Make slot-key recognition a cheap structural predicate, document the absolute tree-path parser contract, and broaden the boundary coverage to catch raw wire-key concatenation.
Also route layout classification ids through AppElementsWire so raw layout-key construction stays fenced inside the codec boundary.
* feat(app-router): add artifact compatibility envelope
App Router payloads now carry a parsed compatibility envelope at the existing payload metadata boundary. Without this skeleton, future cache and skip work would have no typed place to record graph, deployment, schema, root-boundary, or render-epoch proof.
Add a small constructor/parser pair, attach a default unknown-proof envelope to outgoing record payloads, and reject malformed envelope metadata when reading payload metadata. Tests cover explicit envelopes, legacy missing envelopes, malformed envelopes, and non-mutating payload writes.
* test(app-router): cover future artifact schema rejection
The review correctly identified that the envelope parser had malformed-field coverage but no direct test for the version guard path. Add the future-schema-version case and document why the Wave01 skeleton emits separate schema versions and an unknown-proof envelope without yet splitting mismatch from corruption behavior.
* feat(app-router): attach artifact compatibility metadata
App Router payloads previously carried only the COMPAT-01 unknown envelope unless a caller manually supplied metadata. That left COMPAT-02/03 without concrete graph, deployment, root-boundary, and fallback behavior in the PR.
The render lifecycle now attaches compatibility metadata for record payloads from route/root/deployment facts, while renderEpoch remains unknown until an epoch owner exists. The compatibility evaluator returns explicit compatible, unknown, or incompatible decisions so unknown proof always falls back to render fresh instead of becoming reuse proof.
Tests cover render-boundary metadata attachment and unknown graph, deployment, root-boundary, and render-epoch decisions.
* fix(app-router): tolerate unused compatibility metadata parse failures
App Router metadata parsing crashed when __artifactCompatibility was malformed or from a future schema, even though current render and navigation callers do not consume that field yet.
The parser treated compatibility scaffolding like required route metadata. That made an unused proof capable of failing initial mount, SSR, and client navigation paths.
Default invalid compatibility metadata to the unknown proof for Wave01, document the planned hard-fail point for #726-COMPAT-04/05, and cover malformed, future-version, and non-object payloads.
* refactor(server): dedupe HTTP error response builders
Extract notFoundResponse / badRequestResponse / internalServerErrorResponse /
methodNotAllowedResponse into a shared http-error-responses.ts helper,
alongside the existing forbiddenResponse (moved from request-pipeline.ts) and
payloadTooLargeResponse (moved from app-server-action-execution.ts). Sites
with custom bodies (e.g. "404 - API route not found", "Image not found",
"missing pattern", "Page has no default export") and template-string-embedded
sites in deploy.ts / pages-server-entry.ts / dev-origin-check.ts are left
inline. Follow-up to #1058.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: retrigger workflows
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- routing: extract a shared generic matchRouteWithTrie<R> in
route-matching.ts and use it from pages-router.ts (matchRoute) and
app-router.ts (matchAppRoute). Both now share the same query-strip,
trailing-slash, normalize, split, and trie-cache lookup preamble.
Pure refactor; no behavior change. Follow-up to #1058.
Note: the planned migration of pages applyGsspHeaders to the shared
mergeMiddlewareResponseHeaders helper was deferred — applyGsspHeaders
reads Node-style OutgoingHttpHeaders (Record<string, string | number |
boolean | string[]>) and joins multi-value non-cookie arrays with ", "
before set(), neither of which the shared Headers-based helper does.
Collapsing without preserving those semantics risks behavioral drift,
so it'll ship as a separate PR.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(utils): dedupe trailing-slash and basePath normalization
Add removeTrailingSlash to base-path.ts and migrate ad-hoc replace
patterns + ad-hoc basePath checks to the centralized helpers.
Follow-up to #1058.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(utils): use char-code loop in removeTrailingSlash
Replace the /\/+$/ regex with a manual trailing-slash count to avoid a
CodeQL polynomial-regex-on-uncontrolled-data alert. Same behaviour, no
backtracking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: retrigger workflows
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server): dedupe TextDecoder stream consumption
Extract the repeated `new TextDecoder()` + `ReadableStream` chunk-loop
into shared helpers in `utils/text-stream.ts`:
- `readStreamAsText(stream)` for the consume-to-single-string pattern
(used in `app-page-cache.ts` ISR write and `pages-page-response.ts`
ISR cache write).
- `readStreamAsTextWithLimit(stream, maxBytes, onLimitExceeded)` for
the size-limited pattern (used in `app-server-action-execution.ts`
and `pages-node-compat.ts`). Each caller passes its own throwing
callback so error types (`Error` vs `PagesBodyParseError`) and
status codes are preserved.
Sites with additional load-bearing logic intentionally left inline:
- `rsc-stream-hints.ts` (line-buffered TransformStream with carry)
- `app-ssr-stream.ts` (raw-byte accumulator + flight hint fix; HTML
flush transform with `fixPreloadAs`)
- `shims/fetch-cache.ts` (size-limited cache-key body canonicalisation
with mixed string/Uint8Array stream and tee handling)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server): address review feedback from #1083
- Dedupe `readAppPageTextStream` in `app-page-execution.ts` (called
once from `app-page-dispatch.ts`); replace with `readStreamAsText`
and remove the now-unused export. The deduped helper additionally
releases the reader lock in `finally`, which the inline copy did
not — a robustness improvement on a wholly-consumed stream.
- Update `tests/app-page-execution.test.ts` to use `readStreamAsText`.
- Mirror the `try/finally { reader.releaseLock() }` pattern in
`readStreamAsTextWithLimit` for consistency with `readStreamAsText`.
The happy path implicitly releases when the stream completes; the
over-limit path cancels which also releases. The explicit release
prevents lock leaks if a future caller catches the limit error and
reuses the reader handle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove `toRscUrl` from shims/navigation.ts (zero internal callers;
all sites use createRscRequestUrl).
- Remove matching declaration from shims/next-shims.d.ts.
- Update stale comment in server/app-browser-entry.ts that referenced
toRscUrl by name.
Note: the originally-scoped `requestAsyncStorage` legacy alias in
shims/internal/work-unit-async-storage.ts was kept. It is the named
export that resolves through the
`next/dist/client/components/request-async-storage.external` import-path
mapping (see packages/vinext/src/index.ts and check.ts), and is asserted
by tests/shims.test.ts. The shim's JSDoc documents `@sentry/nextjs`
runtime-resolves request context via this name, so removing it would
break that ecosystem path.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>