Skip to content

feat: lockfile-style reproducibility for bun auto-install (lock resolved versions, no drift) #718

Description

@vivek7405

Problem

Under bun zero-install, webjs's pin rewrite forwards the package.json RANGE (e.g. ^0.8.0) as an inline specifier, so bun resolves the HIGHEST in-range version on each run. A fresh bun run dev therefore AUTO-UPDATES to a newly-published in-range version (drift), with no lockfile pinning it. We want the npm / bun lockfile behaviour: a declared range resolves once, gets LOCKED to a specific version, and stays there until deliberately updated.

Design / approach

The building blocks ALREADY EXIST, so this is largely ergonomics + discoverability, not new mechanism:

  • packages/server/src/bun-pin-rewrite.js resolveDepVersions ALREADY prefers a bun.lock EXACT version over the package.json range ("bun.lock exact wins (precise and reproducible). Otherwise pass the declared semver through"). So a committed bun.lock with exact pins makes webjs rewrite each dep to the EXACT version, locked, no drift.
  • CRUCIALLY, webjs READS the bun.lock itself in the rewrite; it does NOT depend on bun's runtime honoring bun.lock (which research: can bun zero-install get lockfile-like reproducible versions? #705, closed, proved it does NOT for bare imports).
  • A bun.lock can be generated fast WITHOUT a full install via bun install --lockfile-only (resolves + writes the lock, no node_modules, ~220ms, verified in research: can bun zero-install get lockfile-like reproducible versions? #705).

So provide an ergonomic, documented lock opt-in:

  • (a) a webjs lock command wrapping bun install --lockfile-only (and refreshing the committed bun.lock), so one command gives reproducible pins with no node_modules;
  • (b) document committing a bun.lock as THE reproducibility opt-in across the runtime docs;
  • (c) decide whether the scaffold ships a bun.lock by default (tradeoff: reproducibility vs the zero-install "no install ever, picks up patches" simplicity, probably keep opt-in);
  • (d) a webjs lock --update to deliberately bump within range (the unlock + re-resolve flow).

The DEFAULT stays range-forwarding (idiomatic auto-update); the lock is the explicit reproducibility tier.

Implementation notes (for the implementing agent)

Acceptance criteria

  • A documented, ergonomic way (e.g. webjs lock) to generate + commit a bun.lock with no full install, so declared ranges lock to exact resolved versions.
  • With a committed bun.lock, a fresh zero-install run pins every dep to the locked exact version (no drift to a newer in-range release).
  • A counterfactual (remove the lock) shows it drifts back to the highest in-range version.
  • The no-lock default (range forwarding, auto-update) is unchanged.
  • Docs (runtime.md + docs site + scaffold template) explain the lock opt-in and that it does not depend on bun honoring bun.lock at runtime.
  • The Node track's separate lock need is noted (links Zero-install dev: no install command, transparent resolution (Node+Bun) #669).

Relates to #705, #698, #703, #700, #669.

Update: the lock should be AUTOMATIC and seamless, not a manual step

Clarified requirement: the user does NOT want a manual bun install --lockfile-only / webjs lock step. The version locking should happen automatically and transparently as part of auto-install. A first zero-install run resolves each declared range to a concrete version and PERSISTS it (a webjs-managed lock), and every subsequent run reuses that exact pin, so versions never drift to a newer in-range release without an explicit update. No command, no ceremony.

Design implication: webjs already controls version resolution (the onLoad rewrite, resolveDepVersions). So webjs can, on first resolution, WRITE the resolved exact version to a webjs-owned lock (e.g. .webjs/auto-lock.json, or refresh bun.lock) and prefer it on every later run (the rewrite already prefers an exact lock over a range). The "auto-update within range" behaviour becomes opt-in (an explicit webjs update, or deleting the lock), the inverse of today. Open questions to settle: where the lock lives and whether it is committed by default; how a package.json range edit invalidates or refreshes the relevant entries; first-run write performance; and the Node URL-resolution track (#669) equivalent. Keep it seamless: the user types the same commands and gets reproducibility for free, with auto-update as the deliberate action.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions