Commit Graph

256 Commits

Author SHA1 Message Date
James 6946d43db6 fix(cache): preserve explicit route handler policy 2026-08-27 02:32:55 +01:00
James Anderson a5b6d05b83 fix(app): authenticate static file signals (#3077) 2026-08-25 10:58:22 +01:00
James Anderson 0736fdd50b fix(app-router): route server actions through their owning pages (#2520)
* fix(app-router): align server action routing

* perf(app-router): cache action ownership discovery

* fix(app-router): retain client-bound action owners

* fix(app-router): harden action owner forwarding

* fix(app-router): escape action owner metadata

* fix(routing): parse route parameters linearly

* perf(app-router): collect action owners during RSC scans

* perf(app-router): reuse RSC action consumer metadata

* perf(app-router): narrow RSC action metadata patch

* perf(app-router): move action edge analysis into vinext

* fix(app-router): finalize action scan observer integration

* fix(app-router): preserve original action scan metadata

* fix(app-router): retain imported inline action ownership

* test(actions): cover production action ownership edge cases

* fix(build): resolve action runtimes from generated entries

* test(actions): cover nested client form actions

* test(actions): cover same-named non-action exports

* refactor(app-router): derive action owners from final module graphs

* fix(app-router): lazily access RSC plugin API

* fix(app-router): harden action ownership forwarding

* fix(app-router): preserve forwarded action misses

* fix(app-router): resolve shared action owner roots

* fix(app-router): route progressive action owners

* fix(app-router): preserve progressive action routing

* fix(app-router): route bound progressive actions safely

* fix(app-router): preserve forwarded action redirects

* fix(app-router): reject unsafe progressive references

* fix(app-router): validate nested action references

* fix(app-router): decode outlined action chunk ids

* test(app-router): cover cached action ownership

* test(app-router): cover action owner route handlers

* test(app-router): cover direct object-wrapped actions

Adapt the regression from #2743 to assert successful direct form submission through the final action ownership graph.
2026-08-19 01:46:19 +01:00
James Anderson 46eb2f031c refactor(app-router): remove dead RSC nav global (#2961) 2026-08-18 00:10:32 +00:00
James Anderson 92ff5b4cbd fix(app-router): preserve static hydration search params (#2944) 2026-08-17 23:00:40 +01:00
James Anderson a5f0bb44cb fix(app-router): match current navigation identity (#2876)
* fix(app-router): match soft navigation identity

* fix(app-router): refresh exact links behind basePath

* fix(app-router): preserve navigation URL spaces

* fix(app-router): preserve raw navigation identity

* fix(app-router): preserve licensed history cache reuse

* fix(app-router): replace visited route aliases

* fix(app-router): preserve exact hash cache bypass

* test(navigation): cover cache bypass runtime argument
2026-08-16 15:43:57 +01:00
James Anderson 1f43ba32a1 fix(app-router): retain loading shell prefetches (#2938) 2026-08-16 15:02:40 +01:00
MaxtuneLee 88ea15a05d fix(headers): preserve config Link headers alongside React preload links (#2791)
* fix(headers): preserve config Link headers alongside React preload links

* fix: vp check error

* fix(headers): preserve Link header precedence

* test(headers): expect framework Link after middleware

* fix(headers): preserve Link provenance across response paths

---------

Co-authored-by: James <james@eli.cx>
2026-08-15 00:26:10 +01:00
MaxtuneLee fb7c22f3a1 test(app-router): remove nested document tags from interception fixtures (#2790) 2026-08-13 09:36:03 +01:00
JamesbbBriz 34d0a57494 test(app-router): cover data-returning server actions skipping visible commits (#2902)
* test(app-router): cover data-returning server actions skipping visible commits

Data-returning fetch actions with no revalidation must resolve their value
without applying the RSC tree — otherwise the server tree hands forms a
fresh initialState and wipes pending edits (the Payload getFormState loop).
vinext's server omits `root` for these responses (shouldSkipPageRendering in
app-server-action-execution.ts) and the client returns the value directly
without committing, mirroring Next.js:

- https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

The behavior exists but was uncovered by unit tests and had no e2e
asserting form edits survive the roundtrip. Add both so regressions fail
loudly instead of silently resetting form state.

Unit tests (tests/app-browser-server-action-client.test.ts):
- root omitted + ok returnValue -> resolves data, no commit, no cache clear
- root omitted + failed action -> throws, no commit
- root present + revalidation header -> commits decoded tree with returnValue
- raw full-tree payload -> commits with undefined returnValue

E2e (tests/e2e/app-router/server-actions.spec.ts): a blur-triggered
data-returning action (Payload getFormState shape) keeps the pending input
edit and does not advance the page's server render counter.

* test(app-router): add revalidated void-action case; fix e2e describe placement

Follow-up from adversarial review:
- Move the form-preservation e2e out of the "Server action forwarding loop
  guard" describe into its own "Data-returning server actions" block.
- Add a unit case for revalidated void actions (root present, no
  returnValue): commits the tree with undefined returnValue and the
  staticAndDynamic revalidation kind.

* test(app-router): lock void-action returnValue shape from round-2 review

Round-2 adversarial review findings:
- The revalidated void-action unit case mocked `{ root }` without a
  returnValue, but the server always pairs a re-rendered root with a
  returnValue record — void actions come back as `{ ok: true, data:
  undefined }`, which Flight serializes as `$undefined` and decodes to a
  truthy object. Mock the real shape and assert the real value, plus the
  caller-facing `undefined` resolution.
- Clear all mocks after each unit test so a stray mockResolvedValueOnce
  cannot leak across cases.
- Reword the e2e comment: the server render counter is the discriminator
  for "no tree update"; the input assertion guards the user-visible
  contract only (React can reconcile an uncontrolled input in place).
2026-08-13 09:26:42 +01:00
James Anderson bc97bf68eb fix(app-router): preserve valued RSC queries through routing (#2883) 2026-08-11 17:40:07 +01:00
James Anderson f48aeb5848 fix(app-router): bail out static search params rendering (#2882)
* fix(app-router): bail out static search params rendering

* fix(app-router): preserve PPR search params tracking

* test(app-router): allow streamed search params fallback
2026-08-11 14:19:48 +01:00
James Anderson 05f2ac6824 fix(app-router): expose not-found fallback flight payload (#2349)
* fix(app-router): expose not-found fallback flight payload

* fix(app-router): release mirrored fallback flight data

* fix(app-router): clear progressive fallback flight mirrors

* fix(app-router): preserve deferred flight consumers

* test(app-router): avoid parsing script tags with regex

* test(app-router): refresh flight transform options
2026-08-11 14:15:18 +01:00
James Anderson acc2b202a2 fix(prerender): expose the production build phase (#2846)
* fix(prerender): expose production build phase

* fix(prerender): load config in production build phase
2026-08-10 16:50:17 +01:00
James Anderson 056cd026b3 fix(app-router): preserve full prefetch stale windows (#2851)
* fix(app-router): preserve full prefetch stale windows

* fix(app-router): retain completed dynamic prefetch bounds
2026-08-10 11:21:29 +01:00
James Anderson 373e8264a1 feat(use-cache): support callable cached functions with rsc plugin api (#2156)
* fix(use-cache): support nested cache functions passed as props

* fix(use-cache): use inline registerServerReference instead of broken forward-reference module-level code

The previous approach used `noExport: true` and appended module-level
`const ${name}_$$vcf` declarations at the end of the transformed file,
then referenced them via forward reference at the call-site. This caused
a temporal dead zone (TDZ) error because `const` bindings are not
hoisted — the call-site assignment evaluated before the TLA const was
initialized, crashing all RSC files that contain function-level "use
cache" (HTTP 500 for use-cache pages, route handlers, etc.).

Fix: keep the existing hoisting/export behaviour (`noExport` stays
false) and instead wrap `registerCachedFunction(...)` with
`registerServerReference(...)` inline at call-site in the RSC
environment. This adds the RSC serialisation metadata ($$typeof, $$id)
so cached functions can be passed as props to client components
(useActionState / formAction), while not disturbing the existing
exported binding that loadServerAction relies on.

* fix(use-cache): use correct normalised id and register in manifest for nested function props

The previous approach passed the raw absolute file path as the $$id to
registerServerReference. @vitejs/plugin-rsc resolves server references by a
normalised key (sha256(toRelativeId) in build; URL-path in dev), so production
would throw "server reference not found" for any cached function passed as a
client-component prop.

Also, the module was never added to the virtual:vite-rsc/server-references
manifest because only the plugin's own "use server" transform writes to
manager.serverReferenceMetaMap. Without a manifest entry, the production
serverReferences lookup has no entry for the module at all.

Fix:
- Capture the plugin-rsc manager via the rsc:minimal plugin API in
  configResolved so we can write to serverReferenceMetaMap directly.
- Compute normalizedRefKey to match vitePluginUseServer's getNormalizedId():
    build → sha256(toRelativeId(id)).hex.slice(0,12)
    dev   → id.slice(root.length)  (Vite URL path)
- After transformHoistInlineDirective succeeds, register the hoisted export
  names in manager.serverReferenceMetaMap[id] so the manifest is populated.
- Pass normalizedRefKey (not raw id) to registerServerReference.

Add unit tests verifying the hash formula matches plugin-rsc's own logic.

* fix(use-cache): wrap hoisted exports as cached server references and register manifest after rsc:use-server

- Derive the build-mode reference key via plugin-rsc's own
  manager.toRelativeId() instead of a string slice, so the hash input is
  byte-for-byte identical to the plugin's hashString(toRelativeId(id)).
- Reassign each hoisted inline 'use cache' export at module level to
  registerServerReference(registerCachedFunction(fn)) so the module
  export itself is the cached wrapper (Next.js parity: direct action
  invocation goes through the cache) and call sites/manifest imports all
  observe the same wrapped function.
- Register serverReferenceMetaMap entries from a new
  vinext:use-cache-server-references plugin placed after the plugin-rsc
  plugins: rsc:use-server deletes metaMap entries for modules without
  'use server', which wiped the entries written during the use-cache
  transform (prod actions 404'd with 'server reference not found').
- Deduplicate the RSC/non-RSC transform branches into a single
  transformHoistInlineDirective call and hoist the
  @vitejs/plugin-rsc/react/rsc resolution out of the per-module path.
- Replace the self-referential key-formula unit test with the ported
  Next.js fixture (use-cache-with-server-function-props/nested-cache), a
  dev-mode Playwright round-trip test, and a production-server
  integration test that resolves the serialized references via action
  POSTs and asserts cached-invoke semantics.

* docs(use-cache): document dev-key normalisation scope for inline cache server references

* fix(use-cache): throw instead of emitting unresolvable inline cache server references when the plugin-rsc manager is missing

When the @vitejs/plugin-rsc manager is unavailable in the rsc environment,
the inline 'use cache' transform previously fell back to a locally computed
reference key and still wrapped the hoisted exports — but the manifest
registration plugin bails without the manager, so the emitted reference
would serialize into the RSC payload yet never resolve (silent 404 on
action POST in production). Fail loudly at transform time instead; the
manager is a structural invariant whenever the rsc environment exists.

Adds transform-level unit tests for the fail-loud path (build + dev), the
non-rsc no-manager control, and build reference-key parity with plugin-rsc.

* test(use-cache): pin unencrypted closure-captured bound args and document the divergence

Extends the nested-fn-props fixture with a cached function that closes over
a value from the cached component's scope, exercising the .bind(null, ...)
bound-arg path end to end: the production round-trip test asserts the
captured value appears in plaintext in the flight payload (pinning the
documented divergence from Next.js, which encrypts bound args by default)
and that invoking the bound reference observes the captured value; the
Playwright test covers the real flight-client encodeReply round-trip in
dev. A transform-level test pins that captures are emitted as plain bind
args. The divergence is now also documented in the README's Known
limitations section.

* refactor(use-cache): route registerServerReference through a vinext shim to decouple from plugin-rsc module-id normalisation

The inline 'use cache' prepend imported registerServerReference from a
file:// URL of @vitejs/plugin-rsc/react/rsc while the cache runtime
imports the same package via the bare specifier, relying on Vite
normalising both to a single module id. Re-export it instead from a new
vinext-owned cache-server-reference shim whose only react/rsc specifier
is the same bare one cache-runtime uses, resolved from the same importer
location — one module instance by construction. The transform unit test
now pins that the emitted import targets the shim and never a plugin-rsc
file URL.

* test(use-cache): pin cached-invoke semantics for the closure-bound getMessage path

Mirror the getDate cache assertion on the closure-bound path: the
fixture's getMessage now appends a Math.random() suffix so cache hits
are observable, and the production-server round-trip asserts that two
identical bound-arg invocations return the same cached value while a
different bound arg misses instead of reusing the entry. The Playwright
assertion matches the suffixed message via regex.

* fix(use-cache): encrypt closure-bound arguments

* refactor(use-cache): use plugin-rsc directive transforms

* test(use-cache): cover directive transforms across environments

* fix(cache): update RSC directive prerelease

* fix(cache): stabilize directive reference tests

* style(cache): format HMR test

* refactor(use-cache): move server function directives to user land

* refactor(use-cache): clarify generic directive plugin naming

* refactor(use-cache): own directive plugin types

* refactor(use-cache): use plugin-rsc metadata map directly

* refactor(use-cache): own server reference metadata lifecycle

* chore(use-cache): keep directive type internal

* refactor(use-cache): adopt server reference claims

* fix(init): install required plugin-rsc prerelease

* feat(rsc): harden use cache server functions

* feat(cache): adopt plugin-rsc transform primitives

* refactor(cache): rename callable plugin

* test(init): update plugin-rsc install expectations

* test(cache): avoid reloading during HMR retries

* test(cache): align callable references with plugin-rsc

* fix(cache): align mixed directives with plugin-rsc 0.5.34

* fix(cache): harden callable use cache transforms

* fix(cache): support manually configured RSC

* fix(cache): harden manual RSC ordering
2026-08-10 00:52:19 +01:00
James Anderson 0bc39ef5bd fix(pages): apply fallback rewrites after API misses (#2827)
* fix(pages): apply fallback rewrites after API misses

* fix(pages): distinguish API normalization from rewrites
2026-08-07 11:33:24 +01:00
Nathan Nguyen aec4421b2a fix(app-router): let concrete Pages routes win middleware rewrites (#2730)
* fix(app-router): let concrete Pages routes win middleware rewrites

A Pages data request that middleware rewrote returned a synthetic empty
JSON body whenever any App route matched the rewrite target, including a
dynamic or catch-all match. Every other App-vs-Pages ownership decision in
this handler treats a dynamic App match as non-owning, so a concrete Pages
route at the same pathname should render instead. Skipping that arbitration
meant getServerSideProps never ran for the rewrite target, and the client
router, which reuses the middleware probe response when the rewrite target
resolves to a Pages route, accepted the empty body as successful page data.
Redirect and notFound markers the Pages route would have returned were
therefore absent during client-side navigation.

Restrict the shortcut to App matches that own the target outright, so
dynamic matches fall through to the existing static and dynamic Pages
fallback arbitration.

That fallthrough reaches the tail Pages data response, which built its
headers from the not-found response alone and dropped headers the
middleware set on the way. Merge the middleware response headers there so
a rewrite landing on a genuinely App-owned dynamic route still carries its
cookies.

* fix(app-router): preserve middleware headers on Pages fallbacks

* fix(app-router): use Pages response merge semantics

* test(app-router): cover rewritten Pages data ownership

---------

Co-authored-by: James <james@eli.cx>
2026-08-03 11:02:14 +01:00
James Anderson 456ddbef11 fix(actions): run redirect targets through full request pipeline (#2785)
* fix(actions): run middleware for server action redirect targets

A server action that throws `redirect()` renders the target page inline
and returns its Flight payload with the action response, instead of
making the client re-request the target. Middleware had only run for the
action's own path, so the target's middleware never saw the request: an
action reachable on a public path could redirect to a middleware-gated
page and return that page's server-rendered payload, which the browser
client decodes and commits. Next.js has no such hole because the client
re-requests the target through the full pipeline.

Run the target's middleware against the synthetic GET before rendering
it, after the redirect render's headers context is installed so
`NextResponse.next({ request: { headers } })` overrides reach the page.
Middleware response headers merge into the action response.

Only a clean pass-through is rendered inline. A block, redirect, rewrite,
or status override diverts to the header-only 303 that already exists for
non-App-route targets, which the client re-requests through the full
request pipeline. Apps without middleware, and targets no matcher
matches, are unaffected.

* fix(actions): match redirect targets with request route identity

Follow-up to the target-middleware fix, addressing three ways the target
could still be evaluated as something other than the request the client
would have made.

`matchRoute` decodes pathname segments, so an encoded alias like
`/adm%69n` resolved to the `/admin` page while middleware and a real
navigation both saw `/adm%69n` — inline-rendering a route neither
reached. Match redirect targets with request route identity instead.

`cloneActionRedirectHeaders` carried `x-vinext-mw-ctx` onto the target's
request. In hybrid app+pages dev that header holds the Pages handler's
middleware result for the *action* path, which
`applyForwardedMiddlewareContext` replays in place of executing
middleware for the target. Strip it, which also keeps the internal header
out of the redirect render's `headers()`.

Middleware object matchers support `has`/`missing` header predicates, so
a matcher gated on `Accept` took a different branch against the render
request, which drops `Accept` with the action transport headers. Give
middleware a request that keeps it.

* docs(actions): note the redirect-target middleware divert trade-off

* fix(actions): run target middleware before importing redirect-target modules

A middleware-blocked redirect target still executed its route modules'
top-level code: the target was hydrated (dynamically imported) to decide
renderability before the middleware probe ran, so an unauthorized action
redirect could trigger module side effects, or turn a module-eval throw
into a 500 instead of the header-only fallback. Renderability is now
decided from the manifest's lazy thunks (__loadPage/__loadRouteHandler),
and hydration happens only after middleware passes the target through.

The probe leaked two more behaviors a real navigation would not produce:

- Pass-through middleware response headers merged verbatim onto the 303
  action wrapper, so a middleware-set Location gave the wrapper genuine
  HTTP-redirect semantics and fetch followed it before the action client
  could read x-action-redirect. Middleware header merges onto the wrapper
  now strip Location.

- The synthetic target requests were built with bare new Request(), which
  drops the Workers cf property, so target middleware keying off
  request.cf (geo checks) failed open. Both requests now re-attach cf via
  the metadata helper the request clone utilities already used.

* fix(actions): carry middleware cookie mutations onto the redirect target

When action-path middleware rotated or deleted an authentication cookie,
the synthetic redirect-target request was still built from the original
inbound Cookie header, so target middleware evaluated a credential the
response was simultaneously revoking and could render the protected page
inline. The middleware's Set-Cookie mutations now feed the same
request-cookie rebuild as the action's own cookies().set() calls, in
browser order (middleware first, action wins for the same name).

Also aligns two more target-request details with the real pipeline:

- The internal _rsc transport param is stripped from the redirect target
  before matching, middleware, and render, as app-rsc-handler does for
  navigations, so matchers cannot branch on a query no navigation
  carries. The client-facing x-action-redirect header keeps the verbatim
  URL; a diverted re-request still goes through real validation.

- The middleware header merge onto the action wrapper now restores all
  protocol headers (Content-Type, x-action-redirect and friends) rather
  than only stripping Location, so a pass-through middleware can neither
  replace the wrapper's destination nor flip the client into treating
  the Flight body as non-RSC.

* fix(actions): scope redirect-target cookies and framing to browser behavior

Applying every pending Set-Cookie to the redirect target's Cookie header
ignored the cookie's Path attribute, so a mutation scoped to another
path (admin=1; Path=/account) reached a target like /admin that a real
browser navigation would never send it to, and target middleware could
authorize on it. Mutations now apply only when their Path, or the RFC
6265 default path derived from the action URL, path-matches the target.
Cookies from cookies().set() and draftMode() always carry Path=/ and are
unaffected.

Also adds Content-Length to the wrapper's protected headers: a
pass-through middleware value would misframe the freshly generated
Flight stream and let adapters truncate or reject the action response.

Drops the fixture node_modules symlink an e2e run left staged; on
Windows checkouts with core.symlinks=false it materializes as a plain
file that defeats the Playwright server's link-creation guard. The
gitignore rule loses its trailing slash so it covers symlinks and stops
these from getting staged again.

* fix(actions): use redirect target CSP nonce

* docs(actions): clarify redirect cookie projection

* fix(actions): forward middleware headers to redirect targets

* fix(actions): preserve redirect response framing

* fix(actions): preserve middleware request overrides

* fix(actions): run redirect targets through full request pipeline

* fix(actions): match redirect target header semantics

* fix(actions): mirror Next cookie forwarding

* fix(actions): preserve forwarded cookie ordering

* fix(actions): avoid stale forwarded cookies

---------

Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
2026-08-03 00:28:04 +01:00
Nathan Nguyen 87a3f37a49 fix(build): exclude filtered require.context modules (#2736)
* fix(build): exclude filtered require.context modules

require.context regexps previously filtered only the runtime map after a broad eager glob had imported every file. This evaluated and bundled excluded modules, including from client components.

Resolve and filter context entries during the transform so only accepted files become static dependencies. Keep context directories watched so create and delete events can update the generated module set.

* test: update require-context unit tests for static-import transform

* fix(build): harden require.context enumeration and bindings

Replace fs.glob (withFileTypes needs Node 22.2, engines allow >=22) with a readdir walk that follows directory symlinks like webpack and guards cycles via realpath, and grow the generated import binding prefix past any identifier already present in the source.

* fix(build): scope symlink cycle guard to the recursion path

A global realpath set deduplicated distinct symlink aliases of the same directory; track realpaths only along the current recursion path so aliases keep their own context keys while cycles still terminate.

* fix(build): stat directory entries with unknown dirent types

Filesystems without dirent type info (NFS, SMB, FUSE) report entries that are neither file nor directory; fall back to stat for any unknown type instead of only symlinks, and skip unresolvable ENOENT/ELOOP entries.

* fix(build): make require.context deterministic and dev-invalidation complete

Assign import binding indices after sorting so readdir order cannot change bundle bytes; invalidate recursive contexts on any membership event since a directory create/delete can change matching descendants without matching the file regexp; and drop watched-context entries for updated modules so importers that lose their last require.context call stop invalidating.

---------

Co-authored-by: James <james@eli.cx>
2026-07-31 11:17:36 +01:00
James Anderson 9f1018cd23 fix(middleware): preserve headers for empty override value (#2767)
* fix(middleware): preserve headers for empty override value

* fix(middleware): ignore stray headers for empty override

* fix(middleware): require override list for forwarded headers

* test(middleware): cover stray request header value

* test(middleware): pair valid and stray header overrides

* fix(middleware): preserve unlisted forwarded headers

* fix(middleware): preserve literal forwarded headers
2026-07-31 10:37:36 +01:00
Nathan Nguyen 1607104749 fix(middleware): stop restoring credentials middleware deleted before external rewrites (#2739)
`x-middleware-override-headers` carries the complete post-middleware header
set, not a diff: `NextResponse.next`/`rewrite` encode every key of the Headers
object middleware passes, and Next.js deletes any request header missing from
that list. Absence means deleted.

`preserveCredentialHeaders` (#1121) read a short override list as a "partial"
override and copied the base request's `cookie`/`authorization` back in. The
documented deletion pattern — clone `request.headers`, delete the credential,
return `NextResponse.rewrite(externalUrl, { request: { headers } })` — produces
exactly such a list, so the option resurrected the stripped credentials. It was
enabled only for external rewrites, so `proxyExternalRequest` then forwarded
first-party session cookies and bearer tokens to a cross-origin target.

The "partial override" the option guarded against cannot occur:
`encodeMiddlewareRequestHeaders` is the only producer of the override list and
always emits the full key set. Remove the option and restore Next.js-exact
deletion semantics.

Covered end to end (fixture middleware deleting credentials before an external
rewrite), at the proxy boundary, and in the Pages Router pipeline.

Co-authored-by: James <james@eli.cx>
2026-07-31 00:09:51 +01:00
James Anderson 9649fb9ec1 fix(app-router): render pages before consuming layouts (#2751)
* fix(app-router): render pages before consuming layouts

* fix(app-router): preserve page loading semantics

* fix(app-router): preserve page initialization and loading

* fix(app-router): preserve loading for initialized pages

* test(app-router): render Suspense payload children

* fix(app-router): wait for initialized pages without loading

* fix(app-router): unify page loading detection
2026-07-29 18:41:15 +01:00
James Anderson 052a2f7b78 fix(server): support Node production entry contracts (#2749) 2026-07-29 15:44:45 +01:00
Nathan Nguyen 73f56ef3f8 fix(app-router): honor cacheLife stale on the client router (#2708)
* fix(app-router): honor cacheLife stale on the client router

`cacheLife` profiles carry three independent numbers, and `stale` is the
client-router dimension: how long the browser may reuse cached route output
without asking the server. vinext aggregated it correctly (`resolveCacheLife`
min-reduces all three; the request scope accumulates min-wins) and then
projected it away at every consumer, so the only staleness a browser ever saw
was `dynamicStaleTimeSeconds` from `experimental.staleTimes` — a build-time
constant unrelated to the cached subtrees that produced the render. A subtree
declaring `cacheLife("seconds")` (stale: 30) was held for the full 5-minute
visited-response TTL.

Carry the resolved `stale` to the client on `x-nextjs-stale-time` (matching
Next.js's `NEXT_ROUTER_STALE_TIME_HEADER`) and combine it with the config
value by taking the minimum, so neither min-wins lattice overrides the other.
The 30s prefetch floor applies to the new value too.

Two normalization rules, both deliberate:

- An absent `stale` is never synthesized from `revalidate`/`expire`. The
  `default` profile has no `stale` and an `expire` of ~136 years, so deriving
  one would license session-long reuse without a refresh.
- The three numbers are not assumed to be ordered — `seconds` is
  `{ stale: 30, revalidate: 1, expire: 60 }` — so `revalidate` never
  constrains `stale`. Only the hard `expire` ceiling does.

* fix(app-router): source the client stale time from completed renders

The previous commit derived `x-nextjs-stale-time` from `peekRequestCacheLife()`
at response-construction time. That read happens after `probeAppPageBeforeRender`
but before the RSC stream is consumed, and the probe only awaits the page
component's own async result — it does not render the returned tree. Any
`use cache` scope in a child Server Component registers later, during stream
consumption, so the header described the probe rather than the output.

Because `cacheLife` aggregation is minimum-wins, missing one late scope is
enough to make the value wrong, and "it can only shorten" did not hold: with no
`dynamicStaleTimeSeconds` present (the usual case for `use cache` pages, which
are not dynamic renders), a peeked `stale: 300` widened the prefetch window from
`PREFETCH_CACHE_TTL` (30s) to 300s — 10x longer than today, in the direction the
change set out to fix.

Carry the value on the cache entry instead. The ISR write path reads the
request-scoped accumulation via the consuming `getRequestCacheLife()` inside the
cache-write closure, which runs after the captured stream has drained, so it
observes the completed render's minimum. `CacheControlMetadata` gains `stale`,
`isrSet` persists it, and `buildAppPageCachedResponse` re-advertises it on hits.
Prerender seeds carry it through `VINEXT_PRERENDER_CACHE_LIFE_HEADER` and the
prerender manifest so a seeded entry makes the same claim a runtime render would.

Two links that would otherwise silently drop the claim are closed with it: the
`use cache` entry write now persists `stale`, and `recordRequestScopedCacheControl`
re-registers it on a data-cache hit — without both, a page's advertised freshness
would depend on data-cache temperature rather than on what it declared.

Streaming responses now advertise nothing and leave the client on its configured
`experimental.staleTimes`, which is the honest answer for a value that is not yet
known when headers are committed. Covering fresh streaming renders needs a
render-completion signal the streaming RSC path does not have today; Next.js
solves it by streaming an `AsyncIterable<number>` in the RSC payload
(`app-render.tsx` `baseResponse.s`, closed after the render settles), which is
the natural follow-up.

The client-side combination logic is unchanged: both stale signals are still
min-reduced, absent `stale` is never synthesized from `revalidate`/`expire`,
`revalidate` never clamps `stale`, and `expire` still caps it.

* fix(app-router): deliver the resolved client stale time on fresh renders

The previous round persisted the completed render's cacheLife stale onto
the ISR entry and replayed it on cache hits, which left the value missing
or drifting wherever the entry's lifetime diverged from the render's:

- Fresh streaming renders advertised nothing, so the first visitor of any
  route (and every dev render, since dev writes no ISR entry) stayed on
  the configured staleTimes despite a declared cacheLife. The done-script
  emitted by the RSC embed transform's finalize() runs only after the
  full RSC stream has drained, so it can carry the completed render's
  minimum where streaming headers cannot. Emit the peeked request-scoped
  cacheLife there and seed the hydration visited-response entry from it.
  This also makes the client's min-combination of the config and
  cacheLife signals reachable: a dynamic render with use cache subtrees
  now legitimately carries both.

- The expire clamp bounded the value but not the elapsed window: an
  entry of { stale: 30, expire: 60 } hit at age 59s replayed a 30s reuse
  window reaching 29s past expire. Age the serve-time clamp with the
  entry's lastModified so only the remaining window is advertised.

- The two client caches applied different floors: prefetch entries
  floored a cacheLife stale at 30s while cold navigations honored it
  verbatim, so the same declaration produced two behaviors keyed on
  whether a prefetch fired first. Floor the cacheLife signal once in the
  shared resolver, mirroring Next.js getStaleTimeMs, before the min so
  it can never raise the config-derived bound.

Mechanically, the app-page cache setter's six-position signature
(declared identically in four modules) collapses into one exported
AppPageCacheSetter taking an AppPageCacheWritePolicy object, with
isrSetAppPage adapting to the shared positional isrSet, which stays
unchanged for the Pages Router and route handlers.

* docs(app-router): pin the cold RSC stale contract and expire precedence

The cold RSC fetch gap is a design decision, not a pending follow-up:
Next.js's AsyncIterable stale transport only works under staged rendering
(cacheComponents), where cache scopes settle before the render task queue
drains — in vinext's lazy streaming model the iterable could never close.
Next.js's plain-mode mechanism is a blocking cold render, which #961
deliberately rejected to keep ISR page streams unblocked. Assert the
resulting no-header contract on the streaming-response test.

Also reword the write-policy expire comment: a cacheLife-declared expire
replaces the config expireTime fallback (Next.js precedence), it is not
min-merged with a route-level ceiling — no such ceiling exists outside
cacheLife.

* fix(app-router): carry stale through regen and bound cold responses

Three fixes from review round 3:

Background regeneration dropped the regenerating render's cacheLife stale:
renderAppPageCacheArtifacts returned only { revalidate, expire }, so
resolveRegeneratedAppPageCachePolicy could never receive the stale it was
built to preserve and the first regen silently widened client reuse back
to the configured fallback. The producer now carries it, with a
real-producer regression test the mocked-cacheControl tests could not
provide.

The age-aware expire clamp is removed. Composed with the client's 30s
floor it delivered neither contract (a clamped 1 re-floored to 30), and
cached HTML replayed the unclamped done-script value regardless. Next.js
stores the stale header at generation and replays it verbatim on every
hit; vinext now does the same, keeping expire a serve-side ceiling.

Cold cacheable RSC responses stream before their cacheLife resolves
(#961), which left them on the 300s client fallback — reproducing the
headline bug for the first request of every entry epoch. They now carry
X-Vinext-Stale-Time-Pending, and both client caches bound such responses
at the 30s floor: the unresolved claim, once floored, could never
license less.

* fix(app-router): bound pending-stale responses by the dynamic stale time

The pending marker meant 'capture was attempted', but the client read it
as 'a cacheLife claim exists'. Capture eligibility is decided before the
lazy stream runs, so a late request-API read can make the completed
render dynamic — the finalizer skips the ISR write and no claim ever
exists, yet the marker granted 30 seconds of reuse even under
staleTimes.dynamic: 0.

Pending responses now carry the configured dynamic stale time (including
0) and the client takes the minimum, so an unresolved response never
receives a wider window than the dynamic bound.

* fix(app-router): keep the pending cap independent of staleTimes.dynamic

Pairing the pending marker with the configured dynamic stale time broke
the segment-cache-client-params compat test: dynamic-param routes
prefetch with no minimum TTL, so the paired 0 default made every cold
prefetch entry expire instantly and navigations refetched routes that
Next.js serves entirely from a static prefetch.

A cold stream cannot distinguish a render that will resolve static from
one that turns dynamic mid-stream, and bounding both by staleTimes.dynamic
sacrifices the guaranteed-correct case for the ambiguous one. Pending
responses go back to the 30s floor cap; the late-dynamic exposure
(one epoch-cold response, at most 30s) is documented as the price of
non-blocking cold renders (#961).

* fix: carry the client stale claim through KV, the prerender index, and nested cache hits

- writePrerenderIndex now copies `stale` into vinext-prerender.json so
  seedMemoryCacheFromPrerender actually receives it
- KVCacheHandler.set() and buildPrerenderKVPairs persist cacheControl.stale
  so warm hits on the Cloudflare KV backend replay the producing render's
  claim; validateCacheEntry accepts the field
- a nested use cache HIT pushes its stored lifetime into the enclosing
  cache context's lifeConfigs, mirroring the MISS path, so the outer entry
  keeps the child's stale claim once the outer goes warm

* perf: trim shipped comment bytes in the stale-time plumbing

The added modules ride in every consumer build environment; the review-grade
rationale lives in the PR body, the code keeps one-line constraints.

* refactor(app-router): model the cached client stale claim as one state

CachedRscResponse carried staleTimePending and staleTimeSeconds as two
independent optionals, so the cached form admitted a state the wire never
produces (pending and resolved at once) and every consumer had to encode
the precedence rule. Replace both with a discriminated serverStaleTime
(pending | resolved), collapsed once at the header-parse boundary.

* refactor(isr): give isrSet a cache-metadata write policy

The generic setter had grown to six positional arguments, the last an
App-Router-only `stale` value, with isrSetAppPage as a policy-object
wrapper that unpacked straight back into it. Take { cacheControl, tags }
instead: routers construct the metadata they actually claim, the wrapper
and its type disappear, and the shared isrCacheControl builder replaces
the cache-control literal duplicated across writers.

* fix(app-router): keep the dynamic bound on captured dynamic renders

The done-script metadata reused the RSC header's rule of dropping the
config-derived dynamic stale time while the speculative ISR capture was
armed. That rule only holds on the header path, which substitutes the
pending marker and its 30s floor; the done script emits the resolved
cacheLife instead, so a production render that turned dynamic shipped the
cacheLife claim as its only bound and let the hydration-seeded entry reuse
dynamic output for its full duration. Dev never took the capture path, so
the two diverged.

Also migrates the pages-basic seed fixture, the last positional isrSet
caller, which typecheck does not cover.

* ci: re-run after unrelated dev-overlay canary flake

* fix(app-router): preserve completed client stale metadata

* fix(app-router): strip completion metadata during HMR

* fix(app-router): stream completion metadata safely

* fix(app-router): validate completed stale metadata

* docs(cache): clarify zero stale client claim

---------

Co-authored-by: James <james@eli.cx>
2026-07-28 14:41:47 +01:00
Boy Steven f517288f40 fix(server): reject unsupported static asset methods (#2714)
* fix(server): reject unsupported static asset methods

* fix(server): route public asset mutations to 405

* chore: retrigger CI

* fix(server): preserve static method parity across runtimes

* fix(server): discard static probe responses safely

* fix(server): sanitize static method responses

* fix(server): preserve static method status

* fix(server): strip stale static range headers

* fix(server): preserve static rewrite ordering

* fix(dev): cache public file routes

* fix(dev): harden cached public route handling

* fix(worker): avoid unnecessary asset probes

* fix(server): preserve static asset lookup coverage

* fix(build): keep public path encoder internal

---------

Co-authored-by: James <james@eli.cx>
2026-07-28 13:59:20 +01:00
Tom Voss 0b58bbc8ff fix(metadata): preserve Content-Length for fully buffered responses (#2703)
* fix(metadata): preserve Content-Length for fully buffered responses

closeAfterResponseWithBody() wraps every body-bearing response in a
TransformStream to support function-form after(), which strips
Content-Length even when the body is already fully materialized.
Metadata file convention responses (robots(), sitemap(), manifest(),
static icons) always serialize to a string or byte array with no
producer left, so mark those bodies (markFullyBufferedBody) and skip
the wrap when nothing is registered.

Arbitrary Route Handler responses and a metadata route's Response
passthrough stay wrapped: their body's producer may still call after()
after the handler resolves, so a "nothing registered yet" check can't
prove them safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqasUFjUPt2Q8gkrzrLGyB

* fix(metadata): preserve deferred response lifecycle

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
2026-07-27 12:04:59 +00:00
James Anderson 8e36c914e0 fix(app-router): honor client cache stale times (#2449)
* fix(app-router): honor client cache stale times

* fix(app-router): preserve static reusable prefetch stale time

* fix(app-router): reset fresh page state after prefetch

* fix(app-router): preserve intercepted source state

* fix(app-router): preserve client cache segment identity

* fix(app-router): preserve dynamic slot cache semantics
2026-07-23 13:48:42 +01:00
James Anderson e7800ad75f fix(app-router): resolve interception-only RSC targets (#2256)
* fix(app-router): dispatch interception-only targets

* fix(app-router): preserve interception action rerenders

* fix(app-router): align interception request path matching

* test(app-router): distinguish encoded interception paths

* test(app-router): preserve intercepted source params
2026-07-22 20:36:49 +01:00
James Anderson eb3b15fdc2 fix(app-router): reconcile streamed metadata icons (#2320)
* fix(app-router): reinsert streamed metadata icons

* fix(app-router): harden streamed metadata icons

* fix(app-router): make streamed icon insertion hydration-safe

* fix(app-router): reconcile all streamed metadata icons

* fix(app-router): preserve distinct streamed icon descriptors

* fix(app-router): match metadata icon descriptor parity

* fix(app-router): sanitize streamed metadata icons

* test(app-router): allow streamed metadata siblings

* test(app-router): ignore icon attribute order

* fix(app-router): preserve parser-time icon order
2026-07-22 17:51:51 +01:00
James Anderson 5b6d0c867d fix(app-router): align prefetch server protocol (#2318)
* fix(app-router): align prefetch server protocol

* fix(app-router): keep prefetch state type internal

* fix(app-router): avoid consuming partial prefetches

* test(app-router): explicitly ignore router push result

* fix(app-router): preserve full prefetch semantics

* fix(app-router): preserve explicit full prefetch requests

* fix(app-router): guard malformed next-url inference

* test(app-router): clarify slot loading boundaries

* fix(app-router): publish transitional cache snapshots

* fix(app-router): canonicalize prefetched response URLs

* fix(app-router): preserve cross-origin prefetch responses
2026-07-22 13:12:09 +01:00
Nathan Nguyen 2824b7653a fix(router): replace stale optimistic layouts across dynamic params (#2609)
* fix(router): replace stale optimistic layouts across dynamic params

A detached optimistic shell can commit stale dynamic-layout output before the authoritative payload resolves. Preparing the latter from live router state then makes the shell appear current, so stale server props and BFCache identity survive a cross-param navigation.

All payloads in one navigation must derive reuse identity from the same initiation state. Capture that state once and pass it through commit preparation, while live router state remains the authority for cancellation and commit approval.

Add composition coverage for cross-param replacement, same-param preservation, and layout-owned slots, plus a deterministic browser regression for the prefetched-shell handoff.

* fix(router): require navigation initiation state for payload preparation

Navigation payload callers could omit the initiation state and silently prepare from live router state. A future caller could therefore compile while reintroducing the optimistic-to-authoritative identity bug.

Require the state at both browser-entry and controller boundaries and remove the live-state fallback. Isolated controller tests now choose current-state preparation through an explicitly named test helper.

* test(router): synchronize payload tests on state dispatch

Navigation payload regressions advanced a fixed number of microtasks before reading router state. That coupled the tests to the controller's current async scheduling depth.\n\nExpose a one-shot visible-commit dispatch waiter from the controller harness and await that explicit boundary before assertions.

* docs(router): document the currentState baseline in createPendingNavigationCommit

createPendingNavigationCommit's currentState param has no note on what
it should be. Navigation callers now pass the frozen navigation-initiation
state (per the previous two commits), while the HMR caller still passes
live state, and nothing marks that split as deliberate.

A future navigation call site that passes live state instead of the
initiation state would silently reintroduce the stale cross-param reuse
bug this branch fixes, with no type error to catch it.

Document the invariant on the field so the split reads as intentional.

---------

Co-authored-by: James <james@eli.cx>
2026-07-21 21:31:12 +01:00
ponharu 5494d182d2 feat(metadata): support viewport fields and parent resolution (#2644)
* feat(metadata): support viewport fields and parent resolution

* fix(metadata): resolve primary viewport before parallel slots

* fix(metadata): match viewport resolution ordering

---------

Co-authored-by: James <james@eli.cx>
2026-07-20 23:01:16 +01:00
Boy Steven 2fcc4443cc test(isr): cover production cache lifecycle (#2651)
* fix(isr): preserve cache headers on initial render

* fix(isr): preserve streaming on initial cache misses

* test(isr): run production lifecycle coverage in CI

* test(isr): wait for asynchronous cache writes

---------

Co-authored-by: James <james@eli.cx>
2026-07-20 21:33:50 +01:00
alex f73ae46edf fix(app-router): stream nested loading boundaries (#2641)
* fix(app-router): stream nested loading boundaries

* fix(app-router): preserve nested loading boundary parity

* fix(app-router): keep prefetch helpers internal

---------

Co-authored-by: James <james@eli.cx>
2026-07-20 11:51:56 +01:00
Nathan Nguyen c6f2a877d0 fix(app-router): stream generated metadata after the document shell (#2619)
* fix(app-router): stream generated metadata after the document shell

Dynamic App Router document renders currently await generateMetadata before constructing the page element tree. This delays response headers and the first HTML chunk for streaming-capable clients.

Head resolution coupled metadata and viewport into one awaited result, so the renderer could not suspend only metadata. Start the branches independently, keep blocking callers unchanged, and expose metadata through paired Suspense tag and error outlets. The hidden host wrapper preserves React's shell flush in the presence of hoistable metadata tags.

The focused head test verifies metadata remains pending while viewport resolution completes.

* test(app-router): verify generated metadata follows the production shell

Production coverage only established that HTML eventually arrived, so delaying the first byte until generateMetadata completed remained undetected.

Read the production response incrementally and require the page shell to precede delayed metadata while still asserting the final tags.

* test(app-router): align metadata error coverage with streamed responses

Streaming generateMetadata errors return a recoverable 200 shell before local or global boundaries render after hydration. The compatibility suite incorrectly expected those boundaries and 500 statuses in raw server HTML, which conflicts with Next.js 16.2.7 and fails the integration shard.

Update raw-response assertions to cover shell behavior and add browser coverage for page and layout metadata errors with and without local boundaries.

* fix(app-router): stream metadata during navigation

* fix(app-router): isolate connection probes from streaming metadata

Dynamic metadata prefetches can leave Flight responses open indefinitely when generateMetadata calls connection(). Streaming starts metadata in parallel with page classification, but the speculative probe mutated shared request state and captured the sibling metadata branch.

Run probes in a nested request scope, then propagate dynamic usage and new diagnostic errors back with concurrency-safe rules. This preserves classification while allowing sibling metadata work to complete.

* refactor(app-router): isolate fallback metadata planning

Streamed metadata fallbacks previously configured the general head resolver with traversal flags for boundary repetition, leaf ordering, and viewport suppression. That made fallback policy part of normal metadata resolution and obscured the RSC navigation transport contract.

Build an explicit HTTP-access fallback metadata source plan, then resolve it through the shared ordered metadata merger. Add focused planner coverage and pin delayed navigation redirects to HTTP 200 Flight digest transport.

* fix(app-router): preserve not-found metadata search params

Page-local not-found metadata lost searchParams after deferred metadata called notFound(), so query-derived tags were wrong and search access was invisible to dynamic-usage tracking. Terminal fallback rendering also recomputed the boundary head without the normalized query.

Classify not-found ownership from module identity and tree position, attach query state and its observer only for page-owned conventions, and thread normalized search params through terminal fallback rendering. Cover repeated fallback leaves, observer access, and a production page-local not-found route.

* fix(app-router): release completed connection probes

Async work created inside a speculative connection probe retained the child request store after the probe returned. Because that store still referenced the completed probe, a later connection() call suspended forever.

Restore the child store's currently inherited probe during deterministic cleanup. This preserves nested probe ownership while allowing late continuations to observe the completed scope, with a real AsyncLocalStorage regression covering the lifecycle.

* fix(app-router): preserve deferred metadata cache signals

Deferred metadata dynamic usage can overlap a speculative layout probe. The probe cleared and later consumed the shared request flag, allowing an RSC cache entry to be written even though the completed response was marked no-store.

The layout classifier treated save-and-restore mutation as async isolation. Run probe classification in a child dynamic-usage scope so sibling metadata retains the parent request state, and cover the overlap through the dispatch cache boundary.

* fix(app-router): preserve fallback metadata parity

---------

Co-authored-by: James <james@eli.cx>
2026-07-15 23:13:04 +01:00
James Anderson 0b6faab89d fix(cache): isolate draft route responses (#2591)
* fix(cache): isolate draft route handler responses

* fix(cache): enforce draft policy on route errors

* fix(cache): privatize draft mode transitions

* fix(cache): respect live draft transitions

* fix(cache): discard route cookies on failed handlers

* Reviewed PR #2591: solid fix, no blocks

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>

* test(cloudflare): use public CDN adapter export

* fix(ci): align Cloudflare fixture lock entry

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
2026-07-15 14:06:00 +01:00
James Anderson 4f7eec967e fix(router): avoid repeated App path decoding (#2556)
* fix(router): avoid repeated App path decoding

* test(router): align encoded middleware pathname

* fix(router): match Next encoded path semantics

* fix(prerender): accept canonical encoded app params

* fix(router): preserve encoded interception params

* test(build): tolerate watch output replacement

* test(router): cover repeatedly encoded production paths

* fix(router): preserve action request route identity

* refactor(middleware): remove stale pathname argument

* refactor(router): align fallback interception paths

* refactor(router): derive request path identity from value

* fix(router): preserve encoded route parity

* fix(router): preserve raw config path matching

* fix(router): preserve raw encoded route identity

* fix(router): canonicalize encoded dot segments

* fix(router): restore encoded path parity after main merge

* fix(app-router): exact-match generated params

* fix(app-router): chain generated parent params

* fix(app-router): preserve generated param ownership

* fix(app-router): preserve per-result param ownership

* fix(app-router): preserve params after empty parallel generation

* fix(app-router): chain parallel static params by branch
2026-07-14 22:09:29 +01:00
James Anderson de94652a1d fix(shims): align public API with vendored Next types (#2617)
* fix(shims): align public API with vendored Next types

* fix(shims): preserve revalidation and runtime behavior

* test(shims): align cache revalidation expectations

* fix(ci): package types for deploy suite
2026-07-14 19:32:33 +01:00
James Anderson b859a03bbd feat(types): ship Next-compatible types without Next.js (#2612)
* feat(types): add vendored Next.js declarations

* feat(types): expose fallback types through vinext

* fix(types): support readonly Node environment declarations

* fix(ci): install vendored types before vinext tarballs

* test(types): avoid brittle packed consumer pins

* fix(ci): configure pnpm 11 local type overrides

* test(types): avoid registry metadata in packed install

* test(types): isolate packed declaration install

* feat(init): add standard script aliases for new apps

* fix(create-vinext-app): run typegen portably

* fix(typegen): report next env updates

* fix(create-vinext-app): use standard scripts only

* refactor(create-vinext-app): use exhaustive package manager return

* fix(create-vinext-app): fall back for unknown package managers
2026-07-14 13:27:20 +01:00
Lubomir Georgiev (Любомир Георгиев) bbec971d8f fix(app-router): preserve streamed metadata placement parity (#2572)
* fix(app-router): hoist streamed metadata into <head> instead of body

Async generateMetadata was serialized via dangerouslySetInnerHTML into a
hidden <div> in the body, which React cannot hoist. For JS-capable clients
(browsers, Googlebot) the tags stayed in <body>, where Google ignores
rel=canonical, hreflang and robots.

Render the resolved metadata as real <title>/<meta>/<link> elements through
MetadataHead so React 19 hoists them into <head>, matching Next.js. Remove
the now-unused renderMetadataToHtml string serializer.

* Fix tests

* fix(app-router): harden metadata hoisting coverage

* fix(app-router): restore streamed metadata placement

* fix(app-router): preserve static metadata placement

---------

Co-authored-by: James <james@eli.cx>
2026-07-10 09:17:32 +01:00
James Anderson 5825c6807f fix(app-router): preserve client state during action revalidation (#2517)
* fix(app-router): preserve client state during action revalidation

* test(app-router): pin loading boundary to slot entry

* test(app-router): deploy action revalidation repro
2026-07-04 11:08:54 +01:00
James Anderson 6bd1ee0153 fix(app-router): expose app rewrites to pages data clients (#2468) 2026-07-02 21:03:05 +01:00
James Anderson 5f8048f660 fix(app-router): align app static ISR lifecycle (#2472)
* fix(app-router): align app static ISR lifecycle

* fix(fetch-cache): honor shorter revalidate on hits

* fix(cache): honor read-time revalidate in KV cache
2026-07-02 20:19:02 +01:00
James Anderson b0367f1448 fix(link): avoid reusing dynamic app route prefetches (#2450)
* fix(link): avoid reusing dynamic app route prefetches

* fix(link): preserve static dynamic app prefetches

* test(e2e): scope segment cache client params to prod
2026-07-01 11:14:11 +01:00
James Anderson de733ce4aa fix(app-router): revalidate dynamic intercepted actions (#2443) 2026-07-01 11:02:53 +01:00
James Anderson 49945a9e05 fix(link): reuse full prefetch loading shells (#2332)
* fix(link): reuse full prefetch loading shells

* fix(link): share loading shell prefetch ownership
2026-06-30 19:34:50 +01:00
Nathan Nguyen 23ed801656 fix(e2e): stabilize nav regression suspense window (#2408)
* fix(e2e): stabilize nav regression suspense window

* test(e2e): strengthen nav regression flake guard
2026-06-29 16:57:29 +01:00
James Anderson 011349b858 fix(app-router): match generated params case-insensitively (#2351)
* fix(app-router): match generated params case-insensitively

* test(app-router): preserve requested static param casing
2026-06-28 23:14:14 +00:00
James Anderson 0ea90fdbaa fix(app-router): preserve layouts across soft navigation (#2316)
* test(app-router): reproduce server layout identity reset

* fix(app-router): preserve soft navigation layout identity

* test(app-router): model bfcache identities in reducer fixtures

* fix(app-router): refresh layouts after revalidating actions

* fix(navigation): balance history restore snapshots

* fix(app-router): refresh layout elements during HMR commits
2026-06-28 23:07:55 +00:00