mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.0
133 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cda19e1c58 |
fix(app-router): preserve recent segment state with Activity BFCache (#1739)
* fix(app-router): preserve recent segment state with Activity BFCache Fresh App Router navigations were reusing the active React tree when moving between dynamic segment values. That leaked client state into a newly entered segment and made the upstream back-forward-cache suite fail before it even reached browser traversal assertions. The root cause was that vinext had history-entry bfcacheId plumbing but no separate segment state-key cache. Next.js treats those identities separately: render state is keyed by segment identity/pathname, while useRouter().bfcacheId identifies a concrete history entry. Add state-key maps for app router slots, render retained entries through React.Activity when cacheComponents is enabled, keep the active one-entry keyed reset path otherwise, and port focused Next.js regression coverage for the three-entry BFCache behavior. * fix(bfcache): store stateKeyMap in retained slot entries and re-provide on render Review feedback requested two changes: 1. Add stateKeyMap to BfcacheSlotEntry so each retained BFCache entry carries the state-key map that belonged to it when active, rather than borrowing the current navigation's map. This prevents nested hidden entries from being rendered with old elements but new pathname-derived state keys — an identity corruption bug that upstream Next.js avoids by rendering each retained entry from its own state. 2. Add a nested dynamic regression test: /nested/[section]/item/[id] with client counters in both the [section] layout and [id] page. The existing flat /page/[n] tests do not exercise nested retained-entry identity, which is exactly where the missing state-key-map bug lives. Changes: - BfcacheSlotEntry gains stateKeyMap?: Readonly<Record<string, string>> - updateBfcacheSlotEntries accepts and stores an optional stateKeyMap argument - BfcacheSlotBoundary stores the current stateKeyMap alongside elements when caching the active entry - Both the Activity visible/hidden path and the non-cacheComponents single-entry path now wrap rendered entries in BfcacheStateKeyMapContext.Provider using the entry's own stateKeyMap (falling back to current map for forward compatibility) - New e2e test verifies nested layout + page counter preservation across segment navigations * refactor(bfcache): extract shared provider stack into BfcacheEntryProviders BfcacheSlotBoundary rendered the BfcacheStateKeyMapContext / ElementsContext / SegmentContext provider stack in two places: the one-entry keyed reset path and the per-entry Activity retention list. The two stacks were identical except for where the React key sat, so the retained-entry bookkeeping and its render shape were tangled together and harder to audit than needed. Extract BfcacheEntryProviders as a pure presentational helper and render it from both paths. The keyed-reset path keeps its remount semantics: the key now sits on the BfcacheEntryProviders element, so a fresh active stateKey still remounts the whole provider subtree. Hook ordering is unchanged. All hooks stay above the early returns in BfcacheSlotBoundary, so the boundary still satisfies the Rules of Hooks across the activeStateKey undefined-to-defined transition. Covered by the slot BFCache entry-state suite in tests/shims.test.ts. * test(bfcache): isolate cacheComponents BFCache coverage in a dedicated fixture The back/forward-cache e2e suite ran against the shared app-basic fixture with cacheComponents: true. app-basic backs the single app-router Playwright server that every app-router spec uses, so enabling cacheComponents globally changed a core DOM invariant for unrelated tests: inactive route trees stay mounted as hidden Activity DOM. The contamination already leaked a :visible workaround into the interception-modal bfcacheId spec, which had nothing to do with Activity. cacheComponents is an opt-in semantic mode, not default app-router behaviour, so it does not belong on the shared baseline fixture. Move the BFCache routes into a dedicated tests/fixtures/app-bfcache fixture with cacheComponents: true, served by a new app-router-bfcache Playwright project on port 4183, and run the back-forward-cache spec there against the project baseURL. The :visible locators stay in that spec, where duplicate hidden DOM is the expected contract. Remove cacheComponents and the BFCache routes from app-basic so the shared fixture models default behaviour again, and revert the :visible workaround in use-router-bfcache-id.spec.ts now that the modal route no longer retains hidden DOM. Fix the nested item page import while moving it: nested/[section]/item/[id]/ page.tsx imported ../counter, but the counter lives two levels up at nested/[section]/counter.tsx, so it must be ../../counter. Validation: PLAYWRIGHT_PROJECT=app-router-bfcache (5 passed) and the full use-router-bfcache-id spec under app-router (11 passed), plus the slot BFCache unit suite (1172 passed) and vp check (formatting + lint + types clean). * ci: run the app-router-bfcache e2e project in the matrix The dedicated app-router-bfcache Playwright project was added to playwright.config.ts but not to the CI E2E matrix, so the back/forward-cache specs never ran in CI. Add it to the matrix project list so the BFCache suite runs on every push. It uses the default chromium browser, so the existing setup and browser-cache steps apply unchanged. * fix(bfcache): keep persisted slots mounted when cacheComponents is off Without cacheComponents there is no Activity retention, yet BfcacheSlotBoundary still keyed the active entry by its segment stateKey. That stateKey tracks the pathname (createBfcacheSegmentIdentity), so any slot whose identity moves with the URL (shared layouts, interception source slots) was remounted on navigation, discarding client state that survives a normal route change. The baseline router reconciles these slots in place. This regressed seven existing app-router e2e tests once cacheComponents was no longer masking it on the shared fixture: interception source-context and modal preservation in advanced.spec.ts, and layout-persistence.spec.ts dynamic-segment layout counter survival. Fresh-entry reset is driven by userland bfcacheId keying in the fixtures, not by remounting the slot subtree, so the keying was pure overreach in the non-cacheComponents path. Render the single active entry unkeyed when cacheComponents is off so the boundary reconciles in place like the baseline. The keyed Activity retention path is unchanged for cacheComponents. Validation: full app-router project 436 passed / 0 failed (the seven regressions now pass); use-router-bfcache-id 11 passed (fresh-push reset intact); app-bfcache 5 passed (Activity retention intact); shims unit suite 1024 passed. * docs(test): point bfcacheId spec comment at the app-router-bfcache project The header noted that Activity-backed preservation is covered in back-forward-cache.spec.ts, but that spec moved to the dedicated app-bfcache fixture under the app-router-bfcache project. Name the project and path so the cross-reference stays accurate. * docs: align cacheComponents comment with unkeyed active-segment behavior * test(bfcache): assert fresh bfcacheId after Activity-restored return * style: collapse locator chains per oxfmt * refactor(bfcache): split slot entry order from snapshots * fix(bfcache): keep non-cacheComponents slot path baseline * fix(bfcache): use string cacheComponents define * test: remove external fetch from revalidate tag fixture * fix(bfcache): harden Activity state key retention * docs(bfcache): document cacheComponents Activity caveats * fix(bfcache): preserve navigation context for fallback SSR Fallback boundary rendering can outlive the original request-scoped navigation context. That made app SSR fail fast with a missing navigation context when loading-boundary digest routes rendered their HTTP fallback shell. The SSR boundary contract still requires a navigation context for real request renders, but boundary renderers now synthesize the same typed shape from the request URL and matched params when request scope has already been cleared. * refactor(bfcache): normalize pathname in state-key map, document invariant callers Moves hydration-pathname normalization into createBfcacheSegmentStateKeyMap so SSR and client always produce byte-identical Activity keys. Documents the requireNavigationContext guaranteeing callers per review feedback. * fix(bfcache): tolerate malformed state-key pathnames * docs(bfcache): document known Next.js divergence and dead-branch contract * chore(deps): update lockfile for vite 0.1.24 * fix(app-router): preserve encoded delimiters in bfcache keys * docs(bfcache): track router-owned retention follow-up * chore: retrigger ci * fix(app-router): preserve redirect bridge fallback |
||
|
|
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 |
||
|
|
fe731b4827 |
test(e2e): mark flaky dev-overlay build-error HMR test as fixme (#1812)
The app-router dev error overlay test "server component HMR surfaces Vite build errors after a clean load" is flaky in CI: after writing the clean file back, the build-error overlay intermittently stays visible past the 10s timeout. Mark it test.fixme to unblock CI while the recovery path is investigated. Tracked in #1811. |
||
|
|
dab28c4b0f |
fix(app-router): refetch same-page search navigations (#1744)
* fix(app-router): refetch same-page search navigations Same-page App Router navigations with a changed search string could replay a prefetched RSC payload instead of requesting the target payload. That diverges from Next.js, which refetches page segments for same-page navigations because search params are page inputs. Bypass visited and prefetched RSC caches for navigate operations whose pathname is unchanged but search params differ. Add a prerendered production browser regression that ports the Next.js hash navigation request assertion. * fix(app-router): hide rsc cache query from middleware * test(app-router): mirror upstream hash scroll offsets * fix(app-router): address review feedback on rsc query navigation - Reuse fixture-scoped node_modules linking in the hash RSC spec instead of symlinking repo-root node_modules, matching sibling production specs. - Add an explicit assertion that no request leaked `_rsc` to the fixture middleware (HTTP 599) for a clear failure mode. - Document that the optimistic shell is intentionally not gated by shouldBypassNavigationCache (detached commit, always superseded). - Detect the RSC cache-busting param via the encoding-aware stripper so the guard and stripper can never diverge on encoded-key edge cases. * fix(app-router): share encoding-aware matcher for _rsc guard Replace the before/after url.search comparison in requestWithoutRscCacheBustingSearchParam with a dedicated hasRscCacheBustingSearchParam helper that reuses the stripper's isRscCacheBustingSearchPair matcher. This makes the guard and stripper share a single source of truth and avoids spuriously rebuilding/normalizing requests whose only difference is degenerate empty query pairs (e.g. ?a=1&&b=2). * fix(app-router): preserve cf metadata when hiding _rsc from middleware requestWithoutRscCacheBustingSearchParam reconstructed the userland request with a bare new Request(url, source), which drops the Workers-specific cf property and omits duplex: "half" for streaming bodies (throwing on Node/undici). Add a cloneRequestWithUrl helper mirroring cloneRequestWithHeaders that overrides the URL while preserving headers/cf and setting duplex, and route the _rsc strip through it. * fix(app-router): stop _rsc leaking to external rewrite proxies The beforeFiles/afterFiles/fallback applyRewrite calls forwarded the original request (with _rsc) to proxyExternalRequest, which reconstructs the upstream URL from request.url and merges its query params — leaking the internal RSC transport query to external rewrite destinations. Pass the _rsc-stripped userlandRequest to all three calls, consistent with the middleware invariant. Add a regression test that points an external rewrite at a loopback server and asserts the forwarded URL drops _rsc while preserving userland query params. * test(app-router): wait for idle network before tracking RSC requests The query-param link is in the viewport and gets prefetched on load. On slower runtimes (WebKit), that prefetch could land after rscRequestUrls.clear(), so it was misattributed to the hash-only navigations and tripped the "no with-query-param request" assertion. Wait for networkidle before clearing, mirroring upstream's waitForIdleNetwork() in the ported Next.js test. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
c8975a48cf |
chore(deps): bump playwright to 1.60.0 (#1796)
* chore(deps): bump playwright to 1.60.0 * u |
||
|
|
c28098694a |
test(pages-router): wait for hydration after middleware redirect (#1789)
The middleware redirect E2E test placed a reload marker immediately after the redirected page heading appeared. That proved the document rendered, but not that the Pages Router Link handler had hydrated before the click. Wait for the shared Pages Router hydration marker before clicking the redirected page's home link so the marker assertion tests client-side navigation instead of racing anchor fallback behavior. |
||
|
|
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. |
||
|
|
cf04ca63e3 |
fix(form): DISALLOWED_FORM_PROPS stripping, file input warning, viewport prefetch, pages-router E2E (#1752)
* fix(form): implement DISALLOWED_FORM_PROPS, file input warning, viewport prefetch, and pages-router E2E - Strip method/encType/target props with dev console.error (Gap 1) - Warn in dev when FormData contains a File value (Gap 2) - Add viewport IntersectionObserver prefetch for App Router (Gap 3) - Add prefetch prop (false | null) to FormProps - Add pages-dir fixture and E2E spec for Pages Router form tests (Gap 4) - Fix Rules of Hooks: move useRef/useCallback/useEffect before conditionals - Update unit tests: stub React 19 dispatcher for hook-safe direct rendering Closes #1355 * test(form): fix 'renders a form with method prop' to assert stripping behavior * fix(form): address code-review findings — JSDoc, parity warnings, prefetch dedup - Fix misleading JSDoc: pages-dir prefetch prop "has no effect" (not "emits a warning") - Add dev warning for invalid prefetch value (not null/false) - Add dev warning for replace/scroll/prefetch passed with a function action - Gate viewport-prefetch IntersectionObserver with getPrefetchedUrls() / hasPrefetchCacheEntryForNavigation() dedup check to match link.tsx - Add 5 new test cases covering the three new warning paths * style(form): fix formatting in form.test.ts * fix(form): match Next.js warning text verbatim, reuse useMergedRef, skip prefetch for unsafe actions Addresses bonk review feedback: - Invalid prefetch warning now matches Next.js verbatim: "The `prefetch` prop of <Form> must be `false` or `null`" - Function-action warnings split into the two distinct Next.js messages (one for `prefetch`, one for `replace`/`scroll` with doc links), gated separately to match app-dir/form.tsx - Reuse the shared useMergedRef helper instead of hand-rolling ref merge - Viewport prefetch now early-returns on unsafe actions, matching the submit-path isSafeAction gate - Update form.test.ts assertions to the new verbatim text * fix(form): run dev-mode prop validations before DISALLOWED_FORM_PROPS strip to match Next.js order * fix(form): match Next.js validation ordering and bail for server/client action submitters Addresses bonk review feedback: - Run prefetch/replace/scroll dev validations before the DISALLOWED_FORM_PROPS strip loop, matching upstream app-dir/form.tsx ordering - Bail out of GET interception for React server-action submitters (`name` starting with $ACTION_ID_/$ACTION_REF_, dev-only silent bail) and client-action submitters (`formAction="javascript:..."`), ported from Next.js form.tsx / form-shared.tsx - Add unit tests covering both submitter bail paths * fix(form): match Next.js dev-validation order and gate submitter warnings Addresses bonk re-review parity findings: - Gate the three console.error calls in hasUnsupportedSubmitterAttributes behind the dev check, matching Next.js form-shared.tsx (the bail itself still runs in all environments). Previously they fired in production. - Hoist checkFormActionUrl(action, "action") into the top dev-validation block so it runs first, matching upstream app-dir/form.tsx ordering. * refactor(form): drop dead-code GET method check for upstream parity Addresses bonk re-review finding: the form-level `method` prop is stripped via DISALLOWED_FORM_PROPS and any non-GET submitter `formmethod` override is already caught by hasUnsupportedSubmitterAttributes, so the explicit getEffectiveMethod GET check was dead code. Upstream app-dir/form.tsx has no equivalent check. Remove it (and the now-unused getEffectiveMethod helper) and document why the method is always GET at that point. * fix(form): align viewport prefetch fetch with link.tsx (priority + purpose) Addresses bonk re-review finding: the Form viewport prefetch fetch() omitted the `priority: "low"` and `purpose: "prefetch"` options that link.tsx passes (link.tsx:445-451). Without them, RSC prefetches competed with higher-priority requests and weren't tagged as prefetches in devtools / Sec-Purpose. Align for consistency with the Link prefetch path. * fix(form): thread interception context + mounted slots into viewport prefetch Addresses bonk re-review substantive finding: the viewport prefetch built headers with createRscRequestHeaders() (no interceptionContext, no mounted-slots header) and keyed the prefetched set on the bare rscUrl. The actual navigation (and link.tsx / the router) compute getPrefetchInterceptionContext(href) + getMountedSlotsHeader() and key on encodeCacheKey(rscUrl, interceptionContext). For intercepted target routes or pages with mounted parallel slots, the Form's prefetched cache key wouldn't match the navigation lookup, so the prefetch was a silent cache miss (wasted work, no dedup against a nearby <Link>). Mirror the Link/router computation: build context-aware headers, key the prefetched set on encodeCacheKey, and thread interceptionContext/mountedSlotsHeader through hasPrefetchCacheEntryForNavigation and prefetchRscResponse. * docs(form): note prefetch effect dep-array redundancy is lint-mandated Bonk flagged actionHref as redundant alongside action in the prefetch effect's dep array. It is derived purely from action + the module constant, but the exhaustive-deps lint requires it to be listed since the effect references it. Document this so it isn't mistaken for an oversight. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
06f37e9c1b |
fix: client HMR dev overlay recovery (#1758)
* Fix client HMR dev overlay recovery * Stabilize dev overlay build error recovery test * Harden dev overlay recovery handling |
||
|
|
fb233b0a34 |
feat: improve dev error overlay source frames (#1746)
* Improve dev overlay stack source mapping * Polish dev error overlay stack display * Add dev overlay code frames * Align dev overlay message spacing * Add dev overlay error copy action * Fix dev overlay knip exports * Polish dev overlay layout * Polish dev error overlay spacing * Fix dev overlay coverage and source frame metadata * Fix dev overlay recovery for RSC HMR errors * Handle Vite HMR build errors in dev overlay * Fix knip * Use Shadow DOM for dev error overlay |
||
|
|
e3090b08fa |
fix: dev overlay browser sourcemap stacks (#1731)
* Add browser runtime sourcemap regression test * Update pnpm lockfile * Fix dev overlay browser sourcemap stacks * Move dev stack sourcemap resolution to server * Fix knip unused stack endpoint export * Fix dev stack sourcemap proxy host handling * test: cover dev stack sourcemap helpers * fix: remove unused sourcemap type export * refactor: share dev stack sourcemap endpoint * chore: trigger ci rerun * fix: cap dev stack sourcemap request body |
||
|
|
51690038d8 |
fix(pages-router): cancel in-flight nav on gSSP/gSP data redirect (#1465) (#1691)
* fix(pages-router): return __N_REDIRECT JSON for gSSP/gSP data redirects (#1465) When getServerSideProps/getStaticProps returns `{ redirect }` on a client-side `_next/data` navigation, vinext emitted a real HTTP redirect. The client's fetch transparently followed it to the destination's HTML (not a valid data envelope), forcing a hard reload with console errors and leaving navigation on the wrong page. Mirror Next.js: for data requests, reply 200 with `__N_REDIRECT` / `__N_REDIRECT_STATUS` in pageProps. The client router now detects these markers and re-dispatches a fresh client navigation to the destination, which increments the navigation id and supersedes (cancels) the in-flight navigation so the intermediate page never commits. HTML requests still emit a real HTTP redirect. Fixed in both server paths to keep dev/prod parity (see AGENTS.md): - prod: buildPagesRedirectResponse() in pages-page-data.ts branches on isDataReq - dev: writeGsspRedirect() in dev-server.ts branches on isDataReq - client: navigateClientData() handles pageProps.__N_REDIRECT via handleDataRedirect(), preserving push/replace mode - tests: data-endpoint redirect shape (vitest) + client cancellation (e2e) * fix(pages-router): hard-navigate to external __N_REDIRECT destination (#1465) Address review: handleDataRedirect() passed the originating page URL to the hard-navigation fallback for external/non-absolute __N_REDIRECT destinations, which would loop back to the redirecting page (e.g. an OAuth callback at https://example.com/...). Hard-navigate to the destination instead, matching Next.js's handleHardNavigation({ url: destination }). Drop the now-unused fallback parameter and add a server test asserting an external destination is preserved verbatim in the data envelope. |
||
|
|
65eaea49a4 |
feat(skip): omit proven static layouts from RSC transport (#1437)
* feat(skip): omit proven static layouts from RSC transport
App Router RSC responses re-send every layout payload on each navigation, even
when the client already holds an identical static layout from a sibling route.
That repeats transport bytes on the common sibling-page navigation path.
When the client sends a verified reuse manifest, the render lifecycle now runs a
skip-transport planner. Each manifest entry is cross-checked against the
static-layout cache proof (exact artifact compatibility, variant cache key,
payload hash, and invalidation). Layouts that pass are omitted from the outgoing
RSC payload, recorded under __skippedLayoutIds, and their per-element render
dependencies are released. The skip disposition also bypasses the RSC cache so a
partial payload never enters a shared cache, and it is suppressed for HTML
responses.
This is the enable slice. Layouts are gated on the cache proof, not yet on
per-layout runtime observations. The observation-based safety gates (param
scope, request APIs, finite revalidate, cacheLife, unstable_cache, cache-tagged
and dynamic fetches) land in the stacked hardening follow-up.
* fix(skip): preserve parallel slots owned by skipped static layouts
When the skip-transport merge restores a server-omitted static layout into
preserveElementIds, it must also restore the default/unmatched parallel slots
that layout owns. Otherwise the planner invariant ("every preserved slot's owner
layout is present in preserveElementIds, and vice versa") is violated: in the
topology-unknown path the planner returns empty slot persistence, so a
slot-owning layout skipped server-side commits with a missing slot
(mergeElements starts from the next payload and, with preserveAbsentSlots:
false, never restores it).
mergeSkippedLayoutPreservation now derives the owned slots for newly-preserved
skipped layouts via the planner's resolveDefaultOrUnmatchedSlotPersistenceForLayouts,
covering both the topology-unknown path and the known-topology past-divergence
case. Added a regression with a mounted @slot default that fails without the fix.
Also hardcode the static-layout cache-proof's observed request-API set to []
(with a comment naming the invariant) instead of reading it back from the
per-layout observation, so a future reordering of the classification gate cannot
feed stale request-API reads into the synthetic proof. Reaching that point
already requires staticLayoutIds membership, which excludes any layout that
observed a request API.
* docs(skip): explain broader-than-planner layout membership guard
Per review: clarify that the set-membership check in
mergeSkippedLayoutPreservation is intentionally broader than the planner's
prefix-based persistence, and why preserving a proven-identical layout past the
ancestor divergence point is sound. Comment-only.
* refactor(skip): drop redundant cacheable-fetch observation record
Per review: the post-key-generation recordCacheableFetchObservation in
fetch-cache.ts was strictly redundant. The synchronous record before the first
await already covers every path that reaches it (the only intervening early
return records a dynamic observation instead), and the store is an idempotent
set, so the second call added no coverage.
* fix(test): match real RSC request URL in client-reuse-manifest e2e
The 'sends a client reuse manifest for retained static layouts on soft
navigation' e2e hand-rolled its RSC-request matcher as
url.pathname === '/client-nav-test.rsc', but App Router soft-navigation RSC
requests use the bare pathname with a _rsc query param and an rsc: 1 header
(e.g. GET /client-nav-test?_rsc), so the predicate never matched and
page.waitForResponse timed out at 30s. The manifest header was being attached
correctly the whole time. Use the shared isAppRouterRscRequestForPath helper
(as the sibling test already does) so the matcher tracks the real request shape.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
82b2720701 |
feat(app-router): support useRouter bfcacheId semantics (#1588)
* feat(app-router): implement useRouter bfcacheId semantics * fix(app-router): refine bfcacheId navigation behavior * test(app-router): align bfcache history state expectations * fix(app-router): handle catch-all bfcache identities * fix(app-router): align bfcache context semantics * fix(app-router): avoid hidden bfcache DOM retention * fix(app-router): hydrate bfcache ids from history state * fix(app-router): handle bfcacheId redirects and slots * test(app-router): expect active slot route ids * Fix bfcache id history restoration * Clarify bfcache id sentinel formatting * Address bfcacheId review feedback * Tie bfcacheId sentinels to shared constants * Address bfcacheId review clarity * Tighten bfcache slot identity check * Deduplicate bfcache history sync check * Handle empty bfcache segment ids * Fix intercepted active slot route ids * Clarify bfcacheId form state coverage * Address bfcacheId review clarity * fix(app-router): skip invisible tree-path segments in bfcache identity getVisibleTreePathSegments only stripped route groups, not parallel @slots or "." default segments. Tree paths contain raw filesystem segments, so a slot preceding visible URL segments over-counted the consumed pathname segments and produced a wrong identity prefix. That re-minted the bfcacheId for layouts/slots that actually persisted across parallel-route navigations (e.g. @modal), diverging from Next.js bfcacheId semantics. Delegate to the canonical isInvisibleSegment helper so the filter stays in sync with the route graph, and add a parallel-slot layout regression test (fails without the fix). Also tighten the hydration-map unit test to actually exercise the "no seeding from minted ids" property instead of asserting a local literal, and correct the SSR bfcache context comment. * fix(app-router): move isInvisibleSegment to browser-safe utils The previous commit imported isInvisibleSegment from app-route-graph.ts into app-browser-state.ts, but app-route-graph.ts imports node:path, node:fs and node:crypto. Pulling it into the browser bundle broke client hydration (app-with-src E2E: instrumentation-client and dev-overlay-recovery timed out waiting for hydration / the navigation runtime). Move the pure isInvisibleSegment helper into the browser-safe routing/utils.ts module (already in the client bundle), re-export it from app-route-graph.ts so Node-side import sites keep working, and have app-browser-state.ts import it from utils.js. The app-route-graph type import is now type-only. * refactor(app-router): address bfcacheId review feedback - Reject history entries with a missing bfcache version in the document-scoped version gate (stop coercing null -> 0) so stale ids from older builds / external pushState can't be restored into a fresh document on the version-0 path. - Parse AppElements metadata once per element map per commit and resolve slot bindings via a precomputed Map (was repeated readMetadata parses plus an O(slots^2) find on every navigation commit). - Read single history-state keys without cloning the whole state object; the write path still clones before mutating. - Dedupe the two identical non-negative-safe-integer validators and share path-segment / catch-all helpers via browser-safe routing/utils. - Move tree-path consumed-segment counting into routing/utils and fix optional-catch-all over-counting. - Drop the unused bfcacheIds param from createHistoryStateWithPreviousNextUrl. * refactor(routing): make catch-all segment predicates module-private isCatchAllSegment/isOptionalCatchAllSegment are only used by countConsumedPathnameSegments in the same module; drop the exports to clear knip's unused-export warning. * test(app-router): lock in bfcache version-gate rejection of missing versions Extract the version-gate decision into a pure, exported isHistoryStateBfcacheVersionCurrent(state, currentVersion) in app-history-state (re-exported via app-browser-state); the browser entry's isCurrentBfcacheVersion now delegates to it. Add a direct regression test asserting a history entry with bfcache ids but no version key is NOT treated as current even when the current version is 0 — the exact cross-document stale-id hole that the earlier `?? 0` coercion left open. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
560f270ee6 |
fix: align app router navigation parity (#1705)
* fix: align app router navigation parity * fix(app-router): preserve canonical RSC and hard-nav semantics App Router RSC requests now keep the route pathname shape when adding _rsc. Stripping a trailing slash made trailingSlash routes fetch a non-canonical URL and let the server redirect the client-side RSC request back to the slash form. The MPA navigation scheduler also keys pending hard navigations by both href and history mode so a later replace does not reuse an earlier push closure. Redirect handling keeps the global browser bridge out of passive server rendering while retaining client-side redirect recovery. Adds focused regression coverage for trailingSlash RSC requests, same-href push-to-replace hard navigation, and client redirect guard loops. Updates existing navigation observers to the canonical route URL plus _rsc transport. * fix(app-router): stabilize action and prefetch parity * fix(app-router): reset MPA dedupe after bfcache restore A bfcache-restored App Router document can keep the scheduler's last pending hard-navigation marker after the external navigation already left the page. Reissuing the same href and history mode then entered the suspended MPA path without scheduling another location change. Expose a scheduler reset for the browser lifecycle boundary and call it on persisted pageshow events. Cover the scheduler contract so the same external navigation can run again after reset. * fix(app-router): isolate redirect bridge from passive boundaries Unhandled client redirect recovery currently lives inside every RedirectBoundary, while Pages Router params fallback can keep stale App Router params after an App-to-Pages transition. That makes passive boundaries own browser runtime wiring and lets stale app params win over live Pages Router params. Move the unhandled redirect bridge into BrowserRoot under AppRouterContext, keep RedirectBoundary passive, and prefer live Pages Router params when that compat context exists. Add an App-to-Pages navigation regression fixture. * fix: extract shared decodeRedirectError helper, rename stale test - Add decodeRedirectError() to shims/navigation.ts as the single source of truth for parsing redirect error digests - Replace three private helpers in error-boundary.tsx with shared decodeRedirectError() call - Replace inline digest parsing in AppRouterRedirectBridge with shared decodeRedirectError() call (fixes unsafe decodeURIComponent) - Rename test that claimed to test redirect() but actually tests <Link> navigation * refactor: eliminate unnecessary type assertions - isRedirectError: use in-narrowing instead of as-casts - AppRouterRedirectBridge: remove as after guard already narrows - RedirectErrorBoundary: inline digest access, use in for handled check * ci: retrigger checks * fix: emit noindex for Forbidden/Unauthorized boundaries, extract retryScrollTo helper * docs: clarify decodeRedirectError comment about defensive join |
||
|
|
8ae5abd56c | fix(app-router): preserve forwarded action redirect wrappers (#1582) | ||
|
|
66519412c3 | fix(app-router): reuse pending prefetched RSC payloads (#1704) | ||
|
|
b1f2e57bc4 |
fix(pages): preserve import.meta.url source paths (#1598)
* fix(pages): preserve import.meta.url source paths
Pages Router production bundles previously left direct import.meta.url expressions for the bundler to evaluate from generated output. Server renders could expose dist/server/entry.js, and prerendered Pages output could miss the client entry needed for hydration to replace the server value with the Turbopack-style browser source URL.
The fix rewrites direct import.meta.url reads in user modules to source-file URLs at transform time, keeps new URL asset bases intact, and loads the Pages client entry from the build manifest for Node production and prerender servers.
Regression coverage ports the Next.js import-meta deploy test, including the prerender and hydrated browser path.
* fix(tests): run import.meta browser coverage in e2e
The import-meta regression test launched Playwright from a Vitest integration shard. That shard does not install Playwright browser binaries in CI, so it failed before exercising the runtime behavior.
Move the browser assertion into the existing pages-router-prod Playwright project, keep the lower-boundary server and client chunk assertions in Vitest, and cache the import-meta plugin root paths after an early source-text bailout to address review feedback.
* refactor(import-meta-url): canonicalize each module once per transform
The transform path canonicalized the same module id twice: once in the
eligibility check and again when computing the replacement value, so every
rewritten module paid for two fs.realpathSync.native() syscalls.
The eligibility check now returns the canonical id and threads it through to
the value computation, leaving a single realpath per module.
Also widens the extension match to .cts/.mts/.cjs and documents that the
new URL() skip only covers the bare URL identifier, matching Vite's own
asset-detection scope.
* test(pages): drop unrelated prerender from top-level-await setup
The top-level-await production test ran runPrerender in its setup, but the
assertions pass identically whether the routes are prerendered or rendered on
demand. The step added unrelated build work to a test that is not about
prerendering or client-entry injection.
* fix(pages): share client entry fallback
* fix(import-meta-url): address PR review feedback
- Handle ChainExpression (import.meta?.url) in new URL() asset-detection
skip so the optional-chain variant preserves Vite asset resolution
- Add explicit ChainExpression check in visit() to avoid fragile
reliance on coincident inner-node start/end ranges
- Add test for new URL('./file', import.meta?.url) preservation
- Fix decodeHtmlText entity ordering (& before ") in both
unit and E2E test helpers
- Add doc comments: excluded prefixes rationale, node_modules
early-exit optimization intent
* fix(pages): select client entry by marker in build manifest
findClientEntryFileFromManifest returned the first chunk flagged
isEntry, but a client build can emit several entry chunks (client
entry plus instrumentation/middleware) and manifest order is not
guaranteed. The on-disk fallback already disambiguates by the
vinext-client-entry / vinext-app-browser-entry marker; apply the
same check to the manifest path so both lookups agree, falling back
to the first entry only when nothing is marked.
* refactor(pages): address review feedback on client-entry/import-meta-url
- prod-server: hoist findClientEntryFile out of the manifest if/else
since it already handles a missing manifest via the on-disk fallback
- import-meta-url: document the two known limitations (destructured
access, aliased new URL() base) that match Vite's own behavior
- import-meta-url: drop the dead visit(value.callee) for new URL() —
the callee is always the bare URL identifier
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
7be428dbc3 |
feat(app-router): support inline CSS parity (#1595)
* feat(app-router): support inline CSS parity
App Router production builds ignored Next.js experimental.inlineCss, so HTML responses kept stylesheet links, next/font CSS lived in a second style tag, and Next-style RSC requests with RSC: 1 plus _rsc could return HTML.
The missing invariant was that inlineCss changes the production CSS delivery boundary: non-RSC HTML gets inlined style resources, RSC payloads keep CSS bodies out, and client navigations must not reintroduce duplicate stylesheet resources for CSS already present inline.
Resolve the config flag into the generated RSC entry, collect emitted CSS assets into an inline manifest for Node and Workers production, rewrite matching React stylesheet links into hoistable style tags, merge next/font CSS into the first inline style with a fallback path, and recognize Next-style RSC requests.
Regression coverage now ports the upstream app-inline-css behavior at production-browser level and adds focused tests for request normalization, stream rewriting, font merging, and fallback emission.
* feat(app-router): preserve inline CSS import ordering
Inline CSS font merging could prepend generated font rules before a stylesheet import. CSS imports must stay at the beginning of their stylesheet, so that ordering caused imported styles to be ignored when experimental.inlineCss and local font CSS were combined.
Keep import-sensitive stylesheet preambles untouched and let the existing font fallback style carry the font rules instead. Add stream-helper coverage for the import case.
* feat(app-router): harden inline CSS review edges
Inline CSS rewriting only saw complete link tags in a single decoded chunk and treated rel as an exact value. That left valid stylesheet links un-inlined when rel had multiple tokens or when streaming split the tag across chunks.
The production manifest injection also assumed the RSC worker bundle was always at dist/server/index.js, so custom rscOutDir builds missed the inline CSS manifest.
Buffer incomplete link tags across stream flushes, match stylesheet as a rel token, and inject the manifest into the configured RSC output entry. Add regressions for the streaming and custom-output cases.
* feat(app-router): match inline CSS cleanup semantics
Client navigation cleanup only pruned links whose rel was exactly stylesheet and whose precedence marker used data-precedence. That diverged from the server inline CSS rewrite, so tokenized rel values or legacy precedence links could survive after navigation as duplicate stylesheet resources.
The fix moves the cleanup into a testable helper, reuses shared HTML token-list matching for server and browser paths, accepts both precedence marker forms, and only buffers split link tags when an inline CSS manifest is present.
Regression coverage now covers browser cleanup and non-inlineCSS streaming chunk granularity.
* feat(app-router): harden inline CSS HTML rewriting
Inline CSS rewriting could prepend SSR font CSS ahead of namespace-bearing stylesheets and could treat stylesheet-like text inside script bodies as real link tags. That violates CSS ordering constraints and corrupts streamed script text when experimental.inlineCss is enabled.
The stream rewrite now treats @namespace as an unsafe preamble, skips link replacement inside raw-text and comment regions, and buffers incomplete rewrite boundaries across flushes. Regression coverage pins namespace fallback, complete script text, split script text, and unterminated script text.
* test(e2e): specify junction type for fs.symlink to support Windows
* refactor(app-router): drop write-only __PAGE__ wire marker
Every outgoing App Router payload carried `__PAGE__: true` in its wire
metadata, but nothing in the runtime ever read it. Its only consumer was
an inline-css E2E assertion checking that a dynamic navigation returned an
RSC payload rather than HTML, so the marker added permanent per-response
wire overhead with no behavioural effect.
The `__route` key is already present on every payload and proves the same
thing: the response carries page metadata. Remove the marker key, its type
field, and its re-export, and point the unit and E2E assertions at
`__route` instead.
* perf(app-router): skip inline-css manifest scan on the streaming hot path
The tick-buffered SSR transform recomputed `Object.keys(manifest).length`
on every flush through `rewriteInlineCssStylesheetLinks`, even when inline
CSS was disabled and the manifest empty. Streaming flushes are a hot path,
so this ran per chunk of every response for no benefit.
Compute the manifest-present flag once at transform creation and reuse it
to gate both the split-link boundary buffering and the link rewrite, so
non-inline-css responses skip the rewrite entirely. Also document the
shared-lastIndex constraint on the module-level rewrite regexes and the
startup/build-time population contract for `__VINEXT_INLINE_CSS__`.
* fix(app-router): forward SSR script nonce to inline-css <style> tags
Sites running `Content-Security-Policy: style-src 'nonce-…'` saw the
inlined `<style>` blocks blocked at parse time and rendered unstyled
when `experimental.inlineCss` was enabled. The `<link rel="stylesheet">`
tags the feature replaces aren't subject to inline-style CSP, but the
new inline blocks are — and React Fizz typically doesn't put a nonce on
the `<link>`, so the rewrite path had no nonce to carry over.
Thread the SSR-time `scriptNonce` through `createTickBufferedTransform`
into `rewriteInlineCssStylesheetLinks`, and prefer the link's own nonce
when present (otherwise fall back to the SSR nonce). Add unit coverage
for the SSR-only, link-only, and missing-nonce cases.
Also replace `html.toLowerCase().lastIndexOf("<link")` in
`splitTrailingIncompleteLinkTag` with a forward `g+i` regex scan so the
streaming hot path no longer allocates a lowercased copy of every flush
(can be tens of KB on large responses).
* chore(check): register experimental.inlineCss as supported
So `vp check` advertises App Router CSS inlining alongside the other
experimental flags. Notes that next/font CSS is folded into the first
inline `<style>` block.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
5e90f16a56 |
fix(app-router): preserve push autoscroll across refresh (#1602)
Calling router.push() and router.refresh() in the same transition could leave vinext on the current page instead of committing the pushed route. That diverged from Next.js and broke the default autoscroll target for the pushed page. The refresh path dispatched a current-route RSC refresh before the nested push transition could finish scheduling, so the refresh superseded the intended navigation. Track App Router push/replace scheduling for the current task, keep refresh cache invalidation, and skip only the overtaking current-route refresh. Add Next parity coverage for refresh, server-action refresh, focus targets, fragments, hoisted children, and metadata routes. |
||
|
|
07be579048 | test(app-router): cover clean replace after search load (#1626) | ||
|
|
f75845ef14 |
fix(pages-router): emit page scripts with defer in <head> by default (#1636)
* fix(pages-router): emit page scripts with defer in <head> by default Next.js' Pages Router emits page scripts with the `defer` attribute in `<head>` when `experimental.disableOptimizedLoading` is false (the default). vinext emits `<script type="module">` (which already defers implicitly), but upstream tests assert the literal `defer` attribute and were failing against the deploy suite. Add `defer` to module scripts emitted by `collectAssetTags` when `experimental.disableOptimizedLoading` is not enabled. Surface the flag in `ResolvedNextConfig` so prod-server respects an explicit opt-out. Browser behaviour is unchanged (module scripts already defer), but parity tests now pass. Fixes #1519. * test(e2e): update hydration regex to allow defer attribute The script tag regex in `tests/e2e/cloudflare-pages-router/hydration.spec.ts` required `type="module"` to be immediately followed by `src="…"`, but the optimized-loading fix inserts a `defer` attribute between them. Relax the regex to allow any whitespace-separated attributes between `type="module"` and `src` so future attribute additions don't break it either. |
||
|
|
53a97e2002 |
fix(navigation): stabilize Pages Router useParams snapshot for SSR (#1639)
`useParams()` / `useSearchParams()` / `usePathname()` from `next/navigation`
work as cross-router compat shims under Pages Router, deriving their state
from `getPagesNavigationContext()` published by `next/router`. React's
`useSyncExternalStore` requires `getServerSnapshot` to return a stable
reference (Object.is) across calls in a single render — otherwise React
logs:
"The result of getServerSnapshot should be cached to avoid an infinite loop"
and may re-render until the snapshots stabilize, which is wasteful and a
hydration-mismatch hazard. The SSR branch of `getPagesNavigationContext`
was returning a fresh object each call.
Cache the shape on a `WeakMap` keyed on the ALS-isolated SSR context, so
the same `{ pathname, searchParams, params }` shape (with reference-stable
`params` / `searchParams`) is returned for every hook call inside a single
request. The WeakMap key is per-request, so concurrency safety is
preserved.
Adds a Vitest unit assertion (reference equality across repeated SSR
calls) and a Playwright regression matching the Next.js deploy-suite case
(`test/e2e/app-dir/use-params/use-params.test.ts` — 'should work on pages
router'): a Pages Router dynamic page using `useParams()` inside a fixture
that also has an `app/` directory must render the matched param after
hydration.
Refs #1466
|
||
|
|
d458b71e6b |
test(link): port Next.js OnNavigate fixture as Pages Router regression (#1357) (#1628)
* test(link): port Next.js OnNavigate fixture as a Pages Router regression Add a local Playwright fixture mirroring `.nextjs-ref/test/e2e/link-on-navigate-prop/shared/OnNavigate.tsx` so the upstream `onNavigate` parity scenarios are exercised against vinext's own Pages Router runtime. The `onNavigate` prop itself was implemented in #1371 and is covered by unit tests in `tests/link-navigation.test.ts`. The nightly Next.js deploy suite (workflow run 26552059147) still surfaces failures under `test/e2e/link-on-navigate-prop/index.test.ts` for the Pages Router half of the suite, where even a plain `<button onClick={() => setIsLocked(...)}>` does not update state. The App Router half of the same suite passes, suggesting a Pages Router-specific hydration regression rather than a missing `onNavigate` implementation. This commit ports the upstream fixture so the same scenarios run against vinext in CI's existing Playwright suite, narrowing the reproduction without depending on the full Next.js deploy harness. Refs #1357. * test(link): address bot review on OnNavigate Pages Router port - Assert `isClicked` / `isNavigated` flip to `true` in the internal-click test by stubbing `history.pushState` so vinext's Link runs its synthetic handlers in full but the URL never commits and the source DOM stays available for state assertions. Split the navigation check into its own test that asserts `#subpage-heading`. - Port the three remaining upstream scenarios from `.nextjs-ref/test/e2e/link-on-navigate-prop/index.test.ts`: modifier-key click, plain external link (alert-driven), and external link with `replace` (history-length check). Add the corresponding `#external-link` and `#external-link-with-replace` anchors to the fixture to match `shared/OnNavigate.tsx`. - Explain in the download-test why the native `click` listener doesn't interfere with React's synthetic onClick / onNavigate. - Switch `setIsLocked((v) => !v)` to `setIsLocked(!isLocked)` to match upstream's exact callback shape (parity port). Refs #1357. |
||
|
|
bdbd1f833d |
fix(app-router): preserve source identity for intercepted renders (#1589)
* fix(app-router): preserve source identity for intercepted renders Intercepted App Router renders used the visible target pathname as the payload route identity. That let a modal navigation such as /feed to /photos/42 replace the retained source tree with target-scoped route and page ids, so refreshes, server actions, chained modals, and history traversal could lose source page state. The violated invariant is that an intercepted render has separate display and matched-route identities. This commits a fenced render identity helper, keeps source route and page ids stable for retained source-route payloads, leaves the target pathname in interception proof, and includes source context in RSC cache variants. Regression coverage asserts source payload keys, cache variants, modal refresh/action/history flows, and source client/search state preservation. * fix(app-router): normalize intercepted rsc cache source * fix(app-router): harden intercepted cache regeneration * refactor(app-router): share intercepted path normalization * fixup! fix(app-router): preserve source identity for intercepted renders fixup! fix(app-router): preserve source identity for intercepted renders |
||
|
|
8e3438d267 |
fix(app-router): complete default autoscroll page target parity (#1601)
* fix(app-router): complete default autoscroll page target parity App Router page navigations can commit loading shells, final payloads, and superseded payloads out of order. A pending scroll intent that is only keyed by navigation start can be consumed by the wrong committed tree, which makes default page target scrolling and focus timing diverge from Next.js when loading boundaries or rapid navigations are involved. Bind scroll intents to approved render commits, consume only the matching intent from the committed scroll target, and clear hard-navigation failures only for their own intent. Extend the Next.js compat fixture coverage for skipped first children, loading boundary timing, same-page search param changes, focus behavior, horizontal scroll preservation, and stale intent races. * test(app-router): update link navigation scroll intent assertion * fix(app-router): address review — consume scroll intent on no-commit, tighten test - Consume scroll intent before returning no-commit in renderNavigationPayload, preventing the navigateClientSide fallback from scrolling for an abandoned navigation. - Replace waitForTimeout(700) with expect.poll for robust race-condition stability assertion (CI-flake-safe, stronger proof of stability). - Update wrapperless targeting comment to clarify outer function / inner class split. - Add three unit tests covering no-commit cleanup, stale intent rejection, and claim-without-commitId consumption paths. |
||
|
|
314fbea508 |
fix(app-router): align RSC navigation scroll targeting with Next.js (#1599)
* fix(app-router): align RSC navigation scroll targeting with Next.js App Router RSC navigations scrolled the document after appNavigate resolved, which lost Next.js segment targeting and focus semantics. That left persistent layout offsets and horizontal scroll behaviour coarser than Next.js during normal navigation. Next.js treats scroll as a navigation intent consumed by the newly navigated segment after commit. This adds a small intent state, wraps the page slot in a client scroll target, and keeps the existing hash-only and popstate paths separate. Adds focused Playwright coverage ported from Next.js autoscroll and navigation focus tests. * test(form): stop asserting app router scroll detail * fix(app-router): prevent focus from changing scroll * fix(app-router): share scroll intent across Vite module instances The App Router scroll intent was stored in plain module-level variables in app-router-scroll-state.ts. navigateClientSide stages the intent and the committed AppRouterScrollTarget consumes it, but next/navigation and the scroll-target module can resolve to separate Vite module instances. If the writer and consumer hold different copies, the staged intent is invisible to the consumer and the post-commit scroll/focus silently no-ops. This is the same multi-instance hazard the rest of the navigation state already guards against with Symbol.for globals (see _CLIENT_NAV_STATE_KEY in navigation.ts and AGENTS.md "RSC and SSR Are Separate Vite Environments"). Move the single pending intent and the id counter onto a Symbol.for global so every module instance shares one slot, matching the established navigation state pattern. Function signatures are unchanged, so callers need no edits. Also document why AppRouterScrollTarget must remain a class component: findDOMNode needs a mounted class instance to locate the first rendered DOM node without a wrapper element, mirroring Next's default scroll handler. |
||
|
|
5fe9789bbd |
fix(app-router): preserve history index on external state writes (#1600)
* fix(app-router): preserve history index on external state writes External history.pushState and history.replaceState calls preserved vinext interception metadata but dropped the traversal index. That left userland or library shallow history entries without the metadata App Router traversal uses to classify and anchor back/forward state. createExternalHistoryStatePreservingMetadata now preserves vinext navigation metadata as one unit from the current history entry. The shim regression covers object, null, undefined, and index-only external state writes. * test(app-router): seed stale history context through unpatched state Public History API calls now sanitize vinext metadata before delegating. The refresh regression test was seeding stale intercepted context through that public wrapper, so it tested the wrapper boundary instead of the stale internal-state condition. Use the saved original replaceState handle to create the same framework-owned stale state the test is meant to refresh away. |
||
|
|
bca641535b |
fix: resolve relative server action redirects (#1579)
* fix: resolve relative server action redirects Server action redirects could carry dot-relative locations from the action response. Treating those locations as raw browser redirects resolved them from the current document URL and forced a reload, so nested App Router actions could land on the wrong route or lose client state. Resolve server action redirect locations against the action initiation pathname using Next.js directory-style semantics, keep same-origin redirects on the App Router navigation path, and hard-navigate only for external destinations or parse fallback. Adds browser-level coverage ported from the Next.js relative server action redirect test plus focused helper assertions for the URL resolution contract. * chore: rerun ci * chore: rerun ci again |
||
|
|
e3b1cde53d |
fix(pages-router): document push/replace Promise<boolean> contract (#1566)
Pages Router `window.next.router.push`/`replace` already resolved to `true` on a successful navigation (and `false` on a hard-navigation fallback) via `performNavigation`, but the public `window.next.router` surface typed both as `(...args: unknown[]) => unknown`, leaving consumers (and the Next.js deploy test suite that `awaits` the result) without a typed boolean to assert on. - Narrow `PagesRouterPublicInstance.push`/`replace` to `(...args: unknown[]) => Promise<boolean>` to match Next.js's `Router.change` contract (router.ts:1206 — `Promise<boolean>`). - Add unit-test regression coverage asserting `window.next.router.push`/`replace` resolve to `true` on success. - Void the `router.push(...)` calls in `tests/e2e/app-router/build-id-navigation.spec.ts` to satisfy the tightened union (App Router push is `void`). Closes #1467 |
||
|
|
62454ef447 |
fix: private use-cache request data (#1445)
* fix use cache private request data * fix private cache nesting guard |
||
|
|
3d5fc8dd84 |
fix(app-router): hoist inline beforeInteractive scripts above resource hints (#1558)
* fix(app-router): hoist inline beforeInteractive scripts above resource hints (#1557) React Fizz puts every stylesheet, modulepreload, and preload link emitted via React Float above user-rendered <script> children of <head>, so an inline `<Script strategy="beforeInteractive">` placed first in the root layout lands near the bottom of head instead — well after styles begin to parse. This breaks the standard no-flash dark-mode pattern. Capture inline beforeInteractive content via a new BeforeInteractiveContext that the App Router SSR pipeline wraps the rendered tree with. The Script shim registers the escaped innerHTML + nonce + attributes and returns null from render. The stream transform splices the captured tag immediately after `<head ...>` opens — before any chunk that contains React-emitted resource hints. On the client the shim suppresses the inline render when the App Router runtime is present (detected via the new hasAppNavigationRuntimeBootstrap helper) so hydration matches. External-src and non-App-Router rendering paths are unchanged: external scripts still preload + render in source order, and Pages Router falls through to the existing inline render. * fix: address PR feedback - e2e spec now reads `self.__vinextThemeInitRan` (matches the fixture's actual marker; the prior `documentElement.dataset` assertion would always have been undefined) - gate `renderBeforeInteractiveInlineScripts` on a `/^[a-zA-Z][\w.-]*$/` attribute-name regex so unknown developer-supplied prop keys can't break out of the unquoted name position - drop unused `BeforeInteractiveProvider` export — the SSR entry uses `BeforeInteractiveContext.Provider` directly - correct the fixture layout docstring to describe the actual `self.__vinextThemeInitRan` marker and explain why we avoid `<html>` dataset attributes here - consolidate the duplicate CSP-nonce middleware blocks for `/revalidate-test` and `/beforeinteractive-head-ordering` * docs/test: address PR nit feedback - correct the createTickBufferedTransform jsdoc: only `injectHTML` falls back to end-of-stream; `injectAfterHeadOpenHTML` is silently dropped when `<head ...>` is never found in a discoverable chunk (covered by the existing unit test) - drop the dead pre-request scaffolding from the CSP-nonce test; the meaningful assertions live in the second request, so the first one was just `void html` noise |
||
|
|
d6bf6413e9 |
feat(pages-router): consume _next/data JSON endpoint from the client (#1412)
* feat(pages-router): consume _next/data JSON endpoint from the client
The server-side `/_next/data/<buildId>/<page>.json` endpoint landed in
#1384, but the vinext client was still fetching the full HTML page on
every navigation and regex-extracting `__NEXT_DATA__` from it. This
wires the client to actually use the JSON endpoint, matching what
Next.js does and avoiding the HTML round trip.
## Client navigation (shims/router.ts)
`navigateClient` is split into two paths:
- `navigateClientData` (new): when `window.__VINEXT_PAGE_LOADERS__` has
a registered code-split loader for the target route AND we have a
buildId, fetch `/_next/data/<buildId>/<page>.json` with
`Accept: application/json` + `x-nextjs-data: 1`. On 200 parse the
envelope, look up the page module via the in-memory loader map (Vite
has already split each page into its own chunk), and re-render.
- `navigateClientHtml`: the previous HTML-fetch + regex-extract logic,
unchanged. Used in dev (where the inline hydration script does not
populate the loader map) and as a fallback for any route not in the
client loader map.
Failure modes — 404, 5xx, network error, malformed JSON, soft
redirect via `x-nextjs-redirect`, missing loader — all queue a hard
navigation (`window.location.href = url`). This is the deploy-skew
safety net: when the server's buildId has rotated, the data endpoint
returns 404 and the client lands on the new build via a full document
load.
## Code-split loader manifest (entries/pages-client-entry.ts)
Vite already code-splits each page into its own chunk via the dynamic
imports in `pageLoaders`. The generated client entry now exposes that
map on `window.__VINEXT_PAGE_LOADERS__` (route pattern → loader thunk),
plus `window.__VINEXT_PAGE_PATTERNS__` (ordered list) and
`window.__VINEXT_APP_LOADER__` (the `_app` thunk, when present). This
replaces Next.js' `_buildManifest.js` indirection — vinext already
generates the equivalent at build time, we just had to expose it.
## Data URL construction (shims/internal/pages-data-url.ts)
New pure helper module mirroring `getAssetPathFromRoute` +
`getDataHref` from Next.js. Handles:
- `/` → `/_next/data/<id>/index.json`
- `/about` → `/_next/data/<id>/about.json`
- `/index` → `/_next/data/<id>/index/index.json` (round-trip)
- `/blog/foo` → `/_next/data/<id>/blog/foo.json`
Also exports `matchPagesPattern` which uses the existing
`matchRoutePattern` to map a URL pathname to a registered route
pattern.
Round-trip tests verify the server's `parseNextDataPathname` agrees
with the client's `buildPagesDataPath` for every shape, and surfaced a
latent server bug: `/_next/data/<id>/index/foo.json` was parsing back
to `/index/foo` instead of `/foo`. Fixed in `pages-data-route.ts` so
the round-trip is now lossless.
## Prefetch (shims/link.tsx + shims/router.ts)
`<Link>` prefetch and `Router.prefetch()` now prefer the data path:
- Inject `<link rel="prefetch" as="fetch" crossorigin href={dataUrl}>`
for the JSON, matching the actual navigation's request shape.
- Invoke the loader thunk to warm the chunk in parallel. Vite's
`import()` caches the result, so the eventual click is a double
cache hit.
When no loader matches (dev, unmapped route), fall back to the legacy
`<link rel="prefetch" as="document">` so the browser at least preloads
the HTML.
Notably this also fixes a long-standing prefetch bug: the previous
Pages branch of `link.tsx`'s prefetch check
(`__NEXT_DATA__.__vinext?.pageModuleUrl`) was only populated by the
dev server's inline hydration script, so prefetch in production was a
no-op.
## Tests
- `tests/pages-data-url.test.ts` (new): 27 tests covering URL
construction and the client/server round trip.
- `tests/shims.test.ts` (+8): JSON-path navigation success, 404
hard-reload (build-skew), `x-nextjs-redirect` soft-redirect handling,
HTML fallback when no loader matches, dynamic param extraction,
malformed JSON hard-reload, prefetch JSON path, prefetch fallback.
- Ports from Next.js: `getDataHref` in
`.nextjs-ref/packages/next/src/client/page-loader.ts:154`,
`fetchNextData` in
`.nextjs-ref/packages/next/src/shared/lib/router/router.ts:498`,
`getAssetPathFromRoute` in
`.nextjs-ref/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts`.
Verified: 1393 vitest tests pass across shims, pages-router,
link/navigation, prefetch, data-route, data-url, entry-templates, and
routing.
Closes #1330 follow-up.
* fix(pages-router): drop unused export on PagesPatternMatch
Knip flagged the exported `PagesPatternMatch` type as unused. It is only
referenced as the return type of `matchPagesPattern` in the same file, so
narrow it to a local type.
* fix(pages-router): merge search params into __NEXT_DATA__.query on data-path nav
bigbonk review found that navigateClientData was synthesising
__NEXT_DATA__.query from dynamic route params only, missing the URL search
params. Code reading window.__NEXT_DATA__.query directly (a public Next.js
surface) would see incomplete data after a JSON navigation, even though
useRouter().query happened to still work because getPathnameAndQuery()
re-reads window.location.search.
Parse target.search and merge it into query alongside the route params,
with route params winning on key collision — same merge order as
getPathnameAndQuery() and Next.js itself.
Also fix a misleading comment in pages-client-entry.ts: the patterns list
is already sorted by pagesRouter()/compareRoutes (static → dynamic →
catch-all), not by the router at runtime.
* test(pages-router): e2e regression guard for JSON nav path
Adds a Playwright assertion that a Link click on a hydrated Pages Router
app on Cloudflare Workers fires a request to /_next/data/<buildId>/<page>.json
and does NOT trigger a full HTML reload. If the build pipeline ever stops
exposing __VINEXT_PAGE_LOADERS__ (or the loader map mis-keys), every other
navigation test still passes — they all assert the URL changed and the new
page rendered, neither of which fails when navigateClient silently falls
back to the HTML path. This test fails loudly in that scenario.
* test(pages-router): use hydration timestamp instead of resource-type sniffing
The original assertion 'no document-resource request to /ssr' caught false
positives: <link rel="prefetch" as="document"> can fire on initial paint
for unrelated reasons, and Chromium does not always classify prefetch hints
in a stable way. Swap to checking that __VINEXT_HYDRATED_AT survives the
click — a document reload would reset it, an in-place re-render preserves it.
Combined with the data-URL network assertion, this remains a tight guard on
the JSON nav path without the flake.
* test(pages-router): use window sentinel instead of timestamp comparison
Timestamps are timing-related signals; swap to a window-scoped sentinel
that a document reload would wipe and an in-place re-render preserves.
Same guarantee, no flake.
* fix(pages-router): handle _next/data requests in Cloudflare Workers example
The Node prod-server normalizes /_next/data/<buildId>/<page>.json before
middleware and flags it as a data request so renderPage emits the JSON
envelope. The Cloudflare worker example never did — data requests hit
matchPageRoute which can't match /_next/data/... and returned 404, so
navigateClientData saw the 404 and triggered a hard navigation. The
client-side JSON nav path was effectively dead on Workers.
Mirror the prod-server normalization in the worker: detect the data path
before middleware, validate the buildId, strip it to the page path, and
pass { isDataReq: true } through to renderPage.
Export ./server/pages-data-route from the vinext package so user-written
workers can import the helpers.
* Revert "fix(pages-router): handle _next/data requests in Cloudflare Workers example"
This reverts commit
|
||
|
|
ddfdd18a1a |
fix(pages-router): honour shallow prop on next/link (#1413)
Closes the last sub-problem of #1332. The Pages Router `Router.push` / `Router.replace` already short-circuit the `_next/data` fetch when `options.shallow === true`, but `<Link shallow>` never made it to the router because `shallow` was missing from `LinkProps`. The prop silently fell into `...rest`, was spread onto the underlying `<a>`, and the click handler called `Router.push(href, undefined, { scroll, locale })` with no shallow flag — so every `<Link shallow>` did a full server roundtrip and GSSP re-ran. This matches Next.js's deploy-suite fixture `test/e2e/middleware-trailing-slash/app/pages/shallow.js`, which clicks a `<Link href="/sha?hello=world" shallow>` and asserts the GSSP-generated random message stays unchanged afterwards. The fix is mechanical: add `shallow?: boolean` to `LinkProps`, destructure it in the Link component, forward it through `navigatePagesRouterLink` into the router transition options. `shallow` is only included in the router options when explicitly set so existing callers that omit it keep the same options shape. |
||
|
|
d504f022b0 |
fix(assets): default assetsDir to _next/static (Next.js parity) (#1411)
* fix(assets): default assetsDir to _next/static (Next.js parity) Closes #1337. Supersedes #1383. vinext's default `assetsDir` was Vite's historical `assets/`, so URL emission (`/_next/static/...` via `resolveAssetUrlPrefix("")`) and on-disk layout (`dist/client/assets/...`) disagreed in the empty-`assetPrefix` case. The `build.assetsDir` and `experimental.renderBuiltUrl` Vite overrides only applied when `assetPrefix` was configured, leaving the no-prefix branch serving from a different path than the URL contract Next.js's client runtime and test harness assert against. This commit flips the default to Next.js's canonical layout: - `resolveAssetsDir("")` returns `_next/static` (was `assets`) - `build.assetsDir` is now set unconditionally from `resolveAssetsDir` - Vite's default `base + assetsDir` composition produces correct URLs in the no-prefix case; `renderBuiltUrl` stays gated on `assetPrefix` because it's only needed for the configured cases - Drops the legacy `/assets/` branches in prod-server (3 sites) and static-file-cache (hard cutover — see PR description for rationale) - Updates `_headers` generation, precompress default, fonts plugin `DEFAULT_ASSETS_DIR` to match the new default With the layout aligned, invalid `_next/static/*` requests naturally return plain-text `404 + "Not Found"` from the static-file layer instead of falling through to the page renderer (which would produce an HTML 404 with bootstrap scripts and CSS). This replaces #1383's parity short-circuit with the natural code path: - Node prod-server (App + Pages branches): missing asset under `resolveAppRouterAssetPath` returns `text/plain; charset=utf-8` 404 - Cloudflare worker entry: `isNextStaticPath` recognises asset-shape after ASSETS-binding misses; returns `notFoundStaticAssetResponse` Mirrors Next.js: packages/next/src/server/lib/router-server.ts. Adds `tests/invalid-static-asset-404.test.ts` (3 App Router + 3 Pages Router cases — no prefix / basePath / assetPrefix) ported from Next.js e2e suites. Updates 11 source files + 12 test files; 914+ tests pass in the touched suites (asset-prefix, app-router, pages-router, deploy, font-google, static-file-cache, serve-static, precompress, features, routing, isr-cache, build-optimization, app-rsc/ssr, standalone, middleware, shims). Ported from Next.js: - test/e2e/invalid-static-asset-404-app/*.test.ts - test/e2e/invalid-static-asset-404-pages/*.test.ts * fix(e2e): update cloudflare-pages-router hydration spec for _next/static * PR #1411 approved. Clean fix, 2 nits. Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> * refactor(assets): derive isHashed checks from ASSET_PREFIX_URL_DIR Replace hardcoded '_next/static' string literals in runtime code paths with the ASSET_PREFIX_URL_DIR constant so the asset URL convention has a single source of truth. - server/prod-server.ts isHashed: pathname.includes(`/${ASSET_PREFIX_URL_DIR}/`) - server/prod-server.ts public-dir guard: startsWith(`/${ASSET_PREFIX_URL_DIR}/`) - server/static-file-cache.ts isHashed: both startsWith + includes derived - plugins/fonts.ts DEFAULT_ASSETS_DIR = ASSET_PREFIX_URL_DIR Behavioural no-op — the constant is '_next/static' so the resolved strings are identical. Centralises the URL contract so future changes to ASSET_PREFIX_URL_DIR (or a refactor that derives it from build config) flow through to every consumer. Doc-comment references to '_next/static' left in place — they describe the canonical value for human readers and are not code paths. --------- Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com> Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> |
||
|
|
517c2bde19 |
feat(pages-router): expose window.next.router on hydration (#1400)
* feat(pages-router): expose window.next.router on hydration
Promote next/router from a lazy dynamic import inside hydrate() to a
static side-effect import at the top of the generated client entry, so
window.next = { version, router, ... } is published before any test
harness or third-party script reads it.
Without this, the Next.js deploy test suite — which drives client-side
navigations via `browser.eval('next.router.push(...)')` — crashes with
`TypeError: Cannot read properties of undefined (reading 'router')` (~80
test failures across middleware-general, middleware-rewrites,
trailing-slashes, basepath, with-router, and ignore-invalid-popstateevent).
Mirrors Next.js's bootstrap order in
.nextjs-ref/packages/next/src/client/next.ts (line 5–13), which
statically imports the router from './' and sets window.next as a
top-level side effect before initialize/hydrate.
Closes #1329
* feat(router): expose state fields as live getters on Router singleton
window.next.router must expose `pathname`, `route`, `query`, `asPath`,
`basePath`, `locale`, `locales`, `defaultLocale`, `domainLocales`,
`isReady`, `isPreview`, and `isFallback` as live properties — not just
the navigation methods. The Next.js deploy test suite drives navigations
via `browser.eval('next.router.push(...)')` and then asserts on
`browser.eval('next.router.pathname')` to verify the navigation
completed; without the field getters the assertion reads undefined.
Mirrors Next.js's `singletonRouter` shape in
.nextjs-ref/packages/next/src/client/router.ts (lines 32-47), which
uses Object.defineProperty for urlPropertyFields so reads forward to
the active router state.
Strengthens the existing shims.test.ts coverage so future regressions
of the field surface fail at unit-test time.
|
||
|
|
5748be3ba5 |
feat(cache): gate planner commits on explicit reuse proof (#1327)
* feat(cache): gate planner commits on explicit reuse proof Runtime cache entries could enter browser commit planning as ordinary payloads even when the entry had no planner-visible reuse proof. That violated the #726 invariant that a cache hit is not semantic authority by itself. The planner now receives a compact cache-entry proof decision through AppElements metadata and rejects missing or incompatible cache entries before route-topology commit approval. Cache-restored browser payloads synthesize the missing-proof rejection when metadata is absent, while fresh payloads keep the existing path. Targeted tests cover proof projection, metadata transport, planner decisions, and the cached-payload lifecycle path. * fix(cache): skip unproofed visited RSC cache restores Visited App Router RSC snapshots were still eligible for replay even though production payloads do not yet carry cache-entry reuse proof. That let the new planner gate reject the cache path and turn soft restores into hard navigations. Require decoded proof metadata before a payload is stored as a visited-response restore candidate, and update the real app-router navigation test to prove unproofed responses refetch without reloading. Also make cache-proof rejection-code parsing exhaustive at the type boundary. * fix(cache): require explicit App Router payload origins App Router navigation commits still carried cache-restore semantics as an optional boolean. A fresh default could hide a future restore path that forgot to request reuse proof, reopening the cache authority bug for visited responses. The violated invariant is that cache origin is part of the payload contract, not a caller memory check. Replace the boolean with a discriminated AppNavigationPayloadOrigin and thread it through browser entry, navigation controller, visible commit classification, and tests. Fresh payloads remain proof-optional; visited-cache payloads synthesize a missing-proof rejection and fail closed. * docs(cache): document static layout proof parsing boundary Runtime cache proof parsing currently accepts one proven reuse class: static layout. Future proof classes must update the parser before visited-cache responses can safely restore as commit-capable payloads. The review also noted that the cache-restorable metadata predicate behaves as a narrowing boundary. Make that contract explicit as a TypeScript type guard without changing runtime behavior. * fix(cache): keep accepted proof in rejection traces Cache-proofed payloads that failed a later interception or root-boundary gate produced correct hard-navigation decisions, but the rejection trace lost the accepted proof fields. That made cache-proof diagnostics incomplete for payloads that passed proof validation and failed a later topology gate. The root cause was that later rejection branches still used the pre-cache-proof trace fields after commitTraceFields had been computed. Thread the enriched trace fields into those rejection decisions, document cache proof parser three-way semantics, narrow the cache-restorable metadata type guard precisely, and read the pending commit proof from the canonical top-level field. |
||
|
|
0d8d3e3c60 |
refactor(router): quarantine navigation runtime topology (#1322)
* refactor(router): quarantine navigation runtime topology * fix(router): restore manifest-backed interception runtime App Router browser navigations lost first-hop intercepted route context after snapshot topology stopped acting as fallback proof. That made /feed to /photos/:id request the direct page payload instead of the declared modal payload. The violated invariant was that route topology must come from the manifest, while snapshots only carry runtime proof. Resolve first-hop interception from the manifest, move RSC bootstrap streams onto the symbol-backed runtime, teach prerender extraction that script shape, and harden the runtime contract around route manifest and RSC data. Coverage includes runtime seam tests, prerender extraction tests, planner/browser navigation tests, package rebuild, and the app-router intercepted routes E2E slice. * fix(router): narrow prerender RSC script parsing The prerender extractor accepted any script starting with the symbol-runtime bootstrap expression, then parsed it as though it had the exact RSC chunk-push prefix. That was harmless at the current head, but it made future runtime scripts a foot-gun. Only the exact runtime chunk prefix and exact done marker are now accepted. A focused extractor test covers non-chunk runtime scripts that share the bootstrap expression prefix. * fix(router): address navigation runtime review * fix(router): resolve navigation runtime review threads |
||
|
|
9415a238f4 |
feat(config): implement assetPrefix (#1311)
* feat(config): parse and apply next.config assetPrefix at build time Wire `assetPrefix` from next.config through to the Vite build so every emitted JS/CSS/image/static asset URL is prefixed accordingly. Mirrors Next.js semantics: `assetPrefix` controls asset URLs only, independent of `basePath` (which still controls route URLs). * Add `assetPrefix` to NextConfig and ResolvedNextConfig, normalised via a new `normalizeAssetPrefix` helper that trims trailing slashes, accepts absolute URLs (`https://cdn.example.com[/sub]`) and path prefixes (`/custom-asset-prefix`), and rejects non-string / unparseable values. * New `packages/vinext/src/utils/asset-prefix.ts` module collects the shared helpers: `resolveAssetsDir` (on-disk layout), `resolveAssetUrlPrefix` (emitted URL prefix), `assetPrefixPathname` (path portion for runtime matching), and the canonical `_next/static` directory constant. * In the Vite plugin, set `build.assetsDir` so on-disk layout matches the URL (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs). Use `experimental.renderBuiltUrl` to override Vite's default `base + url` URL emission so `assetPrefix` and `basePath` stay independent — routes continue to anchor at `base = basePath + '/'`. * Persist `assetPrefix` in the embedded `vinextConfig` for the Pages Router server and in a new `vinext-runtime-config.json` sibling for the App Router prod server (the RSC entry only embeds `__basePath`). @see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix * feat(server): serve assetPrefix-prefixed assets at runtime Update the production server to resolve incoming asset requests under `<assetPrefix>/_next/static/<filename>` (path-prefix form) and `/_next/static/<filename>` (absolute-URL form) in addition to the historical `/assets/` URLs. Both routers (Pages and App) share a new `resolveAppRouterAssetPath` helper that returns the lookup path on disk or `null` to fall through. * App Router prod server loads `assetPrefix` from the new `vinext-runtime-config.json` sibling written by the Vite plugin. * Pages Router prod server reads `assetPrefix` from the embedded `vinextConfig` and threads it through the existing `stripBasePath` lookup. * StaticFileCache marks any path under `_next/static/` as content-hashed (alongside the existing `assets/` rule) so emitted bundles get the long-lived `immutable` cache headers. Same change in the slow-path branch of `tryServeStatic`. Cloudflare Workers needs no code change: the ASSETS binding serves `<prefix>/_next/static/...` directly from `dist/client/<prefix>/_next/static/...` because the on-disk layout already mirrors the URL. Absolute-URL prefixes are expected to be served by the CDN, so they don't reach the worker. * test(asset-prefix): add unit + e2e build tests for next.config assetPrefix Covers the surface area added in the previous two commits: * Unit tests on the new helpers: `normalizeAssetPrefix` (path vs URL, trailing-slash trimming, validation), `resolveAssetsDir` / `resolveAssetUrlPrefix` (on-disk vs emitted-URL layout), `assetPrefixPathname` (runtime matching), and `resolveAppRouterAssetPath` (request → on-disk lookup translation). * End-to-end build tests that exercise `vinext build` against the `tests/fixtures/app-basic` app patched with each `assetPrefix` shape: - Path prefix (`/custom-asset-prefix`): SSR HTML references `/custom-asset-prefix/_next/static/` URLs, on-disk layout matches, and fetching the bootstrap URL from the prod server returns 200. - Absolute URL (`https://cdn.example.com`): emitted asset URLs are fully qualified; no `/assets/` leaks. - Combined `basePath: '/app'` + `assetPrefix: '/cdn-prefix'`: routes stay under `/app/` while asset URLs live under `/cdn-prefix/` — proving the two configs stay independent. - Unset (default): historical `/assets/` layout is preserved. Ported from Next.js: test/e2e/app-dir/asset-prefix/asset-prefix.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/asset-prefix/asset-prefix.test.ts * test(next-config): add assetPrefix field to makeResolved helper ResolvedNextConfig requires assetPrefix; the test helper was missing it which broke the type check. * fix(config): replace polynomial regex with loop (CodeQL) CodeQL flagged trim().replace(/\/+$/, "") and replace(/^\/+/, "") as polynomial-time backtracking on uncontrolled input. Swap both for explicit while loops with linear-time semantics. Same fix applied to the URL pathname trim in assetPrefixPathname(). * refactor(prod-server): drop dead null fallback; tighten hashed-asset detection - Remove the `?? null` after resolveAppRouterAssetPath; the helper already returns null on miss, so the fallback (and its obsolete comment) were no-ops. - Add explicit startsWith("_next/static/") / startsWith("/_next/static/") prefix checks before the substring fallback in both static-file-cache and prod-server. The substring check stays so assetPrefix-prefixed layouts (e.g. "cdn/_next/static/") are still recognised. * fix(build/precompress): use resolved assetsDir so assetPrefix builds get compressed precompressAssets() hardcoded `<clientDir>/assets/`. When `assetPrefix` is configured the build writes hashed assets to a different subdirectory (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs) and the precompression step walked an empty directory, silently emitting zero .br/.gz/.zst files. Cloudflare Workers builds with assetPrefix would then serve uncompressed assets. Thread the resolved subdirectory through from resolveAssetsDir(assetPrefix) at the plugin call site, default to `"assets"` for backward compat, and add focused tests for both assetPrefix layouts. * refactor: drop redundant isHashed startsWith; harden test cleanup against build failures - prod-server.ts: drop the redundant `pathname.startsWith("/_next/static/")` check; `pathname` always has a leading slash so the existing `includes("/_next/static/")` already covers it. - static-file-cache.ts: keep both `startsWith("_next/static/")` and `includes("/_next/static/")` because `relativePath` has no leading slash — the prefix and substring checks cover different layouts (absolute-URL prefix vs path-prefix). Clarify the comment. - tests/asset-prefix.test.ts: register the tmp-dir cleanup with the shared `cleanups` array synchronously inside `buildFixtureWithConfig`, right after `mkdtempSync`. Previously the cleanup was only pushed after the build returned, so a thrown `createBuilder`/`buildApp` would leak the tmp dir. * refactor(build): embed assetPrefix in App Router entry instead of sidecar JSON Mirrors the existing __basePath embed pattern (see entries/app-rsc-entry.ts) and how Pages Router exposes `vinextConfig.assetPrefix` from its own entry. The runtime-config.json sidecar was unnecessary — the App Router entry template already inlines config values as compile-time constants, so assetPrefix should be inlined the same way. Changes: - Add `assetPrefix` to `AppRouterConfig` in entries/app-rsc-entry.ts. - Emit `export const __assetPrefix = <json>` from the generated RSC entry alongside the existing `__basePath` / `__configHeaders` / `__expireTime` constants. Single-line declaration to stay within the "thin generated entry" guideline. - Thread `nextConfig.assetPrefix` through the generateRscEntry call site in index.ts. - App Router prod-server reads `rscModule.__assetPrefix` (with a string fallback for older builds) instead of `vinext-runtime-config.json`. - Delete the vinext-runtime-config.json write in index.ts. Nothing else in the repo reads this file (verified via rg on packages/ tests/ examples/). Pages Router behaviour is unchanged: it already gets assetPrefix from the embedded `vinextConfig` exported by pages-server-entry.ts. Addresses james-elicx review feedback on #1311. * fix(ssr): emit bootstrap as <script src> via bootstrapModules The App Router SSR pipeline previously passed only `bootstrapScriptContent` to React's renderToReadableStream, producing inline `<script>import("/path/to/entry.js")</script>` tags. The URL was therefore inaccessible to anything that reads `script.attribs.src` — including the Next.js asset-prefix fixture test "bundles should return 200 on served assetPrefix" which iterates script tags looking for a `src` attribute prefixed by the configured assetPrefix. Extract the bootstrap URL from the plugin-rsc-generated content and pass it as `bootstrapModules: [url]` so React emits a real `<script type="module" src="url">` tag. The existing `extractModulePreloadHtml` helper already did the same extraction for the modulepreload <link>, so the URL was already exposed (just renamed `extractBootstrapModuleUrl` and lifted out of the head-injection helper to share between the bootstrap and the preload tags). The inline `bootstrapScriptContent` is no longer needed — it was only carrying the dynamic-import call that bootstrapModules now performs natively. Passing both would load the bootstrap module twice. `bootstrapModules` works in both dev and prod: in dev, plugin-rsc emits a Vite dev-server URL (e.g. `/@id/__x00__virtual:vinext-app-browser-entry`) that the browser fetches as a module from the dev server; in prod it's the hashed bundle URL, optionally prefixed by `assetPrefix`. Mirrors Next.js's app-render path which combines both `bootstrapScriptContent` (for session state) and `bootstrapScripts` (for the actual chunk URL). See .nextjs-ref/packages/next/src/server/app-render/required-scripts.tsx. We use `bootstrapModules` instead of `bootstrapScripts` because vinext's chunks are native ES modules (Vite output) — a `type="module"` tag is the correct loader. Tests updated where they asserted on the old `import("URL")` shape: tests/asset-prefix.test.ts, tests/app-router.test.ts, tests/e2e/cloudflare-workers/navigation.spec.ts. Verified all related suites pass locally (asset-prefix, app-router, script-nonce, script-preload, use-client-page-pathname, document, prerender, ISR, fetch-cache, entry-templates, precompress). * fix(build): use resolveAssetsDir for Pages Router client entry fallback scan The Pages Router Cloudflare Workers post-build step scans the on-disk assets directory for the client entry chunk when the SSR manifest lookup doesn't surface one. The directory path was hardcoded to `<clientDir>/assets/`, which is the Vite default — but when `assetPrefix` is configured, the chunk lives under `<prefix>/_next/static/` (path-prefix) or `_next/static/` (absolute-URL prefix). The fallback would silently miss the entry and the worker would ship without its client bootstrap reference. Use the shared `resolveAssetsDir(assetPrefix)` helper that already drives `build.assetsDir` and the prod-server lookup, so this scan works for every supported asset layout. Addresses round-4 reviewer feedback on #1311 (low-priority follow-up item — only fires when the SSR manifest lookup fails). * feat(config): mirror Next.js basePath → assetPrefix fallback Next.js auto-sets `assetPrefix = basePath` when `basePath` is configured and `assetPrefix` is left unset (packages/next/src/server/config.ts:528-531). Without this, an app deployed under a basePath serves its routes correctly but emits its assets from `<basePath>/assets/...` (Vite's default `base + assetsDir` composition) rather than from the Next.js- canonical `<basePath>/_next/static/...`. Add the same fallback in resolveNextConfig, gated by Next.js's own conditions: - basePath !== "" (skip when basePath is unset) - basePath !== "/" (Next.js rejects this earlier; we mirror the guard so we never produce assetPrefix === "/") - assetPrefix === "" (user did not opt out) The fallback feeds the rest of the asset-prefix pipeline (Vite's `build.assetsDir`, `experimental.renderBuiltUrl`, and the prod-server URL→file resolver) so no other code paths need to change. Tests: - tests/next-config.test.ts: 5 unit tests for the four edge cases of the fallback condition. - tests/asset-prefix.test.ts: new end-to-end test that builds with `basePath: "/app"` (no assetPrefix), starts the prod-server, and asserts the bootstrap <script src> lands at /app/_next/static/... (and that the legacy /app/assets/... layout is gone). - tests/pages-router.test.ts: updated the "preserves basePath on backfilled SSR manifest entries" test to expect the new /docs/_next/static/ layout (was /docs/assets/). The manifest assertion is relaxed to startsWith("docs/") because Vite's raw SSR manifest now emits some duplicate-prefixed entries ("docs/docs/_next/static/...") alongside the backfilled ones; both forms anchor under basePath so the prod-server's URL→file lookup remains correct — and the user-visible HTML assertions in the same test are the source of truth. Addresses round-4 reviewer feedback on #1311. * fix(server): Pages Router must not strip basePath before asset lookup The Pages Router prod-server stripped `basePath` from the request pathname BEFORE handing it to `resolveAppRouterAssetPath`, which broke the asset lookup whenever the assetPrefix path-prefix branch was active with a path that happened to equal basePath. Concrete failure: with the round-4 commit `bac8425` mirroring Next.js's `basePath → assetPrefix` fallback, an app configured with `basePath: "/docs"` (and no explicit `assetPrefix`) now resolves `assetPrefix = "/docs"`. Emitted asset URLs are `/docs/_next/static/…` (correct). But the Pages Router branch did: const staticLookupPath = stripBasePath(pathname, basePath); // "/docs/_next/static/x.js" → "/_next/static/x.js" resolveAppRouterAssetPath(staticLookupPath, "/docs", "/docs"); // path-prefix branch needs pathname to start with "/docs/" → null → 404 The App Router branch already passes the un-stripped `pathname` to the same helper (see line ~1140). Match that behaviour. `staticLookupPath` is still computed and used downstream by non-asset paths (image-optimization endpoint, page routing), where the basePath-stripped form is correct. Test coverage: - tests/pages-router.test.ts: extended the existing "preserves basePath on backfilled SSR manifest entries" test to FETCH every emitted asset URL and assert HTTP 200 (previously it only inspected HTML for the URL string). This is the missing assertion that would have caught the bug the first time. - tests/asset-prefix.test.ts: added a parallel Pages Router e2e test that builds an inline pages/ fixture with `basePath: "/docs"`, starts the prod-server, collects every script/link URL from the SSR HTML, and asserts every URL serves a 200 + correct content-type. Confirmed to fail on `bac8425` before this fix and pass after. Addresses round-5 reviewer feedback on #1311. * refactor(ssr): accept either quote style in bootstrap URL regex plugin-rsc currently emits the bootstrap as `import("<url>")` with double quotes, but the contract isn't documented and a future version could switch to single quotes. Tighten the URL extraction regex to `import(["'])([^"']+)\1` so either form works. Addresses non-blocking round-5 review feedback on #1311. * docs(ssr): note CSP implications of bootstrapModules switch The round-3 commit `3a2c886` switched the App Router SSR bootstrap from inline `bootstrapScriptContent` (`<script>import(…)</script>`) to React's `bootstrapModules` option (`<script type="module" src=…>`). That has user-visible CSP consequences worth documenting at the call site so app authors hit them in code search rather than at runtime: - Apps no longer need `script-src 'unsafe-inline'` for the bootstrap (improvement — inline imports previously required it). - Apps that restrict script sources need `'self'` for same-origin, or the CDN origin when assetPrefix is an absolute URL. - React still threads nonce through to the emitted <script> tag, so nonce-based CSP (`script-src 'nonce-…' 'strict-dynamic'`) keeps working unchanged. Addresses non-blocking round-5 review feedback on #1311. |
||
|
|
a85c2c1dee |
fix: add x-action-forwarded guard and E2E tests for server action forwarding loop (#1292)
* feat(server-action): add x-action-forwarded guard to prevent forwarding loops Defensively implements the Next.js server action forwarding loop fix. - Adds ACTION_FORWARDED_HEADER constant to headers.ts - Adds x-action-forwarded to INTERNAL_HEADERS blocklist - Adds guard to handleServerActionRscRequest and handleProgressiveServerActionRequest - Returns 404 + x-nextjs-action-not-found when header is present Ported from Next.js: vercel/next.js@20892dd * test(e2e): add action-forward-loop fixtures and tests - Adds /nextjs-compat/action-forward-loop fixture with middleware rewrite - Adds /nextjs-compat/action-forward-loop-rewrite as rewrite target - Adds error boundary component using unstable_isUnrecognizedActionError - Adds E2E tests: middleware rewrite + action POST, not-found header contract * test(e2e): fix action-forward-loop tests per review feedback - page.tsx: use useActionState so action result renders into #action-result - actions.ts: update runAction signature for useActionState compat - Remove dead error-boundary.tsx (never exercised in single-worker model) - Rename second test to match actual behavior (stale action ID -> 404) * test(request-pipeline): add x-action-forwarded to INTERNAL_HEADERS snapshot |
||
|
|
f4a37b4ade |
fix(app-router): track hash-only traversal metadata (#1306)
* fix(app-router): track hash-only traversal metadata Hash-only App Router navigations wrote null history state even though they are app-owned browser-visible entries. That kept traversal direction unknown, but it broke the metadata invariant introduced by the App Router navigation lifecycle work. Route hash-only push and replace through a browser-entry-owned commit hook so the same traversal index allocator and metadata shape are used without fetching RSC. Also update the local traversal index during same-route hash popstate so replace-after-back uses the current entry index. Closes #1252 * fix(app-router): avoid stale hash replace scroll state Hash-only App Router replace now passes the effective scroll option into the browser-entry commit hook. Default-scroll replaces strip vinext scroll restoration keys before adding traversal metadata, avoiding contradictory history entries whose URL points at a hash target but whose state restores the old scroll position. Explicit scroll:false keeps the previous scroll restoration state. Add browser coverage for replace-after-back to #content, including the traversal back to the replaced entry. |
||
|
|
16f9ea393f |
fix(app-router): model RSC redirect and traversal lifecycle (#1248)
RSC redirects were followed by mutating browser history before the redirected payload had passed the visible commit lifecycle. That could collapse the initiating history entry and leave back/forward traversal without an explicit intent model. Keep redirect hops inside the initiating navigation until an approved commit publishes history, add explicit terminal redirect decisions, and record per-entry traversal metadata so unknown browser history state stays unknown instead of being guessed. Add focused unit coverage for redirect decisions and traversal metadata plus a browser regression for router.push redirect back navigation. |
||
|
|
656d954abf |
feat(router): promote intercepted preservation through planner (#1249)
* feat(router): promote intercepted preservation through planner Intercepted navigation could still preserve source UI through context-bearing payload shape and same-URL history state. That made modal preservation depend on transport metadata instead of the visible route world that had actually been committed. The planner now requires explicit interception proof before preserving source layouts or unrelated slots, and the browser lifecycle clears stale previous-next history state when normal target payloads commit. Wire metadata carries proof, while missing, stale, or incompatible proof falls back to hard navigation. Tests cover approved source-slot preservation, missing, stale, and malformed proof rejection, current-context refresh and action handling, traverse restoration, and direct-target refresh clearing stale interception state. * fix(router): normalize intercepted proof paths Interception proof could be built from browser-derived percent-encoded paths while committed route snapshots used the server-normalized route-state form. Non-ASCII intercepted source or target paths could then fail proof validation and fall back to a hard navigation. Normalize proof matched URLs before encoding source and target route IDs, and normalize client planner snapshot matched URLs to the same route-state representation without changing the user-facing navigation snapshot. Adds regressions for encoded non-ASCII proof generation and planner validation. * review: address ask-bonk PR review comments - Add interceptedRejectedTargetMismatch reason code for target URL mismatch in planner traces (was misusing UnknownSource) - Document double-attempt replaceState fallback for browser quirks - Clarify navigate branches: proven interception vs legacy fallback - Document cache migration safety for legacy interceptionContext - Tighten isInterceptionMetadataValue type guard with string checks - Extract interception attachment into shared step in wire metadata entries to remove asymmetric branching * fix(router): ignore context-only interception metadata Context-only AppElements payloads could enter intercepted preservation planning or leave context-suffixed route identity as later proof authority. That made transport metadata influence visible-world preservation when a normal response was rendered from an intercepted request context. Only explicit interception proof now enters the preservation branch. Planner snapshots also canonicalize context-suffixed route IDs when no proof is present, keeping render keys partitioned while preventing stale context from poisoning future explicit interception proof. Tests cover the direct planner decision and the two-step browser lifecycle regression. * test(router): cover intercepted proof rejection matrix Planner traces now expose distinct rejection reasons for mismatched intercepted targets and incompatible roots. Without direct tests, those branches could regress while the broader intercepted route scenarios still passed. Add focused planner tests for target mismatch and incompatible root rejection, use the local target snapshot consistently, document two lifecycle ordering contracts, and avoid the no-op interception metadata spread on non-intercepted payloads. |
||
|
|
7283e44beb |
fix(router): keep next/router import free of popstate side effects (#1294)
* fix(router): keep next/router import free of popstate side effects Importing next/router currently installs the Pages Router popstate listener as module-level browser state. That listener can be pulled into App Router client bundles by compatibility imports, where it races the App Router history runtime and can turn back/forward traversal into a document navigation. Move Pages Router popstate installation behind an internal vinext runtime module that only Pages Router entrypoints call after hydration setup. Keep the existing popstate decision logic in the router shim, but make global listener ownership explicit at the Pages boundary. Add unit coverage for side-effect-free next/router imports and an App Router e2e regression that imports next/router, traverses history, and asserts no document request occurs. * refactor(router): remove redundant beforePopState assertion The Pages Router popstate handler already narrows the beforePopState callback before invoking it. Call the narrowed function directly so the production router code does not carry an unnecessary type assertion. |
||
|
|
d558923fde |
fix(script): dedupe concurrent same-src loads (#1263)
* fix(script): dedupe concurrent same-src loads Multiple next/script components with the same src could append duplicate DOM scripts when they mounted before the first script load event fired. That double-executed third-party code and diverged from Next.js' in-flight script cache behavior. The shim now tracks remote scripts that are currently loading separately from scripts that have completed loading, and fans out load/error callbacks without adding another script element. Adds a Pages Router browser regression that verifies simultaneous same-src scripts produce one DOM script and one execution. * fix: address review feedback for script load dedupe - Defer loadedScripts.add(key) to after in-flight promise resolves - Add onReady() callback for duplicate callers joining in-flight load - Clean up loadingScripts entry on promise settlement to allow retry |
||
|
|
ba1829a804 |
fix(image): clear blur placeholder after image load (#1267)
* fix(image): clear blur placeholder after image load Blur placeholders currently stay on the img background after the high resolution image finishes loading. That is wrong for transparent image formats because the low quality placeholder remains visible through the rendered pixels. The shim treated placeholder styling as static props instead of load lifecycle state. Track blur completion per src, clear local and remote placeholder backgrounds after successful load or error, and cover the transparent-image regression in an app-router browser test. * chore: rerun ci --------- Co-authored-by: James <james@eli.cx> |
||
|
|
d280e3620b |
fix(app-router): skip RSC navigation for hash-only history traversal (#1270)
App Router popstate currently sends every history traversal through __VINEXT_RSC_NAVIGATE__, even when the target pathname and search already match the committed route and only the hash entry changed. That violates the hash-only traversal invariant and replays cached RSC payloads unnecessarily.\n\nCompare popstate targets against the committed App Router pathname and search before dispatching an RSC traversal. Same-route entries now restore scroll directly, while real route traversals keep the existing pending-navigation coordination.\n\nAdd an App Router E2E regression that wraps __VINEXT_RSC_NAVIGATE__ and verifies hash back/forward traversal does not call it. Co-authored-by: James <james@eli.cx> |
||
|
|
be9f338306 |
fix(router): derive shallow dynamic params from the URL (#1272)
* fix(router): derive shallow dynamic params from the URL Pages Router shallow navigation across one dynamic route template updated the browser URL but kept router.query params from the original __NEXT_DATA__ payload. That left client state stale after transitions such as /posts/42 to /posts/43. The router now matches the current pathname against the hydrated route pattern before falling back to __NEXT_DATA__.query. This preserves shallow data-fetch skipping while keeping dynamic params aligned with the visible URL. Tests cover the router shim boundary and the browser-visible Pages Router shallow transition. * refactor(router): reuse route-pattern matching for shallow params The shallow dynamic params fix originally duplicated route-pattern parsing inside the router shim. Reuse the existing routePatternParts and matchRoutePattern helpers instead, keeping the __NEXT_DATA__ fallback behavior unchanged. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
deef64470b |
fix(app-router): keep refresh transitions pending (#1269)
router.refresh() currently skips the pending browser router state even when it is dispatched from a React transition. That lets useTransition return to idle while the refresh RSC request is still in flight. The refresh path should follow the same App Router invariant as other programmatic navigations: publish a deferred router-state promise before the request and resolve it when the refreshed tree commits. Allow programmatic refresh navigations to create pending router state, and cover the behaviour with a browser regression that observes isPending during a delayed same-route refresh. |
||
|
|
7d1f482050 |
fix(link): preserve unsafe href click handlers (#1264)
Vinext strips dangerous Link href values before rendering the anchor. That keeps javascript: URLs inert, but the unsafe render branch also dropped the developer-provided onClick handler, so links used as click targets stopped working after hydration. The dangerous-href branch now preserves the anchor ref, link status provider, and user click handler while continuing to omit href. The regression fixture verifies that the click handler runs and the browser URL stays unchanged. |