diff options
Diffstat (limited to 'cgithub')
| l--------- | cgithub/GEMINI.md | 1 | ||||
| -rw-r--r-- | cgithub/package.json | 2 | ||||
| -rw-r--r-- | cgithub/src/index.ts | 31 | ||||
| -rw-r--r-- | cgithub/src/scraper.test.ts | 12 | ||||
| -rw-r--r-- | cgithub/src/scraper.ts | 11 | ||||
| -rw-r--r-- | cgithub/views/error.eta | 11 |
6 files changed, 58 insertions, 10 deletions
diff --git a/cgithub/GEMINI.md b/cgithub/GEMINI.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/cgithub/GEMINI.md @@ -0,0 +1 @@ +CLAUDE.md
\ No newline at end of file diff --git a/cgithub/package.json b/cgithub/package.json index 6faa90b..3d339a4 100644 --- a/cgithub/package.json +++ b/cgithub/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "node --watch --watch-preserve-output src/index.ts", "build": "rm -rf dist/* && esbuild --bundle --format=esm --platform=node --outdir=dist/build --sourcemap src/index.ts && cp -R static views dist/", - "test": "node --test src/scraper.test.ts" + "test": "node --test --test-concurrency=4 src/scraper.test.ts" }, "dependencies": { "@hono/node-server": "^1.13.7", diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts index 388b1e0..51e8cf2 100644 --- a/cgithub/src/index.ts +++ b/cgithub/src/index.ts @@ -1,10 +1,11 @@ import { createAdaptorServer, serve } from "@hono/node-server"; import { serveStatic } from "@hono/node-server/serve-static"; import { Eta } from "eta"; -import { Hono } from "hono"; +import { type Context, Hono } from "hono"; +import type { StatusCode } from "hono/utils/http-status"; import path from "node:path"; import url from "node:url"; -import { getGitHubBlob, getGitHubRepo, getGitHubTree } from "./scraper.ts"; +import { getGitHubBlob, getGitHubRepo, getGitHubTree, GitHubHTTPError } from "./scraper.ts"; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); @@ -17,22 +18,36 @@ 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 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/:repo", async (c) => { const { owner, repo } = c.req.param(); - const data = await getGitHubRepo(owner, repo); - return c.html(await eta.renderAsync("repo.eta", data)); + 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(); - const data = await getGitHubTree(owner, repo, branch, path); - return c.html(await eta.renderAsync("tree.eta", data)); + 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(); - const data = await getGitHubBlob(owner, repo, branch, path); - return c.html(await eta.renderAsync("blob.eta", data)); + return tryRender(c, "blob.eta", getGitHubBlob(owner, repo, branch, path)); }); app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => { diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index 3784190..adf70ae 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/src/scraper.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { getGitHubBlob, getGitHubRepo, getGitHubTree } from "./scraper.ts"; +import { getGitHubBlob, getGitHubRepo, getGitHubTree, GitHubHTTPError } from "./scraper.ts"; describe("GitHub scraper", () => { describe("actions/deploy-pages repository", () => { @@ -67,4 +67,14 @@ describe("GitHub scraper", () => { assert.match(data.content, /^MIT License\n\n/); }); }); + + describe("error handling", () => { + it("should throw for non-existent repository", async () => { + await assert.rejects(getGitHubRepo("nosuchowner", "nosuchrepo"), (err) => { + assert(err instanceof GitHubHTTPError, "error should be an HTTPError"); + assert.strictEqual(err.status, 404); + return true; + }); + }); + }); }); diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index 4feda73..b76f8c8 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -1,3 +1,13 @@ +export class GitHubHTTPError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.name = "GitHubHTTPError"; + this.status = status; + } +} + interface TreeItem { contentType: "directory" | "file"; name: string; @@ -78,6 +88,7 @@ async function fetchGitHubPage(path: string): Promise<string> { "Accept-Language": "en-US,en;q=0.5", }; const response = await fetch(`https://github.com/${path}`, { headers }); + if (!response.ok) throw new GitHubHTTPError(response.status, response.statusText); return response.text(); } diff --git a/cgithub/views/error.eta b/cgithub/views/error.eta new file mode 100644 index 0000000..784a843 --- /dev/null +++ b/cgithub/views/error.eta @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title><%= it.title ?? it.message %></title> + <link rel="stylesheet" href="/static/style.css"> +</head> +<body> + <%= it.message %> +</body> +</html> |
