Last active
December 4, 2023 09:39
-
-
Save jacob-ebey/0ae28137fb7be1dba93702a0d612bcad to your computer and use it in GitHub Desktop.
Assemble react-router routes from Vite `import.meta.glob`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | |
}, | |
] | |
`); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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