cpp.js picks sane defaults for every build flag, env var, path, and toolchain. When a default doesn't fit your case, there are 20 documented override points. This doc lists them in order of preference: start with the least invasive that solves your problem.
Every override point exists for a reason — but each adds a layer of "this build differs from the default in a non-obvious way". Reaching for extensions[] to override what targetSpecs[].specs.emccFlags could do makes the project harder to maintain and harder for AI agents (or future-you) to reason about.
Order of preference, from least to most invasive:
- Don't override — restate the constraint as a target filter.
targetSpecs[].specs.*for declarative per-target tweaks.cppjs.config.jsenv: {}for runtime env vars.cppjs.build.jshooks (package authors only) for source-acquisition / build-step logic.extensions[]for cross-cutting plugin behavior.~/.cppjs.jsonfor system-wide environment defaults.
Restrict which of the 20 built-in targets actually build. Doesn't change defaults — just skips targets you don't need.
target: { platform: 'wasm', runtime: 'st' } // skip android, ios, all mt buildsWhen to reach for this first: shipping faster (don't build iOS for an internal Node tool), or constraining a per-package build (a wasm-only package has no reason to define ios/android variants).
Append -D flags to cmake configure for matching targets.
targetSpecs: [{
platform: 'ios',
specs: { cmake: ['-DBUILD_WITHOUT_64BIT_ATOMICS=ON'] },
}]Append -s / -O flags to emcc command. Wasm only.
targetSpecs: [{
platform: 'wasm',
specs: { emccFlags: ['-sINITIAL_MEMORY=64MB', '-sJSPI'] },
}]Inject env vars into the running Wasm process (and into compiler env at build).
targetSpecs: [{
runtime: 'st',
specs: { env: { GDAL_NUM_THREADS: '0' } },
}]Bundle data files into the .data preload.
targetSpecs: [{
platform: 'wasm',
specs: { data: { 'share/myapp': 'myapp/data' } }, // copy share/myapp/* → /<datapath>/myapp/data/
}]Suppress specific .a names from the link line. Use when an upstream lib clashes with another transitive dep.
targetSpecs: [{
platform: 'wasm',
specs: { ignoreLibName: ['libtiff_legacy'] },
}]Env vars passed to Wasm at runtime. Function values resolved lazily — see ADR-0003.
env: {
APP_MODE: 'production',
DATA_DIR: (state, target) => `${state.config.paths.build}/data`,
CERT_PATH: '_CPPJS_DATA_PATH_/certs/cacert.pem', // _CPPJS_DATA_PATH_ replaced at runtime
}Override the default "is this target buildable?" check (default: returns true if the target's output binary already exists). Useful for skipping heavy targets in CI subsets.
functions: {
isEnabled: (target) => target.runtime === 'st' || process.env.CI_FULL === '1',
}Each entry is another resolved cpp.js config. Affects build order (pnpm topological per ADR-0002), and the dep's target.runtime: 'mt' auto-promotes you to mt.
Point at a custom CMakeLists.txt instead of the project default. Rare — cpp.js's bundled CMakeLists works for almost every project.
These are for
cppjs-package-*authors wrapping an upstream library. Consumer apps don't writecppjs.build.js.
Custom source acquisition. URL is simplest; getSource for git clone, monorepo dep copy, generated source.
Returns flags appended to cmake configure (or ./configure if buildType: 'configure'). Receives full state and current target.
Returns extra libs to add to the link line beyond what dependencies already wires up.
Build-time env vars (CFLAGS, CXXFLAGS, LDFLAGS as string literals). Different from cppjs.config.js env which is runtime.
env: (target) => [
'CFLAGS="-fPIC -DSQLITE_ENABLE_FTS5"',
'LDFLAGS="-Wl,--no-undefined"',
]15. replaceList: [{regex, replacement, paths}] or sourceReplaceList: (target, depPaths) => Array<...>
Patch upstream source via regex. Use when the upstream lib has CPU intrinsics, raw pointers, or platform-specific assembly that doesn't compile for your target.
replaceList: [{
regex: /CPL_CPUID\(1, cpuinfo\);/g,
replacement: '#ifdef __wasm__\ncpuinfo[0]=0;\n#else\nCPL_CPUID(1, cpuinfo);\n#endif',
paths: ['port/cpl_cpu_features.cpp'],
}]Real example: gdal-wasm uses this to gate CPU intrinsics; curl-wasm uses it to swap socket calls for emscripten_fetch.
Pre-configure step. Generate headers, write extra source files, fetch sub-deps.
Replace the entire build step. Use only when neither cmake nor configure can run the upstream's build system.
Run shell commands before cmake configure (e.g. autoreconf -fi for autotools projects).
copyToSource injects files into the build dir before configure (gdal's empty.cpp linker hint). copyToDist ships extra files alongside artifacts (openssl's cacert.pem).
copyToDist: { 'assets/cacert.pem': ['ssl/certs/cacert.pem'] }Plugin objects with hooks at config-load and build-step boundaries:
extensions: [{
loadConfig: { after: (config) => { /* mutate */ } },
buildWasm: { beforeBuild: (emccFlags) => { emccFlags.push('-sFOO=1') } },
createLib: { setFlagWithBuildConfig: (env, cFlags, ldFlags) => { /* mutate */ } },
}]Use when you need to share an override across multiple cpp.js packages. Inside a single package, prefer targetSpecs or cppjs.build.js hooks. The OpenSSL Android cert-injection extension is a real example.
| Key | Default | Notes |
|---|---|---|
XCODE_DEVELOPMENT_TEAM |
'' |
Required for iOS device (not simulator) builds |
RUNNER |
'DOCKER_RUN' |
'DOCKER_EXEC' keeps a long-lived container; 'LOCAL' skips Docker entirely (only works if you have all toolchains installed) |
LOG_LEVEL |
'INFO' |
'DEBUG' for verbose tracing during build issues |
These apply to every cpp.js project on the machine. Use sparingly — they don't travel with the project.
Want to change something for ALL builds?
└── Probably you don't — reach for targetSpecs with a precise filter instead.
Want to change something for ONE platform / runtime / buildType?
└── targetSpecs[] with the right filter. (Layer 2)
Need an env var passed to the running Wasm?
└── env: {} in cppjs.config.js. Use function form if it depends on state. (Layer 3)
Are you wrapping an upstream library that needs source patching?
└── cppjs.build.js replaceList (Layer 4 #15) or prepare hook (#16).
Need to share an override across packages?
└── extensions[] (Layer 5 #20).
Need to set XCODE team or pick a non-Docker runner?
└── ~/.cppjs.json (Layer 6).
- Reaching for
build: async (state)whengetBuildParamswould do. Replacing the build runner means you re-implement what cpp.js already does. Override flags first. - Copying patterns from
extensions[]into a single package's config. If only one package needs the override,targetSpecsorcppjs.build.jskeeps it local. - Using
~/.cppjs.jsonfor project-specific things. It's machine-wide; CI won't have your overrides. Project-specific config goes incppjs.config.js. - Stacking emccFlags / cmake flags in
targetSpecsAND ingetBuildParams. Confusing. Pick one location. - Editing the upstream source directly in
getSourceinstead ofreplaceList.replaceListpatches are reproducible across version bumps; manual edits aren't.
build-state.md—stateandtargetshapes that hooks receive.cppjs-config.md— fullcppjs.config.jsfield reference.cppjs-build.md— fullcppjs.build.jshook reference.troubleshooting.md— common errors that map to one of these overrides.performance.md— which Emscripten/CMake defaults are safe to override.- ADR-0003 — function-typed env values.