Skip to content

Commit

Permalink
fix(exec): workspaces support
Browse files Browse the repository at this point in the history
Fixes the proper path location to use when targetting specific
workspaces.

Fixes: npm#3520
Relates to: npm/statusboard#403
  • Loading branch information
ruyadorno committed Apr 19, 2022
1 parent ff1367f commit 7c1faf5
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 57 deletions.
8 changes: 3 additions & 5 deletions lib/commands/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 })
}
}
}
Expand Down
156 changes: 104 additions & 52 deletions test/lib/commands/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1128,10 +1128,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: '',
},
},
Expand Down Expand Up @@ -1159,68 +1160,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'
)
})
76 changes: 76 additions & 0 deletions workspaces/libnpmexec/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand Down

0 comments on commit 7c1faf5

Please sign in to comment.