Skip to content

Commit 55dc1d2

Browse files
committed
feat: add npm diff
- As proposed in RFC: npm/rfcs#144
1 parent 23f3d7d commit 55dc1d2

54 files changed

Lines changed: 9844 additions & 9 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/content/commands/npm-diff.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
title: npm-diff
3+
section: 1
4+
description: The registry diff command
5+
---
6+
7+
### Synopsis
8+
9+
```bash
10+
npm diff
11+
npm diff <pkg-name>
12+
npm diff <version-a> [<version-b>]
13+
npm diff <spec-a> [<spec-b>]
14+
```
15+
16+
### Description
17+
18+
Similar to its `git diff` counterpart, this command will print diff patches
19+
of files for packages published to the npm registry.
20+
21+
A variation of different arguments are supported, along with a range of
22+
familiar options from [git diff](https://git-scm.com/docs/git-diff#_options).
23+
24+
* `npm diff <spec-a> <spec-b>`
25+
26+
Compares two package versions using their registry specifiers, e.g:
27+
`npm diff [email protected] foo@^2.0.0`. It's also possible to compare across forks
28+
of any package, e.g: `npm diff [email protected] [email protected]`.
29+
30+
Any valid spec can be used, so that it's also possible to compare
31+
directories or git repositories, e.g: `npm diff foo@latest ./packages/foo`
32+
33+
* `npm diff` (in a package directory, no arguments):
34+
35+
If the package is published to the registry, `npm diff` will fetch the
36+
tarball version tagged as `latest` (this value can be configured using the
37+
`tag` option) and proceed to compare the contents of files present in that
38+
tarball, with the current files in your local file system.
39+
40+
This workflow provides a handy way for package authors to see what
41+
package-tracked files have been changed in comparison with the latest
42+
published version of that package.
43+
44+
* `npm diff <version-a> [<version-b>]`
45+
46+
Using `npm diff` along with semver-valid version numbers is a shorthand
47+
to compare different versions of the current package. It needs to be run
48+
from a package directory, such that for a package named `foo` running
49+
`npm diff 1.0.0 1.0.1` is the same as running
50+
`npm diff [email protected] [email protected]`. If only a single argument `<version-a>` is
51+
provided, then the current local file system is going to be compared
52+
against that version.
53+
54+
* `npm diff <pkg-name>`
55+
56+
When using a single package name (with no version or tag specifier) as an
57+
argument, `npm diff` will work in a similar way to
58+
[`npm-outdated`](npm-outdated) and reach for the registry to figure out
59+
what current published version of the package named <pkg-name> will satisfy
60+
its dependent declared semver-range. Once that specific version is known
61+
`npm diff` will print diff patches comparing the current version of
62+
<pkg-name> found in the local file system with that specific version
63+
returned by the registry.
64+
65+
* `npm diff <spec-a>` (single specifier argument)
66+
67+
Similar to using only a single package name, it's also possible to declare
68+
a full registry specifier version if you wish to compare the local version
69+
of a installed package with the specific version/tag/semver-range provided
70+
in `<spec-a>`. e.g: (assuming [email protected] is installed in the current
71+
`node_modules` folder) running `npm diff [email protected]` will effectively be
72+
an alias to `npm diff [email protected] [email protected]`.
73+
74+
#### Filtering files
75+
76+
It's possible to also specify file names or globs pattern matching in order to
77+
limit the result of diff patches to only a subset of files for a given package.
78+
79+
Given the fact that paths are also valid specs, a separator `--` is required
80+
when specifying sets of files to filter in diff. Any extra argument declared
81+
after `--` will be treated as a filenames/globs and diff results will be
82+
limited to files included or matched by those. e.g:
83+
84+
`npm diff foo@2 -- lib/* CHANGELOG.md`
85+
86+
Note: When using `npm diff` with two spec/version arguments, the separator `--`
87+
becomes redudant and can be removed, e.g: `npm diff [email protected] [email protected] lib/*`
88+
89+
### Configuration
90+
91+
#### name-only
92+
93+
* Type: Boolean
94+
* Default: false
95+
96+
When set to `true` running `npm diff` only returns the names of the files that
97+
have any difference.
98+
99+
#### unified
100+
101+
* Alias: `-U`
102+
* Type: number
103+
* Default: `3`
104+
105+
The number of lines of context to print in the unified diff format output.
106+
107+
#### ignore-all-space
108+
109+
* Alias: `-w`
110+
* Type: Boolean
111+
* Default: false
112+
113+
Ignore whitespace when comparing lines. This ignores differences even if one
114+
line has whitespace where the other line has none.
115+
116+
#### no-prefix
117+
118+
* Type: Boolean
119+
* Default: false
120+
121+
Do not show any source or destination prefix.
122+
123+
#### src-prefix
124+
125+
* Type: String
126+
* Default: `"a/"`
127+
128+
Show the given source prefix in diff patches headers instead of using "a/".
129+
130+
#### dst-prefix
131+
132+
* Type: String
133+
* Default: `"b/"`
134+
135+
Show the given source prefix in diff patches headers instead of using "b/".
136+
137+
#### text
138+
139+
* Alias: `-a`
140+
* Type: Boolean
141+
* Default: false
142+
143+
Treat all files as text.
144+
145+
#### tag
146+
147+
* Type: String
148+
* Default: `"latest"`
149+
150+
The tag used to fetch the tarball that will be compared with local file system
151+
files when running npm diff with no arguments.
152+
153+
154+
## See Also
155+
156+
* [npm outdated](/commands/npm-outdated)
157+
* [npm install](/commands/npm-install)
158+
* [npm config](/commands/npm-config)
159+
* [npm registry](/using-npm/registry)

lib/diff.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
const semver = require('semver')
2+
const libdiff = require('libnpmdiff')
3+
const npa = require('npm-package-arg')
4+
const Arborist = require('@npmcli/arborist')
5+
const npmlog = require('npmlog')
6+
const pacote = require('pacote')
7+
const pickManifest = require('npm-pick-manifest')
8+
9+
const npm = require('./npm.js')
10+
const usageUtil = require('./utils/usage.js')
11+
const output = require('./utils/output.js')
12+
const completion = require('./utils/completion/none.js')
13+
const readLocalPkg = require('./utils/read-local-package.js')
14+
15+
const usage = usageUtil(
16+
'diff',
17+
'npm diff' +
18+
'\nnpm diff [--ignore-all-space] [--name-only] [-- <path>...]' +
19+
'\nnpm diff <pkg-name>' +
20+
'\nnpm diff <version-a> [<version-b>]' +
21+
'\nnpm diff <spec-a> [<spec-b>]'
22+
)
23+
24+
const cmd = (args, cb) => diff(args).then(() => cb()).catch(cb)
25+
26+
const diff = async (args) => {
27+
const [a, b, ...files] = parseArgs(args)
28+
const specs = await retrieveSpecs([a, b])
29+
npmlog.info(`diff a:${specs.a} b:${specs.b}`)
30+
const res = await libdiff(specs, {
31+
...npm.flatOptions,
32+
...{ diffOpts: {
33+
files,
34+
...getDiffOpts(),
35+
}},
36+
})
37+
return output(res)
38+
}
39+
40+
const parseArgs = (args) => {
41+
const argv = npm.config.parsedArgv.cooked
42+
const sep = argv.indexOf('--')
43+
44+
if (sep > -1) {
45+
const files = argv.slice(sep + 1)
46+
const notFiles = argv.slice(0, sep)
47+
const [a, b] = args.map(arg => notFiles.includes(arg) ? arg : undefined)
48+
return [a, b, ...files]
49+
}
50+
51+
return args
52+
}
53+
54+
const retrieveSpecs = async (args) => {
55+
const [a, b] = await convertVersionsToSpecs(args)
56+
57+
if (!a) {
58+
const spec = await defaultSpec()
59+
return { a: spec }
60+
}
61+
62+
if (!b)
63+
return await transformSingleSpec(a)
64+
65+
return { a, b }
66+
}
67+
68+
const convertVersionsToSpecs = (args) =>
69+
Promise.all(args.map(async arg => {
70+
if (semver.valid(arg)) {
71+
let pkgName
72+
try {
73+
pkgName = await readLocalPkg()
74+
} catch (e) {}
75+
76+
if (!pkgName) {
77+
throw new Error(
78+
'Needs to be run from a project dir in order to use versions.\n\n' +
79+
`Usage:\n${usage}`
80+
)
81+
}
82+
83+
return `${pkgName}@${arg}`
84+
}
85+
return arg
86+
}))
87+
88+
const defaultSpec = async () => {
89+
let pkgName
90+
try {
91+
pkgName = await readLocalPkg()
92+
} catch (e) {}
93+
94+
if (!pkgName) {
95+
throw new Error(
96+
'Needs multiple arguments to compare or run from a project dir.\n\n' +
97+
`Usage:\n${usage}`
98+
)
99+
}
100+
101+
return `${pkgName}@${npm.flatOptions.defaultTag}`
102+
}
103+
104+
const transformSingleSpec = async (a) => {
105+
const spec = npa(a)
106+
let pkgName
107+
108+
try {
109+
pkgName = await readLocalPkg()
110+
} catch (e) {}
111+
112+
if (!pkgName) {
113+
throw new Error(
114+
'Needs to be run from a project dir in order to use a single package name.\n\n' +
115+
`Usage:\n${usage}`
116+
)
117+
}
118+
119+
// when using a single package name as arg and it's part of the current
120+
// install tree, then retrieve the current installed version and compare
121+
// it against the same value `npm outdated` would suggest you to update to
122+
if (spec.registry && spec.name !== pkgName) {
123+
const opts = {
124+
...npm.flatOptions,
125+
path: npm.flatOptions.prefix,
126+
}
127+
const arb = new Arborist(opts)
128+
const actualTree = await arb.loadActual(opts)
129+
const [node] = [
130+
...actualTree.inventory
131+
.query('name', spec.name)
132+
.values(),
133+
]
134+
135+
if (!node || !node.name || !node.package || !node.package.version) {
136+
throw new Error(
137+
`Package ${a} not found in the current installed tree.\n\n` +
138+
`Usage:\n${usage}`
139+
)
140+
}
141+
142+
const tryRootNodeSpec = () =>
143+
(actualTree.edgesOut.get(spec.name) || {}).spec
144+
145+
const tryAnySpec = () => {
146+
for (const edge of node.edgesIn)
147+
return edge.spec
148+
}
149+
150+
const aSpec = node.package.version
151+
152+
// finds what version of the package to compare against, if a exact
153+
// version or tag was passed than it should use that, otherwise
154+
// work from the top of the arborist tree to find the original semver
155+
// range declared in the package that depends on the package.
156+
let bSpec
157+
if (spec.rawSpec)
158+
bSpec = spec.rawSpec
159+
else {
160+
const bTargetVersion =
161+
tryRootNodeSpec()
162+
|| tryAnySpec()
163+
|| `${npm.flatOptions.savePrefix}${node.package.version}`
164+
165+
// figure out what to compare against,
166+
// follows same logic to npm outdated "Wanted" results
167+
const packument = await pacote.packument(spec, {
168+
...npm.flatOptions,
169+
preferOnline: true,
170+
})
171+
bSpec = pickManifest(
172+
packument,
173+
bTargetVersion,
174+
{ ...npm.flatOptions }
175+
).version
176+
}
177+
178+
return {
179+
a: `${spec.name}@${aSpec}`,
180+
b: `${spec.name}@${bSpec}`,
181+
}
182+
}
183+
184+
return { a }
185+
}
186+
187+
const getDiffOpts = () => ({
188+
nameOnly: npm.config.get('name-only', 'cli'),
189+
context: npm.config.get('unified', 'cli') ||
190+
npm.config.get('U', 'cli'),
191+
ignoreWhitespace: npm.config.get('ignore-all-space', 'cli') ||
192+
npm.config.get('w', 'cli'),
193+
noPrefix: npm.config.get('no-prefix', 'cli'),
194+
srcPrefix: npm.config.get('src-prefix', 'cli'),
195+
dstPrefix: npm.config.get('dst-prefix', 'cli'),
196+
text: npm.config.get('text', 'cli') ||
197+
npm.config.get('a', 'cli'),
198+
})
199+
200+
module.exports = Object.assign(cmd, { completion, usage })

lib/utils/cmd-list.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const cmdList = [
119119
'prefix',
120120
'bin',
121121
'whoami',
122+
'diff',
122123
'dist-tag',
123124
'ping',
124125

node_modules/.gitignore

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/@npmcli/disparity-colors/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/@npmcli/disparity-colors/LICENSE

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)