syntaxai/tdd.md · main · src / c14_client_bundle.ts
// c14 — secondary I/O: in-process Bun.build bundler for the admin
// client-side TS. Memoised so a route handler can call bundleAdminClient()
// on every request without repeating the build.
//
// SAMA placement: c14 (not c13) because Bun.build IS external-process
// I/O — it spawns transformers, reads/writes intermediate buffers. Not
// the same flavour as the c14_github/c14_forgejo HTTP clients but the
// same architectural concern: I/O against a non-SQLite subsystem.
//
// Knobs:
// TDD_DEV=1 — force a rebuild on every call (handy in `bun --hot`).
import { join, dirname, resolve } from "node:path";
const isDev = process.env.TDD_DEV === "1";
let cached: { code: string; etag: string } | null = null;
let inFlight: Promise<{ code: string; etag: string }> | null = null;
const ENTRYPOINT = "./src/client/blockeditor.ts";
const buildBundle = async (): Promise<{ code: string; etag: string }> => {
const result = await Bun.build({
entrypoints: [ENTRYPOINT],
target: "browser",
format: "esm",
minify: false,
// We don't ship sourcemaps yet — the file is small enough to read
// directly when something goes wrong.
});
if (!result.success) {
const msgs = result.logs.map((l) => l.message).join("; ");
throw new Error(`admin client bundle failed: ${msgs}`);
}
const first = result.outputs[0];
if (!first) throw new Error("admin client bundle produced no output");
const code = await first.text();
// Cheap content-derived etag — Bun.CryptoHasher matches the pattern in
// c13_database.hashDoc.
const h = new Bun.CryptoHasher("sha1");
h.update(code);
const etag = `"${h.digest("hex").slice(0, 16)}"`;
return { code, etag };
};
export const bundleAdminClient = async (): Promise<{ code: string; etag: string }> => {
if (!isDev && cached) return cached;
// Coalesce concurrent callers so we don't run two builds in parallel
// (Bun.build is not free; the first request after boot triggers it).
if (inFlight) return inFlight;
inFlight = (async () => {
try {
const built = await buildBundle();
cached = built;
return built;
} finally {
inFlight = null;
}
})();
return inFlight;
};
// Drop the cache. Wired to nothing yet — useful as an internal endpoint
// later if we add a /admin/reload-bundle hook for the dev loop.
export const _resetAdminClientCache = (): void => {
cached = null;
};
// Resolve the entry-point path relative to the repo root so callers can
// verify the file exists. Kept here so c14 owns the on-disk pathing.
export const adminClientEntrypoint = (): string =>
resolve(join(dirname(new URL(import.meta.url).pathname), "..", ENTRYPOINT));