Repository navigation
Description
Verify latest release
- I verified that the issue exists in the latest pnpm release
pnpm version
12.15.0
Which area(s) of pnpm are affected? (leave empty if unsure)
Dependencies resolver
Link to the code that reproduces this issue or a replay of the bug
No response
Reproduction steps
A consuming app depends on two local libraries. One library also depends on the other through a path relative to its own manifest:
apps/main -> libs/lib-b -> libs/lib-a
\---------------> libs/lib-a
All three packages are included in the workspace. Enable injectWorkspacePackages: true and install. The nested selector is workspace:../lib-a in libs/lib-b/package.json.
Save this complete script as repro.mjs, then run:
node repro.mjs /absolute/path/to/pnpm
# Or, if the pnpm version to test is on PATH:
node repro.mjsThe script creates an isolated temporary project, prints the pnpm version and command exit codes, then runs a control that changes only the nested dependency's protocol to file:. It intentionally ends with an assertion failure on the affected release, after printing both results.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
// Pass an absolute path if pnpm is not on PATH. No global installation needed.
const pnpm = process.argv[2] ?? 'pnpm';
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-injected-relative-'));
const app = path.join(root, 'apps/main');
function write(name, value) {
const file = path.join(root, name);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, typeof value === 'string' ? value : JSON.stringify(value, null, 2));
}
write('apps/main/package.json', {
name: 'app', version: '1.0.0', private: true,
dependencies: {
'lib-a': 'workspace:../../libs/lib-a',
'lib-b': 'workspace:../../libs/lib-b',
},
});
write('libs/lib-a/package.json', { name: 'lib-a', version: '1.0.0' });
const libB = {
name: 'lib-b', version: '1.0.0',
dependencies: { 'lib-a': 'workspace:../lib-a' },
};
write('libs/lib-b/package.json', libB);
write('apps/main/pnpm-workspace.yaml', `packages:
- .
- ../../libs/*
injectWorkspacePackages: true
ignoreScripts: true
`);
write('empty.npmrc', '');
const home = path.join(root, 'isolated-home');
fs.mkdirSync(home);
const env = Object.fromEntries(Object.entries(process.env)
.filter(([key]) => !/^(npm|pnpm)_config_/i.test(key)));
Object.assign(env, {
HOME: home, USERPROFILE: home,
XDG_CONFIG_HOME: path.join(home, 'config'),
XDG_CACHE_HOME: path.join(home, 'cache'),
XDG_DATA_HOME: path.join(home, 'data'),
XDG_STATE_HOME: path.join(home, 'state'),
npm_config_userconfig: path.join(root, 'empty.npmrc'),
CI: '', NO_COLOR: '1',
});
function run(args) {
const result = spawnSync(pnpm, args, { cwd: app, env, encoding: 'utf8', timeout: 60000 });
if (result.error) throw result.error;
console.log(`> pnpm ${args.join(' ')}\nexit: ${result.status}`);
process.stdout.write(result.stdout + result.stderr);
return result;
}
console.log(`Project: ${root}\nNode: ${process.version}`);
assert.equal(run(['--version']).status, 0);
const args = ['install', '--offline', '--no-frozen-lockfile'];
console.log('\nNested workspace: selector:');
const actual = run(args);
// The desired result is exit 0. Keep running to demonstrate the control.
console.log('\nControl: change ONLY lib-b -> lib-a to file:../lib-a');
libB.dependencies['lib-a'] = 'file:../lib-a';
write('libs/lib-b/package.json', libB);
const control = run(args);
assert.equal(control.status, 0, 'file: control must succeed');
assert.equal(actual.status, 0, 'nested workspace: selector should resolve from libs/lib-b');Describe the Bug
On pnpm 12.15.0 the first install exits 1. It tries to load <repro-root>/apps/lib-a, rather than <repro-root>/libs/lib-a:
Failed to resolve lib-a@workspace:../lib-a
Could not install from "<repro-root>/apps/lib-a" as it does not exist.
The temporary-directory prefix above is abbreviated. Changing only lib-b's dependency to file:../lib-a makes the same offline install exit 0. The package directories exist throughout, and no registry metadata or build output is required.
This is the remaining injected/fresh-resolution variant of the relative-path problem discussed in pnpm/pnpm#8159. Ordinary deployment from a shared workspace lockfile already succeeds on 12.15.0, including running the deployed app. This report does not claim that the closed ordinary-deploy case still fails. A legacy deploy that fresh-resolves these nested selectors can still encounter the wrong base; the install reproduction above isolates the resolver problem from deploy-target rebasing and lockfile conversion.
Source observation: the release resolver's declaring-manifest helper changes project_dir only for file: specifiers. A nested relative workspace: specifier retains the consumer's options. The local path parser then resolves its path using that project_dir.
The same file:-only condition remains in main at cb496f54277a6a36eb124cb61560ef0c6f87e238. The reproduction was executed on the official 12.15.0 binary; main was inspected as source only, not built or executed. No instrumentation or test-only source changes are needed for this reproduction.
Expected Behavior
Resolve lib-b's workspace:../lib-a relative to the directory containing lib-b's manifest, just as the file: control does. Install should exit 0 and inject both libraries. It must not look for lib-a relative to the consuming app's directory.
Which Node.js version are you using?
v26.12.0
Which operating systems have you used?
- macOS
- Windows
- Linux
If your OS is a Linux based, which one it is? (Include the version if relevant)
No response
Contributing a fix
- I am working on a fix
Activity
[-]Concurrent independent workspace installs fail with EEXIST when linking GVS executables[/-][+]Nested relative `workspace:` dependency resolves from the consumer directory with `injectWorkspacePackages`[/+]fix(resolver): resolve a workspace: path of an injected package from …
Update pnpm to v12.16.1 (#2164)