Skip to content

Monorepo & Packages ​

Awaken is a single pnpm workspace named awaken: five framework-free engine packages under packages/*, two apps plus a static folder under apps/*, one service under services/*, and a shared TypeScript config, with no bundler build step between the packages.

The workspace ​

The repo is a plain pnpm workspace. pnpm-workspace.yaml is the whole configuration:

yaml
packages:
  - "packages/*"
  - "apps/*"
  - "services/*"

There is no Turbo, no Nx, no Lerna. Task orchestration is plain pnpm: pnpm --filter <pkg> runs a script in one package, pnpm -r runs it across all of them. The root package.json (name awaken, private: true) holds the top-level scripts (dev, build, test, test:gpu, typecheck, lint, smoke, golden, export:project, export:mobile, build:player-template, build:mobile-template, awaken:pack / unpack / check) and delegates into the workspace with filters.

The five engine packages ​

Each engine package is a real npm package (scope @awaken/*), but a deliberately unusual one: it publishes raw TypeScript source, not a compiled dist.

PackageResponsibilityDepends on
@awaken/coreECS (World/Entity/Component), math (mat4/quat/vec3), transform hierarchy, reflection schema, scene serialize + version migrationnothing
@awaken/renderWebGPU renderer (PBR, cascaded shadows, SSAO, bloom, MSAA, opt-in GPU-driven cull), WGSL shaders, octree/frustum cull, static batching, lights, camera, gizmos@awaken/core
@awaken/terrainThe terrain runtime type: heightfield, chunked meshes, vegetation instances, terrain collision and height queries (api.terrain). See Terrain and @awaken/terrain.@awaken/core, @awaken/render
@awaken/assetsTwo jobs behind two doors. The root entry is the runtime surface: scene load/serialize, the ship container, AssetStore, rig profile. @awaken/assets/import is the importers (glTF/GLB/OBJ/FBX, Unity .unitypackage, Godot, Unreal), the shader-graph transpilers, project formats and signing - editor-time only.@awaken/core, @awaken/render, @awaken/terrain, fflate
@awaken/runtimeThe player's feature systems: script host + script API, the fixed clock, module jobs, animation, UI, audio, input, and physics behind an engine-agnostic backend. Headless-capable.@awaken/core, @dimforge/rapier3d-compat, webaudio-tinysynth (MIDI)

The two doors on @awaken/assets ​

A shipped game may touch only the root entry. Importer code behind /import is editor-time, and it used to ride into every game bundle because both lived behind one entry. The split is enforced twice: layering.test.ts checks the source, and npm run size:player scans the built player bundle for banned paths and fails on a budget breach.

Notice @awaken/runtime does not depend on @awaken/render - the play loop, scripts, and physics are renderer-agnostic. The renderer is wired in by the client (editor or player), not by the runtime.

The apps ​

AppWhat it isDepends on
@awaken/editorThe React authoring shell - Viewport, Hierarchy, Inspector, dock, undo/redo commands, import pipeline. Drives the engine through its public API only.all five @awaken/* packages + react, monaco-editor, esbuild-wasm, assemblyscript, @bokuweb/zstd-wasm, javascript-obfuscator, node-forge
@awaken/playerThe thin standalone runtime - boots a saved scene without the editor (edit == play == ship).all five @awaken/* packages + esbuild-wasm, fzstd; builds with vite-plugin-singlefile
apps/assetsNot a package - a static directory (favicon, app icons, SVG marks, manifest). It is the editor's Vite publicDir and the drop location for the built player-template.html and mobile-template.zip.-

apps/assets has no package.json, so pnpm ignores it as a workspace member. It exists only so the editor can serve stable brand assets at the web root and so the build has one place to write the player template.

The dependency rule ​

The one rule that keeps everything shippable: dependencies flow one way, and nothing depends on the editor.

@awaken/core  ←  @awaken/render, @awaken/assets
@awaken/core  ←  @awaken/runtime
@awaken/core, @awaken/render  ←  @awaken/terrain  ←  @awaken/assets
(all five)   ←  @awaken/editor, @awaken/player
  • Engine packages never import React - that keeps the runtime portable into a shipped game and keeps Play mode running the true runtime. See Architecture Overview.
  • The matrix is not a convention: packages/runtime/src/layering.test.ts fails a build that breaks it.
  • Every importer (six of them) emits the shared SceneImport/GNode IR in packages/assets/src, consumed by one builder (apps/editor/src/state/importPack.ts). New importers target the IR, not the renderer - see Import Overview.

Other top-level folders ​

FolderWhat it is
services/registry@awaken/registry: the identity session verifier, plugin-publish and revocation service behind signed plugins. A reference implementation with no committed dependencies (Node 22 built-ins).
mobile-template/The React Native shell Export for Mobile writes around a game; pnpm build:mobile-template zips it into apps/assets/mobile-template.zip.
starter-content/Materials, particles, prefabs, scripts and an animation graph shipped with the editor.
examples/Example plugins (the terrain plugin source) and their built output.
scripts/Build, GPU-check, golden-frame, export and editor-walk scripts (see Testing).

TypeScript config ​

All packages extends one shared base, tsconfig.base.json, so every corner of the repo is compiled with the same strict settings:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "declaration": true
  }
}

Each package's own tsconfig.json just adds its lib/types (e.g. @webgpu/types for render, DOM + react-jsx for the editor) and sets noEmit - typecheck is a gate, not a producer of build artifacts.

Raw-source consumption (no per-package build) ​

Each engine package's package.json points main, types, and exports straight at ./src/index.ts:

json
{
  "name": "@awaken/core",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": { ".": "./src/index.ts" }
}

There is no build step for the packages. When the editor imports @awaken/core, it imports the .ts source directly, and each app's Vite bundler compiles everything together in one pass. This makes cross-package refactors instant (no rebuild-and-relink) and means "go to definition" always lands on real source. The cost - that consumers must be TypeScript builds - is fine here because the only consumers are the two Vite apps (and the headless exports, which run the editor's modules under vitest).

The ~5.6.3 pin ​

TypeScript is pinned to ~5.6.3 at the root on purpose. TS 5.7 made TypedArray generic, which breaks @webgpu/types (the renderer's typed-array-heavy GPU buffer code stops compiling). Don't bump it casually - a version bump here is a renderer-wide typecheck break, not a routine dependency update.

Typecheck the whole workspace with:

bash
pnpm -r typecheck        # each package's own `tsc --noEmit`

The editor's Vite plugins ​

Each app is a Vite 5 project. The editor's vite.config.ts sets publicDir: "../assets" (serving apps/assets at the web root), port: 5180, and adds three small custom plugins that make authoring behave correctly:

  • awaken-decompressors - bundles any optional decompressor providers configured for import, and fails the build if a codec reaches a distributed artifact it must not ship in (scripts/codec-guard.mjs).
  • awaken-preview - serves an exported game over http from the editor's own origin at /__awaken_preview/<id>. The editor POSTs the game HTML and opens the returned URL in a new tab, so Preview matches real hosting instead of a file:// origin (which blocks ES modules and floods the console).
  • awaken-reload-on-engine-change - forces a full page reload whenever any file under packages/*/src changes. The editor store owns the PlayController and ScriptHost and is created once via useMemo, which React Fast Refresh preserves - so a hot update to engine code would keep running the old runtime. A full reload rebuilds the store from the new code. Edits under apps/editor/src still Fast-Refresh normally, preserving your open scene for UI-only changes.

The player's Vite config is separate and produces two build shapes (a hosted build and a single-file game) - that is covered in Build & Player Template.

See also ​

Awaken: browser-native WebGPU game engine.