Skip to content

Tracking: drive the seven npm-family vendor backends through one generic driver #920

Description

[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: discussion #560 register.

Kind: tracking. Source: review 4.4, 4.7 C; register E22.

Problem (verified on 9c43dfc)

Seven npm-family vendor drivers repeat one skeleton, about 1,940 lines in total:

Driver Lines Literal VendorEntry
npm_lock::vendor_npm 325 #L392-L415
pnpm_lock::vendor_pnpm_dialect (v9 and legacy since #583) 259 #L393-L421
yarn_berry_lock::vendor_yarn_berry 402 #L465-L491
yarn_classic_lock::vendor_yarn_classic 222 #L252-L275
bun_lock::vendor_bun 332 #L618-L641
bun_binary::vendor 233 #L238-L261
vlt_lock::vendor_vlt (directory artifact) 169 #L1356-L1379

Each one runs the same sequence: guard_coordinates → read the lock → flavor preflight → stage_patch_pack (or stage_patch_dir for vlt) → let Some(staged) = staged else { Done { entry: None } } → splice → "nothing changed" → already_patched_result → atomic write, with done_failure_unstage on error → VendorMarker::new("npm", …) + write_marker_or_warn → a 20-field literal VendorEntry whose only differences are flavor, artifact and one optional meta (pnpm, yarn_berry10c0, file_inventory).

Only the middle part (the lock grammar and the splice) is per flavor. The copies have already drifted (read, not executed):

  • The "patch rewrites package.json" warning has two codes and two triggers. npm (#L298-L306) and yarn classic (#L212-L222) recompute the dependency fields and warn vendor_dep_manifest_rewritten, classic only when a block changed. pnpm (#L233-L245), bun (#L439-L451), bun.lockb, berry and vlt preserve them and warn vendor_dep_manifest_stale with five differently worded texts. pnpm and bun warn before the in-sync check, so they warn again on an AlreadyPatched re-run.
  • bun_binary ignores NpmStagedPack::uuid_dir_preexisted (npm_common.rs#L163-L168) and computes its own with exists() before staging (bun_binary.rs#L77). Today the two agree, but the unstage guard has two sources of truth.
  • vlt alone records an entry with empty wiring when it rebuilds a stale artifact, and the tarball flavors return entry: None in the same state.

Symptoms / impact

No open bug is attributed to this yet. Every cross-flavor rule (unstage on failure, marker, the in-sync contract, the package.json warning) is maintained seven times; that is how the warning drift above happened. Behavior change risk is low if done mechanically, because the per-flavor splice code doesn't move.

Target design

trait NpmLockBackend {
    const FLAVOR: &'static str;
    type Plan;
    async fn preflight(&self, root: &Path, coords: &NpmCoords, w: &mut Vec<VendorWarning>) -> Result<Self::Plan, Box<VendorOutcome>>;
    fn splice(&self, plan: Self::Plan, staged: &NpmStagedPack) -> Result<Option<Commit>, String>; // None = in sync
    fn entry_meta(&self, entry: &mut VendorEntry);
}
async fn vendor_npm_family<B: NpmLockBackend>(b: &B, req: VendorRequest<'_>) -> VendorOutcome;

The generic driver owns coordinates, staging, the in-sync return, the write and unstage, the marker, the package.json warning and the entry. The flavor files keep only grammar and splicing.

Ordered checklist

  • 1. One VendorEntry constructor for the npm family plus a shared finish step (marker + entry + Done), deleting the seven literal entries. Build npm-family vendor ledger entries through one constructor instead of seven literal VendorEntry blocks #922
  • 2. NpmLockBackend + vendor_npm_family, migrating npm and yarn classic (the two that recompute dependency fields). Unify the package.json warning trigger: after the in-sync check, once per run.
  • 3. Migrate pnpm (both dialects) and berry.
  • 4. Migrate bun text and bun.lockb. bun_binary uses staged.uuid_dir_preexisted.
  • 5. Migrate vlt (the stage_patch_dir variant), or record why it stays separate.
  • 6. The revert half: revert_*_opts in the same files gets the same treatment (separate tracking under E24, nine revert mechanisms).

Size and scope

About 0.8–1.0K production lines removed overall (review 4.7 C), at 150–400 per child. Out of scope: lock grammars, hosted mode, the dead force/sources parameters (#923) and PackageSource (#800).

Acceptance criteria

  • Each child keeps lockfile bytes identical: the existing per-flavor vendor/revert unit tests, the legacy-ledgers fixtures and the e2e_vendor_* suites stay green unchanged.
  • After step 5, no VendorEntry { literal remains in the npm-family files outside tests.
  • The warning unification in step 2 adds a test: an in-sync re-run of a package.json-rewriting patch emits no manifest warning in any flavor.

Dependencies

Coordinate with #800 (it changes every backend signature) and #835 (the shared wiring kinds and lines↔JSON codec). Blocks the VendorBackend tracking (E21).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)pm:npmnpmpriority:p1refactorStructural change: duplicated code or logic, missing abstraction, layering, dead code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions