Skip to content

Commit ef668ab

Browse files
committed
feat(diff): add workspace support
Refactored a bit so that we can more easily change the `top` and `prefix` params that were being used, and are different under the workspace context. PR-URL: #3368 Credit: @wraithgar Close: #3368 Reviewed-by: @ruyadorno
1 parent 992799c commit ef668ab

File tree

7 files changed

+242
-173
lines changed

7 files changed

+242
-173
lines changed

docs/content/commands/npm-diff.md

+32
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,38 @@ command, if no explicit tag is given.
242242
When used by the `npm diff` command, this is the tag used to fetch the
243243
tarball that will be compared with the local files by default.
244244
245+
#### `workspace`
246+
247+
* Default:
248+
* Type: String (can be set multiple times)
249+
250+
Enable running a command in the context of the configured workspaces of the
251+
current project while filtering by running only the workspaces defined by
252+
this configuration option.
253+
254+
Valid values for the `workspace` config are either:
255+
256+
* Workspace names
257+
* Path to a workspace directory
258+
* Path to a parent workspace directory (will result to selecting all of the
259+
nested workspaces)
260+
261+
When set for the `npm init` command, this may be set to the folder of a
262+
workspace which does not yet exist, to create the folder and set it up as a
263+
brand new workspace within the project.
264+
265+
This value is not exported to the environment for child processes.
266+
267+
#### `workspaces`
268+
269+
* Default: false
270+
* Type: Boolean
271+
272+
Enable running a command in the context of **all** the configured
273+
workspaces.
274+
275+
This value is not exported to the environment for child processes.
276+
245277
<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->
246278
## See Also
247279

lib/diff.js

+74-72
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
const { resolve } = require('path')
22

33
const semver = require('semver')
4-
const libdiff = require('libnpmdiff')
4+
const libnpmdiff = require('libnpmdiff')
55
const npa = require('npm-package-arg')
66
const Arborist = require('@npmcli/arborist')
77
const npmlog = require('npmlog')
88
const pacote = require('pacote')
99
const pickManifest = require('npm-pick-manifest')
1010

11+
const getWorkspaces = require('./workspaces/get-workspaces.js')
1112
const readPackageName = require('./utils/read-package-name.js')
1213
const BaseCommand = require('./base-command.js')
1314

@@ -25,10 +26,6 @@ class Diff extends BaseCommand {
2526
static get usage () {
2627
return [
2728
'[...<paths>]',
28-
'--diff=<pkg-name> [...<paths>]',
29-
'--diff=<version-a> [--diff=<version-b>] [...<paths>]',
30-
'--diff=<spec-a> [--diff=<spec-b>] [...<paths>]',
31-
'[--diff-ignore-all-space] [--diff-name-only] [...<paths>] [...<paths>]',
3229
]
3330
}
3431

@@ -45,19 +42,19 @@ class Diff extends BaseCommand {
4542
'diff-text',
4643
'global',
4744
'tag',
45+
'workspace',
46+
'workspaces',
4847
]
4948
}
5049

51-
get where () {
52-
const globalTop = resolve(this.npm.globalDir, '..')
53-
const global = this.npm.config.get('global')
54-
return global ? globalTop : this.npm.prefix
55-
}
56-
5750
exec (args, cb) {
5851
this.diff(args).then(() => cb()).catch(cb)
5952
}
6053

54+
execWorkspaces (args, filters, cb) {
55+
this.diffWorkspaces(args, filters).then(() => cb()).catch(cb)
56+
}
57+
6158
async diff (args) {
6259
const specs = this.npm.config.get('diff').filter(d => d)
6360
if (specs.length > 2) {
@@ -67,86 +64,96 @@ class Diff extends BaseCommand {
6764
)
6865
}
6966

67+
// diffWorkspaces may have set this already
68+
if (!this.prefix)
69+
this.prefix = this.npm.prefix
70+
71+
// this is the "top" directory, one up from node_modules
72+
// in global mode we have to walk one up from globalDir because our
73+
// node_modules is sometimes under ./lib, and in global mode we're only ever
74+
// walking through node_modules (because we will have been given a package
75+
// name already)
76+
if (this.npm.config.get('global'))
77+
this.top = resolve(this.npm.globalDir, '..')
78+
else
79+
this.top = this.prefix
80+
7081
const [a, b] = await this.retrieveSpecs(specs)
7182
npmlog.info('diff', { src: a, dst: b })
7283

73-
const res = await libdiff([a, b], {
84+
const res = await libnpmdiff([a, b], {
7485
...this.npm.flatOptions,
7586
diffFiles: args,
76-
where: this.where,
87+
where: this.top,
7788
})
7889
return this.npm.output(res)
7990
}
8091

81-
async retrieveSpecs ([a, b]) {
82-
// no arguments, defaults to comparing cwd
83-
// to its latest published registry version
84-
if (!a)
85-
return this.defaultSpec()
86-
87-
// single argument, used to compare wanted versions of an
88-
// installed dependency or to compare the cwd to a published version
89-
if (!b)
90-
return this.transformSingleSpec(a)
91-
92-
const specs = await this.convertVersionsToSpecs([a, b])
93-
return this.findVersionsByPackageName(specs)
92+
async diffWorkspaces (args, filters) {
93+
const workspaces =
94+
await getWorkspaces(filters, { path: this.npm.localPrefix })
95+
for (const workspacePath of workspaces.values()) {
96+
this.top = workspacePath
97+
this.prefix = workspacePath
98+
await this.diff(args)
99+
}
94100
}
95101

96-
async defaultSpec () {
97-
let noPackageJson
98-
let pkgName
102+
// get the package name from the packument at `path`
103+
// throws if no packument is present OR if it does not have `name` attribute
104+
async packageName (path) {
105+
let name
99106
try {
100-
pkgName = await readPackageName(this.npm.prefix)
107+
// TODO this won't work as expected in global mode
108+
name = await readPackageName(this.prefix)
101109
} catch (e) {
102110
npmlog.verbose('diff', 'could not read project dir package.json')
103-
noPackageJson = true
104111
}
105112

106-
if (!pkgName || noPackageJson) {
107-
throw new Error(
108-
'Needs multiple arguments to compare or run from a project dir.\n\n' +
109-
`Usage:\n${this.usage}`
110-
)
111-
}
113+
if (!name)
114+
throw this.usageError('Needs multiple arguments to compare or run from a project dir.\n')
112115

113-
return [
114-
`${pkgName}@${this.npm.config.get('tag')}`,
115-
`file:${this.npm.prefix}`,
116-
]
116+
return name
117117
}
118118

119-
async transformSingleSpec (a) {
119+
async retrieveSpecs ([a, b]) {
120+
if (a && b) {
121+
const specs = await this.convertVersionsToSpecs([a, b])
122+
return this.findVersionsByPackageName(specs)
123+
}
124+
125+
// no arguments, defaults to comparing cwd
126+
// to its latest published registry version
127+
if (!a) {
128+
const pkgName = await this.packageName(this.prefix)
129+
return [
130+
`${pkgName}@${this.npm.config.get('tag')}`,
131+
`file:${this.prefix}`,
132+
]
133+
}
134+
135+
// single argument, used to compare wanted versions of an
136+
// installed dependency or to compare the cwd to a published version
120137
let noPackageJson
121138
let pkgName
122139
try {
123-
pkgName = await readPackageName(this.npm.prefix)
140+
pkgName = await readPackageName(this.prefix)
124141
} catch (e) {
125142
npmlog.verbose('diff', 'could not read project dir package.json')
126143
noPackageJson = true
127144
}
128-
const missingPackageJson = new Error(
129-
'Needs multiple arguments to compare or run from a project dir.\n\n' +
130-
`Usage:\n${this.usage}`
131-
)
132145

133-
const specSelf = () => {
134-
if (noPackageJson)
135-
throw missingPackageJson
136-
137-
return `file:${this.npm.prefix}`
138-
}
146+
const missingPackageJson = this.usageError('Needs multiple arguments to compare or run from a project dir.\n')
139147

140148
// using a valid semver range, that means it should just diff
141149
// the cwd against a published version to the registry using the
142150
// same project name and the provided semver range
143151
if (semver.validRange(a)) {
144152
if (!pkgName)
145153
throw missingPackageJson
146-
147154
return [
148155
`${pkgName}@${a}`,
149-
specSelf(),
156+
`file:${this.prefix}`,
150157
]
151158
}
152159

@@ -160,7 +167,7 @@ class Diff extends BaseCommand {
160167
try {
161168
const opts = {
162169
...this.npm.flatOptions,
163-
path: this.where,
170+
path: this.top,
164171
}
165172
const arb = new Arborist(opts)
166173
actualTree = await arb.loadActual(opts)
@@ -172,9 +179,11 @@ class Diff extends BaseCommand {
172179
}
173180

174181
if (!node || !node.name || !node.package || !node.package.version) {
182+
if (noPackageJson)
183+
throw missingPackageJson
175184
return [
176185
`${spec.name}@${spec.fetchSpec}`,
177-
specSelf(),
186+
`file:${this.prefix}`,
178187
]
179188
}
180189

@@ -220,14 +229,10 @@ class Diff extends BaseCommand {
220229
} else if (spec.type === 'directory') {
221230
return [
222231
`file:${spec.fetchSpec}`,
223-
specSelf(),
232+
`file:${this.prefix}`,
224233
]
225-
} else {
226-
throw new Error(
227-
'Spec type not supported.\n\n' +
228-
`Usage:\n${this.usage}`
229-
)
230-
}
234+
} else
235+
throw this.usageError(`Spec type ${spec.type} not supported.\n`)
231236
}
232237

233238
async convertVersionsToSpecs ([a, b]) {
@@ -238,17 +243,14 @@ class Diff extends BaseCommand {
238243
if (semverA && semverB) {
239244
let pkgName
240245
try {
241-
pkgName = await readPackageName(this.npm.prefix)
246+
pkgName = await readPackageName(this.prefix)
242247
} catch (e) {
243248
npmlog.verbose('diff', 'could not read project dir package.json')
244249
}
245250

246-
if (!pkgName) {
247-
throw new Error(
248-
'Needs to be run from a project dir in order to diff two versions.\n\n' +
249-
`Usage:\n${this.usage}`
250-
)
251-
}
251+
if (!pkgName)
252+
throw this.usageError('Needs to be run from a project dir in order to diff two versions.\n')
253+
252254
return [`${pkgName}@${a}`, `${pkgName}@${b}`]
253255
}
254256

@@ -269,7 +271,7 @@ class Diff extends BaseCommand {
269271
try {
270272
const opts = {
271273
...this.npm.flatOptions,
272-
path: this.where,
274+
path: this.top,
273275
}
274276
const arb = new Arborist(opts)
275277
actualTree = await arb.loadActual(opts)

lib/utils/config/definition.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@ class Definition {
4949
if (!this.typeDescription)
5050
this.typeDescription = describeType(this.type)
5151
// hint is only used for non-boolean values
52-
if (!this.hint)
53-
this.hint = `<${this.key}>`
52+
if (!this.hint) {
53+
if (this.type === Number)
54+
this.hint = '<number>'
55+
else
56+
this.hint = `<${this.key}>`
57+
}
5458
if (!this.usage)
5559
this.usage = describeUsage(this)
5660
}

lib/utils/config/definitions.js

+3
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ define('dev', {
506506

507507
define('diff', {
508508
default: [],
509+
hint: '<pkg-name|spec|version>',
509510
type: [String, Array],
510511
description: `
511512
Define arguments to compare in \`npm diff\`.
@@ -545,6 +546,7 @@ define('diff-no-prefix', {
545546

546547
define('diff-dst-prefix', {
547548
default: 'b/',
549+
hint: '<path>',
548550
type: String,
549551
description: `
550552
Destination prefix to be used in \`npm diff\` output.
@@ -554,6 +556,7 @@ define('diff-dst-prefix', {
554556

555557
define('diff-src-prefix', {
556558
default: 'a/',
559+
hint: '<path>',
557560
type: String,
558561
description: `
559562
Source prefix to be used in \`npm diff\` output.

tap-snapshots/test/lib/load-all-commands.js.test.cjs

+5-7
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,14 @@ The registry diff command
199199
200200
Usage:
201201
npm diff [...<paths>]
202-
npm diff --diff=<pkg-name> [...<paths>]
203-
npm diff --diff=<version-a> [--diff=<version-b>] [...<paths>]
204-
npm diff --diff=<spec-a> [--diff=<spec-b>] [...<paths>]
205-
npm diff [--diff-ignore-all-space] [--diff-name-only] [...<paths>] [...<paths>]
206202
207203
Options:
208-
[--diff <diff> [--diff <diff> ...]] [--diff-name-only]
209-
[--diff-unified <diff-unified>] [--diff-ignore-all-space] [--diff-no-prefix]
210-
[--diff-src-prefix <diff-src-prefix>] [--diff-dst-prefix <diff-dst-prefix>]
204+
[--diff <pkg-name|spec|version> [--diff <pkg-name|spec|version> ...]]
205+
[--diff-name-only] [--diff-unified <number>] [--diff-ignore-all-space]
206+
[--diff-no-prefix] [--diff-src-prefix <path>] [--diff-dst-prefix <path>]
211207
[--diff-text] [-g|--global] [--tag <tag>]
208+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
209+
[-ws|--workspaces]
212210
213211
Run "npm help diff" for more info
214212
`

tap-snapshots/test/lib/utils/npm-usage.js.test.cjs

+5-7
Original file line numberDiff line numberDiff line change
@@ -336,16 +336,14 @@ All commands:
336336
337337
Usage:
338338
npm diff [...<paths>]
339-
npm diff --diff=<pkg-name> [...<paths>]
340-
npm diff --diff=<version-a> [--diff=<version-b>] [...<paths>]
341-
npm diff --diff=<spec-a> [--diff=<spec-b>] [...<paths>]
342-
npm diff [--diff-ignore-all-space] [--diff-name-only] [...<paths>] [...<paths>]
343339
344340
Options:
345-
[--diff <diff> [--diff <diff> ...]] [--diff-name-only]
346-
[--diff-unified <diff-unified>] [--diff-ignore-all-space] [--diff-no-prefix]
347-
[--diff-src-prefix <diff-src-prefix>] [--diff-dst-prefix <diff-dst-prefix>]
341+
[--diff <pkg-name|spec|version> [--diff <pkg-name|spec|version> ...]]
342+
[--diff-name-only] [--diff-unified <number>] [--diff-ignore-all-space]
343+
[--diff-no-prefix] [--diff-src-prefix <path>] [--diff-dst-prefix <path>]
348344
[--diff-text] [-g|--global] [--tag <tag>]
345+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
346+
[-ws|--workspaces]
349347
350348
Run "npm help diff" for more info
351349

0 commit comments

Comments
 (0)