Last active
July 31, 2024 12:06
-
-
Save djalmajr/54f2b0a2ffa1d85232b9571f34ab35d2 to your computer and use it in GitHub Desktop.
File-based routes for React Router following Remix conventions
This file contains hidden or 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 type { Obj } from "help-es"; | |
import { createElement, isValidElement } from "react"; | |
import { Outlet, type RouteObject } from "react-router-dom"; | |
type RO = RouteObject & { _path?: string }; | |
const findRoute = (path: string, routes: RO[]) => { | |
return routes.find((r) => r.path === path || r._path === path); | |
}; | |
const getSegments = (path: string) => { | |
return path.replace(/^\//, "").split(/\/|\.(?!lazy)/); | |
}; | |
const parsePath = (path: string) => { | |
return path | |
.replace(/\/app\/routes|index|_index|route|\.tsx?$/g, "") | |
.replace(/\[\.{3}.+\]|\$(?=[\/.]|$)/g, "*") | |
.replace(/\(\$([^)]+)\)/g, ":$1?") | |
.replace(/\[(.+)\]/g, ":$1") | |
.replace(/\.(?!lazy)/g, "/") | |
.replace(/\$/g, ":"); | |
}; | |
const parseRoute = (route: Obj): RO => { | |
const data = { ...route }; | |
if (data?.default) { | |
data.Component = data.default; | |
Reflect.deleteProperty(data, "default"); | |
} | |
return data; | |
}; | |
// https://omarelhawary.me/blog/file-based-routing-with-react-router/ | |
export function generateRoutes() { | |
const EAGER = import.meta.glob("/app/routes/**/[_$a-z()[]!(*.lazy).(ts|tsx)", { eager: true }); | |
const LAZY = import.meta.glob("/app/routes/**/[_$a-z()[]*.lazy.(ts|tsx)"); | |
const ROOT = import.meta.glob("/app/root.tsx", { eager: true }); | |
const ROUTES = { ...ROOT, ...EAGER, ...LAZY } as Obj<RO>; | |
const rootKeys = ["/app/root.tsx", "/app/routes/_layout.tsx", "/app/routes/__root.tsx"]; | |
const { Component = Outlet, ...root } = parseRoute(ROUTES[rootKeys.find((k) => ROUTES[k])!]); | |
const routes = [{ ...root, path: "/", Component, children: [] }] as RO[]; | |
const validateRoute = (v: Obj) => | |
Object.values(LAZY).includes(v as never) || | |
["action", "loader", "handle", "Component", "ErrorBoundary"].some((k) => k in v) || | |
(v.default && isValidElement(createElement(v.default))); | |
Object.entries(ROUTES) | |
.filter(([key, val]) => !rootKeys.includes(key) && validateRoute(val)) | |
.reduce((result: RO[], [key, val]) => { | |
const route = parseRoute(val); | |
(function parse(children, [segment, ...segments], parent?: RO) { | |
const isGroup = /^[_(]|_$/.test(segment); | |
const isLayout = segments[0] === "_layout"; | |
if (!isGroup && !isLayout && !segments.length) { | |
const [slug = ""] = segment.split(/\./) || []; | |
if (/lazy/.test(segment)) route.lazy = ROUTES[key] as never; | |
if ((parent = findRoute(segment, children))) Object.assign(parent, route); | |
else children.push((slug ? (route.path = slug) : (route.index = true), route)); | |
return; | |
} | |
if (/_$/.test(segment)) { | |
segments = [`${segment.replace(/_$/g, "")}/${segments[0]}`].concat(segments.slice(1)); | |
} | |
if ((parent = findRoute(segment, children))) { | |
parent.children ||= []; | |
if (isLayout) parent.Component = route.Component || Outlet; | |
else if (segments.length) parse(parent.children, segments); | |
return; | |
} | |
if (isLayout) { | |
children.push({ | |
...(route as object), | |
path: segment, | |
Component: route.Component || Outlet, | |
children: [] | |
}); | |
} else { | |
children.push({ | |
[isGroup ? "_path" : "path"]: segment, | |
Component: Outlet, | |
children: [] | |
}); | |
if (segments.length) parse(children.at(-1)!.children!, segments); | |
} | |
})(result, getSegments(parsePath(key))); | |
return result; | |
}, routes[0].children!); | |
return routes; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment