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; 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; }