Skip to content
Merged
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
15 changes: 13 additions & 2 deletions packages/vinext/src/routing/app-route-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,16 @@ export async function buildAppRouteGraph(

const route = directoryToAppRoute(dir, appDir, scanMatcher, null, null);
if (!route) continue;
const optionalCatchAllOwnsPattern = routes.some(
(candidate) =>
candidate.patternParts.length === route.patternParts.length + 1 &&
candidate.patternParts.at(-1)?.endsWith("*") &&
patternsStructurallyEquivalent(candidate.patternParts.slice(0, -1), route.patternParts),
);
if (optionalCatchAllOwnsPattern) {
ghostParentRoutes.push(route);
continue;
}
if (routePatterns.has(route.pattern)) {
ghostParentRoutes.push(route);
continue;
Expand Down Expand Up @@ -1510,6 +1520,7 @@ function directoryToAppRoute(

// Discover loading, error in the route's directory.
const routeDir = dir === "." ? appDir : path.posix.join(appDir, dir);
const effectivePagePath = pagePath ?? (routePath ? null : findFile(routeDir, "default", matcher));
const loadingPath = findFile(routeDir, "loading", matcher);
const errorPath = findFile(routeDir, "error", matcher);

Expand All @@ -1533,15 +1544,15 @@ function directoryToAppRoute(
return {
ids: createAppRouteSemanticIds({
pattern: pattern === "/" ? "/" : pattern,
pagePath,
pagePath: effectivePagePath,
routePath,
routeSegments: segments,
layoutTreePositions,
templateTreePositions,
slots: parallelSlots,
}),
pattern: pattern === "/" ? "/" : pattern,
pagePath,
pagePath: effectivePagePath,
routePath,
layouts,
templates,
Expand Down
75 changes: 75 additions & 0 deletions tests/app-route-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,81 @@ describe("App Router route graph builder", () => {
});
});

// Ported from Next.js:
// test/e2e/app-dir/parallel-routes-catchall-default/parallel-routes-catchall-default.test.ts
// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/parallel-routes-catchall-default/parallel-routes-catchall-default.test.ts
it("matches nested parallel slot pages before an ancestor optional catch-all", async () => {
await withTempApp(async (appDir) => {
await writeAppFile(appDir, "layout.tsx", EMPTY_LAYOUT);
await writeAppFile(appDir, "page.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/[[...catchAll]]/page.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/nested/[foo]/[bar]/layout.tsx", EMPTY_LAYOUT);
await writeAppFile(appDir, "[locale]/nested/[foo]/[bar]/default.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/nested/[foo]/[bar]/@slot/default.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/nested/[foo]/[bar]/@slot/page.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/nested/[foo]/[bar]/@slot/[baz]/default.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/nested/[foo]/[bar]/@slot/[baz]/page.tsx", EMPTY_PAGE);

const graph = await buildAppRouteGraph(appDir, createValidFileMatcher());
const nested = findRoute(graph.routes, "/:locale/nested/:foo/:bar");
const nestedBaz = findRoute(graph.routes, "/:locale/nested/:foo/:bar/:baz");

expect(nested.pagePath).toBe(path.join(appDir, "[locale]/nested/[foo]/[bar]/default.tsx"));
expect(nested.parallelSlots.find((slot) => slot.name === "slot")?.pagePath).toBe(
path.join(appDir, "[locale]/nested/[foo]/[bar]/@slot/page.tsx"),
);
expect(nestedBaz.pagePath).toBe(path.join(appDir, "[locale]/nested/[foo]/[bar]/default.tsx"));
expect(nestedBaz.parallelSlots.find((slot) => slot.name === "slot")?.pagePath).toBe(
path.join(appDir, "[locale]/nested/[foo]/[bar]/@slot/[baz]/page.tsx"),
);
});
});

// Ported from Next.js:
// test/e2e/app-dir/parallel-routes-catchall-slotted-non-catchalls/parallel-routes-catchall-slotted-non-catchalls.test.ts
// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/parallel-routes-catchall-slotted-non-catchalls/parallel-routes-catchall-slotted-non-catchalls.test.ts
it("lets an optional catch-all supply children for a layout-only slot owner", async () => {
await withTempApp(async (appDir) => {
await writeAppFile(appDir, "layout.tsx", EMPTY_LAYOUT);
await writeAppFile(appDir, "page.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/layout.tsx", EMPTY_LAYOUT);
await writeAppFile(appDir, "[locale]/default.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/@slot/default.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/@slot/other/page.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "[locale]/[[...catchAll]]/page.tsx", EMPTY_PAGE);

const graph = await buildAppRouteGraph(appDir, createValidFileMatcher());
const localeCatchAll = findRoute(graph.routes, "/:locale/:catchAll*");

expect(graph.routes.some((route) => route.pattern === "/:locale")).toBe(false);
expect(localeCatchAll.parallelSlots.find((slot) => slot.name === "slot")).toMatchObject({
pagePath: null,
defaultPath: path.join(appDir, "[locale]/@slot/default.tsx"),
});
});
});

it("keeps a standalone layout-only slot owner as a route", async () => {
await withTempApp(async (appDir) => {
await writeAppFile(appDir, "layout.tsx", EMPTY_LAYOUT);
await writeAppFile(appDir, "parallel-nested/home/layout.tsx", EMPTY_LAYOUT);
await writeAppFile(appDir, "parallel-nested/home/@parallel/default.tsx", EMPTY_PAGE);
await writeAppFile(appDir, "parallel-nested/home/@parallel/nested/page.tsx", EMPTY_PAGE);

const graph = await buildAppRouteGraph(appDir, createValidFileMatcher());
const parent = findRoute(graph.routes, "/parallel-nested/home");
const nested = findRoute(graph.routes, "/parallel-nested/home/nested");

expect(parent.pagePath).toBeNull();
expect(parent.parallelSlots.find((slot) => slot.name === "parallel")?.defaultPath).toBe(
path.join(appDir, "parallel-nested/home/@parallel/default.tsx"),
);
expect(nested.parallelSlots.find((slot) => slot.name === "parallel")?.pagePath).toBe(
path.join(appDir, "parallel-nested/home/@parallel/nested/page.tsx"),
);
});
});

// Ported from Next.js: test/e2e/app-dir/parallel-routes-group-depth/parallel-routes-group-depth.test.ts
// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/parallel-routes-group-depth/parallel-routes-group-depth.test.ts
it("keeps a sibling slot active when children are inside a route group", async () => {
Expand Down
16 changes: 16 additions & 0 deletions tests/e2e/app-router/parallel-slot-default.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Ported from Next.js:
// test/e2e/app-dir/parallel-routes-catchall-default/parallel-routes-catchall-default.test.ts
// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/parallel-routes-catchall-default/parallel-routes-catchall-default.test.ts

import { expect, test } from "@playwright/test";

const BASE = "http://localhost:4174";

test("renders the implicit children default beside a nested parallel slot", async ({ page }) => {
await page.goto(`${BASE}/parallel-slot-default/nested/foo/bar/baz`);

await expect(page.getByTestId("parallel-slot-default-children")).toHaveText(
"nested children default",
);
await expect(page.getByTestId("parallel-slot-default-slot")).toHaveText("slot dynamic page: baz");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default async function Page({ params }: { params: Promise<{ baz: string }> }) {
const { baz } = await params;
return <p>slot dynamic page: {baz}</p>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return <p>slot default</p>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>slot root page</p>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return <p>nested children default</p>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function Layout({
children,
slot,
}: {
children: React.ReactNode;
slot: React.ReactNode;
}) {
return (
<main>
<div data-testid="parallel-slot-default-children">{children}</div>
<div data-testid="parallel-slot-default-slot">{slot}</div>
</main>
);
}
Loading