Skip to content

Instantly share code, notes, and snippets.

@jacob-ebey
Last active December 4, 2023 09:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jacob-ebey/0ae28137fb7be1dba93702a0d612bcad to your computer and use it in GitHub Desktop.
Save jacob-ebey/0ae28137fb7be1dba93702a0d612bcad to your computer and use it in GitHub Desktop.
Assemble react-router routes from Vite `import.meta.glob`
import { describe, it, expect } from "vitest";
import { routesFromGlob, type RoutesFromGlob } from "./routes";
describe("routesFromGlob", () => {
it("correctly transforms routes", () => {
let routes = {
"./routes/_index/route.tsx": () => "Index Route",
"./routes/single/route.tsx": () => "Single Route",
"./routes/$param/route.tsx": () => "Param Route",
"./routes/$/route.tsx": () => "Splat Route",
"./routes/nested/route.tsx": () => "Nested Layout",
"./routes/nested._index/route.tsx": () => "Nested Index Route",
"./routes/nested.single/route.tsx": () => "Nested Single Route",
"./routes/nested.$param/route.tsx": () => "Nested Param Route",
"./routes/nested.$/route.tsx": () => "Nested Splat Route",
"./routes/nested.again/route.tsx": () => "Nested Again Layout",
"./routes/nested.again._index/route.tsx": () =>
"Nested Again Index Route",
"./routes/nested.again.single/route.tsx": () =>
"Nested Again Single Route",
"./routes/nested.again.$param/route.tsx": () =>
"Nested Again Param Route",
"./routes/nested.again.$param_.again/route.tsx": () =>
"Nested Again Param Sub Route Unnested from $param",
"./routes/nested.again.$/route.tsx": () => "Nested Again Spalt Route",
"./routes/nested_.not-nested/route.tsx": () => "Not Nested Route",
} as unknown as RoutesFromGlob;
let results = routesFromGlob(routes, "./routes/");
expect(
JSON.parse(
JSON.stringify(results, (_, v) => (typeof v === "function" ? v() : v))
)
).toMatchInlineSnapshot(`
[
{
"id": "$",
"lazy": "Splat Route",
"path": "*",
},
{
"id": "single",
"lazy": "Single Route",
"path": "single",
},
{
"children": [
{
"id": "nested.$",
"lazy": "Nested Splat Route",
"parentId": "nested",
"path": "*",
},
{
"children": [
{
"id": "nested.again.$",
"lazy": "Nested Again Spalt Route",
"parentId": "nested.again",
"path": "/again/*",
},
{
"id": "nested.again.single",
"lazy": "Nested Again Single Route",
"parentId": "nested.again",
"path": "/again/single",
},
{
"id": "nested.again.$param",
"lazy": "Nested Again Param Route",
"parentId": "nested.again",
"path": "/again/:param",
},
{
"id": "nested.again._index",
"index": true,
"lazy": "Nested Again Index Route",
"parentId": "nested.again",
"path": "/again",
},
{
"id": "nested.again.$param_.again",
"lazy": "Nested Again Param Sub Route Unnested from $param",
"parentId": "nested.again",
"path": "/again/:param/again",
},
],
"id": "nested.again",
"lazy": "Nested Again Layout",
"parentId": "nested",
"path": "again",
},
{
"id": "nested.single",
"lazy": "Nested Single Route",
"parentId": "nested",
"path": "single",
},
{
"id": "nested.$param",
"lazy": "Nested Param Route",
"parentId": "nested",
"path": ":param",
},
{
"id": "nested._index",
"index": true,
"lazy": "Nested Index Route",
"parentId": "nested",
},
],
"id": "nested",
"lazy": "Nested Layout",
"path": "nested",
},
{
"id": "$param",
"lazy": "Param Route",
"path": ":param",
},
{
"id": "_index",
"index": true,
"lazy": "Index Route",
},
{
"id": "nested_.not-nested",
"lazy": "Not Nested Route",
"path": "nested/not-nested",
},
]
`);
});
});
import { DataRouteObject } from "react-router-dom";
export type RoutesFromGlob = Record<string, () => Promise<unknown>>;
const ROUTE_DIRECTORY_REGEX = /\/route\.[tj]sx?$/;
const ROUTE_FILE_REGEX = /\.[tj]sx?$/;
export function routesFromGlob(
routes: RoutesFromGlob,
basePath: string
): DataRouteObject[] {
// normalize the base path to use forward slashes
basePath = basePath.replace(/\\/g, "/");
// sort the routes by path length and alphabetically
const routeEntries = sortRouteEntries(routes);
// create the routes
const results: (DataRouteObject & { parentId?: string })[] = [];
for (const [globKey, lazy] of routeEntries) {
// normalize the glob key to use forward slashes and remove the
// base path from the glob key
const globPath = globKey.replace(/\\/g, "/").slice(basePath.length);
let routeId = globPath.replace(ROUTE_DIRECTORY_REGEX, "");
if (routeId == globPath) {
routeId = globPath.replace(ROUTE_FILE_REGEX, "");
}
// Normalize the route ID based on if it's a directory or file
// based glob. `./routes/*.tsx` or `./routes/**/route.tsx`
let path: string;
const isIndex = routeId === "_index" || routeId.endsWith("._index");
if (isIndex) {
path = createReactRouterPath(routeId.replace(/\.?\_index$/, ""));
} else {
path = createReactRouterPath(routeId);
}
let newRoute: DataRouteObject & { parentId?: string };
if (isIndex) {
newRoute = {
id: routeId,
index: true,
path: path ? path : undefined,
lazy: lazy as DataRouteObject["lazy"],
};
} else {
newRoute = {
id: routeId,
path,
lazy: lazy as DataRouteObject["lazy"],
};
}
const parent = [...results]
.reverse()
.find((r) => startsWithSegments(routeId, r.id));
if (parent) {
if (newRoute.path) {
newRoute.path = parent.path
? newRoute.path.slice(parent.path.length + 1)
: newRoute.path;
if (isIndex && !newRoute.path) {
newRoute.path = undefined;
}
}
newRoute.parentId = parent.id;
const children = parent.children || [];
children.push(newRoute);
parent.children = children;
}
results.push(newRoute);
}
return results.filter((r) => !r.parentId);
}
function sortRouteEntries<K extends string, V>(obj: Record<K, V>): [K, V][] {
return (Object.entries(obj) as [K, V][]).sort((a, b) => {
// Compare by length
if (a[0].length !== b[0].length) {
return a[0].length - b[0].length;
}
// If lengths are equal, sort by key in descending alphabetical order to
// get special chars on top
return b[0].localeCompare(a[0], "en");
});
}
function createReactRouterPath(filepath: string): string {
let segments = filepath.split(".");
let path = "";
for (let i = 0; i < segments.length; i++) {
let segment = segments[i];
// Skip segments start with '_'
if (segment.startsWith("_")) continue;
let escaped = false;
let result = "";
for (let j = 0; j < segment.length; j++) {
if (segment[j] === "[") {
escaped = true;
continue;
}
if (segment[j] === "]") {
escaped = false;
continue;
}
if (escaped) {
result += segment[j];
} else {
switch (segment[j]) {
case ".":
result += "/";
break;
case "$":
if (j === 0 && j === segment.length - 1) {
result += "*";
} else {
result += ":";
}
break;
case "_":
if (j !== segment.length - 1) {
result += segment[j];
}
break;
default:
result += segment[j];
break;
}
}
}
if (result !== "") {
if (path !== "") {
path += "/";
}
path += result;
}
}
return path;
}
function startsWithSegments(a: string, startsWith: string) {
const sa = a.split(".");
const sb = startsWith.split(".");
if (sa.length <= sb.length) return false;
for (let i = 0; i < sb.length; i++) {
if (sa[i] !== sb[i]) return false;
}
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment