From ab52f48e50d0dd242ad5b0cfbee9c458946cd427 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 11 Aug 2026 08:37:25 -0400 Subject: Show the build's commit in the page footer Workers can't shell out at request time, so the commit has to be baked in before wrangler bundles the sources. compile-views.ts grows a second job and becomes scripts/build.ts: alongside the precompiled views it writes src/commit.generated.ts with `git describe --tags --always --dirty`, which yields a bare short hash until the repo is tagged and marks builds made from an unclean working copy. Co-Authored-By: Claude Opus 5 --- cgithub/scripts/build.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 cgithub/scripts/build.ts (limited to 'cgithub/scripts/build.ts') diff --git a/cgithub/scripts/build.ts b/cgithub/scripts/build.ts new file mode 100644 index 0000000..766e5e8 --- /dev/null +++ b/cgithub/scripts/build.ts @@ -0,0 +1,52 @@ +// Generates the sources that can't exist at runtime on Workers: +// +// src/views.generated.ts - views/*.eta precompiled into plain functions. +// Workers disallow runtime code generation (`new Function`), so Eta cannot +// compile templates there at request time. +// src/commit.generated.ts - the commit this build was made from, for the +// page footer. Workers can't shell out to git at request time either. +import { execFileSync } from "node:child_process"; +import { Eta } from "eta"; +import fs from "node:fs"; +import path from "node:path"; + +const root = path.join(import.meta.dirname, ".."); +const viewsDir = path.join(root, "views"); +const eta = new Eta({ views: viewsDir }); + +const names = fs + .readdirSync(viewsDir) + .filter((name) => name.endsWith(".eta")) + .sort(); + +let out = `// @ts-nocheck +// Generated by scripts/build.ts -- do not edit. +export const views = { +`; +for (const name of names) { + const source = fs.readFileSync(path.join(viewsDir, name), "utf8"); + const body = eta.compileToString(source); + out += ` ${JSON.stringify(name)}: function (it, options) {\n${body}\n },\n`; +} +out += "};\n"; + +fs.writeFileSync(path.join(root, "src", "views.generated.ts"), out); +console.log(`Compiled ${names.length} templates to src/views.generated.ts`); + +// Untagged repositories describe as a bare short hash; --dirty marks a build +// made from an unclean working copy. +let commit = "unknown"; +try { + commit = execFileSync("git", ["describe", "--tags", "--always", "--dirty"], { + cwd: root, + encoding: "utf8", + }).trim(); +} catch { + // Building from a source copy without git history. +} + +fs.writeFileSync( + path.join(root, "src", "commit.generated.ts"), + `// Generated by scripts/build.ts -- do not edit.\nexport const commit = ${JSON.stringify(commit)};\n`, +); +console.log(`Wrote commit ${commit} to src/commit.generated.ts`); -- cgit v1.3.1 From 439f783c9e3b18519505d1ad6b8533bea081cca0 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Thu, 3 Sep 2026 15:07:25 -0400 Subject: Clean up renderer, redirects, and build commit --- cgithub/.gitignore | 2 +- cgithub/CLAUDE.md | 2 +- cgithub/scripts/build.ts | 12 ++++----- cgithub/src/app.test.ts | 8 ++++-- cgithub/src/app.ts | 63 ++++++++++++++++++++++++++++++------------------ cgithub/views/layout.eta | 2 +- 6 files changed, 54 insertions(+), 35 deletions(-) (limited to 'cgithub/scripts/build.ts') diff --git a/cgithub/.gitignore b/cgithub/.gitignore index b8fc600..55b5412 100644 --- a/cgithub/.gitignore +++ b/cgithub/.gitignore @@ -1,4 +1,4 @@ /node_modules/ /.wrangler/ -/src/commit.generated.ts +/src/build.generated.ts /src/views.generated.ts diff --git a/cgithub/CLAUDE.md b/cgithub/CLAUDE.md index 3f0cd61..edb33f5 100644 --- a/cgithub/CLAUDE.md +++ b/cgithub/CLAUDE.md @@ -24,7 +24,7 @@ pnpm test - `src/worker.ts` - Cloudflare Workers entry (config in `wrangler.jsonc`) - `src/scraper.ts` - GitHub HTML fetching and JSON extraction logic -**Templates:** `views/*.eta` - Eta templates with layout inheritance (`layout.eta` as base). On Workers, runtime code generation is disallowed, so `scripts/build.ts` precompiles templates to `src/views.generated.ts`. The same script records the build's `git describe` output in `src/commit.generated.ts` for the page footer. Both are gitignored and regenerated by the `dev`/`deploy` scripts. +**Templates:** `views/*.eta` - Eta templates with layout inheritance (`layout.eta` as base). On Workers, runtime code generation is disallowed, so `scripts/build.ts` precompiles templates to `src/views.generated.ts`. The same script records the build's `git describe` output in `src/build.generated.ts` for the page footer. Both are gitignored and regenerated by the `dev`/`deploy` scripts. **Static files:** `public/static/*`, served by Workers Static Assets. diff --git a/cgithub/scripts/build.ts b/cgithub/scripts/build.ts index 766e5e8..76d88a0 100644 --- a/cgithub/scripts/build.ts +++ b/cgithub/scripts/build.ts @@ -3,7 +3,7 @@ // src/views.generated.ts - views/*.eta precompiled into plain functions. // Workers disallow runtime code generation (`new Function`), so Eta cannot // compile templates there at request time. -// src/commit.generated.ts - the commit this build was made from, for the +// src/build.generated.ts - the commit this build was made from, for the // page footer. Workers can't shell out to git at request time either. import { execFileSync } from "node:child_process"; import { Eta } from "eta"; @@ -35,9 +35,9 @@ console.log(`Compiled ${names.length} templates to src/views.generated.ts`); // Untagged repositories describe as a bare short hash; --dirty marks a build // made from an unclean working copy. -let commit = "unknown"; +let buildCommit = "unknown"; try { - commit = execFileSync("git", ["describe", "--tags", "--always", "--dirty"], { + buildCommit = execFileSync("git", ["describe", "--tags", "--always", "--dirty"], { cwd: root, encoding: "utf8", }).trim(); @@ -46,7 +46,7 @@ try { } fs.writeFileSync( - path.join(root, "src", "commit.generated.ts"), - `// Generated by scripts/build.ts -- do not edit.\nexport const commit = ${JSON.stringify(commit)};\n`, + path.join(root, "src", "build.generated.ts"), + `// Generated by scripts/build.ts -- do not edit.\nexport const buildCommit = ${JSON.stringify(buildCommit)};\n`, ); -console.log(`Wrote commit ${commit} to src/commit.generated.ts`); +console.log(`Wrote commit ${buildCommit} to src/build.generated.ts`); diff --git a/cgithub/src/app.test.ts b/cgithub/src/app.test.ts index 8519d91..274b2d5 100644 --- a/cgithub/src/app.test.ts +++ b/cgithub/src/app.test.ts @@ -195,7 +195,10 @@ describe("commit history", () => { assert.match(html, /href="\/actions\/deploy-pages\/commits\/main">\(top\)srclib<\/a>\/index\.js/); + assert.match( + html, + /href="\/actions\/deploy-pages\/commits\/main\/src\/lib">lib<\/a>\/index\.js/, + ); }); it("should say so when a path has no history", () => { @@ -363,9 +366,9 @@ describe("redirects to GitHub", () => { assert.strictEqual(res.status, 200); assert.strictEqual(res.headers.get("location"), null); - assert.strictEqual(res.headers.get("Referrer-Policy"), "no-referrer"); const body = await res.text(); + assert.match(body, //); assert.match( body, //, @@ -382,6 +385,7 @@ describe("redirects to GitHub", () => { assert.strictEqual(res.status, 302); assert.strictEqual(res.headers.get("location"), "https://github.com/a/b/c?x=1"); + assert.strictEqual(res.headers.get("Referrer-Policy"), "no-referrer"); }); it("should use a meta refresh when the referer is another site", async () => { diff --git a/cgithub/src/app.ts b/cgithub/src/app.ts index 956a8cb..bbe07fc 100644 --- a/cgithub/src/app.ts +++ b/cgithub/src/app.ts @@ -1,7 +1,7 @@ import type { Eta } from "eta"; import { type Context, Hono } from "hono"; import type { StatusCode } from "hono/utils/http-status"; -import { commit } from "./commit.generated.ts"; +import { buildCommit } from "./build.generated.ts"; import { getGitHubBlob, getGitHubCommits, @@ -22,7 +22,12 @@ import { RedirectError, } from "./scraper.ts"; -// Our URLs mirror GitHub's, so the same path there is the page we scraped. +declare module "hono" { + interface ContextRenderer { + (template: string, data?: object): Response; + } +} + function githubUrlFor(c: Context) { const { pathname, search } = new URL(c.req.url); return `https://github.com${pathname}${search}`; @@ -31,23 +36,36 @@ function githubUrlFor(c: Context) { export function createApp(eta: Eta) { const app = new Hono(); - // Use a meta refresh to avoid redirect loops in certain situations; we become - // the initiator origin even if we are the target of a redirection. Requests - // coming from one of our own pages are already past that hazard, and are - // often subresources (an in a rendered README) that can't do anything - // with an HTML page, so those get a real HTTP redirect. - function redirectToGitHub(c: Context, location: string) { + app.use(async (c, next) => { + // Include render data for layout.eta. + c.setRenderer((template, data) => + c.html(eta.render(template, { ...data, githubUrl: githubUrlFor(c), buildCommit })), + ); + await next(); + }); + + // Redirect off-domain. + function redirectTo(c: Context, location: string) { const referer = c.req.header("Referer"); + // If we are the referer, redirect directly so we can hotlink resources + // (e.g., in README). if (referer && URL.parse(referer)?.origin === new URL(c.req.url).origin) { + c.header("Referrer-Policy", "no-referrer"); return c.redirect(location); } - c.header("Referrer-Policy", "no-referrer"); - return c.html(eta.render("redirect.eta", { location })); + // Otherwise use a meta refresh to make ourself the initiator. This will + // avoid redirect loops if we are part of excludedInitiatorDomains in a + // declarativeNetRequest. + return c.render("redirect.eta", { location }); + } + + function redirectToGitHub(c: Context) { + return redirectTo(c, githubUrlFor(c)); } app.get("/", async (c) => { - return c.html(eta.render("home.eta", {})); + return c.render("home.eta"); }); async function tryRender(c: Context, template: string, promise: Promise) { @@ -57,19 +75,19 @@ export function createApp(eta: Eta) { } catch (e) { if (e instanceof RedirectError) { if (e.location.startsWith("https://")) { - return redirectToGitHub(c, e.location); + return redirectTo(c, e.location); } 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 })); + return c.render("error.eta", { title: e.message, message }); } else { c.status(500); - return c.html(eta.render("error.eta", { message: "" + e })); + return c.render("error.eta", { message: "" + e }); } } - return c.html(eta.render(template, { ...data, githubUrl: githubUrlFor(c), commit })); + return c.render(template, data); } // For fragments fetched asynchronously by client-side JS (see public/static/refs.js): @@ -92,7 +110,7 @@ export function createApp(eta: Eta) { c.status(status); return c.body(null); } - return c.html(eta.render(template, data)); + return c.render(template, data); } app.get("/:owner", async (c) => { @@ -117,10 +135,7 @@ export function createApp(eta: Eta) { app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => { const { owner, repo, branch, path } = c.req.param(); - return redirectToGitHub( - c, - `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`, - ); + return redirectTo(c, `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`); }); // This is not a native GitHub route, but instead it lets us render blob @@ -165,7 +180,7 @@ export function createApp(eta: Eta) { app.get("/:owner/:repo/wiki/:page/:oid", async (c) => { const { owner, repo, page, oid } = c.req.param(); if (page.startsWith("_") || !/^[0-9a-f]{40}$/.test(oid)) { - return redirectToGitHub(c, githubUrlFor(c)); + return redirectToGitHub(c); } return tryRender(c, "wiki.eta", getGitHubWiki(owner, repo, page, oid)); }); @@ -173,7 +188,7 @@ export function createApp(eta: Eta) { app.get("/:owner/:repo/wiki/:page", async (c) => { const { owner, repo, page } = c.req.param(); if (page.startsWith("_")) { - return redirectToGitHub(c, githubUrlFor(c)); + return redirectToGitHub(c); } return tryRender(c, "wiki.eta", getGitHubWiki(owner, repo, page)); }); @@ -209,7 +224,7 @@ export function createApp(eta: Eta) { } // Redirect unhandled search types (like code, which requires sign in anyway). - return redirectToGitHub(c, githubUrlFor(c)); + return redirectToGitHub(c); }); app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => { @@ -260,7 +275,7 @@ export function createApp(eta: Eta) { }); app.all("*", async (c) => { - return redirectToGitHub(c, githubUrlFor(c)); + return redirectToGitHub(c); }); return app; diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta index 1ac82cc..37289dd 100644 --- a/cgithub/views/layout.eta +++ b/cgithub/views/layout.eta @@ -35,7 +35,7 @@ git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git - +
Generated by cgithub commit <%= it.buildCommit %> · View original
- - -
- -
git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git
-git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git
- -
- "> - - -
-
- - <%~ it.body %> - -
Generated by cgithub commit <%= it.buildCommit %> · View original
- - - - diff --git a/cgithub/views/layouts/base.eta b/cgithub/views/layouts/base.eta new file mode 100644 index 0000000..fb644f5 --- /dev/null +++ b/cgithub/views/layouts/base.eta @@ -0,0 +1,17 @@ + + + + + + <%= it.title %> + + <% for (const src of it.scripts ?? []) { %> + + <% } %> + + + <%~ it.body %> + +
Generated by cgithub commit <%= it.buildCommit %> · View original
+ + diff --git a/cgithub/views/layouts/repo.eta b/cgithub/views/layouts/repo.eta new file mode 100644 index 0000000..e4f7880 --- /dev/null +++ b/cgithub/views/layouts/repo.eta @@ -0,0 +1,59 @@ +<% +layout("/layouts/base.eta", { + title: `${it.repo.owner}/${it.repo.name} - ${it.path || "/"}`, + scripts: ["/static/refs.js"], +}) +%> + +
+ +
git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git
+git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git
+ +
+ "> + + +
+
+ +<%~ it.body %> + + diff --git a/cgithub/views/owner.eta b/cgithub/views/owner.eta index e5667fb..b3304fb 100644 --- a/cgithub/views/owner.eta +++ b/cgithub/views/owner.eta @@ -1,65 +1,54 @@ - - - - - - <%= it.name ? `${it.name} (${it.login})` : it.login %> - - - -
- <% if (it.avatarUrl) { %> - - <% } %> -
<%= it.name || it.login %>
-
<%= it.login %><%= it.type === "organization" ? " (organization)" : "" %>
- <% if (it.location) { %> -
<%= it.location %>
- <% } %> - <% if (it.nav.length > 0) { %> - - <% } %> -
+<% layout("/layouts/base.eta", { title: it.name ? `${it.name} (${it.login})` : it.login }) %> - <% if (it.bio || it.links.length > 0 || it.followers || it.company) { %> -
- <% if (it.bio) { %> -
<%= it.bio %>
- <% } %> - <% for (const link of it.links) { %> - - <% } %> - <% if (it.followers) { %> -
<%= it.followers %> followers<%= it.following ? ` · ${it.following} following` : "" %>
- <% } %> - <% if (it.company) { %> -
<%= it.company %>
- <% } %> -
+
+ <% if (it.avatarUrl) { %> + <% } %> - - <% if (it.pinned.length > 0) { %> -
-

Pinned

- <% for (const repo of it.pinned) { %> - <%~ include("./_ownerRepo", { repo, login: it.login }) %> - <% } %> -
+
<%= it.name || it.login %>
+
<%= it.login %><%= it.type === "organization" ? " (organization)" : "" %>
+ <% if (it.location) { %> +
<%= it.location %>
<% } %> - - <% if (it.repos.length > 0) { %> -
-

Repositories

- <% for (const repo of it.repos) { %> - <%~ include("./_ownerRepo", { repo, login: it.login }) %> + <% if (it.nav.length > 0) { %> +
+ <% } %> +
+ +<% if (it.bio || it.links.length > 0 || it.followers || it.company) { %> +
+ <% if (it.bio) { %> +
<%= it.bio %>
+ <% } %> + <% for (const link of it.links) { %> + + <% } %> + <% if (it.followers) { %> +
<%= it.followers %> followers<%= it.following ? ` · ${it.following} following` : "" %>
+ <% } %> + <% if (it.company) { %> +
<%= it.company %>
+ <% } %> +
+<% } %> -
Generated by cgithub
- - +<% if (it.pinned.length > 0) { %> +
+

Pinned

+ <% for (const repo of it.pinned) { %> + <%~ include("./_ownerRepo", { repo, login: it.login }) %> + <% } %> +
+<% } %> + +<% if (it.repos.length > 0) { %> +
+

Repositories

+ <% for (const repo of it.repos) { %> + <%~ include("./_ownerRepo", { repo, login: it.login }) %> + <% } %> +
+<% } %> diff --git a/cgithub/views/pulls.eta b/cgithub/views/pulls.eta index 9ec5ca1..697f6b1 100644 --- a/cgithub/views/pulls.eta +++ b/cgithub/views/pulls.eta @@ -1,4 +1,4 @@ -<% layout("./layout"); it.searchType = "pullrequests" %> +<% layout("/layouts/repo.eta"); it.searchType = "pullrequests" %> <% for (const pull of it.pulls) { %> diff --git a/cgithub/views/release.eta b/cgithub/views/release.eta index 46dd5db..64cca35 100644 --- a/cgithub/views/release.eta +++ b/cgithub/views/release.eta @@ -1,3 +1,3 @@ -<% layout("./layout.eta") %> +<% layout("/layouts/repo.eta") %> <%~ include("./_release.eta", { repo: it.repo, release: it.release, linkToDetail: false }) %> diff --git a/cgithub/views/releases.eta b/cgithub/views/releases.eta index 840908c..4ede28b 100644 --- a/cgithub/views/releases.eta +++ b/cgithub/views/releases.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta") %> +<% layout("/layouts/repo.eta") %> <% for (const release of it.releases) { %> <%~ include("./_release.eta", { repo: it.repo, release, linkToDetail: true }) %> diff --git a/cgithub/views/repo.eta b/cgithub/views/repo.eta index 5dc6beb..60746df 100644 --- a/cgithub/views/repo.eta +++ b/cgithub/views/repo.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta") %> +<% layout("/layouts/repo.eta") %> <% if (it.info) { %>
diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta index 41ded06..fabf5b9 100644 --- a/cgithub/views/tree.eta +++ b/cgithub/views/tree.eta @@ -1,3 +1,3 @@ -<% layout("./layout.eta"); it.searchType = "code" %> +<% layout("/layouts/repo.eta"); it.searchType = "code" %> <%~ include("./_tree.eta", it) %> diff --git a/cgithub/views/wiki.eta b/cgithub/views/wiki.eta index d6047e1..781caff 100644 --- a/cgithub/views/wiki.eta +++ b/cgithub/views/wiki.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta"); it.path = "wiki/" + it.title %> +<% layout("/layouts/repo.eta"); it.path = "wiki/" + it.title %> <%~ include("./_wikiNav.eta", it) %> diff --git a/cgithub/views/wikiPages.eta b/cgithub/views/wikiPages.eta index 58d6d21..eb90781 100644 --- a/cgithub/views/wikiPages.eta +++ b/cgithub/views/wikiPages.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta"); it.path = "wiki/_pages" %> +<% layout("/layouts/repo.eta"); it.path = "wiki/_pages" %> <%~ include("./_wikiNav.eta", it) %> -- cgit v1.3.1