diff --git a/docs/content/commands/npm-diff.md b/docs/content/commands/npm-diff.md index 57d20727a7533..479cb63b11213 100644 --- a/docs/content/commands/npm-diff.md +++ b/docs/content/commands/npm-diff.md @@ -242,6 +242,38 @@ command, if no explicit tag is given. When used by the `npm diff` command, this is the tag used to fetch the tarball that will be compared with the local files by default. +#### `workspace` + +* Default: +* Type: String (can be set multiple times) + +Enable running a command in the context of the configured workspaces of the +current project while filtering by running only the workspaces defined by +this configuration option. + +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 + nested workspaces) + +When set for the `npm init` command, this may be set to the folder of a +workspace which does not yet exist, to create the folder and set it up as a +brand new workspace within the project. + +This value is not exported to the environment for child processes. + +#### `workspaces` + +* Default: false +* Type: Boolean + +Enable running a command in the context of **all** the configured +workspaces. + +This value is not exported to the environment for child processes. + ## See Also diff --git a/lib/diff.js b/lib/diff.js index 58834ca9c2674..d315551d443a5 100644 --- a/lib/diff.js +++ b/lib/diff.js @@ -1,13 +1,14 @@ const { resolve } = require('path') const semver = require('semver') -const libdiff = require('libnpmdiff') +const libnpmdiff = require('libnpmdiff') const npa = require('npm-package-arg') const Arborist = require('@npmcli/arborist') const npmlog = require('npmlog') const pacote = require('pacote') const pickManifest = require('npm-pick-manifest') +const getWorkspaces = require('./workspaces/get-workspaces.js') const readPackageName = require('./utils/read-package-name.js') const BaseCommand = require('./base-command.js') @@ -25,10 +26,6 @@ class Diff extends BaseCommand { static get usage () { return [ '[...]', - '--diff= [...]', - '--diff= [--diff=] [...]', - '--diff= [--diff=] [...]', - '[--diff-ignore-all-space] [--diff-name-only] [...] [...]', ] } @@ -45,19 +42,19 @@ class Diff extends BaseCommand { 'diff-text', 'global', 'tag', + 'workspace', + 'workspaces', ] } - get where () { - const globalTop = resolve(this.npm.globalDir, '..') - const global = this.npm.config.get('global') - return global ? globalTop : this.npm.prefix - } - exec (args, cb) { this.diff(args).then(() => cb()).catch(cb) } + execWorkspaces (args, filters, cb) { + this.diffWorkspaces(args, filters).then(() => cb()).catch(cb) + } + async diff (args) { const specs = this.npm.config.get('diff').filter(d => d) if (specs.length > 2) { @@ -67,75 +64,86 @@ class Diff extends BaseCommand { ) } + // diffWorkspaces may have set this already + if (!this.prefix) + this.prefix = this.npm.prefix + + // this is the "top" directory, one up from node_modules + // in global mode we have to walk one up from globalDir because our + // node_modules is sometimes under ./lib, and in global mode we're only ever + // walking through node_modules (because we will have been given a package + // name already) + if (this.npm.config.get('global')) + this.top = resolve(this.npm.globalDir, '..') + else + this.top = this.prefix + const [a, b] = await this.retrieveSpecs(specs) npmlog.info('diff', { src: a, dst: b }) - const res = await libdiff([a, b], { + const res = await libnpmdiff([a, b], { ...this.npm.flatOptions, diffFiles: args, - where: this.where, + where: this.top, }) return this.npm.output(res) } - async retrieveSpecs ([a, b]) { - // no arguments, defaults to comparing cwd - // to its latest published registry version - if (!a) - return this.defaultSpec() - - // single argument, used to compare wanted versions of an - // installed dependency or to compare the cwd to a published version - if (!b) - return this.transformSingleSpec(a) - - const specs = await this.convertVersionsToSpecs([a, b]) - return this.findVersionsByPackageName(specs) + async diffWorkspaces (args, filters) { + const workspaces = + await getWorkspaces(filters, { path: this.npm.localPrefix }) + for (const workspacePath of workspaces.values()) { + this.top = workspacePath + this.prefix = workspacePath + await this.diff(args) + } } - async defaultSpec () { - let noPackageJson - let pkgName + // get the package name from the packument at `path` + // throws if no packument is present OR if it does not have `name` attribute + async packageName (path) { + let name try { - pkgName = await readPackageName(this.npm.prefix) + // TODO this won't work as expected in global mode + name = await readPackageName(this.prefix) } catch (e) { npmlog.verbose('diff', 'could not read project dir package.json') - noPackageJson = true } - if (!pkgName || noPackageJson) { - throw new Error( - 'Needs multiple arguments to compare or run from a project dir.\n\n' + - `Usage:\n${this.usage}` - ) - } + if (!name) + throw this.usageError('Needs multiple arguments to compare or run from a project dir.\n') - return [ - `${pkgName}@${this.npm.config.get('tag')}`, - `file:${this.npm.prefix}`, - ] + return name } - async transformSingleSpec (a) { + async retrieveSpecs ([a, b]) { + if (a && b) { + const specs = await this.convertVersionsToSpecs([a, b]) + return this.findVersionsByPackageName(specs) + } + + // no arguments, defaults to comparing cwd + // to its latest published registry version + if (!a) { + const pkgName = await this.packageName(this.prefix) + return [ + `${pkgName}@${this.npm.config.get('tag')}`, + `file:${this.prefix}`, + ] + } + + // single argument, used to compare wanted versions of an + // installed dependency or to compare the cwd to a published version let noPackageJson let pkgName try { - pkgName = await readPackageName(this.npm.prefix) + pkgName = await readPackageName(this.prefix) } catch (e) { npmlog.verbose('diff', 'could not read project dir package.json') noPackageJson = true } - const missingPackageJson = new Error( - 'Needs multiple arguments to compare or run from a project dir.\n\n' + - `Usage:\n${this.usage}` - ) - const specSelf = () => { - if (noPackageJson) - throw missingPackageJson - - return `file:${this.npm.prefix}` - } + const missingPackageJson = this.usageError('Needs multiple arguments to compare or run from a project dir.\n') // using a valid semver range, that means it should just diff // the cwd against a published version to the registry using the @@ -143,10 +151,9 @@ class Diff extends BaseCommand { if (semver.validRange(a)) { if (!pkgName) throw missingPackageJson - return [ `${pkgName}@${a}`, - specSelf(), + `file:${this.prefix}`, ] } @@ -160,7 +167,7 @@ class Diff extends BaseCommand { try { const opts = { ...this.npm.flatOptions, - path: this.where, + path: this.top, } const arb = new Arborist(opts) actualTree = await arb.loadActual(opts) @@ -172,9 +179,11 @@ class Diff extends BaseCommand { } if (!node || !node.name || !node.package || !node.package.version) { + if (noPackageJson) + throw missingPackageJson return [ `${spec.name}@${spec.fetchSpec}`, - specSelf(), + `file:${this.prefix}`, ] } @@ -220,14 +229,10 @@ class Diff extends BaseCommand { } else if (spec.type === 'directory') { return [ `file:${spec.fetchSpec}`, - specSelf(), + `file:${this.prefix}`, ] - } else { - throw new Error( - 'Spec type not supported.\n\n' + - `Usage:\n${this.usage}` - ) - } + } else + throw this.usageError(`Spec type ${spec.type} not supported.\n`) } async convertVersionsToSpecs ([a, b]) { @@ -238,17 +243,14 @@ class Diff extends BaseCommand { if (semverA && semverB) { let pkgName try { - pkgName = await readPackageName(this.npm.prefix) + pkgName = await readPackageName(this.prefix) } catch (e) { npmlog.verbose('diff', 'could not read project dir package.json') } - if (!pkgName) { - throw new Error( - 'Needs to be run from a project dir in order to diff two versions.\n\n' + - `Usage:\n${this.usage}` - ) - } + if (!pkgName) + throw this.usageError('Needs to be run from a project dir in order to diff two versions.\n') + return [`${pkgName}@${a}`, `${pkgName}@${b}`] } @@ -269,7 +271,7 @@ class Diff extends BaseCommand { try { const opts = { ...this.npm.flatOptions, - path: this.where, + path: this.top, } const arb = new Arborist(opts) actualTree = await arb.loadActual(opts) diff --git a/lib/utils/config/definition.js b/lib/utils/config/definition.js index 5ec2ba1ab1d58..507be6a644042 100644 --- a/lib/utils/config/definition.js +++ b/lib/utils/config/definition.js @@ -49,8 +49,12 @@ class Definition { if (!this.typeDescription) this.typeDescription = describeType(this.type) // hint is only used for non-boolean values - if (!this.hint) - this.hint = `<${this.key}>` + if (!this.hint) { + if (this.type === Number) + this.hint = '' + else + this.hint = `<${this.key}>` + } if (!this.usage) this.usage = describeUsage(this) } diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index ea9665b5431f5..ce7702aaa4f79 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -506,6 +506,7 @@ define('dev', { define('diff', { default: [], + hint: '', type: [String, Array], description: ` Define arguments to compare in \`npm diff\`. @@ -545,6 +546,7 @@ define('diff-no-prefix', { define('diff-dst-prefix', { default: 'b/', + hint: '', type: String, description: ` Destination prefix to be used in \`npm diff\` output. @@ -554,6 +556,7 @@ define('diff-dst-prefix', { define('diff-src-prefix', { default: 'a/', + hint: '', type: String, description: ` Source prefix to be used in \`npm diff\` output. diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index d40be42868184..097123d46a3cc 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -199,16 +199,14 @@ The registry diff command Usage: npm diff [...] -npm diff --diff= [...] -npm diff --diff= [--diff=] [...] -npm diff --diff= [--diff=] [...] -npm diff [--diff-ignore-all-space] [--diff-name-only] [...] [...] Options: -[--diff [--diff ...]] [--diff-name-only] -[--diff-unified ] [--diff-ignore-all-space] [--diff-no-prefix] -[--diff-src-prefix ] [--diff-dst-prefix ] +[--diff [--diff ...]] +[--diff-name-only] [--diff-unified ] [--diff-ignore-all-space] +[--diff-no-prefix] [--diff-src-prefix ] [--diff-dst-prefix ] [--diff-text] [-g|--global] [--tag ] +[-w|--workspace [-w|--workspace ...]] +[-ws|--workspaces] Run "npm help diff" for more info ` diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index 7fdcf0c5d2dba..54f6c3d2feb2a 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -336,16 +336,14 @@ All commands: Usage: npm diff [...] - npm diff --diff= [...] - npm diff --diff= [--diff=] [...] - npm diff --diff= [--diff=] [...] - npm diff [--diff-ignore-all-space] [--diff-name-only] [...] [...] Options: - [--diff [--diff ...]] [--diff-name-only] - [--diff-unified ] [--diff-ignore-all-space] [--diff-no-prefix] - [--diff-src-prefix ] [--diff-dst-prefix ] + [--diff [--diff ...]] + [--diff-name-only] [--diff-unified ] [--diff-ignore-all-space] + [--diff-no-prefix] [--diff-src-prefix ] [--diff-dst-prefix ] [--diff-text] [-g|--global] [--tag ] + [-w|--workspace [-w|--workspace ...]] + [-ws|--workspaces] Run "npm help diff" for more info diff --git a/test/lib/diff.js b/test/lib/diff.js index 7a52ea5ee0ae1..993dfa4d60718 100644 --- a/test/lib/diff.js +++ b/test/lib/diff.js @@ -1,10 +1,9 @@ -const { resolve } = require('path') +const { resolve, join } = require('path') const t = require('tap') const mockNpm = require('../fixtures/mock-npm') const noop = () => null let libnpmdiff = noop -let rpn = () => 'foo' const config = { global: false, @@ -21,9 +20,11 @@ const flatOptions = { diffText: false, savePrefix: '^', } +const fooPath = t.testdir({ + 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }), +}) const npm = mockNpm({ - globalDir: __dirname, - prefix: '.', + prefix: fooPath, config, flatOptions, output: noop, @@ -33,7 +34,6 @@ const mocks = { npmlog: { info: noop, verbose: noop }, libnpmdiff: (...args) => libnpmdiff(...args), 'npm-registry-fetch': async () => ({}), - '../../lib/utils/read-package-name.js': async (prefix) => rpn(prefix), '../../lib/utils/usage.js': () => 'usage instructions', } @@ -49,10 +49,11 @@ t.afterEach(() => { flatOptions.diffDstPrefix = '' flatOptions.diffText = false flatOptions.savePrefix = '^' - npm.globalDir = __dirname - npm.prefix = '..' + npm.globalDir = fooPath + npm.prefix = fooPath libnpmdiff = noop - rpn = () => 'foo' + diff.prefix = undefined + diff.top = undefined }) const Diff = t.mock('../../lib/diff.js', mocks) @@ -62,23 +63,23 @@ t.test('no args', t => { t.test('in a project dir', t => { t.plan(3) - const path = t.testdir({}) libnpmdiff = async ([a, b], opts) => { t.equal(a, 'foo@latest', 'should have default spec comparison') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') t.match(opts, npm.flatOptions, 'should forward flat options') } - npm.prefix = path + npm.prefix = fooPath diff.exec([], err => { if (err) throw err + t.end() }) }) t.test('no args, missing package.json name in cwd', t => { - rpn = () => undefined - + const path = t.testdir({}) + npm.prefix = path diff.exec([], err => { t.match( err, @@ -89,10 +90,11 @@ t.test('no args', t => { }) }) - t.test('no args, missing package.json in cwd', t => { - rpn = () => { - throw new Error('ERR') - } + t.test('no args, bad package.json in cwd', t => { + const path = t.testdir({ + 'package.json': '{invalid"json', + }) + npm.prefix = path diff.exec([], err => { t.match( @@ -109,21 +111,16 @@ t.test('no args', t => { t.test('single arg', t => { t.test('spec using cwd package name', t => { - t.plan(4) + t.plan(3) - rpn = (prefix) => { - t.equal(prefix, path, 'read-package-name gets proper prefix') - return 'foo' - } - const path = t.testdir({}) libnpmdiff = async ([a, b], opts) => { t.equal(a, 'foo@1.0.0', 'should forward single spec') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') t.match(opts, npm.flatOptions, 'should forward flat options') } config.diff = ['foo@1.0.0'] - npm.prefix = path + npm.prefix = fooPath diff.exec([], err => { if (err) throw err @@ -133,9 +130,6 @@ t.test('single arg', t => { t.test('unknown spec, no package.json', t => { const path = t.testdir({}) - rpn = () => { - throw new Error('ERR') - } config.diff = ['foo@1.0.0'] npm.prefix = path @@ -152,15 +146,13 @@ t.test('single arg', t => { t.test('spec using semver range', t => { t.plan(3) - const path = t.testdir({}) libnpmdiff = async ([a, b], opts) => { t.equal(a, 'foo@~1.0.0', 'should forward single spec') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') t.match(opts, npm.flatOptions, 'should forward flat options') } config.diff = ['foo@~1.0.0'] - npm.prefix = path diff.exec([], err => { if (err) throw err @@ -170,15 +162,13 @@ t.test('single arg', t => { t.test('version', t => { t.plan(3) - const path = t.testdir({}) libnpmdiff = async ([a, b], opts) => { t.equal(a, 'foo@2.1.4', 'should convert to expected first spec') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') t.match(opts, npm.flatOptions, 'should forward flat options') } config.diff = ['2.1.4'] - npm.prefix = path diff.exec([], err => { if (err) throw err @@ -186,10 +176,8 @@ t.test('single arg', t => { }) t.test('version, no package.json', t => { - rpn = () => { - throw new Error('ERR') - } - + const path = t.testdir({}) + npm.prefix = path config.diff = ['2.1.4'] diff.exec([], err => { t.match( @@ -204,10 +192,9 @@ t.test('single arg', t => { t.test('version, filtering by files', t => { t.plan(3) - const path = t.testdir({}) libnpmdiff = async ([a, b], opts) => { t.equal(a, 'foo@2.1.4', 'should use expected spec') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') t.match(opts, { ...npm.flatOptions, diffFiles: [ @@ -218,7 +205,6 @@ t.test('single arg', t => { } config.diff = ['2.1.4'] - npm.prefix = path diff.exec(['./foo.js', './bar.js'], err => { if (err) throw err @@ -277,9 +263,6 @@ t.test('single arg', t => { t.test('unknown package name, no package.json', t => { const path = t.testdir({}) - rpn = () => { - throw new Error('ERR') - } config.diff = ['bar'] npm.prefix = path @@ -531,8 +514,9 @@ t.test('single arg', t => { t.test('unknown package name', t => { t.plan(2) - const path = t.testdir({}) - rpn = async () => undefined + const path = t.testdir({ + 'package.json': JSON.stringify({ version: '1.0.0' }), + }) libnpmdiff = async ([a, b], opts) => { t.equal(a, 'bar@latest', 'should target latest tag of name') t.equal(b, `file:${path}`, 'should compare to cwd') @@ -550,15 +534,12 @@ t.test('single arg', t => { t.test('use project name in project dir', t => { t.plan(2) - const path = t.testdir({}) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { - t.equal(a, 'my-project@latest', 'should target latest tag of name') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(a, 'foo@latest', 'should target latest tag of name') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') } - config.diff = ['my-project'] - npm.prefix = path + config.diff = ['foo'] diff.exec([], err => { if (err) throw err @@ -568,15 +549,12 @@ t.test('single arg', t => { t.test('dir spec type', t => { t.plan(2) - const path = t.testdir({}) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, 'file:/path/to/other-dir', 'should target dir') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') } config.diff = ['/path/to/other-dir'] - npm.prefix = path diff.exec([], err => { if (err) throw err @@ -584,14 +562,11 @@ t.test('single arg', t => { }) t.test('unsupported spec type', t => { - rpn = async () => 'my-project' - config.diff = ['git+https://github.com/user/foo'] - diff.exec([], err => { t.match( err, - /Spec type not supported./, + /Spec type git not supported./, 'should throw spec type not supported error.' ) t.end() @@ -638,7 +613,6 @@ t.test('first arg is a qualified spec', t => { }), }) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, 'bar@2.0.0', 'should set expected first spec') t.equal(b, `bar@file:${resolve(path, 'node_modules/bar')}`, 'should target local node_modules pkg') @@ -707,7 +681,6 @@ t.test('first arg is a known dependency name', t => { }), }) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, `bar@file:${resolve(path, 'node_modules/bar')}`, 'should target local node_modules pkg') t.equal(b, 'bar@2.0.0', 'should set expected second spec') @@ -747,7 +720,6 @@ t.test('first arg is a known dependency name', t => { }), }) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, `bar@file:${resolve(path, 'node_modules/bar')}`, 'should target local node_modules pkg') t.equal(b, `bar-fork@file:${resolve(path, 'node_modules/bar-fork')}`, 'should target fork local node_modules pkg') @@ -781,7 +753,6 @@ t.test('first arg is a known dependency name', t => { }), }) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, `bar@file:${resolve(path, 'node_modules/bar')}`, 'should target local node_modules pkg') t.equal(b, 'bar@2.0.0', 'should use package name from first arg') @@ -815,7 +786,6 @@ t.test('first arg is a known dependency name', t => { }), }) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, `bar@file:${resolve(path, 'node_modules/bar')}`, 'should target local node_modules pkg') t.equal(b, 'bar-fork@latest', 'should set expected second spec') @@ -869,7 +839,6 @@ t.test('first arg is a valid semver range', t => { }), }) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, 'bar@1.0.0', 'should use name from second arg') t.equal(b, `bar@file:${resolve(path, 'node_modules/bar')}`, 'should set expected second spec from nm') @@ -886,10 +855,9 @@ t.test('first arg is a valid semver range', t => { t.test('second arg is ALSO a semver version', t => { t.plan(2) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { - t.equal(a, 'my-project@1.0.0', 'should use name from project dir') - t.equal(b, 'my-project@2.0.0', 'should use name from project dir') + t.equal(a, 'foo@1.0.0', 'should use name from project dir') + t.equal(b, 'foo@2.0.0', 'should use name from project dir') } config.diff = ['1.0.0', '2.0.0'] @@ -901,10 +869,6 @@ t.test('first arg is a valid semver range', t => { t.test('second arg is ALSO a semver version BUT cwd not a project dir', t => { const path = t.testdir({}) - rpn = () => { - throw new Error('ERR') - } - config.diff = ['1.0.0', '2.0.0'] npm.prefix = path diff.exec([], err => { @@ -920,7 +884,6 @@ t.test('first arg is a valid semver range', t => { t.test('second arg is an unknown dependency name', t => { t.plan(2) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, 'bar@1.0.0', 'should use name from second arg') t.equal(b, 'bar@latest', 'should compare against latest tag') @@ -944,7 +907,6 @@ t.test('first arg is a valid semver range', t => { const Diff = t.mock('../../lib/diff.js', { ...mocks, - '../../lib/utils/read-package-name.js': async () => 'my-project', '@npmcli/arborist': class { constructor () { throw new Error('ERR') @@ -977,7 +939,7 @@ t.test('first arg is an unknown dependency name', t => { t.equal(a, 'bar@latest', 'should set expected first spec') t.equal(b, 'bar@2.0.0', 'should set expected second spec') t.match(opts, npm.flatOptions, 'should forward flat options') - t.match(opts, { where: '.' }, 'should forward pacote options') + t.match(opts, { where: fooPath }, 'should forward pacote options') } config.diff = ['bar', 'bar@2.0.0'] @@ -1007,7 +969,6 @@ t.test('first arg is an unknown dependency name', t => { }), }) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { t.equal(a, 'bar-fork@latest', 'should use latest tag') t.equal(b, `bar@file:${resolve(path, 'node_modules/bar')}`, 'should target local node_modules pkg') @@ -1055,9 +1016,6 @@ t.test('first arg is an unknown dependency name', t => { t.plan(2) const path = t.testdir({}) - rpn = () => { - throw new Error('ERR') - } libnpmdiff = async ([a, b], opts) => { t.equal(a, 'bar@latest', 'should use latest tag') t.equal(b, 'bar-fork@latest', 'should use latest tag') @@ -1120,11 +1078,9 @@ t.test('various options', t => { t.test('set files no diff args', t => { t.plan(3) - const path = t.testdir({}) - rpn = async () => 'my-project' libnpmdiff = async ([a, b], opts) => { - t.equal(a, 'my-project@latest', 'should have default spec') - t.equal(b, `file:${path}`, 'should compare to cwd') + t.equal(a, 'foo@latest', 'should have default spec') + t.equal(b, `file:${fooPath}`, 'should compare to cwd') t.match(opts, { ...npm.flatOptions, diffFiles: [ @@ -1134,7 +1090,6 @@ t.test('various options', t => { }, 'should forward all remaining items as filenames') } - npm.prefix = path diff.exec(['./foo.js', './bar.js'], err => { if (err) throw err @@ -1183,3 +1138,80 @@ t.test('too many args', t => { t.end() }) }) + +t.test('workspaces', t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'workspaces-test', + version: '1.2.3-test', + workspaces: ['workspace-a', 'workspace-b', 'workspace-c'], + }), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.2.3-a', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.2.3-b', + }), + }, + 'workspace-c': JSON.stringify({ + 'package.json': { + name: 'workspace-n', + version: '1.2.3-n', + }, + }), + }) + + t.test('all workspaces', t => { + const diffCalls = [] + libnpmdiff = async ([a, b]) => { + diffCalls.push([a, b]) + } + npm.prefix = path + npm.localPrefix = path + diff.execWorkspaces([], [], (err) => { + if (err) + throw err + t.same(diffCalls, [ + ['workspace-a@latest', join(`file:${path}`, 'workspace-a')], + ['workspace-b@latest', join(`file:${path}`, 'workspace-b')], + ], 'should call libnpmdiff with workspaces params') + t.end() + }) + }) + + t.test('one workspace', t => { + const diffCalls = [] + libnpmdiff = async ([a, b]) => { + diffCalls.push([a, b]) + } + npm.prefix = path + npm.localPrefix = path + diff.execWorkspaces([], ['workspace-a'], (err) => { + if (err) + throw err + t.same(diffCalls, [ + ['workspace-a@latest', join(`file:${path}`, 'workspace-a')], + ], 'should call libnpmdiff with workspaces params') + t.end() + }) + }) + + t.test('invalid workspace', t => { + libnpmdiff = () => { + t.fail('should not call libnpmdiff') + } + npm.prefix = path + npm.localPrefix = path + diff.execWorkspaces([], ['workspace-x'], (err) => { + t.match(err, /No workspaces found/) + t.match(err, /workspace-x/) + t.end() + }) + }) + t.end() +})