diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 5e6a94296d287..f764cea528adb 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -48,10 +48,8 @@ class Exec extends BaseCommand { static ignoreImplicitWorkspace = false static isShellout = true - async exec (_args, { locationMsg, path, runPath } = {}) { - if (!path) { - path = this.npm.localPrefix - } + async exec (_args, { locationMsg, runPath } = {}) { + const path = this.npm.localPrefix if (!runPath) { runPath = process.cwd() @@ -95,7 +93,7 @@ class Exec extends BaseCommand { for (const path of this.workspacePaths) { const locationMsg = await getLocationMsg({ color, path }) - await this.exec(args, { locationMsg, path, runPath: path }) + await this.exec(args, { locationMsg, runPath: path }) } } } diff --git a/test/lib/commands/exec.js b/test/lib/commands/exec.js index 6b57bf6666e1e..05b9b7b0cce84 100644 --- a/test/lib/commands/exec.js +++ b/test/lib/commands/exec.js @@ -1334,10 +1334,11 @@ t.test('forward legacyPeerDeps opt', async t => { ) }) -t.test('workspaces', t => { +t.test('workspaces', async t => { npm.localPrefix = t.testdir({ node_modules: { '.bin': { + a: '', foo: '', }, }, @@ -1365,68 +1366,119 @@ t.test('workspaces', t => { }) PROGRESS_IGNORED = true - npm.localBin = resolve(npm.localPrefix, 'node_modules/.bin') + npm.localBin = resolve(npm.localPrefix, 'node_modules', '.bin') - t.test('with args, run scripts in the context of a workspace', async t => { - await exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b']) + // 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: process.cwd(), - stdioString: true, - event: 'npx', - env: { - PATH: [npm.localBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', + 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') - t.test('no args, spawn interactive shell', async t => { - CI_NAME = null - process.stdin.isTTY = true + // clean up + RUN_SCRIPTS.length = 0 - await exec.execWorkspaces([], ['a']) + // with packages, run scripts in the context of a workspace + config.package = ['foo'] + config.call = 'foo' + config.yes = false - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, + ARB_ACTUAL_TREE[npm.localPrefix] = { + children: new Map([['foo', { name: 'foo', version: '1.2.3' }]]), + } + + 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`, - ], + `\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' - ) + ], + 'printed message about interactive shell' + ) - npm.color = true - flatOptions.color = true - npm._mockOutputs.length = 0 - await exec.execWorkspaces([], ['a']) + npm.color = true + flatOptions.color = true + npm._mockOutputs.length = 0 + await exec.execWorkspaces([], ['a']) - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, + 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[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`, - ], + )}\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.end() + ], + 'printed message about interactive shell' + ) }) diff --git a/workspaces/libnpmexec/test/index.js b/workspaces/libnpmexec/test/index.js index bd1b67cec3a5d..834dcff0a1ff6 100644 --- a/workspaces/libnpmexec/test/index.js +++ b/workspaces/libnpmexec/test/index.js @@ -121,6 +121,82 @@ t.test('local pkg, must not fetch manifest for avail pkg', async t => { t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') }) +t.test('multiple local pkgs', async t => { + const foo = { + name: '@ruyadorno/create-foo', + version: '2.0.0', + bin: { + 'create-foo': './index.js', + }, + } + const bar = { + name: '@ruyadorno/create-bar', + version: '2.0.0', + bin: { + 'create-bar': './index.js', + }, + } + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-foo': { + 'package.json': JSON.stringify(foo), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync(process.argv.slice(2)[0], 'foo')`, + }, + 'create-bar': { + 'package.json': JSON.stringify(bar), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync(process.argv.slice(2)[0], 'bar')`, + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-foo': '^2.0.0', + '@ruyadorno/create-bar': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + const setupBins = async (pkg) => { + const executable = + resolve(path, `node_modules/${pkg.name}/index.js`) + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, `node_modules/${pkg.name}`), + pkg, + }) + } + + await Promise.all([foo, bar] + .map(setupBins)) + + await libexec({ + ...baseOpts, + localBin: resolve(path, 'node_modules/.bin'), + cache, + npxCache, + packages: ['@ruyadorno/create-foo', '@ruyadorno/create-bar'], + call: 'create-foo resfile && create-bar bar', + path, + runPath, + }) + + const resFoo = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(resFoo, 'foo', 'should run local pkg bin script') + const resBar = fs.readFileSync(resolve(path, 'bar')).toString() + t.equal(resBar, 'bar', 'should run local pkg bin script') +}) + t.test('local file system path', async t => { const path = t.testdir({ cache: {},