import { createHash } from "node:crypto"; import { copyFile, mkdir } from "node:fs/promises"; import path from "node:path"; import { manifest } from "../src/pwa/manifest"; export async function buildPwa(outdir: string, outputs: readonly { path: string }[]) { const directory = path.join(outdir, "pwa"); await mkdir(directory, { recursive: true }); for (const icon of manifest.icons) await copyFile(`src/pwa/public/${path.basename(icon.src)}`, path.join(directory, path.basename(icon.src))); await Bun.write(path.join(outdir, "manifest.webmanifest"), JSON.stringify(manifest, null, 2)); // Only browser assets emitted by Bun; index.js is the private server bundle. const files = outputs.map(output => path.relative(outdir, output.path)).filter(file => file !== "index.js" && /\.(js|css|png|svg|woff2|webmanifest)$/.test(file)); files.push(...manifest.icons.map(icon => icon.src.slice(1)), "manifest.webmanifest"); for await (const file of new Bun.Glob("fonts/browser/*.woff2").scan(outdir)) files.push(file); files.sort(); const template = await Bun.file("src/pwa/sw.js").text(); const html = await Bun.file(path.join(outdir, "index.html")).text(); const hash = createHash("sha256").update(template).update(html); for (const file of files) hash.update(file).update(await Bun.file(path.join(outdir, file)).arrayBuffer().then(Buffer.from)); const version = hash.digest("hex").slice(0, 16); const shell = `/pwa/shell-${version}.html`; await Bun.write(path.join(outdir, shell.slice(1)), html); const config = { version, shell, assets: [...files.map(file => `/${file}`), shell] }; await Bun.write(path.join(directory, "sw.js"), template.replace('"__PWA_CONFIG__"', JSON.stringify(JSON.stringify(config)))); console.log(` PWA ${version}: ${config.assets.length} public assets`); }