Skip to content

pacquet: port the writable-lockfile install path (resolver → Lockfile adapter) #11813

Description

@zkochan

Problem

pacquet install (no flags) on a clean project does not produce a pnpm-lock.yaml. The install succeeds and node_modules/ is populated, but no lockfile is ever written. Re-running pacquet install in the same directory re-resolves everything against the registry instead of reusing a pinned graph.

Root cause

The install dispatch in pacquet/crates/package-manager/src/install.rs has three branches today and none of them writes a lockfile:

  1. --frozen-lockfile → frozen-lockfile path. Reads pnpm-lock.yaml, writes <virtual_store_dir>/lock.yaml (the current-lockfile snapshot), never touches pnpm-lock.yaml.
  2. no flag + config.lockfile == true → returns InstallError::UnsupportedLockfileMode ("Installing with a writable lockfile is not yet supported.").
  3. no flag + config.lockfile == false (pacquet's current default) → InstallWithoutLockfile: runs the resolver but discards everything except materialization output. No pnpm-lock.yaml is written.

What's already in place

  • Resolver is complete. resolving-deps-resolver produces a full transitive DependenciesGraph from a package.json, with auto-install-peers, abbreviated-metadata fast path, npm aliases, workspaces, catalogs, overrides, patches, runtime resolvers, etc. all landed.
  • Lockfile writer is complete. lockfile/src/save_lockfile.rs serializes a full Lockfile to pnpm-lock.yaml atomically, with round-trip tests.
  • InstallWithoutLockfile already runs the resolver (install_without_lockfile.rs) and uses the resulting graph to fetch, import, and materialize node_modules.

Missing piece

A single adapter that converts the resolver's DependenciesGraph into a Lockfile (its importers / packages / snapshots maps). The resolver output has everything the lockfile needs — dep_path, resolved_package_id, integrity, resolution, peer info, children — but no code today consumes the graph for anything other than materialization. The graph is built, walked once for node_modules, then dropped.

Proposed scope (this issue)

  1. Add pub fn dependencies_graph_to_lockfile(graph: &DependenciesGraph, manifest: &PackageManifest, ...) -> Lockfile somewhere appropriate (likely pacquet/crates/package-manager/).
    • Map each DependenciesGraphNodeSnapshotEntry (peer-suffixed PackageKeys included).
    • Build the importers map (root importer for now; workspaces tracked separately at Add workspace support to pacquet install --frozen-lockfile pacquet#431).
    • Build the packages map (PkgIdPackageMetadata).
    • Honor included (--no-optional, --no-dev) so the lockfile reflects what the resolver actually produced.
  2. In InstallWithoutLockfile::run, after the resolve completes, call the adapter and save_to_path the result to the workspace root.
  3. Rename InstallWithoutLockfile → something like InstallWithFreshLockfile (the "without lockfile" framing stops being accurate once it writes one). Adjust the dispatch in install.rs to remove the UnsupportedLockfileMode branch.
  4. Port the upstream pnpm tests for fresh-install lockfile shape into crates/package-manager/. The most relevant TS tests live in installing/deps-installer/test/lockfile.ts and installing/deps-installer/test/install/*.ts — pick the subset that doesn't depend on incremental update (that's issue 3).

Out of scope (separate issues)

  • GVS layout in the fresh-install path. The fresh-resolve path currently hardcodes VirtualStoreLayout::legacy. Switching it to GVS-when-enabled is its own design decision (the deliberate scoping comment at install.rs:581-585 needs to be revisited once the path actually has a lockfile to anchor a store-registry entry against). Tracked separately.
  • Stale-lockfile rewrite + preferFrozenLockfile dispatch. When a lockfile exists but doesn't match package.json, we should re-resolve and rewrite it (and gate on preferFrozenLockfile=true to keep the frozen path the default when the lockfile matches). Tracked separately.

Why this comes first

This is the smallest standalone unit: the adapter + writer hookup unblocks both follow-ups (issues 2 and 3 both want to mutate the lockfile produced by this path).


Written by an agent (Claude Code, claude-opus-4-7).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions