import type { Context, Middleware, Next, Server } from "./types.ts";
export type App = {
use: (middleware: Middleware) => void;
} & Partial<Server>;
export function sortComparator(a: string, b: string) {
function weight(s: string) {
if (s === "index") {
return -1;
}
if (s === "profile") {
return 1;
}
return 0;
}
const wa = weight(a);
const wb = weight(b);
if (wa !== wb) {
return wa - wb;
}
return a.localeCompare(b);
}
export function sortNames(names: string[]) {
return names.sort(sortComparator);
}
export type AutoRegisterOptions = {
manifest?: Record<string, unknown>;
manifestPath?: string;
requireExplicitGlobals?: boolean;
};
export async function autoRegisterModules(
app: App,
options?: AutoRegisterOptions,
) {
if (options?.manifest) {
try {
autoRegisterModulesFrom(options.manifest, app);
} catch (err) {
console.error(
"❌ [Loader] Failed reading manifest:",
(err as Error).message,
);
}
return;
}
try {
const basePath = options?.manifestPath ?? `${Deno.cwd()}/manifest.ts`;
const url = basePath.startsWith("file://")
? basePath
: `file://${basePath}`;
const mod = await import(url);
autoRegisterModulesFrom(mod as unknown as Record<string, unknown>, app);
} catch (err) {
console.error(
"❌ [Loader] Failed reading manifest:",
(err as Error).message,
);
}
}
export function autoRegisterModulesFrom(
manifestObj: Record<string, unknown>,
app: App,
options?: AutoRegisterOptions,
) {
_registeredMounts.length = 0;
const names = sortNames(Object.keys(manifestObj));
for (const name of names) {
const ns = manifestObj[name];
if (isNamespaceObject(ns)) {
registerFromNamespace(name, ns as Record<string, unknown>, app, options);
}
}
try {
const regs = _getRegisteredMounts();
if (regs.length) {
console.info(
`ℹ️ [Loader] Registered ${regs.length} module(s): ${
regs.map((r) => `${r.name} @ ${r.mount}`).join(", ")
}`,
);
} else {
console.info("ℹ️ [Loader] No modules registered by loader");
}
const mwCount = typeof (app as App & { _getMiddlewareCount?: () => number })
._getMiddlewareCount === "function"
? (app as App & { _getMiddlewareCount?: () => number })
._getMiddlewareCount?.()
: undefined;
const routePaths = typeof (app as App & { _getRoutePaths?: () => string[] })
._getRoutePaths === "function"
? (app as App & { _getRoutePaths?: () => string[] })._getRoutePaths?.()
: undefined;
if (typeof mwCount === "number") {
console.info(`ℹ️ [Loader] Global middlewares: ${mwCount}`);
}
if (Array.isArray(routePaths)) {
console.info(`ℹ️ [Loader] Registered route(s): ${routePaths.length}`);
}
} catch {
}
}
export function registerFromNamespace(
name: string,
ns: Record<string, unknown>,
app: App,
options?: AutoRegisterOptions,
) {
const foundCandidate = getRegistrationCandidate(name, ns);
let candidate: Middleware | null = foundCandidate as unknown as
| Middleware
| null;
function normalizeMount(p: string) {
if (p === "") return p;
p = p.replace(/\/+$/g, "");
if (!p.startsWith("/")) p = "/" + p;
return p;
}
function normalizeMountForRecord(p: string) {
if (p === "") return p;
p = p.replace(/^\/+/, "/");
p = p.replace(/\/+$/g, "");
if (!p.startsWith("/")) p = "/" + p;
return p;
}
function slugFromName(n: string) {
return (
n
.toLowerCase()
.replace(/[_\s]+/g, "-")
.replace(/[^a-z0-9\-]/g, "")
.replace(/^-+|-+$/g, "") || n
);
}
const makeRegistrar = (base: App) => {
const methods = [
"get",
"post",
"put",
"delete",
"patch",
"head",
"options",
];
const reg: Record<string, unknown> = {
use: base.use,
serve: (base as App).serve,
};
for (const m of methods) {
const fn = (base as unknown as Record<string, unknown>)[m];
reg[m] = (...args: unknown[]) => {
if (typeof fn === "function") {
try {
(fn as unknown as (...a: unknown[]) => unknown).call(base, ...args);
} catch {
}
try {
const p = String(args[0] ?? "");
if (p !== "/" && !p.includes("*")) {
const normalized = p.replace(/\/+$/g, "");
const wildcard = `${normalized}/*`;
(fn as unknown as (...a: unknown[]) => unknown).call(
base,
wildcard,
...args,
);
}
} catch {
}
return;
}
if (typeof (base as App).use === "function") {
const handler = args.find((a) => typeof a === "function") as
| Middleware
| undefined;
if (handler) {
(base as App).use(handler);
}
}
};
}
return reg;
};
if (
typeof ns.default === "function" &&
(ns.default as (app: App) => void).length === 1
) {
try {
const wrappedApp = makeRegistrar(app) as unknown as App;
const originalUse = wrappedApp.use;
wrappedApp.use = (mw: Middleware) => {
const wrappedMw: Middleware = (req, ctx, next) => {
if (!ctx.state) ctx.state = {};
ctx.state.module = name;
return mw(req, ctx, next);
};
return originalUse.call(wrappedApp, wrappedMw);
};
(ns.default as (app: App) => void)(wrappedApp);
let recordedMount: string;
if (typeof ns.mountPath === "string") {
const mp = ns.mountPath as string;
if (mp.includes("/") || mp === "") {
recordedMount = normalizeMountForRecord(mp);
} else {
recordedMount = mp;
}
} else {
recordedMount = name === "index" ? "/" : `/${name}`;
}
_registeredMounts.push({ name, mount: recordedMount });
console.info(`✅ Registered module ${name} via registration function`);
return true;
} catch (e) {
console.error(
`❌ Registration function for ${name} threw:`,
(e as Error).message,
);
throw e;
}
}
if (typeof ns.register === "function") {
try {
const wrappedApp = makeRegistrar(app) as unknown as App;
const originalUse = wrappedApp.use;
wrappedApp.use = (mw: Middleware) => {
const wrappedMw: Middleware = (req, ctx, next) => {
if (!ctx.state) ctx.state = {};
ctx.state.module = name;
return mw(req, ctx, next);
};
return originalUse.call(wrappedApp, wrappedMw);
};
(ns.register as (app: App) => void)(wrappedApp);
{
let recordedMount: string;
if (typeof ns.mountPath === "string") {
const mp = ns.mountPath as string;
if (mp.includes("/") || mp === "") {
recordedMount = normalizeMountForRecord(mp);
} else {
recordedMount = mp;
}
} else {
recordedMount = name === "index" ? "/" : `/${name}`;
}
_registeredMounts.push({ name, mount: recordedMount });
}
console.info(`✅ Registered module ${name} via named register()`);
return true;
} catch (err) {
console.error(
`❌ Named register for ${name} threw:`,
(err as Error).message,
);
}
}
if (
ns.default && typeof ns.default === "object" &&
typeof (ns.default as { register?: (app: App) => void }).register ===
"function"
) {
try {
const wrappedApp = makeRegistrar(app) as unknown as App;
const originalUse = wrappedApp.use;
wrappedApp.use = (mw: Middleware) => {
const wrappedMw: Middleware = (req, ctx, next) => {
if (!ctx.state) ctx.state = {};
ctx.state.module = name;
return mw(req, ctx, next);
};
return originalUse.call(wrappedApp, wrappedMw);
};
(ns.default as { register: (app: App) => void }).register(
wrappedApp,
);
{
let recordedMount: string;
if (typeof ns.mountPath === "string") {
const mp = ns.mountPath as string;
if (mp.includes("/") || mp === "") {
recordedMount = normalizeMountForRecord(mp);
} else {
recordedMount = mp;
}
} else {
recordedMount = name === "index" ? "/" : `/${name}`;
}
_registeredMounts.push({ name, mount: recordedMount });
}
console.info(`✅ Registered module ${name} via default.register()`);
return true;
} catch (err) {
console.error(
`❌ default.register for ${name} threw:`,
(err as Error).message,
);
}
}
if (
ns.default && typeof ns.default === "object" &&
typeof (ns.default as { build?: () => unknown }).build === "function"
) {
try {
const built = (ns.default as { build?: () => unknown }).build?.();
if (typeof built === "function") {
candidate = built as unknown as Middleware;
}
} catch {
}
}
if (!candidate) return false;
const wrapped: Middleware = (req, ctx, next) => {
if (!ctx.state) ctx.state = {};
const prevModule = ctx.state.module;
ctx.state.module = name;
let restored = false;
const restore = () => {
if (restored) return;
restored = true;
if (prevModule === undefined) {
delete ctx.state.module;
} else {
ctx.state.module = prevModule;
}
};
const wrappedNext = () => {
restore();
return next();
};
try {
const res = (candidate as unknown as Middleware)(req, ctx, wrappedNext) as
| Response
| Promise<Response>;
if (res && typeof (res as Promise<Response>).then === "function") {
return (res as Promise<Response>).then((v) => {
restore();
return v;
}, (err) => {
restore();
throw err;
});
}
restore();
return res;
} catch (err) {
restore();
throw err;
}
};
const explicitGlobal = ns.global === true;
const explicitMount = typeof ns.mountPath === "string" ? ns.mountPath : null;
const _requireExplicit = !!options?.requireExplicitGlobals;
const mount = explicitMount !== null
? normalizeMount(explicitMount)
: (name === "index" ? "/" : `/${slugFromName(name)}`);
if (explicitGlobal) {
app.use(wrapped);
} else if (typeof app.get === "function") {
try {
const handlerWrapper = (
req: Request,
ctx?: Context,
next?: Next,
) => {
const ctxArg = ctx ?? ({ state: {} } as Context);
return (wrapped as Middleware)(
req,
ctxArg,
next ?? (() => new Response("Not found", { status: 404 })),
);
};
const noopMw = (_r: Request, _c: Context, n: Next) => n();
const methods = [
"get",
"post",
"put",
"delete",
"patch",
"head",
"options",
];
for (const m of methods) {
const fn = (app as unknown as Record<string, unknown>)[m];
if (typeof fn !== "function") continue;
try {
const f = fn as unknown as (
this: unknown,
...args: unknown[]
) => unknown;
if (mount === "/") {
f.call(app, mount, handlerWrapper, noopMw);
f.call(app, `${mount}/*`, handlerWrapper, noopMw);
} else {
f.call(app, mount, handlerWrapper);
f.call(app, `${mount}/*`, handlerWrapper);
}
} catch {
}
}
} catch {
app.use(wrapped);
}
} else {
app.use(wrapped);
}
if (candidate === ns.default) {
console.info(
`✅ Registered default export from ${name}/mod.ts at ${mount}`,
);
} else {
console.info(
`✅ Registered ${name} export from ${name}/mod.ts at ${mount}`,
);
}
_registeredMounts.push({ name, mount });
return true;
}
export function isNamespaceObject(v: unknown): v is Record<string, unknown> {
return !!v && typeof v === "object";
}
export function getRegistrationCandidate(
name: string,
ns: Record<string, unknown>,
) {
const def = ns.default as unknown;
if (typeof def === "function") return def;
const named = ns[name];
if (typeof named === "function") return named;
return null;
}
const _registeredMounts: Array<{ name: string; mount: string }> = [];
export function _getRegisteredMounts() {
return _registeredMounts.slice();
}