Pacquet's shared script environment builder treated TMPDIR as an
always-reserved stamp. Explicit pnpm run calls use unsafePerm, so no
replacement was written and the child lost the user-provided temporary
directory entirely.
Preserve inherited TMPDIR by default and continue replacing it with the
per-package node_modules/.tmp directory when unsafePerm is disabled. This
matches the TypeScript CLI and normal child-process environment inheritance.
Closespnpm/pnpm#13442.
TypeScript pnpm resolves package licenses from both the modern `license` field and the deprecated `licenses` field. Pacquet only accepted a string `license`, causing packages such as `exit`, `prettysize`, and `seq-queue` to be reported as `Unknown`.
Add a shared package-manifest extractor matching TypeScript precedence and legacy-field handling, and use it in `pnpm licenses list`. Cover the parser directly and verify the CLI result with a registry fixture.
Closespnpm/pnpm#13438.
Pacquet's licenses command scanned only the active workspace importer unless recursive mode was explicitly selected. TypeScript pnpm scans every lockfile importer by default, so large workspaces lost dependencies that were reachable only from other projects.
Collect dependencies from every importer in the default mode and filter unsupported optional package subtrees with the same declared and package-name-inferred platform constraints used during installation. Preserve dependency-group priority while walking the graph and render development-only packages with the same `(dev)` suffix as TypeScript pnpm.
Fixespnpm/pnpm#13438.
Select the filtered workspace graph before requiring a script name. When exactly one project is selected, render its scripts and the root workspace scripts through the normal listing path; retain the recursive missing-script error for larger selections.
Add an integration regression test covering the filtered command and keep coverage for the multi-project recursive error.
Fixespnpm/pnpm#13437.
Restore the integrity hash collected while resolving commit-addressed
git-hosted archives. This keeps Rust pnpm aligned with the TypeScript security
behavior that pins downloaded archive bytes in the lockfile.
Revert only the git integrity portion of pnpm/pnpm#13432; retain its peer
resolution and libc serialization changes.
Represent libc metadata as an untagged string-or-list value in the Rust lockfile model. Preserve the source manifest shape when building package metadata, while exposing a normalized slice to installability consumers.
This keeps scalar libc declarations scalar and singleton arrays as arrays, matching TypeScript pnpm's lockfile serialization.
Fixespnpm/pnpm#13435.
Synchronize every workspace importer through an initial required-peer pass
before optional peers are selected. This ensures a non-owner occurrence of a
shared subtree uses the missing-peer context published by that subtree owner,
rather than selecting a provider from an unrelated importer.
Preserve pnpm lockfile serialization conventions for singleton libc metadata
and commit-addressed git-hosted archives. These corrections make fresh n8n
lockfiles generated by pnpm 11 and Rust pnpm byte-identical.
Related to pnpm/pnpm#13305.
Add a concurrent cold-store tarball benchmark alongside the existing single-package integrity benchmark.
The workload covers many file-heavy tarballs in flight at once, matching the extraction shape that made the Rust engine slower than the TypeScript CLI on the Next.js workspace in pnpm/pnpm#13305.
Seed preferred versions from workspace package identities so optional peers can reuse local workspace versions during cross-importer resolution.
Omit empty deprecation messages from generated lockfiles. Separate dedupe's resolution-reuse policy from its preferred-version seed policy so it rebuilds dependency edges without unnecessarily replacing valid pins.
Closespnpm/pnpm#13430.
Registry and manifest metadata can contain Unicode formatting controls that visually reorder terminal output without using C0/C1 control characters. Extend the TypeScript interactive-update cell sanitizer and pacquet terminal sanitizers to remove the full Unicode Cf set while leaving the underlying identifiers unchanged.
Add parity regression tests for bidi overrides and isolates. Also make the scoped-registry routing test require only the mandatory version request because the concurrent full-packument request can be canceled when that response already provides sufficient metadata.
Closespnpm/pnpm#13356.
Pacquet prepared package.json changes before running the install resolver. For a bare new dependency, that preparation always queried the registry for latest, so a workspace-only package failed with a 404 even though the npm resolver already supported workspace fallback.
Index workspace packages during add when direct workspace linking is enabled, select the local version with the resolver's existing matching helper, and pass it through the shared workspace specifier renderer. Keep the add-specific saveWorkspaceProtocol=false behavior in the add layer so update --workspace continues to write an explicit workspace specifier.
Cover rolling, pinned, and disabled saveWorkspaceProtocol modes with an end-to-end regression test.
Closespnpm/pnpm#13336.
Exercise deleted wanted-lockfile reconstruction with an optional dependency
that is incompatible on every OS. Assert that the frozen install retains the
complete graph in the current lockfile before deleting pnpm-lock.yaml, then
verify lockfile-only reconstruction reproduces the original file.
The production fix landed with pnpm/pnpm#13350 for the same root cause as
pnpm/pnpm#13312. This test pins its reconstruction consequence.
Closespnpm/pnpm#13317
Use one resolution-aware store-index key derivation for warm lookups and side-effects uploads.
Hosted tarballs and type: git resolutions now write cache entries to the rows later installs read.
Closespnpm/pnpm#13420
A package that ships no `package.json` of its own — a `file:` or remote
tarball whose archive carries none, a directory pin restored from the
lockfile — reaches `build_pkg_id_with_patch_hash` with no manifest, so
there is no name to prefix its dep path with. The bare `file:<path>` /
URL that results parses as no `PackageKey`, and
`build_packages_and_snapshots` dropped the node with a silent
`continue`: the install reported success while writing no `packages:` /
`snapshots:` row and linking a dangling symlink into a virtual-store
directory that was never created.
pnpm fills the gap in `getManifestFromResponse`, right after resolution
and before the read-package hooks: a response with no manifest borrows
the name and version the lockfile recorded for it, or failing that the
wanted dependency's alias (its bare specifier's last path segment when
there is no alias) at version `0.0.0`. `resolve_wanted_cached` now does
the same, so every resolver's output carries a name and the dep path
becomes `<name>@file:<path>`. The lockfile is byte-identical to pnpm
11's for both a `file:` and an `https:` manifest-less tarball.
With the synthesis in place, a dep path that keys no row is
unambiguously a bug, so `build_packages_and_snapshots` returns
`UnkeyedDepPath` instead of dropping the node. `link:` deps stay exempt
— pnpm writes no row for one either. The two halves have to land
together: alone, the hardening fails closed where pnpm succeeds.
pnpm also writes a placeholder `package.json` into an extraction that
carries none, so `package.json` is every package slot's completion
marker. `apply_placeholder_manifest` does the same, leaving the
store-index row's bundled `manifest` empty so nothing mistakes the
marker for the package's identity.
Manifest-less resolutions are leaf-classified from here on, matching
pnpm, which treats a synthesized manifest as dependency-less
(`pkg.dependencies ??= {}`). The revisit regression guard moves to a
peer-only child — the remaining shape that is non-leaf while its
`children_by_id` entry stays empty.
pnpm 11 already handles manifest-less packages correctly, so this needs
no TypeScript counterpart.
Closespnpm/pnpm#13410.
The v11 store index is an on-disk contract shared between the TypeScript
CLI and pacquet, but the two stacks disagreed on `pkgId` for every
non-registry dependency: pnpm writes the bare resolution id (the tarball
URL, the git-host archive URL, the `git+…#<commit>` spec) while pacquet
wrote the lockfile-shaped `<name>@<id>`. Registry entries — the
overwhelming majority — already agreed. The cost was reuse: a store
warmed by one stack was cold for the other for every URL / git
dependency, so switching between pnpm 11 and pnpm 12 re-downloaded,
re-extracted, and re-imported all of them, and `cat-index` in one stack
could not see the other's rows.
`PkgNameVerPeer::pkg_id` now derives that id from a lockfile key, reusing
`pacquet_deps_path::try_get_package_id` (pnpm's `tryGetPackageId`), and
every store-index keying site goes through it — the install dispatcher,
the warm-key prefetch in `create_virtual_store`, the fresh-lockfile
reuse map and graph prefetch, the side-effects upload, `pnpm patch`, and
`pnpm cat-index`.
A remote tarball also occupied two rows rather than one: the resolve-time
fetch keyed its row by the bundled manifest's `name@version` while the
install pass keyed one by `name@<url>`. `FetchTarballForResolution` now
keys by the caller's `package_id`, and the tarball resolver passes the
normalized bare specifier (the id the lockfile records) rather than the
post-redirect URL, so both passes address one row.
The git fetchers hand `prepare_package` a resolution id now, so it
synthesizes the `<name>@<id>` dep path it gates on from the fetched
manifest's name — exactly what pnpm's `preparePackage` does, keeping the
`allowBuilds` identity unchanged while the store key moves.
Closespnpm/pnpm#13365
Compare the pnpmfile loaded for an install with the checksum recorded in the lockfile before using frozen resolution. Evaluate the module only when no checksum is recorded, which distinguishes a pnpmfile that exports hooks from one that does not without adding Node startup to the common matching-checksum case.
Keep optimistic repeat installs worker-free because workspace-state validation has already established that the pnpmfile is unchanged. Skip the comparison in pnpr because the resolver service has no access to the client-local pnpmfile.
Fixespnpm/pnpm#13385.
pnpm collects warnings raised while reading config and prints them with
console.warn — to stderr, outside the reporter — while pacquet routed its
one config-load warning (the ignored `pnpm` field of package.json)
through LogEvent::Global into the default reporter's stdout frame. A
script capturing stdout got the warning mixed into the command's own
output.
Add a config-warning channel (cli_args::config_warnings) that writes a
[WARN]-labelled line straight to stderr, and emit the ignored-field
warning through it. Reporter-emitted warnings stay on stdout, where the
two stacks already agree; the $-override deprecation warning keeps its
reporter routing because pnpm emits it through globalWarn, not the
config-warnings array. Future config-load warnings inherit the stderr
routing by going through the same channel.
Closespnpm/pnpm#13361
`pnpm update --interactive` sized and padded its table by counting
characters, which is only the rendered width for the ASCII most package
names and versions are made of. A cell holding wide characters — CJK,
most emoji — measures narrower than it renders, so its row's columns
stopped lining up with its neighbours'.
pacquet's hand-rolled `printable_width` gives way to
`console::measure_text_width`, which strips the SGR escapes
`colorize_target` embeds and measures the rest against the East Asian
width tables. `console` is already in `Cargo.lock` through `dialoguer`;
it is now declared in `[workspace.dependencies]`.
The TypeScript CLI lays the table out with `@zkochan/table`, which
measures with `string-width` and so already placed wide package and
workspace names correctly. Its own `getColumnWidth` did not: it sized
the two version columns with `stripVTControlCharacters(...).length`, and
a version wider than the width it computed made `@zkochan/table` reject
the cell outright — `pnpm update --interactive` aborted with "Subject
parameter value width cannot be greater than the container width"
instead of printing the list. It now measures with `string-width` too.
Closespnpm/pnpm#13357
A platform-incompatible optional dependency was dropped from
`node_modules/.pnpm/lock.yaml` entirely, so the file could never again
equal `pnpm-lock.yaml`. The frozen no-op short-circuit in `Install::run`
gates on exactly that equality, which left every repeat
`pnpm install --frozen-lockfile` on such a project taking the full
frozen path and re-running the root lifecycle scripts and every allowed
dependency build script.
`.modules.yaml.skipped` is what records an installability skip across
installs, so the current lockfile can keep the entry and still describe
the tree — which is what the TypeScript CLI's
`filterLockfileByImportersAndEngine` does. Only the transient skips (a
failed optional fetch, `--no-optional`) are left out now, since nothing
else records them and the next install has to redo that work.
Retaining the entries is safe for materialization: the per-snapshot
"unchanged since the previous install" gate in `CreateVirtualStore` also
probes the slot directory, so a package that becomes installable after a
`supportedArchitectures` change still takes the cold path.
Closespnpm/pnpm#13312
Provide the body of that commit below.
This body is for developers reading the git history, so be as detailed as the
change warrants: explain the rationale, design decisions, and trade-offs. The
user-facing release note belongs in the changeset, not here. -->
Implement specifier rewriting for named-registry and JSR dependencies on `update --latest` in pacquet.
Previously, `pacquet` failed to rewrite dependencies mapped to a named registry (or JSR) in `package.json` when running `pnpm update --latest`. This was because the resolving step returned `None` for the normalized bare specifier.
This commit:
- Introduces `calc_prefixed_specifier` in `calc_specifier.rs` to compute manifest-ready specifiers preserving the original protocol prefix and range format.
- Integrates it into JSR and named registry resolver resolution flows.
- Refactors `BuildResolveResult` in `npm_resolver.rs` to accept an optional pre-calculated `calculated_specifier: Option<String>`.
Closes https://github.com/pnpm/pnpm/issues/13393
A `file:*.tgz` dependency's name, version, and dependencies live only in
the archive's own `package.json`. The local resolver left `manifest`
unset for the tarball branch, so `build_pkg_id_with_patch_hash` had no
name to prefix and emitted the bare `file:<path>` as the dep path. That
parses as no lockfile key, and `build_packages_and_snapshots` dropped it
with a silent `continue` — the install reported success while writing no
`packages:` / `snapshots:` row and linking a dangling symlink into a
virtual-store directory that was never created.
Read the archive during resolution, as the tarball resolver already does
for remote tarballs and the git resolver for git deps: `pacquet-tarball`
grows `read_local_tarball_metadata`, which hashes the file and reads its
bundled `package.json` in one pass, replacing the resolver's hash-only
read. Nothing is written to the store — the install pass addresses a
`file:` tarball's store-index row by its `<name>@file:<path>` dep path,
not by the `<name>@<version>` a resolve-time extraction could key, so
such a row would never be read.
With the name in scope the dep path becomes `<name>@file:<path>`, both
lockfile blocks are emitted, the tarball's own dependencies are walked
(they were dropped entirely before), and the install summary prints the
version instead of the raw specifier. The lockfile is byte-identical to
pnpm 11's for the same manifest.
The manifest is borrowed out of the decompressed archive buffer rather
than read through the tar entry, so an entry header can't size an
allocation; the offset/size arithmetic both call sites need is extracted
into `tar_entry_payload`.
Reading the manifest puts its `name` into the dep path and into a
`node_modules/<name>` directory, so the shapes that used to fail
silently are refused the way pnpm refuses them: a `package.json` that
isn't valid JSON raises `ERR_PNPM_TARBALL_EXTRACT`, one that names no
package raises `ERR_PNPM_MISSING_PACKAGE_NAME`, and one whose name isn't
a valid npm name raises `ERR_PNPM_INVALID_DEPENDENCY_NAME`. Degrading to
"no manifest" is right for the extraction path, whose consumers re-read
from disk, but here the manifest is the package's only source of
identity, so clearing it reproduces the bug above.
An archive with no `package.json` at all stays tolerated, matching pnpm,
which synthesizes a name from the alias. Telling that apart from a
nameless manifest needs more than a `None`, so the reader reports
whether a root `package.json` was present.
pnpm 11 already handles `file:` tarballs correctly (it reads the manifest
from the fetch, and resolves and fetches together), so this needs no
TypeScript counterpart.
Closespnpm/pnpm#13379.
An aliased `jsr:` or named-registry dependency (`"bar-from-jsr":
"jsr:@pnpm-e2e/bar@1.0.0"`) resolved and installed, but never reached
the lockfile importer. Those resolvers stamp the protocol's own package
name on the result as its alias, and the tree builder let that name win
over the manifest key. `build_importer` only records aliases the
manifest declares, so the edge was dropped and the importer came out
empty.
The TypeScript CLI reads the same resolver output the other way around
(`wantedDependency.alias ?? pkgResponse.body.alias ?? pkg.name`), so
this applies that precedence in the tree builder, shared by the fresh
and the lockfile-reuse node paths. The resolver's alias still names the
edges that carry no manifest key of their own, such as
`pnpm add jsr:@pnpm-e2e/bar`.
The two `jsr with alias` ports lose their `known_failures` gate, and the
named-registry shape gets its own CLI test; its expected lockfile entry
was taken from a TypeScript-CLI run against the same manifest.
TypeScript needs no change: it already records both shapes correctly.
Closespnpm/pnpm#13362
The npm resolution verifier implemented three of its four violation
codes: a lockfile entry whose tarball resolution records no `integrity`
sailed through the verification gate — which had just printed its green
check for that same lockfile — and was only refused later, one entry at
a time, by the fetch path's `tarball_url_and_integrity` backstop.
Add the missing `MISSING_TARBALL_INTEGRITY` rule so the gate reports
every offending entry in one batched `ERR_PNPM_MISSING_TARBALL_INTEGRITY`
error before anything is downloaded, matching
`createNpmResolutionVerifier` in the TypeScript CLI. The check needs no
network, so it runs before the registry-metadata shortcuts and applies to
URL-keyed entries too, whose non-semver version otherwise skips the
registry policies. `integrityRequired` joins the policy snapshot so a
cache record written before the rule existed can't stat-fast-path around
it.
Two shapes that could have bought the exemption are closed along the
way. `integrity: ''` parses into an SRI with zero hashes, which pins
nothing and makes `Integrity::check` panic on `hashes[0]`; the new
`LockfileResolution::checkable_integrity` reports it as absent, both here
and in the fetch-path backstop, matching pnpm's `getExpectedIntegrity`.
And the git-host archive exemption is now read from the tarball URL alone
rather than the lockfile's own `gitHosted` marker, so an edited entry
can't claim it for an arbitrary URL — the same rule
`isRegistryTarballResolution` and `unverified_fetch_is_allowed` apply.
The fetch-path refusal stays as the backstop for entries that reach a
fetch without having passed the gate, as pnpm keeps
`assertFetchableResolution` for the same reason.
Closespnpm/pnpm#13364
Investigating pnpm/pnpm#13326 showed its reproduction already matches
pnpm 11 byte for byte, fixed by eab73b3994 (pnpm/pnpm#13374) filtering
bundled names out of `extract_children`. Porting the upstream
`bundledDependencies` install suite — which had no pacquet counterpart,
and is why the original defect shipped — surfaced two gaps that fix did
not cover.
A manifest may declare `bundleDependencies: true` instead of a name
list, which npm and pnpm both read as "every entry of `dependencies`
ships inside this tarball". The lockfile writer only understood the list
form, and `PackageMetadata::bundled_dependencies` was typed
`Option<Vec<String>>`, so the boolean could neither be written nor read
back — a `pnpm-lock.yaml` written by pnpm 11 for such a package failed
to parse. Model the field as the `string[] | boolean` union pnpm
records, behind one shared `BundledDependencies::from_manifest` that
both the install and env-installer writers call.
The field's presence is also what tells the installer a package carries
a populated `node_modules` of its own whose bins belong in that
package's `node_modules/.bin`. Bundled packages are not lockfile
children, so neither linker reached them: the isolated path's per-slot
walk iterates `snapshot.dependencies`, and the hoisted path iterates
graph nodes — `has_bundled_dependencies` was computed and stored on
every hoisted node but had no reader. Both paths now run the second bin
pass upstream's `linkAllBins` and `building/during-install` run.
No platform gate is added to the resolver. The issue asks for one, but
pnpm 11 resolves the subtree of a platform-incompatible optional package
too (`dmg-license@1.0.11` on Linux, `@img/sharp-darwin-arm64`'s libvips
edge), and `skip_optional_dependency_that_does_not_support_the_current_os`
already asserts pacquet matches that.
Related to pnpm/pnpm#13326
pacquet never ran the root project's `pnpm:devPreinstall` script. The
hook exists so a workspace can prepare state that resolution or linking
consumes: vercel/next.js uses it to generate a placeholder `next` bin,
and without it every dependent's shim was linked against a file that no
step of the install ever created.
Run it once per install from the lockfile directory, for the root
project only, right after `pnpm:context` and before the lockfile is
touched — including before the frozen path's freshness check, matching
where pnpm runs it at the top of `mutateModules`' `_install`.
The gates were each checked against pnpm 11.17.0 on the same fixture:
- `--ignore-scripts` skips it, as it does every other project script.
- `--lockfile-only` and `--dry-run` skip it. pnpm reaches the same
outcome indirectly: `lockfileOnly` forces `ignoreScripts`, and
`--dry-run` sets `lockfileOnly`.
- A repeat "Already up to date" install skips it in both stacks — the
optimistic short-circuit returns before pnpm's `_install` and before
this call site.
- A rebuild skips it: `pnpm rebuild` never enters `mutateModules`.
- `ignore_manifest_check` skips it, covering both `pacquet fetch`
(pnpm's `ignorePackageManifest`) and the TypeScript CLI delegating a
frozen materialization, which already ran the hook itself before
handing the install over.
One duplicate remains: the TypeScript CLI's resolve-mode delegation
(`runPacquet.run({ resolve: true })`) drops both injected flags, so it
runs the hook and pacquet then runs it again. Every way to suppress it
was worse than the symptom — restating a ten-clause delegation
predicate in `_install`, where it would drift, or inventing a private
env-var marker — and the hook is idempotent by contract.
The env setup shared with the project-script path is split in two:
`pnpm:devPreinstall` takes the plain `extraEnv` + `nodeOptions`, without
the `.package-map.json` `NODE_OPTIONS` entry, because pnpm adds that one
only once it links and builds and the file does not exist yet when the
hook runs.
Closespnpm/pnpm#13313
An `overrides` value of `$foo` means "whatever specifier the root
manifest declares for `foo`". pacquet copied the raw map onto
`Config::overrides` instead, so the reference reached the read-package
hook, `pnpm-lock.yaml#overrides`, and the lockfile freshness check
verbatim. A lockfile written by pnpm 11 — which records the resolved
specifier — then failed a frozen install with ERR_PNPM_OUTDATED_LOCKFILE
(`overrides` in the lockfile `{"is-odd":"3.0.1"}` vs the config's
`{"is-odd":"$is-odd"}`), which is what blocked validating vitejs/vite.
Resolve the references while config is read: `pnpm-workspace.yaml` is
the only source that can set `overrides` (the global config.yaml is
stripped of the key and no `PNPM_CONFIG_*` var carries a map), and the
cascade knows the workspace root at that point, so the root manifest's
`dependencies` / `devDependencies` / `optionalDependencies` are in
reach. Every consumer downstream of `Config` — including the
`.modules.yaml` settings record — therefore sees the concrete
specifier, as it does in pnpm 11.
A reference to a package that is not a direct dependency fails with
ERR_PNPM_CANNOT_RESOLVE_OVERRIDE_VERSION, and the install family warns
about the deprecated syntax and points at catalogs, both matching
pnpm 11's wording.
TypeScript CLI: no change needed, it already resolves the references.
Closespnpm/pnpm#13314
The final-graph rebuild rewrote a resolved-peer edge whenever the
provider's depPath carried a peer segment the consumer had not resolved
itself: it looked for a variant of the provider without those segments,
and fabricated one when none existed. Upstream does no such thing —
`resolveChildren` maps every `childrenNodeIds` entry through
`pathsByNodeId`, so an edge always names the instance the provider node
resolved to.
That rewrite is what was left of pnpm/pnpm#13320 on vercel/next.js. The
workspace resolves two `eslint-plugin-import` versions, and the single
shared `eslint-module-utils` instance had its
`eslint-import-resolver-typescript` edge trimmed to a fabricated
variant, which the peer-dependent dedupe then collapsed into the *other*
importer's resolver instance. The lockfile came out with the peer bound
to the wrong instance and an orphan `snapshots:` entry nothing
referenced.
Dropping the rewrite makes a full re-resolution of vercel/next.js
byte-identical to the TypeScript CLI's lockfile, and narrows the
separate n8n divergence (pnpm/pnpm#13322) from 1371 to 1251 lines.
`NodeRecord::peer_edges` only existed to classify edges for the rewrite,
so it goes with it.
Closespnpm/pnpm#13320.
pnpm 12 rejected `pnpm peers check` — the spelling pnpm.io documents and
pnpm's own dedupe output recommends — as an unexpected argument, and its
`dedupe --check` failed with a bare "Lockfile would be modified by
deduplication": no diff, no error code, no peer-dependency warning.
`peers` now takes an optional subcommand: `check` (or nothing) runs the
check, anything else prints the command's help and exits 1, mirroring the
TypeScript handler.
`dedupe --check` now renders what deduplication would rewrite, under pnpm's
`ERR_PNPM_DEDUPE_CHECK_ISSUES`. The report reuses the lockfile diff behind
`install --dry-run`, which grew per-alias detail for updated `snapshots:`
entries and a key selector: `--dry-run` keeps comparing importers by
manifest specifier, while `--check` compares them by resolved version,
because deduplication rewrites peer-resolved versions without touching
specifiers. Rendering goes through the archy tree renderer `list` and `why`
already share, so the output matches pnpm's `renderDedupeCheckIssues`. A
snapshot that no longer parses (an older lockfile format the run has just
rewritten) degrades to no baseline rather than replacing the verdict with a
parse error, and a text-only rewrite — drifted settings, a config
dependency the run synced — says so instead of printing an empty report.
Dedupe also emits pnpm's `Issues with peer dependencies found. Run "pnpm
peers check" to list them.` warning. It is derived from the deduplicated
lockfile through the same checker and `peerDependencyRules` filtering the
`peers` command uses, so the warning and the command it recommends always
agree, and it is gated like pnpm's install-time one: a bad peer, or a
missing peer at least one parent requires non-optionally. `pnpm install`
still does not warn — unlike dedupe it can finish without resolving, so
that warning has to come from the resolver rather than a lockfile walk.
The `licenses list` half of the issue was already fixed on main by
pnpm/pnpm#13347.
TypeScript CLI: no counterpart change — pnpm 11 already behaves this way.
Closespnpm/pnpm#13321
Installing a `file:` directory dependency with the global virtual store
enabled crashed the headless install:
TypeError: Cannot read properties of undefined (reading 'split')
at assertNoPathTraversal
at formatGlobalVirtualStorePath
at calcGraphNodeHash
pnpm omits `version` from a directory snapshot, so `nameVerFromPkgSnapshot`
hands the hasher `undefined` and the traversal guard added in
pnpm/pnpm#12872 dereferences it. Directory snapshots now use a fixed
`directory` segment in place of the version. That also settles a
disagreement between the two install paths: the resolver reads the version
off the manifest and the headless install off the lockfile, where there is
none, so the same package landed on two different slots.
The crash hid a worse defect. A directory resolution is the one resolution
with no integrity — it is a path relative to the lockfile — so `file:dep`
hashed identically in every project that depended on a directory of that
name, and all of them linked to whichever project installed first. Since
the source directory is mutable, pnpm re-imports it on every install, so
the projects went on overwriting each other's copy. The lockfile directory
now joins the hash payload for directory resolutions, giving each project
its own slot; every other resolution hashes exactly as before.
`iterateHashedGraphNodes` takes an options object now — it was up to five
positional parameters before this change added a sixth.
Closespnpm/pnpm#13335
`update --latest` writes back whatever the resolver reports as the
dependency's normalized bare specifier, and writes nothing when the
resolver reports none — so specifiers no resolver claims are left alone.
The JSR path never reported one, so a `jsr:` dependency always landed in
that "unclaimed" case and kept its old version.
Reporting the npm-shaped range `calc_specifier` builds would have been
wrong: it would rewrite the entry into a registry dependency. Render the
picked version back under the protocol instead, through a new
`calc_prefixed_specifier` mirroring the TypeScript resolver's
`calcPrefixedSpecifier` — `jsr:<range>` when the dependency installs
under the package's own name, `jsr:<pkg name>@<range>` when it is
aliased, keeping the range operator the entry already declared.
`BuildResolveResult` now carries the rendered specifier rather than the
inputs to render it, since which shape to render is the calling
resolver's knowledge, not `build_resolve_result`'s.
The named-registry resolver has the same gap and is tracked separately
in pnpm/pnpm#13393.
TypeScript-side: no change needed, `resolveJsr` already calls
`calcPrefixedSpecifier`.
Closespnpm/pnpm#13363
pacquet decided which projects fire their own preinstall / install /
postinstall / prepare from one `is_full_install` flag: every project the
run materialized ran them for a selector-less `install` or `update`, and
no project ran them for `add` or a targeted `update <pkg>`.
pnpm decides per project, from the mutated-importer list its command
layer builds — the projects the command was pointed at, plus the
workspace root, which the recursive dispatch pushes in as a plain
`mutation: 'install'` whenever the selection leaves it out. A project
runs its scripts when that list covers only part of the workspace, or,
when it covers the whole workspace, when its own mutation is a full
install. The rule was read off the TypeScript CLI by running it, not off
its source: 17 invocations across four workspace shapes, each project
stamping a file from `postinstall`.
Replace the flag with `ProjectMutation`, which names pnpm's mutation kind
(`InstallWorkspace`, `InstallSelected`, `InstallSome`, `NoInstall`), and
select the projects from it. `ProjectMutation::is_full_install()` keeps
the old flag's other four uses — the scope log, the optimistic
repeat-install gate, the resolution-skipped message and the
`--no-optional` exclusion — unchanged.
What changes for users: `update <pkg>` and `add <pkg>` in a workspace
stop skipping the workspace root's scripts, `update` at a workspace root
stops running the other members' scripts, and `update --latest` stops
running the project's own scripts (it rewrites named dependency specs,
so pnpm makes it `installSome` like `update <pkg>`).
The TypeScript CLI is unchanged; it gets the same matrix as e2e tests so
the contract is pinned on both sides.
Closespnpm/pnpm#13358.
A workspace-root dependency declared with link:, file:, or the path form
of workspace: names a directory relative to the root, so hoisting the
specifier verbatim re-resolved it against the directory of the importer
the peer was hoisted into: a root `link:ajv-local` reached pkg/ajv-local
and left a dangling symlink there.
Both stacks now substitute the linked package's own version for its path.
The root keeps the authority over the peer that it has when it declares
the package with a version range — a sibling's newer copy no longer wins
over the version the root pins — and the peer resolves to the same
package from every importer. The manifest is read from disk rather than
taken from the resolved dependency, which a linked dep reused from the
lockfile does not have; reading it keeps a repeat install hoisting what a
fresh install of the same manifest hoists. A target with no manifest to
read (a file: tarball, a path that does not exist) or no valid version in
it is not a candidate, and the peer falls through to the ordinary
auto-install path.
A workspace: range needs none of this: it selects the same workspace
package from every importer, so it is hoisted as-is. pacquet previously
dropped it and resolved a second copy off the registry for a peer the
root already provided from the workspace.
The same candidate list bounds optional-peer hoisting, so
getHoistableOptionalPeers picks up the rule with no change of its own.
Closespnpm/pnpm#13373.
The Pacquet Code Coverage job has been failing on main and on every PR
with `System.IO.IOException: No space left on device`, thrown while the
runner worker writes its own diagnostic log — so the run reports a
failure with no step output at all.
The job compiles the whole workspace with coverage instrumentation and
`--all-targets`, keeps a profraw file per test process for all 7167
tests, and then merges them, but it never dropped the preinstalled
toolchains the way `Rust CI / Test`, `Test` and `build-pnpr` do. Reclaim
the same ~25 GB before anything is compiled, and build the instrumented
artifacts with line-tables-only debug info: llvm-cov reads its regions
from the binary's `__llvm_covmap`/`__llvm_covfun` sections, so the DWARF
that dominates the debug build buys the report nothing.
Also report the free disk after the run and cap the job at 60 minutes,
so the next disk regression shows up as a number instead of a silent
worker death.
pnpm/pnpm#13351 reported that a workspace-root dependency installed from
git under an alias could not satisfy another importer's peer of the
package's real name, so `resolvePeersFromWorkspaceRoot` silently resolved
a second copy from the registry. It does not reproduce: both git shapes
(a `type: git` repository and a `gitHosted: true` host archive) dedupe
onto the root's dependency, and the TypeScript CLI produces a
byte-identical lockfile for the same fixture.
The report read `build_resolve_result` in `git_resolver.rs`, which sets
`name_ver: None` and `manifest: None`, and concluded a git resolution
reaches the hoist loop unnamed. `resolve_impl` then calls
`read_package_metadata`, which fills `manifest` from the package's own
`package.json` — a host archive is downloaded and read, a repository with
no archive endpoint gets a throwaway checkout — so `resolved_pkg_name`
names the dependency before the hoist loop ever sees it. The bare shape
belongs to the no-fetch-context path used by unit tests and the
resolve-only NAPI entry point, neither of which hoists peers. Lockfile
reuse is named too: `synthesize_reused_result` reconstructs the manifest
from the recorded snapshot key.
The doc on `resolved_pkg_name` claimed the opposite, and is what the
report was reading; it now describes what the resolvers actually deliver.
The new end-to-end test pins the behavior against a local git repository
whose package name differs from the alias the root installs it under; it
was verified to fail — with the duplicate registry copy the issue
describes — when `resolved_pkg_name`'s manifest fallback is removed.
Closespnpm/pnpm#13351.
The required-peer picker accepted a prerelease for any range whose base
version accepted it, by stripping the prerelease tag and retrying. That
is not what `semver.maxSatisfying(versions, range, { includePrerelease:
true })` does in the TypeScript CLI, so a `30.0.0-alpha.6` in the graph
was auto-installed for a peer declared `^29.0.0 || ^30.0.0`, which
rejects it. pnpm 11 falls through to hoisting the range and resolves a
stable version from the registry instead.
npm's option does two separate things: it lifts the rule that a
prerelease is eligible only for comparators carrying a prerelease of the
same `major.minor.patch`, and it lowers to `-0` those bounds npm itself
synthesized for an omitted component. `^18.x` therefore starts at
`18.0.0-0` and accepts `18.0.0-rc.1`, while the spelled-out `^18.0.0`
starts at `18.0.0` and does not.
`node-semver`'s `Range::satisfies` bakes in the eligibility rule and
takes no options, and its parser drops the partial/spelled-out
distinction, so `IncludePrereleaseRange` evaluates each comparator on its
own: endpoints come from `node-semver`, membership goes through
`Range::allows_any` against the single-version range (overlap with one
point is the endpoint test without the eligibility rule), and a
comparator whose lower bound npm synthesized also admits the candidate's
release. `<18` and `<=18` are rewritten first, `node-semver` reading them
as `<18.0.0` and `<=18.0.0-0` where npm reads `<18.0.0-0` and
`<19.0.0-0`.
Expectations in the new test matrix are npm's own answers.
The lockfile-reuse gate shared the base-strip helper by convenience
rather than by design; it keeps its behavior under the name the pattern
carries elsewhere in the crate.
Closespnpm/pnpm#13341
Close the four CLI-surface differences left over from pnpm/pnpm#13315
after pnpm/pnpm#13359 and pnpm/pnpm#13375 landed the other two.
Three land in the Rust engine. The `Scope:` line arrives via a new
`pnpm:scope` channel and a port of `reportScope.ts`; pnpm gates it on the
command being one that reports scope *and* the run being workspace-wide,
and since the engine's recursive-by-default set is narrower than pnpm's,
that conjunction is computed at dispatch and seeded into the reporter.
The event is emitted where the counts already exist — the `--filter`
resolution and the installer's workspace walk — so no install pays for a
second directory walk. A `sharedWorkspaceLockfile: false` plan reports
once for the whole selection rather than once per child install, which
would otherwise overwrite the filtered count with the workspace total.
The second is the `allowBuilds` placeholder an install writes for a build
it blocked, which pnpm/pnpm#13375 taught the engine to read but not yet to
write. The third is the elapsed time on a cache-satisfied lockfile
verdict.
The fourth goes the other way. The store block a first install prints is
unreachable in the TypeScript CLI: `reportContext` needs
`pnpm:package-import-method`, which only `@pnpm/fs.indexed-pkg-importer`
emits, and that runs inside a `@pnpm/worker` thread whose logger never
reaches the reporter. The main process already receives the import method
back from the worker, so it is reported from there instead.
Reporter globals are now seeded before the pre-command checks. They are
`OnceLock`s and the pre-command checks emit, so whatever fired first was
locking in unset values.
Closespnpm/pnpm#13315
Two peer-context divergences from pnpm, both in the resolver's tree walk.
pnpm drops a resolved package's `dependencies` entry when the package
also declares that name as a peer and the name is already resolvable
from the level the package resolved in (`options.parentPkgAliases`) —
always under `autoInstallPeers`, and otherwise only for the names in
scope. pacquet modeled the first arm only, so with `autoInstallPeers`
off it nested a copy of the dependency instead of resolving the peer
from the parent. The scope is now threaded down the walk the way the
preferred-versions overlay already is, chained per level rather than
copied. Which names a package sheds is a property of the occurrence
while its children and peers are shared per `pkgIdWithPatchHash`, so
the deterministic children owner decides for the id — pnpm lets the
first occurrence to resolve decide, which is arrival-ordered.
pnpm also rewrites every child edge that points at a collapsed peer
variant. pacquet gated that on `collapsed_target_matches_parent`, a
guard with no upstream counterpart added in pnpm/pnpm#12372 to
compensate for a peer-resolution divergence (pnpm/pnpm#12330), which
left a collapsed variant reachable from a consumer whose own peer
suffix named it. That divergence is gone — the regression coverage from
that PR passes without the guard — so the guard and the dep-path
peer-name parsing it needed are dropped.
Astro's lockfile is now 59 diff lines from pnpm 11.17.0's, down from
124. What is left is one `sharp` × `@types/node` peer-version pick that
diverges without these changes too, and pnpm/pnpm#13379.
Closespnpm/pnpm#13334
The Rust CLI is a workspace project named `pacquet` that publishes as `pnpm`,
because the TypeScript CLI already owns that name in the workspace. The rename
happened in a CI shell step that rewrote the manifest just before publish, so
nothing in the repo could see it — and the release tooling, which keys parked
changelog sections, the ledger, and intents by the manifest name, looked the
renamed package up under a name the registry does not have. Two consequences:
the composed changelog was never found, so the published tarballs carry none at
all, and the publication check could never confirm a section, so sections and
their consumed intents accumulated instead of being collected.
Let the manifest state the rename instead. `name` joins the publishConfig
whitelist in both stacks, so a project can declare the name it publishes under
while keeping its workspace identity. Pack reads the name back off the publish
manifest so the tarball filename, the packed manifest, and the registry
metadata name one artifact, and the changelog paths now separate the two
identities: the parked section is still keyed by the manifest name, which the
ledger joins on, while the registry probe, the previous-changelog lookup, and
the composed changelog's title all use the published one.
The pacquet wrapper declares `publishConfig.name` and the generator no longer
rewrites the name. `@pnpm/exe`, generated by copying that manifest, strips the
inherited rename so it does not publish itself as `pnpm`.
Everything else that addresses the registry follows the same rule: the
unpublished-dirs probe behind `pnpm change status` and `pnpm version -r`, and
both the already-published check and the registry selection of
`pnpm publish -r`. Pack validates the rename before it reaches the tarball
filename, which the manifest-name check never covered.
Refs pnpm/pnpm#13345.
`getOutdatedLockfileSetting` names eleven fields whose drift makes the
lockfile unreproducible; pacquet compared seven. A project that flipped
`autoInstallPeers`, `dedupePeers`, or `excludeLinksFromLockfile` after
writing its lockfile got a clean `--frozen-lockfile` install from
pacquet and `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` from pnpm — the install
proceeded against a lockfile whose recorded resolution no longer
matched the settings that produced it.
Each comparison follows pnpm's: `autoInstallPeers` and
`excludeLinksFromLockfile` are checked only when the lockfile records a
`settings` block, since one written before the field says nothing about
it, while `dedupePeers` is written only while it is on, so an absent key
reads as `false`.
The fields are now compared in pnpm's order too. Only the first drifted
field is reported, so the order decides which name a user sees, and
`injectWorkspacePackages` sat where pnpm checks `autoInstallPeers`.
`pnpmfileChecksum`, the eleventh field, is still unchecked: it needs the
pnpmfile's hooks gate, which means loading the pnpmfile through the Node
worker on a path that deliberately avoids it. Filed as pnpm/pnpm#13385.
`check_lockfile_settings` takes the same options struct its catalog-aware
twin took, and the twin is gone — the two had drifted into different
field sets, which is what let the new fields be added to one and missed
at the other's call site.
Closes the error-code half of item 1 of pnpm/pnpm#13315.
pnpm's pre-command block calls syncEnvLockfile for every command that isn't
in skipPackageManagerCheckForCommand, so a project pinning pnpm through
devEngines.packageManager (or a v12+ packageManager field) gets its
packageManagerDependencies recorded whatever the user ran. pacquet synced
only from the install-family pipelines, so `pnpm list` in a fresh clone left
no lockfile behind.
pre_command_plan now returns a PreCommandPlan — either the version switch it
already planned, or the env-lockfile sync it needs — and lib.rs runs whichever
came back on the pre-command runtime. The sync is planned in the same two
places pnpm reaches syncEnvLockfile: after checkPackageManager on the
warn/error/corepack path, and on the download path when the running pnpm
already satisfies the pin (where pnpm's switchCliVersion still resolves and
saves before returning).
The install family is excluded, because it passes its own frozen-lockfile
value to the sync and pacquet — unlike pnpm — fails a frozen install whose
packageManagerDependencies are out of date. Syncing those commands from the
pre-command block would repair the lockfile before the pipeline could refuse
to. Excluding them also keeps the up-to-date install fast path free of the
lockfile read.
The plan is skipped without touching the network whenever the env lockfile
already records a version satisfying the pin. That answer needs the lockfile,
but the download path has already read it looking for a version to switch to,
and `pnpm-lock.yaml` is read whole — so the parsed lockfile is threaded into
the sync decision rather than read a second time. A warm pinned project pays
one lockfile read per command, the same as before.
package_manager_to_sync now takes the effective pmOnFail rather than reading
the manifest's own onFail, matching how pnpm resolves
wantedPackageManager.onFail before shouldPersistLockfile sees it.
Closespnpm/pnpm#13348
A `file:` specifier is relative to the directory of the manifest that
declares it — pnpm's `parentPkg.rootDir`. The tree walk threaded the
importer's `project_dir` through every depth, so a `file:` dependency of
a package installed from a local directory was resolved against the
importer instead. A `file:../child` in `packages/license/parent` landed
on `packages/child` and the install failed with
`ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND`.
The children walk now derives the declaring package's directory from its
directory resolution (relative to the lockfile root, absolute for
`link:`-shaped ones) and hands it to each `file:` child's resolve
options. It joins the per-wanted dedup key through
`project_relative_cache_scope`, so two parents that declare the same
relative specifier no longer share a resolution. The speculative
children warm-up applies the same override, both to keep it from
resolving a path that doesn't exist and so it warms the key the real
resolve reads back.
Once resolution was correct, the snapshot the lockfile writes for such a
dependency still diverged: pacquet wrote `<name>@file:<path>` where pnpm
writes the bare `file:<path>` whenever the alias equals the package's own
name. The importer side already stripped that self-alias; the strip is
now shared with the snapshot side, so a workspace with nested `file:`
deps (vite's license fixtures) round-trips through `pnpm dedupe --check`
against a pnpm-written lockfile.
pnpm 11 already resolves these correctly and covers the shape with its
`deep local` test, so no TypeScript change is needed.
Closespnpm/pnpm#13323
The hoist rounds ran on every install, so a missing peer that some
version elsewhere in the workspace could satisfy was installed even with
`autoInstallPeers: false` and `dedupePeerDependents: false` — the
dependent then carried a peer context the TypeScript CLI never writes.
Upstream derives one flag from the two settings —
`hoistPeers = autoInstallPeers || dedupePeerDependents` in
`installing/deps-resolver/src/resolveDependencyTree.ts` — and
`resolveRootDependencies` returns before the hoist loop when it is
false: hoisting a peer nobody asked to install only pays off if
peer-suffixed variants are being collapsed. `ImporterHoistState` now
carries the same derived flag and both rounds no-op when it is unset,
which leaves the peer missing and the dependent's snapshot free of a
peer suffix. The final peer pass is untouched, so unmet peers are still
reported.
Verified against the TypeScript CLI on a two-project workspace: all four
`autoInstallPeers` x `dedupePeerDependents` combinations now produce
byte-identical lockfiles, where the both-off combination previously
diverged.
Follow-up to pnpm/pnpm#13372.
`Path::exists` follows the link, so a dangling `peer-c` link in pkg-a's
`node_modules` would have read as "not linked" and passed the check;
`symlink_metadata` fails only when nothing is there at all.
The importer snapshot moves from an unconditional `dbg!` into the
assertion message, so it is printed when a check fails rather than on
every run.
Follow-up to pnpm/pnpm#13372, which merged before this commit landed.
`DirLock::acquire` folded a failure to write the owner record into
`Ok(None)`, which is the same answer it gives when another process holds
the lock. Those are different situations: one is the wait doing its job,
the other means the locking mechanism itself is broken and the caller is
about to run without the lock and without knowing why. That matters most
at the engine install, where the lock exists to keep concurrent
destructive installs out of a shared store slot.
The write failure now propagates as `Err`, alongside the parent-directory
failure that already did. The engine-install caller still proceeds — a
lock it cannot take is not a reason to refuse to run — but logs what
happened rather than silently reverting to the racy behavior.
Follow-up to pnpm/pnpm#13375, raised in review there after it merged.
The warning parsed `package.json` with a bare `serde_json::from_slice`,
which chokes on the UTF-8 BOM an editor may leave at the head of the
file — pnpm strips it and warns, pacquet stayed silent. It also read and
parsed the manifest a second time, immediately before
`package_manager_to_sync` read and parsed the very same file.
Both follow from the helper doing its own IO. It now takes the parsed
manifest, so `derive_config_root_and_package_manager_to_sync` reads the
file once through `read_manifest_json` (the crate's BOM-stripping
reader) and hands it to the warning and to the package-manager policy
alike. `package_manager_to_sync` no longer reads anything, which leaves
it infallible.
The up-to-date fast path keeps its own read, one directory removed from
the manifest it already holds, and goes through the same reader via
`warn_ignored_pnpm_manifest_fields_in`: the two paths coincide only by
an invariant that lives in another function.
Follow-up to pnpm/pnpm#13359, whose changeset is still pending, so the
fix ships with the feature.
A well-formed semver range that the registry publishes nothing for was
folded into `Ok(None)`, so the resolver chain ran out and reported
`ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER` — "pnpm doesn't understand
this specifier", which is the wrong diagnosis and drops the latest
release, the other dist-tags, and the `pnpm view <pkg> versions` pointer.
The npm, jsr, and named-registry paths now raise `NoMatchingVersionError`
(`ERR_PNPM_NO_MATCHING_VERSION`) with that appendix, the way
`pickFromSimpleRegistry` and `resolveNpm` do upstream.
A non-2xx registry answer reached the user as raw transport text with no
code at all. Metadata responses now become `RegistryResponseError`
(`ERR_PNPM_FETCH_<status>`) carrying pnpm's `GET <url>: <reason> -
<status>` message, the "not in the npm registry, or you have no
permission to fetch it" hint, and — for 401/403/404, since a private
registry often answers a permission failure with a 404 — which
authorization header was sent, masked.
`ResolveError` erases a resolver failure to `Box<dyn Error>`, dropping
the `Diagnostic` facet the codes live on, so both types are recovered by
downcast in the tree walker and re-surfaced through transparent variants.
Optional dependencies keep skipping on either of them.
The report renderer also folds a cause whose message its wrapper already
quotes in full, so "Failed to resolve dependency tree: <msg>" no longer
prints <msg> again on the line below.
Closespnpm/pnpm#13319
pnpm 10 moved the install settings out of package.json's `pnpm` field
into pnpm-workspace.yaml, and pnpm warns about every migrated key a
manifest still declares so the setting is not silently dropped. pacquet
ignored the field without a word, so a project that had not migrated its
`pnpm.overrides` met only the downstream symptom.
`legacy_pnpm_field` already carried the port — the same 19-key migrated
list as config/reader/src/index.ts, the same message, and unit tests —
but was never declared as a module, so nothing compiled or called it.
The emit sits in `derive_config_root_and_package_manager_to_sync`, which
every install-family command already calls to resolve its root and which
computes the same directory pnpm reads the root manifest from. One site
covers all ten commands, and it runs before any install output so the
warning lands first, as in pnpm. The repeat-install fast path finishes
before that site is reached, so it warns from its deciding branch too.
Closes item 1 of pnpm/pnpm#13315.
Two peer-hoisting fixes, in both the TypeScript CLI and pacquet.
overrides now govern peers pnpm auto-installs. Overrides are applied by
the read-package hook, which only sees manifests; a peer nobody declares
never passes through one, so it resolved against its declared peer range
and could introduce a second copy of the package the override pinned.
The required-peer picker now consults the override set first. The
per-edge lookup that backs the hook is factored out and shared, so the
two paths cannot drift: hooks/read-package-hook exports
createDependencyOverrider, and pacquet's VersionsOverrider grows
override_for_undeclared_dependency. Parent-scoped overrides cannot apply
(no parent manifest), and a convergence override still applies only when
its version satisfies the edge's range. The convergence collector does
not see these edges — a range no manifest declares would skew the
staleness verdict. An override redirects a hoist rather than
creating one: without autoInstallPeers, a peer neither the graph nor the
workspace root provides stays uninstalled. The overrider is handed the
directory of the importer the peer is hoisted into, so a link: / file:
target stays relative to it.
getHoistableOptionalPeers now bounds its candidates by the workspace
root's own specifier for the package, the same input hoistPeers
short-circuits on. It previously maximised over every version in the
graph, so an optional peer declared as `*` — the shape
peerDependenciesMeta produces — pulled the newest version any importer
had resolved into a sibling that declares nothing, next to the version
the root pins. The alias-then-package-name precedence for finding that
root dependency is now one helper shared by both pickers, and the root's
specifier is read through getPeerVersionRange so a workspace: range or an
npm: alias bounds the candidates as well.
Measured on vercel/next.js: the lockfile diff between the two stacks
drops from 718 to 138 lines, the two stacks' resolved package key sets
and every importer block now match, and the only remaining difference is
three snapshot instances pacquet does not deduplicate.
Related to pnpm/pnpm#13320.
Resolving the Astro workspace with pacquet produced a `pnpm-lock.yaml`
that differed from pnpm 11's by 306 lines. Two independent bugs account
for 143 of them.
`dedupe_injected_deps` splits a resolved package id into its name and
its `file:` payload on the first `@`. A scoped id
(`@test/pkg@file:packages/…`) starts with `@`, so the split returned the
scope tail and the `file:` strip failed — the injected workspace dep was
never recognized and stayed a `file:` snapshot where pnpm records
`link:`. Split on the whole `@file:` separator instead.
`extract_children` walked every entry of `dependencies` and
`optionalDependencies`, including the ones the manifest declares as
bundled. npm ships those inside the package's own tarball, and pnpm's
`getNonDevWantedDependencies` filters them out before resolution; the
missing filter pulled extra packages into the lockfile (`napi-wasm`
under `@parcel/watcher-wasm`, the whole `@napi-rs`/`@emnapi` set under
`@tailwindcss/oxide-wasm32-wasi`). Both spellings are honored, and
`true` means every entry of `dependencies`, matching npm. The bundled
set is computed once and shared by both `collect_deps` calls, so an
alias declared in both groups is dropped from both — upstream filters
the merged `{...optionalDependencies, ...dependencies}` map and reaches
the same result. The alias validation still runs over bundled entries
so an unusable alias is rejected either way, as it is upstream.
A third divergence from the same reproduction — a project's own
`peerDependencies` reaching its lockfile importer entry without
`autoInstallPeers` — landed separately as pnpm/pnpm#13372 while this was
in review, so it is no longer part of this change.
The remaining divergences — the `parentPkgAliases` arm of upstream's
peer-shadowed-dependency omission, and local `file:*.tgz` tarballs
resolving without a manifest — are described in the issue; both need
design work beyond this change.
Related to pnpm/pnpm#13334.