Commit Graph

17 Commits

Author SHA1 Message Date
Divanshu Chauhan (divkix) b25be5e167 fix(routing): correct (.) interception target for nested slot subdirectories (#1751)
* fix(routing): correct (.) interception target for nested slot subdirectories

When the (.) interception marker lives inside a subdirectory of the @slot
dir (e.g. app/@modal/sub/(.)target/[id]/page.tsx), baseParts was computed
from routeDir (the slot owner) instead of the directory containing the
marker. This caused visible subdirectory segments to be dropped, producing
/target/:id instead of /sub/target/:id.

Fix: derive baseParts from path.dirname(interceptRoot) relative to appDir,
stripping invisible segments via convertSegmentsToRouteParts — the same
pattern used by computeInterceptSourceMatchPattern and the (..) / (...)
convention branches.

* ci: retrigger to clear flaky test failures

* fix(routing): drop double-conversion of (.) interception baseParts to preserve dynamic ancestor params
2026-06-05 23:26:18 +01:00
James Anderson 7bb669c0e9 test(routing): regression for parallel slot page-over-default priority (#1339) (#1689)
Issue #1339 reported parallel route slots falling back to default.tsx
instead of the matched page. That gap was already closed on main by
#1389 (resolve nested slot pages over default.tsx) and #1659/#1535
(treat @children as transparent). This adds a regression test mirroring
the Next.js parallel-routes-layouts fixture — the canonical
"Hello from Nested" vs "default page" case — asserting that:

- the children slot at /nested resolves nested/page.tsx, not
  nested/default.tsx;
- sibling @foo/@bar slots that each own both a page and a default pick
  their page; and
- at /nested/subroute (only @bar matches) the children slot falls back
  to default, @bar mirrors its subroute page, and @foo keeps its default.

Locks in page-over-default priority across sibling slots so the
behavior cannot silently regress.
2026-05-31 09:29:30 +01:00
James Anderson c1a6adc19a fix(routing): treat @children as transparent so explicit page wins over catchall (#1535) (#1659)
In Next.js, the `@children` directory is special: it provides the
layout's `children` prop rather than acting as a parallel slot.
`app/foo/@children/page.tsx` should therefore register a real page
route at `/foo` and take priority over a sibling `[...catchAll]`
catch-all. Previously vinext excluded all `@`-prefixed directories
from the page scan, so `@children/page.tsx` was hidden and `/` (or
`/nested`) was either served by the catch-all or failed to resolve
when no `default.tsx` existed.

Three small changes restore Next.js parity:

- Let `@children` directories through the page-scan exclusion so the
  page file at `@children/page.tsx` registers as a route.
- Strip a trailing `@children` segment in `fileToAppRoute` so the
  route is anchored at the parent directory — sibling `@slot`
  directories remain owned (not inherited) and layout/boundary
  discovery is sourced from the parent.
- Skip `@children` in `discoverParallelSlots` and
  `hasParallelSlotDirectory` so it never appears in
  `parallelSlots` (matching Next.js' types-plugin behaviour).

Ported from Next.js: test/e2e/app-dir/parallel-routes-catchall-children-slot/
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-catchall-children-slot/parallel-routes-catchall-children-slot.test.ts

References Next.js' `normalizeAppPath` and `normalize-catchall-routes`
which both treat `@children` as a non-matchable slot:
- packages/next/src/shared/lib/router/utils/app-paths.ts
- packages/next/src/build/normalize-catchall-routes.ts
- packages/next/src/build/webpack/plugins/next-types-plugin/index.ts

Fixes #1535.
2026-05-28 20:54:32 +01:00
James Anderson f61de5d39a fix(parallel-routes): resolve nested slot pages over default.tsx (#1389)
* fix(parallel-routes): resolve nested slot pages over default.tsx

When two sibling route groups occupy the same URL pattern (e.g.
`(group-a)/layout.tsx` next to `(group-b)/page.tsx`), the layout-only
sibling was dropped entirely because its pattern collided with the page
route, taking its parallel slots with it. Nested pages inside its
`@slot` directories were never materialized, so URLs that only the
catch-all slot could handle returned 404 / not-found instead of
rendering the matched slot page.

This change keeps the dropped layout-only routes as "ghost parents"
that participate in `discoverSlotSubRoutes`. Their slot sub-pages still
mint synthetic sub-routes (carrying the ghost's layout chain and slot
metadata), but the ghost itself is not added to the route table to
preserve the existing single-route-per-pattern invariant.

Closes #1339

Ported from Next.js tests:
- test/e2e/app-dir/parallel-routes-catchall-groups

* test(parallel-routes): cover shared-root URL of sibling route groups

Adds an e2e assertion that /parallel-group-catchall (the URL shared by
both (group-a) and (group-b)) renders (group-b)/page.tsx, not the
(group-a) ghost or its catch-all slot. Closes the coverage gap called
out in code review for the existing /foo and /bar tests.
2026-05-21 10:30:34 +01:00
James Anderson 086c6e8b52 fix(app-router): parse interception route markers in route scanner (#1398)
* fix(app-router): parse interception route markers in route scanner

Refs #1364

* test(app-router): cover (.) and (...) markers, fix JSDoc typo

Addresses Bonk review feedback on #1398: add explicit coverage for the
remaining two interception marker shapes when used outside `@slot`, and
remove the stray quote in `isInterceptionMarkerDir`'s JSDoc.
2026-05-21 10:29:02 +01:00
JamesbbBriz 7075d210e7 feat(cli): native App Router route type generation (#1144)
* Add native App Router route typegen

* Address route typegen review feedback

* Respect typegen target directory

* Require explicit LayoutProps route

* Serialize route type regeneration

* Derive layout params from route graph

* Scope layout slot typegen by route group

* Map typegen slots to owner layouts

* Preserve slot-local layout type keys

* Generate next-env.d.ts during typegen

* Address review feedback for route typegen

* Defer route sort and tighten typegen helpers

* Stabilize Windows create-next-app smoke check

* Address route typegen review fixes

* Match Next route type import

* Stabilize Windows create-next-app smoke

* Skip redundant route param shape writes

* Defer dev route type generation
2026-05-20 10:45:51 +01:00
Nathan Nguyen e0eabc122b feat(router): add manifest-backed interception topology (#1314)
* feat(router): add manifest-backed interception topology

* test(router): cover concrete dynamic interception proof ids

* feat(router): index interception facts by slot
2026-05-19 13:15:31 +01:00
James Anderson 0a77ebd46f fix(router): gate intercept matching on Next-URL source pattern (#1302)
Vinext's app-rsc route matcher previously fired interception on any RSC
request whose URL matched an intercept target, regardless of the source
pathname. Next.js implements interception as a rewrite that is only
activated when the Next-URL header matches the *intercepting route*
regex (`^<interceptingRoute>(?:/.*)?$`). Without that gate, a direct
`.rsc` fetch to an intercept target would incorrectly render the modal
page instead of the underlying route, and unrelated sources could pull
in modals they have no slot for.

This change:

1. Captures the *intercepting route* URL pattern (`sourceMatchPattern`)
   alongside each `InterceptingRoute` at scan time. The pattern is the
   slot owner's path with route groups and `@slot` segments stripped,
   matching Next.js' `extractInterceptionRouteInformation`. The value
   is propagated through the App RSC manifest.

2. Adds source-pathname filtering to `createAppRscRouteMatcher`'s
   `findIntercept`: an entry only matches when the provided source
   pathname satisfies its `sourceMatchPattern` (with descendants
   allowed). When no source pathname is provided (no Next-URL /
   X-Vinext-Interception-Context header), no intercept fires.

3. Adds unit coverage in `app-rsc-route-matching.test.ts` and
   `app-route-graph.test.ts` for the new behavior, including the
   patterns used by the failing Next.js deploy suite cases
   (`parallel-routes-and-interception-basepath`,
   `parallel-routes-and-interception-from-root`, multi-catchall, and
   two-levels-up).

4. Updates `app-router.test.ts` integration tests that previously
   relied on the buggy behavior; intercept fires only when the test
   sends `X-Vinext-Interception-Context` and that context matches the
   slot's intercepting-route prefix.

Cross-references:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/interception-routes.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/generate-interception-routes-rewrites.ts

PR #1249 is orthogonal: it carries proof of interception through the
planner/lifecycle when the matcher has already fired. This change
restricts when the matcher fires in the first place.
2026-05-18 11:55:31 +01:00
Nathan Nguyen a0737e971b feat(router): expose RouteManifest default and segment boundary facts (#1136)
* feat(router): expose RouteManifest default and segment boundary facts

RouteManifest slot facts only exposed whether a default existed, and boundary facts were limited to layout-aligned outcomes. That left later #726 planner work without graph-owned IDs for default-rendering slot bindings or layoutless route-group error segments.

Add explicit default entries keyed by semantic slot ID, thread default IDs through slot declarations and default-rendering bindings, and materialize segment-level error boundaries even when no sibling layout owns that segment.

Validation: vp test run tests/app-route-graph.test.ts; vp test run tests/app-route-graph.test.ts tests/routing.test.ts; vp check packages/vinext/src/routing/app-route-graph.ts tests/app-route-graph.test.ts; vp run vinext#build.

* ci: rerun flaked checks

* ci: rerun flaked checks again

* fix(router): avoid duplicate RouteManifest error boundary writes

RouteManifest now exposes layoutless segment error boundaries, but the segment pass also revisited layout-bearing error.tsx files that the layout pass had already written.

Skip segment error entries with an owning layout so layout-owned boundaries remain single-sourced, while layoutless route-group boundaries still materialize as ownerless facts.

Validation: vp test run tests/app-route-graph.test.ts; vp check packages/vinext/src/routing/app-route-graph.ts tests/app-route-graph.test.ts.
2026-05-11 08:21:22 +01:00
Nathan Nguyen 7b3fc49b92 fix(app-router): honor route group error boundaries (#1124) 2026-05-07 11:22:30 +01:00
Nathan Nguyen 4932b69b17 feat(router): expose RouteManifest semantic facts (#1113)
* feat(router): expose RouteManifest semantic facts

RouteManifest only exposed minimal route, layout, template, and slot lookup maps. That left later #726 planner and cache-proof work without stable graph-owned facts for slot ownership, slot defaults, template reset scope, or boundary outcomes.

The graph builder now derives declaration-level slot facts, per-route slot bindings, template remount facts, and layout-boundary outcome facts from existing App Router metadata. The manifest graphVersion includes the new maps so downstream compatibility checks can detect semantic topology changes.

Tests cover route groups, slots, defaults, templates, and boundaries in one fixture.

* test(router): align CI fixtures with graph facts

Full CI checks more App Router fixtures than the targeted RouteManifest test run. The new ParallelSlot declaration fields must be present in hand-built fixtures, otherwise typecheck fails even though production graph scanning populates them.

The app browser trace tests also still asserted the stale reason at entry zero, but no-commit transaction tracing is prepended before the reason entry. Update those assertions to inspect the stale reason entry without changing runtime behavior.
2026-05-07 09:15:43 +01:00
Nathan Nguyen ebbf72cc8d feat(router): expose route graph manifest read model (#1089)
* feat(router): expose route graph manifest read model

The App Router graph now exposes root-boundary identity and a stable RouteManifest read model instead of leaving these facts implicit inside per-route arrays. Future #726 planner and compatibility work needs a semantic lookup surface for routes, layouts, pages, templates, slots, and root boundaries before it can stop reading meaning from transport keys.

This adds RootBoundaryId, graphVersion, StaticSegmentGraph maps, and fail-fast invariant checks for mismatched layout/template positions while preserving the existing routes array for current callers.

* chore: retrigger ci
2026-05-06 17:44:20 +01:00
Nathan Nguyen c1a4c8da1a fix(routing): accept any non-] chars in dynamic segment names (Next.js parity) (#1054)
Next.js PARAMETER_PATTERN accepts any non-] characters inside brackets:
  [repo:name], [variant.id], [user@domain]

vinext was restricting param names to [\w-]+, silently treating segments
with dots, colons, or @ as literal URLs. This fix aligns the regexes with
Next.js:

- app-route-graph.ts: ^\[([^\]]+)\]$ (was [\w-]+)
- pages-router.ts: same
- route-validation.ts: patternToNextFormat now backtracks over +/* suffixes
- shims/router.ts: extractRouteParamNames now accepts dotted/colon names

Also corrects a misleading comment in routePrecedence: dynamic routes CAN
score negative, but the trie matcher (route-trie.ts) always checks static
children before dynamic children at each node, so purely-static routes still
win at request time.

Next.js reference:
https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-dynamic-param.ts#L175

Closes #7
2026-05-05 19:21:40 +01:00
Nathan Nguyen 834ccc6ce7 feat(app-router): mint semantic route graph ids (#1060)
App Router route graph entries previously left route, page, layout, template, and slot identity to later render and wire layers. That kept topology meaning coupled to reconstructed payload keys instead of a graph-owned fact.

Mint stable semantic IDs during graph construction, preserve them in generated RSC route metadata, and cover route-group, dynamic-segment, slot-owner, and filesystem-root stability cases.
2026-05-05 19:08:00 +01:00
Nathan Nguyen 68c90808e0 fix(routing): discover nested parallel slot sub-routes from layout-only parents (#1051)
* fix(routing): discover nested parallel slot sub-routes from layout-only parents

Nested parallel routes failed when the parent segment had a layout but no
page.tsx. discoverSlotSubRoutes skipped any parent route without pagePath,
so slots like @parallelB/nested/page.tsx under home/layout.tsx (but no
home/page.tsx) were never discovered.

Next.js explicitly tests this shape in parallel-routes-and-interception:
app/parallel-nested/home/layout.tsx + @parallelB/default.tsx +
@parallelB/nested/page.tsx creates a valid /parallel-nested/home/nested
route.

The fix makes three targeted changes to discoverSlotSubRoutes:

1. Remove the hard pagePath requirement. Layout-only routes with parallel
   slots can still own nested slot sub-routes.

2. Compute parentPageDir from the innermost layout when pagePath is null,
   so slot ownership checks still work for layout-only routes.

3. Relax the children default.tsx requirement, but only for layout-only
   parent routes. When a parent HAS a children page, default.tsx is still
   required as a fallback for the synthetic sub-route. Layout-only parents
   do not need this fallback because the children slot was never occupied.

4. Add structural-conflict detection before creating synthetic routes. This
   prevents layout-only root routes from generating synthetic routes that
   collide with existing page routes differing only by param name (e.g.
   /shop/:id vs /shop/:name), which validateRoutePatterns rejects.

Tests ported from the Next.js parallel-routes-and-interception suite cover
both route-discovery and end-to-end rendering.

Refs: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts

* fix(routing): correct patternsStructurallyEquivalent tree-node comparison

The initial implementation of patternsStructurallyEquivalent used a naive
dynamic-segment check (startsWith(':')) that produced two categories of
false positives:

1. Literal vs dynamic segments (e.g. 'about' vs ':id') were considered
   equivalent, incorrectly suppressing valid synthetic routes.

2. Single dynamic vs catch-all segments (e.g. ':id' vs ':slug+') were
   considered equivalent, even though they map to different validator tree
   nodes ([] vs [...]).

Replace the ad-hoc boolean logic with segmentTreeNodeType, which classifies
each segment by its Next.js validator tree node (literal, dynamic, catchAll,
optionalCatchAll). Two patterns are structurally equivalent only when every
segment maps to the same tree node type.

Also add a clarifying comment to the parentPageDir computation so the next
developer understands why the innermost layout proxies the route directory
for layout-only routes.

* fix(routing): scan synthetic routes in structural conflict check

Copilot review identified that patternsStructurallyEquivalent only scanned
the original routes array, missing conflicts between synthetic routes created
earlier in the same loop. Fixed by scanning routesByPattern.values() which
includes both original and synthetic routes.

Also adds a TODO comment documenting the known prerender limitation for
layout-only synthetic routes (skipped at build/prerender.ts:849). These
routes work in dev/SSR but won't be statically exported — this is
pre-existing behaviour for all layout-only routes.

* test(routing): cover structural conflict skip for synthetic parallel routes

Adds a unit test proving that discoverSlotSubRoutes skips synthetic routes
when an existing page route has the same structural shape but a different
param name (e.g. /shop/:id page + @feed/[name]/page.tsx slot).

Without this coverage the structural-conflict safeguard is easy to regress.

* fix(routing): guard discoverSlotSubRoutes to layout-only UI routes

Review identified that removing the old !parentRoute.pagePath guard also
let route-handler-only AppRoutes enter discoverSlotSubRoutes. Route
handlers have pagePath: null but are not layout-only UI routes. A shape
like app/api/route.ts + app/@feed/foo/page.tsx could make the /api route
handler scan the ancestor slot and materialise a synthetic UI route under
/api/foo.

Add an explicit isLayoutOnlyUiRoute check:
  !parentRoute.pagePath && !parentRoute.routePath && parentRoute.layouts.length > 0

Only page-bearing routes or layout-only UI routes can own nested parallel-
slot sub-routes. Route handlers and layout-less routes are skipped.

Also adds a regression test: a route.ts handler with an ancestor parallel
slot must not emit synthetic routes under the handler pattern.

* fix(routing): scan synthetic routes in structural conflict check + regression test

The structural-conflict guard had regressed back to scanning the original
routes array, missing conflicts against synthetic routes created earlier in
the same discoverSlotSubRoutes pass. Fixed by scanning
Array.from(routesByPattern.values()) instead.

Also adds a regression test proving that two slot sub-pages with different
param names under the same parent do not both materialise (e.g. @a/[id] and
@b/[name] under /shop — only one of /shop/:id or /shop/:name is emitted).
2026-05-05 18:30:50 +01:00
James Anderson 2cf21433cb fix(routing): mirror inherited parallel slots to descendant sub-pages (#1039)
* fix(routing): mirror inherited parallel slots to descendant sub-pages

discoverInheritedParallelSlots was hard-coding pagePath to null for any
ancestor slot, so inherited slots only ever rendered default.tsx — even
when an obvious mirror like @breadcrumbs/about/page.tsx existed for the
matching route. Track each ancestor's segment index, then look up a
mirror via either a literal filesystem match or, when route groups appear
on either side, by URL-parts comparison through convertSegmentsToRouteParts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(routing): extract slot-specific params for inherited mirrors

When an inherited parallel slot's mirrored sub-page declares dynamic
markers under different names than the route's (e.g. slot uses
@breadcrumbs/distinct/[name] for a route at distinct/[id]), the runtime
was passing the route's matchedParams through unchanged — so the slot
page received {id} instead of {name} and rendered with an empty value.

Three pieces:

* `findMirroredSlotPage` now also accepts pattern-compatible matches and
  returns the slot's URL parts and param names. Specificity scoring
  picks the most-specific compatible sub-page (literal > dynamic >
  catch-all).
* Each ParallelSlot carries optional `slotPatternParts` and
  `slotParamNames`, serialized through the manifest.
* `buildPageElements` extracts a per-request `slotOverrides[key].params`
  by matching the request URL against `slotPatternParts` whenever a
  slot's param names aren't a subset of the route's. `pageModule` on
  `AppPageSlotOverride` becomes optional so a params-only override
  doesn't have to swap the slot's page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(routing): address bonk review on inherited parallel slot mirror

* Swap inverted scoring for required vs optional catch-alls — `:name+`
  (matches ≥1 segments) is more constrained than `:name*` (≥0), so it
  now scores higher, preventing a less-specific optional catch-all from
  shadowing a required one when both compete for the same route.
* Tighten findMirroredSlotPage docstring: the implementation collapses
  exact URL-parts equality and pattern compatibility into one scored
  loop, where literals naturally win. Rewrite the comment to reflect
  what the code actually does.
* Document patternsCompatible's conservative rejection of literal slot
  segments paired with dynamic route segments (and the consequence that
  a literal slot sub-page never matches a catch-all route).
* Note in buildSlotOverrides why an empty slotParamNames also skips the
  override (no dynamic markers means nothing to extract).
* Add unit tests for two cases that were only covered indirectly:
  graph-level slotPatternParts/slotParamNames when route and slot use
  different param names, and a slot owned at an intermediate ancestor
  rather than appDir.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:02:20 +01:00
Nathan Nguyen c33f96a904 refactor(routing): extract App Router route graph builder (#998)
App Router route discovery currently shares a single module with route caching and request-time matching. That makes discovery changes harder to review and increases the chance that graph construction work collides with generated entry changes.

The route graph construction types and discovery helpers now live in app-route-graph.ts, while app-router.ts remains the public cache and trie matching wrapper. A direct graph-builder test covers pages, route handlers, layouts, and inherited parallel slots at the new boundary.
2026-05-02 20:03:50 +01:00