Skip to content

fix: intercept routes (..) climb visible route segments, not filesystem dirs#499

Merged
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:fix/intercept-route-segment-climbing
Mar 12, 2026
Merged

fix: intercept routes (..) climb visible route segments, not filesystem dirs#499
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:fix/intercept-route-segment-climbing

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

Summary

  • Fixed computeInterceptTarget() to climb by visible route segments instead of filesystem directories. Previously it used path.dirname() which counted route groups like (marketing) as levels, causing (..) and (..)(..) to resolve to the wrong target when route groups sit between visible segments.
  • Consolidated invisible-segment detection into a single isInvisibleSegment() helper, replacing three duplicated inline checks across computeInterceptTarget, convertSegmentsToRouteParts, and hasRemainingVisibleSegments.

Example of the bug

app/a/(group)/b/@modal/(..)(..)target/page.tsx

Visible route path: /a/b. (..)(..) should climb 2 visible segments → root → intercept /target.

Before: path.dirname(path.dirname("app/a/(group)/b")) = "app/a" → produced /a/target (wrong — climbed b then (group), wasting a level on the invisible route group).

After: Walks backward through ["a", "(group)", "b"], counting only visible segments (ba), correctly resolving to /target.

Test plan

  • 3 new test cases covering (..) and (..)(..) with route groups between segments
  • All 90 routing tests pass
  • All 247 app-router tests pass
  • CI: Format, Lint, Typecheck, Vitest, Playwright E2E

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 12, 2026

Open in StackBlitz

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

commit: 8e590e2

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review: Fix intercept route segment climbing

This is a well-executed fix for a real correctness bug. The core insight is correct: Next.js intercept conventions (..) and (..)(..) climb by visible route segments, not filesystem directories. The old path.dirname() approach counted route groups like (marketing) as levels, producing wrong targets.

What I verified

  1. The climbing logic is correct. Walking backward through routeSegments, skipping invisible segments (route groups, @slots), and only counting visible ones matches Next.js behavior. Traced the full call chain from discoverInterceptingRoutescomputeInterceptTarget to confirm routeDir is always the page-owning directory (never the @slot or intercept dir).

  2. baseParts can still contain invisible segments after climbing (e.g., ["a", "(group)"] after climbing 1 level from ["a", "(group)", "b"]). This is correctly handled because convertSegmentsToRouteParts independently filters them out via isInvisibleSegment on line 976.

  3. isInvisibleSegment consolidation is sound. The three call sites (computeInterceptTarget, convertSegmentsToRouteParts, hasRemainingVisibleSegments) now use a single source of truth. The behavioral change in hasRemainingVisibleSegments — now treating "." as invisible, which it didn't before — makes it consistent with convertSegmentsToRouteParts (which already skipped "."). In practice "." never appears in these segments because callers use filter(Boolean) or guard with dir === "." ? [], so it's a correctness-for-consistency fix, not a behavior change.

  4. Edge case: (.), (..) as segment names — these would match isInvisibleSegment's startsWith("(")/endsWith(")") check. However, they cannot appear in routeSegments because the page scanner excludes @-prefixed directories, and intercept convention directories only live inside @slot dirs. Not a real concern.

  5. Edge case: climbing more levels than visible segments exist — the while (cutIndex > 0 && climbed < levelsToClimb) loop handles this gracefully by stopping at index 0, producing baseParts = [] (same as root). This matches Next.js behavior where over-climbing resolves to root.

  6. All 90 routing tests pass, including the 3 new test cases.

Tests

The three new tests provide good coverage:

  • (..)(..) with a single route group between segments
  • (..) with a single route group
  • (..)(..) with multiple consecutive route groups

Clean PR — LGTM.

* Compute the target URL pattern for an intercepting route.
*
* Interception conventions (..), (..)(..)" climb by *visible route segments*
* (not filesystem directories). Route groups like (marketing) and parallel
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.

Nit: stray " in the doc comment — (..)(..)".

Suggested change
* (not filesystem directories). Route groups like (marketing) and parallel
* Interception conventions (..), (..)(..) climb by *visible route segments*

case "../..":
baseDir = path.dirname(path.dirname(routeDir));
case "../..": {
const levelsToClimb = convention === ".." ? 1 : 2;
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.

Worth noting: if levelsToClimb exceeds the available visible segments, the loop exits with cutIndex = 0 and climbed < levelsToClimb. This silently resolves to root, which matches Next.js behavior. If you ever want to emit a warning for "(..)(..) used but only 1 visible segment above", this is where it would go — but no action needed now.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Mar 12, 2026

Review submitted. Here's a summary of my findings:

Approved -- this is a correct fix for a real bug where (..) and (..)(..) intercept conventions were climbing by filesystem directories instead of visible route segments, causing wrong target resolution when route groups sat between visible segments.

Key points from the review:

  1. The fix is correct. The backward-walking loop that skips invisible segments (route groups, @slots) matches Next.js semantics exactly.

  2. The isInvisibleSegment consolidation is clean. Three duplicated inline checks collapsed into one helper with no behavioral regressions. The one subtle change (hasRemainingVisibleSegments now treating "." as invisible) is a consistency improvement — convertSegmentsToRouteParts already did this, and "." never appears in these code paths in practice.

  3. Edge cases are handled gracefully. Over-climbing resolves to root (matching Next.js). Leftover invisible segments in baseParts after climbing are correctly stripped by convertSegmentsToRouteParts downstream.

  4. Tests are solid. Three new cases covering single route group, route group between segments, and multiple consecutive route groups. All 90 routing tests pass.

I left two minor inline comments: a stray " in a doc comment, and a note about the over-climbing behavior for future reference.

github run

…em dirs

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.
@NathanDrake2406 NathanDrake2406 force-pushed the fix/intercept-route-segment-climbing branch from a3c4e94 to 1dd9ac9 Compare March 12, 2026 13:33
@james-elicx james-elicx enabled auto-merge (squash) March 12, 2026 13:35
@james-elicx james-elicx merged commit 3d05dce into cloudflare:main Mar 12, 2026
17 checks passed
@NathanDrake2406 NathanDrake2406 deleted the fix/intercept-route-segment-climbing branch March 18, 2026 09:50
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.

2 participants