summaryrefslogtreecommitdiff
path: root/cgithub/src
diff options
context:
space:
mode:
Diffstat (limited to 'cgithub/src')
-rw-r--r--cgithub/src/app.ts132
-rw-r--r--cgithub/src/index.ts132
-rw-r--r--cgithub/src/worker.ts27
3 files changed, 164 insertions, 127 deletions
diff --git a/cgithub/src/app.ts b/cgithub/src/app.ts
new file mode 100644
index 0000000..78b59ae
--- /dev/null
+++ b/cgithub/src/app.ts
@@ -0,0 +1,132 @@
+import type { Eta } from "eta";
+import { type Context, Hono } from "hono";
+import type { StatusCode } from "hono/utils/http-status";
+import {
+ getGitHubBlob,
+ getGitHubCommits,
+ getGitHubIssues,
+ getGitHubOwner,
+ getGitHubPulls,
+ getGitHubRelease,
+ getGitHubReleases,
+ getGitHubRepo,
+ getGitHubTree,
+ GitHubHTTPError,
+ RedirectError,
+} from "./scraper.ts";
+
+export function createApp(eta: Eta) {
+ const app = new Hono();
+
+ app.get("/", async (c) => {
+ return c.html(eta.render("home.eta", {}));
+ });
+
+ async function tryRender<T extends object>(c: Context, template: string, promise: Promise<T>) {
+ let data: T;
+ try {
+ data = await promise;
+ } catch (e) {
+ if (e instanceof RedirectError) {
+ if (e.location.startsWith("https://")) {
+ c.header("Referrer-Policy", "no-referrer");
+ }
+ return c.redirect(e.location);
+ } else if (e instanceof GitHubHTTPError) {
+ c.status(e.status as StatusCode);
+ const message = `GitHub responded with HTTP ${e.status} ${e.message}`;
+ return c.html(eta.render("error.eta", { title: e.message, message }));
+ } else {
+ c.status(500);
+ return c.html(eta.render("error.eta", { message: "" + e }));
+ }
+ }
+ return c.html(eta.render(template, data));
+ }
+
+ app.get("/:owner", async (c) => {
+ const { owner } = c.req.param();
+ return tryRender(c, "owner.eta", getGitHubOwner(owner));
+ });
+
+ app.get("/:owner/:repo", async (c) => {
+ const { owner, repo } = c.req.param();
+ return tryRender(c, "repo.eta", getGitHubRepo(owner, repo));
+ });
+
+ app.get("/:owner/:repo/tree/:branch/:path{.*}?", async (c) => {
+ const { owner, repo, branch, path = "" } = c.req.param();
+ return tryRender(c, "tree.eta", getGitHubTree(owner, repo, branch, path));
+ });
+
+ app.get("/:owner/:repo/blob/:branch/:path{.*}", async (c) => {
+ const { owner, repo, branch, path } = c.req.param();
+ return tryRender(c, "blob.eta", getGitHubBlob(owner, repo, branch, path));
+ });
+
+ app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => {
+ const { owner, repo, branch, path } = c.req.param();
+ c.header("Referrer-Policy", "no-referrer");
+ return c.redirect(`https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`);
+ });
+
+ app.get("/:owner/:repo/issues", async (c) => {
+ const { owner, repo } = c.req.param();
+ const q = c.req.query("q");
+
+ // GitHub supports combined issue/PR search, apparently for legacy. We just
+ // redirect to issue-only search.
+ if (q && !/\bis:issue\b/.test(q)) {
+ return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(`is:issue ${q}`)}`);
+ }
+
+ return tryRender(c, "issues.eta", getGitHubIssues(owner, repo, { q }));
+ });
+
+ app.get("/:owner/:repo/pulls", async (c) => {
+ const { owner, repo } = c.req.param();
+ const q = c.req.query("q");
+ return tryRender(c, "pulls.eta", getGitHubPulls(owner, repo, { q }));
+ });
+
+ app.get("/:owner/:repo/search", async (c) => {
+ const { owner, repo } = c.req.param();
+ const q = c.req.query("q") ?? "";
+ const type = c.req.query("type") ?? "issues";
+
+ if (type === "issues") {
+ return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(q)}`);
+ } else if (type === "pullrequests") {
+ return c.redirect(`/${owner}/${repo}/pulls?q=${encodeURIComponent(q)}`);
+ }
+
+ // Redirect unhandled search types (like code, which requires sign in anyway).
+ c.header("Referrer-Policy", "no-referrer");
+ return c.redirect(
+ `https://github.com/${owner}/${repo}/search?${new URLSearchParams(c.req.query()).toString()}`,
+ );
+ });
+
+ app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => {
+ const { owner, repo, branch, path = "" } = c.req.param();
+ return tryRender(c, "commits.eta", getGitHubCommits(owner, repo, branch, path));
+ });
+
+ app.get("/:owner/:repo/releases/tag/:tag", async (c) => {
+ const { owner, repo, tag } = c.req.param();
+ return tryRender(c, "release.eta", getGitHubRelease(owner, repo, tag));
+ });
+
+ app.get("/:owner/:repo/releases", async (c) => {
+ const { owner, repo } = c.req.param();
+ return tryRender(c, "releases.eta", getGitHubReleases(owner, repo));
+ });
+
+ app.all("*", async (c) => {
+ const path = c.req.path;
+ c.header("Referrer-Policy", "no-referrer");
+ return c.redirect(`https://github.com${path}`);
+ });
+
+ return app;
+}
diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts
index f676bd6..65c46d9 100644
--- a/cgithub/src/index.ts
+++ b/cgithub/src/index.ts
@@ -1,140 +1,18 @@
import { createAdaptorServer, serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { Eta } from "eta";
-import { type Context, Hono } from "hono";
-import type { StatusCode } from "hono/utils/http-status";
+import { Hono } from "hono";
import path from "node:path";
import url from "node:url";
-import {
- getGitHubBlob,
- getGitHubCommits,
- getGitHubIssues,
- getGitHubOwner,
- getGitHubPulls,
- getGitHubRelease,
- getGitHubReleases,
- getGitHubRepo,
- getGitHubTree,
- GitHubHTTPError,
- RedirectError,
-} from "./scraper.ts";
+import { createApp } from "./app.ts";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
-const app = new Hono();
const eta = new Eta({ views: path.join(__dirname, "..", "views") });
-app.use("/static/*", serveStatic({ root: path.join(__dirname, "..") }));
-
-app.get("/", async (c) => {
- return c.html(await eta.renderAsync("home.eta", {}));
-});
-
-async function tryRender<T extends object>(c: Context, template: string, promise: Promise<T>) {
- let data: T;
- try {
- data = await promise;
- } catch (e) {
- if (e instanceof RedirectError) {
- if (e.location.startsWith("https://")) {
- c.header("Referrer-Policy", "no-referrer");
- }
- return c.redirect(e.location);
- } else if (e instanceof GitHubHTTPError) {
- c.status(e.status as StatusCode);
- const message = `GitHub responded with HTTP ${e.status} ${e.message}`;
- return c.html(await eta.renderAsync("error.eta", { title: e.message, message }));
- } else {
- c.status(500);
- return c.html(await eta.renderAsync("error.eta", { message: "" + e }));
- }
- }
- return c.html(await eta.renderAsync(template, data));
-}
-
-app.get("/:owner", async (c) => {
- const { owner } = c.req.param();
- return tryRender(c, "owner.eta", getGitHubOwner(owner));
-});
-
-app.get("/:owner/:repo", async (c) => {
- const { owner, repo } = c.req.param();
- return tryRender(c, "repo.eta", getGitHubRepo(owner, repo));
-});
-
-app.get("/:owner/:repo/tree/:branch/:path{.*}?", async (c) => {
- const { owner, repo, branch, path = "" } = c.req.param();
- return tryRender(c, "tree.eta", getGitHubTree(owner, repo, branch, path));
-});
-
-app.get("/:owner/:repo/blob/:branch/:path{.*}", async (c) => {
- const { owner, repo, branch, path } = c.req.param();
- return tryRender(c, "blob.eta", getGitHubBlob(owner, repo, branch, path));
-});
-
-app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => {
- const { owner, repo, branch, path } = c.req.param();
- c.header("Referrer-Policy", "no-referrer");
- return c.redirect(`https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`);
-});
-
-app.get("/:owner/:repo/issues", async (c) => {
- const { owner, repo } = c.req.param();
- const q = c.req.query("q");
-
- // GitHub supports combined issue/PR search, apparently for legacy. We just
- // redirect to issue-only search.
- if (q && !/\bis:issue\b/.test(q)) {
- return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(`is:issue ${q}`)}`);
- }
-
- return tryRender(c, "issues.eta", getGitHubIssues(owner, repo, { q }));
-});
-
-app.get("/:owner/:repo/pulls", async (c) => {
- const { owner, repo } = c.req.param();
- const q = c.req.query("q");
- return tryRender(c, "pulls.eta", getGitHubPulls(owner, repo, { q }));
-});
-
-app.get("/:owner/:repo/search", async (c) => {
- const { owner, repo } = c.req.param();
- const q = c.req.query("q") ?? "";
- const type = c.req.query("type") ?? "issues";
-
- if (type === "issues") {
- return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(q)}`);
- } else if (type === "pullrequests") {
- return c.redirect(`/${owner}/${repo}/pulls?q=${encodeURIComponent(q)}`);
- }
-
- // Redirect unhandled search types (like code, which requires sign in anyway).
- c.header("Referrer-Policy", "no-referrer");
- return c.redirect(
- `https://github.com/${owner}/${repo}/search?${new URLSearchParams(c.req.query()).toString()}`,
- );
-});
-
-app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => {
- const { owner, repo, branch, path = "" } = c.req.param();
- return tryRender(c, "commits.eta", getGitHubCommits(owner, repo, branch, path));
-});
-
-app.get("/:owner/:repo/releases/tag/:tag", async (c) => {
- const { owner, repo, tag } = c.req.param();
- return tryRender(c, "release.eta", getGitHubRelease(owner, repo, tag));
-});
-
-app.get("/:owner/:repo/releases", async (c) => {
- const { owner, repo } = c.req.param();
- return tryRender(c, "releases.eta", getGitHubReleases(owner, repo));
-});
-
-app.all("*", async (c) => {
- const path = c.req.path;
- c.header("Referrer-Policy", "no-referrer");
- return c.redirect(`https://github.com${path}`);
-});
+const app = new Hono();
+app.use("/static/*", serveStatic({ root: path.join(__dirname, "..", "public") }));
+app.route("/", createApp(eta));
if (process.env.LISTEN_FDS === "1") {
const server = createAdaptorServer(app);
diff --git a/cgithub/src/worker.ts b/cgithub/src/worker.ts
new file mode 100644
index 0000000..024b01a
--- /dev/null
+++ b/cgithub/src/worker.ts
@@ -0,0 +1,27 @@
+// Cloudflare Workers entry point. Static files under public/ are served by
+// Workers Static Assets before the worker runs (see wrangler.jsonc).
+import { Eta } from "eta";
+import { createApp } from "./app.ts";
+import { views } from "./views.generated.ts";
+
+// Workers disallow runtime code generation, so templates are precompiled by
+// scripts/compile-views.ts and preloaded into Eta's cache here. With
+// cache: true, Eta serves them from the cache and never compiles.
+const eta = new Eta({ cache: true });
+
+eta.resolvePath = (templatePath: string) => {
+ let name = templatePath.replace(/^\.\//, "");
+ if (!name.endsWith(".eta")) name += ".eta";
+ return name;
+};
+
+// Only reached on a cache miss, i.e. a template missing from the build.
+eta.readFile = (path: string) => {
+ throw new Error(`Unknown template: ${path}`);
+};
+
+for (const [name, fn] of Object.entries(views)) {
+ eta.loadTemplate(name, fn);
+}
+
+export default createApp(eta);