/** * Production build (`bun run build`) — Decisions.md D-007. * * Emits a self-contained `dist/`: * 1. `bun build src/index.html` → hashed app-shell assets * 2. inference + translate worker bundles at stable /inference.worker.js and * /translate.worker.js URLs * 3. manifest + icons + onnxruntime .wasm/.mjs (dist/ort/, offline inference) * 4. sw.js with the precache manifest + a content-hash version injected * * `bun run start` (NODE_ENV=production bun src/index.ts) then serves dist/ * verbatim, so the URLs the service worker precached are exactly the URLs * the server responds to. */ import { cp, readdir, rm } from "node:fs/promises"; import { join, relative } from "node:path"; import { injectPrecacheManifest, precachePathsFromOutputs } from "../src/pwa/precache.ts"; import { ensureFonts, FONTS_DIR } from "./fonts.ts"; import { ensureIcons, ICON_SIZES, ICONS_DIR } from "./generate-icons.ts"; const ROOT = join(import.meta.dir, ".."); const DIST = join(ROOT, "dist"); await rm(DIST, { recursive: true, force: true }); // Icons before the shell build — index.html references src/pwa/icons/ (the // HTML loader resolves the file at bundle time). They are checked in, but // regenerate any that are missing. await ensureIcons(ICONS_DIR); // Self-hosted Inter must exist for index.css to resolve (scripts/fonts.ts). const fontFiles = await ensureFonts(); // 1. App shell (hashed assets). const shellBuild = await Bun.build({ entrypoints: [join(ROOT, "src/index.html")], outdir: DIST, target: "browser", minify: true, sourcemap: "linked", define: { "process.env.NODE_ENV": '"production"' }, env: "BUN_PUBLIC_*", // Self-hosted fonts stay as plain url(fonts/...) files — see scripts/fonts.ts. external: ["*.woff2"], }); // 2. Inference worker at its stable URL (Bun does not follow `new Worker` // graphs — same mechanism as the dev server in src/index.ts). const workerBuild = await Bun.build({ entrypoints: [join(ROOT, "src/worker/inference.worker.ts")], target: "browser", format: "esm", minify: true, }); const workerScript = await workerBuild.outputs[0]!.text(); await Bun.write(join(DIST, "inference.worker.js"), workerScript); // Transcript-translation worker (D-WS4b), same mechanism. const translateWorkerBuild = await Bun.build({ entrypoints: [join(ROOT, "src/worker/translate.worker.ts")], target: "browser", format: "esm", minify: true, }); const translateWorkerScript = await translateWorkerBuild.outputs[0]!.text(); await Bun.write(join(DIST, "translate.worker.js"), translateWorkerScript); // 3. Static PWA assets + onnxruntime wasm binaries (offline inference — the // worker loads them from /ort/ instead of the jsdelivr CDN). The manifest and // icons are also emitted as hashed shell assets (index.html links them), but // the manifest's internal icon URLs point at these fixed /icons/ paths. await cp(join(ROOT, "src/pwa/manifest.webmanifest"), join(DIST, "manifest.webmanifest")); for (const size of ICON_SIZES) { await cp(join(ICONS_DIR, `icon-${size}.png`), join(DIST, `icons/icon-${size}.png`)); } const ortDist = join(ROOT, "node_modules/onnxruntime-web/dist"); const ortFiles = (await readdir(ortDist)).filter((name) => /^ort-wasm-simd-threaded(\.jsep|\.jspi|\.asyncify)?\.(wasm|mjs)$/.test(name), ); for (const name of ortFiles) { await cp(join(ortDist, name), join(DIST, "ort", name)); } for (const name of fontFiles) { await cp(join(FONTS_DIR, name), join(DIST, "fonts", name)); } // 4. Service worker with the injected precache manifest (D-007). Icons, // manifest, the worker, and fonts are unhashed, so the cache version hashes // their contents (the hashed shell assets are covered by their names). const swBuild = await Bun.build({ entrypoints: [join(ROOT, "src/pwa/sw.ts")], target: "browser", format: "esm", // Placeholder string literals must survive verbatim (src/pwa/precache.ts). minify: false, }); const swSource = await swBuild.outputs[0]!.text(); const precachePaths = precachePathsFromOutputs( shellBuild.outputs.map((output) => relative(DIST, output.path)), [ "inference.worker.js", "translate.worker.js", "manifest.webmanifest", ...ICON_SIZES.map((size) => `icons/icon-${size}.png`), ...fontFiles.map((name) => `fonts/${name}`), ], ); const fontBytes = await Promise.all( fontFiles.map((name) => Bun.file(join(FONTS_DIR, name)).bytes()), ); const version = Bun.hash( JSON.stringify(precachePaths) + swSource + workerScript + translateWorkerScript + fontBytes.map((b) => Bun.hash(b)).join(), ).toString(36); await Bun.write(join(DIST, "sw.js"), injectPrecacheManifest(swSource, precachePaths, version)); console.log(`Built dist/ (cache version ${version}); precached app shell:`); for (const path of precachePaths) console.log(` ${path}`); console.log(` + ${ortFiles.length} onnxruntime files in /ort/ (runtime-cached)`);