Skip to content

fix(routing): @children slot explicit page wins over catchall (#1535)#1659

Merged
james-elicx merged 1 commit into
mainfrom
fix/issue-1535-parallel-children-priority
May 28, 2026
Merged

fix(routing): @children slot explicit page wins over catchall (#1535)#1659
james-elicx merged 1 commit into
mainfrom
fix/issue-1535-parallel-children-priority

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • Fixes Parallel routes: explicit @children slot/page should win over catchall and resolve when no default #1535app/foo/@children/page.tsx now registers a real page route at /foo and beats a sibling [...catchAll] catch-all, matching Next.js.
  • @children is treated as transparent in routing: it is allowed through the page scan, stripped when computing the anchored route directory, and skipped by discoverParallelSlots / hasParallelSlotDirectory so it never appears in parallelSlots.
  • Adds focused regression tests in tests/app-route-graph.test.ts ported from Next.js' parallel-routes-catchall-children-slot e2e fixture, covering both the root case and the no-default.tsx nested case.

Why this matches Next.js

@children is the way to name the layout's children prop, not a parallel slot. Next.js mirrors this in three places we cite in the code:

  • packages/next/src/shared/lib/router/utils/app-paths.tsnormalizeAppPath strips every @ segment (including @children) from the URL.
  • packages/next/src/build/normalize-catchall-routes.tsisMatchableSlot explicitly excludes @children so a [...catchAll] is not pushed onto a path that already has an @children/page entry.
  • packages/next/src/build/webpack/plugins/next-types-plugin/index.ts — the slot enumerator skips @children because it is matched to the children prop.

Test plan

  • pnpm test tests/app-route-graph.test.ts (40 tests, including 2 new)
  • pnpm test tests/routing.test.ts tests/route-sorting.test.ts tests/app-rsc-route-matching.test.ts tests/slot.test.ts tests/route-classification-manifest.test.ts tests/app-router.test.ts tests/intercepting-routes-build.test.ts tests/metadata-routes.test.ts (no regressions)
  • pnpm run check (format/lint/types clean)
  • CI exercises the rest of the Vitest suite and Playwright E2E

Scope note

Issue #1535 listed a second symptom from test/e2e/app-dir/parallel-routes-catchall ("explicit slot but no page times out"). That one is more invasive (catch-all route slot mirroring across a parent that has no default.tsx for children) and is best handled in a separate PR — this change deliberately stays focused on the @children-vs-catchall priority fix the issue's recommendation called out first.

…er catchall (#1535)

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.
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review

@pkg-pr-new

pkg-pr-new Bot commented May 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1659

commit: 6875010

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
web preview production

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix, well-scoped. The four touch points (excludeDir, hasParallelSlotDirectory, fileToAppRoute, discoverParallelSlots) are exactly the right places, the Next.js source references are accurate, and the tests port the right fixture.

A few observations (none blocking):

@children/route.ts (API route handler) now gets scanned too. The excludeDir change lets the scanner walk into @children for both the **/page and **/route passes. The fileToAppRoute stripping is gated on type === "page", so a route.ts inside @children wouldn't get its dir rewritten — but isInvisibleSegment in convertSegmentsToRouteParts still strips @children from the URL, so it'd register at the parent pattern (e.g., /). This is correct behavior (matches normalizeAppPath), but worth noting that @children/route.ts is a degenerate case nobody would write in practice. If you wanted to be defensive, you could add the same path.basename(dir) === "@children" stripping for type === "route" too, but it's not necessary since convertSegmentsToRouteParts already handles it.

Deep nesting like @children/sub/page.tsx works correctly for URL patterns because isInvisibleSegment drops @children from the segment list during conversion. The layout discovery walk would traverse through the @children directory on disk though (looking for @children/layout.tsx, etc.), which matches how all @-prefixed invisible segments are handled today.

Tests are well-structured — good use of withTempApp, good assertions on both pagePath provenance and slot attachment. The scope note about deferring the second symptom from #1535 is the right call.

@ask-bonk

ask-bonk Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Approved. The fix is correct and well-scoped — four surgical changes at the right touch points, consistent with how Next.js handles @children, and backed by solid regression tests ported from the Next.js fixture.

github run

@james-elicx james-elicx marked this pull request as ready for review May 28, 2026 15:59
@james-elicx james-elicx merged commit c1a6adc into main May 28, 2026
38 checks passed
@james-elicx james-elicx deleted the fix/issue-1535-parallel-children-priority branch May 28, 2026 19:54
james-elicx added a commit that referenced this pull request May 31, 2026
#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.
james-elicx added a commit that referenced this pull request Jun 8, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parallel routes: explicit @children slot/page should win over catchall and resolve when no default

1 participant