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:

  1. 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.

  2. 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.

  3. 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:hosted script exactly as your SDK's dev-host-harness instructions specify, then run it. The port must be one of your manifest's local allowedParentOrigins (the harness derives its port from them), so keep a 127.0.0.1 origin 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