Architecture
You never call the compiler yourself, which is exactly why it helps to know what happens when you do. This is the one-screen model of a crossbind build - from cppjs build to the files your bundler picks up.
The pipeline
- Config load.
cppjs.config.jsis read and merged withcppjs.build.js(packages only) and machine settings from~/.cppjs.json. Transitive dependencies are flattened; if any of them is multithreaded, the project is promoted tomt. - Target expansion. CLI flags filter the target matrix down to the
{platform, arch, runtime, runtimeEnv, buildType}tuples this host can actually build. - `createLib`. Per target, your C++ is compiled to a static library - cmake or configure, inside Docker for wasm/Android/WASI, through Xcode for iOS.
- Bridge generation. Each imported header becomes a bridge translation unit that registers the binding surface; that is what makes classes and functions appear in JavaScript.
- Link.
buildWasmlinks the archives withemcc;buildWasiCommandlinks a wasi command;buildCargostages a Rust crate's.a;createXCFrameworkcombines the iOS slices. - `buildJs`. Rollup assembles the loader for the chosen runtime environment and writes the final artifacts.
Targets are the unit of work
Everything downstream of config load is per target, and the target name is stamped into every artifact - myapp-wasm-wasm32-st-release.browser.js is platform, arch, runtime, build type and runtime environment in one string. That is why st and mt artifacts can never be confused for one another, and why mixing them in one bundle fails loudly.
The JavaScript runtime layer
The loader is a shared core plus thin per-environment adapters: URL-based asset resolution in the browser, filesystem-based in Node and on edge; OPFS mounting and file auto-mount in the browser; a Comlink bridge when the module runs in a worker. Supporting a new runtime is one more shim over the same core, which is why initNative() behaves identically everywhere.
Where the toolchain lives
| Target | Runs in | Needs |
|---|---|---|
| wasm, Android | the digest-pinned Docker image | Docker |
| WASI | Docker, or a local wasi-sdk when configured | Docker or wasi-sdk 34+ |
| iOS | the host | macOS, Xcode, CocoaPods |
Rust (export.type: 'cargo') | the host | the cargo toolchain |
Docker mounts the project path as its working directory, so nothing above that path is visible to the build. In a monorepo, point paths.base at the repository root - see Configuration.
Cache and rebuilds
| Path | What it holds | Safe to delete |
|---|---|---|
.cppjs/ | build cache: cmake output, generated bridges, generated types | yes - rebuilt on the next build |
dist/prebuilt/<target>/ | the static library and headers other packages link against | yes, but dependents must rebuild |
dist/<name>.<env>.{js,wasm,data.txt} | what your app loads | yes |
A build short-circuits when the artifact is newer than the sources, which is what keeps incremental builds cheap. Bundler plugins compare source timestamps for you and force a rebuild in dev when a .h or .cpp changes - that is the hot-reload path.
paths.native does not point where you think it does.The override hierarchy
When you need to change what the build does, reach for the highest layer that solves it - the higher the layer, the less of the pipeline you take ownership of.
- Narrow which targets build:
target.{platform, arch, runtime, buildType}. - Per-target flags and data, declaratively:
targetSpecs[].specs.{cmake, emccFlags, env, data, ignoreLibName, wasiFlags}. - Project-wide:
dependencies,env,functions.isEnabled. - Package authoring:
cppjs.build.jshooks - source fetch, patches, build parameters, extra libs. - Cross-package plugins:
extensions[]hooks at config-load and build-step boundaries. - Machine-wide:
~/.cppjs.json(runner, Xcode team, log level, local wasi-sdk).
Layers one to three live in Configuration; the rest are package-author territory, documented in the API reference linked from the sidebar.