Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuy Adorno <ruyadorno@hotmail.com>2021-03-07 20:13:58 +0300
committerGar <gar+gh@danger.computer>2021-03-22 20:22:38 +0300
commite1b3b318f095a7e1a7cc4b131907de4955275d9d (patch)
treecb2cf7f6fa70a6eb546431f10bbe8124febe1db5
parentb876442241b9d366a0541714bbee1ae50d6746fd (diff)
feat: add exec workspaces
Add workspaces support to `npm exec` - Refactored logic to read and filter workspaces into `lib/workspaces/get-workspaces.js` - Added location context message when entering interactive shell using `npm exec` (with no args) - Add ability to execute a package in the context of each configured workspace Fixes: https://github.com/npm/statusboard/issues/288 PR-URL: https://github.com/npm/cli/pull/2886 Credit: @ruyadorno Close: #2886 Reviewed-by: @wraithgar
-rw-r--r--docs/content/commands/npm-exec.md87
-rw-r--r--lib/exec.js87
-rw-r--r--lib/run-script.js29
-rw-r--r--lib/workspaces/get-workspaces.js33
-rw-r--r--test/lib/exec.js110
-rw-r--r--test/lib/workspaces/get-workspaces.js199
6 files changed, 504 insertions, 41 deletions
diff --git a/docs/content/commands/npm-exec.md b/docs/content/commands/npm-exec.md
index cb3e51c82..88b98e3bc 100644
--- a/docs/content/commands/npm-exec.md
+++ b/docs/content/commands/npm-exec.md
@@ -11,6 +11,7 @@ npm exec -- <pkg>[@<version>] [args...]
npm exec --package=<pkg>[@<version>] -- <cmd> [args...]
npm exec -c '<cmd> [args...]'
npm exec --package=foo -c '<cmd> [args...]'
+npm exec [-ws] [-w <workspace-name] [args...]
npx <pkg>[@<specifier>] [args...]
npx -p <pkg>[@<specifier>] <cmd> [args...]
@@ -145,6 +146,68 @@ $ npm x -c 'eslint && say "hooray, lint passed"'
$ npx -c 'eslint && say "hooray, lint passed"'
```
+### Workspaces support
+
+You may use the `workspace` or `workspaces` configs in order to run an
+arbitrary command from an npm package (either one installed locally, or fetched
+remotely) in the context of the specified workspaces.
+If no positional argument or `--call` option is provided, it will open an
+interactive subshell in the context of each of these configured workspaces one
+at a time.
+
+Given a project with configured workspaces, e.g:
+
+```
+.
++-- package.json
+`-- packages
+ +-- a
+ | `-- package.json
+ +-- b
+ | `-- package.json
+ `-- c
+ `-- package.json
+```
+
+Assuming the workspace configuration is properly set up at the root level
+`package.json` file. e.g:
+
+```
+{
+ "workspaces": [ "./packages/*" ]
+}
+```
+
+You can execute an arbitrary command from a package in the context of each of
+the configured workspaces when using the `workspaces` configuration options,
+in this example we're using **eslint** to lint any js file found within each
+workspace folder:
+
+```
+npm exec -ws -- eslint ./*.js
+```
+
+#### Filtering workspaces
+
+It's also possible to execute a command in a single workspace using the
+`workspace` config along with a name or directory path:
+
+```
+npm exec --workspace=a -- eslint ./*.js
+```
+
+The `workspace` config can also be specified multiple times in order to run a
+specific script in the context of multiple workspaces. When defining values for
+the `workspace` config in the command line, it also possible to use `-w` as a
+shorthand, e.g:
+
+```
+npm exec -w a -w b -- eslint ./*.js
+```
+
+This last command will run the `eslint` command in both `./packages/a` and
+`./packages/b` folders.
+
### Compatibility with Older npx Versions
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`.
Forces full offline mode. Any packages not locally cached will result in
an error.
+#### workspace
+
+* Alias: `-w`
+* Type: Array
+* Default: `[]`
+
+Enable running scripts in the context of workspaces while also filtering by
+the provided names or paths provided.
+
+Valid values for the `workspace` config are either:
+- Workspace names
+- Path to a workspace directory
+- Path to a parent workspace directory (will result to selecting all of the
+children workspaces)
+
+#### workspaces
+
+* Alias: `-ws`
+* Type: Boolean
+* Default: `false`
+
+Run scripts in the context of all configured workspaces for the current
+project.
+
### See Also
* [npm run-script](/commands/npm-run-script)
diff --git a/lib/exec.js b/lib/exec.js
index 5b2e15831..25ddeb3bd 100644
--- a/lib/exec.js
+++ b/lib/exec.js
@@ -1,5 +1,6 @@
const { promisify } = require('util')
const read = promisify(require('read'))
+const chalk = require('chalk')
const mkdirp = require('mkdirp-infer-owner')
const readPackageJson = require('read-package-json-fast')
const Arborist = require('@npmcli/arborist')
@@ -12,6 +13,7 @@ const npa = require('npm-package-arg')
const fileExists = require('./utils/file-exists.js')
const PATH = require('./utils/path.js')
const BaseCommand = require('./base-command.js')
+const getWorkspaces = require('./workspaces/get-workspaces.js')
// it's like this:
//
@@ -38,6 +40,13 @@ const BaseCommand = require('./base-command.js')
// runScript({ pkg, event: 'npx', ... })
// process.env.npm_lifecycle_event = 'npx'
+const nocolor = {
+ reset: s => s,
+ bold: s => s,
+ dim: s => s,
+ green: s => s,
+}
+
class Exec extends BaseCommand {
/* istanbul ignore next - see test/lib/load-all-commands.js */
static get description () {
@@ -60,20 +69,27 @@ class Exec extends BaseCommand {
}
exec (args, cb) {
- this._exec(args).then(() => cb()).catch(cb)
+ const path = this.npm.localPrefix
+ const runPath = process.cwd()
+ this._exec(args, { path, runPath }).then(() => cb()).catch(cb)
+ }
+
+ execWorkspaces (args, filters, cb) {
+ this._execWorkspaces(args, filters).then(() => cb()).catch(cb)
}
// When commands go async and we can dump the boilerplate exec methods this
// can be named correctly
- async _exec (args) {
+ async _exec (_args, { locationMsg, path, runPath }) {
const call = this.npm.config.get('call')
const shell = this.npm.config.get('shell')
// dereferenced because we manipulate it later
const packages = [...this.npm.config.get('package')]
- if (call && args.length)
+ if (call && _args.length)
throw this.usage
+ const args = [..._args]
const pathArr = [...PATH]
// nothing to maybe install, skip the arborist dance
@@ -81,8 +97,11 @@ class Exec extends BaseCommand {
return await this.run({
args,
call,
+ locationMsg,
shell,
+ path,
pathArr,
+ runPath,
})
}
@@ -105,7 +124,10 @@ class Exec extends BaseCommand {
return await this.run({
args,
call,
+ locationMsg,
+ path,
pathArr,
+ runPath,
shell,
})
}
@@ -120,11 +142,11 @@ class Exec extends BaseCommand {
// node_modules/${name}/package.json, and only pacote fetch if
// that fails.
const manis = await Promise.all(packages.map(async p => {
- const spec = npa(p, this.npm.localPrefix)
+ const spec = npa(p, path)
if (spec.type === 'tag' && spec.rawSpec === '') {
// fall through to the pacote.manifest() approach
try {
- const pj = resolve(this.npm.localPrefix, 'node_modules', spec.name)
+ const pj = resolve(path, 'node_modules', spec.name)
return await readPackageJson(pj)
} catch (er) {}
}
@@ -143,7 +165,7 @@ class Exec extends BaseCommand {
// figure out whether we need to install stuff, or if local is fine
const localArb = new Arborist({
...this.npm.flatOptions,
- path: this.npm.localPrefix,
+ path,
})
const tree = await localArb.loadActual()
@@ -195,16 +217,24 @@ class Exec extends BaseCommand {
pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
}
- return await this.run({ args, call, pathArr, shell })
+ return await this.run({
+ args,
+ call,
+ locationMsg,
+ path,
+ pathArr,
+ runPath,
+ shell,
+ })
}
- async run ({ args, call, pathArr, shell }) {
+ async run ({ args, call, locationMsg, path, pathArr, runPath, shell }) {
// turn list of args into command string
const script = call || args.shift() || shell
// do the fakey runScript dance
// still should work if no package.json in cwd
- const realPkg = await readPackageJson(`${this.npm.localPrefix}/package.json`)
+ const realPkg = await readPackageJson(`${path}/package.json`)
.catch(() => ({}))
const pkg = {
...realPkg,
@@ -220,7 +250,19 @@ class Exec extends BaseCommand {
if (process.stdin.isTTY) {
if (ciDetect())
return this.npm.log.warn('exec', 'Interactive mode disabled in CI environment')
- this.npm.output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`)
+
+ const color = this.npm.config.get('color')
+ const colorize = color ? chalk : nocolor
+
+ locationMsg = locationMsg || ` at location:\n${colorize.dim(runPath)}`
+
+ this.npm.output(`${
+ colorize.reset('\nEntering npm script environment')
+ }${
+ colorize.reset(locationMsg)
+ }${
+ colorize.bold('\nType \'exit\' or ^D when finished\n')
+ }`)
}
}
return await runScript({
@@ -228,7 +270,7 @@ class Exec extends BaseCommand {
pkg,
banner: false,
// we always run in cwd, not --prefix
- path: process.cwd(),
+ path: runPath,
stdioString: true,
event: 'npx',
args,
@@ -288,5 +330,28 @@ class Exec extends BaseCommand {
.digest('hex')
.slice(0, 16)
}
+
+ async workspaces (filters) {
+ return getWorkspaces(filters, { path: this.npm.localPrefix })
+ }
+
+ async _execWorkspaces (args, filters) {
+ const workspaces = await this.workspaces(filters)
+ const getLocationMsg = async path => {
+ const color = this.npm.config.get('color')
+ const colorize = color ? chalk : nocolor
+ const { _id } = await readPackageJson(`${path}/package.json`)
+ return ` in workspace ${colorize.green(_id)} at location:\n${colorize.dim(path)}`
+ }
+
+ for (const workspacePath of workspaces.values()) {
+ const locationMsg = await getLocationMsg(workspacePath)
+ await this._exec(args, {
+ locationMsg,
+ path: workspacePath,
+ runPath: workspacePath,
+ })
+ }
+ }
}
module.exports = Exec
diff --git a/lib/run-script.js b/lib/run-script.js
index 0f4c40b0d..61572561a 100644
--- a/lib/run-script.js
+++ b/lib/run-script.js
@@ -1,13 +1,12 @@
const { resolve } = require('path')
const chalk = require('chalk')
const runScript = require('@npmcli/run-script')
-const mapWorkspaces = require('@npmcli/map-workspaces')
const { isServerPackage } = runScript
const rpj = require('read-package-json-fast')
const log = require('npmlog')
-const minimatch = require('minimatch')
const didYouMean = require('./utils/did-you-mean.js')
const isWindowsShell = require('./utils/is-windows-shell.js')
+const getWorkspaces = require('./workspaces/get-workspaces.js')
const cmdList = [
'publish',
@@ -184,31 +183,7 @@ class RunScript extends BaseCommand {
}
async workspaces (filters) {
- const cwd = this.npm.localPrefix
- const pkg = await rpj(resolve(cwd, 'package.json'))
- const workspaces = await mapWorkspaces({ cwd, pkg })
- const res = filters.length ? new Map() : workspaces
-
- for (const filterArg of filters) {
- for (const [key, path] of workspaces.entries()) {
- if (filterArg === key
- || resolve(cwd, filterArg) === path
- || minimatch(path, `${resolve(cwd, filterArg)}/*`))
- res.set(key, path)
- }
- }
-
- if (!res.size) {
- let msg = '!'
- if (filters.length) {
- msg = `:\n ${filters.reduce(
- (res, filterArg) => `${res} --workspace=${filterArg}`, '')}`
- }
-
- throw new Error(`No workspaces found${msg}`)
- }
-
- return res
+ return getWorkspaces(filters, { path: this.npm.localPrefix })
}
async runWorkspaces (args, filters) {
diff --git a/lib/workspaces/get-workspaces.js b/lib/workspaces/get-workspaces.js
new file mode 100644
index 000000000..64812d540
--- /dev/null
+++ b/lib/workspaces/get-workspaces.js
@@ -0,0 +1,33 @@
+const { resolve } = require('path')
+const mapWorkspaces = require('@npmcli/map-workspaces')
+const minimatch = require('minimatch')
+const rpj = require('read-package-json-fast')
+
+const getWorkspaces = async (filters, { path }) => {
+ const pkg = await rpj(resolve(path, 'package.json'))
+ const workspaces = await mapWorkspaces({ cwd: path, pkg })
+ const res = filters.length ? new Map() : workspaces
+
+ for (const filterArg of filters) {
+ for (const [workspaceName, workspacePath] of workspaces.entries()) {
+ if (filterArg === workspaceName
+ || resolve(path, filterArg) === workspacePath
+ || minimatch(workspacePath, `${resolve(path, filterArg)}/*`))
+ res.set(workspaceName, workspacePath)
+ }
+ }
+
+ if (!res.size) {
+ let msg = '!'
+ if (filters.length) {
+ msg = `:\n ${filters.reduce(
+ (res, filterArg) => `${res} --workspace=${filterArg}`, '')}`
+ }
+
+ throw new Error(`No workspaces found${msg}`)
+ }
+
+ return res
+}
+
+module.exports = getWorkspaces
diff --git a/test/lib/exec.js b/test/lib/exec.js
index 5e859a57a..7104795f6 100644
--- a/test/lib/exec.js
+++ b/test/lib/exec.js
@@ -108,6 +108,7 @@ t.afterEach(cb => {
LOG_WARN.length = 0
PROGRESS_IGNORED = false
flatOptions.legacyPeerDeps = false
+ config.color = false
config.package = []
flatOptions.package = []
config.call = ''
@@ -241,14 +242,27 @@ t.test('npm exec <noargs>, run interactive shell', t => {
cb()
})
}
-
t.test('print message when tty and not in CI', t => {
CI_NAME = null
process.stdin.isTTY = true
run(t, true, () => {
t.strictSame(LOG_WARN, [])
t.strictSame(OUTPUT, [
- ['\nEntering npm script environment\nType \'exit\' or ^D when finished\n'],
+ [`\nEntering npm script environment at location:\n${process.cwd()}\nType 'exit' or ^D when finished\n`],
+ ], 'printed message about interactive shell')
+ t.end()
+ })
+ })
+
+ t.test('print message with color when tty and not in CI', t => {
+ CI_NAME = null
+ process.stdin.isTTY = true
+ config.color = true
+
+ run(t, true, () => {
+ t.strictSame(LOG_WARN, [])
+ t.strictSame(OUTPUT, [
+ [`\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.end()
})
@@ -419,7 +433,7 @@ t.test('npm exec --package=foo bar', t => {
if (er)
throw er
t.strictSame(MKDIRPS, [], 'no need to make any dirs')
- t.match(ARB_CTOR, [{ package: ['foo'], path }])
+ 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, [{
@@ -1084,3 +1098,93 @@ t.test('forward legacyPeerDeps opt', t => {
t.done()
})
})
+
+t.test('workspaces', t => {
+ npm.localPrefix = t.testdir({
+ node_modules: {
+ '.bin': {
+ foo: '',
+ },
+ },
+ packages: {
+ 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',
+ }),
+ },
+ },
+ 'package.json': JSON.stringify({
+ name: 'root',
+ version: '1.0.0',
+ workspaces: ['packages/*'],
+ }),
+ })
+
+ PROGRESS_IGNORED = true
+ npm.localBin = resolve(npm.localPrefix, 'node_modules/.bin')
+
+ t.test('with args, run scripts in the context of a workspace', t => {
+ exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b'], er => {
+ if (er)
+ throw er
+
+ 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, ...PATH].join(delimiter),
+ },
+ stdio: 'inherit',
+ }])
+ t.end()
+ })
+ })
+
+ t.test('no args, spawn interactive shell', async t => {
+ CI_NAME = null
+ process.stdin.isTTY = true
+
+ await new Promise((res, rej) => {
+ exec.execWorkspaces([], ['a'], er => {
+ if (er)
+ return rej(er)
+
+ t.strictSame(LOG_WARN, [])
+ t.strictSame(OUTPUT, [
+ [`\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')
+ res()
+ })
+ })
+
+ config.color = true
+ OUTPUT.length = 0
+ await new Promise((res, rej) => {
+ exec.execWorkspaces([], ['a'], er => {
+ if (er)
+ return rej(er)
+
+ t.strictSame(LOG_WARN, [])
+ t.strictSame(OUTPUT, [
+ [`\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`],
+ ], 'printed message about interactive shell')
+ res()
+ })
+ })
+ })
+
+ t.end()
+})
diff --git a/test/lib/workspaces/get-workspaces.js b/test/lib/workspaces/get-workspaces.js
new file mode 100644
index 000000000..ebed9dd35
--- /dev/null
+++ b/test/lib/workspaces/get-workspaces.js
@@ -0,0 +1,199 @@
+const { resolve } = require('path')
+const t = require('tap')
+const getWorkspaces = require('../../../lib/workspaces/get-workspaces.js')
+
+const normalizePath = p => p
+ .replace(/\\+/g, '/')
+ .replace(/\r\n/g, '\n')
+
+const cleanOutput = (str, path) => normalizePath(str)
+ .replace(normalizePath(path), '{PATH}')
+
+const clean = (res, path) => {
+ const cleaned = new Map()
+ for (const [key, value] of res.entries())
+ cleaned.set(key, cleanOutput(value, path))
+ return cleaned
+}
+
+t.test('get-workspaces', async t => {
+ const path = t.testdir({
+ packages: {
+ a: {
+ 'package.json': JSON.stringify({
+ name: 'a',
+ version: '1.0.0',
+ scripts: { glorp: 'echo a doing the glerp glop' },
+ }),
+ },
+ b: {
+ 'package.json': JSON.stringify({
+ name: 'b',
+ version: '2.0.0',
+ scripts: { glorp: 'echo b doing the glerp glop' },
+ }),
+ },
+ c: {
+ 'package.json': JSON.stringify({
+ name: 'c',
+ version: '1.0.0',
+ scripts: {
+ test: 'exit 0',
+ posttest: 'echo posttest',
+ lorem: 'echo c lorem',
+ },
+ }),
+ },
+ d: {
+ 'package.json': JSON.stringify({
+ name: 'd',
+ version: '1.0.0',
+ scripts: {
+ test: 'exit 0',
+ posttest: 'echo posttest',
+ },
+ }),
+ },
+ e: {
+ 'package.json': JSON.stringify({
+ name: 'e',
+ scripts: { test: 'exit 0', start: 'echo start something' },
+ }),
+ },
+ noscripts: {
+ 'package.json': JSON.stringify({
+ name: 'noscripts',
+ version: '1.0.0',
+ }),
+ },
+ },
+ 'package.json': JSON.stringify({
+ name: 'x',
+ version: '1.2.3',
+ workspaces: ['packages/*'],
+ }),
+ })
+
+ let workspaces
+
+ workspaces = await getWorkspaces(['a', 'b'], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ a: '{PATH}/packages/a',
+ b: '{PATH}/packages/b',
+ })),
+ 'should filter by package name'
+ )
+
+ workspaces = await getWorkspaces(['./packages/c'], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ c: '{PATH}/packages/c',
+ })),
+ 'should filter by package directory'
+ )
+
+ workspaces = await getWorkspaces(['packages/c'], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ c: '{PATH}/packages/c',
+ })),
+ 'should filter by rel package directory'
+ )
+
+ workspaces = await getWorkspaces([resolve(path, 'packages/c')], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ c: '{PATH}/packages/c',
+ })),
+ 'should filter by absolute package directory'
+ )
+
+ workspaces = await getWorkspaces(['packages'], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ a: '{PATH}/packages/a',
+ b: '{PATH}/packages/b',
+ c: '{PATH}/packages/c',
+ d: '{PATH}/packages/d',
+ e: '{PATH}/packages/e',
+ noscripts: '{PATH}/packages/noscripts',
+ })),
+ 'should filter by parent directory name'
+ )
+
+ workspaces = await getWorkspaces(['./packages/'], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ a: '{PATH}/packages/a',
+ b: '{PATH}/packages/b',
+ c: '{PATH}/packages/c',
+ d: '{PATH}/packages/d',
+ e: '{PATH}/packages/e',
+ noscripts: '{PATH}/packages/noscripts',
+ })),
+ 'should filter by parent directory path'
+ )
+
+ workspaces = await getWorkspaces([resolve(path, './packages')], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ a: '{PATH}/packages/a',
+ b: '{PATH}/packages/b',
+ c: '{PATH}/packages/c',
+ d: '{PATH}/packages/d',
+ e: '{PATH}/packages/e',
+ noscripts: '{PATH}/packages/noscripts',
+ })),
+ 'should filter by absolute parent directory path'
+ )
+
+ workspaces = await getWorkspaces([], { path })
+ t.deepEqual(
+ clean(workspaces, path),
+ new Map(Object.entries({
+ a: '{PATH}/packages/a',
+ b: '{PATH}/packages/b',
+ c: '{PATH}/packages/c',
+ d: '{PATH}/packages/d',
+ e: '{PATH}/packages/e',
+ noscripts: '{PATH}/packages/noscripts',
+ })),
+ 'should return all workspaces if no filter set'
+ )
+
+ try {
+ await getWorkspaces(['missing'], { path })
+ throw new Error('missed throw')
+ } catch (err) {
+ t.match(
+ err,
+ /No workspaces found/,
+ 'should throw no workspaces found error'
+ )
+ }
+
+ const unconfiguredWorkspaces = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'no-configured-workspaces',
+ version: '1.0.0',
+ }),
+ })
+ try {
+ await getWorkspaces([], { path: unconfiguredWorkspaces })
+ throw new Error('missed throw')
+ } catch (err) {
+ t.match(
+ err,
+ /No workspaces found/,
+ 'should throw no workspaces found error'
+ )
+ }
+})