Skip to content

Monorepo & Packages

Awaken is a single pnpm workspace named forge: four framework-free engine packages under packages/*, two apps under apps/*, 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/*"

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 - and delegates into the workspace with filters.

The four 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/assetsImporters (glTF/GLB/OBJ/FBX, Unity .unitypackage, Godot, Unreal) → shared SceneImport IR, AssetStore, scene save/load (.awaken.json)@awaken/core, @awaken/render
@awaken/runtimePlay loop, ScriptHost + script API, audio, Rapier physics behind an engine-agnostic backend@awaken/core, @dimforge/rapier3d-compat

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 four @awaken/* packages + react, monaco-editor, esbuild-wasm, @bokuweb/zstd-wasm
@awaken/playerThe thin standalone runtime - boots a saved scene without the editor (edit == play == ship).all four @awaken/* packages + esbuild-wasm, fzstd, 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.-

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
(all four)   ←  @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.
  • 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.

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.

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 two small custom plugins that make authoring behave correctly:

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

📸 Screenshot - save as img/architecture-monorepo-tree.png

A file-tree view of the repo root showing packages/{core,render,assets,runtime} and apps/{editor,player,assets}, with pnpm-workspace.yaml and tsconfig.base.json visible.

See also

Awaken — browser-native WebGPU game engine.