#15007·pnpm

A synthesized 0.0.0 version makes every `name@<X` packageExtensions selector match

Author: zkochanCreated Sep 17, 2026Updated Sep 17, 2026
Labelstype: bugarea: resolution

pnpm version

12.4.2 and 11.27.0

Code to reproduce the issue

Split out of #15000, which this half was reported in. That issue's fix is the manifest-less custom resolver; this one is independent of it and reproduces on both pnpm 11 and pnpm 12.

When a package's manifest cannot be read, both stacks synthesize one so the package still has an identity: pnpm 11's getManifestFromResponse and pacquet's walk::fallback_manifest both produce { "name": <name>, "version": "0.0.0" }. That happens for a package whose archive ships no package.json, and for a link: target directory that has none.

The synthesized manifest is then handed to the readPackageHook pipeline, which is where the built-in compatibility database (@yarnpkg/extensions) and a user's packageExtensions are applied. Both select packages with name[@range] selectors, and both compare the range against the manifest's version.

Expected behavior

A package whose version is unknown should match only bare name selectors.

Actual behavior

It matches every name@<X selector for its name, because 0.0.0 satisfies every "less than" range. The compatibility database ships many of those, for example:

json
["debug@<4.2.0", { "peerDependenciesMeta": { "supports-color": { "optional": true } } }]

Any manifest-less package named debug therefore picks up that entry's peerDependenciesMeta, whatever the archive actually contains. The same applies to a user's packageExtensions with a range selector, and to overrides, which go through the same pipeline.

An extension can only add entries the package does not declare itself, so a misfire adds dependencies or peer entries rather than replacing any. It still reaches the lockfile.

Additional information

Treating the synthesized version as unknown rather than as 0.0.0 closes this: entry_matches in pacquet's package_extender.rs already declines a ranged selector when the version does not parse, and semver.satisfies does the same in the TypeScript extender. The identity default of 0.0.0 would move to where the dep path and the packages: entry are built, after the hooks have run.

The catch is that the pipeline is one composed hook in both stacks, so a pnpmfile readPackage hook would then see a manifest with no version for these packages. That is a contract change worth deciding on its own, which is why it is filed separately rather than carried by the fix for #15000.

Node.js version

24.18.1

Operating System

macOS


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