Skip to content

feat: resolve bun zero-install range/tag deps to exact (pin in rewrite + importmap) #697

Description

@vivek7405

Problem

webjs ships packages/server/src/bun-pin-rewrite.js (#685/#686) to make Bun zero-install (bun run with no node_modules) honor package.json. Bun runtime auto-install ignores package.json for a BARE import 'zod' (installs latest, #683/#684), so the rewrite turns a bare specifier into an inline-versioned one (import 'zod@3.22.4') because Bun DOES honor an inline version.

Today resolveDepVersions() pins exact versions only: bun.lock exact when present, else an exact package.json value; a range or dist-tag dependency with no bun.lock entry is left BARE (so it falls back to latest, ignoring the declared range/tag). That is the gap.

The in-code rationale for exact-only is stated in bun-pin-rewrite.js (the comment around the isExactVersion gate): "Bun zero-install auto-install resolves name@1.2.3 but ENOENTs on a range or dist-tag (name@^1.2, name@latest)". That rationale is empirically false on bun 1.3.14 (see below). Inline ranges and tags resolve and fetch fine. So the exact-only restriction is stricter than necessary, AND it leaves range/tag deps unpinned.

Empirical findings (bun 1.3.14 stable + 1.4.0-canary, on a real machine)

  • BARE import ignores package.json: with package.json declaring is-odd: "1.0.0", runtime auto-install installed is-odd@3.0.1 (latest) even though is-odd@1.0.0 was already in the cache. Confirms the bare-path bug and that it is not cache-limited.
  • INLINE range/tag DO work on a fresh fetch (never-cached packages), contradicting the code comment:
    • lodash.camelcase@^4.0.0 -> 4.3.0
    • lodash.snakecase@latest -> 4.1.1
    • lodash.startcase@~4.4.0 -> 4.4.0
    • zod@^3.20.0 -> 3.25.76 (highest 3.x, not latest 4.4.3)
  • INLINE exact works for most packages on a fresh fetch (lodash.kebabcase@4.1.1, ms@2.0.0).
  • CEILING (separate Bun bug, document, do not try to fix here): some packages ENOENT under runtime auto-install regardless of specifier form (exact OR range OR tag), while bun add <same> works. Observed: nanoid (all forms) and zod@3.22.4 / zod@~3.22.0 / zod@next. Matches Bun's own source comment ("auto install ... is also quite buggy and untested"). These need a real install (node_modules / vendor pin).

Design / approach

Resolve every declared dep to ONE concrete exact version up front, then pin that exact version. Do not forward a raw range/tag to the runtime (it works, but it drifts as new versions publish, and it must agree with the importmap, see below).

  1. In resolveDepVersions(): keep bun.lock exact as the first choice. When a dep is a range/tag with no bun.lock entry, resolve it to an exact version via a single registry query (reuse the jspm/registry resolution already in vendor.js) instead of leaving it bare. Exact package.json values still pass through unchanged.
  2. Correct the isExactVersion rationale comment (ranges/tags do NOT ENOENT inline; the real reasons to resolve-to-exact are determinism and importmap coherence).
  3. Server/client coherence: the SAME resolved exact version must feed BOTH the server specifier rewrite AND the browser jspm importmap, or the server runs one version while the browser loads another. Verify how the importmap obtains versions under Bun zero-install: vendor.js getPackageVersion()/resolvePackageDir() read from node_modules, which does not exist under zero-install, so the resolved-exact map must be the shared source.

Implementation notes (for the implementing agent)

  • Where to edit:
    • packages/server/src/bun-pin-rewrite.js: resolveDepVersions(pkgJsonText, bunLockText) (the exact-only gate isExactVersion() and the "range without a lock is left BARE" branch), rewriteDepSpecifiers(), packageNameOf(), skipSpecifier().
    • packages/server/src/vendor.js: registry/version resolution to reuse for range->exact (getPackageVersion, resolvePackageDir, vendorImportMapEntries, the jspm generate path) and the importmap entry build that must consume the same resolved-exact version under zero-install.
    • Wiring is via the Bun onLoad plugin in packages/server/src/action-seed-bun.js / action-seed.js (the transform runs alongside the TS-strip onLoad); confirm the resolved map reaches both.
  • Landmines:
    • The exact-only behavior was deliberate (feat: pin Bun zero-install deps via an onLoad specifier-rewrite from package.json #686, commit that added isExactVersion). Do not just forward ranges; resolve to exact for determinism + importmap coherence.
    • Runtime auto-install is flaky per package (nanoid, some zod versions ENOENT). Do not assume a green pin means every package installs; document the ceiling and fail soft.
    • Bun zero-install has no node_modules, so any getPackageVersion node_modules read returns null there; the resolved-exact map must come from package.json + bun.lock (+ a registry query), not disk resolution.
  • Invariants: framework packages/ stays buildless plain JS + JSDoc (no .ts). Bun parity is part of the task (AGENTS.md code-workflow item 1): add/extend test/bun/pin-rewrite.mjs + test/bun/pin-rewrite.test.mjs and the unit suite packages/server/test/bun-pin-rewrite/bun-pin-rewrite.test.js.
  • Docs surfaces to sync: agent-docs/runtime.md, docs/app/docs/runtime/page.ts, AGENTS.md, packages/cli/templates/AGENTS.md, and the webjs-config schema/types if a config knob is added.

Acceptance criteria

  • A range/tag dependency with no bun.lock entry is resolved to a concrete exact version and pinned (no longer left bare -> latest).
  • The same resolved exact version is used for BOTH the server specifier rewrite and the browser importmap (no server/client skew).
  • Exact package.json values and bun.lock exacts keep working unchanged.
  • The stale isExactVersion "ENOENTs on a range or dist-tag" comment is corrected.
  • The per-package runtime-auto-install ceiling (nanoid-style ENOENT) is documented with the node_modules/vendor-pin fallback.
  • Tests at every layer it touches, including a counterfactual that fails when reverted, and a test/bun/* cross-runtime assertion run under Bun.
  • Docs/AGENTS.md/runtime.md updated.

Relates to #669 (zero-install transparent resolution Node+Bun), #683/#684/#690 (research), #685/#686 (the existing bun-pin-rewrite).

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions