diff --git a/lib/commands/exec.js b/lib/commands/exec.js index ed4b07dc39b4d..d532eca107c6c 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -34,24 +34,33 @@ class Exec extends BaseCommand { for (const [name, path] of this.workspaces) { const locationMsg = `in workspace ${this.npm.chalk.green(name)} at location:\n${this.npm.chalk.dim(path)}` - await this.callExec(args, { locationMsg, runPath: path }) + await this.callExec(args, { name, locationMsg, runPath: path }) } } - async callExec (args, { locationMsg, runPath } = {}) { - // This is where libnpmexec will look for locally installed packages + async callExec (args, { name, locationMsg, runPath } = {}) { + // This is where libnpmexec will look for locally installed packages at the project level const localPrefix = this.npm.localPrefix + // This is where libnpmexec will look for locally installed packages at the workspace level + let localBin = this.npm.localBin + let path = localPrefix // This is where libnpmexec will actually run the scripts from 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') + // 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 + path = runPath } const call = this.npm.config.get('call') let globalPath const { flatOptions, - localBin, globalBin, globalDir, chalk, @@ -79,14 +88,14 @@ class Exec extends BaseCommand { // copy args so they dont get mutated args: [...args], call, - localBin, - locationMsg, + chalk, globalBin, globalPath, + localBin, + locationMsg, output, - chalk, packages, - path: localPrefix, + path, runPath, scriptShell, yes, diff --git a/test/lib/commands/exec.js b/test/lib/commands/exec.js index 07a3e6ebd8ed9..3cb4e90e855fe 100644 --- a/test/lib/commands/exec.js +++ b/test/lib/commands/exec.js @@ -88,7 +88,7 @@ t.test('--prefix', async t => { t.ok(exists.isFile(), 'bin ran, creating file') }) -t.test('workspaces', async t => { +t.test('runs in workspace path', async t => { const registry = new MockRegistry({ tap: t, registry: 'https://registry.npmjs.org/', @@ -124,12 +124,101 @@ t.test('workspaces', async t => { await registry.package({ manifest, tarballs: { '1.0.0': path.join(npm.prefix, 'npm-exec-test'), - } }) + }, + }) await npm.exec('exec', ['@npmcli/npx-test']) const exists = await fs.stat(path.join(npm.prefix, 'workspace-a', 'npm-exec-test-success')) t.ok(exists.isFile(), 'bin ran, creating file inside workspace') }) +t.test('finds workspace bin first', async t => { + const { npm } = await loadMockNpm(t, { + config: { + workspace: ['workspace-a'], + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-root-test', + bin: { 'npx-test': 'index.js' }, + workspaces: ['workspace-a'], + }), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-fail', '')`, + 'workspace-a': { + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-test', + bin: { 'npx-test': 'index.js' }, + }), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, + }, + }, + }) + + await npm.exec('install', []) // reify + 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(), 'bin ran, creating file inside workspace') + t.rejects(fs.stat(path.join(npm.prefix, 'npm-exec-test-fail'))) +}) + +t.test('finds workspace dep first', async t => { + const registry = new MockRegistry({ + tap: t, + registry: 'https://registry.npmjs.org/', + }) + + const manifest = registry.manifest({ name: '@npmcli/subdep', versions: ['1.0.0', '2.0.0'] }) + manifest.versions['1.0.0'].bin = { 'npx-test': 'index.js' } + manifest.versions['2.0.0'].bin = { 'npx-test': 'index.js' } + + const { npm } = await loadMockNpm(t, { + prefixDir: { + subdep: { + one: { + 'package.json': JSON.stringify(manifest.versions['1.0.0']), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-one', '')`, + }, + two: { + 'package.json': JSON.stringify(manifest.versions['2.0.0']), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-two', '')`, + }, + }, + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-root-test', + dependencies: { '@npmcli/subdep': '1.0.0' }, + bin: { 'npx-test': 'index.js' }, + workspaces: ['workspace-a'], + }), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-fail', '')`, + 'workspace-a': { + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-test', + dependencies: { '@npmcli/subdep': '2.0.0' }, + bin: { 'npx-test': 'index.js' }, + }), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, + }, + }, + }) + + await registry.package({ manifest, + tarballs: { + '1.0.0': path.join(npm.prefix, 'subdep', 'one'), + '2.0.0': path.join(npm.prefix, 'subdep', 'two'), + }, + }) + 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(), 'bin ran, creating file') +}) + t.test('npx --no-install @npmcli/npx-test', async t => { const registry = new MockRegistry({ tap: t,