// fixtures/build-bundles.mjs — run by a person, output committed. // The app performs no build step (spec §6.5); this produces the // artifact that makes that true: a single, minified, content-hashed // JS bundle committed to internal/docs/static/, embedded into the Go // binary by internal/docs/assets.go's //go:embed static. // // internal/docs/static/, not a top-level static/: Go's //go:embed // cannot reach outside (or above) the directory of the .go file doing // the embedding — no ".." is allowed in an embed pattern — so the // tree assets.go embeds has to live under internal/docs/, the same // way internal/docs/views/*.html already does for this app's page // templates. // // Paths below are resolved from this file's own location // (fixtures/generate.mjs's own pattern), not from the caller's // working directory, so `node build-bundles.mjs` works the same // whether it's run from fixtures/ or anywhere else. 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', 'docs', 'static') mkdirSync(staticDir, { recursive: true }) // Two bundles, built and hashed the same way but kept separate on // purpose. The editor is ProseMirror and Yjs — about 300KB — and only // a document page needs it. The jump-to palette is a few KB and every // signed-in page needs it, including the list. Bundling them together // would make opening the list download an editor in order to offer a // keyboard shortcut. // // The names here are the names internal/docs/assets.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' }, { name: 'jumpto', entry: 'jump-to.mjs' }, // The share page's Copy button: a few hundred bytes, and only that // page needs it. { name: 'share', entry: 'share-link.mjs' }, ] 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 (internal/docs/assets.go's own doc comment). 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.go // panics at init if it finds more or less than one — so a rebuild // must remove every previous hashed file for THIS name before // writing the new one. The regex is anchored to the name so // rebuilding one bundle never deletes the other's. 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/docs/static/${name}.${hash}.js (${bytes.length} bytes)`) }