Skip to content

Commit 6253d19

Browse files
ruyadornolukekarrys
authored andcommitted
fix(exec): workspaces support
Fixes the proper path location to use when targetting specific workspaces. Fixes: #3520 Relates to: npm/statusboard#403
1 parent 8da28b4 commit 6253d19

File tree

3 files changed

+183
-57
lines changed

3 files changed

+183
-57
lines changed

lib/commands/exec.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,8 @@ class Exec extends BaseCommand {
4848
static ignoreImplicitWorkspace = false
4949
static isShellout = true
5050

51-
async exec (_args, { locationMsg, path, runPath } = {}) {
52-
if (!path) {
53-
path = this.npm.localPrefix
54-
}
51+
async exec (_args, { locationMsg, runPath } = {}) {
52+
const path = this.npm.localPrefix
5553

5654
if (!runPath) {
5755
runPath = process.cwd()
@@ -95,7 +93,7 @@ class Exec extends BaseCommand {
9593

9694
for (const path of this.workspacePaths) {
9795
const locationMsg = await getLocationMsg({ color, path })
98-
await this.exec(args, { locationMsg, path, runPath: path })
96+
await this.exec(args, { locationMsg, runPath: path })
9997
}
10098
}
10199
}

test/lib/commands/exec.js

+104-52
Original file line numberDiff line numberDiff line change
@@ -1334,10 +1334,11 @@ t.test('forward legacyPeerDeps opt', async t => {
13341334
)
13351335
})
13361336

1337-
t.test('workspaces', t => {
1337+
t.test('workspaces', async t => {
13381338
npm.localPrefix = t.testdir({
13391339
node_modules: {
13401340
'.bin': {
1341+
a: '',
13411342
foo: '',
13421343
},
13431344
},
@@ -1365,68 +1366,119 @@ t.test('workspaces', t => {
13651366
})
13661367

13671368
PROGRESS_IGNORED = true
1368-
npm.localBin = resolve(npm.localPrefix, 'node_modules/.bin')
1369+
npm.localBin = resolve(npm.localPrefix, 'node_modules', '.bin')
13691370

1370-
t.test('with args, run scripts in the context of a workspace', async t => {
1371-
await exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b'])
1371+
// with arg matching existing bin, run scripts in the context of a workspace
1372+
await exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b'])
13721373

1373-
t.match(RUN_SCRIPTS, [
1374-
{
1375-
pkg: { scripts: { npx: 'foo' } },
1376-
args: ['one arg', 'two arg'],
1377-
banner: false,
1378-
path: process.cwd(),
1379-
stdioString: true,
1380-
event: 'npx',
1381-
env: {
1382-
PATH: [npm.localBin, process.env.PATH].join(delimiter),
1383-
},
1384-
stdio: 'inherit',
1374+
t.match(RUN_SCRIPTS, [
1375+
{
1376+
pkg: { scripts: { npx: 'foo' } },
1377+
args: ['one arg', 'two arg'],
1378+
banner: false,
1379+
path: npm.localPrefix,
1380+
stdioString: true,
1381+
event: 'npx',
1382+
env: {
1383+
PATH: [npm.localBin, process.env.PATH].join(delimiter),
13851384
},
1386-
])
1387-
})
1385+
stdio: 'inherit',
1386+
},
1387+
{
1388+
pkg: { scripts: { npx: 'foo' } },
1389+
args: ['one arg', 'two arg'],
1390+
banner: false,
1391+
path: npm.localPrefix,
1392+
stdioString: true,
1393+
event: 'npx',
1394+
env: {
1395+
PATH: [npm.localBin, process.env.PATH].join(delimiter),
1396+
},
1397+
stdio: 'inherit',
1398+
},
1399+
], 'should run with multiple args across multiple workspaces')
13881400

1389-
t.test('no args, spawn interactive shell', async t => {
1390-
CI_NAME = null
1391-
process.stdin.isTTY = true
1401+
// clean up
1402+
RUN_SCRIPTS.length = 0
13921403

1393-
await exec.execWorkspaces([], ['a'])
1404+
// with packages, run scripts in the context of a workspace
1405+
config.package = ['foo']
1406+
config.call = 'foo'
1407+
config.yes = false
13941408

1395-
t.strictSame(LOG_WARN, [])
1396-
t.strictSame(
1397-
npm._mockOutputs,
1409+
ARB_ACTUAL_TREE[npm.localPrefix] = {
1410+
children: new Map([['foo', { name: 'foo', version: '1.2.3' }]]),
1411+
}
1412+
1413+
await exec.execWorkspaces([], ['a', 'b'])
1414+
1415+
// path should point to the workspace folder
1416+
t.match(RUN_SCRIPTS, [
1417+
{
1418+
pkg: { scripts: { npx: 'foo' } },
1419+
args: [],
1420+
banner: false,
1421+
path: resolve(npm.localPrefix, 'packages', 'a'),
1422+
stdioString: true,
1423+
event: 'npx',
1424+
stdio: 'inherit',
1425+
},
1426+
{
1427+
pkg: { scripts: { npx: 'foo' } },
1428+
args: [],
1429+
banner: false,
1430+
path: resolve(npm.localPrefix, 'packages', 'b'),
1431+
stdioString: true,
1432+
event: 'npx',
1433+
stdio: 'inherit',
1434+
},
1435+
], 'should run without args in multiple workspaces')
1436+
1437+
t.match(ARB_CTOR, [
1438+
{ path: npm.localPrefix },
1439+
{ path: npm.localPrefix },
1440+
])
1441+
1442+
// no args, spawn interactive shell
1443+
CI_NAME = null
1444+
config.package = []
1445+
config.call = ''
1446+
process.stdin.isTTY = true
1447+
1448+
await exec.execWorkspaces([], ['a'])
1449+
1450+
t.strictSame(LOG_WARN, [])
1451+
t.strictSame(
1452+
npm._mockOutputs,
1453+
[
13981454
[
1399-
[
1400-
`\nEntering npm script environment in workspace [email protected] at location:\n${resolve(
1401-
npm.localPrefix,
1402-
'packages/a'
1403-
)}\nType 'exit' or ^D when finished\n`,
1404-
],
1455+
`\nEntering npm script environment in workspace [email protected] at location:\n${resolve(
1456+
npm.localPrefix,
1457+
'packages/a'
1458+
)}\nType 'exit' or ^D when finished\n`,
14051459
],
1406-
'printed message about interactive shell'
1407-
)
1460+
],
1461+
'printed message about interactive shell'
1462+
)
14081463

1409-
npm.color = true
1410-
flatOptions.color = true
1411-
npm._mockOutputs.length = 0
1412-
await exec.execWorkspaces([], ['a'])
1464+
npm.color = true
1465+
flatOptions.color = true
1466+
npm._mockOutputs.length = 0
1467+
await exec.execWorkspaces([], ['a'])
14131468

1414-
t.strictSame(LOG_WARN, [])
1415-
t.strictSame(
1416-
npm._mockOutputs,
1469+
t.strictSame(LOG_WARN, [])
1470+
t.strictSame(
1471+
npm._mockOutputs,
1472+
[
14171473
[
1418-
[
1474+
/* eslint-disable-next-line max-len */
1475+
`\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m in workspace \u001b[[email protected]\u001b[39m at location:\u001b[0m\n\u001b[0m\u001b[2m${resolve(
1476+
npm.localPrefix,
1477+
'packages/a'
14191478
/* eslint-disable-next-line max-len */
1420-
`\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m in workspace \u001b[[email protected]\u001b[39m at location:\u001b[0m\n\u001b[0m\u001b[2m${resolve(
1421-
npm.localPrefix,
1422-
'packages/a'
1423-
/* eslint-disable-next-line max-len */
1424-
)}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`,
1425-
],
1479+
)}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`,
14261480
],
1427-
'printed message about interactive shell'
1428-
)
1429-
})
1430-
1431-
t.end()
1481+
],
1482+
'printed message about interactive shell'
1483+
)
14321484
})

workspaces/libnpmexec/test/index.js

+76
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,82 @@ t.test('local pkg, must not fetch manifest for avail pkg', async t => {
121121
t.equal(res, 'LOCAL PKG', 'should run local pkg bin script')
122122
})
123123

124+
t.test('multiple local pkgs', async t => {
125+
const foo = {
126+
name: '@ruyadorno/create-foo',
127+
version: '2.0.0',
128+
bin: {
129+
'create-foo': './index.js',
130+
},
131+
}
132+
const bar = {
133+
name: '@ruyadorno/create-bar',
134+
version: '2.0.0',
135+
bin: {
136+
'create-bar': './index.js',
137+
},
138+
}
139+
const path = t.testdir({
140+
cache: {},
141+
npxCache: {},
142+
node_modules: {
143+
'.bin': {},
144+
'@ruyadorno': {
145+
'create-foo': {
146+
'package.json': JSON.stringify(foo),
147+
'index.js': `#!/usr/bin/env node
148+
require('fs').writeFileSync(process.argv.slice(2)[0], 'foo')`,
149+
},
150+
'create-bar': {
151+
'package.json': JSON.stringify(bar),
152+
'index.js': `#!/usr/bin/env node
153+
require('fs').writeFileSync(process.argv.slice(2)[0], 'bar')`,
154+
},
155+
},
156+
},
157+
'package.json': JSON.stringify({
158+
name: 'pkg',
159+
dependencies: {
160+
'@ruyadorno/create-foo': '^2.0.0',
161+
'@ruyadorno/create-bar': '^2.0.0',
162+
},
163+
}),
164+
})
165+
const runPath = path
166+
const cache = resolve(path, 'cache')
167+
const npxCache = resolve(path, 'npxCache')
168+
169+
const setupBins = async (pkg) => {
170+
const executable =
171+
resolve(path, `node_modules/${pkg.name}/index.js`)
172+
fs.chmodSync(executable, 0o775)
173+
174+
await binLinks({
175+
path: resolve(path, `node_modules/${pkg.name}`),
176+
pkg,
177+
})
178+
}
179+
180+
await Promise.all([foo, bar]
181+
.map(setupBins))
182+
183+
await libexec({
184+
...baseOpts,
185+
localBin: resolve(path, 'node_modules/.bin'),
186+
cache,
187+
npxCache,
188+
packages: ['@ruyadorno/create-foo', '@ruyadorno/create-bar'],
189+
call: 'create-foo resfile && create-bar bar',
190+
path,
191+
runPath,
192+
})
193+
194+
const resFoo = fs.readFileSync(resolve(path, 'resfile')).toString()
195+
t.equal(resFoo, 'foo', 'should run local pkg bin script')
196+
const resBar = fs.readFileSync(resolve(path, 'bar')).toString()
197+
t.equal(resBar, 'bar', 'should run local pkg bin script')
198+
})
199+
124200
t.test('local file system path', async t => {
125201
const path = t.testdir({
126202
cache: {},

0 commit comments

Comments
 (0)