Visitar URL original
Nested relative `workspace:` dependency resolves from the consumer directory with `injectWorkspacePackages` · Issue #17284 · pnpm/pnpm · GitHub
Skip to content

Nested relative workspace: dependency resolves from the consumer directory with injectWorkspacePackages #17284

Closed

Description

@zaverden

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.mjs

The 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

  1. added theissue type on Oct 23, 2026
  2. changed the title [-]Concurrent independent workspace installs fail with EEXIST when linking GVS executables[/-] [+]Nested relative `workspace:` dependency resolves from the consumer directory with `injectWorkspacePackages`[/+] on Oct 23, 2026
  3. added a commit that references this issue on Oct 23, 2026
    b7952df
  4. added a commit that references this issue on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions