mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.5
271 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f8b0ef78b |
fix(build): preserve lazy RSC framework chunks (#2074)
* fix(build): preserve lazy RSC framework chunks * test(image): wait for completed watch output * test(build): isolate RSC startup regression * fix(build): preserve lazy framework chunks on Vite 7 * test(build): cover transitive framework reachability * test(build): harden framework chunk regressions |
||
|
|
78bb36a656 | fix(app-router): preserve dynamic interception source routes (#2042) | ||
|
|
48f4df28cb | fix(pages-router): cancel deduped GSSP data waits (#2038) | ||
|
|
68414e68a5 |
fix(router): re-enter filesystem routes after rewrites (#2032)
* fix(router): re-enter static files after i18n rewrites * fix(router): re-enter filesystem routes for all rewrites |
||
|
|
b91d5d1396 |
fix(pages): interpolate same-segment rewrite params (#2030)
* fix(pages): interpolate same-segment rewrite params * test(pages): fix same-segment navigation CI |
||
|
|
c3dcdba1fe |
fix(pages): honor basePath in dev module loaders (#2033)
* fix(pages): honor basePath in dev module loaders * fix(pages): preserve basePath in ISR loaders * style(pages): format dev module loader changes * fix(pages): preserve dynamic route module paths * test(pages): run basePath dev coverage in CI |
||
|
|
9958567949 | fix(pages): route hybrid form data requests (#2029) | ||
|
|
866c71809e |
fix(pages): preserve rewritten URLs during navigation (#2028)
* fix(router): preserve rewritten pages query navigation * fix(router): align pages url object locale handling * fix(router): preserve raw pages url search * fix(router): escape fragments in raw pages search * fix(router): preserve localized rewritten pages urls * test(router): fix pages navigation CI failures * fix(router): preserve Next search fragment formatting |
||
|
|
8b5390504e |
fix(actions): return body-limit errors without closing responses (#2026)
* fix(actions): preserve overflow error responses * fix(actions): backpressure body overflow handling * fix(server): own Node request stream chunks * fix(server): preserve cloned request bodies |
||
|
|
5146b1f47b | fix(router): render slots beside grouped children (#2022) | ||
|
|
fc8e6f0e86 | fix(app-router): settle superseded link navigations (#2025) | ||
|
|
b67a8ddd1e |
fix(prerender): deduplicate generateStaticParams entries (#2031)
* fix(prerender): deduplicate generateStaticParams entries to prevent duplicate routes in prerendering * docs(prerender): simplify deduplication comment for generateStaticParams entries |
||
|
|
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> |
||
|
|
10b5086f4d |
fix(app-router): align router autoscroll with Next (#2004)
* fix(app-router): align router autoscroll with Next App Router navigation could diverge from Next.js router autoscroll semantics around hydration timing, loading-shell same-page search navigations, and routes whose first committed node is React-hoisted into head. That broke the upstream router-autoscroll deploy-suite because the client marked hydration too early, static loading-shell templates were not usable for navigation, and the fallback scroll path masked the old-handler hoisted-head behavior. The fix moves the Next-compatible hydration marker into a passive effect, wires production hydrateRoot error callbacks to match Next's implicit root-boundary and recoverable-error handling, lets authoritative loading-shell templates drive static route optimistic payloads, and skips fallback document scrolling when the committed document only exposes a React-hoisted head resource. The ported tests cover the upstream router-autoscroll behavior plus focused unit boundaries for prefetch mode, optimistic routing, head-resource detection, and hydrateRoot callbacks. * fix(app-router): hoisted-head scroll fallback is per-intent, not global head scan * test(app-router): add full-chain scroll intent integration test * fix(app-router): match legacy scroll focus parity * fix(app-router): preserve latest scroll navigation intent * fix(app-router): scope scroll and error recovery * fix(app-router): gate navigation failure recovery * fix(app-router): match navigation failure handling * fix(app-router): align scroll completion parity * fix(app-router): retain latest failure target * fix(app-router): preserve route error boundaries * fix(app-router): clear discarded failure targets * fix(app-router): align failure recovery boundaries * fix(app-router): disarm refused recovery targets * fix(app-router): preserve failure target ownership * fix(app-router): match boundary recovery parity --------- 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
|
||
|
|
930e03c449 |
fix(ppr): gate fallback shells until request-time resume is supported (#1716)
* fix(app-router): prerender cacheComponents root-param fallback shells PR 1 of 4: core model + build integration. Model: app-ppr-fallback-shell, pregenerated-concrete-paths, prerender-manifest Build: prerender/run-prerender fallback shell artifact generation Tests: createAppPprFallbackShells, pregenerated-concrete-paths core * feat(ppr): add fallback shell payload identity * feat(ppr): safely serve fallback shell cache entries * refactor(ppr): extract fallback shell render pipeline and dedupe getViteMajorVersion * fix(ppr): restore safe-serving CI checks The safe-serving branch failed CI because a client-side server-only guard rejected valid 'use server' action modules, a fallback-shell dependency cast no longer matched its helper type, apps/web lost the Cloudflare cache adapter source path during typecheck, and Knip could not resolve the documented Cloudflare cache adapter subpaths. The server-only guard now reads the directive prologue and skips top-level 'use server' modules, fallback-shell regeneration passes an explicit typed dependency object, apps/web gets the workspace Cloudflare cache path, the Cloudflare package exports explicit adapter subpaths, Knip uses a relative source import in the dispatch unit test, and the readiness test awaits the actual cache-ready promise. * fix(ppr): guard fallback shell reads for queries * fix(ppr): normalize pregenerated concrete paths * fix(ppr): address review comments on fallback shell serving and react runtime check * fix(ci): restore knip ignores for unlisted binaries and prune dependencies * fix(ppr): restrict bracket check in fallback detection to legacy manifests * fix(ppr): prefer stable ESM react-dom/static.edge before CJS fallback The loadStaticPrerender dev-mode path hard-codes react-dom's internal CJS file layout (cjs/react-dom-server.edge.development.js). A React upgrade could silently break fallback-shell rendering in dev without affecting production because the function never tried the stable ESM entry in development. Change the order so that react-dom/static.edge is attempted first in all environments. Only fall back to the CJS path when the ESM entry does not export prerender() and we are in a development runtime. This lets future React dev builds that expose prerender() from the ESM entry work automatically, and it surfaces the CJS-path failure only when the stable path is genuinely unavailable. Also handle the CJS default-export interop shape that Node.js ESM produces when dynamically importing a CommonJS module. * refactor(ppr): address four review cleanup items 1. Remove redundant double-normalization in seed-cache.ts addPregeneratedConcretePath already normalizes internally, so the call-site normalizePregeneratedPathname() was redundant. Dropping it gives a single source of truth for pathname normalization. 2. Lazily construct FallbackShellRenderDeps in tryServePprFallbackShell Extract the large FallbackShellRenderDeps object into a top-level buildFallbackShellRenderDeps helper. The closure in the probe loop now just delegates, so the heavy object is only created when a STALE entry actually triggers regeneration, not on every HIT probe. 3. Add dev-only warning for legacy bracket heuristic When isFallbackShellArtifactPath falls back to the bracket substring scan (route.fallback === undefined), emit a console.warn in dev so developers know a legacy manifest is being used and concrete URLs with literal brackets may be misclassified. 4. Update pregenerated-concrete-paths.test.ts to colon notation The production manifest writer uses colon notation (/:slug) for the route field, but the test fixtures used bracket notation (/[slug]). Update the fixtures so the test format matches production, exercising the real key format that the safe-serving guard depends on. * test(ppr): compact fallback shell dispatch fixtures * refactor(ppr): name fallback shell render phases * refactor(app-router): deduplicate PPR fallback shell and app page cache render paths * fix(ppr): avoid serving incomplete fallback shells * chore: drop redundant cloudflare cache aliases * fix(ppr): track fallback navigation hooks * test(ppr): render navigation hook probes * test(ppr): type navigation hook probes * refactor(ppr): remove unused shell regeneration path * refactor(ppr): remove unused shell result type * fix(ppr): safely gate fallback shell prerendering * fix(ppr): keep fallback shell gate internal * fix(ppr): mark generated dynamic fallback shells * test(ppr): align fallback fixture and search params --------- Co-authored-by: James <james@eli.cx> |
||
|
|
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> |
||
|
|
3253aafd77 |
fix(pages-router): pass rewrite URL to edge API requests (#1998)
* fix(pages-router): pass rewrite URL to edge API requests * fix(pages-router): preserve edge API request URL parity * fix(pages-router): configure edge API NextRequest * fix(pages-router): preserve edge API locale config * fix(pages-router): preserve edge request metadata * fix(pages-router): align edge API URL formatting * fix(pages-router): match API locale casing * fix(pages-router): match domain locales by locale * docs(pages-router): clarify edge API basePath parity * test(deploy): expect metadata-preserving URL clone * docs(pages-router): note basePath rewrite parity --------- Co-authored-by: James <james@eli.cx> |
||
|
|
70ee909adf |
fix(app-router): support extensionless variable imports (#1958)
* fix(app-router): support extensionless variable imports * fix(build): harden extensionless dynamic imports * fix(build): align variable import resolution * fix(build): cover dynamic import contexts * fix(build): scope variable import contexts * fix(build): avoid comment parser backtracking |
||
|
|
075919357e |
fix(router): hard navigate Pages links to App routes (#1960)
* fix(router): hard navigate pages links to app routes * fix(router): normalize localized app route detection |
||
|
|
e018a08708 |
fix(pages-router): restore scroll across reload history traversal (#1905)
* fix(pages-router): restore scroll across reload history traversal
Pages Router pages with experimental.scrollRestoration currently expose window.next.router before the popstate runtime is installed and only keep Vinext scroll coordinates in the in-memory history state. After a reload, a back or forward traversal can run before the runtime is ready and has no durable scroll snapshot for the target history entry.
The violated invariant is that the Pages Router owns browser scroll restoration whenever the Next.js scroll restoration flag is enabled, with one stable history key per entry and routeChangeComplete emitted after the new page commits.
Resolve the Next.js config flag into process.env.__NEXT_SCROLL_RESTORATION, install the Pages Router runtime during next/router evaluation, persist Next-style sessionStorage scroll snapshots per history key, and render navigations through a stable commit boundary before emitting routeChangeComplete.
Regression coverage ports Next.js reload-scroll-backforward-restoration and verifies manual restoration plus exact scroll positions across reload, back, and forward traversal.
* fix(pages-router): sequence routeChangeComplete after scroll restoration and settle on render failures
* fix(router): guard against missing window.history in stampInitialHistoryState
* fix(router): defer hash scroll until after routeChangeComplete
Move hash scrolling out of renderPagesRouterElement's commit callback
and into the post-routeChangeComplete path, matching Next.js ordering:
x/y restoration happens during render commit, hash scrolling happens
after the completion event.
Also fix E2E test expectRouteChangeComplete to use one-shot event
listeners instead of checking for stale text, and add listener cleanup
to the fixture to prevent accumulation.
* fix(test): remove post-push deadlock and throw on missing events in E2E helper
expectRouteChangeComplete() now throws if window.next.router.events is
absent instead of silently resolving. Remove the redundant
expectRouteChangeComplete call after pushWithPagesRouter in the third
test — the awaited router.push already resolves after routeChangeComplete,
so registering a listener after it would deadlock.
* fix(test): replace as any casts with type-safe window.next narrowing in E2E scroll tests
* fix(router): narrow renderPagesRouterElement scroll param, drop dead hash branch
* fix(router): reset x/y scroll on hash routes before routeChangeComplete and cancel pending render on supersede
- Pass {x:0, y:0} through commit boundary for all scrollable
navigations, not just hash-less ones (hash scroll deferred to after
routeChangeComplete as before).
- Cancel pending render-commit Promise when a newer navigation starts,
preventing hang when the first root.render() commit is dropped by
React re-reconciliation.
- Add regression tests for both edge cases.
* fix(router): use SSR-safe layout effect in commit boundary and pin shallow scroll ordering
Address bonk review on #1905:
- swap the commit boundary's raw useLayoutEffect for the SSR-safe
useNonWarningLayoutEffect pattern from shims/image.tsx so Pages Router
SSR renders no longer log React's useLayoutEffect warning
- document that the default-noop commit boundary intentionally also wraps
SSR and initial hydration
- document the intentional shallow-scroll-before-routeChangeComplete
ordering and add a unit test pinning it
* fix(router): keep history-state scroll restore on popstate when scrollRestoration flag is off
Without experimental.scrollRestoration, popstate must still honour the
__vinext_scrollX/Y saved into history state on push, since a soft popstate
re-renders content the browser's native restoration can't position. Also
document why the popstate-time sessionStorage snapshot depends on
history.scrollRestoration = "manual".
* fix(router): guard render-commit canceller against late commits from superseded trees
* fix(router): defer current-history-key commit until beforePopState allows navigation
When beforePopState cancels a popstate the app stays on the previous
history entry, but _currentHistoryKey had already been updated to the
target key, skewing subsequent scroll-position bookkeeping. Move the
key commit below the beforePopState early-return, matching the
deliberate deferral of _lastPathnameAndSearch.
* chore(router): note intentional eager scroll snapshot; fix fixture link increment
- Document that the outgoing-entry scroll snapshot is deliberately written
before the beforePopState gate: on cancellation the app stays on
currentKey, so the saved value remains that entry's live scroll.
- Coerce id to a number in the pages-scroll-restoration fixture Link so
the href increments numerically instead of string-concatenating.
* fix(router): keep manual scroll restoration opt-in Pages-Router-only
Gate installManualScrollRestoration on the document not being an App
Router one (window.next.appDir === true): with the now-global
__NEXT_SCROLL_RESTORATION define, an App Router app that value-imports
next/router for compat would otherwise have its native browser scroll
restoration disabled at module eval. Upstream only flips
history.scrollRestoration inside the Pages Router constructor, which
never runs on App Router documents.
Also document the hash-only popstate path discarding the per-entry
__next_scroll_<key> snapshot as intentional upstream parity (change()'s
onlyAHashChange branch passes null to this.set and only scrollToHash
runs), and add unit coverage for both behaviors.
* ci: run pages-scroll-restoration E2E project in the CI matrix
Add the missing E2E matrix entry so the ported Next.js regression spec
(reload-scroll-backforward-restoration.spec.ts) actually executes in CI,
matching the shape of the sibling single-shard Pages Router entries.
Also document why the soft-popstate scroll fallback differs between
manual restoration on ({ x: 0, y: 0 }) and off (null -> native auto
restoration handles unstamped entries), per review.
* refactor(router): fold App Router gate into manualScrollRestoration constant
The popstate handler's sessionStorage save/read branches gated on the
module-level constant, which lacked the window.next?.appDir check that
installManualScrollRestoration used, leaving those branches relying
solely on the __N state filter to stay inert on App Router documents.
Fold the appDir check into the constant so every consumer is
Pages-Router-only by the same gate; the App Router bootstrap stamps
window.next.appDir before any client module evaluates, so the
module-eval-time check is reliable.
* test(pages-scroll-restoration): default getServerSideProps id to string "0"
Route params always arrive as strings, so the numeric 0 fallback was
inconsistent with the string values the component relies on
(Number(id) + 1 href, id === "error" check). Default to "0" for type
consistency.
* test(pages-scroll-restoration): align fixture deps and vinext import with pages-basic
Import vinext via the workspace package name instead of reaching into
dist by relative path, and declare the same react/react-dom/vinext
dependencies as the other fixtures so the fixture is self-describing
and the pnpm-lock.yaml importer entry is populated.
* fix(router): snapshot outgoing scroll on hash-only pushes
Upstream Next.js snapshots the outgoing entry's scroll into
__next_scroll_<key> inside Router.push() itself, before change()'s
onlyAHashChange short-circuit, so hash-only pushes still capture the
departed entry's position. Our hash-only push branch returned before
saveScrollPosition() ran, so a later back-popstate to that entry
restored {x: 0, y: 0} instead of the position the user had reached.
Save the outgoing scroll before updateHistory mints the new key,
matching the full-navigation push path and upstream ordering.
* docs(router): clarify manual scroll-target fallback chain precedence
* test(shims): tag scrollRestoration App Router test as eval-order guard
* docs(router): note missing-snapshot fallback intent at forcedScroll default
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
fd1023321b |
fix(router): support experimental gesture push (#1909)
* fix(router): support experimental gesture push Vinext did not expose useRouter().experimental_gesturePush when Next.js apps enabled experimental.gestureTransition. Upstream gesture-transition fixtures call the method inside a held async transition, so the target route never became visible before the canonical router.push completed. The fix resolves the config flag into the same __NEXT_GESTURE_TRANSITION define used by Next.js, exposes the optional router method only under that flag, and lets gesture navigation use the existing RSC navigation pipeline with an immediate approved visible commit. The ported e2e fixture preserves the upstream held-transition flow and target/static/dynamic assertions. * test(router): update navigate mock assertions to expect default transition commit mode * fix(router): address ask-bonk review comments on gesture push - track scheduled app router navigation in experimental_gesturePush for parity with push/replace, so a refresh() issued in the same task skips its redundant re-fetch - document the synchronous visible-commit mode's null pendingRouterState invariant in the browser navigation controller - add a unit test asserting experimental_gesturePush is only attached when the __NEXT_GESTURE_TRANSITION define is set * style(tests): fix formatting in shims.test.ts * docs(router): clarify gesture push normalization and flushSync commit paths * test(router): fix unbound-method lint in shims test and document gesture spec coverage * docs(router): clarify gesture define boolean form and spec held-state rationale * docs(test): note gestureTransition is fixture-wide for the gesture spec only * fix(router): no-op experimental_gesturePush before the navigation runtime is ready Upstream gesturePush early-returns when getCurrentAppRouterState() is null, so a gesture dispatched before hydration is a no-op. Previously vinext fell through navigateClientSide's non-runtime branch and performed a real history push. Guard on the runtime's navigate function (the same readiness check navigateClientSide uses) and document that the un-awaited void navigateClientSide(...) deliberately matches push/replace's fire-and-forget rejection shape. * docs(router): note gesturePush guard/dispatch runtime reads have no TOCTOU window * docs(test): document gesture fixture's double-navigation divergence from upstream --------- Co-authored-by: James <james@eli.cx> |
||
|
|
dea4472c2f |
fix(app-router): include parallel slot params in useParams (#1904)
* fix(app-router): include parallel slot params in useParams App Router navigation state previously used only the primary route match params when setting request navigation context, RSC param headers, render observations, and optimistic navigation payloads. That lost catch-all params from active parallel slot branches, so useParams() diverged from the slot page's params prop after SSR and client navigations. Next.js derives params from the selected router tree across parallel route branches. Resolve active slot params from route metadata, merge them into the navigation params boundary, and feed that selected param set through server render and optimistic navigation payloads. Port the upstream parallel-route-navigations fixture and cover the route graph, selected params helper, and optimistic payload boundary. * fix(app-router): preserve canonical pathname in navigation context after middleware rewrite * fix(app-router): resolve parallel slot params for static, revalidation, and action paths * chore: retrigger CI * fix(app-router): address ask-bonk review comments on parallel slot params - Remove unused parameter from - Add clarifying comment on routeParams fallback being deliberate no-op - Use (full matched params) instead of narrowed as base for slot param merge, preserving interception-specific extras on ISR regeneration path * fix(app-router): use full navigationParams in server-action re-render path The server-action re-render path was passing narrowed actionRerenderTarget.params to resolveAppPageNavigationParams, dropping interception-specific extras from source-route intercepts. Use actionRerenderTarget.navigationParams (full matched params) instead, matching the pattern used in the dispatch ISR path at app-page-dispatch.ts:602. Adds unit tests for the intercepted-slot and mixed intercepted + non-intercepted slot branches of resolveAppPageNavigationParams. * docs(router): clarify decode invariant and type cast rationale in parallel-slot params - app-optimistic-routing.ts: add comment explaining the pre-existing double-decode asymmetry between route params and slot params, noting TODO to converge decode passes - app-server-action-execution.ts: expand the comment on the interceptOpts cast to document why it's safe and what a follow-up would look like * fix(app-router): merge slot params for fresh intercepts --------- Co-authored-by: James <james@eli.cx> |
||
|
|
f5bee1a26f |
fix(app-router): preserve unstable_catchError boundaries (#1906)
* fix(app-router): preserve unstable_catchError boundaries
* test(e2e): add Next-parity test for shouldRerenderAppPageWithGlobalError (#1906)
Add a test proving that for uncaught post-shell RSC errors (with no
client boundary), the server preserves the original HTML stream and the
client renders global-error. This is the Next.js behavior that justifies
keeping shouldRerenderAppPageWithGlobalError unconditional at false.
The server cannot detect userland client boundaries like
unstable_catchError from the route manifest, so the only safe way to
get a global-error rerender is to make a targeted error-boundary test.
The test uses a loading.tsx boundary so the error is truly post-shell,
not caught by the pre-shell error-recovery catch block.
* fix(error): restore sentinel wrapper for falsy thrown values in unstable_catchError
Restores the _CatchErrorState sentinel wrapper ({ thrownValue: unknown }) so
falsy thrown values (null, undefined, 0, "") are routed to the fallback
instead of being treated as no error. Also restores ErrorInfo.error as unknown.
Adds back unit tests for falsy values, class-component lifecycle, and
unstable_retry behavior parity with the component-rendered fallback shape.
Refs: #1906
* fix(error): align ambient ErrorInfo.error type with runtime implementation
* refactor(app-router): remove shouldRerenderAppPageWithGlobalError dead code
The function always returned false and its _options parameter and
ShouldRerenderAppPageWithGlobalErrorOptions type were vestigial.
The call site at app-page-render.ts:892-904 was therefore never taken.
Remove the function, type, import, call site, and the now-redundant
unit test, leaving the shell-level recovery path in
renderAppPageHtmlStreamWithRecovery as the sole error recovery seam.
Addresses ask-bonk review comment on #1906.
* fix(test): restore typed globalAny view and document Fallback as any intent
Addresses two non-blocking review findings from ask-bonk on PR #1906:
- Restored the typed `globalThis as unknown as { window?: unknown }`
pattern for window stubs in the unstable_catchError test section,
replacing the `(globalThis as any).window` pattern that was a type-safety
regression from the prior rewrite.
- Added an inline comment explaining why `Fallback as any` is necessary
in the class-component lifecycle test: the test probes the inner
_CatchError class directly, where the fallback function is used as a
React component type (receiving a single `{ props, errorInfo }` object)
rather than the public API signature `(props, errorInfo)`.
* chore: trigger CI
* fix(test): rename forwardedProps to props in unstable-catchError test
Addresses ask-bonk review finding on PR #1906: the test was seeding
the inner _CatchError class with the stale key name forwardedProps,
but the production wrapper now passes props (and isPagesRouter).
Since this test only exercises the App Router unstable_retry path
(which reads neither key), behavior was unaffected, but renaming
aligns the test with the current boundary shape for future readers.
* refactor(server): remove dead routeHasLocalBoundary field
Removing shouldRerenderAppPageWithGlobalError (commit
|
||
|
|
23dd15f763 |
fix(prod-server): stop double-evaluating the server bundle when chunks import the entry back (#1924)
* fix(prod-server): stop double-evaluating the server bundle when chunks import the entry back The production server imported the built server entry with a ?t=<mtime> query string. Node keys its ESM cache on the full URL including the query, so any code-split chunk that resolves the same entry file by its plain URL evaluated the entire server bundle a second time. Default Vite builds on both supported majors (Rollup on Vite 7, Rolldown on Vite 8) hoist modules shared between the entry's static graph and lazy route chunks into the entry chunk, and the chunks import them back as ../../index.js; realpath- canonicalized chunk ids also reach the entry via its real path when the server was started through a symlinked path. Module-level singletons then diverged between the two copies: boot-time initialization ran on the instance the server imported while route handlers read the never- initialized duplicate. Import the entry by its bare canonical (realpathed) file:// URL on first use so all importers converge on one module instance, and only append the ?t= cache-buster when the same path is imported again with a different mtime — the same-process rebuild scenario the query existed for. * docs(prod-server): clarify rebuild-branch tradeoff and probe constraints (#1924) - note in resolveServerEntryImportUrl's JSDoc that the ?t= rebuild branch trades back the single-instance guarantee (bare chunk back-imports keep resolving to the first build); freshness + single instance only hold together on the first import of a path - clarify the realpath try/catch only covers realpathSync.native failures, not missing entry paths (those still throw at statSync, as before) - document that the prod-singleton-state fixture probe is only valid on a freshly built output path |
||
|
|
d2ffc83670 |
fix(config): define cache components flag as boolean (#1903)
* fix(config): define cache components flag as boolean Disabled cache components currently compile to a truthy "false" string. That diverges from Next.js and makes bundled user code that checks !!process.env.__NEXT_CACHE_COMPONENTS select the cache-components path even when next.config rewrites select the legacy destination. Mirror Next.js by defining process.env.__NEXT_CACHE_COMPONENTS as a boolean expression, and keep the local slot shim tolerant of both boolean defines and string env values. Add a faithful revalidatePath-with-rewrites production regression that proves rewritten static and dynamic routes invalidate after revalidating the resolved destination path. * test(prod-server): cover cacheComponents enabled revalidatePath with rewrites * test(prod-server): skip dist/ copy in cacheComponents fs.cpSync filter Addresses review feedback: fs.cpSync(APP_FIXTURE_DIR, fixtureRoot) copies the earlier-built dist/ directory into the temp fixture before it is immediately deleted. Adding a filter skips the unnecessary copy. |
||
|
|
835e6b9943 |
fix(app-router): suppress redirect console errors in production onCaughtError (root-layout-redirect) (#1878)
* fix(app-router): suppress redirect console errors in production onCaughtError (#1830) When redirect() is called from a 'use client' root layout (or any client component), React catches the NEXT_REDIRECT sentinel via RedirectErrorBoundary and then calls the hydrateRoot onCaughtError callback. In dev mode vinext already filtered these via devOnCaughtError, but the production path had no onCaughtError at all — React's built-in fallback calls console.error, producing a spurious browser console error that fails the Next.js compat test assertion `expect(foundErrors).toBe(false)`. Fix: add prodOnCaughtError to app-browser-error.ts that silently drops navigation signal errors (NEXT_REDIRECT, NEXT_NOT_FOUND, NEXT_HTTP_ERROR_FALLBACK) and logs all other caught errors, preserving React's default behavior. Add fixture tests/fixtures/root-layout-redirect/ (dedicated 'use client' root layout with redirect()) and Playwright project root-layout-redirect (port 4184) with spec porting the Next.js root-layout-redirect > should work using browser. Part of #1830 * fix: include result/ page in root-layout-redirect fixture * fix: add lockfile entry for root-layout-redirect fixture, use prod server, import isNavigationSignalError Three fixes to unblock catastrophically-failing CI: 1. pnpm-lock.yaml: Add missing importer entry for tests/fixtures/root-layout-redirect. The fixture was added without updating the lockfile, causing ERR_PNPM_OUTDATED_LOCKFILE on every CI job during `vp install`. 2. playwright.config.ts: Switch the root-layout-redirect server from `npx vp dev` (dev mode) to `vinext build + vinext start` (production mode). The fix under test — prodOnCaughtError — is only active in production builds; running in dev mode exercised the pre-existing devOnCaughtError path and never touched the new code. 3. app-browser-error.ts: Replace the inline isNavigationSignalDigest helper with an import of isNavigationSignalError from utils/navigation-signal.ts. The module is dependency-free and already in the production browser bundle; inlining it reintroduced the drift risk that navigation-signal.ts was created to prevent. Addresses ask-bonk findings: blocking (test ran in dev mode) and maintainability (inline classifier duplication). * test: add prodOnCaughtError unit tests, run root-layout-redirect e2e in CI, scope gitignore negation |
||
|
|
9194cb55c6 |
fix(pages-router): hard-navigate to App Router destinations from Pages Router links (#1879)
* fix(pages-router): hard-navigate to App Router destinations from Pages Router links
When a Pages Router <Link> targets an App Router route (detected via the
prefetch manifest and recorded as `components[path] = { __appRouter: true }`),
`performNavigation` now checks for that marker and immediately calls
`window.location.assign/replace` instead of attempting a Pages Router SPA
fetch cycle that cannot succeed.
Mirrors Next.js router.ts:1448-1453. Fixes the deploy-suite test
`pages-to-app-routing > should work using browser`.
- packages/vinext/src/shims/router.ts: add __appRouter hard-nav guard
- tests/fixtures/app-basic/app/about/page.tsx: add id="app-page" to <h1>
- tests/fixtures/app-basic/pages/pages-to-app/[slug].tsx: new GSSP fixture page
- tests/e2e/app-router/pages-to-app-routing.spec.ts: new Playwright regression test
* fix(pages-router): replace no-explicit-any cast with in-operator type narrowing
`(getPagesRouterComponentsMap()[_appPath] as any)?.__appRouter` triggered
the `no-explicit-any` lint rule. Replace with a two-step lookup that uses
the `in` operator so TypeScript narrows the union type
`{ __appRouter: true } | Record<string, unknown>` without an `any` cast.
* fix(pages-router): normalize trailing slash in __appRouter marker key, rename locals, pin fast-path test
- Strip trailing slash on both the write side (markAppRouteDetectedOnPrefetch)
and the read side (performNavigation) so the marker key agrees when
trailingSlash:true adds a slash to the prefetch URL but not to the nav URL
(or vice-versa via router.prefetch with a raw un-normalised path).
Mirrors Next.js: removeTrailingSlash(removeBasePath(pathname)) at router.ts:1442.
- Rename _appPath/_appPathEntry to appPath/appPathNorm/appPathEntry (they are
used, not ignored; the underscore prefix was misleading).
- Add two new unit assertions in pages-router-app-prefetch-detection.test.ts:
one pins that the marker key is always stored without trailing slash, and one
pins that performNavigation calls window.location.assign (the fast-path) when
the __appRouter marker is present, so a regression in the new code path cannot
be silently masked by the pre-existing fallback.
* test(pages-router): address review nits in pages-to-app navigation tests
- e2e: wait for App Router hydration after the hard navigation so the
test asserts the App Router landing explicitly instead of only
matching the new DOM
- unit: drop the unnecessary microtask tick before asserting
window.location.assign — the fast path has no await before it, so
asserting synchronously also pins that the hard navigation cannot
lose a race against the SPA path
|
||
|
|
08504a7745 |
fix(app-router): preload next/dynamic chunks with CSP nonce (#1594)
* fix(app-router): preload next/dynamic chunks with CSP nonce
Rendered next/dynamic boundaries produced no SSR preload links for their client chunks. That diverged from Next.js and broke nonce-based CSP tests that inspect preload tags before hydration.
The runtime assumed React.lazy alone was enough for dynamic imports. It never carried the dynamic module IDs from the source call into SSR, and client JS chunk URLs did not use Next.js's static/chunks path shape.
Add a focused dynamic metadata transform, resolve boundary files from the client build manifest, emit nonce-bearing preload hints during SSR, and keep client JS assets under _next/static/chunks. Ported regression coverage verifies the nonce and the emitted path shape.
* fix: honor assetPrefix for dynamic preload globals
* test: cover asset-prefixed dynamic preload nonces
App Router production only covered the default dynamic preload URL path. That left assetPrefix regressions at the manifest-to-runtime emission boundary unguarded.
Add a startProdServer regression that builds app-basic with assetPrefix "/cdn" and asserts nonce-bearing next/dynamic chunk preloads render under /cdn/_next/static/chunks/.
* test: isolate asset-prefixed prod server globals
The assetPrefix regression starts a second production server in the same Vitest process. startProdServer and the generated RSC runtime install global manifest and module-loader state, so leaving the temporary build's globals in place breaks later production requests in the shared server.
Snapshot and restore the relevant globals around the temporary /cdn server while keeping the integration assertion at the real runtime boundary.
* refactor: reuse shared record guard in dynamic preload metadata
The dynamic preload metadata transform carried a local object guard even though the codebase already has a shared record predicate. That made the AST helper drift from the existing non-array record invariant used elsewhere.
Reuse isUnknownRecord from utils/record and keep the plugin-specific AST alias local to the transform.
* fix: make dynamic preload metadata binding-aware
The next/dynamic metadata transform matched calls by imported identifier text. Shadowed parameters and block bindings with the same name were therefore mutated even when they no longer referred to the next/dynamic import.
Walk the parsed module with lexical binding state so shadowed names are excluded, and collect object-form imports only from loader/modules rather than every property on the options object.
* fix: model switch and class scopes in dynamic preload transform
The dynamic preload metadata transform still treated imported next/dynamic names as active inside switch case lexical scopes and named class expression bodies. That could inject loadableGenerated metadata into calls that resolve to local bindings rather than the import.
The scope walker now removes switch case-block bindings while traversing case tests and consequents, leaves the switch discriminant in the outer scope, and scopes named class declarations and expressions through their children. Regression tests cover switch case shadowing and named class expressions.
* fix: address PR review feedback for next/dynamic CSP nonce
- Drop `as="style"` from `<link rel="stylesheet">` — `as` is a
preload attribute per the HTML spec, semantically incorrect on
stylesheet links. Browsers ignore it but it produces bad HTML.
- Remove brittle `import(` string gate from the transform — it could
miss `import ("./x")` (whitespace between import and paren). The
Vite filter already gates on "next/dynamic" and the AST parse is the
real validation.
- Add comment explaining MagicString mutation safety in Promise.all.
- Add comment explaining AST end-exclusive assumption on closeParen.
- Add test for whitespace-separated dynamic import syntax.
* fix: update global.d.ts comment to mention asset prefix, fix formatting
* refactor: extract computeClientRuntimeMetadata shared helper
Extract the duplicated client-manifest runtime metadata computation
(lazy chunks, dynamic preloads, client entry) into a single shared
helper in utils/client-runtime-metadata.ts.
Both prod-server.ts (Pages Router Node.js production server) and
index.ts (Cloudflare closeBundle hook) now call the same helper,
eliminating the highest-risk seam in the PR where basePath/
assetPrefix handling produced bugs.
* test: add TSX-generic and object-form metadata regression tests
- Add post-type-strip generic call test verifying multi-line
dynamic() calls work after TS/JSX stripping by esbuild.
- Add object-form existing loadableGenerated preservation test
to guard against duplicate injection in the object overload.
* chore: remove unused ClientRuntimeMetadata type export
* revert: discard unrelated lockfile change from previous commit
* test: replace deploy test simulators with computeClientRuntimeMetadata
simulateCloseBundleAppRouter and simulateCloseBundlePagesRouter
no longer hand-roll lazy chunk and dynamic preload computation.
They call the shared computeClientRuntimeMetadata helper directly,
matching production wiring.
Also:
- Update helper doc comment to mention Node server startup
- Remove unused imports (manifestFileWithAssetPrefix,
manifestFileWithBase, computeDynamicImportPreloads,
dynamicImportPreloadsWithBase) from deploy.test.ts
* test: add absolute assetPrefix CSP nonce tests and clean up redundant globals
* fix(app-router): keep CSP nonce on next/dynamic preloads from Server Component call sites
DynamicPreloadChunks rendered in the environment of the dynamic() call site. For a Server Component call site it ran in the RSC environment, where the script-nonce React context is unavailable, so the emitted dynamic preload <link>s dropped the request nonce — a CSP violation under `script-src 'nonce-…' 'strict-dynamic'`. The existing tests missed this because every fixture called dynamic() from "use client" modules, which render in the SSR pass where the nonce is present.
Extract DynamicPreloadChunks into its own "use client" module (mirroring Next.js's PreloadChunks and vinext's next/script shim) so it always renders in the SSR pass where withScriptNonce() installs the provider, regardless of whether dynamic() is called from a Server or Client Component.
Add fixture regression tests: server-call-site JS and CSS preloads carry the nonce, an ssr:false boundary emits no server preload (Next.js loadable.tsx parity), and preloads are still emitted without a nonce when no CSP is set.
* test(app-router): add browser CSP-nonce e2e for Server Component next/dynamic call site
The existing csp.spec.ts browser test only exercises the client call-site page (/nextjs-compat/dynamic). Add a Playwright test for the Server Component call site (/nextjs-compat/dynamic/rsc-imports-client) asserting the lazily-loaded client widget hydrates under script-src 'nonce-…' 'strict-dynamic' with no CSP violations in the console.
* fix(app-router): correct next/dynamic preload key-spaces and harden the transform
Addresses review findings on the next/dynamic CSP-nonce feature.
- Lazy chunks must stay in the SSR-manifest key-space (basePath only); only dynamic preloads take the assetPrefix. Applying an absolute-URL assetPrefix to lazy chunks broke the Pages Router modulepreload-exclusion membership test and leaked lazy chunks into <link rel=modulepreload>.
- Resolve symlinks on both sides when computing a dynamic boundary's module ID (reusing relativeWithinRoot/tryRealpathSync), so the key matches the client manifest under pnpm/Cloudflare symlinked layouts instead of silently dropping the preload.
- Throw on dynamic() calls with >2 args (Next.js react-loadable parity).
- Insert the generated options arg right after the first arg, preserving comments/whitespace (the previous substring-comma scan ate commas inside comments).
- Recurse into _next/static/chunks/ in the on-disk client-entry fallback (client JS now lands under chunks/).
- Add typeof-window guard to DynamicPreloadChunks and document the deliberate as="style" omission for stylesheet links.
- Extract buildRuntimeGlobalsScript so the Cloudflare closeBundle injection shares one source of truth with the deploy tests (which now mirror includeClientEntry + the mixed app+pages branch).
Tests: lazy/dynamic key-space split incl. absolute-assetPrefix leak regression and a realistic already-prefixed manifest; mixed app+pages client-entry injection; symlinked module-id resolution; nested dynamic() calls; >2-arg throw; comment-safe insertion; ssr:false positive content assertion; honest dev-mode E2E naming (preload-nonce coverage lives in the prod-server Vitest suite).
* fix(app-router): fix parenthesized next/dynamic loader corruption + harden re-review findings
Follow-up to the re-review of
|
||
|
|
de33c3e5a0 |
fix(build): inline ../-relative font assets in OG routes (#1866)
* fix(build): inline ../-relative font assets in OG routes
The vinext:og-inline-fetch-assets plugin only matched paths starting
with "./" (e.g. "./font.ttf"), but Next.js test fixtures for OG custom
fonts use "../"-relative paths like "../../../assets/typewr__.ttf".
Without the inline, at runtime:
- Cloudflare Workers: import.meta.url is "worker" (not a URL), so
new URL("../../../assets/...", import.meta.url) throws TypeError.
- Node.js: fetch() does not support file:// URLs, so the inlined
file:// URL produced by the import-meta-url plugin fails.
Fix: relax the regex from `\.\/[^"']+` to `\.[^"']+` so any
dot-relative path (./x, ../x, ../../x, etc.) is inlined as base64.
Apply the same fix to the readFileSync(fileURLToPath(...)) pattern.
Failing tests addressed:
- test/e2e/og-routes-custom-font: should render og with custom font
for app routes (edge runtime fetch + Node.js fs.promises.readFile)
- test/e2e/app-dir/metadata-font: should handle custom fonts in both
edge and nodejs runtime
* fix(build): inline OG font assets when a formatter adds a trailing comma
The vinext:og-inline-fetch-assets regex only matched the single-line,
comma-less form of `fetch(new URL(...)).then((res) => res.arrayBuffer())`.
When a formatter (Prettier `trailingComma: "all"`, oxfmt) wraps the call
across lines it appends a trailing comma:
.then((res) =>
res.arrayBuffer(),
)
which no longer matched, so the font was left as a runtime fetch. On
Cloudflare Workers `import.meta.url` is "worker", so
`new URL("../...", "worker")` throws "TypeError: Invalid URL" and the OG
route returns 500 — even though the ../-relative path itself was already
supported. Relax both `.then(...)` alternatives with an optional `,?`.
Add real e2e coverage (`/api/og-custom-font`) mirroring Next.js'
og-routes-custom-font fixture: an edge OG route that loads
assets/noto-sans.ttf via
`fetch(new URL("../../../assets/noto-sans.ttf", import.meta.url))`. It
runs in tests/e2e/og-image.spec.ts across the app-router (Node dev),
cloudflare-dev and cloudflare-workers (workerd) projects, with the route
and font added to both the app-basic fixture and the app-router-cloudflare
example. Also adds a unit test for the formatted/trailing-comma shape in
tests/og-inline.test.ts.
* fix(build): inline OG font assets with a semicolon-terminated block body
Follow-up from the /bigbonk review on PR #1866: the block-body alternative
of the og-inline-fetch-assets regex ended `…\.arrayBuffer\(\)\s*\}?\s*,?\s*\)`
with no allowance for a `;` before `}`, so formatter output such as
.then((res) => {
return res.arrayBuffer();
})
.then(function (res) { return res.arrayBuffer(); })
was left as a runtime fetch — which throws "Invalid URL" on Workers
(import.meta.url === "worker"). Add `;?` before the block-body `}` and unit
tests for the arrow- and function-expression block-body forms.
|
||
|
|
3a1e56748f |
fix(app-router): match Pages navigation params in hybrid builds (#1741)
* fix(app-router): emit pages client entry for hybrid builds * fix pages router navigation params readiness * fix(pages-router): track hashed fallback client entry * fix(pages-router): honor navigation readiness in hooks * fix(pages-router): derive server router.isReady from SSR readiness Address review feedback on #1741: - getRouterSnapshot() now reads navigationIsReady from the SSR context on the server instead of unconditionally returning true. For pre-ready Pages routes (auto-export dynamic / query-string / rewrite-capable builds) this keeps useRouter().isReady consistent between SSR and client hydration, avoiding a hydration mismatch for components that read it in JSX. Mirrors Next.js render.tsx's server readiness rule. - Drop the redundant guard branch in getClientParamsSnapshot. - Add a regression test asserting server useRouter().isReady reflects the SSR navigationIsReady context. * fix(pages-router): align param resolution and dev/prod readiness parity Address review feedback (ask-bonk + self-review) on #1741: - Align client Pages navigation params to query-first, matching the server snapshot and Next.js's adaptForPathParams (derives path params from router.query). __NEXT_DATA__.query is kept current on client navigation, so the two snapshots are now provably identical and avoid a latent post- hydration mismatch. - Populate window.__VINEXT_PAGE_PATTERNS__ in the dev server (own script tag) so the next/navigation compat hooks can resolve a dynamic pattern from a resolved path in dev exactly as in production. Closes a dev/prod parity gap (prod set this via the client entry; dev never did). - Drop the redundant pre-readiness setSSRContext in dev; the single post- module-load call now publishes navigationIsReady before rendering, matching the prod handler. - Document that installPagesClientAssetGlobals is intentionally inert for App-only builds, and that getReadonlyPagesSearchParams' string-keyed cache is safe to share across concurrent SSR requests (immutable wrappers). - Add a dev regression test asserting __VINEXT_PAGE_PATTERNS__ exposure. * fix(pages-router): prefer URL params for client snapshots * fix(app-router): gate pages asset globals * test(app-router): add production test for hybrid basePath + assetPrefix * fix(navigation): widen usePathname return type to string | null * refactor(pages-router): dedupe navigation readiness modeling and cache pattern scan Addresses code-review cleanup findings on the Pages Router navigation readiness work: - Extract buildPagesReadinessNextData() so the dev SSR handler and the production Pages page handler derive the gssp/gsp/gip/appGip/autoExport __NEXT_DATA__ readiness flags from one place, instead of two hand-kept copies that could drift and produce a dev/prod hydration mismatch. - Extract resolvePagesNavigationPathname() shared by the client and SSR branches of getPagesNavigationContext, so the pathname null-ness rule (fallback / pre-ready auto-export dynamic) cannot diverge between them. - Memoize resolvePagesRoutePatternForPath()'s pattern scan, which ran an uncached O(routes) loop on every useSyncExternalStore snapshot read. - In getServerSearchParamsSnapshot, serialize the source only when the object-identity fast path misses, avoiding a toString() on every read. * refactor(pages-router): consistent ISR readiness data + robustness cleanups Addresses further code-review findings: - Thread the readiness flags (gssp/gsp/gip/appGip/autoExport) through the ISR background-regeneration path: renderPagesIsrHtml now serializes the same __NEXT_DATA__ readiness state the initial render emits, so a regenerated, re-cached page hydrates with the identical initial router.isReady the server computed. Previously these flags were dropped on that path — benign today (ISR-eligible getStaticProps routes never trigger the flag-gated early-ready branches) but a latent dev/prod hydration-readiness drift. Adds a shared PagesNextDataExtras type so every render path stays in lockstep. - readSsrManifest: degrade a malformed/unparseable SSR manifest to "no SSR manifest" with a warning instead of throwing and aborting server startup, restoring the previous tolerant behavior. - pages-readiness: project PagesReadinessNextData from the canonical VinextNextData and accept PagesPageModule, instead of bespoke types that could drift from the __NEXT_DATA__ shape. - getReadonlyPagesSearchParams: drop the redundant string-keyed cache slot; the per-object WeakMap is authoritative and concurrency-safe on its own. - isPagesRouterReady: drop the redundant server-side window check. * fix(pages-router): restore string-keyed readonly search-params cache Reverts the cache simplification from the previous commit. The string-keyed slot is load-bearing, not redundant: across the Pages Router pre-ready → ready transition the navigation context swaps in a new URLSearchParams instance even when the query string is unchanged, so the per-object WeakMap alone returns a fresh ReadonlyURLSearchParams wrapper and useSearchParams() loses Object.is stability — re-firing a [searchParams] effect. Caught by the rewrite-driven pre-ready static page e2e (search-params-snapshots saw ['',''] not ['']). Keeps the improved comment explaining why both cache levels are needed. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
9596b72495 |
fix(pages-router): pass revalidateReason "on-demand" to gsp/gssp (#1462) (#1856)
* fix(pages-router): pass revalidateReason "on-demand" to getStaticProps (#1462) Implements res.revalidate() for Pages Router API routes and wires the on-demand revalidation trigger through to getStaticProps/getServerSideProps context as revalidateReason: "on-demand". res.revalidate(urlPath) issues an internal HEAD request to the target path carrying the x-prerender-revalidate header (mirroring Next.js's api-resolver). The Pages render path (dev-server and prod pages-page-data) detects that header, bypasses the fresh/stale cache-hit short-circuits, and regenerates the entry synchronously with reason "on-demand". The build and stale reasons already worked; this finishes the on-demand case. Closes #1462 * fix(pages-router): authenticate on-demand revalidation with a secret (#1462) The on-demand `revalidateReason` detection treated `x-prerender-revalidate` as an unauthenticated presence check and sent the literal value "1". Any external client could send `x-prerender-revalidate: 1` to force synchronous regeneration of any ISR page, bypassing the fresh/stale cache short-circuits — a cache-stampede/DoS vector. Mirror Next.js's `checkIsOnDemandRevalidate`: a request is only treated as on-demand revalidation when the `x-prerender-revalidate` header value EQUALS a secret (the vinext analog of `previewModeId`). Introduce a per-process revalidate secret (`getRevalidateSecret`, 256-bit random hex via Web Crypto) that `res.revalidate()` attaches to its internal loopback request, and a constant-time `isOnDemandRevalidateRequest` equality check on the receiving dev-server and Pages page handler. Header presence alone is never honored. Also fix the test's HTML-reason extraction to avoid CodeQL's incomplete-sanitization / bad-HTML-regex findings, and add a negative test asserting forged `x-prerender-revalidate` values ("1", "", a guess) are rejected while the real secret is accepted. * refactor(pages-router): share res.revalidate() impl; support unstable_onlyGenerated (#1462) Address non-blocking bonk review findings: - Extract the two near-identical `res.revalidate()` implementations (`api-handler.ts`, `pages-node-compat.ts`) into a shared `performOnDemandRevalidate` helper so the secret wiring and success detection cannot drift between dev and prod. - Align success detection with Next.js's api-resolver: accept `x-nextjs-cache: REVALIDATED`, status 200, or a 404 when the caller passed `unstable_onlyGenerated`. Add `unstable_onlyGenerated` support (`x-prerender-revalidate-if-generated` header). * docs(pages-router): note x-nextjs-cache REVALIDATED branch is parity-only (#1462) * fix(pages-router): make on-demand revalidate secret build-time (shared across isolates) The on-demand ISR revalidation secret was a per-process random value. On Cloudflare Workers, res.revalidate()'s loopback fetch() can land on a different isolate than the sender, so a per-process secret mismatches across isolates and false-rejects legitimate revalidations. Mirror Next.js's previewModeId: generate one 256-bit secret at build time (in the vinext build CLI, once per build, shared across plugin instances like __VINEXT_SHARED_BUILD_ID) and bake it SERVER-ONLY into every server bundle via a Vite define (__VINEXT_REVALIDATE_SECRET). It is byte-for-byte identical in every isolate and never reaches the client bundle. getRevalidateSecret() reads the baked constant at runtime and falls back to a process-shared random secret in dev (single-process, no regression). Also persist revalidateSecret in vinext-server.json for diagnostics/tooling parity with prerenderSecret. Constant-time safeEqual validation and reject-arrays/empty/absent behavior are unchanged. Refs #1462 * test(pages-router): cover server-only revalidate secret; drop dead manifest reader Address ask-bonk review of the build-time revalidate secret: - Add tests pinning the security-critical invariant that the __VINEXT_REVALIDATE_SECRET define is baked into server environments (rsc/ssr) but NEVER into the client bundle (the client env returns null outright). A leak would ship the secret to every browser and re-open the cache-stampede vector the equality check prevents. Also harden the existing "no-ops when defineServer unset" test to explicitly clear the env var instead of relying on it happening to be unset. - Drop the unused readRevalidateSecret export and the orphaned revalidateSecret manifest field — the runtime authenticates against the baked define (both Workers isolates and the Node prod-server run the bundle), so the on-disk copy had no consumer (knip-flagged dead code). - Hoist the client early-return in vinext:compiler-define-server so the server-only guarantee is structural (removes the redundant name check), and reword the dev-fallback comments ("absent unless set during vinext build"). Refs #1462 |
||
|
|
c669930f56 |
fix(build): correct CSS ordering for global-not-found (#1549) (#1858)
* fix(build): correct CSS ordering for global-not-found (#1549) * test(build): assert global-not-found CSS ordering in production (#1549) Adds a production-build regression test for #1549: builds the global-not-found-css-order fixture, serves it via the Vite preview server, and asserts the route-miss 404 document serves global-not-found's own CSS (red wins) without leaking the root layout's stylesheet (green), while matched routes still serve the layout's CSS. Verified the test fails without the framework-chunk split in client-build-config.ts (the 404 leaks the layout's green) and passes with it. * chore: update lockfile for global-not-found-css-order test fixture (#1549) * refactor(build): migrate RSC framework chunk to codeSplitting; add unit tests Address bonk review on #1858 (issue #1549): - Replace the deprecated rolldown `advancedChunks` output config with the non-deprecated `codeSplitting` form for Vite 8+, mirroring the client build. Stops the per-prod-build deprecation warning. - Derive both RSC_FRAMEWORK_CHUNK_TEST (regex) and isRscFrameworkModule (predicate) from a single FRAMEWORK_PACKAGES list so they can't drift. - Add focused unit tests for createRscFrameworkChunkOutputConfig (both the Vite 7 manualChunks and Vite 8+ codeSplitting branches) and for the framework package matching (matches react/react-dom/scheduler/ react-server-dom-webpack, excludes react-icons / @react-aria/*). |
||
|
|
29a040dd21 |
fix(app-router): include route pattern in repeated-slash Link warning (#1554) (#1855)
* fix(app-router): include route pattern in repeated-slash Link warning (#1554) The repeated forward-slashes Link warning added in #1563 fired with the wrong "in page" segment: it used `window.location.pathname` (or `"/"` during SSR) instead of Next.js's route pattern (`router.pathname`, e.g. `/my/path/[name]`). The Next.js compat test `repeated-forward-slashes-error` asserts the *full* message including that route pattern, so it kept failing in the deploy suite despite #1563 — the `/Invalid href/` regex matched but the `toContain` of the exact message did not. Source the page segment from the current render's route pattern via a new `getCurrentRoutePathnameForWarning()` helper: the Pages Router SSR context's route pattern on the server, `window.location.pathname` on the client, falling back to `"/"`. Normal Link behaviour is unchanged. Add an SSR-level compat test (app + pages router) that renders a Link with a repeated-slash href and asserts the exact warning, plus the upstream dynamic-route fixture `pages/my/path/[name]`. Finishes #1554 (the warning wasn't firing with the right page name despite #1563). Closes #1554 * fix(app-router): read route pattern via side-effect-free accessor The previous commit imported `router.ts` into `link.tsx` to source the route pattern for the repeated-slash warning. But `router.ts` has a browser-only top-level side effect (`installWindowNext({ router })`), so pulling it into the Link shim — which is in the App Router client bundle too — clobbered `window.next.router` with the Pages Router instance, breaking App Router client navigation (useParams/prefetch/interception E2E timeouts). This is exactly why `navigation.ts` deliberately avoids importing `router.ts` and reads the Pages nav context through a `Symbol.for` handle. Mirror that pattern: add a side-effect-free `route-pattern-for-warning` module that reads the SSR route pattern through a `Symbol.for` accessor published by the server-only `router-state.ts`. The Link shim imports only this tiny module, so no Pages Router side effect leaks into the App Router bundle. Behaviour is unchanged: pages-router SSR reports `in page: '/my/path/[name]'`, client/App Router fall back to `window.location.pathname` then `"/"`. |
||
|
|
dd3fb05977 |
test(pages-router): cover javascript: URL blocking (#1576) (#1854)
* fix(pages-router): emit console.error for javascript: URL navigation (#1576) Finishes the Pages Router half of #1576 (the App Router warnings already land). Adds the four Pages Router scenarios from the upstream test/e2e/app-dir/javascript-urls suite — Link `href`, Link `as`, router.push, and router.replace — as e2e fixtures + a Playwright spec, plus unit coverage for the shared Link shim's dangerous-click handler. No production change is required: the Link shim already emits the canonical "Next.js has blocked a javascript: URL as a security precaution." console.error on click for both `href` and `as`, and the Pages Router router.push/replace synchronous guard from #1575 already does the same. This PR adds the missing Pages Router coverage that proves all four scenarios. Closes #1576 * test(pages-router): suppress dev error overlay before safe navigation The router.push/replace javascript: scenarios throw synchronously (the #1575 behaviour that surfaces the console.error), which trips vinext's dev error overlay. Its backdrop intercepted the follow-up safe-link click, timing out the test even though the security block already passed. Suppress the overlay via the existing disableDevErrorOverlay helper after asserting the block, matching how other pages-router/app-router specs handle intentional dev-time errors. * test(pages-router): cover javascript: URL blocking in production build The Pages Router javascript: URL guard (Link dangerous-click handler in shims/link.tsx + router.push/replace synchronous guard in shims/router.ts, both via shims/url-safety.ts) is already correct on main. The existing e2e spec, however, only runs against the dev server (vp dev), while the Next.js deploy suite exercises these scenarios against a production build where the dev error overlay is absent and the client bundle is minified. Add a pages-router-prod Playwright spec that re-runs all four upstream scenarios (Link href, Link as, router.push, router.replace) against vinext build + vinext start, so a prod-only regression is caught here. Refs #1576 |
||
|
|
ca690db954 |
test(actions): cover server action POST under node-runtime middleware (#1480) (#1847)
* test(actions): cover server action POST under node-runtime middleware (#1480) Adds regression coverage for cloudflare/vinext#1480: a server-action POST must still be intercepted and executed when a node-runtime middleware matches the action path and consumes the request body before falling through with NextResponse.next(). The app-basic fixture's middleware now matches `/nextjs-compat/action-node-mw` and reads the POST body (mirroring Next.js' `middleware-node.js`), with a bound form server action on that page. Two tests exercise the scenario: - tests/nextjs-compat/action-node-middleware.test.ts (dev inline-middleware path) - tests/app-router-production-server.test.ts (production RSC entry path, the layer the upstream deploy suite exercises) vinext already isolates the middleware's body branch from the downstream request (applyAppMiddleware / createNextRequest clone the body) and treats node-runtime middleware identically to edge middleware, so the action POST is dispatched correctly. These tests lock that behavior in across the dev and production request paths. Closes #1480 * style(test): apply formatter to action-node-mw prod test * test(actions): declare runtime:nodejs on fixture middleware for #1480 Address review: make the app-basic fixture middleware faithfully mirror Next.js' middleware-node.js by exporting config.runtime = 'nodejs'. vinext runs all middleware on Node regardless, so this is a behavioural no-op for the other matcher entries, but it makes the action-node-mw regression a true guard for the named node-runtime scenario from the issue title. * docs(test): point #1480 fixture comment at middleware-runtime config logic Address review nit: the config-reading logic (matcher-only, no runtime branch) lives in server/middleware-runtime.ts, so reference that file where a future runtime branch would land. |
||
|
|
49d753217b |
fix(app-router): propagate "use cache" tags to route-handler ISR entries (#1453) (#1848)
* fix(app-router): propagate "use cache" tags to route-handler ISR entries (#1453) * fix(app-router): bubble nested use-cache tags to parent on data cache HIT (#1453) |
||
|
|
600ba69b4d |
fix(routing): propagate middleware draft cookie to pages/api fallback (#1520) (#1845)
* fix(routing): propagate middleware draft cookie to pages/api fallback (#1520)
In a hybrid app (app/ + pages/api/*), requests with no matching App Router
route fall through to the Pages Router via renderPagesFallback. The bridge
forwarded middleware-mutated request headers and response headers, but dropped
the __prerender_bypass Set-Cookie emitted by draftMode().enable()/disable()
inside middleware — that cookie lives in a request-scoped side channel that
App Router route handlers and page renders read via getDraftModeCookieHeader(),
but the Pages fallback path never did.
Thread getDraftModeCookieHeader through the bridge and append the draft cookie
to the Pages Router fallback response, matching how App Router responses surface
it. Closes the draft-mode flow called out in #1520.
Closes #1520
* test(routing): cover draft-cookie fallback in app-pages-bridge unit test
Add getDraftModeCookieHeader to the renderPagesFallback dependency stub and
assert the draft cookie is appended (API + renderPage branches) and omitted
when middleware did not enable draft mode. Keeps the unit test in sync with
the new required dependency.
* docs(routing): document hybrid-dev draft-mode limitation in pages bridge
Address review feedback on #1845: clarify in app-pages-bridge that the
draft-cookie fallback covers production (inline RSC handler), while hybrid dev
runs middleware in a separate Vite Pages SSR runner where draftMode() inside
middleware is not yet permitted — a pre-existing limitation tracked separately
from #1520. The unit-test regression flagged by review was already fixed in
|
||
|
|
6958dfd0b4 |
fix(build): expand require.context into import.meta.glob map (#1501) (#1836)
* fix(build): expand require.context into import.meta.glob map (#1501) * style: format require-context fixture page * fix(build): strip g/y regex flags and default require.context recursive to true Address ask-bonk review on #1836: - Strip global/sticky flags when reconstructing the filter regexp; a stateful g/y RegExp.test() drops every other matching module. - Default the omitted recursive arg to true to match Webpack's useSubdirectories default (was silently false). - Add a g-flagged fixture/test case locking in the flag-stripping fix. - Clarify the misleading descent comment in collectRequireContextCalls. * test(require-context): cover ctx(key) retrieval and MODULE_NOT_FOUND Address ask-bonk follow-up: lock in the runtime contract beyond .keys(). - Assert ctx('./parent/file1.js').default resolves the module namespace. - Assert an unknown key throws with code 'MODULE_NOT_FOUND'. * docs(require-context): document extensionless no-regex parity caveat Address ask-bonk note: with no filter regexp, import.meta.glob only surfaces resolvable modules, so extensionless keys Webpack would include can be dropped. Documented as a low-risk divergence (upstream Next.js's extensionless test is disabled/Turbopack-pending). |
||
|
|
3f6c174c20 |
fix(app-router): resolve query-only Link href against current path (#1540) (#1844)
A <Link href={{ query: {...} }} /> with no `pathname` was resolved by
defaulting the base to "/", producing "/?query" instead of a query-only
href. The shallow navigation then recorded the wrong history entry (the
site root) so back/forward traversal landed on the wrong URL/page.
Mirror Next.js's formatUrl() (`pathname = urlObj.pathname || ''`): when
`pathname` is omitted, emit a relative query-only href (`?query`) so the
existing relative-href resolution applies it against the current path.
Adds a ported e2e regression (middleware-rewritten shallow link history
traversal) plus a Link unit test.
Closes #1540
|
||
|
|
def033614b |
fix(app-router): render built-in fallback when global-error.tsx throws (#1548) (#1837)
* fix(app-router): render built-in fallback when global-error.tsx throws (#1548) When a user's app/global-error.tsx itself throws while rendering, vinext previously crashed the request with a raw 500. Next.js instead nests the user's global-error inside an outer boundary whose fallback is the built-in default global-error, so a broken boundary degrades to a usable 'This page couldn't load' document. This change mirrors that structure: - Client-side: wrap the user's global-error in a GlobalErrorBoundary whose fallback is the built-in DefaultGlobalError (matches Next.js's outer RootErrorBoundary errorComponent={DefaultGlobalError}). - Server-side: React's renderToReadableStream rejects on a shell-level throw even with an error boundary present, so when the global-error boundary throws during SSR we re-render with the built-in default global error. Scoped to the global-error boundary (which owns the whole document); other boundaries propagate as before. The original error message still reaches the boundary in dev and a digest in prod via sanitizeErrorForClient (unchanged). Adds nextjs-compat coverage (dev + production preview) ported from test/e2e/app-dir/global-error/error-in-global-error. Closes #1548 * fix(app-router): format TRACKING.md table and address review notes (#1548) - Reflow TRACKING.md global-error table (vp check formatting). - Document GlobalErrorBoundary's intentional lack of a resetKey. - Add a port note explaining the SSR-via-pathname throw deviates from upstream's client useEffect throw (HTTP/SSR suite has no browser). * test(app-router): unit-cover global-error server-side fallback retry (#1548) Add a focused renderAppPageErrorBoundary unit test that asserts a throwing global-error boundary re-renders with the built-in default global-error (HTTP 200), locking in the try/catch retry directly rather than only via the fixture-coupled integration test. * fix(app-router): re-throw navigation signals from throwing global-error (#1548) Narrow the server-side global-error fallback so it only degrades genuine render failures to the built-in default. Navigation/HTTP-access signals (redirect/notFound/forbidden/unauthorized) thrown from within global-error are re-thrown so they reach their dedicated handlers instead of being swallowed into a built-in 200. Addresses review feedback that the catch was broader than Next.js's React error boundary. Adds a unit test asserting a notFound() signal from global-error propagates. * docs(app-router): clarify global-error signal re-throw comments (#1548) Soften wording: re-thrown navigation signals propagate to the top-level handler (same as before this change), rather than reaching dedicated handlers from this position. No behavior change. |
||
|
|
de2ad5278f |
fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552) (#1841)
* fix(app-router): respect reactMaxHeadersLength for preload Link header (#1552) * test(app-router): cover reactMaxHeadersLength config resolution * refactor(app-router): prioritize React preloads in Link header; cover buildAppPageLinkHeader Address review feedback on #1841: - Order React preload hints before font preloads so render-critical entries survive a tight reactMaxHeadersLength cap. - Rename the buildAppPageHtmlResponse option to linkHeader (it now carries the combined header, not just fonts). - Add unit coverage for buildAppPageLinkHeader (cap, 0-disables, ordering, whole-entry truncation). |
||
|
|
97947ce775 |
fix(app-router): apply trailingSlash to route handler request URL (#1827) (#1835)
* fix(app-router): apply trailingSlash to route handler request URL (#1827) App Router route handlers received a NextRequest whose `url` was normalized with the default `trailingSlash: false` policy, so a request to `/runtime/edge/` (with `trailingSlash: true`) surfaced `req.url` without the trailing slash even though `req.nextUrl.pathname` kept it. Thread the configured `trailingSlash` through the route-handler dispatch chain (dispatch -> execution/cache -> tracked request -> NextRequest) so `NextURL` applies the correct policy when computing `request.url`, matching Next.js (test/e2e/app-dir/app-routes-trailing-slash). Closes #1827 * test(app-router): cover node runtime for trailingSlash:false route handler (#1827) Address ask-bonk review note on PR #1835: the trailingSlash:false suite only exercised the edge runtime. Parameterize it over edge + node to match the trailingSlash:true suite's coverage. |
||
|
|
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 |
||
|
|
6005541c19 |
fix(app-router): restore scroll snapshots on back (#1743)
* fix(app-router): restore scroll snapshots on back * fix(app-router): invalidate scroll snapshots with bfcache epoch * fix(app-router): guard synchronous popstate scroll restore * fix(app-router): strip basePath for snapshot restore targets * fix(app-router): canonicalize snapshot restore target search * docs: document sync-over-async invariant in popstate snapshot restore * docs: document cross-module ordering contracts per review Adds comments for three load-bearing invariants identified in review: - rememberHistoryStateSnapshot layout effect ordering - synchronousPopstateScrollRestoreNavigationId single-slot latch - restoredBfcacheIds/reuseCurrentBfcacheIds snapshot timing |
||
|
|
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> |
||
|
|
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 |
||
|
|
aab9240e10 |
fix(app-router): hard navigate streamed redirects (#1742)
* fix(app-router): hard navigate streamed redirects * fix(navigation): guard page redirect marker checks * test(app-router): count redirect document loads |
||
|
|
bc31644649 |
fix(app-router): match streaming metadata error responses (#1794)
* fix(app-router): match streaming metadata error responses Vinext currently serializes metadata-originated redirects into a Flight payload for document requests. That response is not an HTML page, so direct browser visits cannot follow the streamed redirect and html-limited bots receive 200 instead of the blocking redirect. The dispatcher also renders HTTP access fallback head metadata from layouts only, so not-found boundary metadata is omitted when generateMetadata() throws notFound(). Use Next.js's split response model: RSC requests keep the Flight redirect digest, streaming-capable document requests receive the refresh meta tag HTML, and html-limited bots fall through to the HTTP redirect. Thread the selected HTTP access boundary module into fallback rendering so boundary metadata is resolved exactly once. * test(app-router): cover metadata RSC redirects The unit CI failure came from a strict dispatch test receiving optional fallback fields with undefined or null values. Those fields are only meaningful when a boundary module is actually selected. Build fallback option objects without absent boundary fields, preserving the existing call shape for ordinary page-level HTTP access fallbacks. Add focused coverage for fromMetadata redirects on RSC requests so metadata document refresh handling cannot accidentally consume the client navigation Flight path. * docs(app-router): update metadata redirect transport docs and drop dead boundary fallback Address review feedback on the metadata streaming error split: - Update the stale APP_PAGE_METADATA_ERROR_MARKER docblock to describe the new three-way transport split (flight digest / HTML refresh meta tag / 307). - Note the intentional minimal-stub HTML divergence from Next.js's full-document insertion in buildMetadataRedirectHtmlResponse. - Drop the redundant resolveAppPageHttpAccessBoundaryComponent fallback in renderAppPageHttpAccessFallback (boundaryModule already covers both paths). * docs(app-router): fix stale metadata redirect docs in head resolver and test reference Address remaining bonk re-review nits: - Update resolveModuleMetadata docblock in app-page-head.ts to describe the three-way transport split (it still claimed redirects ride in the flight payload even for document SSR). - Point the APP_PAGE_METADATA_ERROR_MARKER Next.js test reference at metadata-streaming, which is where the behavioural contracts are ported from. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
330de786c5 |
test(interception): fixtures and E2E specs for 4 dynamic interception suites (#1364 Part B) (#1785)
* test(interception): add fixtures and E2E specs for 4 dynamic interception suites Adds fixture pages and Playwright E2E specs for the 4 Next.js interception route test suites tracked in issue #1364 (Part B): - interception-dynamic-segment: @modal/(.)[username]/[id] and @modal/(.)test-nested/@sidebar — 6 tests (all pass) - interception-dynamic-single-segment: @modal/(.)groups/[id]/new from a multi-segment source route — 5 tests - interception-dynamic-segment-middleware: locale-rewrite middleware + (.)[username]/p/[id] interception — 4 tests - parallel-routes-and-interception-from-root: (...)[locale]/intercepted from the app root — 3 tests Suite 3 middleware rewrite is tightly scoped to /interception-mw/* in the shared app-basic middleware.ts to avoid interfering with other tests. Tests for suites 2/3/4 that depend on the computeInterceptTarget fix (fix/interception-compute-target, Part A) will fail until that branch is merged, which is expected. * test(interception): mark 7 known-failing tests with test.fail() pending Part C The 7 tests across suites 2-4 exercise behavior that Part C (#1364) will fix. Marking them test.fail() keeps CI green while preserving the specs as executable documentation of the expected behavior. |