Runtime dependency-graph pinning¶
AutoMobile publishes a pinned runtime dependency graph so that a clean
bun install -g @kaeawc/auto-mobile@<version> resolves the same versions no
matter what compatible releases appear in the registry afterward (issue #5421).
The problem it solves¶
bun install -g re-resolves the published dependencies from their version
ranges at install time. With caret ranges, an unchanged release can start
selecting dependency versions that did not exist when it was published. On
2026-08-20 a staged @peculiar/asn1-*@2.9.4 publish made clean installs of a
fixed @kaeawc/auto-mobile version fail transiently with
No version matching "^2.9.4" found until every matching version became
resolvable. A fixed release must resolve a fixed graph.
Why the graph is small¶
build.ts bundles the server into a single dist/src/index.js, externalizing
only the image backends it loads from node_modules at runtime — the jimp
family (jimp, @jimp/core) and the sharp family (sharp + the platform
@img/sharp-* binaries). One more package is a runtime dependency without being
in that bundle: kysely. The DB migration .ts files are copied verbatim
into dist/ and loaded from disk at runtime (via AUTOMOBILE_MIGRATIONS_DIR),
and each imports kysely’s sql tag — so kysely is the fourth runtime root
even though it is not import()-ed from the bundle.
The server uses the zod/v4 subpath exported by zod@3.25.76. It remains
bundled into dist/, while the exact root package is also published so every
Jimp zod: ^3.23.8 edge resolves to the same pinned version. Every other
inlined package (werift, the MCP SDK, …) is not needed at install time and
lives in devDependencies; consumers never install them, which is what removed
the @peculiar/asn1-* install path entirely.
How the graph is pinned¶
The primary mechanism Bun honors for a consumer’s bun install -g is exact
top-level dependencies (verified empirically). A published
npm-shrinkwrap.json is ignored by Bun. The graph is therefore flattened into
exact dependencies wherever a build-time version does not conflict, with a
small bundledDependencies set for the conflicting Jimp paths:
- runtime roots —
jimp,@jimp/core,sharp,kysely— and every pure-transitive node of their closure are pinned to exact versions; - shared Jimp validator —
zod@3.25.76is a direct exact pin. AutoMobile imports itszod/v4subpath, so the bundled server retains the v4 API while Jimp’s v3-compatible ranges cannot re-resolve; - platform-native
@img/sharp-*binaries stay inoptionalDependencies(already exact-pinned, resolved per platform); - non-native
@imgtransitives, including sharp’s@img/colour, remain independenciesand are exact-pinned like every other pure-transitive node; @jimp/diff,@jimp/js-png, andparse-bmfont-xmlare bundled so their nestedpixelmatch,pngjs, andxml2jsversions remain part of the published artifact instead of being resolved from later registry releases. The clean-room gate verifies each bundled owner path, including nested dependency paths, not merely that one matching residual exists somewhere in the package. The native@img/sharp-*binaries remain unbundled and platform-selected.
The selected bundle adds about 17 MiB to the package. The unpacked-size guard is therefore 36 MiB: enough for the reproducible Jimp closure, but still a bounded release contract.
The pinned graph is mirrored in scripts/release/runtime-graph.json (the
manifest) and enforced by:
| Guard | Where | What it proves |
|---|---|---|
pin-runtime-deps.ts --check |
Fast Validation (runtime-pins) |
package.json + manifest are in lock-step with bun.lock (hermetic) |
verify-pinned-runtime-graph.sh |
PR benchmarks job + release preflight | a clean-cache install of the packed artifact reproduces every exact and bundled runtime version |
Refreshing the graph (dependency / security updates)¶
When a runtime dependency or a security override changes the resolved graph
(e.g. a Dependabot bump to sharp, jimp, or one of their transitives):
- Update the version(s) as usual and run
bun installsobun.lockreflects the new resolution. - Rebuild so the roots derivation reads the current bundle:
bun run build - Regenerate the pinned graph and manifest:
bun scripts/release/pin-runtime-deps.ts --write bun install # refresh bun.lock for any newly-direct pins - Commit
package.json,bun.lock, andscripts/release/runtime-graph.jsontogether. - Confirm locally before pushing:
bun scripts/release/pin-runtime-deps.ts --check bash scripts/ci/verify-pinned-runtime-graph.sh
If the clean-room gate reddens, regenerate the manifest and inspect the packed tree before publishing. A bundled or exact runtime version changing without that refresh is a release failure, not an expected registry update.