12 Commits

Author SHA1 Message Date
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