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");
};