Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 2d870074b807ee717c0502ac5f636674ac42972a
parent b568edb817b2a4c2fa658dd624cb0786cbb5dda8
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 01:38:11 -0400

common: builtHomepageProblem + builtHomepageAt beside builtHubProblem, and homepageOutDir — homepage/out, the one path the editor's homepage actions judge and deployHomepage ships

builtHomepageProblem(outDir) refuses an out dir with no index.html, the file
deployHomepage checks inside its job; builtHomepageAt is that file's mtime,
for "built <when>" on /sites. homepageOutDir(paths) is homepage/out of the
checkout, exported so the editor does not re-derive it. buildHomepage and
deployHomepage are unchanged. +3 unit tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/lib/builtExport.test.ts | 51+++++++++++++++++++++++++++++++++++++++++++++++++--
Mcommon/lib/builtExport.ts | 32+++++++++++++++++++++++++++++++-
Mcommon/publish/build.test.ts | 7+++++++
Mcommon/publish/build.ts | 10++++++++++
4 files changed, 97 insertions(+), 3 deletions(-)

diff --git a/common/lib/builtExport.test.ts b/common/lib/builtExport.test.ts @@ -1,9 +1,15 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, utimesSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { builtHubProblem, builtSiteIdIn, builtSiteProblem } from "./builtExport"; +import { + builtHomepageAt, + builtHomepageProblem, + builtHubProblem, + builtSiteIdIn, + builtSiteProblem, +} from "./builtExport"; function tempOut(siteJson?: string): { dir: string; cleanup: () => void } { const dir = mkdtempSync(path.join(tmpdir(), "built-export-")); @@ -113,3 +119,44 @@ test("builtHubProblem accepts only a hub bundle", () => { none.cleanup(); } }); + +// The homepage package builds into its own homepage/out, which nothing else +// writes, so "built" is one question: is there an index.html? The deploy-only +// action refuses before any job on exactly the file deployHomepage checks +// inside it. +test("builtHomepageProblem refuses an out dir with no index.html, and only that", () => { + const built = tempOut(); + const empty = tempOut(); + try { + writeFileSync(path.join(built.dir, "index.html"), "<!doctype html>"); + assert.equal(builtHomepageProblem(built.dir), null); + + const problem = "homepage/out holds no build — build the homepage first"; + assert.equal(builtHomepageProblem(empty.dir), problem); + // No directory at all is the same answer, not a throw. + assert.equal(builtHomepageProblem(path.join(tmpdir(), "no-such-homepage-out-ever")), problem); + // A stray file that is not the page is still no build. + writeFileSync(path.join(empty.dir, "404.html"), "<!doctype html>"); + assert.equal(builtHomepageProblem(empty.dir), problem); + } finally { + built.cleanup(); + empty.cleanup(); + } +}); + +test("builtHomepageAt is index.html's mtime, and null with no build", () => { + const built = tempOut(); + const empty = tempOut(); + try { + const index = path.join(built.dir, "index.html"); + writeFileSync(index, "<!doctype html>"); + const when = new Date("2026-09-28T00:53:00Z"); + utimesSync(index, when, when); + assert.equal(builtHomepageAt(built.dir), when.getTime()); + assert.equal(builtHomepageAt(empty.dir), null); + assert.equal(builtHomepageAt(path.join(tmpdir(), "no-such-homepage-out-ever")), null); + } finally { + built.cleanup(); + empty.cleanup(); + } +}); diff --git a/common/lib/builtExport.ts b/common/lib/builtExport.ts @@ -13,7 +13,7 @@ // public/ into out/. So the check is a file read, and it is cheap enough to do // before every deploy. -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; /** @@ -80,3 +80,33 @@ export function builtHubProblem(outDir: string): string | null { } return null; } + +/** + * Why `outDir` — the homepage package's `homepage/out` — may not be deployed as + * the homepage, as one sentence, or null when it holds a build. + * + * Unlike export/out nothing else ever builds into homepage/out, so the only + * question is whether a build is there at all. `index.html` is the file + * deployHomepage checks inside its job, so the refusal an operator gets before + * the job and the one the job would give agree on what "built" means. + */ +export function builtHomepageProblem(outDir: string): string | null { + if (!existsSync(path.join(outDir, "index.html"))) { + return "homepage/out holds no build — build the homepage first"; + } + return null; +} + +/** + * When the homepage build in `outDir` was made (its `index.html`'s mtime, in + * ms), or null when builtHomepageProblem would refuse it. What `/sites` shows + * as "built <when>" beside Deploy homepage, so the operator sees how old the + * bundle a deploy-only would ship is. + */ +export function builtHomepageAt(outDir: string): number | null { + try { + return statSync(path.join(outDir, "index.html")).mtimeMs; + } catch { + return null; + } +} diff --git a/common/publish/build.test.ts b/common/publish/build.test.ts @@ -7,6 +7,7 @@ import { buildSiteSteps, hubProjectProblem, homepageDeployArgs, + homepageOutDir, deployHomepage, dockerSiteOutDir, dockerSiteStagingDir, @@ -136,6 +137,12 @@ test("homepageDeployArgs: production is branch main; a preview is its own branch assert.deepEqual(homepageDeployArgs("/repo/homepage/out", " r8-home "), [...base, "--branch", "r8-home"]); }); +// The editor's homepage actions judge "is there a build?" on this path before +// any job, so it must be the directory deployHomepage ships. +test("homepageOutDir is homepage/out of the checkout", () => { + assert.equal(homepageOutDir({ monorepoRoot: "/repo" } as Paths), "/repo/homepage/out"); +}); + test("deployHomepage refuses a bad preview branch before it looks for a build", async () => { const noBuild = { monorepoRoot: "/nonexistent-repo" } as Paths; await assert.rejects( diff --git a/common/publish/build.ts b/common/publish/build.ts @@ -939,6 +939,16 @@ function homepageDir(paths: Paths): string { return path.join(paths.monorepoRoot, "homepage"); } +/** + * Where buildHomepage writes and deployHomepage ships: `homepage/out` of this + * checkout. Exported for the editor's homepage actions, which refuse a + * deploy-only before any job when nothing is built there + * (builtHomepageProblem) and show when it was built. + */ +export function homepageOutDir(paths: Paths): string { + return path.join(homepageDir(paths), "out"); +} + function homepageEnv(paths: Paths): NodeJS.ProcessEnv { return { ...process.env,