rsshub
Version:
Make RSS Great Again!
149 lines (148 loc) • 5.65 kB
JavaScript
import { t as logger } from "./logger-BPV-Cx6k.mjs";
import { routePath } from "hono/route";
//#region lib/registry-helpers.ts
const SEPARATOR = /[/\\]/;
/**
* A directory under lib/routes that contains a `namespace.ts` is a "namespace root". Roots are keyed by their path
* relative to lib/routes, joined with `/` (e.g. `github/enterprise`).
*/
function collectNamespaceRoots(moduleKeys) {
const roots = /* @__PURE__ */ new Set();
for (const key of moduleKeys) {
const segments = key.split(SEPARATOR).filter(Boolean);
if (segments.at(-1) === "namespace.ts") roots.add(segments.slice(0, -1).join("/"));
}
return roots;
}
/**
* A module belongs to its longest matching namespace root; modules with no matching root fall back to their first
* path segment (flat namespaces). `location` is the module path relative to the resolved namespace root.
*/
function resolveModuleNamespace(moduleKey, roots) {
const segments = moduleKey.split(SEPARATOR).filter(Boolean);
for (let i = segments.length - 1; i >= 1; i--) {
const candidate = segments.slice(0, i).join("/");
if (roots.has(candidate)) return {
namespace: candidate,
location: segments.slice(i).join("/")
};
}
return {
namespace: segments[0],
location: segments.slice(1).join("/")
};
}
/**
* Merge directory-imported modules into `namespaces` (mutated in place). Module keys must be relative to
* lib/routes (e.g. `/github/enterprise/news.ts`).
*/
function applyModulesToNamespaces(modules, namespaces) {
const namespaceRoots = collectNamespaceRoots(Object.keys(modules));
for (const module in modules) {
const content = modules[module];
const { namespace, location } = resolveModuleNamespace(module, namespaceRoots);
if ("namespace" in content) namespaces[namespace] = Object.assign({
routes: {},
apiRoutes: {}
}, namespaces[namespace], content.namespace);
else if ("route" in content) {
if (!Object.hasOwn(namespaces, namespace)) namespaces[namespace] = {
name: namespace,
routes: {},
apiRoutes: {}
};
if (Array.isArray(content.route.path)) for (const path of content.route.path) namespaces[namespace].routes[path] = {
...content.route,
location
};
else namespaces[namespace].routes[content.route.path] = {
...content.route,
location
};
} else if ("apiRoute" in content) {
if (!Object.hasOwn(namespaces, namespace)) namespaces[namespace] = {
name: namespace,
routes: {},
apiRoutes: {}
};
if (Array.isArray(content.apiRoute.path)) for (const path of content.apiRoute.path) namespaces[namespace].apiRoutes[path] = {
...content.apiRoute,
location
};
else namespaces[namespace].apiRoutes[content.apiRoute.path] = {
...content.apiRoute,
location
};
}
}
}
const sortRoutes = (routes) => Object.entries(routes).toSorted(([pathA], [pathB]) => {
const segmentsA = pathA.split("/");
const segmentsB = pathB.split("/");
const lenA = segmentsA.length;
const lenB = segmentsB.length;
const minLen = Math.min(lenA, lenB);
for (let i = 0; i < minLen; i++) {
const segmentA = segmentsA[i];
const segmentB = segmentsB[i];
if (segmentA.startsWith(":") !== segmentB.startsWith(":")) return segmentA.startsWith(":") ? 1 : -1;
if (segmentA.startsWith(":") && segmentA.includes("{") !== segmentB.includes("{")) return segmentA.includes("{") ? -1 : 1;
}
return 0;
});
const sortNamespacesByDepth = (namespaces) => Object.keys(namespaces).toSorted((a, b) => b.split("/").length - a.split("/").length);
function registerRssRoutes(app, namespaces) {
for (const namespace of sortNamespacesByDepth(namespaces)) {
const subApp = app.basePath(`/${namespace}`);
const namespaceData = namespaces[namespace];
if (!namespaceData || !namespaceData.routes) continue;
const sortedRoutes = sortRoutes(namespaceData.routes);
for (const [path, routeData] of sortedRoutes) {
const wrappedHandler = async (ctx) => {
logger.debug(`Matched route: ${routePath(ctx)}`);
if (!ctx.get("data")) {
if (!routeData.handler) {
if (process.env.NODE_ENV === "test") {
const { route } = await import(`./routes/${namespace}/${routeData.location}`);
routeData.handler = route.handler;
} else if (routeData.module) {
const { route } = await routeData.module();
routeData.handler = route.handler;
}
}
const response = await routeData.handler(ctx);
if (response instanceof Response) return response;
ctx.set("data", response);
}
};
subApp.get(path, wrappedHandler);
}
}
}
function registerApiRoutes(app, namespaces) {
for (const namespace of sortNamespacesByDepth(namespaces)) {
const subApp = app.basePath(`/api/${namespace}`);
const namespaceData = namespaces[namespace];
if (!namespaceData || !namespaceData.apiRoutes) continue;
const sortedRoutes = Object.entries(namespaceData.apiRoutes);
for (const [path, routeData] of sortedRoutes) {
const wrappedHandler = async (ctx) => {
if (ctx.get("apiData")) return;
if (!routeData.handler) {
if (process.env.NODE_ENV === "test") {
const { apiRoute } = await import(`./routes/${namespace}/${routeData.location}`);
routeData.handler = apiRoute.handler;
} else if (routeData.module) {
const { apiRoute } = await routeData.module();
routeData.handler = apiRoute.handler;
}
}
const data = await routeData.handler(ctx);
ctx.set("apiData", data);
};
subApp.get(path, wrappedHandler);
}
}
}
//#endregion
export { resolveModuleNamespace as a, registerRssRoutes as i, collectNamespaceRoots as n, sortRoutes as o, registerApiRoutes as r, applyModulesToNamespaces as t };