Vite Config & the Build
Your game is a Vite project, but the production build is not a plain
vite build. This page explains what the scaffold's vite.config.js and
tools/build.mjs do, the one build setting you must never remove, and the two
ways to run your game locally.
You rarely edit these files. Read this once so you understand why they look the way they do — and so the "blank bundle" trap never catches you.
The scripts, at a glance
The scaffold's package.json wires up a small set of commands:
| Command | What it runs | When you use it |
|---|---|---|
npm run dev |
Vite dev server on http://127.0.0.1:7500 (standalone) |
The everyday inner loop. Hot-reload, no host, no camera required. |
npm run build |
node tools/build.mjs (not bare vite build) |
Produce the self-contained dist/. |
npm run validate |
The fail-closed submission gate against dist/ |
Before every submission. |
npm run export:zip |
build + write bodylink-bundles/<id>@<version>.zip |
Produce the shippable bundle. |
npm run preview |
Vite preview of the built dist/ on :7501 |
Sanity-check the built output. |
npm test |
node --test test/*.test.mjs (host-free template tests) |
Every time you change game logic. |
The scaffold also writes export (bundle without zipping) and version:bump.
Note what is not here: a fresh scaffold does not create a dev:hosted
script — the self-hosted runner is an opt-in you add yourself (see Two ways to
run locally, below).
What vite.config.js does
Three things worth knowing:
It serves the SDK in dev. In
npm run dev, a small dev-server plugin serves the BodyLink SDK modules and tracking assets so your game resolves its imports locally. You don't configure this.It externalizes the SDK for the build. The build marks the SDK as external — the SDK is not copied into your bundle. At runtime the host injects the SDK's origin and your bundle loads it cross-origin, SRI-pinned (see SDK Delivery & SRI). This keeps your bundle small and lets the platform ship SDK fixes without you re-exporting.
It sets
output.codeSplitting: false. This is the load-bearing line. Read the next section before you ever touch it.
The blank-bundle trap: keep output.codeSplitting: false
This is the single most important thing on this page.
The symptom. Your game runs perfectly under npm run dev, but the
exported bundle white-screens, or throws something like X is not iterable
/ undefined is not a function at runtime.
The cause. When you export, the bundle flattener inlines only the single
<script type="module"> entry of your index.html and resolves the remaining
bindings from the SDK namespace. If the bundler is allowed to split code, it
hoists some of your modules into a separate shared chunk — and that chunk is
dropped from the runtime bundle. Its exports then arrive undefined at
runtime. That's the blank bundle.
The fix (already in your config). output.codeSplitting: false forces each
entry to be fully self-contained — nothing is hoisted into a sibling chunk,
so nothing gets dropped. Your vite.config.js ships with it set:
build: {
rollupOptions: {
output: {
codeSplitting: false, // <-- keep this. Removing it = blank bundle.
},
},
},
Never remove it. If a tutorial or an AI suggestion tells you to delete it or turn on code splitting "for performance", don't — an opaque-origin iframe cannot fetch a split chunk at runtime, so there is no bundle to split into.
What tools/build.mjs does (and why not bare vite build)
The bundler only allows codeSplitting: false with a single input. So the
build can't just point Vite at every HTML file at once. Instead,
tools/build.mjs runs one single-input Vite pass per HTML entry, selecting
the entry with an environment variable, and makes sure a later pass never wipes
the output of an earlier one.
A fresh scaffold ships exactly one HTML entry (index.html), so today
tools/build.mjs does one pass. The pattern is there so that the day you add a
second HTML page (say an editor or a debug view), you extend the build
target list and each page still builds self-contained — no shared-chunk
regression, no blank bundle.
Bottom line: always build with npm run build (or npm run export:zip,
which calls it). A bare vite build will not apply this discipline.
Two ways to run locally: dev and an optional hosted runner
There are two local modes, and they test different things. Only the first is wired by the scaffold.
npm run dev — standalone (scaffolded)
Runs your game directly at http://127.0.0.1:7500: no host, no SDK
handshake, tracking wired straight in. This is the fast gameplay loop with
hot-reload — and it is the one local run script the scaffold creates. It's
great, but it does not exercise the part that only exists in production: the
hosted contract (handshake, calibration ordering, frame gating, camera
ownership).
A hosted runner — opt-in, not scaffolded
A fresh scaffold does not ship a dev:hosted script (its package.json
wires dev, build, preview, validate, export, export:zip, test, and
version:bump — no more). If you want to boot your game inside a real
BodyLink host page locally — to drive the full Runtime v1 flow: the
hello/sdk.ready handshake, host-owned camera, the get-ready calibration
ceremony, frame forward-gating, and camera-preview intents — add the hosted
runner yourself.
Adding it. Wire a
dev:hostedscript exactly as your SDK's dev-host-harness instructions specify, then run it. The port must be one of your manifest's localallowedParentOrigins(the harness derives its port from them), so keep a127.0.0.1origin in the manifest. Two flags then apply to that runner:
?camSim=1— drive tracking from a bundled camera-fixture clip, so you need no webcam (still real pose detection).?calibration=ceremony(default) /synthetic/game— how the get-ready calibration is produced.
Related pages
- Your First Game with Vite — the end-to-end build → validate → export walkthrough.
- SDK Delivery & SRI — why the SDK is externalized and integrity-pinned rather than bundled.
- Troubleshooting (Vite) — blank bundle, white-screen, and dev-works-but-export-fails, step by step.