Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/vinext/src/routing/app-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,9 @@ function discoverSlotSubRoutes(
}
}

if (subPathMap.size === 0) continue;

// Find the default.tsx for the children slot at the parent directory
const childrenDefault = findFile(parentPageDir, "default", matcher);
if (subPathMap.size === 0 || !childrenDefault) continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the right fix — previously, if childrenDefault was null, synthetic routes were created with pagePath: null, which would 500 at render time. Combining the two guards into one continue is clean.

Minor: the original code had the subPathMap.size === 0 check first (cheap) before the findFile call (does filesystem I/O). The new code always calls findFile even when subPathMap is empty. Consider preserving the short-circuit:

Suggested change
if (subPathMap.size === 0 || !childrenDefault) continue;
if (subPathMap.size === 0) continue;
// Find the default.tsx for the children slot at the parent directory
const childrenDefault = findFile(parentPageDir, "default", matcher);
if (!childrenDefault) continue;

This avoids a pointless findFile call when there are no sub-paths. Not a big deal since fs.existsSync is fast, but it's a free optimization and arguably clearer about the two independent exit conditions.


for (const [subPath, slotPages] of subPathMap) {
// Convert sub-path segments to URL pattern parts
Expand Down
18 changes: 18 additions & 0 deletions tests/routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,24 @@ describe("appRouter - route discovery", () => {
});
});

it("does not create nested @slot sub-routes without a children default fallback", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Good test. The name accurately describes what it asserts.

Note that this test gives @modal a default.tsx (line 484), so the slot passes the discoverParallelSlots filter. What's being tested is purely the childrenDefault guard in discoverSlotSubRoutes.

It might be worth adding a companion test for the other case: @modal with no root page.tsx/default.tsx and only @modal/profile/page.tsx — which tests that discoverParallelSlots itself excludes such a slot. That would document the current behavior and guard against future regressions if someone relaxes the filter.

await withTempDir("vinext-app-slot-missing-children-default-", async (tmpDir) => {
const appDir = path.join(tmpDir, "app");
await mkdir(path.join(appDir, "inbox", "@modal", "profile"), { recursive: true });
await writeFile(path.join(appDir, "inbox", "page.tsx"), EMPTY_PAGE);
await writeFile(path.join(appDir, "inbox", "@modal", "default.tsx"), EMPTY_PAGE);
await writeFile(path.join(appDir, "inbox", "@modal", "profile", "page.tsx"), EMPTY_PAGE);

invalidateAppRouteCache();
const routes = await appRouter(appDir);
const patterns = routes.map((route) => route.pattern);

expect(patterns).toContain("/inbox");
expect(patterns).not.toContain("/inbox/profile");
expect(matchAppRoute("/inbox/profile", routes)).toBeNull();
});
});

it("rejects non-terminal catch-all intercept targets", async () => {
await withTempDir("vinext-app-intercept-nonterminal-catchall-", async (tmpDir) => {
const appDir = path.join(tmpDir, "app");
Expand Down
Loading