From 9b556ffe6c0c0528697e15f7f4f35107de212ce1 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 15 Jul 2026 02:08:17 -0400 Subject: Add Cloudflare Workers deployment target Split routes into a shared createApp() (src/app.ts) used by both the Node entry (src/index.ts) and a new Workers entry (src/worker.ts). Workers disallow runtime code generation, so scripts/compile-views.ts precompiles the Eta templates into src/views.generated.ts and rendering switches from renderAsync to the sync render. Static files move to public/static so Workers Static Assets can serve them; the Node build and serveStatic root are updated to match. Deploy with pnpm run deploy:cf, local dev with pnpm run dev:cf. Co-Authored-By: Claude Fable 5 --- cgithub/scripts/compile-views.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 cgithub/scripts/compile-views.ts (limited to 'cgithub/scripts') diff --git a/cgithub/scripts/compile-views.ts b/cgithub/scripts/compile-views.ts new file mode 100644 index 0000000..785efc1 --- /dev/null +++ b/cgithub/scripts/compile-views.ts @@ -0,0 +1,29 @@ +// Precompiles views/*.eta into plain functions for the Cloudflare Workers +// build (src/views.generated.ts). Workers disallow runtime code generation +// (`new Function`), so Eta cannot compile templates there at request time. +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/compile-views.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`); -- cgit v1.3.1 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/.gitignore | 1 + cgithub/CLAUDE.md | 2 +- cgithub/package.json | 4 ++-- cgithub/scripts/build.ts | 52 ++++++++++++++++++++++++++++++++++++++++ cgithub/scripts/compile-views.ts | 29 ---------------------- cgithub/src/app.ts | 3 ++- cgithub/src/worker.ts | 2 +- cgithub/views/layout.eta | 2 +- 8 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 cgithub/scripts/build.ts delete mode 100644 cgithub/scripts/compile-views.ts (limited to 'cgithub/scripts') diff --git a/cgithub/.gitignore b/cgithub/.gitignore index 5a5e404..b8fc600 100644 --- a/cgithub/.gitignore +++ b/cgithub/.gitignore @@ -1,3 +1,4 @@ /node_modules/ /.wrangler/ +/src/commit.generated.ts /src/views.generated.ts diff --git a/cgithub/CLAUDE.md b/cgithub/CLAUDE.md index 37774cc..0ef4b47 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/compile-views.ts` precompiles templates to `src/views.generated.ts` (gitignored, 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/commit.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/package.json b/cgithub/package.json index 337fabc..7673612 100644 --- a/cgithub/package.json +++ b/cgithub/package.json @@ -5,8 +5,8 @@ "license": "MIT", "type": "module", "scripts": { - "dev": "node scripts/compile-views.ts && wrangler dev", - "deploy": "node scripts/compile-views.ts && wrangler deploy", + "dev": "node scripts/build.ts && wrangler dev", + "deploy": "node scripts/build.ts && wrangler deploy", "test": "node --test --test-concurrency=4 src/scraper.test.ts" }, "dependencies": { 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`); diff --git a/cgithub/scripts/compile-views.ts b/cgithub/scripts/compile-views.ts deleted file mode 100644 index 785efc1..0000000 --- a/cgithub/scripts/compile-views.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Precompiles views/*.eta into plain functions for the Cloudflare Workers -// build (src/views.generated.ts). Workers disallow runtime code generation -// (`new Function`), so Eta cannot compile templates there at request time. -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/compile-views.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`); diff --git a/cgithub/src/app.ts b/cgithub/src/app.ts index 5bed45d..0218918 100644 --- a/cgithub/src/app.ts +++ b/cgithub/src/app.ts @@ -1,6 +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 { getGitHubBlob, getGitHubCommits, @@ -47,7 +48,7 @@ export function createApp(eta: Eta) { // Our URLs mirror GitHub's, so the page we scraped is the same path there. const { pathname, search } = new URL(c.req.url); const githubUrl = `https://github.com${pathname}${search}`; - return c.html(eta.render(template, { ...data, githubUrl })); + return c.html(eta.render(template, { ...data, githubUrl, commit })); } // For fragments fetched asynchronously by client-side JS (see public/static/refs.js): diff --git a/cgithub/src/worker.ts b/cgithub/src/worker.ts index 024b01a..7bf9100 100644 --- a/cgithub/src/worker.ts +++ b/cgithub/src/worker.ts @@ -5,7 +5,7 @@ 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 +// scripts/build.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 }); diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta index f803caf..ca6fff2 100644 --- a/cgithub/views/layout.eta +++ b/cgithub/views/layout.eta @@ -34,7 +34,7 @@ git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git - + - - -
- -
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