Files
pnpm/.changeset/add-command-fast-path.md
T
Zoltan Kochan f100948180 perf: let pnpm add skip resolution for an already-locked version (#13807)
pnpm remove already skips resolution when the lockfile proves the
outcome; its twin did not. `pnpm add <pkg>@<version>` re-resolved the
whole graph even when the requested version was one the lockfile
already held — promoting a transitive dependency to a direct one, or
adding to a second workspace package what a first one already depends
on.

The rule is the one pnpm/pnpm#13779 established: resolution reuses the
already-locked version it would dedupe onto rather than fetching a
higher one, because preferredVersions biases it toward what the graph
already holds. The importers handler now records a brand-new importer
edge at that version, under the group the manifest declares, alongside
the whole-new-project entry pnpm/pnpm#13803 added. It reuses that
change's guards: a range several locked versions satisfy under
`resolutionMode: time-based` or `lowest-direct` keeps the resolver,
since those pick the low end and only for a run that leaves the
manifest alone, and so does a plain range on a workspace project's
name, which may resolve to a link instead. A lockfile carrying `time`
keeps it too: `time` records a publish date per direct dependency and
is only pruned on save, never extended, so a package promoted into
that position would be missing the one a resolution records.

`installSome` cannot edit the manifests in place the way
`uninstallSome` does — the resolution path reads them to tell a new
dependency from a re-added one, and reads them again to pick the range
style it saves — so the edit is staged on copies, handed to the fast
update and its freshness gates, and the context takes it only once the
rewrite commits. The range a selector saves is computed with the
resolver's own rule, extracted as `calcVersionRange` in
`@pnpm/pkg-manifest.utils` alongside the `inferRangeSpecStyle` it needs.

pacquet needed only the gate: `add` already pins the manifest before
the install runs, so `may_fast_update_lockfile` admits `InstallSome`
and `add` stops forcing `preferFrozenLockfile` off. `update` is also
`InstallSome` and keeps its explicit opt-out, so it is unaffected. Its
manifest pin still consults the registry packument; only the
install-side resolution is skipped.

The narrow cut is a plain registry specifier. A dist tag, an alias, a
`workspace:`/`catalog:`/git/tarball specifier, `--save-peer`, a
`catalogMode` other than `manual`, an overridden package, and a version
nothing locked satisfies all keep the resolver.

Closes pnpm/pnpm#13798.
2026-08-11 14:32:55 +02:00

919 B

@pnpm/pkg-manifest.utils, @pnpm/installing.deps-installer, @pnpm/resolving.npm-resolver, pacquet, pnpm
@pnpm/pkg-manifest.utils @pnpm/installing.deps-installer @pnpm/resolving.npm-resolver pacquet pnpm
minor patch patch patch patch

pnpm add no longer re-resolves the dependency graph when pnpm-lock.yaml already holds a version satisfying the request — promoting a transitive dependency to a direct one, or adding to a second workspace package what a first one already depends on, now only saves the dependency in package.json and records its importer entry. A satisfying locked version is necessary but not sufficient: the install still falls back to a full resolution for a dist tag, an alias, a workspace:/catalog:/git/tarball specifier, --save-peer, an overridden package, a catalogMode other than manual, and — under resolutionMode: time-based or lowest-direct, which resolve a direct dependency to the low end of its range — a range several locked versions satisfy.