Skip to content

Instantly share code, notes, and snippets.

@djalmajr
Last active July 31, 2024 12:06
Show Gist options
  • Save djalmajr/54f2b0a2ffa1d85232b9571f34ab35d2 to your computer and use it in GitHub Desktop.
Save djalmajr/54f2b0a2ffa1d85232b9571f34ab35d2 to your computer and use it in GitHub Desktop.
File-based routes for React Router following Remix conventions
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