// fixtures/build-bundles.mjs โ€” run by a person, output committed. // Docs' script, adopted (oficina/docs/fixtures/build-bundles.mjs). // The app performs no build step (spec ยง3.1: "one esbuild bundle // committed hashed"); this produces the artifact that makes that true: // a single, minified, content-hashed JS bundle committed to // internal/slides/static/, embedded into the Go binary by render.go's // //go:embed and resolved at init by internal/slides/assets_embed.go. // // internal/slides/static/, not a top-level static/: Go's //go:embed // cannot reach outside the directory of the .go file doing the // embedding, so the bundle lives beside the stylesheets that tree // already holds. // // Paths are resolved from this file's own location, so // `node build-bundles.mjs` works the same from fixtures/ or anywhere. // The gate runs it twice and requires a clean tree: the output is a // pure function of the sources and the pinned toolchain. import { build } from 'esbuild' import { createHash } from 'node:crypto' import { readFileSync, writeFileSync, rmSync, readdirSync, mkdirSync } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const here = dirname(fileURLToPath(import.meta.url)) const staticDir = join(here, '..', 'internal', 'slides', 'static') mkdirSync(staticDir, { recursive: true }) // One bundle. The names here are the names assets_embed.go's // bundleKinds expects; that file panics at init if one of them has no // committed file, so a build that silently stopped emitting one cannot // start. const bundles = [ { name: 'editor', entry: 'editor-entry.js' } ] for (const { name, entry } of bundles) { const outfile = join(staticDir, `${name}.js`) await build({ entryPoints: [join(here, entry)], bundle: true, format: 'esm', minify: true, target: 'es2020', outfile }) // Content hash in the filename so the asset is immutable-cacheable // and a stale browser cache cannot serve a bundle that disagrees // with the server. const bytes = readFileSync(outfile) const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16) // Exactly one file per bundle must ever be committed โ€” assets_embed.go // panics at init if it finds more or less than one โ€” so a rebuild // removes every previous hashed file for THIS name before writing // the new one. Anchored to the name so a second bundle, if one ever // arrives, is never deleted by the first's rebuild. const stale = new RegExp(`^${name}\\.[0-9a-f]{16}\\.js$`) for (const f of readdirSync(staticDir)) { if (stale.test(f)) rmSync(join(staticDir, f)) } writeFileSync(join(staticDir, `${name}.${hash}.js`), bytes) rmSync(outfile) console.log(`wrote internal/slides/static/${name}.${hash}.js (${bytes.length} bytes)`) }