From 86c6fd5cc7eeb5ce2d4c129f145d6554c30fb22e Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Wed, 24 Jun 2026 22:28:26 +0530 Subject: [PATCH] fix(exec): resolve workspace-local bin under linked install strategy --- lib/commands/exec.js | 6 +++--- test/lib/commands/exec.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 23c47a0cc1ad7..7b7d8ad30fcd5 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -42,7 +42,7 @@ class Exec extends BaseCommand { } } - async callExec (args, { name, locationMsg, runPath } = {}) { + async callExec (args, { locationMsg, runPath } = {}) { let localBin = this.npm.localBin let pkgPath = this.npm.localPrefix @@ -50,8 +50,8 @@ class Exec extends BaseCommand { if (!runPath) { runPath = process.cwd() } else { - // We have to consider if the workspace has its own separate versions libnpmexec will walk up to localDir after looking here - localBin = resolve(this.npm.localDir, name, 'node_modules', '.bin') + // Use the workspace's own node_modules/.bin, not localDir/, since the linked strategy does not symlink workspaces into the root node_modules. + localBin = resolve(runPath, 'node_modules', '.bin') // We also need to look for `bin` entries in the workspace package.json // libnpmexec will NOT look in the project root for the bin entry pkgPath = runPath diff --git a/test/lib/commands/exec.js b/test/lib/commands/exec.js index 30d8c2c046c37..87422d1519a44 100644 --- a/test/lib/commands/exec.js +++ b/test/lib/commands/exec.js @@ -220,6 +220,41 @@ t.test('finds workspace dep first', async t => { t.ok(exists.isFile(), 'bin ran, creating file') }) +t.test('finds workspace dep bin under linked install strategy', async t => { + const { npm } = await loadMockNpm(t, { + config: { + 'install-strategy': 'linked', + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-root-test', + workspaces: ['workspace-a', 'tool'], + }), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + dependencies: { tool: '*' }, + }), + }, + tool: { + 'package.json': JSON.stringify({ + name: 'tool', + version: '1.0.0', + bin: { 'npx-test': 'index.js' }, + }), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, + }, + }, + }) + + await npm.exec('install', []) + npm.config.set('workspace', ['workspace-a']) + await npm.exec('exec', ['npx-test']) + const exists = await fs.stat(path.join(npm.prefix, 'workspace-a', 'npm-exec-test-success')) + t.ok(exists.isFile(), 'workspace-local bin ran instead of falling back to the registry') +}) + t.test('npx --no-install @npmcli/npx-test', async t => { const registry = new MockRegistry({ tap: t,