oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/build-bundles.mjs — run by a person, output committed.
2// The app performs no build step (spec §6.5); this produces the
3// artifact that makes that true: a single, minified, content-hashed
4// JS bundle committed to internal/docs/static/, embedded into the Go
5// binary by internal/docs/assets.go's //go:embed static.
6//
7// internal/docs/static/, not a top-level static/: Go's //go:embed
8// cannot reach outside (or above) the directory of the .go file doing
9// the embedding — no ".." is allowed in an embed pattern — so the
10// tree assets.go embeds has to live under internal/docs/, the same
11// way internal/docs/views/*.html already does for this app's page
12// templates.
13//
14// Paths below are resolved from this file's own location
15// (fixtures/generate.mjs's own pattern), not from the caller's
16// working directory, so `node build-bundles.mjs` works the same
17// whether it's run from fixtures/ or anywhere else.
18import { build } from 'esbuild'
19import { createHash } from 'node:crypto'
20import { readFileSync, writeFileSync, rmSync, readdirSync, mkdirSync } from 'node:fs'
21import { join, dirname } from 'node:path'
22import { fileURLToPath } from 'node:url'
23
24const here = dirname(fileURLToPath(import.meta.url))
25const staticDir = join(here, '..', 'internal', 'docs', 'static')
26
27mkdirSync(staticDir, { recursive: true })
28
29// Two bundles, built and hashed the same way but kept separate on
30// purpose. The editor is ProseMirror and Yjs — about 300KB — and only
31// a document page needs it. The jump-to palette is a few KB and every
32// signed-in page needs it, including the list. Bundling them together
33// would make opening the list download an editor in order to offer a
34// keyboard shortcut.
35//
36// The names here are the names internal/docs/assets.go's bundleKinds
37// expects; that file panics at init if one of them has no committed
38// file, so a build that silently stopped emitting one cannot start.
39const bundles = [
40 { name: 'editor', entry: 'editor-entry.js' },
41 { name: 'jumpto', entry: 'jump-to.mjs' },
42 // The share page's Copy button: a few hundred bytes, and only that
43 // page needs it.
44 { name: 'share', entry: 'share-link.mjs' },
45]
46
47for (const { name, entry } of bundles) {
48 const outfile = join(staticDir, `${name}.js`)
49 await build({
50 entryPoints: [join(here, entry)],
51 bundle: true,
52 format: 'esm',
53 minify: true,
54 target: 'es2020',
55 outfile
56 })
57
58 // Content hash in the filename so the asset is immutable-cacheable
59 // and a stale browser cache cannot serve a bundle that disagrees
60 // with the server (internal/docs/assets.go's own doc comment).
61 const bytes = readFileSync(outfile)
62 const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16)
63
64 // Exactly one file per bundle must ever be committed — assets.go
65 // panics at init if it finds more or less than one — so a rebuild
66 // must remove every previous hashed file for THIS name before
67 // writing the new one. The regex is anchored to the name so
68 // rebuilding one bundle never deletes the other's.
69 const stale = new RegExp(`^${name}\\.[0-9a-f]{16}\\.js$`)
70 for (const f of readdirSync(staticDir)) {
71 if (stale.test(f)) rmSync(join(staticDir, f))
72 }
73 writeFileSync(join(staticDir, `${name}.${hash}.js`), bytes)
74 rmSync(outfile)
75
76 console.log(`wrote internal/docs/static/${name}.${hash}.js (${bytes.length} bytes)`)
77}
78