Commit Graph

29 Commits

Author SHA1 Message Date
James Anderson 866c71809e fix(pages): preserve rewritten URLs during navigation (#2028)
* fix(router): preserve rewritten pages query navigation

* fix(router): align pages url object locale handling

* fix(router): preserve raw pages url search

* fix(router): escape fragments in raw pages search

* fix(router): preserve localized rewritten pages urls

* test(router): fix pages navigation CI failures

* fix(router): preserve Next search fragment formatting
2026-06-15 22:34:36 +01:00
Divanshu Chauhan (divkix) b4c829d6a2 feat(interception): sibling-style interception routes (#1364 Part C) (#1804)
* test(interception): add failing sibling-interception graph tests

* feat(routing): discover sibling-style interception markers

* feat(routing): emit sibling interception manifest facts

* feat(app-router): serialize sibling intercepts in route manifest

* feat(app-router): match sibling intercepts in route matcher

* feat(app-router): render sibling intercept as full page response

* test: add siblingIntercepts field to AppRoute test fixtures

* test(interception): add sibling interception fixtures

* fix(app-router): emit null proof for sibling intercepts to bypass slot-validation

* test(interception): add sibling interception e2e specs

* fix(interception): correct (...)→(..)(..) marker in interception-from-root fixture

* fix(interception): resolve sourceRouteIndex to slot-owner route in intercept lookup

When a route inherits a @slot from an ancestor via discoverInheritedParallelSlots,
the inherited slot's interceptingRoutes are copied from the ancestor. Previously,
createInterceptLookup used routeIndex (the inheriting descendant's index) as
sourceRouteIndex, causing resolveAppPageInterceptState to see sourceRoute ===
currentRoute → kind='current-route' instead of kind='source-route'.

Fix: build a patternToIndex map and resolve sourceRouteIndex via the intercept's
sourceMatchPattern (which names the actual slot-owner route), falling back to
routeIndex only when no match is found.

* fix(interception): use locale-prefixed href so middleware interception fires correctly

* fix(interception): address codex review issues in sibling intercept implementation

- Pass effectiveParams to resolveAppPageHead so generateMetadata/generateViewport
  sees the intercepted route's params instead of the source route's params
- Rename SIBLING_INTERCEPT_SLOT_NAME from __page to __vinext_sibling_intercept
  to prevent collision with a user @__page parallel route directory
- Restore middleware-rewrite fixture link without locale prefix so the
  interception-dynamic-segment-middleware spec exercises the intended
  middleware rewrite path

* fix(test): update slotId assertions and restore locale-prefixed fixture link

- Update test assertions from slot:__page to slot:__vinext_sibling_intercept
  following the rename in commit 2248b8fe
- Restore locale-prefixed href in interception-mw fixture so the
  interception-dynamic-segment-middleware E2E tests pass (reverts the
  accidental revert from 2248b8fe back to the working state from 02e80c26)

* fix(interception): address review findings on sibling interception PR

- Restore locale-less href in interception-mw fixture and fix the
  underlying middleware-rewrite interception gap: add a Referer-based
  fallback in app-rsc-handler so interception fires when middleware
  rewrites the URL before the server matches it (the client-side manifest
  check sees the pre-rewrite target and skips setting the interception
  context header; the Referer carries the source page the server needs)
- Apply interceptLayouts for sibling intercepts in buildPageElements so
  a layout.tsx under the interception marker dir wraps the intercepting
  page, matching Next.js segment-layout wrapping behaviour; add fixture
  layout and E2E assertion
- Add unit test proving findOwnerRouteForDir ancestor walk attaches a
  sibling intercept to the nearest ancestor route when the marker's
  parent dir has no page.tsx (not dropped silently)

* fix(interception): send current pathname as fallback interception context

The client-side manifest check compares the pre-middleware target URL
against declared interception patterns. When middleware rewrites the URL
(e.g. /foo/p/1 → /en/foo/p/1), the pre-rewrite URL has fewer segments
and doesn't match the pattern, so no X-Vinext-Interception-Context
header is sent and the server cannot fire interception.

Fix: when the manifest check yields no match and there is no prior
intercepted navigation in browser state, fall back to sending the
current page pathname as the interception context. This mirrors Next.js
sending Next-URL on every RSC navigation. The server's findIntercept is
double-gated on both source and target matching, so false positives
cannot occur. The previousNextUrl is also populated so back/forward
traversal can restore intercepted state.

Revert the Referer-based server-side fallback (unreliable across
environments) in favour of this explicit browser-side fix.

* fix(interception): gate middleware-rewrite fallback on declared source pattern

The previous fix sent window.location.pathname as fallback interception
context on every unmatched navigation, which broke prefetch cache reuse:
a prefetch stored with null context wouldn't be found when the navigation
used a non-null context key.

Refine the fallback to only fire when the current page URL matches at
least one sourcePatternParts declared in the route manifest — i.e., the
current page is a known interception source. This preserves prefetch
cache reuse for ordinary navigations while still enabling middleware-
rewrite interception for pages that declare an interception relationship.

* fix(interception): support middleware-rewrite targets in fallback context and planner

The previous fallback-context gate only checked whether the current page
is a declared interception source; it did not verify the target URL
could plausibly be a middleware-rewritten version of the declared target
pattern.  This meant the client sent interception context when it
shouldn't (pre-middleware target had no static anchors in common with the
pattern) and the planner hard-navigated when the proof target matched
only after the rewrite.

Three coordinated fixes:

1. Client fallback gate (app-browser-entry / app-browser-interception-context):
   replace the broad source-only check with matchRoutePatternWithOptionalDynamicSegments
   on the target side — the pre-middleware URL must still share every
   static anchor of the declared target pattern, in order, while allowing
   dynamic segments to be absent.

2. Planner interception proof (navigation-planner):
   findRouteManifestInterceptionForProof now accepts a proof whose target
   matched URL equals the pre-rewrite canonical pathname (shorter than the
   full pattern) when matchRoutePatternWithOptionalDynamicSegments passes.
   Exact pattern match still skips the targetRouteId guard; the relaxed
   path skips it too so the middleware-added locale segment does not
   invalidate an otherwise valid proof.

3. displayPathname threading (app-rsc-handler / app-rsc-entry / app-page-element-builder):
   the handler now passes canonicalPathname as displayPathname separately
   from cleanPathname.  buildPageElements uses cleanPathname for all
   internal routing work (slot param extraction, slot overrides) and
   displayPathname only for render identity — keeping the browser-visible
   URL in RSC metadata without affecting internal matching.

* fix(interception): guard siblingInterceptElement construction and document reserved slot name

* fix(interception): surface missing default export on sibling intercept pages

A sibling-style interception whose intercepting page module is missing
its `default` export previously fell back to the source route's page
component (`effectivePageModule?.default ?? PageComponent`), silently
rendering a *different* page than the one requested.

Resolve the effective component from the intercepting page only and
extend the existing no-default-export guard to cover sibling intercepts,
so a missing export surfaces the same explicit "Page has no default
export" error the source/slot paths already produce. For a normal
request this is identical to the previous behavior since
`effectivePageModule === pageModule`.

Addresses bonk review feedback on #1804.

* test(app-router): guard canonical pathname commit on soft-nav config rewrites

The `displayPathname = canonicalPathname` change feeds the RSC payload
identity the client planner uses to commit a navigation, so it affects
every rewritten route, not just interceptions. Existing coverage
(nextjs-compat/hooks.test.ts) only exercises the hard-nav (SSR) path.

Add an e2e that soft-navigates (client-side) to an afterFiles-rewritten
route and asserts the committed URL and `usePathname()` stay canonical
(pre-rewrite), with a marker check proving no full page reload occurred.

Addresses bonk review feedback on #1804.

* fix(interception): restore __pageLoader on slot intercept lookup entries

The main merge auto-resolved incorrectly: it moved the `__pageLoader`
field that #1738 added to the *slot* intercept push block onto the new
*sibling* push block instead (the two `interceptLookup.push({...})` calls
are textually similar). As a result slot intercept lookup entries lost
`__pageLoader`, so resolveAppPageInterceptState never loaded the lazy
intercepting page (`page` stayed null) and the modal slot fell back to
default.tsx — breaking every slot interception.

Restore `__pageLoader: intercept.__pageLoader` on the slot push block.
The sibling block already carries it (see merge commit), and both
intercept types declare the optional field.

Fixes the app-router-dev-server intercept tests and the interception
e2e suites that regressed after merging main.

---------

Co-authored-by: James Anderson <james@eli.cx>
2026-06-08 19:01:29 +01:00
James Anderson 7f0b7ee9f7 test(routing): regression for /api-… page prefixes (#1542) (#1662)
* test(routing): regression for /api-… page prefixes (#1542)

Adds a focused unit test in tests/routing.test.ts that creates a
fixture with `pages/api-docs/[...slug].tsx`, `pages/api-info/index.tsx`,
and a real API route at `pages/api/hello.ts`, then asserts:

- `/api-docs/:slug+` and `/api-info` are discovered as page routes,
  not API routes (matches the Next.js fixture at
  `.nextjs-ref/test/e2e/prerender/pages/api-docs/[...slug].js`).
- The real `/api/hello` API route is still discovered correctly.
- `matchRoute` resolves `/api-docs/first` and `/api-info` against the
  page route table, pointing at the correct file paths.

The existing API-route detection already requires the trailing slash
(`startsWith("/api/")` or `=== "/api"`) in every code path, and the
`pages/` scanner only excludes the literal `"api"` directory — so the
test passes on `main`. This test pins that behaviour in place to
prevent a future regression of the substring-`api-` false positive
called out in #1542.

Refs #1542.

* style: apply biome formatter to routing.test.ts
2026-06-03 23:03:17 +01:00
Divanshu Chauhan (divkix) fdcc850638 fix(routing): serve pages/_foo.tsx when app/ also exists (#1610) 2026-05-27 22:24:02 +01:00
Nathan Nguyen 7b3fc49b92 fix(app-router): honor route group error boundaries (#1124) 2026-05-07 11:22:30 +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 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
Nathan Nguyen 1e4c175ee4 fix(routing): omit empty optional catch-all params (#987)
Optional catch-all routes currently materialize the missing segment list as an empty array. Next.js leaves the param key absent when the optional catch-all capture group does not match, so pages and app routes can observe a different params shape at the zero-segment path.

The route trie treated zero remaining segments as a captured value. The fix keeps the route match while returning an empty params object for that zero-segment branch, and applies the same normalization to standalone app RSC pattern matching.

Tests cover shared trie matching, app RSC matching, app route discovery, pages route discovery, and hyphenated optional catch-all params.
2026-04-30 10:24:51 +01:00
Divanshu Chauhan (divkix) dc28af1cb6 fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764) (#945)
* fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764)

Per-layout forbidden/unauthorized boundary files (forbidden.tsx, unauthorized.tsx)
are now discovered by the router and wired into the App Router component tree via
dedicated ForbiddenBoundary and UnauthorizedBoundary client components. Previously,
only NotFoundBoundary existed, causing 403/401 errors thrown during RSC rendering
(e.g. inside Suspense boundaries) to escape into the generic error shell — this
could produce mismatched Flight/HTML data and client-side 'Connection closed' failures.

Next.js upstream: vercel/next.js#92231
  https://github.com/vercel/next.js/pull/92231

* fix: address review feedback on ForbiddenBoundary/UnauthorizedBoundary (#945)

- Export ForbiddenBoundaryInner and UnauthorizedBoundaryInner for testing
- Strengthen tests to import and call getDerivedStateFromError directly
- Use exact equality instead of startsWith for status code digest matching
- Make forbiddenPaths and unauthorizedPaths non-optional on AppRoute (matching notFoundPaths)
- Fix inaccurate ported-from attribution in test header

* fix: remove duplicate forbidden/unauthorized path iteration and update comment

Removes dead code in app-rsc-entry.ts (lines 185-190) that duplicated the existing import registration loops.

Also updates the stale nesting order comment in app-page-route-wiring.tsx to include Unauthorized and Forbidden boundaries.

* fix: align NotFoundBoundary digest matching and tests with Forbidden/Unauthorized patterns

- Use exact === instead of startsWith in NotFoundBoundary digest check
- Add forbiddenPaths/unauthorizedPaths per-layout test fixtures
- Fix property ordering: singular before plural for all boundary types

* fix: resolve rebase conflicts — align manifest code with main refactor

- Use buildAppRscManifestCode for manifest generation (matching main)
- Remove redundant || [] guards for non-optional forbiddenPaths/unauthorizedPaths
- Fix test fixture indices to match new module import count

* fix: add null guards for notFoundPaths, forbiddenPaths, unauthorizedPaths in manifest

TypeError: route.notFoundPaths is not iterable when routes lack per-layout boundary files.

CI was failing on app-router.test.ts with the error. Added null guards in both registerRouteModules() and buildRouteEntries() to handle undefined arrays.

* fix: reorder route-level boundary nesting for merge with main

Move ErrorBoundary after NotFoundBoundary/ForbiddenBoundary/
UnauthorizedBoundary to match Next.js nesting order (Error >
Unauthorized > Forbidden > NotFound). Resolves merge conflict
with bc70e1c.
2026-04-30 07:46:07 +01:00
Divanshu Chauhan (divkix) db57d45070 Shim next/root-params (#941)
* fix(app-router): shim next root params

* fix: address review feedback

* address review feedback

* test: update snapshots for root params cleanup change
2026-04-28 21:43:45 +01:00
Nathan Nguyen bee7e6b654 fix(router): reject invalid app route discovery conflicts (#928)
* fix(router): reject invalid app route discovery conflicts

App Router route discovery currently lets upward interception markers climb past the normalized root and relies on generic duplicate-pattern validation when a page and route handler share the same app route.

The scanner now validates visible interception climb depth before resolving the target, and reports an explicit page/route conflict before running generic route pattern validation.

Regression coverage captures the Next.js parity cases for root interception markers, route-group-transparent roots, two-level upward markers, and page plus route handler conflicts.

* fix(router): address app route validation review
2026-04-28 19:06:39 +01:00
Nathan Nguyen ae45ff2cf2 fix(router): ignore pre-root parallel slots for routed layouts (#927)
Routes whose first layout lives below app/ currently inherit slots discovered before that layout and clamp them to layout index 0. That incorrectly passes top-level parallel slots into a separate root layout.

Keep the pre-layout sentinel while scanning ancestors, skip those slots once the route has a real layout, and preserve the existing layout-less scanner metadata path for separate validation work. Add routing tests for the pre-root leak and the valid in-subtree slot case.
2026-04-28 11:32:38 +01:00
Nathan Nguyen faf1d9b4d6 Render intercept route layouts inside App Router slots (#872)
* Render intercept route layouts inside App Router slots

Intercepted App Router pages currently render without any layout.tsx files that live inside the intercepting route tree. This is wrong because Next.js applies those layouts before the slot layout, and nested intercept routes lose required wrappers when a modal intercept matches.

The routing pipeline assumed intercepts only needed a page module. That drops the intercept layout chain during discovery and never gives the slot override renderer enough information to rebuild the wrapper tree.

Collect intercept layout paths during route discovery, serialize them through the generated RSC entry, and apply the loaded layout modules around the intercepted page before the slot layout. Add regression coverage for discovery, code generation, and slot render order.

* Avoid repeated intercept layout scans and slot param wrapping

Intercept layout discovery rescanned the same directories for every intercepted page, and slot rendering rebuilt thenable params multiple times inside one render path. That adds avoidable work and makes the intercept wrapper path harder to read.

The implementation assumed these repeated lookups and allocations were harmless. That leaves the recursive router walk doing duplicate filesystem checks and gives slot wrappers different params wrapper identities in the same tree.

Thread the discovered intercept layout chain through the recursion, compute slot thenable params once per slot render, and keep the generated entry snapshots in sync with the added intercept layout plumbing.

* Clarify intercept layout plumbing and cover nested layout chains

The intercept layout plumbing still used mixed names in generated code, and the reverse layout wrapper loop took extra decoding to read. That makes the fix harder to review and maintain even though the behavior is correct.

The generated entry treated intercept-local layouts as a generic `layouts` field while the rest of the pipeline called them `interceptLayouts`, and the slot wrapper loop used a less direct reverse-index pattern. The tests also only covered a single intercept-local layout level.

Rename the generated intercept field to `interceptLayouts`, switch the reverse wrapper loop to the clearer classic form, and add regression coverage for nested intercept layout chains. Update the generated-entry snapshots to match.
2026-04-23 08:03:58 +01:00
Nathan Nguyen dd498bdea9 Fix sibling intercepted App Router routes (#867)
* fix app router sibling interception

* update app router entry snapshots

* preserve chained intercepted navigations

* address intercepted route review feedback

* guard probePage() against undefined PageComponent for slot-only routes

probePage() calls PageComponent(...) unconditionally. When a layout-owned
slot-only route is hit, route.page is null so PageComponent is undefined,
causing probePage() to throw TypeError on every request. probeAppPageComponent
catches it and returns null, so it does not crash, but it fires an avoidable
exception on every request to a slot-only URL and can confuse error monitoring.

probeLayoutAt() already uses the same guard pattern: if (!LayoutComp) return null.
Apply the same guard to probePage().
2026-04-21 18:23:51 +01:00
Nathan Nguyen 0dbfeff1af feat: flat keyed payload for App Router layout persistence (#750)
* Extract app page route wiring helpers

* Add slot client primitives

* Fix app page error boundary serialization

* Fix client error boundary pathname reset

* Document Next.js error boundary verification

* Implement flat App Router payload for layout persistence

* fix: address review findings in flat payload implementation

- Fix stale closure on readBrowserRouterState by using a useRef updated
  synchronously during render instead of a closure captured in
  useLayoutEffect. External callers (navigate, server actions, HMR) now
  always read the current router state.

- Restore GlobalErrorBoundary wrapping that was dropped when switching
  from buildPageElement to buildAppPageElements. Apps with
  app/global-error.tsx now get their global error boundary back.

- Add exhaustive default case to routerReducer so new action types
  produce a compile error and a runtime throw instead of silent undefined.

- Remove dead code: createRouteNodeSnapshot, AppRouteNodeSnapshot,
  AppRouteNodeValue were defined but never imported.

- Remove deprecated buildAppPageRouteElement and its test — no
  production callers remain after the flat payload cutover.

- Short-circuit normalizeAppElements when no slot keys need rewriting
  to avoid unnecessary allocation on every payload.

- Align test data in error boundary RSC payload test (matchedParams
  slug: "post" -> "missing" to match requestUrl /posts/missing).

* fix: normalize flat payload after use(), not before

createFromReadableStream() returns a React thenable whose .then()
returns undefined (not a Promise). Chaining .then(normalizeAppElements)
broke SSR by assigning undefined to flightRoot.

Fix: call use() on the raw thenable, then normalize synchronously
after resolution. Also widen renderAppPageLifecycle element type to
accept flat map payloads.

* fix: produce flat RSC payload on all rendering paths

The SSR entry always expects a flat Record<string, ReactNode> with
__route and __rootLayout metadata from the RSC stream. Three paths
were still producing bare ReactNode payloads:

1. renderAppPageBoundaryElementResponse only created the flat map for
   isRscRequest=true, but HTML requests also flow through RSC→SSR
2. buildPageElements "no default export" early return
3. Server action "Page not found" fallback

All three now produce the flat keyed element map, fixing 17 test
failures across 404/not-found, forbidden/unauthorized, error boundary,
production build, rewrite, and encoded-slash paths.

* test: update unit tests for flat RSC payload on all paths

- Update renderElementToStream mock to extract the route element from
  the flat map before rendering to HTML (mirrors real SSR entry flow)
- Update entry template snapshots for the buildPageElements changes

* fix: wrap Flight thenable in Promise.resolve() before chaining .then()

createFromReadableStream() returns a React Flight thenable whose
.then() returns undefined instead of a new Promise. The browser
entry's normalizeAppElementsPromise chained .then() on this raw
thenable, producing undefined — which crashed use() during hydration
with "An unsupported type was passed to use(): undefined".

Wrapping in Promise.resolve() first converts the Flight thenable
into a real Promise, making .then() chains work correctly.

The same fix was already applied to the SSR entry in 5395efc but
was missed in the browser entry.

* fix: eliminate Promise from ElementsContext to fix React 19 hydration

React 19.2.4's use(Promise) during hydration triggers "async Client
Component" because native Promises lack React's internal .status
property (set only by Flight thenables). When use() encounters a
Promise without .status, it suspends — which React interprets as the
component being async, causing a fatal error.

Fix: store resolved AppElements directly in ElementsContext and
router state instead of Promise<AppElements>. The navigation async
flow (createPendingNavigationCommit) awaits the Promise before
dispatching, so React state never holds a Promise.

- ElementsContext: Promise<AppElements> → AppElements
- AppRouterState.elements: Promise<AppElements> → AppElements
- mergeElementsPromise → mergeElements (sync object spread)
- Slot: useContext only, no use(Promise)
- SSR entry: pass resolved elements to context
- dispatchBrowserTree: simplified, no async error handler

Also fix flaky instrumentation E2E test that read the last error
entry instead of finding by path.

* test: update slot and browser state tests for resolved ElementsContext

- Remove Promise wrappers from ElementsContext test values
- mergeElementsPromise → mergeElements (sync)
- Replace Suspense streaming test with direct render test
- Remove unused createDeferred helper and Suspense import
- Update browser state test assertions (no longer async)

* ci: retrigger

* fix: address code review findings (P1-P3)

P1a: mergeElements preserves previous slot content when the new payload
marks a parallel slot as unmatched. On soft navigation, unmatched slots
keep their previous subtree instead of triggering notFound().

P1b: renderNavigationPayload now receives navId and checks for
superseded navigations after its await. Stale payloads are discarded
instead of being dispatched into the React tree.

P2: The catch block in renderNavigationPayload only calls
commitClientNavigationState() when activateNavigationSnapshot() was
actually reached, preventing counter underflow.

P3: The no-default-export fallback in buildPageElements now derives
the root layout tree path from route.layoutTreePositions and
route.routeSegments instead of hardcoding "/".

* fix: avoid serializing app render dependency wrappers

* Fix flat payload dependency barriers

* Fix template-only route wrappers

* chore: trigger CI review

* fix: skip Slot wrapping for layout entries without a default export

When a layout entry exists but has no default export, the element is
never written to the flat map. The unconditional Slot wrapping would
return null (id not in elements), silently dropping the entire route
subtree below that layout level.

Guard the Slot wrapping with a check for the layout component. When
absent, pass layoutChildren through directly — preserving the
LayoutSegmentProvider and error/not-found boundaries while skipping
the Slot indirection.

* fix: restore merged app router entry behavior

* fix: address app router review regressions

* Fix app-page-request intercept tests

* Address PR 2c review follow-ups

* Refactor same-url payload commits

* Clarify PR 2c browser invariants

* Update App Router entry snapshots

* Tighten App Router review follow-ups

* Align App Router entry helper usage

* Tighten browser commit invariants

* fix(app-router): preserve scoped parallel slot identity

* test(app-router): align slot identity assertions

* fix(app-router): keep boundary root layout metadata unknown

* docs(app-router): refresh stale review comments
2026-04-10 07:49:18 +01:00
Brendan Dash a9d3455487 test: regression tests for inherited intercepting route slot deduplication (#721)
* fix: allow inherited intercepting routes to share target patterns

* fix: validate intercepting route target consistency per page

* refactor: simplify intercept target pattern collection by using Set for deduplication

* fixup: use main's inline Set dedup, remove helper export, restore .gitignore

* fix(test): pass explicit out dirs and appDir to buildApp to fix UNRESOLVED_IMPORT in RSC build

* chore: restore .gitignore to match main

---------

Co-authored-by: James <james@eli.cx>
2026-04-05 09:31:57 +00:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* chore: migrate to vite plus

* Disable typeAware and typeCheck

* Update CI

* Fix CI

* Fix test

* Clean

* Run test with vp

* Try revert

* react: false In test

* Fix test

* Revert "Try revert"

This reverts commit 009da10473.

* Update

* Update

* Try revert ci changes

* revert

* Run vp migrate

* Disable typeAware and typeCheck for now

* Better resolve for test

* Use vp dev instead of vite

* Update expect

* Fix NormalizeManifestModuleId

* Try increase timeout

* Update to use vp

* Try new check

* Bring back npx vp

* Migrate CI

* Make next-intl resolvable

* Update

* Update

* Update
2026-03-15 10:50:13 +00:00
Nathan Nguyen 6723edb3cd fix: use closest ancestor parallel slot instead of farthest (#545)
discoverInheritedParallelSlots iterates from root to leaf, but used
a `has()` guard that let the first (farthest) ancestor win. Remove
the guard so later (closer) ancestors overwrite earlier ones.
2026-03-15 10:01:29 +00:00
Nathan Nguyen 3d05dceeab fix: intercept routes (..) climb visible route segments, not filesystem dirs (#499)
computeInterceptTarget() used path.dirname() to climb parent directories,
but Next.js defines interception levels in terms of route segments. Route
groups like (marketing) are filesystem directories but invisible in the
URL, so path.dirname() would waste a climb on them.

Now walks backward through segments, counting only visible ones (skipping
route groups, parallel slots, and ".").

Also consolidates three copies of invisible-segment detection into a
single isInvisibleSegment() helper.
2026-03-12 13:38:21 +00:00
Nathan Nguyen af6e0ce421 fix: exclude private folders (_prefix) from App Router route discovery (#484)
* fix: exclude private folders (_prefix) from App Router route discovery

Next.js treats underscore-prefixed directories in app/ as "private
folders" that are excluded from routing. This is a recommended pattern
for colocating non-route files (e.g. _components, _lib, _utils).

vinext was only excluding @-prefixed slot directories, so files like
app/_components/page.tsx would incorrectly create routes.

Add the underscore check to all four App Router filesystem scan points:
- Main page/route discovery (scanWithExtensions exclude callback)
- Slot sub-page scanner (already had it)
- Intercepting route scanner (scanForInterceptingPages)
- Intercepting page collector (collectInterceptingPages)

* chore: format app-router.ts
2026-03-12 08:25:18 +00:00
Jared Stowell 26d5eabb92 fix: skip synthetic slot sub-routes without parent default fallback (#458)
* Fix nested parallel slot routes

* Fix nested parallel slot discovery

* Fix parallel slot routing parity
2026-03-12 07:47:25 +00:00
Jared Stowell b66fd6488c fix: Preserve encoded path delimiters in route discovery and matching (#456)
* Fix App Router decoding collision

* Fix App Router path decoding

* Address bonk review: codegen safe segment decode, static serving, nits

* fix: add codegen strict normalizer, use it at 400-returning call sites

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 16:52:29 +00:00
Jared Stowell 11b27cd0a8 fix: reject conflicting dynamic route siblings during route discovery (#420)
* Validate route patterns in routers

* Validate intercepting routes
2026-03-10 21:48:36 +00:00
Jared Stowell cf0786ef34 fix: Reject non-terminal catch-all routes in pages and app routers (#401)
* Fix catch-all route overmatching

* Fix non-terminal slot routes

* Fix malformed slot route discovery

* Add validation for intercept catch-⁤

* Fix intercepting route catch-alls

* bonk

* Fix routing test route literals for typecheck

* Fix app route matcher test fixtures
2026-03-10 20:24:31 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

* fix: add Format to required checks list, remove dead ignore pattern

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
ask-bonk[bot] aff5d24449 fix(routing): allow hyphens in dynamic segment param names (#78)
* fix(routing): allow hyphens in dynamic segment param names

Catch-all and dynamic route params with hyphens (e.g. [[...sign-in]],
[auth-method]) caused 404 because the regex used \w+ which doesn't
match hyphens. Next.js allows hyphens in param directory names — this
is commonly used by Clerk ([[...sign-in]], [[...sign-up]]).

Changed all \w+ patterns to [\w-]+ across:
- app-router.ts (fileToAppRoute, discoverSlotSubRoutes, computeInterceptTarget)
- pages-router.ts (fileToRoute, patternToNextFormat)
- app-dev-server.ts (middleware matcher regex, config pattern matching)
- middleware.ts (pattern matching)

Closes #71

* fix(routing): allow hyphens in config matching and middleware-codegen param patterns

Addresses review feedback on PR #78: the same \w+ bug that was fixed in
routing/middleware also existed in the config pattern matching functions
(redirects, rewrites, headers) and the middleware-codegen tokenizer.

Updated all remaining param-related regex patterns to [\w-]+ across:
- index.ts: matchConfigPattern guard, tokenizer, catchAllMatch, applyHeaders
- config-matchers.ts: matchConfigPattern guard, tokenizer, catchAllMatch,
  escapeHeaderSource tokenizer
- app-dev-server.ts: __matchConfigPattern template (4 .replace() chains,
  catchAllMatch, __applyConfigHeaders)
- middleware-codegen.ts: generated matchMiddlewarePattern tokenizer

Subsumes PR #127 which fixed middleware-codegen.ts independently.

Co-authored-by: SeolJaeHyeok <milkboy2564@naver.com>

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: Fred K. Schott <622227+FredKSchott@users.noreply.github.com>
Co-authored-by: SeolJaeHyeok <milkboy2564@naver.com>
2026-02-26 22:15:45 -08:00
skeptrune 4349af3ecc fix: decode URL-encoded directory names and improve route precedence (#386)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 09:29:45 -06:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00