Verify latest release
pacquet version
0.2.9 (also reproduces on 0.2.8).
Which area(s) of pnpm are affected?
pacquet resolver (resolving-npm-resolver).
Describe the Bug
Pacquet's npm resolver doesn't honor linkWorkspacePackages in pnpm-workspace.yaml. When a workspace package depends on another workspace package via a bare semver range (e.g. "foo": "^1.0.0", no workspace: prefix), pacquet always goes to the registry instead of preferring the matching workspace package.
Symptoms:
- If a same-named package exists on npm at a matching version, pacquet silently links the wrong package (registry copy, not the local workspace one).
- If no same-named npm package exists, the install fails with a 404 from the registry.
The workspace:-prefixed branch works correctly; only the bare-semver path is missing.
Why this matters
- vlt.sh chart babylon column is DNF on every variation (see pacquet#11902). Babylon's root
package.json lists @dev/build-tools: ^1.0.0 (a workspace package), and @dev/build-tools doesn't exist on the public npm registry — so pacquet's resolver 404s and the install errors out instantly.
- Silent-wrong-install risk. Any user whose local workspace package shares a name with an npm package would have their local copy bypassed without warning.
Repro
Minimal — name chosen so npm can't accidentally satisfy it.
package.json:
{
"name": "root",
"private": true,
"devDependencies": {
"local-only-pkg-xyz999": "^1.0.0"
}
}
pnpm-workspace.yaml:
linkWorkspacePackages: true
packages:
- "packages/*"
packages/local-only-pkg-xyz999/package.json:
{
"name": "local-only-pkg-xyz999",
"version": "1.0.0",
"private": true
}
$ pnpm install --ignore-scripts --silent
$ readlink node_modules/local-only-pkg-xyz999
../packages/local-only-pkg-xyz999 # ← workspace match, correct
$ pacquet install
Error: pacquet_package_manager::resolve_importer
× installing dependencies
├─▶ Failed to resolve importer: Failed to resolve dependency: Failed to
│ fetch metadata from https://registry.npmjs.org/local-only-pkg-xyz999:
│ HTTP status client error (404 Not Found)
Larger repro: the babylon fixture at https://github.com/vltpkg/benchmarks/tree/main/fixtures/babylon reproduces the same error on every variation.
Root cause
Pacquet's NpmResolver::resolve_impl only routes workspace:-prefixed specs through try_resolve_from_workspace. For bare semver specs, it falls straight through to pick_package against the registry without ever consulting workspace_packages.
The LinkWorkspacePackages field exists on PnpmSettings but is never read by the resolver.
Pnpm's three workspace branches (port targets)
In pnpm's resolving/npm-resolver/src/index.ts:
workspace: prefix (line 417 / tryResolveFromWorkspace). ✅ Already ported.
- Registry-pick succeeds + workspace match (line 550). When the registry returned a real version and a workspace package matches the same
(name, version), prefer the workspace one. ❌ Missing in pacquet.
- Registry-pick fails / no matching version (line 507 / 528 —
tryResolveFromWorkspacePackages). On 404 or NoMatchingVersion, fall back to the workspace if a matching package exists. ❌ Missing in pacquet — this is the branch that would unblock babylon.
The gate that controls whether branches 2 and 3 fire is opts.alwaysTryWorkspacePackages !== false (line 435), which pnpm wires from the link-workspace-packages config:
true (default) → both branches active.
false → workspace packages only matched on the workspace: prefix path.
"deep" → also threaded through transitive resolution.
Implementation hints
- Plumb
link_workspace_packages from PnpmSettings through to ResolveOptions (ResolveOptions::always_try_workspace_packages).
- Add a
tryResolveFromWorkspacePackages equivalent to resolving-npm-resolver (sibling of the existing try_resolve_from_workspace).
- In
NpmResolver::resolve_impl, wire branches 2 and 3 around the existing pick_package call, mirroring pnpm's structure.
- Port the upstream tests at
resolving/npm-resolver/test/ that cover linkWorkspacePackages so behavior parity is verifiable.
Cardinal-rule reminder
Per pacquet/AGENTS.md: match pnpm's structure, edge cases, and config-resolution semantics. linkWorkspacePackages accepts true, false, "deep"; the field on PnpmSettings is typed Option<serde_json::Value> for that reason — preserve the three-way semantics, not just the boolean.
Written by an agent (Claude Code, claude-opus-4-7).
Verify latest release
pacquet version
0.2.9(also reproduces on0.2.8).Which area(s) of pnpm are affected?
pacquetresolver (resolving-npm-resolver).Describe the Bug
Pacquet's npm resolver doesn't honor
linkWorkspacePackagesinpnpm-workspace.yaml. When a workspace package depends on another workspace package via a bare semver range (e.g."foo": "^1.0.0", noworkspace:prefix), pacquet always goes to the registry instead of preferring the matching workspace package.Symptoms:
The
workspace:-prefixed branch works correctly; only the bare-semver path is missing.Why this matters
package.jsonlists@dev/build-tools: ^1.0.0(a workspace package), and@dev/build-toolsdoesn't exist on the public npm registry — so pacquet's resolver 404s and the install errors out instantly.Repro
Minimal — name chosen so npm can't accidentally satisfy it.
package.json:{ "name": "root", "private": true, "devDependencies": { "local-only-pkg-xyz999": "^1.0.0" } }pnpm-workspace.yaml:packages/local-only-pkg-xyz999/package.json:{ "name": "local-only-pkg-xyz999", "version": "1.0.0", "private": true }Larger repro: the babylon fixture at https://github.com/vltpkg/benchmarks/tree/main/fixtures/babylon reproduces the same error on every variation.
Root cause
Pacquet's
NpmResolver::resolve_implonly routesworkspace:-prefixed specs throughtry_resolve_from_workspace. For bare semver specs, it falls straight through topick_packageagainst the registry without ever consultingworkspace_packages.The
LinkWorkspacePackagesfield exists onPnpmSettingsbut is never read by the resolver.Pnpm's three workspace branches (port targets)
In pnpm's
resolving/npm-resolver/src/index.ts:workspace:prefix (line 417 /tryResolveFromWorkspace). ✅ Already ported.(name, version), prefer the workspace one. ❌ Missing in pacquet.tryResolveFromWorkspacePackages). On 404 orNoMatchingVersion, fall back to the workspace if a matching package exists. ❌ Missing in pacquet — this is the branch that would unblock babylon.The gate that controls whether branches 2 and 3 fire is
opts.alwaysTryWorkspacePackages !== false(line 435), which pnpm wires from thelink-workspace-packagesconfig:true(default) → both branches active.false→ workspace packages only matched on theworkspace:prefix path."deep"→ also threaded through transitive resolution.Implementation hints
link_workspace_packagesfromPnpmSettingsthrough toResolveOptions(ResolveOptions::always_try_workspace_packages).tryResolveFromWorkspacePackagesequivalent toresolving-npm-resolver(sibling of the existingtry_resolve_from_workspace).NpmResolver::resolve_impl, wire branches 2 and 3 around the existingpick_packagecall, mirroring pnpm's structure.resolving/npm-resolver/test/that coverlinkWorkspacePackagesso behavior parity is verifiable.Cardinal-rule reminder
Per
pacquet/AGENTS.md: match pnpm's structure, edge cases, and config-resolution semantics.linkWorkspacePackagesacceptstrue,false,"deep"; the field onPnpmSettingsis typedOption<serde_json::Value>for that reason — preserve the three-way semantics, not just the boolean.Written by an agent (Claude Code, claude-opus-4-7).