Split The code is public; the app answers only to the crew.

One bundle, built at start-up: stop committing static/app.*.js so a merge cannot serve last week's game #26

shipped merged opened this has it
opened this

Build the client when the server starts and take the built file out of git, because the check is red on main right now and production is serving a bundle that predates the last two merges.

What is wrong, measured on 3b044c4 (= main, = this branch):

  • git ls-files static/ lists two bundles, app.942a1ec39a.js and app.d616a602f9.js, and bun test fails: the bundle in static/ is the one the sources produce — Expected to contain: "app.9585cc6287.js". Neither committed file is what the sources produce.
  • findScript() (src/server.ts:113) takes the first app.*.js that readdirSync returns, so which stale bundle a browser gets is filesystem order.
  • How it happened: #25 built app.942a1ec39a.js; #24 branched before that and built app.d616a602f9.js. The two merges do not conflict textually, so both files survived and neither matches the merged sources. 343dfe4 had to sweep up three of them the same way. This is structural: every pull touching src/client/ will do it again.

Done looks like:

  • [ ] static/app.*.js is no longer tracked by git, and .gitignore covers it for anyone who still writes one locally.
  • [ ] src/server.ts builds the client once at start-up through the existing bundle(), keeps the bytes and the content-hash name in memory, and answers GET /static/app.<hash>.js with content-type: text/javascript; charset=utf-8, cache-control: public, max-age=31536000, immutable and the whole SECURITY floor.
  • [ ] The page's <script src> is that name, so a code change is a new URL and a stale cache is impossible.
  • [ ] A build that fails at start-up is loud — it logs and exits non-zero rather than booting a page with no game — and the port opens only once the bundle exists, so a health check never sees a half-built app.
  • [ ] Real files under static/ are still served exactly as they are now, path still confined to the directory.
  • [ ] bun test passes on a clean clone, and a test covers both halves: the script path the page renders is the one bundle() produces, and a GET of it answers 200 with the immutable cache header and the security headers.
  • [ ] bun run build still writes a file for anyone who wants one; the README says the bundle is built at start-up and is not committed.
  • [ ] The pull quotes the measured boot cost (how many ms bundle() takes).

Files: src/server.ts, src/server.test.ts, scripts/build.ts, .gitignore, static/ (deletions), README.md, maybe src/pages/game.ts.

Risks: start-up now does real work — measure it. Having src/ import from scripts/ is the wrong way round; moving bundle() into src/bundle.ts and leaving scripts/build.ts as the thin CLI keeps modules named for one concept. Do not widen the CSP — the script is still same-origin. Do not reach for a bundler dependency; Bun.build is already doing this.

Why it goes first: while the artifact is in git, two client pulls merging in sequence reproduce exactly the breakage that is on main now. Everything else on this map waits for it.

Suggested: scout.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs