Skip to content

Commit 14fec07

Browse files
committed
fixup! feat: add exec workspaces
1 parent 789a01c commit 14fec07

File tree

3 files changed

+220
-24
lines changed

3 files changed

+220
-24
lines changed

docs/content/commands/npm-exec.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ npm exec -- <pkg>[@<version>] [args...]
1111
npm exec --package=<pkg>[@<version>] -- <cmd> [args...]
1212
npm exec -c '<cmd> [args...]'
1313
npm exec --package=foo -c '<cmd> [args...]'
14+
npm exec [-ws] [-w <workspace-name] [args...]
1415

1516
npx <pkg>[@<specifier>] [args...]
1617
npx -p <pkg>[@<specifier>] <cmd> [args...]
@@ -145,6 +146,68 @@ $ npm x -c 'eslint && say "hooray, lint passed"'
145146
$ npx -c 'eslint && say "hooray, lint passed"'
146147
```
147148

149+
### Workspaces support
150+
151+
You may use the `workspace` or `workspaces` configs in order to run an
152+
arbitrary command from an npm package (either one installed locally, or fetched
153+
remotely) in the context of the specified workspaces.
154+
If no positional argument or `--call` option is provided, it will open an
155+
interactive subshell in the context of each of these configured workspaces one
156+
at a time.
157+
158+
Given a project with configured workspaces, e.g:
159+
160+
```
161+
.
162+
+-- package.json
163+
`-- packages
164+
+-- a
165+
| `-- package.json
166+
+-- b
167+
| `-- package.json
168+
`-- c
169+
`-- package.json
170+
```
171+
172+
Assuming the workspace configuration is properly set up at the root level
173+
`package.json` file. e.g:
174+
175+
```
176+
{
177+
"workspaces": [ "./packages/*" ]
178+
}
179+
```
180+
181+
You can execute an arbitrary command from a package in the context of each of
182+
the configured workspaces when using the `workspaces` configuration options,
183+
in this example we're using **eslint** to lint any js file found within each
184+
workspace folder:
185+
186+
```
187+
npm exec -ws -- eslint ./*.js
188+
```
189+
190+
#### Filtering workspaces
191+
192+
It's also possible to execute a command in a single workspace using the
193+
`workspace` config along with a name or directory path:
194+
195+
```
196+
npm exec --workspace=a -- eslint ./*.js
197+
```
198+
199+
The `workspace` config can also be specified multiple times in order to run a
200+
specific script in the context of multiple workspaces. When defining values for
201+
the `workspace` config in the command line, it also possible to use `-w` as a
202+
shorthand, e.g:
203+
204+
```
205+
npm exec -w a -w b -- eslint ./*.js
206+
```
207+
208+
This last command will run the `eslint` command in both `./packages/a` and
209+
`./packages/b` folders.
210+
148211
### Compatibility with Older npx Versions
149212

150213
The `npx` binary was rewritten in npm v7.0.0, and the standalone `npx`
@@ -195,6 +258,30 @@ requested from the server. To force full offline mode, use `offline`.
195258
Forces full offline mode. Any packages not locally cached will result in
196259
an error.
197260

261+
#### workspace
262+
263+
* Alias: `-w`
264+
* Type: Array
265+
* Default: `[]`
266+
267+
Enable running scripts in the context of workspaces while also filtering by
268+
the provided names or paths provided.
269+
270+
Valid values for the `workspace` config are either:
271+
- Workspace names
272+
- Path to a workspace directory
273+
- Path to a parent workspace directory (will result to selecting all of the
274+
children workspaces)
275+
276+
#### workspaces
277+
278+
* Alias: `-ws`
279+
* Type: Boolean
280+
* Default: `false`
281+
282+
Run scripts in the context of all configured workspaces for the current
283+
project.
284+
198285
### See Also
199286

200287
* [npm run-script](/commands/npm-run-script)

lib/exec.js

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { promisify } = require('util')
22
const read = promisify(require('read'))
3+
const chalk = require('chalk')
34
const mkdirp = require('mkdirp-infer-owner')
45
const readPackageJson = require('read-package-json-fast')
56
const Arborist = require('@npmcli/arborist')
@@ -39,6 +40,13 @@ const getWorkspaces = require('./workspaces/get-workspaces.js')
3940
// runScript({ pkg, event: 'npx', ... })
4041
// process.env.npm_lifecycle_event = 'npx'
4142

43+
const nocolor = {
44+
reset: s => s,
45+
bold: s => s,
46+
dim: s => s,
47+
green: s => s,
48+
}
49+
4250
class Exec extends BaseCommand {
4351
/* istanbul ignore next - see test/lib/load-all-commands.js */
4452
static get name () {
@@ -72,7 +80,7 @@ class Exec extends BaseCommand {
7280

7381
// When commands go async and we can dump the boilerplate exec methods this
7482
// can be named correctly
75-
async _exec (_args, { path, runPath }) {
83+
async _exec (_args, { locationMsg, path, runPath }) {
7684
const { package: p, call, shell } = this.npm.flatOptions
7785
const packages = [...p]
7886

@@ -87,6 +95,7 @@ class Exec extends BaseCommand {
8795
return await this.run({
8896
args,
8997
call,
98+
locationMsg,
9099
shell,
91100
path,
92101
pathArr,
@@ -113,6 +122,7 @@ class Exec extends BaseCommand {
113122
return await this.run({
114123
args,
115124
call,
125+
locationMsg,
116126
path,
117127
pathArr,
118128
runPath,
@@ -205,10 +215,18 @@ class Exec extends BaseCommand {
205215
pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
206216
}
207217

208-
return await this.run({ args, call, path, pathArr, runPath, shell })
218+
return await this.run({
219+
args,
220+
call,
221+
locationMsg,
222+
path,
223+
pathArr,
224+
runPath,
225+
shell,
226+
})
209227
}
210228

211-
async run ({ args, call, path, pathArr, runPath, shell }) {
229+
async run ({ args, call, locationMsg, path, pathArr, runPath, shell }) {
212230
// turn list of args into command string
213231
const script = call || args.shift() || shell
214232

@@ -230,7 +248,19 @@ class Exec extends BaseCommand {
230248
if (process.stdin.isTTY) {
231249
if (ciDetect())
232250
return this.npm.log.warn('exec', 'Interactive mode disabled in CI environment')
233-
this.npm.output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`)
251+
252+
const color = this.npm.config.get('color')
253+
const colorize = color ? chalk : nocolor
254+
255+
locationMsg = locationMsg || ` at location:\n${colorize.dim(runPath)}`
256+
257+
this.npm.output(`${
258+
colorize.reset('\nEntering npm script environment')
259+
}${
260+
colorize.reset(locationMsg)
261+
}${
262+
colorize.bold('\nType \'exit\' or ^D when finished\n')
263+
}`)
234264
}
235265
}
236266
return await runScript({
@@ -305,9 +335,21 @@ class Exec extends BaseCommand {
305335

306336
async _execWorkspaces (args, filters) {
307337
const workspaces = await this.workspaces(filters)
338+
const getLocationMsg = async path => {
339+
const color = this.npm.config.get('color')
340+
const colorize = color ? chalk : nocolor
341+
const { _id } = await readPackageJson(`${path}/package.json`)
342+
return ` in workspace ${colorize.green(_id)} at location:\n${colorize.dim(path)}`
343+
}
308344

309-
for (const workspacePath of workspaces.values())
310-
await this._exec(args, { path: workspacePath, runPath: workspacePath })
345+
for (const workspacePath of workspaces.values()) {
346+
const locationMsg = await getLocationMsg(workspacePath)
347+
await this._exec(args, {
348+
locationMsg,
349+
path: workspacePath,
350+
runPath: workspacePath,
351+
})
352+
}
311353
}
312354
}
313355
module.exports = Exec

test/lib/exec.js

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const npm = {
3838
globalBin: 'global-bin',
3939
config: {
4040
get: k => {
41+
if (k === 'color')
42+
return false
4143
if (k !== 'cache')
4244
throw new Error('unexpected config get')
4345

@@ -240,14 +242,35 @@ t.test('npm exec <noargs>, run interactive shell', t => {
240242
cb()
241243
})
242244
}
243-
244245
t.test('print message when tty and not in CI', t => {
245246
CI_NAME = null
246247
process.stdin.isTTY = true
247248
run(t, true, () => {
248249
t.strictSame(LOG_WARN, [])
249250
t.strictSame(OUTPUT, [
250-
['\nEntering npm script environment\nType \'exit\' or ^D when finished\n'],
251+
[`\nEntering npm script environment at location:\n${process.cwd()}\nType 'exit' or ^D when finished\n`],
252+
], 'printed message about interactive shell')
253+
t.end()
254+
})
255+
})
256+
257+
t.test('print message with color when tty and not in CI', t => {
258+
CI_NAME = null
259+
process.stdin.isTTY = true
260+
261+
const _config = npm.config
262+
npm.config = { get (k) {
263+
if (k === 'color')
264+
return true
265+
} }
266+
t.teardown(() => {
267+
npm.config = _config
268+
})
269+
270+
run(t, true, () => {
271+
t.strictSame(LOG_WARN, [])
272+
t.strictSame(OUTPUT, [
273+
[`\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`],
251274
], 'printed message about interactive shell')
252275
t.end()
253276
})
@@ -1116,22 +1139,66 @@ t.test('workspaces', t => {
11161139
PROGRESS_IGNORED = true
11171140
npm.localBin = resolve(npm.localPrefix, 'node_modules/.bin')
11181141

1119-
exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b'], er => {
1120-
if (er)
1121-
throw er
1142+
t.test('with args, run scripts in the context of a workspace', t => {
1143+
exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b'], er => {
1144+
if (er)
1145+
throw er
11221146

1123-
t.match(RUN_SCRIPTS, [{
1124-
pkg: { scripts: { npx: 'foo' }},
1125-
args: ['one arg', 'two arg'],
1126-
banner: false,
1127-
path: process.cwd(),
1128-
stdioString: true,
1129-
event: 'npx',
1130-
env: {
1131-
PATH: [npm.localBin, ...PATH].join(delimiter),
1132-
},
1133-
stdio: 'inherit',
1134-
}])
1135-
t.end()
1147+
t.match(RUN_SCRIPTS, [{
1148+
pkg: { scripts: { npx: 'foo' }},
1149+
args: ['one arg', 'two arg'],
1150+
banner: false,
1151+
path: process.cwd(),
1152+
stdioString: true,
1153+
event: 'npx',
1154+
env: {
1155+
PATH: [npm.localBin, ...PATH].join(delimiter),
1156+
},
1157+
stdio: 'inherit',
1158+
}])
1159+
t.end()
1160+
})
11361161
})
1162+
1163+
t.test('no args, spawn interactive shell', async t => {
1164+
CI_NAME = null
1165+
process.stdin.isTTY = true
1166+
1167+
await new Promise((res, rej) => {
1168+
exec.execWorkspaces([], ['a'], er => {
1169+
if (er)
1170+
return rej(er)
1171+
1172+
t.strictSame(LOG_WARN, [])
1173+
t.strictSame(OUTPUT, [
1174+
[`\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`],
1175+
], 'printed message about interactive shell')
1176+
res()
1177+
})
1178+
})
1179+
1180+
const _config = npm.config
1181+
npm.config = { get (k) {
1182+
if (k === 'color')
1183+
return true
1184+
} }
1185+
t.teardown(() => {
1186+
npm.config = _config
1187+
})
1188+
OUTPUT.length = 0
1189+
await new Promise((res, rej) => {
1190+
exec.execWorkspaces([], ['a'], er => {
1191+
if (er)
1192+
return rej(er)
1193+
1194+
t.strictSame(LOG_WARN, [])
1195+
t.strictSame(OUTPUT, [
1196+
[`\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')}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`],
1197+
], 'printed message about interactive shell')
1198+
res()
1199+
})
1200+
})
1201+
})
1202+
1203+
t.end()
11371204
})

0 commit comments

Comments
 (0)