| 1 | // The editor bundle (spec §3.1, §8: the one surface that needs |
| 2 | // scripts) is a committed, prebuilt artifact — never built by this app |
| 3 | // or by CI — produced by fixtures/build-bundles.mjs and committed under |
| 4 | // internal/slides/static/ as editor.<hash>.js. Docs' assets.go |
| 5 | // pattern, adopted: the bundle's name is resolved once at package init |
| 6 | // and the process refuses to start without it. |
| 7 | // |
| 8 | // It lives in static/ beside the vendored stylesheets rather than in a |
| 9 | // tree of its own because render.go's assets already embeds and |
| 10 | // fingerprints everything under static/. The bundle carries a content |
| 11 | // hash in its own name (so a build that changed the bytes changes the |
| 12 | // URL) and assets.Path adds a second, which is harmless: the asset |
| 13 | // handler strips one hash to find the file and serves it immutable |
| 14 | // when the hash it stripped matches the content, which it always does. |
| 15 | package slides |
| 16 | |
| 17 | import ( |
| 18 | "fmt" |
| 19 | "io/fs" |
| 20 | "strings" |
| 21 | ) |
| 22 | |
| 23 | // bundleKinds is every bundle that must exist. Listed rather than |
| 24 | // discovered from the directory: a build that produced no bundle at all |
| 25 | // would otherwise start cleanly and serve an editor page whose script |
| 26 | // tag 404s, which is the same silent-failure shape the exactly-one rule |
| 27 | // below exists to prevent. One today; Docs has two. |
| 28 | var bundleKinds = []string{"editor"} |
| 29 | |
| 30 | // bundleNames maps each committed bundle's kind to its filename under |
| 31 | // static/ — "editor" to "editor.f13f5e0ce6fa7467.js" — resolved once, |
| 32 | // at package init, from the embedded tree. |
| 33 | // |
| 34 | // Exactly one file must ever be present per kind. |
| 35 | // fixtures/build-bundles.mjs deletes every previous "<kind>.<hash>.js" |
| 36 | // before writing the new one, so a tree with two (a script edited to |
| 37 | // skip that step, a hand-added file, a bad merge) is a mistake — this |
| 38 | // panics at init rather than silently picking one, because a wrong |
| 39 | // bundle silently served is indistinguishable from a correct one until |
| 40 | // it disagrees with the server at runtime. |
| 41 | var bundleNames = mustResolveBundleNames() |
| 42 | |
| 43 | // isBundleFile reports whether name is the shape build-bundles.mjs |
| 44 | // writes: the kind, a dot, sixteen hex digits, ".js". |
| 45 | func isBundleFile(kind, name string) bool { |
| 46 | rest, ok := strings.CutPrefix(name, kind+".") |
| 47 | if !ok { |
| 48 | return false |
| 49 | } |
| 50 | h, ok := strings.CutSuffix(rest, ".js") |
| 51 | if !ok || len(h) != 16 { |
| 52 | return false |
| 53 | } |
| 54 | for _, c := range h { |
| 55 | if (c < '0' || c > '9') && (c < 'a' || c > 'f') { |
| 56 | return false |
| 57 | } |
| 58 | } |
| 59 | return true |
| 60 | } |
| 61 | |
| 62 | func mustResolveBundleNames() map[string]string { |
| 63 | entries, err := fs.ReadDir(appFS, "static") |
| 64 | if err != nil { |
| 65 | panic(fmt.Sprintf("slides: read embedded static/: %v", err)) |
| 66 | } |
| 67 | out := make(map[string]string, len(bundleKinds)) |
| 68 | for _, kind := range bundleKinds { |
| 69 | var found []string |
| 70 | for _, e := range entries { |
| 71 | if !e.IsDir() && isBundleFile(kind, e.Name()) { |
| 72 | found = append(found, e.Name()) |
| 73 | } |
| 74 | } |
| 75 | if len(found) != 1 { |
| 76 | panic(fmt.Sprintf("slides: want exactly one %q bundle in internal/slides/static/, found %v — run `node fixtures/build-bundles.mjs` and commit its output", kind, found)) |
| 77 | } |
| 78 | out[kind] = found[0] |
| 79 | } |
| 80 | return out |
| 81 | } |
| 82 | |
| 83 | // bundlePath is the URL a committed bundle is served at, through the |
| 84 | // same fingerprinting handler every stylesheet goes through. The |
| 85 | // template's <script> tag uses this rather than a literal, so it can |
| 86 | // never drift from what the handler actually serves. |
| 87 | func bundlePath(kind string) string { |
| 88 | name, ok := bundleNames[kind] |
| 89 | if !ok { |
| 90 | // Unreachable while every caller passes a member of |
| 91 | // bundleKinds, which init has already proved resolvable. |
| 92 | panic(fmt.Sprintf("slides: no committed bundle named %q", kind)) |
| 93 | } |
| 94 | return assets.Path("static/" + name) |
| 95 | } |
| 96 | |
| 97 | // editorBundlePath is the editor's bundle — templates/editor.html's |
| 98 | // <script type="module"> tag. |
| 99 | func editorBundlePath() string { return bundlePath("editor") } |
| 100 | |