From f807cc8c258c464696f2f451b9972a511c8ed004 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 4 Nov 2026 15:22:14 -0800 Subject: [PATCH] fix(hub): fail the design check when a design file cannot be read --- apps/hub/src/builds/design-check.test.ts | 30 ++++++++++++++++++++++++ apps/hub/src/builds/design-check.ts | 23 ++++++++++++------ 2 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 apps/hub/src/builds/design-check.test.ts diff --git a/apps/hub/src/builds/design-check.test.ts b/apps/hub/src/builds/design-check.test.ts new file mode 100644 index 00000000..2f294b0f --- /dev/null +++ b/apps/hub/src/builds/design-check.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "bun:test"; +import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createDesignCheck, DESIGNS } from "./design-check.ts"; + +test("a design file that cannot be read fails the check instead of being left out of what it saw", async () => { + const workspace = await mkdtemp(join(tmpdir(), "sb-design-check-")); + const screen = join(workspace, DESIGNS, "screen.html"); + let runs = 0; + const check = createDesignCheck(workspace, async () => { + runs += 1; + return { exitCode: 0, output: "check passed" }; + }); + try { + await mkdir(join(workspace, DESIGNS), { recursive: true }); + await writeFile(screen, "
\n"); + await chmod(screen, 0o000); + await expect( + check.current(new AbortController().signal), + ).rejects.toMatchObject({ + code: "EACCES", + }); + expect(runs).toBe(0); + } finally { + check.dispose(); + await chmod(screen, 0o644); + await rm(workspace, { recursive: true, force: true }); + } +}); diff --git a/apps/hub/src/builds/design-check.ts b/apps/hub/src/builds/design-check.ts index 21a27e8a..4bc5ae00 100644 --- a/apps/hub/src/builds/design-check.ts +++ b/apps/hub/src/builds/design-check.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { readFile, realpath } from "node:fs/promises"; import { join, relative, resolve, sep } from "node:path"; +import { hasCode } from "@intx/types"; import { withDesignTokens } from "../artifacts/design-tokens.ts"; import { DESIGN_STYLES } from "../artifacts/design-tweaks.ts"; @@ -62,10 +63,14 @@ const DESIGN_INDEX_PATH = join(".builder", "designs", "index.html"); const PLACEHOLDER = /<[A-Za-z][^<>=`"]*\s[^<>=`"]*>/g; async function readTextIfPresent(path: string): Promise { + return (await readIfPresent(path))?.toString("utf8"); +} + +async function readIfPresent(path: string): Promise { try { - return await readFile(path, "utf8"); + return await readFile(path); } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; + if (hasCode(error) && error.code === "ENOENT") return undefined; throw error; } } @@ -140,16 +145,20 @@ export function createDesignCheck(workspace: string, run: Run) { const fingerprint = async () => { const hash = createHash("sha256"); const root = join(workspace, DESIGNS); - const paths = await Array.fromAsync( - new Bun.Glob("**/*").scan({ cwd: root }), - ).catch(() => [] as string[]); + let paths: string[]; + try { + paths = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: root })); + } catch (error) { + if (!(hasCode(error) && error.code === "ENOENT")) throw error; + paths = []; + } for (const path of paths.sort()) hash .update(`${path}\0`) - .update(await readFile(join(root, path)).catch(() => "")); + .update((await readIfPresent(join(root, path))) ?? ""); hash .update(`\0${DESIGN_DOC}\0`) - .update(await readFile(join(workspace, DESIGN_DOC)).catch(() => "")); + .update((await readIfPresent(join(workspace, DESIGN_DOC))) ?? ""); return hash.digest("hex"); };