ADR-0002: Use pnpm workspace dependencies for transitive C++ build order
- Status: Accepted
- Date: 2026-05-03
- Affects:
cppjs-packages/*/cppjs-package-*/package.json(thedependenciesfield of every sub-arch package), rootpnpm-workspace.yaml, CI build scripts.
Context
Many cpp.js packages link against other cpp.js packages. GDAL needs PROJ, GEOS, libtiff, libgeotiff, OpenSSL, zlib, zstd, libcurl, libexpat, iconv, lerc, jpegturbo, sqlite3, spatialite, webp. PROJ needs libtiff and SQLite. SQLite is leaf. Building these in the wrong order produces "undefined symbol" linker errors that surface much later than the root cause.
We had three options for managing build order:
- A hand-maintained list in a script (
build-packages.shwith explicit order). - A separate manifest file describing the C++ link graph.
- Encoding the dependency graph in
package.jsonso pnpm can derive topological order.
Decision
Each sub-arch's package.json declares its C++ dependencies as "dependencies": { "@cpp.js/package-X-<arch>": "workspace:^" }. The build is invoked with pnpm --filter='@cpp.js/package-*' run build, which pnpm executes in topological order derived from those dependencies.
The dependency edge encodes both:
- Build order — the dependent package can't build until its dependency's
dist/is on disk. - Workspace publish wiring — pnpm rewrites
workspace:^to a real version number onpnpm publish, so consumers get a coherent dep graph.
Consequences
Positive:
- The build order is automatically correct without any manual maintenance.
- A new package just declares its deps in
package.json; no separate manifest to update. - pnpm catches cycles and reports them clearly. CI fails fast if someone declares a cycle.
- Publish-time version pinning is consistent with build-time topology — one source of truth.
Negative:
- The
dependenciesfield is now load-bearing for both npm semantics and C++ link order. Forgetting to add a dep produces a linker error, not a JS-level error, which is harder to diagnose. - pnpm's filter+topological behavior is a pnpm-specific feature. Migrating to npm or yarn would require a different build orchestrator. We accept this lock-in.
- Each sub-arch (
-wasm,-android,-ios) maintains its owndependencieslist, even though they're usually the same. We accept the duplication for explicitness.
Alternatives considered
- Hand-maintained shell script — fast to start, fragile at scale. Every new package needs the script updated; merge conflicts when two PRs add packages. Rejected.
- Separate manifest file (
build-graph.json) — single source of truth, but disconnected from the npm-level dep graph. We'd have to keep them in sync manually. Rejected for redundancy. - CMake
find_packagediscovery only — relies on the build sequence being right; doesn't help pnpm decide what to build first. Still used inside each package's CMake config, but doesn't replace the npm-level ordering.
See also
- Root
pnpm-workspace.yaml— workspace globber declaringcppjs-packages/*/*as a workspace. - Any
cppjs-packages/cppjs-package-gdal/cppjs-package-gdal-wasm/package.json— example with 13 transitive deps. docs/playbooks/new-package.md— Step 4 ("Wire transitive C++ deps") references this ADR.