From 7ba3c74910044cde5b4f730871f28700f0ce6fb8 Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:08:21 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Point=20the=20site=20at=20a=20local?= =?UTF-8?q?=20effectionx=20checkout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The site clones thefrontside/effectionx into build/clones and reads its packages from main, so there was no way to see a package you were working on until it was pushed and merged. `initClones` now takes checkouts, a map of `owner/repo` to a directory to use in place of a clone, and main wires the `effectionxDir` parameter to it: EFFECTIONX_DIR=../effectionx deno task dev deno task dev --effectionx-dir ../effectionx A mapped repository is never cloned, fetched or reset, so the site reads the branch and the uncommitted work that are there and cannot disturb a checkout you are working in. Paths are resolved before the clone directory is emptied, so a path that is not there fails at startup rather than on the first request that needs the repository. --- www/README.md | 26 +++++++++++++++++++++++++ www/cli.ts | 7 +++++++ www/deno.json | 2 +- www/lib/clones.test.ts | 22 +++++++++++++++++++++ www/lib/clones.ts | 44 +++++++++++++++++++++++++++++++++++++++++- www/main.tsx | 16 ++++++++++++++- 6 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 www/lib/clones.test.ts diff --git a/www/README.md b/www/README.md index 28686366e..47fb0562e 100644 --- a/www/README.md +++ b/www/README.md @@ -21,6 +21,7 @@ environment variable — and the command line wins when both are supplied. | `--github-token` | `GITHUB_TOKEN` | _unauthenticated_ | GitHub access token for the API | | `--jsr-api` | `JSR_API` | _none_ | JSR API token; the score card is skipped without it | | `--deno-deployment-id` | `DENO_DEPLOYMENT_ID` | fresh id per boot | Deployment identity behind the `ETag` | +| `--effectionx-dir` | `EFFECTIONX_DIR` | _none_ | Local effectionx checkout to read instead of cloning | | `--clones-dir` | `CLONES_DIR` | `build/clones` | Git clones of the documented repositories | | `--worktrees-dir` | `WORKTREES_DIR` | `build/worktrees` | A git worktree per documented series | | `--pagefind-dir` | `PAGEFIND_DIR` | `pagefind` | Generated Pagefind search bundle | @@ -31,6 +32,31 @@ environment variable — and the command line wins when both are supplied. The defaults reproduce what used to be hardcoded, so `deno task dev` and the deployment workflow need no flags. +## Development + +``` +deno task dev +``` + +### Using a local checkout of effectionx + +The website clones +[thefrontside/effectionx](https://github.com/thefrontside/effectionx) into +`build/clones` and reads its packages from `main`. To see a checkout you are +working in instead — its branch, its uncommitted changes and all — point +`EFFECTIONX_DIR` at it: + +``` +EFFECTIONX_DIR=../effectionx deno task dev +``` + +`--effectionx-dir ../effectionx` does the same thing, as with every other +parameter in the table above. + +The directory is used exactly as it is on disk. It is never fetched or reset, so +the site cannot disturb work in progress, and a path that does not exist fails +at startup rather than on the first request that needs it. + ## About Git Integration The Effection website uses sophisticated GitHub integration to dynamically load diff --git a/www/cli.ts b/www/cli.ts index d29e49dac..15e0f14e9 100644 --- a/www/cli.ts +++ b/www/cli.ts @@ -65,6 +65,13 @@ export const www = command( ), schema(fallback("")), ), + option( + name("effectionxDir"), + description( + "Local checkout of thefrontside/effectionx to read instead of cloning it.", + ), + schema(fallback("")), + ), option( name("clonesDir"), description("Directory holding git clones of the documented repositories."), diff --git a/www/deno.json b/www/deno.json index 2f1487f67..b2810587e 100644 --- a/www/deno.json +++ b/www/deno.json @@ -2,7 +2,7 @@ "tasks": { "dev": "deno run -A @effectionx/watch deno run -A main.tsx", "staticalize": "deno run -A jsr:@frontside/staticalize@0.2.2/cli --site http://localhost:8000 --output=built --base=http://localhost:8000", - "test": "deno test --allow-run --allow-write --allow-read" + "test": "deno test --allow-run --allow-write --allow-read --allow-env" }, "lint": { "exclude": [ diff --git a/www/lib/clones.test.ts b/www/lib/clones.test.ts new file mode 100644 index 000000000..092a10b89 --- /dev/null +++ b/www/lib/clones.test.ts @@ -0,0 +1,22 @@ +import { assertEquals, assertThrows } from "@std/assert"; +import { resolve } from "node:path"; + +import { resolveCheckouts } from "./clones.ts"; + +Deno.test("resolveCheckouts resolves a checkout to an absolute path", () => { + let lib = import.meta.dirname!; + + assertEquals(resolveCheckouts({ "acme/widgets": `${lib}/../lib` }), { + "acme/widgets": lib, + }); +}); + +Deno.test("resolveCheckouts rejects a directory that is not there", () => { + let missing = resolve(import.meta.dirname!, "nowhere"); + + assertThrows( + () => resolveCheckouts({ "acme/widgets": missing }), + Error, + `cannot use ${missing} as a local checkout of acme/widgets: no such directory`, + ); +}); diff --git a/www/lib/clones.ts b/www/lib/clones.ts index c70eae167..02da4225d 100644 --- a/www/lib/clones.ts +++ b/www/lib/clones.ts @@ -15,7 +15,28 @@ type Checkout = (nameWithOwner: string) => Operation; const Clones = createContext("clones"); -export function* initClones(path: string): Operation { +export interface ClonesOptions { + /** + * Directories to use in place of a clone, keyed by `owner/repo`. + * + * A local checkout is used exactly as it is on disk. It is never fetched or + * reset, both so that uncommitted work shows up on the site, and so that the + * site never touches a checkout you are working in. + */ + checkouts?: Record; +} + +export function* initClones( + path: string, + options: ClonesOptions = {}, +): Operation { + // resolved before anything is removed, so that a bad path fails at startup + // rather than on the first request that needs the repo + let checkouts = resolveCheckouts(options.checkouts ?? {}); + for (let [nameWithOwner, dirpath] of Object.entries(checkouts)) { + console.log(`${nameWithOwner} -> ${dirpath}`); + } + yield* $(`rm -rf ${path}`); yield* $(`mkdir -p ${path}`); @@ -28,6 +49,11 @@ export function* initClones(path: string): Operation { // scope and every other checkout with it, so failures come back as a Result // and the entry is evicted to allow a retry. yield* Clones.set(function* (nameWithOwner) { + let checkout = checkouts[nameWithOwner]; + if (checkout) { + return checkout; + } + let attempt = attempts.get(nameWithOwner); if (!attempt) { attempt = scope.run(() => cloneOrRefresh(path, nameWithOwner)); @@ -47,6 +73,22 @@ export function* useClone(nameWithOwner: string): Operation { return yield* checkout(nameWithOwner); } +export function resolveCheckouts( + checkouts: Record, +): Record { + return Object.fromEntries( + Object.entries(checkouts).map(([nameWithOwner, path]) => { + let dirpath = resolve(path); + if (!existsSync(dirpath)) { + throw new Error( + `cannot use ${dirpath} as a local checkout of ${nameWithOwner}: no such directory`, + ); + } + return [nameWithOwner, dirpath]; + }), + ); +} + function* cloneOrRefresh( basepath: string, nameWithOwner: string, diff --git a/www/main.tsx b/www/main.tsx index 1d8fdcf38..b1f846a0f 100644 --- a/www/main.tsx +++ b/www/main.tsx @@ -66,7 +66,9 @@ function* serve(options: Options) { // Get stable series (no prereleases) for guides let stableSeries = series.filter((s) => !s.includePrerelease); - yield* initClones(options.clonesDir); + yield* initClones(options.clonesDir, { + checkouts: localCheckouts(options.effectionxDir), + }); yield* initWorktrees(options.worktreesDir); yield* initGuides({ current, @@ -152,6 +154,18 @@ function* serve(options: Options) { yield* suspend(); } +/** + * Checkouts to use instead of cloning from GitHub, so that a repository you + * are working in shows up on the site: + * + * ``` + * EFFECTIONX_DIR=../effectionx deno task dev + * ``` + */ +function localCheckouts(effectionx: string): Record { + return effectionx ? { "thefrontside/effectionx": effectionx } : {}; +} + function urlFromServer(server: ServerInfo) { return new URL( "/",