Build Pipeline
Audience: Core contributors, AI agents · Prereqs: Shared Shell Layer, Server-Only Markers
Canonical source:
server-client/src/prepare-modified.js
How src/client-static/src/shellshock.min.js (the hand-maintained, not actually minified browser game source) becomes the real, served, minified bundle in store/client-modified/.
At a glance
Runs on every client server boot
prepareModified() is called as part of server-client's own startup (alongside stampsGenerator.js and the wiki build - see Architecture Overview), running two things in parallel:
await Promise.all([
prepareBabylons(path.join(ss.rootDir, 'server-client', 'store', 'client-modified', 'models')),
modifyFiles(),
]);
prepareBabylons handles model merging - see Stamps and Babylons. modifyFiles() is the actual JS/HTML build step this page covers.
What modifyFiles() processes
Five source files get read and transformed, written into store/client-modified/: src/client-static/src/shellshock.min.js (the game itself), src/client-static/src/servers.js (server-list template), src/client-static/editor/js/mapEdit.js and editor/index.html (the map editor), and the root src/index.html.
The two-pass token replacement
The build works by literal string substitution against LEGACYSHELLXXX-style placeholder tokens baked into the source files, applied in two separate passes:
Pass one (replacementsBefore, ~35 entries) happens first, and is what actually inlines src/shell/* shared modules - each entry maps a token to a #hashtag:
{ pattern: /LEGACYSHELLPLUGINMANAGER/g, file: "#plugins" },
{ pattern: /LEGACYSHELLPICKUPS/g, file: "#items" },
{ pattern: /LEGACYSHELLCOMM/g, file: "#comm" },
// ...
For a file: entry, the actual inserted content is misc.hashtagToString(hashtag) - the shared module's live source, transformed by prepareForClient (see Server-Only Markers). This pass is also where plugin-injected client code lands, via the pluginSourceInsertion emit (see Client-Side Code) resolving the LEGACYSHELLPLUGINSBEFOREBEFORE/LEGACYSHELLPLUGINSBEFORE/LEGACYSHELLPLUGINSAFTER tokens. replacementsBefore itself is also a documented plugin extension point - a listener can push its own entries onto the array before this pass runs.
Pass two (replacementsAfter) happens near the end, after minification - it inlines the final items/maps JSON (now annotated with stamp grid coordinates by stampsGenerator.js, hence waiting until after that's done - see Stamps and Babylons), plus the raw Babylon.js library source and its GLSL shaders.
IIFE wrapping
If config.client.iif is true, the whole assembled script gets wrapped in an immediately-invoked function expression - the config comment calls this a mitigation against "console crackers" (people poking at global variables via the browser console). It hides top-level const/let/function declarations from becoming actual window globals, though the source is explicit that this is not remotely foolproof - it raises the bar for casual tampering, nothing more.
Minification
If config.client.minify is true, the assembled (and possibly IIFE-wrapped) script is run through UglifyJS.minify(...), with minificationBefore/minificationAfter/minificationSkipped plugin hooks around the step - minificationBefore is the one to cancel (plugins.cancel = true) if you want to substitute an entirely different minifier/obfuscation pipeline. See Events (concept).
Cache-busting hashes
A SHA-256 hash of the built servers.js (SERVERJSHASH) gets computed and embedded into index.html, and the hashes event fires twice - once early, once again after the final build pass - giving plugins two points to observe or extend the hash set used for cache-busting.
Common Issues
My change to a shared src/shell/* file isn't showing up in the browser. The client bundle only rebuilds when the client server (re)starts - there's no watch mode or hot reload for this pipeline. Restart npm run client after every change.
A plugin's client-injected code runs before something it depends on is defined. See Client-Side Code - beforebefore/before insertion points run before most src/shell/* globals exist in the concatenated script.
This page was drafted with AI assistance and reviewed for accuracy. If something looks wrong, please open a PR or flag it.
