mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.8
26 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fbf68d588e |
fix(app-router): restore cache and parallel route parity (#2261)
* fix(app-router): preserve searchParams cache variants * test(app-router): cover RSC metadata cache variants * fix(app-router): isolate metadata searchParams observation * fix(app-router): preserve static ISR cache proofs * fix(app-router): limit queryless cache probes * fix(app-router): skip redundant cache proof probes * fix(app-router): harden incomplete cache proofs * fix(app-router): observe loading page execution * fix(app-router): preserve exotic page exports * fix(app-router): preserve client page ISR * fix(app-router): avoid duplicate queryless page execution * fix(app-router): observe viewport search params * fix(app-router): clear force-static search params * fix(app-router): isolate intercept static context * fix(app-router): preserve intercept request context * fix(app-router): isolate action static context * fix(app-router): match static request API semantics * fix(app-router): preserve dynamic segment constraints * fix(app-router): apply slot-only segment config * fix(app-router): preserve draft mode reads in static errors * fix(app-router): include nested slot constraints * fix(app-router): refresh slot layout constraints * fix(app-router): render nested slot layouts * fix(app-router): preserve nested slot params * fix(app-router): preserve intercepted slot head * fix(app-router): scope parallel layout params * fix(app-router): preserve intercept layout parity * fix(app-router): preserve intercept ancestor params * fix(app-router): scope intercept source params * fix(app-router): preserve intercept source route params * test(app-router): stabilize parity validation |
||
|
|
5146b1f47b | fix(router): render slots beside grouped children (#2022) | ||
|
|
dabcb55e63 |
fix(app-router): expose active source page on window.next (#1995)
* fix(app-router): expose active source page on window.next App Router pages did not update window.next.__internal_src_page, so Next.js-compatible tooling and deploy tests could not observe the active source page on initial render or client transitions. The payload only exposed URL route IDs, which drop route groups and bracketed dynamic segment names. The fix carries a source-page metadata field from the app route's filesystem segments, reads it from committed browser router state, and mirrors it onto window.next like Next.js does. Adds regression coverage for route-group and dynamic source-page encoding plus the window.next writer contract ported from the upstream app-dir test. * fix(app-router): preserve source page across boundary renders * fix(app-router): tolerate malformed source page metadata * fix(app-router): preserve intercepted fallback source page * fix(app-router): omit unknown boundary source page --------- Co-authored-by: James <james@eli.cx> |
||
|
|
cd921ab709 |
perf: cache fs probes and precompute route sort keys in scan/deploy paths (#1930)
* perf(routing): precompute route precedence before sorting
`compareRoutes` re-parsed every pattern on each comparison, so a sort
called `routePrecedence` ~2·log n times per route instead of once. Replace
it with `sortRoutes`, which scores each pattern once into a Map and sorts
by (score, pattern.localeCompare). The localeCompare tiebreaker already
guarantees a total order, so ordering is byte-identical; this just removes
the redundant re-parsing on every route scan (dev re-scans on file change,
plus the pages/api and app-graph builds).
* perf(app-router): cache convention-file probes per route-graph scan
`buildAppRouteGraph` walks the appDir→leaf chain separately for every
route (layouts, templates, errors, boundaries, slots), each step probing
up to 4 extensions per convention file via `existsSync`. Shared ancestor
directories — the `app/` root above all — were re-stat'd once per
descendant route, so a scan cost roughly routes × depth × conventions ×
extensions syscalls.
Memoize `findFile` through a `WeakMap<ValidFileMatcher, Map>` keyed by a
per-scan matcher clone created in `buildAppRouteGraph`. Results are
immutable within one scan, so no invalidation is needed: the cache lives
exactly as long as the scan and is GC'd afterward. The fresh per-scan key
also keeps overlapping scans isolated, and the `null` not-found outcome is
cached too. Probe results are identical to the previous direct `existsSync`
calls.
* perf(deploy): single tree walk for detection and reuse parsed package.json
`analyzeProject` walked the app/ tree twice — once in `detectISR` and again
in `detectMDX` — and `detectNativeModules` re-read and re-parsed
package.json that `detectProject` had already parsed into `allDeps`.
Merge the two walkers into one `scanTreeForDetection(dir, { isr, mdx })`
that evaluates both predicates per entry with each flag short-circuiting
independently, extract the next.config MDX check into `detectMDXFromConfig`,
and add `resolveProjectDir` for the shared root/`src` precedence. Pass the
already-merged `allDeps` into `detectNativeModules`. Detection semantics are
unchanged: same files visited, same patterns tested, same booleans out.
|
||
|
|
dea4472c2f |
fix(app-router): include parallel slot params in useParams (#1904)
* fix(app-router): include parallel slot params in useParams App Router navigation state previously used only the primary route match params when setting request navigation context, RSC param headers, render observations, and optimistic navigation payloads. That lost catch-all params from active parallel slot branches, so useParams() diverged from the slot page's params prop after SSR and client navigations. Next.js derives params from the selected router tree across parallel route branches. Resolve active slot params from route metadata, merge them into the navigation params boundary, and feed that selected param set through server render and optimistic navigation payloads. Port the upstream parallel-route-navigations fixture and cover the route graph, selected params helper, and optimistic payload boundary. * fix(app-router): preserve canonical pathname in navigation context after middleware rewrite * fix(app-router): resolve parallel slot params for static, revalidation, and action paths * chore: retrigger CI * fix(app-router): address ask-bonk review comments on parallel slot params - Remove unused parameter from - Add clarifying comment on routeParams fallback being deliberate no-op - Use (full matched params) instead of narrowed as base for slot param merge, preserving interception-specific extras on ISR regeneration path * fix(app-router): use full navigationParams in server-action re-render path The server-action re-render path was passing narrowed actionRerenderTarget.params to resolveAppPageNavigationParams, dropping interception-specific extras from source-route intercepts. Use actionRerenderTarget.navigationParams (full matched params) instead, matching the pattern used in the dispatch ISR path at app-page-dispatch.ts:602. Adds unit tests for the intercepted-slot and mixed intercepted + non-intercepted slot branches of resolveAppPageNavigationParams. * docs(router): clarify decode invariant and type cast rationale in parallel-slot params - app-optimistic-routing.ts: add comment explaining the pre-existing double-decode asymmetry between route params and slot params, noting TODO to converge decode passes - app-server-action-execution.ts: expand the comment on the interceptOpts cast to document why it's safe and what a follow-up would look like * fix(app-router): merge slot params for fresh intercepts --------- Co-authored-by: James <james@eli.cx> |
||
|
|
55815233c8 |
fix(routing): normalize route root to forward slashes, drop downstream path workarounds (#1896)
* routing: normalize the route root to forward slashes and drop downstream workarounds * fix(routing): compare findOwnerRouteForDir paths in forward-slash space With appDir now normalized to forward slashes, the ancestor walk's current === appDir termination never fired on Windows (current descends through native path.join/path.dirname and stays backslash), so the walk overshot the app root and could attach a sibling intercept to the wrong owner. Top-level markers had the inverse mismatch: a forward-slash dir probed against backslash routesByDir keys. Normalize dir, appDir, routesByDir keys, and route file paths to forward slashes inside the lookup so all comparisons happen in one separator space, and add POSIX-runnable unit tests simulating the Windows path shapes. * refactor(routing): use normalizePathSeparators in findOwnerRouteForDir Swap the inline backslash replaces for the shared util. It is a platform-gated no-op on POSIX, so the Windows-shape unit tests mock it to keep exercising the normalization logic on POSIX CI. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
30be2b330b |
fix(app-router): discover parallel-slot pages inside route groups for catch-all + parallel-routes-group (#1865)
When a parallel slot's page lives inside a route-group subdirectory of the slot root (e.g. `[...catchAll]/@slot/(group)/page.tsx`), `discoverParallelSlots` was looking only for a direct `page.tsx` at the slot root and skipping the slot entirely when none was found. This caused the slot to be absent from the route graph, so the route rendered without the expected slot content. Two related fixes: 1. Add `findSlotRootPage` — a helper that searches for a page file at the slot root URL level, descending through transparent route-group subdirectories (directories whose name matches `(…)`). `discoverParallelSlots` now uses this instead of a bare `findFile` for the slot's page path. 2. In `discoverSlotSubRoutes`, skip the `applySlotSubPages` call when `urlParts.length === 0` (all sub-segments are URL-invisible). A zero-length visible sub-path means the slot page is at the same URL level as the parent route; `discoverParallelSlots` already assigned it with the correct empty `routeSegments`. Calling `applySlotSubPages` in this case would overwrite `routeSegments` with the raw filesystem segments (e.g. `["(group)"]`), making `useSelectedLayoutSegment` surface the route-group name rather than `null`. Fixes the e2e test: `catchall-parallel-routes-group > should work without throwing any errors about invalid pages`. |
||
|
|
cfc0f8146d |
fix(app-router): resolve explicit parallel slot with no page (#1535) (#1852)
* fix(app-router): resolve explicit parallel slot with no page (#1535) When a parallel slot defines an explicit sub-page (e.g. `@slot/baz/page.tsx`) but no corresponding children page (`baz/page.tsx`) and no `default.tsx` for the children slot, vinext minted a synthetic `/baz` route with a null children page. That route shadowed the sibling catch-all and rendered an empty children prop, so the request hung (the upstream "explicit slot but no page" case timed out). Next.js serves such a slot-only sub-route's children from the nearest sibling catch-all. Add `findCatchAllPage` and use it as the children fallback in `discoverSlotSubRoutes` when no `default.tsx` exists, so `/baz` renders `[...catchAll]/page.tsx` for children while `@slot/baz/page.tsx` fills the slot. A children `default.tsx` still wins over the catch-all when both exist. Finishes the explicit-slot-but-no-page case of #1535 (the children-slot priority case landed in #1659). Fixes #1535. * test(app-router): document catch-all children params limitation (#1535) Address ask-bonk review: the synthetic slot-only sub-route (e.g. /baz) has a static URL pattern, so a catch-all children page reading params.catchAll gets empty params (Next.js passes ["baz"]). Document the divergence inline in discoverSlotSubRoutes and add a graph-level test locking in the current static shape so a future param-population fix must update it. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
7b3fc49b92 | fix(app-router): honor route group error boundaries (#1124) | ||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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). |
||
|
|
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> |
||
|
|
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. |