From 30000071189e1a615e15303c47a0051de7714995 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 25 Oct 2022 09:58:10 -0400 Subject: [PATCH 1/7] fix: avoid collisions on pathless routes with indices (#4355) * fix: avoid collisions on pathless routes with indices * Add changeset * Remove unused import from e2e test --- .changeset/stale-spoons-tie.md | 5 + integration/conventional-routes-test.ts | 105 ++++++++++++++++++ packages/remix-dev/config/routesConvention.ts | 2 + 3 files changed, 112 insertions(+) create mode 100644 .changeset/stale-spoons-tie.md create mode 100644 integration/conventional-routes-test.ts diff --git a/.changeset/stale-spoons-tie.md b/.changeset/stale-spoons-tie.md new file mode 100644 index 00000000000..27458191df9 --- /dev/null +++ b/.changeset/stale-spoons-tie.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +Avoid collisions between pathless layout routes and their nested index routes diff --git a/integration/conventional-routes-test.ts b/integration/conventional-routes-test.ts new file mode 100644 index 00000000000..ff399894318 --- /dev/null +++ b/integration/conventional-routes-test.ts @@ -0,0 +1,105 @@ +import { test, expect } from "@playwright/test"; + +import { PlaywrightFixture } from "./helpers/playwright-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; + +let fixture: Fixture; +let appFixture: AppFixture; + +test.beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.tsx": js` + import { Link, Outlet, Scripts, useMatches } from "@remix-run/react"; + + export default function App() { + let matches = 'Number of matches: ' + useMatches().length; + return ( + + + +

{matches}

+ + + + + ); + } + `, + "app/routes/nested/index.jsx": js` + export default function Index() { + return

Index

; + } + `, + "app/routes/nested/__pathless.jsx": js` + import { Outlet } from "@remix-run/react"; + + export default function Layout() { + return ( + <> +
Pathless Layout
+ + + ); + } + `, + "app/routes/nested/__pathless/foo.jsx": js` + export default function Foo() { + return

Foo

; + } + `, + }, + }); + + appFixture = await createAppFixture(fixture); +}); + +test.afterAll(async () => appFixture.close()); + +test.describe("with JavaScript", () => { + runTests(); +}); + +test.describe("without JavaScript", () => { + test.use({ javaScriptEnabled: false }); + runTests(); +}); + +/** + * Routes for this test look like this, for reference for the matches assertions: + * + * + * + * + * + * + * + * + * + * + */ + +function runTests() { + test("displays index page and not pathless layout page", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/nested"); + expect(await app.getHtml()).toMatch("Index"); + expect(await app.getHtml()).not.toMatch("Pathless Layout"); + expect(await app.getHtml()).toMatch("Number of matches: 2"); + }); + + test("displays page inside of pathless layout", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/nested/foo"); + expect(await app.getHtml()).not.toMatch("Index"); + expect(await app.getHtml()).toMatch("Pathless Layout"); + expect(await app.getHtml()).toMatch("Foo"); + expect(await app.getHtml()).toMatch("Number of matches: 3"); + }); +} diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 742d1bb0e7c..41ee3775bbf 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -245,6 +245,8 @@ export function createRoutePath(partialRouteId: string): string | undefined { if (rawSegmentBuffer === "index" && result.endsWith("index")) { result = result.replace(/\/?index$/, ""); + } else { + result = result.replace(/\/$/, ""); } if (rawSegmentBuffer === "index" && result.endsWith("index?")) { From 8a64e5dd9be1931af86216a69c89075088f5a718 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 25 Oct 2022 17:52:43 -0400 Subject: [PATCH 2/7] fix path collision detection fr pathless layout routes --- integration/conventional-routes-test.ts | 27 +++ integration/route-collisions-test.ts | 164 ++++++++++++++++++ packages/remix-dev/config/routesConvention.ts | 58 ++++++- 3 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 integration/route-collisions-test.ts diff --git a/integration/conventional-routes-test.ts b/integration/conventional-routes-test.ts index ff399894318..665ad3516c1 100644 --- a/integration/conventional-routes-test.ts +++ b/integration/conventional-routes-test.ts @@ -54,6 +54,23 @@ test.beforeAll(async () => { return

Foo

; } `, + "app/routes/nested/__pathless2.jsx": js` + import { Outlet } from "@remix-run/react"; + + export default function Layout() { + return ( + <> +
Pathless 2 Layout
+ + + ); + } + `, + "app/routes/nested/__pathless2/bar.jsx": js` + export default function Bar() { + return

Bar

; + } + `, }, }); @@ -102,4 +119,14 @@ function runTests() { expect(await app.getHtml()).toMatch("Foo"); expect(await app.getHtml()).toMatch("Number of matches: 3"); }); + + // This also asserts that we support multiple sibling pathless route layouts + test("displays page inside of second pathless layout", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/nested/bar"); + expect(await app.getHtml()).not.toMatch("Index"); + expect(await app.getHtml()).toMatch("Pathless 2 Layout"); + expect(await app.getHtml()).toMatch("Bar"); + expect(await app.getHtml()).toMatch("Number of matches: 3"); + }); } diff --git a/integration/route-collisions-test.ts b/integration/route-collisions-test.ts new file mode 100644 index 00000000000..a8c852ccd4b --- /dev/null +++ b/integration/route-collisions-test.ts @@ -0,0 +1,164 @@ +import { test, expect } from "@playwright/test"; + +import { createFixture, js } from "./helpers/create-fixture"; + +let ROOT_FILE_CONTENTS = js` + import { Outlet, Scripts } from "@remix-run/react"; + + export default function App() { + return ( + + + + + + + ); + } +`; + +let LAYOUT_FILE_CONTENTS = js` + import { Outlet } from "@remix-run/react"; + + export default function Layout() { + return + } +`; + +let LEAF_FILE_CONTENTS = js` + export default function Foo() { + return

Foo

; + } +`; + +test.describe("build failures", () => { + let errorLogs: string[]; + let oldConsoleError: typeof console.error; + + test.beforeEach(() => { + errorLogs = []; + oldConsoleError = console.error; + console.error = (str) => errorLogs.push(str); + }); + + test.afterEach(() => { + console.error = oldConsoleError; + }); + + test("detects path collisions inside pathless layout routes", async () => { + try { + await createFixture({ + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/__pathless/foo.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/foo"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions across pathless layout routes", async () => { + try { + await createFixture({ + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/__pathless/foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/__pathless2.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/__pathless2/foo.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "foo" defined by route "routes/__pathless/foo" conflicts with route "routes/__pathless2/foo"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions inside multiple pathless layout routes", async () => { + try { + await createFixture({ + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/__pathless/__again.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/__pathless/__again/foo.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/__again/foo"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions of index files inside pathless layouts", async () => { + try { + await createFixture({ + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/index.jsx": LEAF_FILE_CONTENTS, + "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/__pathless/index.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "/" defined by route "routes/index" conflicts with route "routes/__pathless/index"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions of index files across multiple pathless layouts", async () => { + try { + await createFixture({ + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/nested/__pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/nested/__pathless/index.jsx": LEAF_FILE_CONTENTS, + "app/routes/nested/__oops.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/nested/__oops/index.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "nested" defined by route "routes/nested/__oops/index" conflicts with route "routes/nested/__pathless/index"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions of param routes inside pathless layouts", async () => { + try { + await createFixture({ + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/$param.jsx": LEAF_FILE_CONTENTS, + "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/__pathless/$param.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path ":param" defined by route "routes/$param" conflicts with route "routes/__pathless/$param"' + ); + expect(errorLogs.length).toBe(1); + } + }); +}); diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 41ee3775bbf..100215e099f 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -73,11 +73,63 @@ export function defineConventionalRoutes( let isIndexRoute = routeId.endsWith("/index"); let fullPath = createRoutePath(routeId.slice("routes".length + 1)); let uniqueRouteId = (fullPath || "") + (isIndexRoute ? "?index" : ""); - - if (uniqueRouteId) { + let isPathlessLayoutRoute = + routeId.split("/").pop()?.startsWith("__") === true; + + /** + * We do not try to detect path collisions for pathless layout route + * files because, by definition, they create the potential for route + * collisions _at that level in the tree_. For example, consider the + * following route structure: + * + * routes/ + * parent.tsx + * parent/ + * __pathless.tsx + * + * The route path for both parent.tsx and parent/__pathless.tsx + * is the same (/parent), but it's not expected you are matching at that + * level. It's up to you to handle that in your `parent.tsx` or provide + * an index route in your pathless route folder. + * + * Consider this more complex example where a user may want multiple + * pathless layout routes for different subfolders + * + * routes/ + * account.tsx + * account/ + * __public/ + * login.tsx + * perks.tsx + * __private/ + * orders.tsx + * profile.tsx + * __public.tsx + * __private.tsx + * + * In order to support both a public and private layout for `/account/*` + * URLs, we are creating a mutually exclusive set of URLs beneath 2 + * separate pathless layout routes. In this case, the route paths for + * both account/__public.tsx and account/__private.tsx is the same + * (/account), but we're again not expecting to match at that level. + * + * By only ignoring this check when the final portion of the filename is + * pathless, we will still detect path collisions such as: + * + * routes/parent/__pathless/foo.tsx + * routes/parent/__pathless2/foo.tsx + * + * and + * + * routes/parent/__pathless/index.tsx + * routes/parent/__pathless2/index.tsx + */ + if (uniqueRouteId && !isPathlessLayoutRoute) { if (uniqueRoutes.has(uniqueRouteId)) { throw new Error( - `Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify( + `Path ${JSON.stringify( + fullPath || "/" + )} defined by route ${JSON.stringify( routeId )} conflicts with route ${JSON.stringify( uniqueRoutes.get(uniqueRouteId) From a41fd133bb9a0af34ad61fb8ba24a56d53ee3a08 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 24 Jan 2023 15:32:06 -0500 Subject: [PATCH 3/7] Update changeset --- .changeset/stale-spoons-tie.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.changeset/stale-spoons-tie.md b/.changeset/stale-spoons-tie.md index 27458191df9..ffe2da09933 100644 --- a/.changeset/stale-spoons-tie.md +++ b/.changeset/stale-spoons-tie.md @@ -2,4 +2,7 @@ "@remix-run/dev": patch --- -Avoid collisions between pathless layout routes and their nested index routes +- Fix route ranking bug with pathless layout route next to a sibling index route + - Under the hood this is done by removing the trailing slash from all generated `path` values since the number of slash-delimited segments counts towards route ranking so the trailing slash incorrectly increases the score for routes + +- Support sibling pathless layout routes by removing pathless layout routes from the unique route path checks in conventional route generation since they inherently trigger duplicate paths \ No newline at end of file From 63400c643c48b02aa2a2e7631c2c57bcef193416 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 21 Apr 2023 16:55:07 -0400 Subject: [PATCH 4/7] Add same fixes for flat routes --- integration/flat-routes-test.ts | 129 ++++++++++++++++ integration/route-collisions-test.ts | 146 +++++++++++++++++- .../remix-dev/__tests__/flat-routes-test.ts | 76 +++++++++ packages/remix-dev/config/flat-routes.ts | 43 ++++++ packages/remix-dev/config/routesConvention.ts | 17 +- 5 files changed, 396 insertions(+), 15 deletions(-) diff --git a/integration/flat-routes-test.ts b/integration/flat-routes-test.ts index f685f4b4fff..31bc29e849c 100644 --- a/integration/flat-routes-test.ts +++ b/integration/flat-routes-test.ts @@ -312,3 +312,132 @@ test.describe("", () => { expect(buildOutput).not.toContain(`Route Path Collision`); }); }); + +test.describe("pathless routes and route collisions", () => { + test.beforeAll(async () => { + fixture = await createFixture({ + future: { v2_routeConvention: true }, + files: { + "app/root.tsx": js` + import { Link, Outlet, Scripts, useMatches } from "@remix-run/react"; + + export default function App() { + let matches = 'Number of matches: ' + useMatches().length; + return ( + + + +

{matches}

+ + + + + ); + } + `, + "app/routes/nested._index.jsx": js` + export default function Index() { + return

Index

; + } + `, + "app/routes/nested._pathless.jsx": js` + import { Outlet } from "@remix-run/react"; + + export default function Layout() { + return ( + <> +
Pathless Layout
+ + + ); + } + `, + "app/routes/nested._pathless.foo.jsx": js` + export default function Foo() { + return

Foo

; + } + `, + "app/routes/nested._pathless2.jsx": js` + import { Outlet } from "@remix-run/react"; + + export default function Layout() { + return ( + <> +
Pathless 2 Layout
+ + + ); + } + `, + "app/routes/nested._pathless2.bar.jsx": js` + export default function Bar() { + return

Bar

; + } + `, + }, + }); + + appFixture = await createAppFixture(fixture); + }); + + test.afterAll(async () => appFixture.close()); + + test.describe("with JavaScript", () => { + runTests(); + }); + + test.describe("without JavaScript", () => { + test.use({ javaScriptEnabled: false }); + runTests(); + }); + + /** + * Routes for this test look like this, for reference for the matches assertions: + * + * + * + * + * + * + * + * + * + * + */ + + function runTests() { + test("displays index page and not pathless layout page", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/nested"); + expect(await app.getHtml()).toMatch("Index"); + expect(await app.getHtml()).not.toMatch("Pathless Layout"); + expect(await app.getHtml()).toMatch("Number of matches: 2"); + }); + + test("displays page inside of pathless layout", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/nested/foo"); + expect(await app.getHtml()).not.toMatch("Index"); + expect(await app.getHtml()).toMatch("Pathless Layout"); + expect(await app.getHtml()).toMatch("Foo"); + expect(await app.getHtml()).toMatch("Number of matches: 3"); + }); + + // This also asserts that we support multiple sibling pathless route layouts + test("displays page inside of second pathless layout", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/nested/bar"); + expect(await app.getHtml()).not.toMatch("Index"); + expect(await app.getHtml()).toMatch("Pathless 2 Layout"); + expect(await app.getHtml()).toMatch("Bar"); + expect(await app.getHtml()).toMatch("Number of matches: 3"); + }); + } +}); diff --git a/integration/route-collisions-test.ts b/integration/route-collisions-test.ts index a8c852ccd4b..211e80018df 100644 --- a/integration/route-collisions-test.ts +++ b/integration/route-collisions-test.ts @@ -31,7 +31,7 @@ let LEAF_FILE_CONTENTS = js` } `; -test.describe("build failures", () => { +test.describe("build failures (v1 routes)", () => { let errorLogs: string[]; let oldConsoleError: typeof console.error; @@ -48,6 +48,7 @@ test.describe("build failures", () => { test("detects path collisions inside pathless layout routes", async () => { try { await createFixture({ + future: { v2_routeConvention: false }, files: { "app/root.tsx": ROOT_FILE_CONTENTS, "app/routes/foo.jsx": LEAF_FILE_CONTENTS, @@ -67,6 +68,7 @@ test.describe("build failures", () => { test("detects path collisions across pathless layout routes", async () => { try { await createFixture({ + future: { v2_routeConvention: false }, files: { "app/root.tsx": ROOT_FILE_CONTENTS, "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS, @@ -87,6 +89,7 @@ test.describe("build failures", () => { test("detects path collisions inside multiple pathless layout routes", async () => { try { await createFixture({ + future: { v2_routeConvention: false }, files: { "app/root.tsx": ROOT_FILE_CONTENTS, "app/routes/foo.jsx": LEAF_FILE_CONTENTS, @@ -107,6 +110,7 @@ test.describe("build failures", () => { test("detects path collisions of index files inside pathless layouts", async () => { try { await createFixture({ + future: { v2_routeConvention: false }, files: { "app/root.tsx": ROOT_FILE_CONTENTS, "app/routes/index.jsx": LEAF_FILE_CONTENTS, @@ -126,6 +130,7 @@ test.describe("build failures", () => { test("detects path collisions of index files across multiple pathless layouts", async () => { try { await createFixture({ + future: { v2_routeConvention: false }, files: { "app/root.tsx": ROOT_FILE_CONTENTS, "app/routes/nested/__pathless.jsx": LAYOUT_FILE_CONTENTS, @@ -146,6 +151,7 @@ test.describe("build failures", () => { test("detects path collisions of param routes inside pathless layouts", async () => { try { await createFixture({ + future: { v2_routeConvention: false }, files: { "app/root.tsx": ROOT_FILE_CONTENTS, "app/routes/$param.jsx": LEAF_FILE_CONTENTS, @@ -162,3 +168,141 @@ test.describe("build failures", () => { } }); }); + +test.describe("build failures (v2 routes)", () => { + let errorLogs: string[]; + let oldConsoleError: typeof console.error; + + test.beforeEach(() => { + errorLogs = []; + oldConsoleError = console.error; + console.error = (str) => errorLogs.push(str); + }); + + test.afterEach(() => { + console.error = oldConsoleError; + }); + + test("detects path collisions inside pathless layout routes", async () => { + try { + await createFixture({ + future: { v2_routeConvention: true }, + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless.foo.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/foo"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions across pathless layout routes", async () => { + try { + await createFixture({ + future: { v2_routeConvention: true }, + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless.foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless2.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless2.foo.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "foo" defined by route "routes/__pathless/foo" conflicts with route "routes/__pathless2/foo"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions inside multiple pathless layout routes", async () => { + try { + await createFixture({ + future: { v2_routeConvention: true }, + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless._again.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless._again.foo.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/__again/foo"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions of index files inside pathless layouts", async () => { + try { + await createFixture({ + future: { v2_routeConvention: true }, + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/_index.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless._index.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "/" defined by route "routes/index" conflicts with route "routes/__pathless/index"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions of index files across multiple pathless layouts", async () => { + try { + await createFixture({ + future: { v2_routeConvention: true }, + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/nested._pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/nested._pathless._index.jsx": LEAF_FILE_CONTENTS, + "app/routes/nested._oops.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/nested._oops._index.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path "nested" defined by route "routes/nested/__oops/index" conflicts with route "routes/nested/__pathless/index"' + ); + expect(errorLogs.length).toBe(1); + } + }); + + test("detects path collisions of param routes inside pathless layouts", async () => { + try { + await createFixture({ + future: { v2_routeConvention: true }, + files: { + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/$param.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless.$param.jsx": LEAF_FILE_CONTENTS, + }, + }); + expect(false).toBe(true); + } catch (e) { + expect(errorLogs[0]).toMatch( + 'Error: Path ":param" defined by route "routes/$param" conflicts with route "routes/__pathless/$param"' + ); + expect(errorLogs.length).toBe(1); + } + }); +}); diff --git a/packages/remix-dev/__tests__/flat-routes-test.ts b/packages/remix-dev/__tests__/flat-routes-test.ts index 8929c5084ff..9c4dc15e3dc 100644 --- a/packages/remix-dev/__tests__/flat-routes-test.ts +++ b/packages/remix-dev/__tests__/flat-routes-test.ts @@ -712,5 +712,81 @@ describe("flatRoutes", () => { getRoutePathConflictErrorMessage("/products/:pid", testFiles) ); }); + + test("pathless layouts should not collide", () => { + let testFiles = [ + path.join(APP_DIR, "routes", "_a.tsx"), + path.join(APP_DIR, "routes", "_a._index.tsx"), + path.join(APP_DIR, "routes", "_a.a.tsx"), + path.join(APP_DIR, "routes", "_b.tsx"), + path.join(APP_DIR, "routes", "_b.b.tsx"), + ]; + + let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + let routes = Object.values(routeManifest); + + expect(consoleError).not.toHaveBeenCalled(); + expect(routes).toHaveLength(5); + }); + + test("nested pathless layouts should not collide", () => { + let testFiles = [ + path.join(APP_DIR, "routes", "nested._a.tsx"), + path.join(APP_DIR, "routes", "nested._a._index.tsx"), + path.join(APP_DIR, "routes", "nested._a.a.tsx"), + path.join(APP_DIR, "routes", "nested._b.tsx"), + path.join(APP_DIR, "routes", "nested._b.b.tsx"), + ]; + + let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + let routes = Object.values(routeManifest); + + expect(consoleError).not.toHaveBeenCalled(); + expect(routes).toHaveLength(5); + }); + + test("legit collisions without nested pathless layouts should collide", () => { + let testFiles = [ + path.join(APP_DIR, "routes", "nested._a.tsx"), + path.join(APP_DIR, "routes", "nested._a.a.tsx"), + path.join(APP_DIR, "routes", "nested._b.tsx"), + path.join(APP_DIR, "routes", "nested._b.a.tsx"), + ]; + + let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + let routes = Object.values(routeManifest); + + expect(consoleError).toHaveBeenCalledWith( + getRoutePathConflictErrorMessage("/nested/a", [ + "routes/nested._a.a.tsx", + "routes/nested._b.a.tsx", + ]) + ); + expect(routes).toHaveLength(3); + }); + + test("legit collisions without nested pathless layouts should collide", () => { + let testFiles = [ + path.join(APP_DIR, "routes", "nested._a.tsx"), + path.join(APP_DIR, "routes", "nested._a._index.tsx"), + path.join(APP_DIR, "routes", "nested._b.tsx"), + path.join(APP_DIR, "routes", "nested._b._index.tsx"), + ]; + + let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + let routes = Object.values(routeManifest); + + expect(consoleError).toHaveBeenCalledWith( + getRoutePathConflictErrorMessage("/nested", [ + "routes/nested._a._index.tsx", + "routes/nested._b._index.tsx", + ]) + ); + expect(routes).toHaveLength(3); + }); }); }); diff --git a/packages/remix-dev/config/flat-routes.ts b/packages/remix-dev/config/flat-routes.ts index e4f49393f94..b83f939b72d 100644 --- a/packages/remix-dev/config/flat-routes.ts +++ b/packages/remix-dev/config/flat-routes.ts @@ -214,6 +214,49 @@ export function flatRoutesUniversal( .replace(/\/$/, ""); } + /** + * We do not try to detect path collisions for pathless layout route + * files because, by definition, they create the potential for route + * collisions _at that level in the tree_. + * + * Consider example where a user may want multiple pathless layout routes + * for different subfolders + * + * routes/ + * account.tsx + * account._private.tsx + * account._private.orders.tsx + * account._private.profile.tsx + * account._public.tsx + * account._public.login.tsx + * account._public.perks.tsx + * + * In order to support both a public and private layout for `/account/*` + * URLs, we are creating a mutually exclusive set of URLs beneath 2 + * separate pathless layout routes. In this case, the route paths for + * both account._public.tsx and account._private.tsx is the same + * (/account), but we're again not expecting to match at that level. + * + * By only ignoring this check when the final portion of the filename is + * pathless, we will still detect path collisions such as: + * + * routes/parent._pathless.foo.tsx + * routes/parent._pathless2.foo.tsx + * + * and + * + * routes/parent._pathless/index.tsx + * routes/parent._pathless2/index.tsx + */ + let lastRouteSegment = config.id.split(".").pop(); + let isPathlessLayoutRoute = + lastRouteSegment && + lastRouteSegment.startsWith("_") && + lastRouteSegment !== "_index"; + if (isPathlessLayoutRoute) { + continue; + } + let conflictRouteId = originalPathname + (config.index ? "?index" : ""); let conflict = uniqueRoutes.get(conflictRouteId); diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 100215e099f..35713e756bc 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -79,21 +79,10 @@ export function defineConventionalRoutes( /** * We do not try to detect path collisions for pathless layout route * files because, by definition, they create the potential for route - * collisions _at that level in the tree_. For example, consider the - * following route structure: + * collisions _at that level in the tree_. * - * routes/ - * parent.tsx - * parent/ - * __pathless.tsx - * - * The route path for both parent.tsx and parent/__pathless.tsx - * is the same (/parent), but it's not expected you are matching at that - * level. It's up to you to handle that in your `parent.tsx` or provide - * an index route in your pathless route folder. - * - * Consider this more complex example where a user may want multiple - * pathless layout routes for different subfolders + * Consider example where a user may want multiple pathless layout routes + * for different subfolders * * routes/ * account.tsx From 1879cadd51dcb3d1d12ab365ade0ab4bb7b7124d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 16 May 2023 14:19:47 -0400 Subject: [PATCH 5/7] Add more tests for folder/route.tsx flat routes convention --- .../remix-dev/__tests__/flat-routes-test.ts | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/__tests__/flat-routes-test.ts b/packages/remix-dev/__tests__/flat-routes-test.ts index 9c4dc15e3dc..ec8fa4f9ffd 100644 --- a/packages/remix-dev/__tests__/flat-routes-test.ts +++ b/packages/remix-dev/__tests__/flat-routes-test.ts @@ -728,6 +728,22 @@ describe("flatRoutes", () => { expect(consoleError).not.toHaveBeenCalled(); expect(routes).toHaveLength(5); + + // When using folders and route.tsx files + testFiles = [ + path.join(APP_DIR, "routes", "_a", "route.tsx"), + path.join(APP_DIR, "routes", "_a._index", "route.tsx"), + path.join(APP_DIR, "routes", "_a.a", "route.tsx"), + path.join(APP_DIR, "routes", "_b", "route.tsx"), + path.join(APP_DIR, "routes", "_b.b", "route.tsx"), + ]; + + routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + routes = Object.values(routeManifest); + + expect(consoleError).not.toHaveBeenCalled(); + expect(routes).toHaveLength(5); }); test("nested pathless layouts should not collide", () => { @@ -745,9 +761,25 @@ describe("flatRoutes", () => { expect(consoleError).not.toHaveBeenCalled(); expect(routes).toHaveLength(5); + + // When using folders and route.tsx files + testFiles = [ + path.join(APP_DIR, "routes", "nested._a", "route.tsx"), + path.join(APP_DIR, "routes", "nested._a._index", "route.tsx"), + path.join(APP_DIR, "routes", "nested._a.a", "route.tsx"), + path.join(APP_DIR, "routes", "nested._b", "route.tsx"), + path.join(APP_DIR, "routes", "nested._b.b", "route.tsx"), + ]; + + routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + routes = Object.values(routeManifest); + + expect(consoleError).not.toHaveBeenCalled(); + expect(routes).toHaveLength(5); }); - test("legit collisions without nested pathless layouts should collide", () => { + test("legit collisions without nested pathless layouts should collide (paths)", () => { let testFiles = [ path.join(APP_DIR, "routes", "nested._a.tsx"), path.join(APP_DIR, "routes", "nested._a.a.tsx"), @@ -766,9 +798,30 @@ describe("flatRoutes", () => { ]) ); expect(routes).toHaveLength(3); + + // When using folders and route.tsx files + consoleError.mockClear(); + testFiles = [ + path.join(APP_DIR, "routes", "nested._a", "route.tsx"), + path.join(APP_DIR, "routes", "nested._a.a", "route.tsx"), + path.join(APP_DIR, "routes", "nested._b", "route.tsx"), + path.join(APP_DIR, "routes", "nested._b.a", "route.tsx"), + ]; + + routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + routes = Object.values(routeManifest); + + expect(consoleError).toHaveBeenCalledWith( + getRoutePathConflictErrorMessage("/nested/a", [ + "routes/nested._a.a/route.tsx", + "routes/nested._b.a/route.tsx", + ]) + ); + expect(routes).toHaveLength(3); }); - test("legit collisions without nested pathless layouts should collide", () => { + test("legit collisions without nested pathless layouts should collide (index routes)", () => { let testFiles = [ path.join(APP_DIR, "routes", "nested._a.tsx"), path.join(APP_DIR, "routes", "nested._a._index.tsx"), @@ -787,6 +840,27 @@ describe("flatRoutes", () => { ]) ); expect(routes).toHaveLength(3); + + // When using folders and route.tsx files + consoleError.mockClear(); + testFiles = [ + path.join(APP_DIR, "routes", "nested._a", "route.tsx"), + path.join(APP_DIR, "routes", "nested._a._index", "route.tsx"), + path.join(APP_DIR, "routes", "nested._b", "route.tsx"), + path.join(APP_DIR, "routes", "nested._b._index", "route.tsx"), + ]; + + routeManifest = flatRoutesUniversal(APP_DIR, testFiles); + + routes = Object.values(routeManifest); + + expect(consoleError).toHaveBeenCalledWith( + getRoutePathConflictErrorMessage("/nested", [ + "routes/nested._a._index/route.tsx", + "routes/nested._b._index/route.tsx", + ]) + ); + expect(routes).toHaveLength(3); }); }); }); From 93c48d0923a1af621e35b7ac7d6f2dec8a08f1a7 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 May 2023 12:03:27 -0400 Subject: [PATCH 6/7] Update integration tests for v2 routes --- integration/route-collisions-test.ts | 203 ++++++++++------------- packages/remix-dev/config/flat-routes.ts | 5 +- 2 files changed, 93 insertions(+), 115 deletions(-) diff --git a/integration/route-collisions-test.ts b/integration/route-collisions-test.ts index 211e80018df..f84cf504e10 100644 --- a/integration/route-collisions-test.ts +++ b/integration/route-collisions-test.ts @@ -1,3 +1,4 @@ +import { PassThrough } from "node:stream"; import { test, expect } from "@playwright/test"; import { createFixture, js } from "./helpers/create-fixture"; @@ -169,140 +170,114 @@ test.describe("build failures (v1 routes)", () => { }); }); -test.describe("build failures (v2 routes)", () => { - let errorLogs: string[]; - let oldConsoleError: typeof console.error; +test.describe.only("build failures (v2 routes)", () => { + let originalConsoleLog = console.log; + let originalConsoleWarn = console.warn; + let originalConsoleError = console.error; - test.beforeEach(() => { - errorLogs = []; - oldConsoleError = console.error; - console.error = (str) => errorLogs.push(str); + test.beforeAll(async () => { + console.log = () => {}; + console.warn = () => {}; + console.error = () => {}; }); - test.afterEach(() => { - console.error = oldConsoleError; + test.afterAll(() => { + console.log = originalConsoleLog; + console.warn = originalConsoleWarn; + console.error = originalConsoleError; }); - test("detects path collisions inside pathless layout routes", async () => { - try { - await createFixture({ - future: { v2_routeConvention: true }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/foo.jsx": LEAF_FILE_CONTENTS, - "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/_pathless.foo.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/foo"' + async function setup(files: Record) { + let buildStdio = new PassThrough(); + let buildOutput: string; + await createFixture({ + buildStdio, + future: { v2_routeConvention: true }, + files, + }); + let chunks: Buffer[] = []; + buildOutput = await new Promise((resolve, reject) => { + buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk))); + buildStdio.on("error", (err) => reject(err)); + buildStdio.on("end", () => + resolve(Buffer.concat(chunks).toString("utf8")) ); - expect(errorLogs.length).toBe(1); - } + }); + return buildOutput; + } + + test("detects path collisions inside pathless layout routes", async () => { + let buildOutput = await setup({ + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless.foo.jsx": LEAF_FILE_CONTENTS, + }); + expect(buildOutput).toContain(`⚠️ Route Path Collision: "/foo"`); + expect(buildOutput).toContain(`🟢 routes/_pathless.foo.jsx`); + expect(buildOutput).toContain(`⭕️️ routes/foo.jsx`); }); test("detects path collisions across pathless layout routes", async () => { - try { - await createFixture({ - future: { v2_routeConvention: true }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/_pathless.foo.jsx": LEAF_FILE_CONTENTS, - "app/routes/_pathless2.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/_pathless2.foo.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "foo" defined by route "routes/__pathless/foo" conflicts with route "routes/__pathless2/foo"' - ); - expect(errorLogs.length).toBe(1); - } + let buildOutput = await setup({ + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless.foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless2.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless2.foo.jsx": LEAF_FILE_CONTENTS, + }); + expect(buildOutput).toContain(`⚠️ Route Path Collision: "/foo"`); + expect(buildOutput).toContain(`🟢 routes/_pathless2.foo.jsx`); + expect(buildOutput).toContain(`⭕️️ routes/_pathless.foo.jsx`); }); test("detects path collisions inside multiple pathless layout routes", async () => { - try { - await createFixture({ - future: { v2_routeConvention: true }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/foo.jsx": LEAF_FILE_CONTENTS, - "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/_pathless._again.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/_pathless._again.foo.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/__again/foo"' - ); - expect(errorLogs.length).toBe(1); - } + let buildOutput = await setup({ + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/foo.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless._again.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless._again.foo.jsx": LEAF_FILE_CONTENTS, + }); + expect(buildOutput).toContain(`⚠️ Route Path Collision: "/foo"`); + expect(buildOutput).toContain(`🟢 routes/_pathless._again.foo.jsx`); + expect(buildOutput).toContain(`⭕️️ routes/foo.jsx`); }); test("detects path collisions of index files inside pathless layouts", async () => { - try { - await createFixture({ - future: { v2_routeConvention: true }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/_index.jsx": LEAF_FILE_CONTENTS, - "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/_pathless._index.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "/" defined by route "routes/index" conflicts with route "routes/__pathless/index"' - ); - expect(errorLogs.length).toBe(1); - } + let buildOutput = await setup({ + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/_index.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless._index.jsx": LEAF_FILE_CONTENTS, + }); + expect(buildOutput).toContain(`⚠️ Route Path Collision: "/"`); + expect(buildOutput).toContain(`🟢 routes/_pathless._index.jsx`); + expect(buildOutput).toContain(`⭕️️ routes/_index.jsx`); }); test("detects path collisions of index files across multiple pathless layouts", async () => { - try { - await createFixture({ - future: { v2_routeConvention: true }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/nested._pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/nested._pathless._index.jsx": LEAF_FILE_CONTENTS, - "app/routes/nested._oops.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/nested._oops._index.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path "nested" defined by route "routes/nested/__oops/index" conflicts with route "routes/nested/__pathless/index"' - ); - expect(errorLogs.length).toBe(1); - } + let buildOutput = await setup({ + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/nested._pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/nested._pathless._index.jsx": LEAF_FILE_CONTENTS, + "app/routes/nested._oops.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/nested._oops._index.jsx": LEAF_FILE_CONTENTS, + }); + expect(buildOutput).toContain(`⚠️ Route Path Collision: "/nested"`); + expect(buildOutput).toContain(`🟢 routes/nested._pathless._index.jsx`); + expect(buildOutput).toContain(`⭕️️ routes/nested._oops._index.jsx`); }); test("detects path collisions of param routes inside pathless layouts", async () => { - try { - await createFixture({ - future: { v2_routeConvention: true }, - files: { - "app/root.tsx": ROOT_FILE_CONTENTS, - "app/routes/$param.jsx": LEAF_FILE_CONTENTS, - "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, - "app/routes/_pathless.$param.jsx": LEAF_FILE_CONTENTS, - }, - }); - expect(false).toBe(true); - } catch (e) { - expect(errorLogs[0]).toMatch( - 'Error: Path ":param" defined by route "routes/$param" conflicts with route "routes/__pathless/$param"' - ); - expect(errorLogs.length).toBe(1); - } + let buildOutput = await setup({ + "app/root.tsx": ROOT_FILE_CONTENTS, + "app/routes/$param.jsx": LEAF_FILE_CONTENTS, + "app/routes/_pathless.jsx": LAYOUT_FILE_CONTENTS, + "app/routes/_pathless.$param.jsx": LEAF_FILE_CONTENTS, + }); + expect(buildOutput).toContain(`⚠️ Route Path Collision: "/:param"`); + expect(buildOutput).toContain(`🟢 routes/_pathless.$param.jsx`); + expect(buildOutput).toContain(`⭕️️ routes/$param.jsx`); }); }); diff --git a/packages/remix-dev/config/flat-routes.ts b/packages/remix-dev/config/flat-routes.ts index b83f939b72d..b0c9a2007f9 100644 --- a/packages/remix-dev/config/flat-routes.ts +++ b/packages/remix-dev/config/flat-routes.ts @@ -248,7 +248,10 @@ export function flatRoutesUniversal( * routes/parent._pathless/index.tsx * routes/parent._pathless2/index.tsx */ - let lastRouteSegment = config.id.split(".").pop(); + let lastRouteSegment = config.id + .replace(new RegExp(`^${prefix}/`), "") + .split(".") + .pop(); let isPathlessLayoutRoute = lastRouteSegment && lastRouteSegment.startsWith("_") && From cf00df997b3e162787cc6b3f075065041a1860ec Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 May 2023 14:23:07 -0400 Subject: [PATCH 7/7] Move route parentId assignment before the short circuit --- integration/route-collisions-test.ts | 2 +- packages/remix-dev/config/flat-routes.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/integration/route-collisions-test.ts b/integration/route-collisions-test.ts index f84cf504e10..d6ea3606804 100644 --- a/integration/route-collisions-test.ts +++ b/integration/route-collisions-test.ts @@ -170,7 +170,7 @@ test.describe("build failures (v1 routes)", () => { }); }); -test.describe.only("build failures (v2 routes)", () => { +test.describe("build failures (v2 routes)", () => { let originalConsoleLog = console.log; let originalConsoleWarn = console.warn; let originalConsoleError = console.error; diff --git a/packages/remix-dev/config/flat-routes.ts b/packages/remix-dev/config/flat-routes.ts index b0c9a2007f9..70811b727cb 100644 --- a/packages/remix-dev/config/flat-routes.ts +++ b/packages/remix-dev/config/flat-routes.ts @@ -214,6 +214,8 @@ export function flatRoutesUniversal( .replace(/\/$/, ""); } + if (!config.parentId) config.parentId = "root"; + /** * We do not try to detect path collisions for pathless layout route * files because, by definition, they create the potential for route @@ -263,7 +265,6 @@ export function flatRoutesUniversal( let conflictRouteId = originalPathname + (config.index ? "?index" : ""); let conflict = uniqueRoutes.get(conflictRouteId); - if (!config.parentId) config.parentId = "root"; config.path = pathname || undefined; uniqueRoutes.set(conflictRouteId, config);