mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.8
63 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fbf68d588e |
fix(app-router): restore cache and parallel route parity (#2261)
* fix(app-router): preserve searchParams cache variants * test(app-router): cover RSC metadata cache variants * fix(app-router): isolate metadata searchParams observation * fix(app-router): preserve static ISR cache proofs * fix(app-router): limit queryless cache probes * fix(app-router): skip redundant cache proof probes * fix(app-router): harden incomplete cache proofs * fix(app-router): observe loading page execution * fix(app-router): preserve exotic page exports * fix(app-router): preserve client page ISR * fix(app-router): avoid duplicate queryless page execution * fix(app-router): observe viewport search params * fix(app-router): clear force-static search params * fix(app-router): isolate intercept static context * fix(app-router): preserve intercept request context * fix(app-router): isolate action static context * fix(app-router): match static request API semantics * fix(app-router): preserve dynamic segment constraints * fix(app-router): apply slot-only segment config * fix(app-router): preserve draft mode reads in static errors * fix(app-router): include nested slot constraints * fix(app-router): refresh slot layout constraints * fix(app-router): render nested slot layouts * fix(app-router): preserve nested slot params * fix(app-router): preserve intercepted slot head * fix(app-router): scope parallel layout params * fix(app-router): preserve intercept layout parity * fix(app-router): preserve intercept ancestor params * fix(app-router): scope intercept source params * fix(app-router): preserve intercept source route params * test(app-router): stabilize parity validation |
||
|
|
95b320f79f |
perf(app): omit unused PPR runtime (#2223)
Co-authored-by: James <james@eli.cx> |
||
|
|
eb18704318 | perf(app): omit unused file metadata runtime (#2224) | ||
|
|
7c5f4c592a | perf(app): omit unused server action runtime (#2206) | ||
|
|
b9653e8a3a |
perf(app): omit unused metadata route runtime (#2196)
* perf(app): omit unused metadata route runtime * fix(app): keep metadata route helper type private --------- Co-authored-by: James <jamesanderson@cloudflare.com> |
||
|
|
9970430c29 | perf(app): omit unused middleware runtime (#2194) | ||
|
|
301418a3f1 |
perf(app-router): defer layout, template and boundary modules to first request (#2145)
* perf(app-router): defer layout, template and boundary modules to first request The generated RSC entry statically imported every route's layout, template, loading/error/not-found/forbidden/unauthorized boundary and parallel-slot module at the top level. On a cold isolate — and on the dev server's first request — that eagerly evaluated the whole app's segment tree, including routes never visited in that lifetime. Page and route-handler modules were already lazy; the surrounding segment modules were not. Emit those modules as lazy `() => import()` loaders paired with `null` placeholders in the manifest, and hydrate them onto the route's synchronous fields in `ensureAppRouteModulesLoaded` before any read (extended from page/handler to layouts, templates, boundaries and parallel-slot modules). Intercept layouts hydrate via the dedicated `loadAppInterceptLayouts` because they live on the match object, not the route. Every reader already runs behind `ensureAppRouteModulesLoaded` (handler dispatch and `buildPageElements`) or the intercept resolvers, so no synchronous module field is read before hydration. The shared intercept `interceptLayouts` arrays keep their `readonly` public types; the loader is their sole writer and fills slots through one boundary-local cast, so route metadata stays immutable to callers. Dev cold start is unchanged on page-heavy apps — pages were already lazy — but on a boundary-heavy app (80 independent sections, each with layout+template+error+loading+not-found) first-request module compile drops 704ms -> 534ms (~24%) and time-to-first-200 1917ms -> 1665ms. The saving scales with the number of unrelated segments deferred. * test(app-router): localize react max header font fixture * fix(app-router): dedupe lazy intercept module loads * fix(app-router): share intercept load state across matches * docs(app-router): explain root layout loader reuse --------- Co-authored-by: James <james@eli.cx> |
||
|
|
59a2de53a3 |
perf(app-router): defer action and route-handler runtimes (#2079)
* perf(app-router): defer action and route-handler runtimes * perf(app-router): defer middleware runtime * perf(rsc): isolate server navigation runtime * perf(ssr): isolate bfcache identity runtime * perf(ssr): avoid browser navigation runtime * refactor(navigation): share server-safe primitives * fix(ssr): track error boundary pathname in fallback shells --------- Co-authored-by: James <james@eli.cx> |
||
|
|
56d7311994 | perf(dev): externalize App Router request handler (#2108) | ||
|
|
bacee9ef52 |
fix(pages): emit canonical __NEXT_DATA__ JSON (#2043)
* fix(pages): emit canonical next data script * test(pages): expect canonical next data markup * fix(pages): initialize canonical next data before routing * test(pages): parse canonical next data markup * fix(pages): preserve router bootstrap ordering * test(pages): expect readiness bootstrap import * fix(pages): guard readiness bootstrap without next data |
||
|
|
b58adb3cfa | fix(middleware): classify Pages data requests by URL (#2039) | ||
|
|
a3d2f92152 |
fix(router): honor hybrid pages route priority (#1997)
* fix(router): honor hybrid pages route priority * fix(router): share hybrid owner decision with client navigation PR #1997 fixed the server-side route ownership for direct document loads, but the same invariant broke for client-side soft navigations and the matching prefetch path: - navigateClientSide() always delegated to the App runtime's RSC fetch, even when the Pages route had higher priority. renderPagesFallback() short-circuits RSC requests with null, so the App catch-all won. - prefetchUrl() prefetched an RSC stream for any URL that matched an App route, again ignoring Pages ownership. - The Pages entry was loaded on every hybrid request, even when a static App route had already matched (Pages can never win in that case). Expose the Pages route manifest on the client via a new __VINEXT_PAGES_LINK_PREFETCH_ROUTES__ window global emitted by both the App and Pages browser entries. The link shim consults a shared resolveHybridClientRouteOwner helper that mirrors the server-side pagesRouteHasPriorityOverAppRoute comparison. When Pages owns the URL, the click handler issues a window.location navigation and the prefetch path returns early. Gate the renderPagesFallback call behind a static-App-route check in handleAppRscRequest: when a static App route matches, the bridge cannot win, so skip the eager Pages entry load. Centralise the comparison in a new resolveHybridRouteOwner helper so server and client reach the same answer for the same (URL, route pair). Adds the missing client-navigation coverage to the use-params e2e fixture (Link from /app/ to /pages-dir/foobar) and unit tests for the shared owner decision. * fix(router): mirror server hybrid owner decision on the client PR review flagged two split-brain bugs in the previous hybrid ownership fix: a hand-copied client comparator and a Link-only ownership gate that left programmatic App Router navigations on the wrong path. The hand-copied routePrecedence in hybrid-client-route-owner.ts omitted the static-prefix reduction that lives in routing/utils.ts#routePrecedence, and used a strict-less-than comparison that returned App for identical dynamic patterns. The server returns Pages for both cases (Pages providers sort ahead of App providers, and routePrecedence subtracts 50 per static prefix segment). The split produced a real ownership disagreement on overlapping patterns like /_sites/:slug* (Pages) vs /:slug* (App). Add a shared compareHybridRoutePatterns to routing/utils.ts as the single source of truth: the static/dynamic short-circuits plus a sortRoutes call (which carries the static-prefix reduction and the Pages-first equal-pattern tiebreak). The server pagesRouteHasPriorityOverAppRoute and the client resolveHybridClientRouteOwner both delegate to it, so they cannot diverge. Drop the hand-copied routePrecedence entirely. Wire the ownership check at the App navigation runtime boundary so useRouter().push, useRouter().replace, gesturePush, and form submits all get the same hard-nav contract as the Link click handler. Specifically: - navigateClientSide: after same-origin normalization, if Pages owns the URL, hard-navigate via window.location and return (matching the existing external-URL branch). - _appRouter.prefetch: short-circuit RSC URL construction for Pages-owned targets so we do not warm an unusable cache entry. Centralise the existing two inline hard-nav branches in navigateClientSide into a hardNavigateTo helper for clarity. Tests: - Direct unit tests for compareHybridRoutePatterns covering identical-dynamic tiebreak, static-prefix dynamic overlap, static-prefix catch-all overlap, and infix-static bonus. - Direct unit tests for resolveHybridClientRouteOwner mirroring the server assertions plus a basePath-stripping test. - e2e: useRouter().push('/pages-dir/foobar') from an App page resolves to the Pages document. - e2e: useRouter().prefetch('/pages-dir/foobar') issues zero RSC requests for the target URL. * docs(router): fix stale score in static-prefix catch-all test comment The hand-copied comparator note quoted the dynamic-segment scores (51 / 1000) for the optional-catch-all example (/_sites/:slug* vs /:slug*). The current optional-catch-all scoring actually produces 1951 vs 2000. The test assertion was correct; only the comment was stale. * test(use-params): fix direct-load single dynamic param to use /a instead of /a/b * fix(router): compare hybrid routes structurally * fix(router): reject hybrid route conflicts * fix(router): refresh hybrid route ownership * fix(router): preserve hybrid routing lifecycle * fix(router): recheck pages routes after rewrites * fix(router): preserve rewritten pages queries * fix(router): preserve rewritten route ownership * fix(router): resolve client rewrites sequentially * fix(router): apply rewrite phases sequentially * fix(router): preserve rewrite params and endpoints * fix(router): hand off endpoint navigations * fix(router): preserve rewrite fragment params * test(e2e): avoid hybrid fixture route conflicts * test(pages): await async config validation * test(hybrid): avoid duplicate page fixtures * chore(router): clarify hybrid priority semantics * refactor(router): remove duplicate app route matcher * docs(router): clarify client hybrid comparator --------- Co-authored-by: James <james@eli.cx> |
||
|
|
b7ef466742 |
fix(pages): preserve app props for GSSP requests (#1996)
* fix(pages): preserve app props for gssp requests
* docs(pages): update _next/data comment to reflect full props envelope
* fix(pages-router): preserve _app.getInitialProps during stale ISR regeneration
Stale ISR background regeneration called getStaticProps directly and rendered
HTML and __NEXT_DATA__ with only the page's pageProps, dropping app-level
props from _app.getInitialProps. The foreground path already loads
_app.getInitialProps first and merges its props, but the background
regeneration path skipped that step.
Extract a shared loadPagesAppInitialRenderProps helper from the production
foreground path and reuse it during background regeneration in both
pages-page-data.ts and dev-server.ts. This ensures regenerated pages carry the
same full props envelope as the original render.
Add a regression test in tests/pages-page-data.test.ts.
* fix(pages-router): skip _app.getInitialProps on ISR cache hits
The foreground render path ran _app.getInitialProps before consulting the
ISR cache, so fresh and stale cache hits executed userland App data code
even though the cached HTML was returned verbatim. That wasted work on the
hot path and ran side-effecting App code whose result the cached response
never reflected.
The eager App initial-props load happened up front, ahead of the HIT and
STALE short-circuits. Defer it until the request will actually render:
load it lazily inside the getServerSideProps / getStaticProps /
getInitialProps branches, after the cache short-circuits return.
Stale-while-revalidate already re-runs it inside the regeneration closure,
so regenerated HTML still carries app-level props. The dev SSR handler and
the production page-data resolver are updated in lockstep to preserve
dev/prod parity.
Tests cover the fresh-HIT skip and the stale foreground-skip vs
regeneration-run split at the page-data resolver boundary.
* refactor(pages-router): extract dev App getInitialProps loader into a tested helper
The dev SSR handler ran _app.getInitialProps inside an inline closure that
mutated captured pageProps/renderProps and read res directly, so its lazy
load-only-on-render behavior could only be covered by a heavyweight
integration test that vi.doMock'd the isr-cache and file-matcher modules —
a mock cascade with little signal.
Move the decision into loadDevAppInitialProps in pages-get-initial-props.ts.
It takes the App component, req/res, and an injected AppTree builder (keeping
React out of the module) and returns a discriminated result —
skip / response-sent / render — that the caller applies. The dev-server
closure is now thin glue: build the React tree, delegate, apply the result.
Adds focused unit tests that drive the helper through its real boundary
(genuine getInitialProps functions, plain req/res inputs) — no module mocks:
the skip path, App/page prop merging, pageProps normalization, the
response-sent short-circuit, and the router/ctx wiring passed to userland.
* fix(pages): preserve app props across data requests
* fix(pages): preserve fallback cache expiry
* fix(pages): preserve dev app context during ISR
* fix(pages): hydrate middleware rewrite fallbacks
* fix(pages): retarget middleware data rewrites
* fix(pages): preserve rewrite query on navigation
* fix(pages): finalize rewrite navigation state
* fix(pages): resolve rewrite data targets consistently
* fix(pages): await static props during regeneration
* fix(pages): preserve original query in resolved url
* Revert "fix(pages): await static props during regeneration"
This reverts commit
|
||
|
|
4baf85ad5e |
fix(router): restore pages scroll traversal state (#1999)
* fix(router): restore pages scroll traversal state * fix(router): preserve pages component identity across HTML fallback Pages Router HTML fallback could import the module URL from fetched __NEXT_DATA__ even when the generated page loader already existed. In Vite chunk graphs that can evaluate a duplicate page or router module, same-route param navigations remounted the page and lost listener state before routeChangeComplete. Store the router wrapper component types on the shared browser runtime singleton, prefer registered page loaders on the HTML fallback path, and resolve navigation at the root commit boundary that Next.js awaits instead of waiting for passive effects. Non-DOM document mocks still resolve immediately so unit harnesses do not masquerade as real browser commits. Validated with the upstream reload-scroll-backforward-restoration deploy suite and the local router-events and scroll-restoration regressions. * test(router): simulate pages router commits in shims tests * fix(router): move Pages Router readiness into shared runtime state Move the pagesRouterReady bit into PagesRouterRuntimeState so duplicated next/router module instances (client entry + page chunks) observe the same readiness value. The initial value is still computed from shouldDeferInitialPagesRouterReady() once per document. Also tighten the scroll-restoration e2e fixture so routeChangeComplete assertions cannot pass from stale page state, and add a regression test that simulates two evaluated router instances on a deferred-ready page. * fix(router): early-exit scroll restoration when target unreachable, document useRouter fallback Addresses ask-bonk review feedback on PR #1999: - restorePagesRouterScrollPosition now breaks early when window.scrollY stops changing across frames, avoiding the full 60-frame budget when the saved scroll position exceeds the rendered page height. - useRouter() fallback path now has an explanatory comment documenting the non-reactive singleton return is intentional for split-chunk compat. * fix(router): harden scroll restoration settling --------- Co-authored-by: James <james@eli.cx> |
||
|
|
b4c829d6a2 |
feat(interception): sibling-style interception routes (#1364 Part C) (#1804)
* test(interception): add failing sibling-interception graph tests * feat(routing): discover sibling-style interception markers * feat(routing): emit sibling interception manifest facts * feat(app-router): serialize sibling intercepts in route manifest * feat(app-router): match sibling intercepts in route matcher * feat(app-router): render sibling intercept as full page response * test: add siblingIntercepts field to AppRoute test fixtures * test(interception): add sibling interception fixtures * fix(app-router): emit null proof for sibling intercepts to bypass slot-validation * test(interception): add sibling interception e2e specs * fix(interception): correct (...)→(..)(..) marker in interception-from-root fixture * fix(interception): resolve sourceRouteIndex to slot-owner route in intercept lookup When a route inherits a @slot from an ancestor via discoverInheritedParallelSlots, the inherited slot's interceptingRoutes are copied from the ancestor. Previously, createInterceptLookup used routeIndex (the inheriting descendant's index) as sourceRouteIndex, causing resolveAppPageInterceptState to see sourceRoute === currentRoute → kind='current-route' instead of kind='source-route'. Fix: build a patternToIndex map and resolve sourceRouteIndex via the intercept's sourceMatchPattern (which names the actual slot-owner route), falling back to routeIndex only when no match is found. * fix(interception): use locale-prefixed href so middleware interception fires correctly * fix(interception): address codex review issues in sibling intercept implementation - Pass effectiveParams to resolveAppPageHead so generateMetadata/generateViewport sees the intercepted route's params instead of the source route's params - Rename SIBLING_INTERCEPT_SLOT_NAME from __page to __vinext_sibling_intercept to prevent collision with a user @__page parallel route directory - Restore middleware-rewrite fixture link without locale prefix so the interception-dynamic-segment-middleware spec exercises the intended middleware rewrite path * fix(test): update slotId assertions and restore locale-prefixed fixture link - Update test assertions from slot:__page to slot:__vinext_sibling_intercept following the rename in commit |
||
|
|
5dd7a34eb6 |
fix(app-router): isolate page CSS chunks in production (#1738)
* fix(app-router): isolate production page CSS chunks Production App Router builds statically imported every page module into the generated RSC manifest. That let Vite/Rolldown concatenate page-level global CSS from sibling routes into shared CSS assets, so a sibling hash page with scroll-padding-top changed native hash scroll offsets on unrelated pages. The manifest now emits cached dynamic loaders for page modules and loads the matched page module at the route dispatch boundary. Page generateStaticParams sources use the same loader with an explicit missing-export sentinel so prerender still distinguishes absent exports from malformed results. * test(app-router): add focused unit test for route module loader Address the non-blocking review note from PR #1738: per AGENTS.md, behavior moved out of a template string should get a focused helper test. Cover createRouteModuleLoader (promise dedup, rejection propagation), loadRouteModules (page assignment, concurrent dedup, no-op cases, missing `page` key) and loadRouteMatch (null short-circuit, match load). Also cover createLazyGenerateStaticParamsSource (delegation, missing sentinel for null/empty/non-function exports). Document the rejected-load cache intent and the per-request intercept copy in the WeakMap so future readers don't try to "fix" them as bugs. * test: stabilize dev overlay HMR recovery * chore: retrigger ci * chore: fix knip check config * test(app-router): guard intercepted modal CSS + load lazy page before probe Adds the positive-direction guard the bonk review asked for: navigating /feed -> intercepted /photos/[id] must load the lazy modal page and emit its scroll-padding-top CSS chunk. The existing test only proved the negative (CSS absent on a direct /feed visit), so a regression where the lazy modal page failed to load its CSS would have passed silently. Also fixes the probePage() intercept branch, which read intercept.page without resolving the now-lazy __pageLoader. The render path awaits the loader (resolveAppPageInterceptState), but the probe path did not, so the dynamic-bailout probe silently inspected an undefined component and never observed the intercept page's searchParams/headers access. Resolve the lazy page before probing to match the render path. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
eca6d3fe83 |
perf(router): lazy-load App Router page and route-handler modules (#1781)
* feat(router): lazy-load App Router page and route-handler modules
Statically-imported route modules were all evaluated at Worker startup.
For typical pages this is cheap, but routes with expensive module-level
initialization make startup scale linearly and can exceed Cloudflare's
~400ms startup CPU budget. Measured on real Workers: 150 pages with heavy
module-init went from ~414-467ms startup (eager) to ~17ms (lazy); typical
pages are unaffected (~0.01ms/route either way).
The RSC manifest now emits page modules of static routes and all
route-handler modules as `() => import()` thunks instead of eager
`import * as`, so they are code-split out of the entry's top-level
evaluation and loaded on demand for the matched route only.
ensureAppRouteModulesLoaded() hydrates a route's lazy modules onto the
synchronous page/routeHandler fields before any consumer reads them; it is
idempotent and dedups concurrent loads. Hydration is invoked at the central
match point and at every mid-flight route lookup that reads modules before
buildPageElement (server-action redirect/rerender targets, interception and
ISR revalidation source routes).
Dynamic-route pages stay eager because their generateStaticParams is
referenced in the module-level generateStaticParamsMap; making those lazy
needs a prerender-resolver rework and is tracked as follow-up.
* fix(router): retry failed lazy route-module imports instead of caching the rejection
Addresses /bigbonk review feedback on the lazy route-module loader:
- ensureAppRouteModulesLoaded no longer caches a rejected dynamic import().
On failure it clears __loading (leaving __loaded false) and re-throws, so
the current request still sees the error but the next request retries —
matching the eager model's per-isolate retry semantics instead of wedging
the route into a permanent 500 and risking an unhandled rejection.
- Document why the getLazyLoaderVar import() codegen is a CodeQL false
positive (trusted filesystem-scan path, same trust model as the eager
getImportVar import * as).
- Add a rejection-path unit test covering retry-after-failure.
* docs(router): clarify why route handlers can always be lazy-loaded
Reword the getLazyLoaderVar comment per /bigbonk re-review: route handlers
are lazy because they are never sourced into generateStaticParamsMap (which
only reads layouts + page), not because they 'have no generateStaticParams'
(Next.js route handlers can export it for prerendering — a separate gap).
* feat(router): lazy-load all page modules, including dynamic routes
The previous cut kept dynamic-route pages (and anything nested under a
dynamic segment) eager, because their generateStaticParams was referenced
synchronously in the module-level generateStaticParamsMap. That gutted the
benefit for a large class of apps: anything with a dynamic root segment
(i18n `[locale]`, multi-tenant `[org]`) made every route dynamic, so
nothing was lazy — and the heavy `[slug]` page (the prime heavy-module-init
candidate) stayed eager.
Now ALL page modules are lazy. generateStaticParamsMap embeds lazy `{ load }`
page sources instead of `mod.generateStaticParams`, and
createAppPrerenderStaticParamsResolver imports them on demand at prerender
time (already async). It returns the same null "no static params" sentinel
the prerender driver relies on (skip / output:export error), so SSG
behavior is unchanged. Layout generateStaticParams sources stay eager.
The eager set is now just genuinely-shared modules: layouts, templates,
boundaries, intercepts, and global-error.
Verified on real Cloudflare: 150 heavy pages under a dynamic `[locale]`
root went from eager (~450ms startup) to ~22ms, and SSG via the `[locale]`
layout's generateStaticParams still prerenders correctly.
Tests: prerender + prerender-route-params + prerender-endpoints + features +
prod/dev server + interception + actions all green; new
app-prerender-static-params unit tests cover the lazy-source path and null
sentinel; entry-templates updated for all-lazy pages and `{ load }` sources.
* refactor(router): preserve source order in prerender static-params resolver
Per /bigbonk re-review: the resolver partitioned sources into
[...eagerFns, ...lazyFns], which only matched the intended composition order
because the codegen happens to emit layout (eager) sources before the page
(lazy) source. Resolve sources in their original declared order instead —
each source maps to its eager function or its awaited lazy function, then
non-functions are dropped — so composition order no longer depends on the
emitter's append order. Added a unit test with a lazy source ordered before
an eager one.
|
||
|
|
fb233b0a34 |
feat: improve dev error overlay source frames (#1746)
* Improve dev overlay stack source mapping * Polish dev error overlay stack display * Add dev overlay code frames * Align dev overlay message spacing * Add dev overlay error copy action * Fix dev overlay knip exports * Polish dev overlay layout * Polish dev error overlay spacing * Fix dev overlay coverage and source frame metadata * Fix dev overlay recovery for RSC HMR errors * Handle Vite HMR build errors in dev overlay * Fix knip * Use Shadow DOM for dev error overlay |
||
|
|
6dd6e76228 |
fix(pages-router): run instrumentation-client.ts before hydration (#1474) (#1671)
The generated Pages Router client entry imported `vinext/instrumentation-client` which transitively loads the user's `instrumentation-client.ts` via the `private-next-instrumentation-client` alias. That indirection left the contract implicit and the side effects vulnerable to aggressive tree-shaking. Side-effect-import the user's file directly from the generated client entry when it exists, ahead of `hydrateRoot()`. Mirrors Next.js's `page-bootstrap.ts`, which side-effect-imports `require-instrumentation-client` before `initialize`/`hydrate`. Adds focused unit tests in `tests/entry-templates.test.ts` asserting that the generated client entry imports the user's `instrumentation-client.ts` before `hydrateRoot(` is called, and that nothing spurious is emitted when no file is present. Refs #1474. |
||
|
|
8498e4a37e |
fix(app-router): emit per-page dynamic stale time metadata (#1711)
* fix(app-router): emit per-page dynamic stale time metadata App Router pages that export unstable_dynamicStaleTime currently behave as if no page-level stale window exists. The server segment config resolver discards the export, so RSC responses cannot carry the per-page metadata that the client cache needs for independently configured pages. The violated invariant is that route segment config resolution must preserve page-only stale-time metadata across active parallel pages before response construction. Parse the page export, reduce active page slots by shortest stale time, thread the value through generated App page dispatch, and emit it on RSC responses for the client cache to consume. * fix(app-router): gate dynamic stale time header to dynamic-only renders Only emit X-Vinext-Dynamic-Stale-Time when the render is truly dynamic (not prerender or force-static), matching Next.js behavior where the response `d` field is only set for non-static-generation renders. The client treats the headers presence as authoritative; leaking it onto static/prerendered RSC responses would cause the client cache to apply dynamic stale time semantics to a statically-cached entry. * test(app-router): add behavioral tests for dynamic stale time gating * docs(app-page-render): add line-numbered link to Next.js source * refactor(app-router): drop redundant Number.isFinite check Number.isInteger already returns false for Infinity/NaN, so the preceding Number.isFinite guard is redundant in both the dynamic stale-time header check and resolveDynamicStaleTimeSeconds. * fix(app-router): gate dynamic stale time on cache-capture predicate The previous gating (isPrerender || isForceStatic) was too narrow for production: isPrerender is only true during the build-time prerender pass, so a production ISR render (revalidate > 0) still emitted X-Vinext-Dynamic-Stale-Time even though that render is captured into the ISR cache. Next.js gates on !workStore.isStaticGeneration; the runtime analog here is shouldCaptureRscForCacheMetadata, so add it to the guard. Adds production ISR coverage to lock down the gating. * docs(app-router): document dynamic stale time over-gating boundary Add a comment explaining that shouldCaptureRscForCacheMetadata is computed from config before the stream runs, so a production default-config route that turns dynamic via a late dynamic API loses the advisory stale-time hint even though Next.js (!isStaticGeneration) would emit it. The value is unknowable until after headers are built. Adds a complementary test pinning down this boundary behavior. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
8ae5abd56c | fix(app-router): preserve forwarded action redirect wrappers (#1582) | ||
|
|
5bcbee60e0 |
fix(app-router): isolate global-not-found chunk to fix CSS cascade (#1549) (#1654)
When `app/global-not-found.tsx` is defined, route-miss 404s render that
module standalone (it ships its own `<html>`/`<body>` and replaces the
root layout). Previously the entry registered global-not-found.tsx as a
static `import * as`, which co-located its CSS with the root layout's CSS
in a single chunk. Lightningcss then dropped overlapping declarations
between the two stylesheets as dead code, so any rule global-not-found
defined that the layout also defined was silently removed — breaking the
CSS cascade for 404s.
Switching the entry to `() => import("…/global-not-found.tsx")` puts the
module in its own JS chunk and lets the SSR pipeline (and `Resources`
metadata) treat its CSS independently of the rest of the app. The
fallback renderer now accepts a `loadGlobalNotFoundModule` loader (cached
per worker) instead of an eagerly-imported module.
Also adds a regression test asserting the SSR markup for `/does-not-exist`
links only global-not-found's stylesheet — never the root layout's — so
the cascade can never resolve to the wrong color.
Ported from Next.js: test/e2e/app-dir/initial-css-order/initial-css-order.test.ts
|
||
|
|
82b6388ece |
fix(app-router): render optimistic loading shells for dynamic navigation (#1421)
* fix(app-router): render optimistic loading shells for dynamic navigation Dynamic App Router links currently skip automatic prefetch for route patterns with params, so a navigation to a sibling dynamic URL waits for fresh RSC data before committing anything. That diverges from Next.js, where automatic prefetch learns the loading shell for dynamic routes and the router can commit the shell immediately while the full payload is fetched. The client prefetch path now requests a loading-shell render for dynamic app routes, keeps that entry out of the navigation cache, learns reusable optimistic route templates from settled shell prefetches, and commits a detached optimistic payload for matching dynamic navigations. The server emits shell-only RSC payloads under a separate cache variant so normal navigation data is not polluted. Tests cover matching semantics, shell-only page wiring, cache key isolation, prefetch cache consumption, Link prefetch behavior, and the upstream Next.js optimistic-routing deploy-suite case. * chore(app-router): address optimistic routing review |
||
|
|
76c5d448fc |
fix: enforce strict parameter isolation for root-params in prerendering and SSR (#1324)
* fix: support root-params dynamic getters during static prerendering and SSR * fix: enforce strict parameter isolation for root params in prerendering and SSR * fix: correct parameter mapping in multi-source resolver and clean up handleSsr fallback * fix: use dedicated standalone ALS for root-params outside of unified request context * fix: thread root params through generated app page dispatch Normal App Router page requests computed root params in the typed handler, but the generated RSC entry did not forward them into page dispatch. SSR then received an empty root params scope for requests that should expose root params. Destructure and forward rootParams in the generated entry, assert that template wiring, reuse the shared root-param picker, and align the scope helper API with the existing runWith overload pattern. |
||
|
|
0d8d3e3c60 |
refactor(router): quarantine navigation runtime topology (#1322)
* refactor(router): quarantine navigation runtime topology * fix(router): restore manifest-backed interception runtime App Router browser navigations lost first-hop intercepted route context after snapshot topology stopped acting as fallback proof. That made /feed to /photos/:id request the direct page payload instead of the declared modal payload. The violated invariant was that route topology must come from the manifest, while snapshots only carry runtime proof. Resolve first-hop interception from the manifest, move RSC bootstrap streams onto the symbol-backed runtime, teach prerender extraction that script shape, and harden the runtime contract around route manifest and RSC data. Coverage includes runtime seam tests, prerender extraction tests, planner/browser navigation tests, package rebuild, and the app-router intercepted routes E2E slice. * fix(router): narrow prerender RSC script parsing The prerender extractor accepted any script starting with the symbol-runtime bootstrap expression, then parsed it as though it had the exact RSC chunk-push prefix. That was harmless at the current head, but it made future runtime scripts a foot-gun. Only the exact runtime chunk prefix and exact done marker are now accepted. A focused extractor test covers non-chunk runtime scripts that share the bootstrap expression prefix. * fix(router): address navigation runtime review * fix(router): resolve navigation runtime review threads |
||
|
|
5b6b764bbd |
fix(app-router): prerender layout static params (#1318)
* fix(app-router): prerender layout static params * address review: uniform resolver contract, sentinel comment, drop defensive clamp --------- Co-authored-by: James <james@eli.cx> |
||
|
|
1dbcbfbcc8 |
feat(app-router): plan navigation from RouteManifest topology (#1312)
* feat(app-router): plan navigation from RouteManifest topology Navigation commit approval still trusted flight payload snapshots for root-boundary and target-slot topology even when the static RouteManifest existed. That made stale or missing snapshot metadata authoritative during App Router commits, which could preserve layouts across the wrong root or fall back to soft commits when the graph already proved a same-root transition. The planner now resolves current and target topology through RouteManifest when available, uses snapshot slot bindings only as visible content proof, and keeps explicit interception proof as the authority for intercepted slot commits. The generated browser entry embeds the manifest and approval paths pass it through to the planner. Targeted coverage exercises stale snapshot roots, missing snapshot roots, dynamic URL pattern resolution, manifest target slot bindings, visible-slot proof, intercepted source topology, and browser-entry manifest serialization. * fix(app-router): read route manifest lazily during navigation approval The browser entry assigned the generated RouteManifest global before a static import, but ESM evaluates imported modules before the importer body. That let the browser navigation controller capture null at module evaluation, leaving manifest-aware planning disabled in real browser bootstraps. Make the controller accept a lazy RouteManifest getter and read it at commit-planning time. Also require direct manifest route-id hits to match the snapshot matchedUrl before trusting them, so stale-but-valid route ids fall back to concrete URL pattern resolution. Covered by app browser controller and navigation planner regressions, plus targeted entry template checks. * fix(app-router): clarify manifest route topology resolution Manifest route lookup now shares one matched-url normalization helper across direct route checks and pattern fallback. That keeps concrete dynamic URL fallback from drifting across call sites. Document that manifest pattern iteration follows buildAppRouteGraph route precedence, and record why intercepted targets cannot use direct-target manifest slot bindings. |
||
|
|
0a77ebd46f |
fix(router): gate intercept matching on Next-URL source pattern (#1302)
Vinext's app-rsc route matcher previously fired interception on any RSC request whose URL matched an intercept target, regardless of the source pathname. Next.js implements interception as a rewrite that is only activated when the Next-URL header matches the *intercepting route* regex (`^<interceptingRoute>(?:/.*)?$`). Without that gate, a direct `.rsc` fetch to an intercept target would incorrectly render the modal page instead of the underlying route, and unrelated sources could pull in modals they have no slot for. This change: 1. Captures the *intercepting route* URL pattern (`sourceMatchPattern`) alongside each `InterceptingRoute` at scan time. The pattern is the slot owner's path with route groups and `@slot` segments stripped, matching Next.js' `extractInterceptionRouteInformation`. The value is propagated through the App RSC manifest. 2. Adds source-pathname filtering to `createAppRscRouteMatcher`'s `findIntercept`: an entry only matches when the provided source pathname satisfies its `sourceMatchPattern` (with descendants allowed). When no source pathname is provided (no Next-URL / X-Vinext-Interception-Context header), no intercept fires. 3. Adds unit coverage in `app-rsc-route-matching.test.ts` and `app-route-graph.test.ts` for the new behavior, including the patterns used by the failing Next.js deploy suite cases (`parallel-routes-and-interception-basepath`, `parallel-routes-and-interception-from-root`, multi-catchall, and two-levels-up). 4. Updates `app-router.test.ts` integration tests that previously relied on the buggy behavior; intercept fires only when the test sends `X-Vinext-Interception-Context` and that context matches the slot's intercepting-route prefix. Cross-references: - https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/interception-routes.ts - https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/generate-interception-routes-rewrites.ts PR #1249 is orthogonal: it carries proof of interception through the planner/lifecycle when the matcher has already fired. This change restricts when the matcher fires in the first place. |
||
|
|
c6e8bda55e |
feat(app-router): support app/global-not-found.tsx (Next.js 16 parity) (#1301)
When users define app/global-not-found.tsx, route-miss 404s should render that module standalone — it provides its own <html>/<body> and replaces the root layout, matching the behavior of Next.js 16's experimental.globalNotFound feature. https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/global-not-found https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/app-render.tsx#L495-L520 Implementation: - Discover app/global-not-found.{tsx,ts,jsx,js} alongside the existing global-error scan (src/index.ts). - Thread the path through generateRscEntry via AppRouterConfig and buildAppRscManifestCode so the generated entry imports it and binds it as globalNotFoundModule. - Pass the module into createAppFallbackRenderer; renderHttpAccessFallback switches to the global-not-found document when statusCode === 404, the route is null (route-miss), and no boundary override was supplied. - Add a skipLayoutWrapping flag to renderAppPageHttpAccessFallback so the standalone document isn't wrapped in the root layout, and so the RSC payload omits layout-id entries that would otherwise refer to a layout tree the markup doesn't contain. Page-triggered notFound() calls keep the existing path: they still resolve through the regular not-found.tsx boundary inside layouts — global-not-found.tsx only takes over for route-miss 404s. Tests: - tests/app-fallback-renderer.test.ts adds four unit tests covering the new branch (route-miss with global-not-found, page-call with global-not-found, absent global-not-found, 403/401 unaffected). - tests/entry-templates.test.ts adds four codegen assertions covering the manifest import and the generated-entry wiring in both directions (present and absent). - tests/nextjs-compat/global-not-found.test.ts ports the basic Next.js fixture as an integration test against a dev server, asserting that the route-miss 404 renders global-not-found's <html> standalone with exactly one <html>/<body>, and that page-call notFound() does not use the global-not-found document. |
||
|
|
bafbcd4d75 |
fix(server): clear browser globals before SSR user modules (#1285)
Server-rendered user modules can currently observe a partially populated browser global when the runtime provides one. That violates Next.js SSR semantics, where server code must see browser globals as absent, and it breaks common typeof window guards during module evaluation. Add a shared server globals bootstrap that removes window and document before user modules load, preserves the existing AsyncLocalStorage edge-runtime global, and import it ahead of generated App and Pages Router user imports. Cover the runtime behavior and generated import order with focused regression tests. |
||
|
|
6cf4148ffd |
fix(link): keep auto prefetch from fetching dynamic app routes (#1177)
* fix(link): keep auto prefetch from fetching dynamic app routes
App Router Link treated null, undefined, and auto prefetch as the same full RSC prefetch used by prefetch={true}. That violates Next.js's public prefetch contract and over-fetches dynamic route payloads as soon as links enter the viewport.
The broken assumption was that prefetch only needed a boolean enabled state. Split Link prefetch into disabled, auto, and full modes, emit a small browser route manifest, and allow automatic full RSC prefetch only for matched static app routes while preserving explicit full prefetch.
Add focused tests for the mode mapping and generated route manifest.
* test(link): opt interception prefetch fixture into full mode
The interception-context E2E asserts that full RSC prefetch cache keys stay separate by source context. After auto prefetch stops full-fetching dynamic routes, that fixture needs to request the full prefetch mode explicitly.
|
||
|
|
4932b69b17 |
feat(router): expose RouteManifest semantic facts (#1113)
* feat(router): expose RouteManifest semantic facts RouteManifest only exposed minimal route, layout, template, and slot lookup maps. That left later #726 planner and cache-proof work without stable graph-owned facts for slot ownership, slot defaults, template reset scope, or boundary outcomes. The graph builder now derives declaration-level slot facts, per-route slot bindings, template remount facts, and layout-boundary outcome facts from existing App Router metadata. The manifest graphVersion includes the new maps so downstream compatibility checks can detect semantic topology changes. Tests cover route groups, slots, defaults, templates, and boundaries in one fixture. * test(router): align CI fixtures with graph facts Full CI checks more App Router fixtures than the targeted RouteManifest test run. The new ParallelSlot declaration fields must be present in hand-built fixtures, otherwise typecheck fails even though production graph scanning populates them. The app browser trace tests also still asserted the stale reason at entry zero, but no-commit transaction tracing is prepended before the reason entry. Update those assertions to inspect the stale reason entry without changing runtime behavior. |
||
|
|
ebbf72cc8d |
feat(router): expose route graph manifest read model (#1089)
* feat(router): expose route graph manifest read model The App Router graph now exposes root-boundary identity and a stable RouteManifest read model instead of leaving these facts implicit inside per-route arrays. Future #726 planner and compatibility work needs a semantic lookup surface for routes, layouts, pages, templates, slots, and root boundaries before it can stop reading meaning from transport keys. This adds RootBoundaryId, graphVersion, StaticSegmentGraph maps, and fail-fast invariant checks for mismatched layout/template positions while preserving the existing routes array for current callers. * chore: retrigger ci |
||
|
|
834ccc6ce7 |
feat(app-router): mint semantic route graph ids (#1060)
App Router route graph entries previously left route, page, layout, template, and slot identity to later render and wire layers. That kept topology meaning coupled to reconstructed payload keys instead of a graph-owned fact. Mint stable semantic IDs during graph construction, preserve them in generated RSC route metadata, and cover route-group, dynamic-segment, slot-owner, and filesystem-root stability cases. |
||
|
|
9d5d1ce50d |
fix(app-router): validate RSC cache-busting params (#991)
* fix(app-router): validate RSC cache-busting params App Router RSC responses could vary by request headers while client fetches and server validation still relied on Vary-aware caches. CDNs that key only by URL could reuse a mounted-slot or interception-context RSC payload for a different request. Next.js validates a cache-busting _rsc search param against the RSC request headers and redirects malformed requests to the canonical URL. Vinext now applies the same invariant to its .rsc request model, centralizes the RSC Vary header set in normal server modules, and de-duplicates middleware Vary additions. Tests cover missing and stale _rsc values, variant-header hashes, generated-entry delegation, browser RSC request markers, response Vary propagation, and App Router RSC integration. * fix(app-router): hide RSC cache tokens from navigation URLs * fix(app-router): hide RSC cache token from searchParams * fix(app-router): address RSC cache-busting review * fix(app-router): preserve HTML redirect destination for direct .rsc navigations Config redirects from next.config.js were unconditionally converted to .rsc destinations for any request whose URL ended in .rsc. This broke direct browser navigations to .rsc URLs (e.g. a bookmark or external link) because the browser was redirected to an RSC payload it could not render. Client-side RSC fetches always send the RSC: 1 header, so restrict the .rsc destination conversion to requests that carry that header. Direct browser hits without the header continue to redirect to the canonical HTML URL, matching the pre-PR behaviour for config redirects. |
||
|
|
763736eae0 |
refactor(app-rsc-entry): delegate pure helpers to typed server modules (#1017)
* refactor(app-rsc-entry): delegate pure helpers to typed server modules The generated RSC entry template contained five inline implementations of pure runtime helpers: post-middleware request context construction, RSC error handler creation, request context cleanup, RSC stream preload hint normalization, and hook warning suppression. These inline blocks repeated runtime behavior across multiple call sites and could not be unit-tested independently. Move each helper to a dedicated typed module under server/ so the generated entry imports and wires them instead of owning the logic: - buildPostMwRequestContext → server/app-post-middleware-context.ts - createRscOnErrorHandler → server/app-rsc-error-handler.ts - __clearRequestContext + setNavigationContext → server/app-request-context.ts - renderToReadableStream preload hints → server/rsc-stream-hints.ts (createRscRenderer) - hook warning console.error patch → server/app-hook-warning-suppression.ts Each helper module has focused unit tests. The entry template is 55 lines shorter and now describes the app shape; the typed modules own behaviour. * fix: remove redundant as cast and fix test module caching app-hook-warning-suppression.ts contained an `as string` cast that was redundant because the typeof check on the prior line already narrows the type. The hook warning suppression tests in tests 2-4 were passing vacuously because dynamic import() returned the cached module whose console.error patch was no longer active after test 1 restored the original. Use vi.resetModules() before each test to force fresh module evaluation. clearAppRequestContext now delegates to setAppNavigationContext(null) instead of calling the raw navigation and root-params setters directly, matching the DRY pattern from the original inline code. * fix: resolve entry helper rebase conflicts * ci: rerun checks (font-google flake) * Update packages/vinext/src/entries/app-rsc-entry.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --------- Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> |
||
|
|
3f287ff5ab |
chore: remove useless code assertion tests and snaps (#1023)
* chore: remove useless code assertion tests and snaps * . |
||
|
|
d85421b3e5 |
refactor(app-router): extract buildPageElements into typed helper module (#1018)
* refactor(app-router): extract buildPageElements into typed helper module The generated RSC entry template owned the central element-construction path (buildPageElements) as an inline template function with ~110 lines of behaviour. This made the code untestable directly and forced any change to element-tree construction to be verified through snapshot diffs of the generated output rather than through behavioural assertions. The generated entry should describe the app shape (route manifests, module references); runtime behaviour belongs in normal typed modules. Extract buildPageElements into a new server/app-page-element-builder.ts module with fully typed options (BuildPageElementsOptions), a route shape (AppPageBuildRoute extending AppPageRouteWiringRoute), and intercept types. The generated entry now delegates to this module with a 12-line closure that passes the per-app module references (globalErrorModule, rootNotFoundModule, etc.). Next.js equivalents: - create-component-tree.tsx -- builds the nested React element tree - create-metadata.tsx -- resolves page head metadata including parallel route interpolation Behavioural changes: none. All existing app-router behaviour is preserved. Tests added: 9 focused unit tests covering error payload construction, interception context threading, search-params-triggered dynamic marking, slot override wiring, and thenable-params proxy behaviour. * chore: register app-page-element-builder in knip entry list The module is imported by the generated RSC entry template as a string literal, so knip cannot statically trace the dependency. Same pattern as app-page-dispatch.ts, app-page-head.ts, and prerender-work-unit-setup.ts. |
||
|
|
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 |
||
|
|
1d7c031e30 | feat(app-router): feed file metadata routes into head output (#891) | ||
|
|
dc28af1cb6 |
fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764) (#945)
* fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764)
Per-layout forbidden/unauthorized boundary files (forbidden.tsx, unauthorized.tsx)
are now discovered by the router and wired into the App Router component tree via
dedicated ForbiddenBoundary and UnauthorizedBoundary client components. Previously,
only NotFoundBoundary existed, causing 403/401 errors thrown during RSC rendering
(e.g. inside Suspense boundaries) to escape into the generic error shell — this
could produce mismatched Flight/HTML data and client-side 'Connection closed' failures.
Next.js upstream: vercel/next.js#92231
https://github.com/vercel/next.js/pull/92231
* fix: address review feedback on ForbiddenBoundary/UnauthorizedBoundary (#945)
- Export ForbiddenBoundaryInner and UnauthorizedBoundaryInner for testing
- Strengthen tests to import and call getDerivedStateFromError directly
- Use exact equality instead of startsWith for status code digest matching
- Make forbiddenPaths and unauthorizedPaths non-optional on AppRoute (matching notFoundPaths)
- Fix inaccurate ported-from attribution in test header
* fix: remove duplicate forbidden/unauthorized path iteration and update comment
Removes dead code in app-rsc-entry.ts (lines 185-190) that duplicated the existing import registration loops.
Also updates the stale nesting order comment in app-page-route-wiring.tsx to include Unauthorized and Forbidden boundaries.
* fix: align NotFoundBoundary digest matching and tests with Forbidden/Unauthorized patterns
- Use exact === instead of startsWith in NotFoundBoundary digest check
- Add forbiddenPaths/unauthorizedPaths per-layout test fixtures
- Fix property ordering: singular before plural for all boundary types
* fix: resolve rebase conflicts — align manifest code with main refactor
- Use buildAppRscManifestCode for manifest generation (matching main)
- Remove redundant || [] guards for non-optional forbiddenPaths/unauthorizedPaths
- Fix test fixture indices to match new module import count
* fix: add null guards for notFoundPaths, forbiddenPaths, unauthorizedPaths in manifest
TypeError: route.notFoundPaths is not iterable when routes lack per-layout boundary files.
CI was failing on app-router.test.ts with the error. Added null guards in both registerRouteModules() and buildRouteEntries() to handle undefined arrays.
* fix: reorder route-level boundary nesting for merge with main
Move ErrorBoundary after NotFoundBoundary/ForbiddenBoundary/
UnauthorizedBoundary to match Next.js nesting order (Error >
Unauthorized > Forbidden > NotFound). Resolves merge conflict
with
|
||
|
|
f8d2d39107 | refactor: extract app prerender endpoints (#965) | ||
|
|
9ab38193eb | refactor: extract server action rsc flow (#967) | ||
|
|
776fc9eea6 | refactor: extract app rsc manifest construction (#966) | ||
|
|
0846931bcb |
fix: report stale ISR background revalidation errors via onRequestError (#947)
* fix: report stale ISR background revalidation errors via onRequestError (#775) Next.js 16 now reports errors during background ISR revalidation of stale pages via routeModule.onRequestError(). Previously these errors were silently swallowed (only logged to console). This ensures instrumentation hooks (Sentry, OpenTelemetry) observe revalidation failures. - Add errorContext parameter to triggerBackgroundRegeneration() in isr-cache.ts - Report errors via reportRequestError() with revalidateReason: "stale" - Update App Router entry template to pass route context to regen callbacks - Update Pages Router entry template to forward error context through wrappers - Update dev-server.ts and pages-page-data.ts to pass route context - Update snapshots and test assertions for generated code changes * fix: remove dead ISRRegenErrorContext export flagged by knip |
||
|
|
1e5f80e008 |
refactor(app-router): delegate RSC preload hint normalization (#952)
* refactor(app-router): delegate RSC preload hint normalization The App Router RSC entry currently owns the React Flight preload hint stream transform inline. That keeps byte-stream buffering logic inside generated code, so the behavior is only indirectly covered by generated-entry snapshots. Move the stream normalization into a typed server helper and keep the generated RSC entry as a thin wrapper around renderToReadableStream. Add direct coverage for complete lines, split chunks, and final unterminated Flight hint lines. * test(app-router): cover mixed Flight preload hints |
||
|
|
d0b81db244 |
refactor(app-router): delegate RSC route matching (#953)
The RSC entry still owned route matching, intercept lookup construction, and dynamic metadata pattern matching inside generated source. That kept request-path behavior in a template string and made direct tests awkward. Move the matching behavior into a typed server helper, keep route import and manifest emission in the generator, and update entry-template assertions to verify delegation instead of inline matcher code. |
||
|
|
8f224051b7 |
Preserve intercepted app-router state across server actions (#854)
* Preserve interception context on app-router actions * Keep intercept slot overrides on action rerenders * Pin React versions in static-export fixture * refactor(app-page-request): unify intercept resolution into a single state machine Collapse the parallel GET/POST intercept lookup paths into one shared internal helper that returns a tagged union: none, current-route, or source-route. Both resolveAppPageInterceptMatch and resolveAppPageActionRerenderTarget now read from the same lookup result, eliminating the double findIntercept() scan on current-route fallbacks. Also add the two missing resolveAppPageActionRerenderTarget fallthrough test cases (no intercept, current-route intercept with no matching action route) and a regression assertion that the current-route branch only performs one intercept lookup. |
||
|
|
0dbfeff1af |
feat: flat keyed payload for App Router layout persistence (#750)
* Extract app page route wiring helpers
* Add slot client primitives
* Fix app page error boundary serialization
* Fix client error boundary pathname reset
* Document Next.js error boundary verification
* Implement flat App Router payload for layout persistence
* fix: address review findings in flat payload implementation
- Fix stale closure on readBrowserRouterState by using a useRef updated
synchronously during render instead of a closure captured in
useLayoutEffect. External callers (navigate, server actions, HMR) now
always read the current router state.
- Restore GlobalErrorBoundary wrapping that was dropped when switching
from buildPageElement to buildAppPageElements. Apps with
app/global-error.tsx now get their global error boundary back.
- Add exhaustive default case to routerReducer so new action types
produce a compile error and a runtime throw instead of silent undefined.
- Remove dead code: createRouteNodeSnapshot, AppRouteNodeSnapshot,
AppRouteNodeValue were defined but never imported.
- Remove deprecated buildAppPageRouteElement and its test — no
production callers remain after the flat payload cutover.
- Short-circuit normalizeAppElements when no slot keys need rewriting
to avoid unnecessary allocation on every payload.
- Align test data in error boundary RSC payload test (matchedParams
slug: "post" -> "missing" to match requestUrl /posts/missing).
* fix: normalize flat payload after use(), not before
createFromReadableStream() returns a React thenable whose .then()
returns undefined (not a Promise). Chaining .then(normalizeAppElements)
broke SSR by assigning undefined to flightRoot.
Fix: call use() on the raw thenable, then normalize synchronously
after resolution. Also widen renderAppPageLifecycle element type to
accept flat map payloads.
* fix: produce flat RSC payload on all rendering paths
The SSR entry always expects a flat Record<string, ReactNode> with
__route and __rootLayout metadata from the RSC stream. Three paths
were still producing bare ReactNode payloads:
1. renderAppPageBoundaryElementResponse only created the flat map for
isRscRequest=true, but HTML requests also flow through RSC→SSR
2. buildPageElements "no default export" early return
3. Server action "Page not found" fallback
All three now produce the flat keyed element map, fixing 17 test
failures across 404/not-found, forbidden/unauthorized, error boundary,
production build, rewrite, and encoded-slash paths.
* test: update unit tests for flat RSC payload on all paths
- Update renderElementToStream mock to extract the route element from
the flat map before rendering to HTML (mirrors real SSR entry flow)
- Update entry template snapshots for the buildPageElements changes
* fix: wrap Flight thenable in Promise.resolve() before chaining .then()
createFromReadableStream() returns a React Flight thenable whose
.then() returns undefined instead of a new Promise. The browser
entry's normalizeAppElementsPromise chained .then() on this raw
thenable, producing undefined — which crashed use() during hydration
with "An unsupported type was passed to use(): undefined".
Wrapping in Promise.resolve() first converts the Flight thenable
into a real Promise, making .then() chains work correctly.
The same fix was already applied to the SSR entry in
|
||
|
|
14ff303cbd |
feat: csp support (#795)
* fix: support csp * tweaks * tweaks * tweaks * tweaks * tweaks * tweaks * clean * tweaks * clean * fix test * tweaks * fix lock * fix test * trigger ci * tweaks * tweaks |