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
7 changes: 6 additions & 1 deletion apps/builder/app/canvas/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
50 changes: 50 additions & 0 deletions apps/builder/app/canvas/shared/routing-priority.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
41 changes: 41 additions & 0 deletions apps/builder/app/canvas/shared/routing-priority.ts
Original file line number Diff line number Diff line change
@@ -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);
};