mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-21 05:02:30 -04:00
0f51f820dfc84a0d7eaff8f8fadbb7481f4dc57d
## Summary Closes #447. Adds a `satisfies_package_manifest` check to pacquet's frozen-lockfile dispatcher so a stale `pnpm-lock.yaml` no longer silently installs the wrong shape of `node_modules`. Before this, `pacquet install --frozen-lockfile` would happily materialize whatever the lockfile said, even when a dev had edited `package.json` (added, removed, or bumped a dep) without re-running the resolver. Upstream pnpm catches this at the dispatch site with `ERR_PNPM_OUTDATED_LOCKFILE` ([`pkg-manager/core/src/install/index.ts:808-832`](https://github.com/pnpm/pnpm/blob/94240bc046/pkg-manager/core/src/install/index.ts#L808-L832)); this PR ports the same gate. ### What's checked Ported from upstream's [`satisfiesPackageManifest`](https://github.com/pnpm/pnpm/blob/94240bc046/lockfile/verification/src/satisfiesPackageManifest.ts). Four phases, short-circuiting on the first failure: 1. **Flat-record specifier diff** over `dependencies ∪ devDependencies ∪ optionalDependencies`. Catches added/removed/modified deps in one bucket; rendered as a `SpecDiff` with per-bucket lists. 2. **`publishDirectory`** parity between the importer entry and the manifest's `publishConfig.directory`. 3. **`dependenciesMeta`** JSON equality between the importer and the manifest (with absent ≡ empty-object equivalence to match upstream's `?? {}` coercion). 4. **Per-field name-set + specifier match.** Catches same-name-same-specifier moves between fields the flat-record diff doesn't see (e.g. `react` moved from `dependencies` to `devDependencies`). Applies upstream's **precedence rule** (`optional` > `prod` > `dev`): a dep that appears in a higher-precedence manifest field is filtered out of the lower-precedence field's check, so a manifest listing the same dep in both prod and dev still satisfies a lockfile that records it under prod only. Reasons are surfaced as typed `StalenessReason` variants (`SpecifiersDiffer`, `DepSpecifierMismatch`, `PublishDirectoryMismatch`, `DependenciesMetaMismatch`, `NoImporter`) so callers match on the discriminant rather than parsing format strings, and tests assert on shape rather than wording. The `SpecDiff::Display` impl handles singular/plural ("1 dependency was added" vs "2 dependencies were added") so user-facing CI output reads cleanly. ### New errors - `InstallError::OutdatedLockfile { reason: StalenessReason }` — surfaced as `ERR_PNPM_OUTDATED_LOCKFILE` (miette code `pacquet_package_manager::outdated_lockfile`). Hint points at `pnpm install --lockfile-only` to regenerate. - `InstallError::NoImporter { importer_id }` — distinguishes "lockfile file is missing" (`NoLockfile`) from "lockfile is present but has no importer entry for this project." Renders as `importers["{id}"]` for readability. ### Performance Confirmed by hyperfine on a 1352-package fixture with 110-dep manifest (warm reinstall, `--warmup 2 --runs 10`): | | mean | range | |---|---:|---:| | main (no check) | 573.5 ms | 554-603 | | **PR (with check)** | **574.7 ms** | 549-602 | Ratio 1.00 ± 0.05 — within noise. The check is pure-CPU map/set operations on string keys with no syscalls or async; ~50-200 μs for the alot7 manifest. ### Out of scope (matching the issue) - Catalogs (pacquet has no catalog support yet). - `auto-install-peers` pre-pass (pacquet has no separate auto-install-peers mode). - `excludeLinksFromLockfile` and `link:` resolutions (#431 territory). - Semver-range-satisfies check (lives in pnpm's `localTarballDepsAreUpToDate`, outside this gate). - Multi-importer support (#431 workspace install — single-importer-only here).
Description
Languages
Rust
61.8%
TypeScript
37.7%
JavaScript
0.4%