syntaxai/tdd.md · main · src / c14_client_bundle.ts

c14_client_bundle.ts 73 lines · 2681 bytes raw
// 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));