mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.1
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cda19e1c58 |
fix(app-router): preserve recent segment state with Activity BFCache (#1739)
* fix(app-router): preserve recent segment state with Activity BFCache Fresh App Router navigations were reusing the active React tree when moving between dynamic segment values. That leaked client state into a newly entered segment and made the upstream back-forward-cache suite fail before it even reached browser traversal assertions. The root cause was that vinext had history-entry bfcacheId plumbing but no separate segment state-key cache. Next.js treats those identities separately: render state is keyed by segment identity/pathname, while useRouter().bfcacheId identifies a concrete history entry. Add state-key maps for app router slots, render retained entries through React.Activity when cacheComponents is enabled, keep the active one-entry keyed reset path otherwise, and port focused Next.js regression coverage for the three-entry BFCache behavior. * fix(bfcache): store stateKeyMap in retained slot entries and re-provide on render Review feedback requested two changes: 1. Add stateKeyMap to BfcacheSlotEntry so each retained BFCache entry carries the state-key map that belonged to it when active, rather than borrowing the current navigation's map. This prevents nested hidden entries from being rendered with old elements but new pathname-derived state keys — an identity corruption bug that upstream Next.js avoids by rendering each retained entry from its own state. 2. Add a nested dynamic regression test: /nested/[section]/item/[id] with client counters in both the [section] layout and [id] page. The existing flat /page/[n] tests do not exercise nested retained-entry identity, which is exactly where the missing state-key-map bug lives. Changes: - BfcacheSlotEntry gains stateKeyMap?: Readonly<Record<string, string>> - updateBfcacheSlotEntries accepts and stores an optional stateKeyMap argument - BfcacheSlotBoundary stores the current stateKeyMap alongside elements when caching the active entry - Both the Activity visible/hidden path and the non-cacheComponents single-entry path now wrap rendered entries in BfcacheStateKeyMapContext.Provider using the entry's own stateKeyMap (falling back to current map for forward compatibility) - New e2e test verifies nested layout + page counter preservation across segment navigations * refactor(bfcache): extract shared provider stack into BfcacheEntryProviders BfcacheSlotBoundary rendered the BfcacheStateKeyMapContext / ElementsContext / SegmentContext provider stack in two places: the one-entry keyed reset path and the per-entry Activity retention list. The two stacks were identical except for where the React key sat, so the retained-entry bookkeeping and its render shape were tangled together and harder to audit than needed. Extract BfcacheEntryProviders as a pure presentational helper and render it from both paths. The keyed-reset path keeps its remount semantics: the key now sits on the BfcacheEntryProviders element, so a fresh active stateKey still remounts the whole provider subtree. Hook ordering is unchanged. All hooks stay above the early returns in BfcacheSlotBoundary, so the boundary still satisfies the Rules of Hooks across the activeStateKey undefined-to-defined transition. Covered by the slot BFCache entry-state suite in tests/shims.test.ts. * test(bfcache): isolate cacheComponents BFCache coverage in a dedicated fixture The back/forward-cache e2e suite ran against the shared app-basic fixture with cacheComponents: true. app-basic backs the single app-router Playwright server that every app-router spec uses, so enabling cacheComponents globally changed a core DOM invariant for unrelated tests: inactive route trees stay mounted as hidden Activity DOM. The contamination already leaked a :visible workaround into the interception-modal bfcacheId spec, which had nothing to do with Activity. cacheComponents is an opt-in semantic mode, not default app-router behaviour, so it does not belong on the shared baseline fixture. Move the BFCache routes into a dedicated tests/fixtures/app-bfcache fixture with cacheComponents: true, served by a new app-router-bfcache Playwright project on port 4183, and run the back-forward-cache spec there against the project baseURL. The :visible locators stay in that spec, where duplicate hidden DOM is the expected contract. Remove cacheComponents and the BFCache routes from app-basic so the shared fixture models default behaviour again, and revert the :visible workaround in use-router-bfcache-id.spec.ts now that the modal route no longer retains hidden DOM. Fix the nested item page import while moving it: nested/[section]/item/[id]/ page.tsx imported ../counter, but the counter lives two levels up at nested/[section]/counter.tsx, so it must be ../../counter. Validation: PLAYWRIGHT_PROJECT=app-router-bfcache (5 passed) and the full use-router-bfcache-id spec under app-router (11 passed), plus the slot BFCache unit suite (1172 passed) and vp check (formatting + lint + types clean). * ci: run the app-router-bfcache e2e project in the matrix The dedicated app-router-bfcache Playwright project was added to playwright.config.ts but not to the CI E2E matrix, so the back/forward-cache specs never ran in CI. Add it to the matrix project list so the BFCache suite runs on every push. It uses the default chromium browser, so the existing setup and browser-cache steps apply unchanged. * fix(bfcache): keep persisted slots mounted when cacheComponents is off Without cacheComponents there is no Activity retention, yet BfcacheSlotBoundary still keyed the active entry by its segment stateKey. That stateKey tracks the pathname (createBfcacheSegmentIdentity), so any slot whose identity moves with the URL (shared layouts, interception source slots) was remounted on navigation, discarding client state that survives a normal route change. The baseline router reconciles these slots in place. This regressed seven existing app-router e2e tests once cacheComponents was no longer masking it on the shared fixture: interception source-context and modal preservation in advanced.spec.ts, and layout-persistence.spec.ts dynamic-segment layout counter survival. Fresh-entry reset is driven by userland bfcacheId keying in the fixtures, not by remounting the slot subtree, so the keying was pure overreach in the non-cacheComponents path. Render the single active entry unkeyed when cacheComponents is off so the boundary reconciles in place like the baseline. The keyed Activity retention path is unchanged for cacheComponents. Validation: full app-router project 436 passed / 0 failed (the seven regressions now pass); use-router-bfcache-id 11 passed (fresh-push reset intact); app-bfcache 5 passed (Activity retention intact); shims unit suite 1024 passed. * docs(test): point bfcacheId spec comment at the app-router-bfcache project The header noted that Activity-backed preservation is covered in back-forward-cache.spec.ts, but that spec moved to the dedicated app-bfcache fixture under the app-router-bfcache project. Name the project and path so the cross-reference stays accurate. * docs: align cacheComponents comment with unkeyed active-segment behavior * test(bfcache): assert fresh bfcacheId after Activity-restored return * style: collapse locator chains per oxfmt * refactor(bfcache): split slot entry order from snapshots * fix(bfcache): keep non-cacheComponents slot path baseline * fix(bfcache): use string cacheComponents define * test: remove external fetch from revalidate tag fixture * fix(bfcache): harden Activity state key retention * docs(bfcache): document cacheComponents Activity caveats * fix(bfcache): preserve navigation context for fallback SSR Fallback boundary rendering can outlive the original request-scoped navigation context. That made app SSR fail fast with a missing navigation context when loading-boundary digest routes rendered their HTTP fallback shell. The SSR boundary contract still requires a navigation context for real request renders, but boundary renderers now synthesize the same typed shape from the request URL and matched params when request scope has already been cleared. * refactor(bfcache): normalize pathname in state-key map, document invariant callers Moves hydration-pathname normalization into createBfcacheSegmentStateKeyMap so SSR and client always produce byte-identical Activity keys. Documents the requireNavigationContext guaranteeing callers per review feedback. * fix(bfcache): tolerate malformed state-key pathnames * docs(bfcache): document known Next.js divergence and dead-branch contract * chore(deps): update lockfile for vite 0.1.24 * fix(app-router): preserve encoded delimiters in bfcache keys * docs(bfcache): track router-owned retention follow-up * chore: retrigger ci * fix(app-router): preserve redirect bridge fallback |
||
|
|
7d280edfbe |
refactor(server): replace prerender HTML buffering with metadataReady contract (#1809)
* refactor(server): replace prerender HTML buffering with metadataReady contract * fix(server): wait for allReady before piping through tick-buffered transform when waitForAllReady is true * refactor(server): clean up AppSsrRenderResult normalization and metadataReady promise resolution * test(server): cover ISR revalidation waitForAllReady; address review nits - Add focused test asserting stale HTML cache regeneration passes waitForAllReady: true to handleSsr so Suspense fallbacks never leak into the cache entry - Document why metadataReady resolves eagerly (allReady is already awaited for the prerender path) in app-ssr-entry.ts - Remove stray blank line left by the readStreamAsText import removal - Type capturedOnError in the prerender special-error test instead of any * fix(pages): export PagesPageModule type consumed by pages-page-handler The dts bundle build (vp pack) failed because pages-page-handler.ts imports PagesPageModule from pages-page-data.js, but the type was not exported, so rolldown-plugin-dts could not resolve it. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
9bab92f012 |
test(deploy): cover generated worker package exports (#1807)
Generated worker entries can import vinext subpaths that must resolve from the published package. Missing package exports break consumers even when the generated template itself looks correct. Add deploy coverage that extracts vinext subpath imports from generated App and Pages worker entries and verifies each one is covered by packages/vinext/package.json exports. Tighten the prefer-shared-utils lint rule so copied shared helpers in repo tests are reported too. Test files mask template literal fixture payloads, while source files still scan generated-source templates. |
||
|
|
1a480016f6 |
fix(app-router): track searchParams access for static bailout (#1788)
* fix(app-router): track searchParams access for static bailout App Router pages that awaited or used the searchParams prop during prerender could still be classified as static when the prerender request had an empty query string. That seeded no-query output into the deploy cache and served it for requests like ?search=hello. The dynamic decision was based on query-string content instead of page prop access. Observe the searchParams thenable itself, including React use() status reads, and mark searchParams dynamic only when the prop is consumed. * chore: rerun ci * fix(app-router): avoid static cache for searchParams pages * fix(app-router): tolerate synthetic page paths in manifest codegen * fix(app-router): observe searchParams access for static bailout * fix(app-router): preserve app page cache hits * fix(app-router): observe loading search params during render * refactor(app-router): extract searchParams probe fan-out into helper Move the per-request page probe fan-out (matched page + parallel slot pages + interception page) out of the generated RSC entry and into a unit-testable buildAppPageProbes() helper in server/app-page-probe.ts, keeping entries/app-rsc-entry.ts thin (AGENTS.md guidance). Addresses bonk review concern #3 on #1788. Document the known slot over-bail (probing inactive parallel-route slot pages can mark an otherwise-static page dynamic; fails safe toward dynamic) and track it as a caching-efficiency follow-up in #1798 (bonk review concern #1). * fix(app-router): skip probing interception-overridden slot pages Narrow the searchParams probe fan-out so it only probes page components that actually render for the request. When an interception matches, it replaces the page of the slot named by intercept.slotKey (the element builder sets overrides[slotKey].pageModule to the interception page, which wins over slot.page in app-page-route-wiring.tsx). We now probe the interception page in place of that slot's own page instead of probing both — probing the overridden slot page marked an otherwise static request dynamic for a component that never renders. Fixes the over-bail flagged in review of #1788. * docs(app-router): clarify buildAppPageProbes slot precision Address review feedback: the doc comment overstated precision without explaining why non-overridden slots are exact. Spell out that a slot with a page.tsx always renders that page (so probing it is correct), default-only slots have no slot.page.default so probing is a no-op (not an over-bail), and a default.tsx awaiting searchParams is backstopped by the real render's observation (so no under-bail). * fix(app-router): gate searchParams interception probe on isRscRequest Interception only fires for RSC navigations (resolveAppPageInterceptState returns kind:none when !isRscRequest, app-page-request.ts:324). The probe fan-out called findIntercept unconditionally, so on HTML requests it would skip the matched route's slot page (which DOES render normally) and probe the interception page (which never renders). buildAppPageProbes now ignores the interception match when !isRscRequest, so HTML requests probe every slot's own page and skip the interception probe. The source-route interception case (a different route renders) never reaches this probe: dispatchAppPage returns the intercepted response before calling probePage, so any interception seen here is the current-route override case. Documented both in the buildAppPageProbes doc comment. Addresses review feedback on #1788. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
65eaea49a4 |
feat(skip): omit proven static layouts from RSC transport (#1437)
* feat(skip): omit proven static layouts from RSC transport
App Router RSC responses re-send every layout payload on each navigation, even
when the client already holds an identical static layout from a sibling route.
That repeats transport bytes on the common sibling-page navigation path.
When the client sends a verified reuse manifest, the render lifecycle now runs a
skip-transport planner. Each manifest entry is cross-checked against the
static-layout cache proof (exact artifact compatibility, variant cache key,
payload hash, and invalidation). Layouts that pass are omitted from the outgoing
RSC payload, recorded under __skippedLayoutIds, and their per-element render
dependencies are released. The skip disposition also bypasses the RSC cache so a
partial payload never enters a shared cache, and it is suppressed for HTML
responses.
This is the enable slice. Layouts are gated on the cache proof, not yet on
per-layout runtime observations. The observation-based safety gates (param
scope, request APIs, finite revalidate, cacheLife, unstable_cache, cache-tagged
and dynamic fetches) land in the stacked hardening follow-up.
* fix(skip): preserve parallel slots owned by skipped static layouts
When the skip-transport merge restores a server-omitted static layout into
preserveElementIds, it must also restore the default/unmatched parallel slots
that layout owns. Otherwise the planner invariant ("every preserved slot's owner
layout is present in preserveElementIds, and vice versa") is violated: in the
topology-unknown path the planner returns empty slot persistence, so a
slot-owning layout skipped server-side commits with a missing slot
(mergeElements starts from the next payload and, with preserveAbsentSlots:
false, never restores it).
mergeSkippedLayoutPreservation now derives the owned slots for newly-preserved
skipped layouts via the planner's resolveDefaultOrUnmatchedSlotPersistenceForLayouts,
covering both the topology-unknown path and the known-topology past-divergence
case. Added a regression with a mounted @slot default that fails without the fix.
Also hardcode the static-layout cache-proof's observed request-API set to []
(with a comment naming the invariant) instead of reading it back from the
per-layout observation, so a future reordering of the classification gate cannot
feed stale request-API reads into the synthetic proof. Reaching that point
already requires staticLayoutIds membership, which excludes any layout that
observed a request API.
* docs(skip): explain broader-than-planner layout membership guard
Per review: clarify that the set-membership check in
mergeSkippedLayoutPreservation is intentionally broader than the planner's
prefix-based persistence, and why preserving a proven-identical layout past the
ancestor divergence point is sound. Comment-only.
* refactor(skip): drop redundant cacheable-fetch observation record
Per review: the post-key-generation recordCacheableFetchObservation in
fetch-cache.ts was strictly redundant. The synchronous record before the first
await already covers every path that reaches it (the only intervening early
return records a dynamic observation instead), and the store is an idempotent
set, so the second call added no coverage.
* fix(test): match real RSC request URL in client-reuse-manifest e2e
The 'sends a client reuse manifest for retained static layouts on soft
navigation' e2e hand-rolled its RSC-request matcher as
url.pathname === '/client-nav-test.rsc', but App Router soft-navigation RSC
requests use the bare pathname with a _rsc query param and an rsc: 1 header
(e.g. GET /client-nav-test?_rsc), so the predicate never matched and
page.waitForResponse timed out at 30s. The manifest header was being attached
correctly the whole time. Use the shared isAppRouterRscRequestForPath helper
(as the sibling test already does) so the matcher tracks the real request shape.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
49d2f0966d |
feat(skip): plumb client reuse manifests through the app request path (#1717)
* feat(skip): plumb client reuse manifests through the app request path The browser never sends a client reuse manifest and the server has no path to receive one, so the static-layout skip planner (#1674) and observation foundations (#1672) have nothing to act on. Skip transport stays fully off, but the transport wiring those slices depend on is missing. Add a bounded browser producer that derives a client reuse manifest from the visible AppElements state and attaches it as X-Vinext-Client-Reuse-Manifest only on soft navigations. Parse the header into a render/send-by-default disposition and thread the parsed result through the RSC handler, the generated app entry, page dispatch, and the render lifecycle. Wire the per-layout observation tracker and segment-config classification into dispatch so the enable-transport slice can read them. No RSC payload is omitted and no response cache-control changes: the parsed manifest is carried but never authorizes a skip yet. Unproven and absent- manifest requests render and send exactly as before. Slice 3 of 4 from #1437; enable-transport follows. Refs #726 * fix(skip): address review feedback on manifest plumbing - app-page-execution: defer layout debug classification until after the observation override so the emitted reason agrees with the final flag; emit a runtime-probe reason when an observation flips a build-time static layout to dynamic. - app-page-dispatch: document classifyLayoutSegmentConfigFromModule as the request-time counterpart to the build-time classifier, noting the intentional revalidate===false divergence and the dev-mode classification. - app-render-dependency: mark registerAppElementRenderDependencies as write-only until the enable slice's consumer lands. * docs(skip): clarify layout classification comments per review - app-page-dispatch: correct the classifyLayoutSegmentConfigFromModule docblock — the build-time classifier already normalizes revalidate=false to Infinity, so both agree on that case; the real difference is scope (request-time classifies dev-mode layouts not captured at build time). - app-page-dispatch: document the merge precedence in createEffectiveLayoutClassifications — module segment config is authoritative over the build-time map. * test(skip): pin observation override flipping build-time static layout to dynamic Adds a parity assertion for the request-time behavioral change the review flagged: isLayoutObservationDynamic downgrades a build-time `static` layout to `d` (and emits a runtime-probe reason) when the observed render is unsafe for static reuse, while a safe observation keeps it static. Complements the existing children-subtree probe assertion in app-page-route-wiring.test.ts. * fix(skip): fold probe-scoped dynamic usage into layout observation The probe rewrite runs layout probes inside runWithIsolatedProbeDependencies, which opens a child unified scope and resets dynamicUsageDetected. A markDynamicUsage() during the probe that leaves no other observable trace (e.g. "use cache: private") was therefore discarded on scope exit, so the parent's consumeDynamicUsage() in the Layer-3 path returned false and the layout could classify static where the pre-PR direct probe classified dynamic. recordProbeDependencies now peeks the child scope's dynamic-usage flag before it is reset and folds it into the per-layout observation (dynamicUsageObserved), which isAppLayoutObservationUnsafeForStaticReuse treats as unsafe. This keeps the observation signal a superset of the markDynamicUsage() triggers the Layer-3 dynamicDetected path covered, so the two signals can no longer diverge into a false-static. Adds peekDynamicUsage() (non-resetting read) and a regression test. * perf(skip): defer client reuse manifest production to the cache-miss path The manifest header is excluded from VINEXT_RSC_VARY_HEADER, so it never affects the cache-busting URL or the visited-response cache lookup. Producing it up front ran createClientReuseManifestHeaderFromVisibleAppState (iterates visible layout ids, builds a cache-variant proof per static layout, then binary-searches the byte budget) on every soft navigate — including ones that resolve entirely from the visited-response cache or a prefetch and never send a request, where the work is pure waste. Move production to just before the network fetch, gated on the same `navigationKind === "navigate"` condition and computed from the captured nav-start router state, so the emitted header is unchanged on the request path while the cache-hit/prefetch-hit hot paths skip it entirely. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
bdbd1f833d |
fix(app-router): preserve source identity for intercepted renders (#1589)
* fix(app-router): preserve source identity for intercepted renders Intercepted App Router renders used the visible target pathname as the payload route identity. That let a modal navigation such as /feed to /photos/42 replace the retained source tree with target-scoped route and page ids, so refreshes, server actions, chained modals, and history traversal could lose source page state. The violated invariant is that an intercepted render has separate display and matched-route identities. This commits a fenced render identity helper, keeps source route and page ids stable for retained source-route payloads, leaves the target pathname in interception proof, and includes source context in RSC cache variants. Regression coverage asserts source payload keys, cache variants, modal refresh/action/history flows, and source client/search state preservation. * fix(app-router): normalize intercepted rsc cache source * fix(app-router): harden intercepted cache regeneration * refactor(app-router): share intercepted path normalization * fixup! fix(app-router): preserve source identity for intercepted renders fixup! fix(app-router): preserve source identity for intercepted renders |
||
|
|
a0b8ae068a |
fix(draft-mode): keep draft secret out of client defines (#1592)
* fix(draft-mode): keep draft secret out of client defines The draft-mode bypass token was emitted through Vite define, so Vite's client env payload and client chunks could contain the server-only cookie secret when app code referenced that env key. The violated boundary was treating a server runtime credential like a shared compile-time define. Generate one plugin-scoped secret and initialize server entries with it, while next/headers reads only internal server state. Adds regression coverage for dev env exposure, production client bundle leakage, and the built server draft cookie round trip. * test(draft-mode): initialize secret after module reset * fix(draft-mode): scope draft secret to request context Draft mode validation briefly lived in a package-global singleton after removing the client-visible define. That made multiple app instances in the same process able to overwrite each other's validator. Thread the generated app secret through the App Router request and dispatch boundaries, store it on HeadersContext, and make cache bypass validation a pure request-plus-secret check. Add a regression that proves two contexts keep independent draft validators. * fix(draft-mode): guard stale draft controls |
||
|
|
c84e383ccd |
fix(server): render progressive action not-found HTML (#1254)
* fix(server): render progressive action not-found HTML Progressive multipart server actions returned an empty status response when an action called notFound() or another HTTP fallback. That diverged from Next.js' MPA action path, where the action error re-enters app rendering so the not-found boundary produces full HTML. The root cause was treating HTTP fallback action throws as terminal responses in the action execution helper. Pass action failures through the progressive form-state path, preserve redirects as 303 responses, and rethrow action failures during page element construction so existing special-error rendering handles the response body. Regression coverage asserts the action helper handoff and the rendered not-found HTML body. * chore: retrigger CI * fix(server): forward progressive action failures in RSC entry * refactor(server): make progressive action failure state explicit * Apply suggestion from @james-elicx --------- Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
b4898289e7 |
fix(app-router): match Next.js RSC Content-Type and 404 plain-text body (#1233)
* fix(app-router): match Next.js RSC Content-Type (no charset suffix)
Next.js sets the RSC (React Server Components / 'flight') response
Content-Type to exactly 'text/x-component' (RSC_CONTENT_TYPE_HEADER in
.nextjs-ref/packages/next/src/client/components/app-router-headers.ts:17,
used by FlightRenderResult in
.nextjs-ref/packages/next/src/server/app-render/flight-render-result.ts:15).
vinext was sending 'text/x-component; charset=utf-8'. The suffix breaks
Next.js's own segment-cache deduplication checks (action-handler.ts uses
.startsWith(RSC_CONTENT_TYPE_HEADER) on the unsuffixed string) and fails
Next.js e2e tests that assert strict equality, e.g.:
- test/e2e/app-dir/app/index.test.ts L362, L371
- test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts L80
Replace the duplicated literal at seven sites with the existing
VINEXT_RSC_CONTENT_TYPE constant in app-rsc-cache-busting.ts.
* fix(app-router): match Next.js 404 plain-text body 'This page could not be found'
Next.js's plain-text 404 response body is exactly
'This page could not be found' (no trailing period in the response body;
the React-rendered not-found component uses the same text with a period).
vinext was returning 'Not Found' / '404 Not Found' / '404 - Not found',
which fails Next.js e2e tests that assert .toContain('This page could
not be found'), e.g.:
- test/e2e/app-dir/app/index.test.ts (several cases)
- test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts L52
- test/e2e/app-dir/prefetching-not-found/prefetching-not-found.test.ts L16
Next.js source citations (in .nextjs-ref):
- packages/next/src/server/route-modules/pages/pages-handler.ts L121, L535
- packages/next/src/build/templates/app-route.ts L170, L349
- packages/next/src/build/templates/app-page.ts L701, L1043
- packages/next/src/pages/_error.tsx L7
- packages/next/src/client/components/builtin/not-found.tsx L7 (HTML variant)
Updates the body in notFoundResponse() and the open-redirect / pages-router
SSR-fallback / worker-entry inline 404 sites. The HTTP status reason phrase
('Not Found') is unchanged. Route-specific bodies like '404 - API route not
found' are intentionally retained.
|
||
|
|
11cc64a3af |
fix(app-router): preserve useActionState form state after hydration (#1162)
* fix(app-router): preserve useActionState form state after hydration Progressive multipart server actions executed before the initial HTML render were dropping React form state. That meant useActionState hydrated from the JavaScript initializer instead of the server action result after a no-JS form submission. The progressive action path now decodes React form state, carries it through the App Router render pipeline, passes it to React's server and client hydration APIs, and marks form-state HTML responses as uncacheable so action results cannot be replaced by stale ISR HTML. Adds targeted unit coverage for action decoding, SSR forwarding, cache policy, cache read bypass, and a Playwright no-JS useActionState regression. * fix(app-router): keep progressive action result type internal Knip reported ProgressiveServerActionResult as an unused exported type. The type is only part of this module's implementation detail for the progressive action return path, so keep it module-local. * fix(app-router): separate progressive action cache bypass * chore(app-router): tighten form state bootstrap wiring |
||
|
|
937f5d6a09 |
fix(app-router): read indefinite app page cache entries (#1129)
* 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 |
||
|
|
38e6b9f6c2 |
fix(app-router): bypass app page cache in draft mode (#1123)
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. |
||
|
|
de00ed2e68 |
refactor(app-router): extract app page dispatch (#986)
* refactor(app-router): extract app page dispatch App Router page requests now enter a shared dispatch module after generated route matching. The generated RSC entry still describes route modules, app-shape callbacks, and request-local values, but cache reads, static generation context, dynamic param checks, intercept responses, and lifecycle rendering move into normal typed code. This keeps page orchestration out of codegen without changing the observable App Router behavior covered by the integration suite. * chore: rerun ci |