Skip to content

Instantly share code, notes, and snippets.

@arisris
Created August 22, 2024 23:25
Show Gist options
  • Select an option

  • Save arisris/b6312a5b1595c511bc487c053c6c5542 to your computer and use it in GitHub Desktop.

Select an option

Save arisris/b6312a5b1595c511bc487c053c6c5542 to your computer and use it in GitHub Desktop.
Hono FS Router For Vite
import { type Env, type Handler } from "hono";
import type { HonoOptions } from "hono/hono-base";
import { Hono } from "hono/tiny";
import { type PropsWithChildren } from "hono/jsx";
import type {
BlankEnv,
BlankInput,
BlankSchema,
HandlerResponse,
Input,
Schema,
} from "hono/types";
import type { JSX } from "hono/jsx/jsx-runtime";
import { jsxRenderer } from "hono/jsx-renderer";
type HttpVerb = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS";
interface RouteComponent<P = unknown> {
(props?: PropsWithChildren<P>): JSX.Element;
}
type RouteModule = {
default?: RouteComponent;
handlers?: RouteHandlers;
metadata?: { [key: string]: any };
};
export type RouteHandlers<
E extends Env = any,
P extends string = any,
I extends Input = BlankInput,
R extends HandlerResponse<any> = any
> = {
[key in HttpVerb]?: Handler<E, P, I, R> | Handler<E, P, I, R>[];
};
const isAsyncFunction = (fn: any) =>
fn && fn.constructor && fn.constructor.name === "AsyncFunction";
export default function FileRoutes<
E extends Env = BlankEnv,
S extends Schema = BlankSchema,
BasePath extends string = "/"
>({
before,
after,
...options
}: {
before?: (app: Hono<E, S, BasePath>) => void;
after?: (app: Hono<E, S, BasePath>) => void;
} & HonoOptions<E> = {}) {
const patterns = {
route: [/^.*\/src\/routes\/|\.(jsx|tsx)$/g, ""],
splat: [/\[\.{3}\w+\]/g, "*"],
param: [/\[([^\]]+)\]/g, ":$1"],
optional: [/\[([^\]]+)\$]/g, ":$1?"],
} as const;
const app = new Hono<E, S, BasePath>(options);
before?.(app);
// const PRESERVED = import.meta.glob('/src/routes/(_app|404).{jsx,tsx}', { eager: true })
const ROUTES = import.meta.glob<RouteModule>(
[
"/src/routes/**/[\\w[-]*.{jsx,tsx}",
"!/src/routes/**/(_!(layout)*(/*)?|_error|404)*",
],
{ eager: true }
);
let routes: (RouteModule & { pattern: string })[] = [];
for (const [key, mod] of Object.entries(ROUTES)) {
let pattern = key
.replace(...patterns.route)
.replace(...patterns.splat)
.replace(...patterns.optional)
.replace(...patterns.param)
.split("/")
.filter(Boolean)
.map((s) => s.replace(/^index$|\./g, "").concat("/"))
.join("")
.replace(/\/+/g, "/")
.replace(/\/$/, "");
if (!pattern.includes("_layout")) {
pattern = "/" + pattern;
}
routes.push({ pattern, ...mod });
}
routes.sort((a, b) => {
if (a.pattern.includes("_layout") && !b.pattern.includes("_layout")) {
return -1;
} else {
return a.pattern.localeCompare(b.pattern);
}
});
for (const route of routes) {
const isLayout = route.pattern.includes("_layout");
if (route.handlers) {
for (const [key, handler] of Object.entries(route.handlers)) {
if (Array.isArray(handler)) {
app.on(key, route.pattern as any, ...handler);
} else {
app.on(key, route.pattern as any, handler);
}
}
}
if (route.default) {
if (isLayout) {
const Layout = route.default as RouteComponent<any>;
app.use(
route.pattern.replace("_layout", "*") as any,
jsxRenderer((props) => <Layout {...props} />, {
docType: true,
stream: isAsyncFunction(Layout),
})
);
} else {
const Component = route.default as RouteComponent<any>;
app.on("ALL", route.pattern as any, (ctx) => {
return ctx.render(<Component />, route.metadata ?? {});
});
}
}
}
after?.(app);
return app;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment