// 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));