Skip to content

Build & Player Template

pnpm build runs the editor build, which first bakes the standalone player into a single self-contained player-template.html, then compiles the editor around it - and getting that order (and its freshness) right is what makes exported games actually run.

The build pipeline

The root build script delegates to the editor build, and the editor build bakes the player template as its first step (before typecheck and vite build):

json
// root package.json
"build": "pnpm --filter @awaken/editor build",

// apps/editor/package.json
"build": "node ../../scripts/build-player-template.mjs && tsc --noEmit && vite build"
  1. The editor build's first step runs scripts/build-player-template.mjs (the same script the standalone pnpm build:player-template runs), compiling the player into one inlined HTML file and dropping it at apps/assets/player-template.html.
  2. The editor build then runs tsc --noEmit (typecheck gate) and vite build. Because the editor's publicDir is apps/assets, the freshly baked template is copied into the editor's output and served at /player-template.html in both dev and production.

At export time, the editor fetches that template and stamps your scene into it to produce a shippable, double-clickable game.html.

What build:player-template does

The script (scripts/build-player-template.mjs) is small but every line is load-bearing, because the output has to run from a file:// origin when someone double-clicks the exported game.

1. Build the player as a single file

bash
AWAKEN_SINGLE=1  pnpm --filter @awaken/player exec vite build

The player has two build shapes, selected by the AWAKEN_SINGLE env var in its vite.config.ts:

ShapeCommandOutputUse
hostedvite buildapps/player/dist/fetches ./scene.awaken.json at runtime, can compile raw TS scripts via esbuild-wasm
singleAWAKEN_SINGLE=1 vite buildapps/player/dist-single/index.htmleverything inlined into one HTML file; the export template

In single mode, vite-plugin-singlefile inlines all JS/CSS, the build emits an IIFE (format: "iife", inlineDynamicImports: true, modulePreload: false), and __AWAKEN_INLINE_ONLY__ is defined true so the esbuild-wasm script compiler is dead-code-eliminated (a shipped game only runs precompiled behaviours, so it doesn't need the compiler).

2. Rewrite module → classic script

js
html = html.replace(/<script\s+type="module"(?:\s+crossorigin)?\s*>/i, "<script>");

Even though the bundle is a classic IIFE, Vite still tags it <script type="module">. Chrome refuses to execute ES module scripts from a file:// origin (a double-clicked file is a unique opaque origin), so a module-tagged player would silently never boot when the game is double-clicked. Dropping type="module" (and crossorigin) makes it run as a classic script.

The script also strips <link rel="icon"> / apple-touch-icon tags, whose absolute /favicon/... paths would 404 on a double-clicked page - pure console noise a shipped game doesn't need.

3. Fail loudly if a module survived

js
if (/<script\b[^>]*\btype="module"/i.test(html)) {
  console.error('[awaken] BUILD FAILED - player template still has a <script type="module">…');
  process.exit(1);
}

This is a hard build gate. The headless smoke can't catch it - headless Chrome allows file:// module scripts, but headed Chrome does not - so if the rewrite ever fails to match, the build stops here rather than shipping a game that boots in tests and dies on a real double-click. Only when the check passes is apps/assets/player-template.html written.

The critical stale-template rule

The template is a pre-built snapshot - rebuild it after engine or player changes

An exported game.html embeds whatever player-template.html was last built - not the code currently in your working tree. If you change anything in packages/* (the engine) or apps/player and then export without rebuilding the template, the exported game runs the old player code.

The failure mode is nasty because it's invisible in the editor: the feature works in Play (the editor uses live engine code), but is missing or broken in the export (which runs the stale embedded template).

Always run one of these after engine/player changes, before exporting:

bash
pnpm build:player-template     # rebuild just the template
# or
pnpm build                     # rebuilds the template, then the editor

This is the single most common "it works in the editor but not in my exported game" cause. If an exported game is missing a feature you know exists, rebuild the template first and re-export before debugging anything else.

📸 Screenshot - save as img/architecture-build-template.png

A terminal running pnpm build:player-template, showing the final success line [awaken] player template → apps/assets/player-template.html (… KB).

Deploying the editor

The hosted editor deploys to Vercel. vercel.json runs the full gate as its build command and rewrites all routes to the SPA entry:

json
{
  "buildCommand": "pnpm test && pnpm build",
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

So every deploy (and every PR preview) runs the whole test suite and the build - which includes rebuilding the player template - before anything ships. A red test fails the deploy.

See also

Awaken — browser-native WebGPU game engine.