rsshub
Version:
Make RSS Great Again!
575 lines (562 loc) • 19.5 kB
JavaScript
import { t as config } from "./config-C37vj7VH.mjs";
import { t as logger_default } from "./logger-Czu8UMNd.mjs";
import path from "node:path";
import { Hono } from "hono";
import { serveStatic } from "@hono/node-server/serve-static";
import { directoryImport } from "directory-import";
import { routePath } from "hono/route";
import { execSync } from "node:child_process";
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
import { PrometheusExporter, PrometheusSerializer } from "@opentelemetry/exporter-prometheus";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { trace } from "@opentelemetry/api";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
//#region lib/routes/healthz.ts
const handler$3 = (ctx) => {
ctx.header("Cache-Control", "no-cache");
return ctx.text("ok");
};
var healthz_default = handler$3;
//#endregion
//#region lib/utils/debug-info.ts
const debug = {
hitCache: 0,
request: 0,
etag: 0,
error: 0,
routes: {},
paths: {},
errorRoutes: {},
errorPaths: {}
};
const getDebugInfo = () => debug;
const setDebugInfo = (info) => Object.assign(debug, info);
//#endregion
//#region lib/utils/git-hash.ts
let gitHash = process.env.HEROKU_SLUG_COMMIT?.slice(0, 8) || process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 8);
let gitDate;
if (!gitHash) try {
gitHash = execSync("git rev-parse HEAD").toString().trim().slice(0, 8);
gitDate = new Date(execSync("git log -1 --format=%cd").toString().trim());
} catch {
gitHash = "unknown";
}
//#endregion
//#region lib/views/layout.tsx
const Layout = (props) => /* @__PURE__ */ jsxs("html", { children: [/* @__PURE__ */ jsxs("head", { children: [
/* @__PURE__ */ jsx("title", { children: "Welcome to RSSHub!" }),
/* @__PURE__ */ jsx("script", { src: "https://cdn.tailwindcss.com" }),
/* @__PURE__ */ jsx("style", { children: `
details::-webkit-scrollbar {
width: 0.25rem;
}
details::-webkit-scrollbar-thumb {
border-radius: 0.125rem;
background-color: #e4e4e7;
}
details::-webkit-scrollbar-thumb:hover {
background-color: #a1a1aa;
}
@font-face {
font-family: SN Pro;
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/sn-pro@latest/latin-400-normal.woff2) format(woff2);
}
@font-face {
font-family: SN Pro;
font-style: normal;
font-display: swap;
font-weight: 500;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/sn-pro@latest/latin-500-normal.woff2) format(woff2);
}
@font-face {
font-family: SN Pro;
font-style: normal;
font-display: swap;
font-weight: 700;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/sn-pro@latest/latin-700-normal.woff2) format(woff2);
}
body {
font-family: SN Pro, sans-serif;
}
` })
] }), /* @__PURE__ */ jsx("body", {
className: "antialiased min-h-screen text-zinc-700 flex flex-col",
children: props.children
})] });
//#endregion
//#region lib/views/index.tsx
const startTime = Date.now();
const Index = ({ debugQuery }) => {
const debug$1 = getDebugInfo();
const showDebug = !config.debugInfo || config.debugInfo === "false" ? false : config.debugInfo === "true" || config.debugInfo === debugQuery;
const { disallowRobot, nodeName, cache } = config;
const duration = Date.now() - startTime;
const info = {
showDebug,
disallowRobot,
debug: [
...nodeName ? [{
name: "Node Name",
value: nodeName
}] : [],
...gitHash ? [{
name: "Git Hash",
value: /* @__PURE__ */ jsx("a", {
className: "underline",
href: `https://github.com/DIYgod/RSSHub/commit/${gitHash}`,
children: gitHash
})
}] : [],
...gitDate ? [{
name: "Git Date",
value: gitDate.toUTCString()
}] : [],
{
name: "Cache Duration",
value: cache.routeExpire + "s"
},
{
name: "Request Amount",
value: debug$1.request
},
{
name: "Request Frequency",
value: (debug$1.request / (duration / 1e3) * 60).toFixed(3) + " times/minute"
},
{
name: "Cache Hit Ratio",
value: debug$1.request ? (debug$1.hitCache / debug$1.request * 100).toFixed(2) + "%" : 0
},
{
name: "ETag Matched Ratio",
value: debug$1.request ? (debug$1.etag / debug$1.request * 100).toFixed(2) + "%" : 0
},
{
name: "Health",
value: debug$1.request ? ((1 - debug$1.error / debug$1.request) * 100).toFixed(2) + "%" : 0
},
{
name: "Uptime",
value: (duration / 36e5).toFixed(2) + " hour(s)"
},
{
name: "Hot Routes",
value: Object.keys(debug$1.routes).toSorted((a, b) => debug$1.routes[b] - debug$1.routes[a]).slice(0, 30).map((route) => /* @__PURE__ */ jsxs(Fragment, { children: [
debug$1.routes[route],
" ",
route,
/* @__PURE__ */ jsx("br", {})
] }))
},
{
name: "Hot Paths",
value: Object.keys(debug$1.paths).toSorted((a, b) => debug$1.paths[b] - debug$1.paths[a]).slice(0, 30).map((path$1) => /* @__PURE__ */ jsxs(Fragment, { children: [
debug$1.paths[path$1],
" ",
path$1,
/* @__PURE__ */ jsx("br", {})
] }))
},
{
name: "Hot Error Routes",
value: Object.keys(debug$1.errorRoutes).toSorted((a, b) => debug$1.errorRoutes[b] - debug$1.errorRoutes[a]).slice(0, 30).map((route) => /* @__PURE__ */ jsxs(Fragment, { children: [
debug$1.errorRoutes[route],
" ",
route,
/* @__PURE__ */ jsx("br", {})
] }))
},
{
name: "Hot Error Paths",
value: Object.keys(debug$1.errorPaths).toSorted((a, b) => debug$1.errorPaths[b] - debug$1.errorPaths[a]).slice(0, 30).map((path$1) => /* @__PURE__ */ jsxs(Fragment, { children: [
debug$1.errorPaths[path$1],
" ",
path$1,
/* @__PURE__ */ jsx("br", {})
] }))
}
]
};
return /* @__PURE__ */ jsxs(Layout, { children: [
/* @__PURE__ */ jsx("div", {
className: "pointer-events-none absolute w-full min-h-screen",
style: {
backgroundImage: `url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAzMiAzMicgd2lkdGg9JzMyJyBoZWlnaHQ9JzMyJyBmaWxsPSdub25lJyBzdHJva2U9J3JnYigxNSAyMyA0MiAvIDAuMDQpJz48cGF0aCBkPSdNMCAuNUgzMS41VjMyJy8+PC9zdmc+')`,
maskImage: "linear-gradient(transparent, black, transparent)"
}
}),
/* @__PURE__ */ jsxs("div", {
className: "w-full grow shrink-0 py-8 flex items-center justify-center flex-col space-y-4",
children: [
/* @__PURE__ */ jsx("img", {
src: "./logo.png",
alt: "RSSHub",
width: "100",
loading: "lazy"
}),
/* @__PURE__ */ jsxs("h1", {
className: "text-4xl font-bold",
children: [
"Welcome to ",
/* @__PURE__ */ jsx("span", {
className: "text-[#F5712C]",
children: "RSSHub"
}),
"!"
]
}),
/* @__PURE__ */ jsx("p", {
className: "text-xl font-medium text-zinc-600",
children: "The world's largest RSS Network."
}),
/* @__PURE__ */ jsx("p", {
className: "text-zinc-500",
children: "If you see this page, the RSSHub is successfully installed and working."
}),
/* @__PURE__ */ jsxs("div", {
className: "font-bold space-x-4 text-sm",
children: [/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://docs.rsshub.app",
children: /* @__PURE__ */ jsx("button", {
className: "text-white bg-[#F5712C] hover:bg-[#DD4A15] py-2 px-4 rounded-full transition-colors",
children: "Home"
})
}), /* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://github.com/DIYgod/RSSHub",
children: /* @__PURE__ */ jsx("button", {
className: "bg-zinc-200 hover:bg-zinc-300 py-2 px-4 rounded-full transition-colors",
children: "GitHub"
})
})]
}),
info.showDebug ? /* @__PURE__ */ jsxs("details", {
className: "text-xs w-96 !mt-8 max-h-[400px] overflow-auto",
children: [/* @__PURE__ */ jsx("summary", {
className: "text-sm cursor-pointer",
children: "Debug Info"
}), info.debug.map((item) => /* @__PURE__ */ jsxs("div", {
class: "debug-item my-3 pl-8",
children: [/* @__PURE__ */ jsxs("span", {
class: "debug-key w-32 text-right inline-block mr-2",
children: [item.name, ": "]
}), /* @__PURE__ */ jsx("span", {
class: "debug-value inline-block break-all align-top",
children: item.value
})]
}))]
}) : null
]
}),
/* @__PURE__ */ jsxs("div", {
className: "text-center pt-4 pb-8 w-full text-sm font-medium space-y-2",
children: [
/* @__PURE__ */ jsxs("p", {
className: "space-x-4",
children: [
/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://github.com/DIYgod/RSSHub",
children: /* @__PURE__ */ jsx("img", {
className: "inline",
src: "https://icons.ly/github/_/fff",
alt: "github",
width: "20",
height: "20"
})
}),
/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://t.me/rsshub",
children: /* @__PURE__ */ jsx("img", {
className: "inline",
src: "https://icons.ly/telegram",
alt: "telegram group",
width: "20",
height: "20"
})
}),
/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://t.me/awesomeRSSHub",
children: /* @__PURE__ */ jsx("img", {
className: "inline",
src: "https://icons.ly/telegram",
alt: "telegram channel",
width: "20",
height: "20"
})
}),
/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://x.com/intent/follow?screen_name=_RSSHub",
className: "text-[#F5712C]",
children: /* @__PURE__ */ jsx("img", {
className: "inline",
src: "https://icons.ly/x",
alt: "X",
width: "20",
height: "20"
})
})
]
}),
/* @__PURE__ */ jsxs("p", {
className: "!mt-6",
children: [
"Please consider",
" ",
/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://docs.rsshub.app/sponsor",
className: "text-[#F5712C]",
children: "sponsoring"
}),
" ",
"to help keep this open source project alive."
]
}),
/* @__PURE__ */ jsxs("p", { children: [
"Made with ❤️ by",
" ",
/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://diygod.cc",
className: "text-[#F5712C]",
children: "DIYgod"
}),
" ",
"and",
" ",
/* @__PURE__ */ jsx("a", {
target: "_blank",
href: "https://github.com/DIYgod/RSSHub/graphs/contributors",
className: "text-[#F5712C]",
children: "Contributors"
}),
" ",
"under MIT License."
] })
]
})
] });
};
var views_default = Index;
//#endregion
//#region lib/routes/index.tsx
const handler$2 = (ctx) => {
ctx.header("Cache-Control", "no-cache");
return ctx.html(/* @__PURE__ */ jsx(views_default, { debugQuery: ctx.req.query("debug") }));
};
var routes_default = handler$2;
//#endregion
//#region lib/utils/otel/metric.ts
const METRIC_PREFIX = "rsshub";
const exporter$1 = new PrometheusExporter({});
const provider$1 = new MeterProvider({
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "rsshub" }),
readers: [exporter$1]
});
const serializer = new PrometheusSerializer();
const meter = provider$1.getMeter("rsshub");
const requestTotal = meter.createCounter(`${METRIC_PREFIX}_request_total`);
const requestErrorTotal = meter.createCounter(`${METRIC_PREFIX}_request_error_total`);
const requestDurationSecondsBucket = meter.createHistogram(`${METRIC_PREFIX}_request_duration_seconds_bucket`, { advice: { explicitBucketBoundaries: config.otel.seconds_bucket?.split(",").map(Number) } });
const request_duration_milliseconds_bucket = meter.createHistogram(`${METRIC_PREFIX}_request_duration_milliseconds_bucket`, { advice: { explicitBucketBoundaries: config.otel.milliseconds_bucket?.split(",").map(Number) } });
const requestMetric = {
success: (value, attributes) => {
requestTotal.add(1, attributes);
request_duration_milliseconds_bucket.record(value, {
unit: "millisecond",
...attributes
});
requestDurationSecondsBucket.record(value / 1e3, {
unit: "second",
...attributes
});
},
error: (attributes) => {
requestErrorTotal.add(1, attributes);
}
};
const getContext = () => new Promise((resolve, reject) => {
exporter$1.collect().then((value) => {
resolve(serializer.serialize(value.resourceMetrics));
}).finally(() => {
reject("");
});
});
//#endregion
//#region lib/utils/otel/trace.ts
const exporter = new OTLPTraceExporter({});
const provider = new BasicTracerProvider({
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "rsshub" }),
spanProcessors: [new BatchSpanProcessor(exporter, {
maxQueueSize: 4096,
scheduledDelayMillis: 3e4
})]
});
trace.setGlobalTracerProvider(provider);
const tracer = provider.getTracer("rsshub");
const mainSpan = tracer.startSpan("main");
//#endregion
//#region lib/routes/metrics.ts
const handler$1 = (ctx) => getContext().then((val) => ctx.text(val)).catch((error) => {
ctx.status(500);
ctx.json({ error });
});
var metrics_default = handler$1;
//#endregion
//#region lib/routes/robots.txt.ts
const handler = (ctx) => {
if (config.disallowRobot) return ctx.text("User-agent: *\nDisallow: /");
else {
ctx.status(404);
return ctx.text("");
}
};
var robots_txt_default = handler;
//#endregion
//#region lib/registry.ts
const __dirname = import.meta.dirname;
function isSafeRoutes(routes) {
return Object.values(routes).every((route) => !route.features?.nsfw);
}
function safeNamespaces(namespaces$1) {
const safe = {};
for (const [key, value] of Object.entries(namespaces$1)) if (value.routes === null || value.routes === void 0 || isSafeRoutes(value.routes)) safe[key] = value;
return safe;
}
let modules = {};
let namespaces = {};
if (config.isPackage) namespaces = (await import("./routes-DSc26Lhr.mjs")).default;
else switch (process.env.NODE_ENV || process.env.VERCEL_ENV) {
case "production":
namespaces = (await import("./routes-DSc26Lhr.mjs")).default;
break;
case "test":
namespaces = await import("./routes-0gfsvfPA.mjs");
if (namespaces.default) namespaces = namespaces.default;
break;
default: modules = directoryImport({
targetDirectoryPath: path.join(__dirname, "./routes"),
importPattern: /\.ts$/
});
}
if (config.feature.disable_nsfw) namespaces = safeNamespaces(namespaces);
if (Object.keys(modules).length) for (const module in modules) {
const content = modules[module];
const namespace = module.split(/[/\\]/)[1];
if ("namespace" in content) namespaces[namespace] = Object.assign({ routes: {} }, namespaces[namespace], content.namespace);
else if ("route" in content) {
if (!namespaces[namespace]) namespaces[namespace] = {
name: namespace,
routes: {},
apiRoutes: {}
};
if (Array.isArray(content.route.path)) for (const path$1 of content.route.path) namespaces[namespace].routes[path$1] = {
...content.route,
location: module.split(/[/\\]/).slice(2).join("/")
};
else namespaces[namespace].routes[content.route.path] = {
...content.route,
location: module.split(/[/\\]/).slice(2).join("/")
};
} else if ("apiRoute" in content) {
if (!namespaces[namespace]) namespaces[namespace] = {
name: namespace,
routes: {},
apiRoutes: {}
};
if (Array.isArray(content.apiRoute.path)) for (const path$1 of content.apiRoute.path) namespaces[namespace].apiRoutes[path$1] = {
...content.apiRoute,
location: module.split(/[/\\]/).slice(2).join("/")
};
else namespaces[namespace].apiRoutes[content.apiRoute.path] = {
...content.apiRoute,
location: module.split(/[/\\]/).slice(2).join("/")
};
}
}
const app = new Hono();
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;
}
return 0;
});
for (const namespace in namespaces) {
const subApp = app.basePath(`/${namespace}`);
const namespaceData = namespaces[namespace];
if (!namespaceData || !namespaceData.routes) continue;
const sortedRoutes = sortRoutes(namespaceData.routes);
for (const [path$1, routeData] of sortedRoutes) {
const wrappedHandler = async (ctx) => {
logger_default.debug(`Matched route: ${routePath(ctx)}`);
if (!ctx.get("data")) {
if (typeof routeData.handler !== "function") {
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$1, wrappedHandler);
}
}
for (const namespace in 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$1, routeData] of sortedRoutes) {
const wrappedHandler = async (ctx) => {
if (!ctx.get("apiData")) {
if (typeof routeData.handler !== "function") {
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$1, wrappedHandler);
}
}
app.get("/", routes_default);
app.get("/healthz", healthz_default);
app.get("/robots.txt", robots_txt_default);
if (config.debugInfo) app.get("/metrics", metrics_default);
if (!config.isPackage) app.use("/*", serveStatic({
root: path.join(__dirname, "assets"),
rewriteRequestPath: (path$1) => path$1 === "/favicon.ico" ? "/favicon.png" : path$1
}));
var registry_default = app;
//#endregion
export { Layout as a, getDebugInfo as c, requestMetric as i, setDebugInfo as l, registry_default as n, gitDate as o, tracer as r, gitHash as s, namespaces as t };