Commit Graph

38 Commits

Author SHA1 Message Date
James Anderson 8b122744ef fix(pages): align middleware data prefetch caching (#2451)
* fix(pages): align middleware data prefetch caching

* fix(pages): guard middleware prefetch data kind

* fix(dev): lazily classify pages data kind

* fix(pages): preserve legacy prefetch fallback

* fix(pages): reject unknown prefetch data kind
2026-07-02 21:33:32 +01:00
James Anderson 5343a3ff32 fix(app-router): cache segment prefetches by rendered search params (#2477)
* fix(app-router): cache segment prefetches by rendered search params

* fix(app-router): protect rendered prefetch alias header

* fix(app-router): repair segment prefetch merge
2026-07-02 19:42:46 +00:00
James Anderson 239595e69e fix(app-router): reuse metadata rewrite prefetches (#2455)
* fix(app-router): reuse metadata rewrite prefetches

* fix(app-router): normalize rsc reference validation ids

* Revert "fix(app-router): normalize rsc reference validation ids"

This reverts commit bbff1eb13a.

* chore: format link shim
2026-07-01 17:02:35 +00:00
James Anderson ebf8e59289 fix(app-router): avoid runtime route-tree prefetch sizing (#2467)
* fix(app-router): start optimistic prefetches immediately

* fix(app-router): support route tree prefetch inlining

* fix(app-router): dedupe max prefetch inlining requests

* fix(app-router): cap route-tree prefetch size reads

* fix(app-router): avoid runtime route-tree prefetch sizing

* Fixed CI type error, flagged parity gap

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

* fix(app-router): preserve negative prefetch inlining thresholds

* fix(app-router): normalize rsc reference validation ids

* Revert "fix(app-router): normalize rsc reference validation ids"

This reverts commit 68b96b596c.

* fix(app-router): keep route-tree prefetch metadata scoped

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
2026-07-01 17:47:08 +01:00
James Anderson 03f02177ec fix(app-router): start optimistic prefetches immediately (#2440)
* fix(app-router): start optimistic prefetches immediately

* fix(app-router): normalize rsc reference validation ids

* Revert "fix(app-router): normalize rsc reference validation ids"

This reverts commit 34a37658c2.
2026-07-01 16:45:35 +00:00
James Anderson b0367f1448 fix(link): avoid reusing dynamic app route prefetches (#2450)
* fix(link): avoid reusing dynamic app route prefetches

* fix(link): preserve static dynamic app prefetches

* test(e2e): scope segment cache client params to prod
2026-07-01 11:14:11 +01:00
James Anderson bc1dfce00f fix(app-router): evict segment prefetches under memory pressure (#2466)
* fix(app-router): evict segment prefetches under memory pressure

* fix(app-router): hold prefetch queue slots until body read
2026-07-01 10:54:47 +01:00
James Anderson 49945a9e05 fix(link): reuse full prefetch loading shells (#2332)
* fix(link): reuse full prefetch loading shells

* fix(link): share loading shell prefetch ownership
2026-06-30 19:34:50 +01:00
James Anderson 31e87439df fix(pages-router): retain prefetched SSG data (#2366)
* fix(pages-router): retain prefetched SSG data

* fix(pages-router): align SSG export detection

* fix(pages-router): skip data prefetch for non-SSG routes

* fix(pages-router): align data navigation with Next.js

* chore: avoid exporting middleware matcher helper type

* fix(pages-router): preserve client navigation effects

* fix(pages-router): preserve query rewrite history timing

* fix(pages-router): keep data fetch startup synchronous

* fix(pages-router): keep superseded data fetches alive

* fix(pages-router): skip nonmatching redirect matcher awaits

* fix(pages-router): reuse middleware SSR prefetch data

* Fix pages data prefetch CI regressions

* fix(pages-router): evict middleware prefetch cache
2026-06-29 14:52:35 +01:00
James Anderson ad14e9b039 fix(app-router): avoid repeated hover prefetches (#2396)
* fix(app-router): avoid repeated hover prefetches

* fix(app-router): floor prefetch fallback stale time
2026-06-28 23:24:30 +01:00
James Anderson f0ba7b4afe fix(app-router): reuse committed client cache payloads (#2251)
* fix(app-router): seed client navigation cache

* fix(cache): parse prerender cache metadata marker

* test(cache): cover prerender metadata done marker

* fix(cache): preserve static committed stale time

* fix(link): honor dynamic stale time for auto prefetches

* fix(router): retain committed client cache state

* fix(router): publish client cache after commit

* fix(router): gate hydration cache publication

* fix(router): guard late client cache publication

* fix(router): isolate client cache parity coverage

* fix(router): preserve authoritative navigation commits

* fix(router): preserve committed navigation streams
2026-06-28 22:01:58 +00:00
James Anderson da086c0d65 Revert flaky "fix(link): start viewport prefetches immediately" (#2386)
This reverts commit 06775d3694.
2026-06-27 00:23:40 +01:00
James Anderson 06775d3694 fix(link): start viewport prefetches immediately (#2229)
* fix(link): start viewport prefetches immediately

* fix(navigation): skip client prefetches for bots
2026-06-26 20:38:28 +00:00
James Anderson feb27e8422 fix(client): isolate Pages Router module graph (#2380)
* fix(client): isolate Pages Router module graph

* fix(client): trim hybrid route ownership graph

* fix(router): preserve document-only hard navigation
2026-06-26 20:52:13 +01:00
James Anderson c5bd7b33c2 fix(app-router): skip prefetches for bots (#2323)
* fix(app-router): skip prefetches for bots

* fix(link): scope bot suppression to app prefetch

* fix(app-router): skip form prefetches for bots
2026-06-26 19:59:29 +01:00
Alexander Lichter d6afa2e874 perf(link): omit Pages runtime in app-only builds (#2225)
* perf(link): omit Pages runtime in app-only builds

* fix(link): use document fallback in app-only builds
2026-06-22 14:28:10 +01:00
James Anderson b91d5d1396 fix(pages): interpolate same-segment rewrite params (#2030)
* fix(pages): interpolate same-segment rewrite params

* test(pages): fix same-segment navigation CI
2026-06-15 21:49:28 +00:00
James Anderson b50af3e39d fix(link): preserve basePath hash navigation (#1952)
* fix(link): preserve relative hash navigation with basePath

* fix(link): remove stale relative href resolution
2026-06-13 00:15:49 +01:00
Nathan Nguyen fd1023321b fix(router): support experimental gesture push (#1909)
* fix(router): support experimental gesture push

Vinext did not expose useRouter().experimental_gesturePush when Next.js apps enabled experimental.gestureTransition. Upstream gesture-transition fixtures call the method inside a held async transition, so the target route never became visible before the canonical router.push completed.

The fix resolves the config flag into the same __NEXT_GESTURE_TRANSITION define used by Next.js, exposes the optional router method only under that flag, and lets gesture navigation use the existing RSC navigation pipeline with an immediate approved visible commit. The ported e2e fixture preserves the upstream held-transition flow and target/static/dynamic assertions.

* test(router): update navigate mock assertions to expect default transition commit mode

* fix(router): address ask-bonk review comments on gesture push

- track scheduled app router navigation in experimental_gesturePush for
  parity with push/replace, so a refresh() issued in the same task skips
  its redundant re-fetch
- document the synchronous visible-commit mode's null pendingRouterState
  invariant in the browser navigation controller
- add a unit test asserting experimental_gesturePush is only attached
  when the __NEXT_GESTURE_TRANSITION define is set

* style(tests): fix formatting in shims.test.ts

* docs(router): clarify gesture push normalization and flushSync commit paths

* test(router): fix unbound-method lint in shims test and document gesture spec coverage

* docs(router): clarify gesture define boolean form and spec held-state rationale

* docs(test): note gestureTransition is fixture-wide for the gesture spec only

* fix(router): no-op experimental_gesturePush before the navigation runtime is ready

Upstream gesturePush early-returns when getCurrentAppRouterState() is null,
so a gesture dispatched before hydration is a no-op. Previously vinext fell
through navigateClientSide's non-runtime branch and performed a real history
push. Guard on the runtime's navigate function (the same readiness check
navigateClientSide uses) and document that the un-awaited
void navigateClientSide(...) deliberately matches push/replace's
fire-and-forget rejection shape.

* docs(router): note gesturePush guard/dispatch runtime reads have no TOCTOU window

* docs(test): document gesture fixture's double-navigation divergence from upstream

---------

Co-authored-by: James <james@eli.cx>
2026-06-12 03:14:24 +01:00
Nathan Nguyen cb27f3a682 fix(link): full-prefetch dynamic routes without loading shells (#1734)
* fix(link): full-prefetch dynamic routes without loading shells

Automatic App Router prefetch skipped dynamic routes unless they exposed a loading boundary. That diverged from Next.js segment-cache behavior for client-param routes, where making the link visible must initiate a prefetch and a click to the exact URL can reuse the prefetched payload without another request.

The bad assumption was that every dynamic route needs shell-only learning or no prefetch. Vinext keys the prefetched RSC snapshot by the exact target URL, so dynamic routes without a loading shell can safely seed navigation cache while dynamic routes with loading.tsx keep the existing shell-only path.

Port the focused Next.js client-params regression into the Link decision and scheduling tests.

* fix(link): skip shell-first inlining for client-param prefetches

Automatic dynamic routes without loading shells now seed the navigation cache, but the prefetch-inlining branch still treated those cacheable requests like static route payloads and issued a loading-shell request before the full fetch.

That shell request has no loading boundary to learn from for client-param routes, so it is a wasted roundtrip. Split cache eligibility from shell-first transport eligibility, document the route-shape heuristic, and derive the test-only full-prefetch helper from the same decision.

Add coverage that dynamic no-loading routes remain single-fetch under prefetch inlining.
2026-06-05 11:25:17 +01:00
Nathan Nguyen 610cf2e63a fix(app-router): honor per-response dynamic stale times on the client (#1712)
* fix(app-router): honor per-response dynamic stale times on the client

App Router navigation reused and evicted cached RSC responses using one global prefetch TTL, so pages with different unstable_dynamicStaleTime values could not stay fresh independently. Traversal also needed a route instance boundary so browser history restores hidden state instead of reusing a forward-navigation component instance.

The client cache now snapshots dynamic stale-time response metadata, computes expiry per entry, preserves staleAt across cache replays, and derives mounted-slot cache variants from the decoded payload. Browser traversal keeps route instances under React Activity while normal navigations allocate fresh route instances, matching Next's BFCache/stale-time contract.

* fix(link): make prefetch dedupe freshness-aware for navigation prefetches

The old prefetched.has(cacheKey) gate in prefetchUrl returned early for
any exact key match, including stale entries. This meant an expired
prefetch with the same cache key would never reach
hasPrefetchCacheEntryForNavigation, so it would never be deleted and
never refreshed.

The new helper is built to check resolvePrefetchCacheEntryExpiresAt,
delete stale entries, and return false. But the Link path bypassed it
when the old prefetched Set contained the exact key.

Fix: gate the exact-key check on the helper's freshness decision. When
the helper reports the exact entry is stale and deletes it, also remove
the key from the prefetched Set so the fall-through path can issue a
fresh fetch. Keep the second helper call for equivalent _rsc variants.

Add an integration test proving visible Links re-prefetch when their
exact cache entry goes stale, not just that the helper reports stale
entries as unavailable.

* refactor(app-router): collapse redundant prefetch/visited-cache gates

Address review feedback on the client dynamic-stale-time changes:

- link.tsx: the prefetch dedupe path called
  hasPrefetchCacheEntryForNavigation twice when an exact entry already
  existed. Collapse to a single freshness-aware gate that covers both the
  exact entry and equivalent _rsc variants.
- app-browser-entry.ts: drop the redundant early refresh return in
  getVisitedResponse; isVisitedResponseCacheEntryFresh already bypasses
  the cache for refresh navigations (and the miss path evicts the entry).

* docs(app-router): document intentional refresh eviction in visited cache

Per review feedback: getVisitedResponse now routes refresh navigations
through isVisitedResponseCacheEntryFresh (the single source of truth),
which returns false for refresh and therefore falls through to the read
eviction. Document at the fall-through delete that a refresh intentionally
drops any prior snapshot, since the navigation re-fetches and re-stores a
fresh one.

* docs(navigation): clarify load-bearing prefetch-cache dual checks

Per review feedback, document two checks that look superficially redundant
but preserve observable contracts:

- isPrefetchCacheEntryCompatibleWithMountedSlots: the two clauses accept
  either the entry's pinned request-time slot context or the server-declared
  snapshot header; they diverge in the prefetchRscResponse case.
- consumePrefetchResponse expiresAt guard: only synthesize expiresAt onto the
  returned snapshot when the entry already carried one, so entries without an
  explicit expiry round-trip unchanged for callers/tests.

---------

Co-authored-by: James <james@eli.cx>
2026-06-04 16:01:43 +01:00
Nathan Nguyen 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
2026-06-01 12:37:27 +01:00
Nathan Nguyen 66519412c3 fix(app-router): reuse pending prefetched RSC payloads (#1704) 2026-05-31 20:08:13 +01:00
James Anderson 6d4dd949f6 test(link): cover Pages Router onClick/preventDefault semantics (#1470) (#1685)
Adds regression coverage mirroring Next.js's
`test/e2e/new-link-behavior/index.test.ts` so the Pages Router `<Link>`
shim is guarded against ever regressing on:

  1. `onClick` firing on click with the synthetic event
  2. `event.preventDefault()` inside `onClick` cancelling the Link's
     own client-side navigation (no router.push, no history mutation,
     no popstate dispatch)

The unit-level invariants in the current Link shim match Next.js's
`linkClicked` ordering — call user onClick, check defaultPrevented,
only navigate if not prevented — so these tests pass against the
current implementation and lock that behaviour in place.
2026-05-29 09:56:12 +01:00
James Anderson 30e374cd68 test(link): cover javascript: URL console.error for App + Pages Router Link (#1622)
Fixes #1576. PR #1575 hoisted the dangerous-scheme guard out of the async
`performNavigation` body for Pages Router `router.push`/`router.replace`, which
covers two of the six Next.js `test/e2e/app-dir/javascript-urls/javascript-urls.test.ts`
scenarios. This adds regression coverage for the remaining four Link-flavoured
scenarios — App Router and Pages Router, both `href` and `as` — by toggling
`appNavigation` in `renderIsolatedLink` and swapping `href` <-> `as`. Each test
clicks the rendered anchor and asserts:

- `console.error` is emitted with a message containing "has blocked a
  javascript: URL as a security precaution." (the substring the Next.js E2E
  matcher asserts on).
- `navigate` / `fetch` are never invoked, so navigation does not proceed.
- A user-provided `onClick` still fires, so analytics or local UI state is
  preserved.

The Link shim's existing `handleDangerousClick` path (`shims/link.tsx`) and the
`assertSafeNavigationUrl` guard inside the app router push/replace already
emit the matching message; this PR locks that contract behind a test matrix
so a future Link refactor cannot regress it silently.

The two `router.push` / `router.replace` scenarios from the issue are already
covered by `tests/router-javascript-urls.test.ts:96-128` (App Router) and
`tests/router-javascript-urls.test.ts:180-302` (Pages Router, from #1575).

Ported from Next.js: test/e2e/app-dir/javascript-urls/javascript-urls.test.ts
2026-05-28 13:52:36 +01:00
Nathan Nguyen 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.
2026-05-28 12:33:05 +01:00
Nathan Nguyen 82b6388ece fix(app-router): render optimistic loading shells for dynamic navigation (#1421)
* fix(app-router): render optimistic loading shells for dynamic navigation

Dynamic App Router links currently skip automatic prefetch for route patterns with params, so a navigation to a sibling dynamic URL waits for fresh RSC data before committing anything. That diverges from Next.js, where automatic prefetch learns the loading shell for dynamic routes and the router can commit the shell immediately while the full payload is fetched.

The client prefetch path now requests a loading-shell render for dynamic app routes, keeps that entry out of the navigation cache, learns reusable optimistic route templates from settled shell prefetches, and commits a detached optimistic payload for matching dynamic navigations. The server emits shell-only RSC payloads under a separate cache variant so normal navigation data is not polluted.

Tests cover matching semantics, shell-only page wiring, cache key isolation, prefetch cache consumption, Link prefetch behavior, and the upstream Next.js optimistic-routing deploy-suite case.

* chore(app-router): address optimistic routing review
2026-05-22 10:07:26 +01:00
James Anderson 6e1669d76d fix: warn on blocked javascript: URL navigation (#1372)
Match Next.js's observable behaviour when `<Link>` clicks, `router.push`,
`router.replace`, and `router.prefetch` block a `javascript:` (or `data:`/
`vbscript:`) URL. Vinext already prevented navigation, but the
`console.error` that Next.js's E2E suite asserts on
(`test/e2e/app-dir/javascript-urls`) was missing because:

- App Router `router.push`/`replace`/`prefetch` threw synchronously
  inside `React.startTransition`, so the throw never reached React's
  event-handler error reporter.
- `<Link>` rendered an inert anchor with no `href`, so clicking did
  nothing — no throw, no log.
- Pages Router `next/router` had no `javascript:` guard at all and
  would fall through to `window.location.assign(...)`.

Centralise the matching message in `url-safety.ts` via a new
`reportBlockedDangerousNavigation()` helper. `assertSafeNavigationUrl`
now logs before throwing, and Link's dangerous-click branch and the
Pages Router `performNavigation` both invoke the guard so the throw +
console.error pair mirrors Next.js exactly.

Closes #1349

References:
- packages/next/src/client/components/app-router-instance.ts:345,402,442,460
- packages/next/src/client/components/segment-cache/navigation.ts:537
- packages/next/src/shared/lib/router/router.ts:1025,1057
2026-05-21 16:34:55 +01:00
James Anderson 7e060b98f7 feat(link): support onNavigate prop (#1371)
vinext already exposes `<Link onNavigate>` and the matching `NavigateEvent`
contract (`{ url, preventDefault(), defaultPrevented }`), but the Next.js
deploy suite still fails the `replace history state for external links
with replace prop` case: when `<Link>` points at a truly external URL and
has `replace` set, vinext falls through to the browser's default click
navigation, which pushes a new entry instead of replacing the current one.

This mirrors `linkClicked` in
`.nextjs-ref/packages/next/src/client/link.tsx` for the external + replace
branch: call `e.preventDefault()` and `window.location.replace(href)` so
the entry is replaced rather than pushed.

Adds focused unit tests around the onNavigate contract: onClick + onNavigate
fire for internal clicks, `preventDefault()` cancels the navigation, and
onNavigate is skipped for modifier-key clicks, target=_blank, download
links, and external URLs (with and without `replace`).

Closes #1357
2026-05-21 11:28:14 +01:00
Nathan Nguyen 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
2026-05-20 10:29:13 +01:00
Nathan Nguyen ff46514563 fix(link): preserve native URI scheme navigation (#1268)
* fix(link): preserve native URI scheme navigation

Link treated only http, https, and protocol-relative hrefs as browser-owned. Native schemes such as mailto:, tel:, and sms: were therefore locale-prefixed, eligible for prefetch normalization, and intercepted by client navigation.

The URL boundary now mirrors Next.js absolute URL scheme classification while preserving vinext's protocol-relative handling. Link rendering, click interception, relative resolution, and prefetch normalization all share that classification so non-local schemes stay with the browser while same-origin absolute URLs remain routable.

Adds focused regression coverage for native scheme rendering, click handling, and prefetch decisions.

* refactor(router): reuse shared URL scheme detection

Router and navigation shims already had local external URL predicates with the same browser-owned URL semantics now used by Link.

Reuse the shared url-utils predicate for Pages Router locale handling, Pages Router external detection, App Router external detection, and App Router prefetch normalization. Add focused shim coverage for native URI schemes through the Pages Router helpers.

* test(link): add hasAttribute stub to native URI scheme click event

The merge of main introduced a download-attribute check in handleClick that
runs before the native-URI early return, so test click events must provide
currentTarget.hasAttribute.

---------

Co-authored-by: James <james@eli.cx>
2026-05-16 19:14:26 +00:00
Nathan Nguyen 25ac4db7f9 fix(link): preserve native download clicks (#1262)
Link currently treats anchors with the HTML download attribute like normal same-origin SPA navigations. That prevents the browser from performing its native download behavior and can also run onNavigate for a click that should never become client-side navigation.

The click handler now bails out after user onClick and before preventDefault whenever the anchor has a download attribute, matching Next.js Link semantics. A focused regression test covers the default action, onNavigate, transition, and RSC navigate boundaries.
2026-05-16 19:12:30 +01:00
Nathan Nguyen 042ac438b0 fix(link): align visible and intent prefetching (#1258)
* fix(link): reschedule visible prefetches

Visible App Router links were prefetched as one-shot observer callbacks. That lost the mounted visibility state needed to re-prefetch after cache invalidation or router state changes, and automatic dynamic links could not be upgraded to a full prefetch on hover.

The Link shim now keeps mounted prefetch instances, tracks the visible set, exposes an internal ping hook for cache and router commits, and persists the full-prefetch strategy after unstable_dynamicOnHover intent.

Regression coverage verifies visible-link cache invalidation and dynamic-on-hover strategy upshift.

* fix(link): preserve pages intent prefetch

Pages Router Link prefetch=false should only disable viewport prefetch. The shared gate also blocked hover and touch intent prefetches, which matches App Router but diverges from the Pages Router contract.

Split intent eligibility by router mode so App Router keeps the stricter gate while Pages Router intent still prefetches in production. Pages viewport prefetch remains one-shot per mounted link to avoid duplicate document prefetch nodes under the visible-link registry.

Regression coverage verifies App Router prefetch=false intent suppression, Pages Router hover and touch intent prefetch, and Pages viewport re-entry dedupe.
2026-05-16 18:42:01 +01:00
James Anderson d9906b94d6 chore: add basic vinext site (#1236)
* chore: scaffold new next app with vinext

* basic app

* add to deploy workflow

* fmt
2026-05-15 13:41:10 +00:00
James Anderson f91d478d9f test(link-navigation): sequence link.js and importActual to fix flake (#1218)
The "clicking an RSC Link starts app-router navigation inside a React
transition" test ran link.js's dynamic import and vi.importActual("react")
in parallel via Promise.all. When importActual won the race, "react" got
cached as the actual module before the doMock factory ran, so link.tsx's
transitive `import React from "react"` resolved to the unmocked module.
That made React.startTransition the real implementation rather than the
spy, causing toHaveBeenCalledTimes(1) to occasionally see 0.

Sequencing the imports — load link.js first (with the doMock factory in
effect), then grab React via importActual — eliminates the race.

Refs: https://github.com/cloudflare/vinext/actions/runs/25878626751/job/76052351860
2026-05-14 19:01:55 +00:00
Nathan Nguyen 6cf4148ffd fix(link): keep auto prefetch from fetching dynamic app routes (#1177)
* fix(link): keep auto prefetch from fetching dynamic app routes

App Router Link treated null, undefined, and auto prefetch as the same full RSC prefetch used by prefetch={true}. That violates Next.js's public prefetch contract and over-fetches dynamic route payloads as soon as links enter the viewport.

The broken assumption was that prefetch only needed a boolean enabled state. Split Link prefetch into disabled, auto, and full modes, emit a small browser route manifest, and allow automatic full RSC prefetch only for matched static app routes while preserving explicit full prefetch.

Add focused tests for the mode mapping and generated route manifest.

* test(link): opt interception prefetch fixture into full mode

The interception-context E2E asserts that full RSC prefetch cache keys stay separate by source context. After auto prefetch stops full-fetching dynamic routes, that fixture needs to request the full prefetch mode explicitly.
2026-05-14 09:25:53 +01:00
Nathan Nguyen 2d6978f1f9 fix(link): align app-router prefetch scheduling with intent (#1149)
* fix(link): align app-router prefetch scheduling with intent

Link currently schedules viewport RSC prefetches outside production and does not promote user mouse or touch intent into an immediate prefetch. That diverges from Next.js Link behavior and can waste dev-server work during rapid iteration while missing an important production navigation warmup path.

The shim now gates automatic prefetching to production, shares same-origin prefetch normalization, and schedules high-priority RSC prefetches from mouse and touch intent while preserving user handlers. Focused Link navigation tests cover development suppression, intent prefetch, prefetch=false, and dangerous-link handler passthrough.

* fix(link): align app-router prefetch scheduling with intent

Link currently schedules viewport RSC prefetches outside production and does not promote user mouse or touch intent into an immediate prefetch. That diverges from Next.js Link behavior and can waste dev-server work during rapid iteration while missing an important production navigation warmup path.

The shim now gates automatic prefetching to production, shares same-origin prefetch normalization, and schedules high-priority RSC prefetches from mouse and touch intent while preserving user handlers. Focused Link navigation tests cover development suppression, intent prefetch, prefetch=false, and dangerous-link handler passthrough.

* test: tighten link prefetch coverage

* test(link): tighten prefetch decision coverage

* test(link): address prefetch review notes
2026-05-13 11:33:30 +01:00
Nathan Nguyen 108d913bfe fix(app-router): schedule link navigations as transitions (#1133)
* fix(app-router): schedule link navigations as transitions

App Router link clicks could commit cross-route payloads synchronously because the browser entry passed route path equality into the navigation controller as a transition toggle. That diverged from Next.js, where app-router push and replace enter React.startTransition for every app navigation.

Remove the transition override from normal navigation payload commits, keep synchronous commits private for HMR and same-URL server-action handling, and start RSC Link navigations through the same programmatic transition path used by useRouter().

* test(app-router): cover link transition scheduling behavior

* docs(navigation): drop stale isSameRoute references in comments

The isSameRoute concept was removed from app-browser-entry.ts in this PR;
update two comments in navigation.ts that still referenced it.

---------

Co-authored-by: James <james@eli.cx>
2026-05-08 21:43:08 +01:00