diff --git a/apps/builder/app/canvas/interceptor.ts b/apps/builder/app/canvas/interceptor.ts index 908c28403770..2f62062a6cf3 100644 --- a/apps/builder/app/canvas/interceptor.ts +++ b/apps/builder/app/canvas/interceptor.ts @@ -11,6 +11,7 @@ import { $selectedPageHash, } from "~/shared/nano-states"; import { $currentSystem, updateCurrentSystem } from "~/shared/system"; +import { comparePatterns } from "./shared/routing-priority"; const isAbsoluteUrl = (href: string) => { try { @@ -55,7 +56,11 @@ const switchPageAndUpdateSystem = (href: string, formData?: FormData) => { } } const pageHref = new URL(href, "https://any-valid.url"); - for (const page of [pages.homePage, ...pages.pages]) { + // sort pages before matching to not depend on order of page creation + const sortedPages = [pages.homePage, ...pages.pages].toSorted( + (leftPage, rightPage) => comparePatterns(leftPage.path, rightPage.path) + ); + for (const page of sortedPages) { const pagePath = getPagePath(page.id, pages); const params = matchPathnamePattern(pagePath, pageHref.pathname); if (params) { diff --git a/apps/builder/app/canvas/shared/routing-priority.test.ts b/apps/builder/app/canvas/shared/routing-priority.test.ts new file mode 100644 index 000000000000..b771a9ee8fc0 --- /dev/null +++ b/apps/builder/app/canvas/shared/routing-priority.test.ts @@ -0,0 +1,50 @@ +import { test, expect } from "vitest"; +import { comparePatterns } from "./routing-priority"; + +test("sort top-level patterns", () => { + const patterns = ["/foo*", "/:foo", "/foo"]; + const expected = ["/foo", "/:foo", "/foo*"]; + expect(patterns.toSorted(comparePatterns)).toEqual(expected); +}); + +test("sort static paths", () => { + const patterns = ["/a/z", "/a/b/c", "/a/c", "/a/b"]; + const expected = ["/a/b", "/a/c", "/a/z", "/a/b/c"]; + expect(patterns.toSorted(comparePatterns)).toEqual(expected); +}); + +test("sort mixed static, dynamic, spread at multiple levels", () => { + const patterns = [ + "/foo", + "/:id", + "/bar*", + "/foo/bar", + "/foo/:id", + "/foo/bar*", + ]; + const expected = [ + // static first-segment + "/foo", + "/foo/bar", + "/foo/:id", + "/foo/bar*", + // dynamic then spread at top level + "/:id", + "/bar*", + ]; + expect(patterns.toSorted(comparePatterns)).toEqual(expected); +}); + +test("sort deeply nested mixed segments", () => { + const patterns = ["/u/bar", "/u/:id", "/u/bar/b", "/u/:id/c", "/u/bar/*"]; + const expected = [ + // static second-segment + "/u/bar", + "/u/bar/b", + "/u/bar/*", + // dynamic second-segment + "/u/:id", + "/u/:id/c", + ]; + expect(patterns.toSorted(comparePatterns)).toEqual(expected); +}); diff --git a/apps/builder/app/canvas/shared/routing-priority.ts b/apps/builder/app/canvas/shared/routing-priority.ts new file mode 100644 index 000000000000..17e8a4d060ed --- /dev/null +++ b/apps/builder/app/canvas/shared/routing-priority.ts @@ -0,0 +1,41 @@ +const STATIC = 1; +const DYNAMIC = 2; +const SPREAD = 3; + +const getSegmentScore = (segment: string) => { + // give spread the least priority + if (segment.endsWith("*")) { + return SPREAD; + } + // sort dynamic segments before splat + if (segment.startsWith(":")) { + return DYNAMIC; + } + // sort static routes before dynamic routes + return STATIC; +}; + +export const comparePatterns = (leftPattern: string, rightPattern: string) => { + const leftSegments = leftPattern.split("/"); + const rightSegments = rightPattern.split("/"); + const commonLength = Math.min(leftSegments.length, rightSegments.length); + + // compare each segment first + for (let index = 0; index < commonLength; index++) { + const leftScore = getSegmentScore(leftSegments[index]); + const rightScore = getSegmentScore(rightSegments[index]); + if (leftScore !== rightScore) { + return leftScore - rightScore; + } + } + + // compare amount of segments + const leftLength = leftSegments.length; + const rightLength = rightSegments.length; + if (leftLength !== rightLength) { + return leftLength - rightLength; + } + + // sort alphabetically + return leftPattern.localeCompare(rightPattern); +};