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
2 changes: 1 addition & 1 deletion src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class BaseFileSystemRouter extends EventTarget {
}

async buildRoutes(): Promise<RouteManifestEntry[]> {
for (const src of glob(this.glob())) {
for (const src of glob(this.glob()).sort()) {
await this.addRoute(src);
}

Expand Down
2 changes: 1 addition & 1 deletion src/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 39 additions & 0 deletions test/convention.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => <h1>Home</h1>;",
"about.tsx": "export default () => <h1>About</h1>;",
"zebra.tsx": "export default () => <h1>Z</h1>;",
"blog/index.tsx": "export default () => <h1>Blog</h1>;",
"blog/[id].tsx": "export default () => <h1>Post</h1>;",
"docs/[...path].tsx": "export default () => <h1>Docs</h1>;",
"contact.tsx": "export default () => <h1>Contact</h1>;"
});

// 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"
]);
});
});
13 changes: 13 additions & 0 deletions test/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
});
Loading