diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 0bbf7b3fa0b72..85a71923f521f 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -1,31 +1,5 @@ const libexec = require('libnpmexec') const BaseCommand = require('../base-command.js') -const getLocationMsg = require('../exec/get-workspace-location-msg.js') - -// it's like this: -// -// npm x pkg@version <-- runs the bin named "pkg" or the only bin if only 1 -// -// { name: 'pkg', bin: { pkg: 'pkg.js', foo: 'foo.js' }} <-- run pkg -// { name: 'pkg', bin: { foo: 'foo.js' }} <-- run foo? -// -// npm x -p pkg@version -- foo -// -// npm x -p pkg@version -- foo --registry=/dev/null -// -// const pkg = npm.config.get('package') || getPackageFrom(args[0]) -// const cmd = getCommand(pkg, args[0]) -// --> npm x -c 'cmd ...args.slice(1)' -// -// we've resolved cmd and args, and escaped them properly, and installed the -// relevant packages. -// -// Add the ${npx install prefix}/node_modules/.bin to PATH -// -// pkg = readPackageJson('./package.json') -// pkg.scripts.___npx = ${the -c arg} -// runScript({ pkg, event: 'npx', ... }) -// process.env.npm_lifecycle_event = 'npx' class Exec extends BaseCommand { static description = 'Run a command from a local or remote npm package' @@ -64,7 +38,7 @@ class Exec extends BaseCommand { localBin, globalBin, } = this.npm - const output = (...outputArgs) => this.npm.output(...outputArgs) + const output = this.npm.output.bind(this.npm) const scriptShell = this.npm.config.get('script-shell') || undefined const packages = this.npm.config.get('package') const yes = this.npm.config.get('yes') @@ -94,10 +68,10 @@ class Exec extends BaseCommand { async execWorkspaces (args, filters) { await this.setWorkspaces(filters) - const color = this.npm.color - for (const path of this.workspacePaths) { - const locationMsg = await getLocationMsg({ color, path }) + 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.exec(args, { locationMsg, runPath: path }) } } diff --git a/lib/commands/init.js b/lib/commands/init.js index cff8340dcd707..039f08e06059e 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -10,7 +10,6 @@ const PackageJson = require('@npmcli/package-json') const log = require('../utils/log-shim.js') const updateWorkspaces = require('../workspaces/update-workspaces.js') -const getLocationMsg = require('../exec/get-workspace-location-msg.js') const BaseCommand = require('../base-command.js') class Init extends BaseCommand { @@ -119,13 +118,7 @@ class Init extends BaseCommand { localBin, globalBin, } = this.npm - // this function is definitely called. But because of coverage map stuff - // it ends up both uncovered, and the coverage report doesn't even mention. - // the tests do assert that some output happens, so we know this line is - // being hit. - /* istanbul ignore next */ - const output = (...outputArgs) => this.npm.output(...outputArgs) - const locationMsg = await getLocationMsg({ color, path }) + const output = this.npm.output.bind(this.npm) const runPath = path const scriptShell = this.npm.config.get('script-shell') || undefined const yes = this.npm.config.get('yes') @@ -135,7 +128,6 @@ class Init extends BaseCommand { args: newArgs, color, localBin, - locationMsg, globalBin, output, path, diff --git a/lib/exec/get-workspace-location-msg.js b/lib/exec/get-workspace-location-msg.js deleted file mode 100644 index 813b11e789222..0000000000000 --- a/lib/exec/get-workspace-location-msg.js +++ /dev/null @@ -1,25 +0,0 @@ -const chalk = require('chalk') -const readPackageJson = require('read-package-json-fast') - -const nocolor = { - dim: s => s, - green: s => s, -} - -const getLocationMsg = async ({ color, path }) => { - const colorize = color ? chalk : nocolor - const { _id } = - await readPackageJson(`${path}/package.json`) - .catch(() => ({})) - - const workspaceMsg = _id - ? ` in workspace ${colorize.green(_id)}` - : ` in a ${colorize.green('new')} workspace` - const locationMsg = ` at location:\n${ - colorize.dim(path) - }` - - return `${workspaceMsg}${locationMsg}` -} - -module.exports = getLocationMsg diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index 032caefbda18e..a03d22e5e6305 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -1117,7 +1117,7 @@ Directory in which \`npm pack\` will save tarballs. * Default: * Type: String (can be set multiple times) -The package to install for [\`npm exec\`](/commands/npm-exec) +The package or packages to install for [\`npm exec\`](/commands/npm-exec) diff --git a/test/lib/commands/exec.js b/test/lib/commands/exec.js index 049ed327c8fc5..6ac719122bfb2 100644 --- a/test/lib/commands/exec.js +++ b/test/lib/commands/exec.js @@ -1,1547 +1,97 @@ const t = require('tap') -const { fake: mockNpm } = require('../../fixtures/mock-npm') -const { resolve, delimiter } = require('path') +const fs = require('@npmcli/fs') +const path = require('path') +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') +const MockRegistry = require('../../fixtures/mock-registry.js') -const ARB_CTOR = [] -const ARB_ACTUAL_TREE = {} -const ARB_REIFY = [] -class Arborist { - constructor (options) { - ARB_CTOR.push(options) - this.path = options.path - } - - async loadActual () { - return ARB_ACTUAL_TREE[this.path] - } - - async reify (options) { - ARB_REIFY.push(options) - } -} - -let PROGRESS_ENABLED = true -const LOG_WARN = [] -let PROGRESS_IGNORED = false -const flatOptions = { - npxCache: 'npx-cache-dir', - color: false, - cache: 'cache-dir', - legacyPeerDeps: false, - package: [], -} -const config = { - cache: 'bad-cache-dir', // this should never show up passed into libnpmexec - yes: true, - call: '', - package: [], - 'script-shell': 'shell-cmd', -} - -const npm = mockNpm({ - flatOptions, - config, - localPrefix: 'local-prefix', - localBin: 'local-bin', - globalBin: 'global-bin', -}) - -const RUN_SCRIPTS = [] -const runScript = async opt => { - RUN_SCRIPTS.push(opt) - if (!PROGRESS_IGNORED && PROGRESS_ENABLED) { - throw new Error('progress not disabled during run script!') - } -} - -const MANIFESTS = {} -const pacote = { - manifest: async (spec, options) => { - return MANIFESTS[spec] - }, -} - -const MKDIRPS = [] -const mkdirp = async path => MKDIRPS.push(path) - -let READ_RESULT = '' -let READ_ERROR = null -const READ = [] -const read = (options, cb) => { - READ.push(options) - process.nextTick(() => cb(READ_ERROR, READ_RESULT)) -} - -let CI_NAME = 'travis-ci' - -const log = { - 'proc-log': { - warn: (...args) => { - LOG_WARN.push(args) - }, - }, - npmlog: { - disableProgress: () => { - PROGRESS_ENABLED = false - }, - enableProgress: () => { - PROGRESS_ENABLED = true - }, - clearProgress: () => {}, - }, -} - -const mocks = { - libnpmexec: t.mock('libnpmexec', { - '@npmcli/arborist': Arborist, - '@npmcli/run-script': runScript, - '@npmcli/ci-detect': () => CI_NAME, - pacote, - read, - 'mkdirp-infer-owner': mkdirp, - ...log, - }), - ...log, -} -const Exec = t.mock('../../../lib/commands/exec.js', mocks) -const exec = new Exec(npm) - -t.afterEach(() => { - MKDIRPS.length = 0 - ARB_CTOR.length = 0 - ARB_REIFY.length = 0 - RUN_SCRIPTS.length = 0 - READ.length = 0 - READ_RESULT = '' - READ_ERROR = null - LOG_WARN.length = 0 - PROGRESS_IGNORED = false - flatOptions.legacyPeerDeps = false - flatOptions.color = false - config['script-shell'] = 'shell-cmd' - config.package = [] - flatOptions.package = [] - config.call = '' - config.yes = true - npm.color = false - npm.localBin = 'local-bin' - npm.globalBin = 'global-bin' -}) - -t.test('npx foo, bin already exists locally', async t => { - const path = t.testdir({ - node_modules: { - '.bin': { - foo: 'just some file', - }, - }, - }) - - PROGRESS_IGNORED = true - npm.localBin = resolve(path, 'node_modules', '.bin') - - await exec.exec(['foo', 'one arg', 'two arg']) - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - cache: flatOptions.cache, - npxCache: flatOptions.npxCache, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { - PATH: [npm.localBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npx foo, bin already exists globally', async t => { - const path = t.testdir({ - node_modules: { - '.bin': { - foo: 'just some file', - }, - }, - }) - - PROGRESS_IGNORED = true - npm.globalBin = resolve(path, 'node_modules', '.bin') - - await exec.exec(['foo', 'one arg', 'two arg']) - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { - PATH: [npm.globalBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, already present locally', async t => { - const path = t.testdir() - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec , run interactive shell', t => { - CI_NAME = null - const { isTTY } = process.stdin - process.stdin.isTTY = true - t.teardown(() => (process.stdin.isTTY = isTTY)) - - const run = async (t, doRun) => { - LOG_WARN.length = 0 - ARB_CTOR.length = 0 - MKDIRPS.length = 0 - ARB_REIFY.length = 0 - npm._mockOutputs.length = 0 - await exec.exec([]) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.strictSame(ARB_CTOR, [], 'no need to instantiate arborist') - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - if (doRun) { - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'shell-cmd' } }, - args: [], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) - } else { - t.strictSame(RUN_SCRIPTS, []) - } - - RUN_SCRIPTS.length = 0 - } - t.test('print message when tty and not in CI', async t => { - CI_NAME = null - process.stdin.isTTY = true - await run(t, true) - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - /* eslint-disable-next-line max-len */ - `\nEntering npm script environment at location:\n${process.cwd()}\nType 'exit' or ^D when finished\n`, - ], - ], - 'printed message about interactive shell' - ) - }) - - t.test('print message with color when tty and not in CI', async t => { - CI_NAME = null - process.stdin.isTTY = true - npm.color = true - flatOptions.color = true - - await run(t, true) - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - /* eslint-disable-next-line max-len */ - `\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m at location:\u001b[0m\n\u001b[0m\u001b[2m${process.cwd()}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`, - ], - ], - 'printed message about interactive shell' - ) - }) - - t.test('no message when not TTY', async t => { - CI_NAME = null - process.stdin.isTTY = false - await run(t, true) - t.strictSame(LOG_WARN, []) - t.strictSame(npm._mockOutputs, [], 'no message about interactive shell') - }) - - t.test('print warning when in CI and interactive', async t => { - CI_NAME = 'travis-ci' - process.stdin.isTTY = true - await run(t, false) - t.strictSame(LOG_WARN, [['exec', 'Interactive mode disabled in CI environment']]) - t.strictSame(npm._mockOutputs, [], 'no message about interactive shell') - }) - - t.test('not defined script-shell config value', async t => { - CI_NAME = null - process.stdin.isTTY = true - config['script-shell'] = undefined - - await exec.exec([]) - - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: /sh|cmd/ } }, - }, - ]) - - LOG_WARN.length = 0 - ARB_CTOR.length = 0 - MKDIRPS.length = 0 - ARB_REIFY.length = 0 - npm._mockOutputs.length = 0 - RUN_SCRIPTS.length = 0 - }) - - t.end() -}) - -t.test('npm exec foo, not present locally or in central loc', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add: ['foo@'], legacyPeerDeps: false }], 'need to install foo@') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, packageLockOnly set to true', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - npm.config.set('package-lock-only', true) - t.teardown(() => { - npm.config.set('package-lock-only', false) - }) - - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ - path, - packageLockOnly: false, - }]) - t.match(ARB_REIFY, [{ - add: ['foo@'], - legacyPeerDeps: false, - packageLockOnly: false, - }], 'need to install foo@') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, not present locally but in central loc', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to install again, already there') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, present locally but wrong version', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/2badf4630f1cfaad') - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS['foo@2.x'] = { - name: 'foo', - version: '2.3.4', - bin: { - foo: 'foo', - }, - _from: 'foo@2.x', - } - await exec.exec(['foo@2.x', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add: ['foo@2.x'], legacyPeerDeps: false }], 'need to add foo@2.x') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, present locally but outdated version', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '2.3.4', - bin: { - foo: 'foo', - }, - _from: 'foo@2.x', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add: ['foo'], legacyPeerDeps: false }], 'need to add foo@2.x') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec --package=foo bar', async t => { - const path = t.testdir() - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - config.package = ['foo'] - flatOptions.package = ['foo'] - await exec.exec(['bar', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'bar' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec @foo/bar -- --some=arg, locally installed', async t => { - const foobarManifest = { - name: '@foo/bar', - version: '1.2.3', - bin: { - foo: 'foo', - bar: 'bar', - }, - } - const path = t.testdir({ - node_modules: { - '@foo/bar': { - 'package.json': JSON.stringify(foobarManifest), - }, - }, - }) - const pkg = { - name: '@foo/bar', - version: '1.2.3', - bin: { foo: 'foo', bar: 'bar' }, - } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS['@foo/bar'] = foobarManifest - await exec.exec(['@foo/bar', '--some=arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'bar' } }, - args: ['--some=arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test( - 'npm exec @foo/bar, with same bin alias and no unscoped named bin, locally installed', - async t => { - const pkg = { - name: '@foo/bar', - version: '1.2.3', - bin: { - baz: 'corge', // pick the first one - qux: 'corge', - quux: 'corge', - }, - } - const path = t.testdir({ - node_modules: { - '@foo/bar': { - 'package.json': JSON.stringify(pkg), - }, - }, - }) - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS['@foo/bar'] = pkg - await exec.exec(['@foo/bar', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'baz' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) - } -) - -t.test( - 'npm exec @foo/bar, with different bin alias and no unscoped named bin, locally installed', - async t => { - const path = t.testdir() - const pkg = { - name: '@foo/bar', - version: '1.2.3.', - bin: { foo: 'qux', corge: 'qux', baz: 'quux' }, - } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ - ...pkg, - package: pkg, - pkgid: `${pkg.name}@${pkg.version}`, - }]) - }, - }, - } - MANIFESTS['@foo/bar'] = { - name: '@foo/bar', - version: '1.2.3', - bin: { - foo: 'qux', - corge: 'qux', - baz: 'quux', - }, - _from: 'foo@', - _id: '@foo/bar@1.2.3', - } - await t.rejects(exec.exec(['@foo/bar']), { - message: 'could not determine executable to run', - pkgid: '@foo/bar@1.2.3', - }) - } -) - -t.test('run command with 2 packages, need install, verify sort', async t => { - // test both directions, should use same install dir both times - // also test the read() call here, verify that the prompts match - const cases = [ - ['foo', 'bar'], - ['bar', 'foo'], - ] - t.plan(cases.length) - for (const packages of cases) { - t.test(packages.join(', '), async t => { - config.package = packages - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await exec.exec(['foobar', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - }) - } -}) - -t.test('npm exec foo, no bin in package', async t => { - const pkg = { name: 'foo', version: '1.2.3' } - const path = t.testdir({ - node_modules: { - foo: { - 'package.json': JSON.stringify(pkg), - }, - }, - }) - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ - ...pkg, - package: pkg, - pkgid: `${pkg.name}@${pkg.version}`, - }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - _from: 'foo@', - _id: 'foo@1.2.3', - } - await t.rejects(exec.exec(['foo']), { - message: 'could not determine executable to run', - pkgid: 'foo@1.2.3', - }) -}) - -t.test('npm exec foo, many bins in package, none named foo', async t => { - const path = t.testdir() - const pkg = { - name: 'foo', - version: '1.2.3', - bin: { bar: 'bar', baz: 'baz' }, - } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ - ...pkg, - package: pkg, - pkgid: `${pkg.name}@${pkg.version}`, - }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - bar: 'bar', - baz: 'baz', - }, - _from: 'foo@', - _id: 'foo@1.2.3', - } - await t.rejects(exec.exec(['foo']), { - message: 'could not determine executable to run', - pkgid: 'foo@1.2.3', - }) -}) - -t.test('npm exec -p foo -c "ls -laF"', async t => { - const path = t.testdir() - const pkg = { name: 'foo', version: '1.2.3' } - npm.localPrefix = path - config.package = ['foo'] - config.call = 'ls -laF' - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - _from: 'foo@', - } - await exec.exec([]) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'ls -laF' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('positional args and --call together is an error', async t => { - config.call = 'true' - await t.rejects(exec.exec(['foo']), exec.usage) -}) - -t.test('prompt when installs are needed if not already present and shell is a TTY', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' - }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_RESULT = 'yolo' - - config.package = packages - config.yes = undefined - - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', +t.test('call with args', async t => { + const { npm } = await loadMockNpm(t, { + config: { + call: 'foo', }, - _from: 'bar@', - } - await exec.exec(['foobar']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - t.strictSame(READ, [ - { - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }, - ]) -}) - -t.test( - /* eslint-disable-next-line max-len */ - 'skip prompt when installs are needed if not already present and shell is not a tty (multiple packages)', - async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' - }) - process.stdout.isTTY = false - process.stdin.isTTY = false - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_RESULT = 'yolo' - - config.package = packages - config.yes = undefined - - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await exec.exec(['foobar']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - t.strictSame(READ, [], 'should not have prompted') - t.strictSame( - LOG_WARN, - [['exec', 'The following packages were not found and will be installed: bar, foo']], - 'should have printed a warning' - ) - } -) - -t.test( - /* eslint-disable-next-line max-len */ - 'skip prompt when installs are needed if not already present and shell is not a tty (single package)', - async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' - }) - process.stdout.isTTY = false - process.stdin.isTTY = false - CI_NAME = false - - const packages = ['foo'] - READ_RESULT = 'yolo' - - config.package = packages - config.yes = undefined - - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foobar']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install the package') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - t.strictSame(READ, [], 'should not have prompted') - t.strictSame( - LOG_WARN, - [['exec', 'The following package was not found and will be installed: foo']], - 'should have printed a warning' - ) - } -) - -t.test('abort if prompt rejected', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_RESULT = 'no, why would I want such a thing??' - config.package = packages - config.yes = undefined - - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await t.rejects(exec.exec(['foobar']), /canceled/, 'should be canceled') - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, [ - { - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }, - ]) + await t.rejects( + npm.exec('exec', ['bar']), + { code: 'EUSAGE' } + ) }) -t.test('abort if prompt false', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' +t.test('registry package', async t => { + const registry = new MockRegistry({ + tap: t, + registry: 'https://registry.npmjs.org/', }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_ERROR = 'canceled' - config.package = packages - config.yes = undefined + const manifest = registry.manifest({ name: '@npmcli/npx-test' }) + manifest.versions['1.0.0'].bin = { 'npx-test': 'index.js' } - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, + const { npm } = await loadMockNpm(t, { + config: { + yes: true, }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() + prefixDir: { + 'npm-exec-test': { + 'package.json': JSON.stringify(manifest), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, }, }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await t.rejects(exec.exec(['foobar']), 'canceled', 'should be canceled') - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, [ - { - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }, - ]) -}) - -t.test('abort if -n provided', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' + globals: ({ prefix }) => ({ + 'process.cwd': () => prefix, + }), }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - const packages = ['foo', 'bar'] + await registry.package({ manifest, + tarballs: { + '1.0.0': path.join(npm.prefix, 'npm-exec-test'), + } }) - config.package = packages - config.yes = false - - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await t.rejects(exec.exec(['foobar']), /canceled/, 'should be canceled') - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, []) -}) - -t.test('forward legacyPeerDeps opt', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - config.yes = true - flatOptions.legacyPeerDeps = true - await exec.exec(['foo']) - t.match( - ARB_REIFY, - [{ add: ['foo@'], legacyPeerDeps: true }], - 'need to install foo@ using legacyPeerDeps opt' - ) + await npm.exec('exec', ['@npmcli/npx-test']) + const exists = await fs.stat(path.join(npm.prefix, 'npm-exec-test-success')) + t.ok(exists.isFile(), 'bin ran, creating file') }) t.test('workspaces', async t => { - npm.localPrefix = t.testdir({ - node_modules: { - '.bin': { - a: '', - foo: '', - }, - }, - packages: { - a: { + const registry = new MockRegistry({ + tap: t, + registry: 'https://registry.npmjs.org/', + }) + + const manifest = registry.manifest({ name: '@npmcli/npx-test' }) + manifest.versions['1.0.0'].bin = { 'npx-test': 'index.js' } + + const { npm } = await loadMockNpm(t, { + config: { + yes: true, + workspace: ['workspace-a'], + }, + prefixDir: { + 'npm-exec-test': { + 'package.json': JSON.stringify(manifest), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, + }, + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-test', + workspaces: ['workspace-a'], + }), + 'workspace-a': { 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - bin: 'cli.js', - }), - 'cli.js': '', - }, - b: { - 'package.json': JSON.stringify({ - name: 'b', - version: '1.0.0', + name: 'workspace-a', }), }, }, - 'package.json': JSON.stringify({ - name: 'root', - version: '1.0.0', - workspaces: ['packages/*'], + globals: ({ prefix }) => ({ + 'process.cwd': () => prefix, }), }) - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - PROGRESS_IGNORED = true - npm.localBin = resolve(npm.localPrefix, 'node_modules', '.bin') - - // with arg matching existing bin, run scripts in the context of a workspace - await exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b']) - - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: npm.localPrefix, - stdioString: true, - event: 'npx', - env: { - PATH: [npm.localBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: npm.localPrefix, - stdioString: true, - event: 'npx', - env: { - PATH: [npm.localBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - ], 'should run with multiple args across multiple workspaces') - - // clean up - RUN_SCRIPTS.length = 0 - - // with packages, run scripts in the context of a workspace - config.package = ['foo'] - config.call = 'foo' - config.yes = false - - ARB_ACTUAL_TREE[npm.localPrefix] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - - await exec.execWorkspaces([], ['a', 'b']) - - // path should point to the workspace folder - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: [], - banner: false, - path: resolve(npm.localPrefix, 'packages', 'a'), - stdioString: true, - event: 'npx', - stdio: 'inherit', - }, - { - pkg: { scripts: { npx: 'foo' } }, - args: [], - banner: false, - path: resolve(npm.localPrefix, 'packages', 'b'), - stdioString: true, - event: 'npx', - stdio: 'inherit', - }, - ], 'should run without args in multiple workspaces') - - t.match(ARB_CTOR, [ - { path: npm.localPrefix }, - { path: npm.localPrefix }, - ]) - - // no args, spawn interactive shell - CI_NAME = null - config.package = [] - config.call = '' - process.stdin.isTTY = true - - await exec.execWorkspaces([], ['a']) - - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - `\nEntering npm script environment in workspace a@1.0.0 at location:\n${resolve( - npm.localPrefix, - 'packages/a' - )}\nType 'exit' or ^D when finished\n`, - ], - ], - 'printed message about interactive shell' - ) - - npm.color = true - flatOptions.color = true - npm._mockOutputs.length = 0 - await exec.execWorkspaces([], ['a']) - - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - /* eslint-disable-next-line max-len */ - `\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m in workspace \u001b[32ma@1.0.0\u001b[39m at location:\u001b[0m\n\u001b[0m\u001b[2m${resolve( - npm.localPrefix, - 'packages/a' - /* eslint-disable-next-line max-len */ - )}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`, - ], - ], - 'printed message about interactive shell' - ) + 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') })