From 07f8422db210dacc8c8f0fa335cfc67971ef0c7c Mon Sep 17 00:00:00 2001 From: sabercoy Date: Sun, 20 Sep 2026 02:53:31 -0500 Subject: [PATCH] make route order deterministic --- src/router.ts | 2 +- src/tree.ts | 2 +- test/convention.spec.ts | 39 +++++++++++++++++++++++++++++++++++++++ test/tree.spec.ts | 13 +++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/router.ts b/src/router.ts index dbd825d..f416e0f 100644 --- a/src/router.ts +++ b/src/router.ts @@ -71,7 +71,7 @@ export class BaseFileSystemRouter extends EventTarget { } async buildRoutes(): Promise { - for (const src of glob(this.glob())) { + for (const src of glob(this.glob()).sort()) { await this.addRoute(src); } diff --git a/src/tree.ts b/src/tree.ts index d3f293c..73ad628 100644 --- a/src/tree.ts +++ b/src/tree.ts @@ -46,7 +46,7 @@ export function buildRouteTree(entries: readonly RouteManifestEntry[]): RouteTre } return [...entries] - .sort((a, b) => a.path.length - b.path.length) + .sort((a, b) => (a.path.length - b.path.length) || a.path.localeCompare(b.path)) .reduce((routes: RouteTreeEntry[], route) => { processRoute(routes, route, route.path); return routes; diff --git a/test/convention.spec.ts b/test/convention.spec.ts index 8ac4a7f..9cc25ed 100644 --- a/test/convention.spec.ts +++ b/test/convention.spec.ts @@ -226,4 +226,43 @@ describe("PageFileSystemRouter", () => { expect(routes.map(route => route.path)).toEqual(["/home"]); }); + + it("produces the manifest in a deterministic order across scans", async () => { + const dir = createRouteTree({ + "index.tsx": "export default () =>

Home

;", + "about.tsx": "export default () =>

About

;", + "zebra.tsx": "export default () =>

Z

;", + "blog/index.tsx": "export default () =>

Blog

;", + "blog/[id].tsx": "export default () =>

Post

;", + "docs/[...path].tsx": "export default () =>

Docs

;", + "contact.tsx": "export default () =>

Contact

;" + }); + + // Fresh router per scan — getRoutes() memoizes, so a new instance + // forces a real re-glob each time. Map to paths but do NOT .sort(). + const scan = async () => + (await new PageFileSystemRouter({ dir, extensions: ["tsx"] }).getRoutes()) + .map(route => route.path); + + const first = await scan(); + + // 1. Repeatable: N independent scans yield the same order. + for (let i = 0; i < 5; i++) { + expect(await scan()).toEqual(first); + } + + // 2. Pin the exact order so the guarantee is explicit. + // The sort is on source file paths (lexicographic), so the order + // reflects that: about.tsx < blog/[id].tsx < blog/index.tsx < + // contact.tsx < docs/[...path].tsx < index.tsx < zebra.tsx. + expect(first).toEqual([ + "/about", + "/blog/:id", + "/blog/", + "/contact", + "/docs/*path", + "/", + "/zebra" + ]); + }); }); diff --git a/test/tree.spec.ts b/test/tree.spec.ts index 8daab1f..f58f364 100644 --- a/test/tree.spec.ts +++ b/test/tree.spec.ts @@ -52,4 +52,17 @@ describe("buildRouteTree", () => { expect(entries.map(e => e.path)).toEqual(paths); expect(entries.every(e => !("children" in e))).toBe(true); }); + + it("sorts equal-length paths deterministically", () => { + const tree = buildRouteTree([ + entry("/zebra"), + entry("/about"), + entry("/alpha") + ]); + + // All have the same path length, so the tie-breaker (localeCompare) + // guarantees a stable lexicographic order instead of relying on + // whatever order the glob happened to return. + expect(tree.map(node => node.path)).toEqual(["/about", "/alpha", "/zebra"]); + }); });