Game Rig

Home/Blog/No build step

Why this site has no build step

The build is a copy, the tests run in Node, and the parts a toolchain would have written are four scripts run by hand.

Fifty-one HTML files, four scripts run by hand, and nothing at all between a commit and a visitor. package.json declares ten scripts and not one is called build, because there is nothing to build.

A toolchain offers JSX, CSS nesting, tree-shaking and filenames carrying content hashes. What settled it here was debugging: with a bundler in the path, an exception names a line in an artefact, and a source map is the only route back to the file that was written.

Here the exception names the page. Neon Snake is one HTML file with its logic inline, and the line number the browser reports is the line in the editor. Everything below is what that costs.

The deploy copies files and rewrites nothing

Vercel serves the repository root directly: no output directory, no build command. Vercel's own documentation covers the case under Skip Build Step, where a site of only HTML, CSS and JS source files is served as-is once the build command is left empty.

.vercelignore keeps the tooling out of the artefact, excluding scripts/, serve.js, package.json, package-lock.json, node_modules/, README.md and itself. What reaches the edge is the pages, assets/, the two catalogues and the sitemap.

The URL rules are two properties of vercel.json. cleanUrls strips the .html extension and answers a request still carrying one with a 308; trailingSlash sends a path without a trailing slash to the path with one. So blog/no-build-step/index.html answers at /blog/no-build-step/, with no rewrite in between.

Four scripts whose output is committed, three of which edit pages

Four scripts do the job a generator would do. Three write into HTML pages; the fourth writes the sitemap. All four are run by hand, and what they write is committed.

The four scripts whose output is committed, by size and by target
ScriptLinesWritesInto
build-nav.js221the header and the footer50 HTML pages
build-cards.js173sixteen inlined SVG game cardsthe landing page and the four category hubs
build-blog.js153the post list and the Blog JSON-LDblog/index.html
make-sitemap.js109sitemap.xml and robots.txtthe repository root

Running by hand is the risk. Adding a game to the catalogue leaves the landing page and the category hub wrong until npm run cards runs, and leaves the menu without the new link until npm run nav does. Three of the four take a --check flag that reports what they would write instead — the nearest thing here to continuous integration.

The odd one out is scripts/vendor-three.js, which does run esbuild once, on this machine, and commits the minified result into assets/vendor/. The bundler is present in the project; it is never in the deploy path.

No content hash in any asset filename

vercel.json gives /assets/(.*) a year of caching: public, max-age=31536000, immutable. MDN's wording for the last directive is that it tells a cache the response will not be updated while it is fresh, sparing the conditional request a browser would otherwise send on a reload.

That is half of a pattern. MDN's guidance is that a long max-age and immutable belong together with a URL carrying a version or a content hash: edit the file, change its address, and no cache holds the wrong one. The hash is the half this site cannot produce, because a hash is an output of a build. assets/style.css is 38,003 bytes and its name has never changed.

The bill arrives the first time that stylesheet is edited. Everyone who has already visited keeps the old copy for up to 31,536,000 seconds — 365 days, by arithmetic rather than by preference — and immutable removes the conditional request a reload would have made. Renaming it by hand means touching all fifty-one pages that link it.

Three suites in Node, and the game that refuses to be faked

Skipping a build is not skipping tests. npm run lint runs scripts/check-pages.js, 411 lines that read every page as text: a title under sixty characters, a description under a hundred and sixty, JSON-LD that parses, a table, a pros-and-cons block, 900 to 1600 words. Then the checks that need every page at once, including the one that fails on any sentence of eight words or more appearing twice. A phrase borrowed from another page is a test failure here, not a style note.

npm run smoke is the one to keep if only one could be kept. scripts/smoke-game.js builds the smallest browser a game can plausibly run in — a canvas context behind a Proxy that accepts any call, elements that record their text, AudioContext and localStorage stubs — lifts each game's inline script out of its page and runs it through vm, presses Enter, then plays 7,200 frames. Two minutes at 60 fps, keys held in twenty-four frame stretches, with separate passes for no audio and for touch. The reasoning behind that fake browser gets a piece of its own, Testing a game in a fake browser.

Every game being a single HTML file with its logic inline is what makes the harness possible, and the exception shows how narrow that is. Highway Rush 3D needs a real WebGL context and loads its logic as an ES module, so the page declares data-smoke="skip" and names the suite that covers it instead: npm run test:3d, 698 lines driving Chrome through puppeteer-core. A build step would have handed every game a module boundary, and this harness would not exist in its present shape.

What the unminified bytes weigh

Six files, measured off the disk, with a zlib pass over each so the raw figure is not mistaken for a transfer figure.

Six files as they sit on disk, raw and gzipped at level 9
FileBytes on diskGzippedLoaded by
index.html108,79622,734every visitor
games/puzzle/sudoku/index.html60,95117,243anyone who opens Sudoku
assets/style.css38,0038,953all fifty-one pages
assets/nav.js3,1691,211all fifty-one pages
assets/vendor/three.module.js376,20792,841Highway Rush 3D only
assets/vendor/three.core.js389,617104,786Highway Rush 3D only

The landing page is 108,796 bytes, most of it sixteen SVG game cards inlined by build-cards.js so they inherit the page's colours and cost no extra request. Nothing minifies it, and the same holds for 38,003 bytes of hand-written CSS.

Three.js is the heavy one: two files, 765,824 bytes together, for a single game. Nothing is shaken out of that tree and nothing can be without the build step this site is about not having. A fair trade for one game in sixteen; at five it would not be.

Where a build step would start to earn its keep

Fifty-one pages under one menu is the size at which a hand-run script is still cheaper than a plugin.

The first sign of trouble is a script learning a rule instead of applying one. build-nav.js works because the header is the same string at every depth with the relative prefix changed. The day the menu differs by anything more than depth, that file becomes a templating language somebody wrote badly.

The second sign is reach. build-nav.js covers all fifty-one pages, but only because each one sits in a list written by hand — a function, not a filesystem walk. A page missing from it is a page nothing will notice, and that page ships with last month's footer.

The dependency argument is the one this design narrows rather than answers. Three devDependencies unfold into twenty-six directories under node_modules, and none of them ships. On 9 September 2018 a new maintainer published event-stream@3.3.6 with a dependency, flatmap-stream@0.1.1, that existed only in the version on npm and never in the public repository. npm removed both on 26 November, seventy-eight days later, by which time the payload had shipped in Copay 5.0.2 through 5.1.0. Fewer packages narrows the surface; it does not close it, and the three declared here use caret ranges, the range that accepts a compromised patch release without anyone deciding to upgrade.

Pros and cons

Pros

  • An exception names the file that is on the server, so nothing sits between a bug report and the line that caused it.
  • The smoke harness can lift a game's script out of its page and run it in Node, with no module boundary to cross.
  • Three devDependencies, no runtime dependencies, and not one installed file uploaded with the deploy.
  • A new page is a folder with an index.html in it; nothing has to be registered or compiled for the URL to work.

Cons

  • No JSX, no CSS nesting, no hot reload and no tree-shaking — 765,824 bytes of vendored Three.js sit in the repository for one game.
  • The header is a hand-run script, so a game added to the catalogue is invisible in the menu until someone remembers npm run nav.
  • The asset rule most in need of a content hash has none, leaving a year of immutable caching parked on the name style.css.
  • A page the catalogue does not know about is a page the menu script never visits, and one redirect stub is already in that position.
  • Nothing checks that the hand-run scripts were run before a commit; the report only happens if somebody types the command.

Sources