Files
whisper_web/scripts/build.ts
T
Lucas WintherandClaude Fable 5 9f39a82982 WIP: M3 PWA/offline implementation, M4 auth (backend + frontend), D-013 faster-whisper
- M3: hand-rolled service worker (precache + runtime /ort caching), precache
  manifest injection, SW registration/update toast, manifest + generated icons,
  production build script, offline indicator in ModelPicker (D-005/D-007)
- M4 backend: bun:sqlite users/sessions (D-009), Passport local strategy behind
  connect-compat adapter (D-008), /api/auth routes wired into dev+prod serving,
  Bun.password Argon2id, 38 auth tests
- M4 frontend: AuthForms + useAuth + framework-free authClient, session-aware
  header, anonymous-first fail-silent offline handling, 18 client tests
- D-013: server engine (M5) will use faster-whisper via Python sidecar;
  browser side stays Transformers.js (CTranslate2 has no WASM build)
- bun test: 83 pass / tsc --noEmit clean
- Note: M3 doc closeout (WorkBreakdown/Architecture/tests/offline.md) still in
  flight from a background agent; follow-up commit will finish docs

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-10 04:02:28 +02:00

95 lines
3.7 KiB
TypeScript

/**
* 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 worker bundle at the stable /inference.worker.js URL
* 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";
import { ensureIcons, ICON_SIZES, ICONS_DIR } from "./generate-icons";
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) and they aren't checked in.
await ensureIcons(ICONS_DIR);
// 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_*",
});
// 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);
// 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));
}
// 4. Service worker with the injected precache manifest (D-007). Icons,
// manifest, and the worker 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",
"/manifest.webmanifest",
...ICON_SIZES.map((size) => `/icons/icon-${size}.png`),
],
);
const version = Bun.hash(
JSON.stringify(precachePaths) + swSource + workerScript,
).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)`);