File tree

10 files changed

+357
-197
lines changed

10 files changed

+357
-197
lines changed
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,9 @@ result in new modules being installed.
8080

8181
Using `npm find-dupes` will run the command in `--dry-run` mode.
8282

83-
Note that by default `npm dedupe` will not update the semver values of direct
84-
dependencies in your project `package.json`, if you want to also update
85-
values in `package.json` you can run: `npm dedupe --save` (or add the
86-
`save=true` option to a [configuration file](/configuring-npm/npmrc)
87-
to make that the default behavior).
83+
Note: `npm dedupe` will never update the semver values of direct
84+
dependencies in your project `package.json`, if you want to update
85+
values in `package.json` you can run: `npm update --save` instead.
8886

8987
### Configuration
9088

@@ -158,22 +156,6 @@ This configuration does not affect `npm ci`.
158156
<!-- automatically generated, do not edit manually -->
159157
<!-- see lib/utils/config/definitions.js -->
160158
161-
#### `save`
162-
163-
* Default: `true` unless when using `npm update` or `npm dedupe` where it
164-
defaults to `false`
165-
* Type: Boolean
166-
167-
Save installed packages to a `package.json` file as dependencies.
168-
169-
When used with the `npm rm` command, removes the dependency from
170-
`package.json`.
171-
172-
Will also prevent writing to `package-lock.json` if set to `false`.
173-
174-
<!-- automatically generated, do not edit manually -->
175-
<!-- see lib/utils/config/definitions.js -->
176-
177159
#### `omit`
178160
179161
* Default: 'dev' if the `NODE_ENV` environment variable is set to
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class Dedupe extends ArboristWorkspaceCmd {
1212
'legacy-bundling',
1313
'strict-peer-deps',
1414
'package-lock',
15-
'save',
1615
'omit',
1716
'ignore-scripts',
1817
'audit',
@@ -29,19 +28,17 @@ class Dedupe extends ArboristWorkspaceCmd {
2928
throw er
3029
}
3130

32-
// In the context of `npm dedupe` the save
33-
// config value should default to `false`
34-
const save = this.npm.config.isDefault('save')
35-
? false
36-
: this.npm.config.get('save')
37-
3831
const dryRun = this.npm.config.get('dry-run')
3932
const where = this.npm.prefix
4033
const opts = {
4134
...this.npm.flatOptions,
4235
path: where,
4336
dryRun,
44-
save,
37+
// Saving during dedupe would only update if one of your direct
38+
// dependencies was also duplicated somewhere in your tree. It would be
39+
// confusing if running this were to also update your package.json. In
40+
// order to reduce potential confusion we set this to false.
41+
save: false,
4542
workspaces: this.workspaceNames,
4643
}
4744
const arb = new Arborist(opts)
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,16 @@ class Npm extends EventEmitter {
131131

132132
const isGlobal = this.config.get('global')
133133
const workspacesEnabled = this.config.get('workspaces')
134+
// if cwd is a workspace, the default is set to [that workspace]
134135
const implicitWorkspace = this.config.get('workspace', 'default').length > 0
135136
const workspacesFilters = this.config.get('workspace')
136137
const includeWorkspaceRoot = this.config.get('include-workspace-root')
137138
// only call execWorkspaces when we have workspaces explicitly set
138139
// or when it is implicit and not in our ignore list
139140
const hasWorkspaceFilters = workspacesFilters.length > 0
140141
const invalidWorkspaceConfig = workspacesEnabled === false && hasWorkspaceFilters
142+
143+
// (-ws || -w foo) && (cwd is not a workspace || command is not ignoring implicit workspaces)
141144
const filterByWorkspaces = (workspacesEnabled || hasWorkspaceFilters) &&
142145
(!implicitWorkspace || !command.ignoreImplicitWorkspace)
143146
// normally this would go in the constructor, but our tests don't
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ npm dedupe
161161
162162
Options:
163163
[--global-style] [--legacy-bundling] [--strict-peer-deps] [--no-package-lock]
164-
[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]
165164
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--ignore-scripts]
166165
[--no-audit] [--no-bin-links] [--no-fund] [--dry-run]
167166
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,6 @@ All commands:
293293
294294
Options:
295295
[--global-style] [--legacy-bundling] [--strict-peer-deps] [--no-package-lock]
296-
[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]
297296
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--ignore-scripts]
298297
[--no-audit] [--no-bin-links] [--no-fund] [--dry-run]
299298
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ const LoadMockNpm = async (t, {
8383

8484
const { Npm, ...rest } = RealMockNpm(t, mocks)
8585

86+
// We want to fail fast when writing tests. Default this to 0 unless it was
87+
// explicitly set in a test.
88+
config = { 'fetch-retries': 0, ...config }
89+
8690
if (!init && load) {
8791
throw new Error('cant `load` without `init`')
8892
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Mock registry class
3+
*
4+
* This should end up as the centralized place where we generate test fixtures
5+
* for tests against any registry data.
6+
*/
7+
const pacote = require('pacote')
8+
class MockRegistry {
9+
#tap
10+
#nock
11+
#registry
12+
#authorization
13+
14+
constructor (opts) {
15+
if (!opts.registry) {
16+
throw new Error('mock registry requires a registry value')
17+
}
18+
this.#registry = (new URL(opts.registry)).origin
19+
this.#authorization = opts.authorization
20+
// Required for this.package
21+
this.#tap = opts.tap
22+
}
23+
24+
get nock () {
25+
if (!this.#nock) {
26+
const tnock = require('./tnock.js')
27+
const reqheaders = {}
28+
if (this.#authorization) {
29+
reqheaders.authorization = `Bearer ${this.#authorization}`
30+
}
31+
this.#nock = tnock(this.#tap, this.#registry, { reqheaders })
32+
}
33+
return this.#nock
34+
}
35+
36+
set nock (nock) {
37+
this.#nock = nock
38+
}
39+
40+
async package ({ manifest, times = 1, query, tarballs }) {
41+
let nock = this.nock
42+
if (!nock) {
43+
throw new Error('cannot mock packages without a tap fixture')
44+
}
45+
nock = nock.get(`/${manifest.name}`).times(times)
46+
if (query) {
47+
nock = nock.query(query)
48+
}
49+
nock = nock.reply(200, manifest)
50+
if (tarballs) {
51+
for (const version in manifest.versions) {
52+
const packument = manifest.versions[version]
53+
const dist = new URL(packument.dist.tarball)
54+
const tarball = await pacote.tarball(tarballs[version])
55+
nock.get(dist.pathname).reply(200, tarball)
56+
}
57+
}
58+
this.nock = nock
59+
}
60+
61+
// the last packument in the packuments array will be tagged as latest
62+
manifest ({ name = 'test-package', packuments } = {}) {
63+
packuments = this.packuments(packuments, name)
64+
const latest = packuments.slice(-1)[0]
65+
const manifest = {
66+
_id: `${name}@${latest.version}`,
67+
_rev: '00-testdeadbeef',
68+
name,
69+
description: 'test package mock manifest',
70+
dependencies: {},
71+
versions: {},
72+
time: {},
73+
'dist-tags': { latest: latest.version },
74+
...latest,
75+
}
76+
77+
for (const packument of packuments) {
78+
manifest.versions[packument.version] = {
79+
_id: `${name}@${packument.version}`,
80+
name,
81+
description: 'test package mock manifest',
82+
dependencies: {},
83+
dist: {
84+
tarball: `${this.#registry}/${name}/-/${name}-${packument.version}.tgz`,
85+
},
86+
...packument,
87+
}
88+
manifest.time[packument.version] = new Date()
89+
}
90+
91+
return manifest
92+
}
93+
94+
packuments (packuments = ['1.0.0'], name) {
95+
return packuments.map(p => this.packument(p, name))
96+
}
97+
98+
// Generate packument from shorthand
99+
packument (packument, name = 'test-package') {
100+
if (!packument.version) {
101+
packument = { version: packument }
102+
}
103+
return {
104+
name,
105+
version: '1.0.0',
106+
description: 'mocked test package',
107+
dependencies: {},
108+
...packument,
109+
}
110+
}
111+
}
112+
113+
module.exports = MockRegistry
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
const t = require('tap')
22
const { load: loadMockNpm } = require('../../fixtures/mock-npm')
3+
const path = require('path')
4+
const fs = require('fs')
5+
6+
const MockRegistry = require('../../fixtures/mock-registry.js')
37

48
t.test('should throw in global mode', async (t) => {
59
const { npm } = await loadMockNpm(t, {
@@ -14,45 +18,78 @@ t.test('should throw in global mode', async (t) => {
1418
)
1519
})
1620

17-
t.test('should remove dupes using Arborist', async (t) => {
18-
t.plan(5)
19-
const { npm } = await loadMockNpm(t, {
20-
mocks: {
21-
'@npmcli/arborist': function (args) {
22-
t.ok(args, 'gets options object')
23-
t.ok(args.path, 'gets path option')
24-
t.ok(args.dryRun, 'gets dryRun from user')
25-
this.dedupe = () => {
26-
t.ok(true, 'dedupe is called')
27-
}
28-
},
29-
'../../lib/utils/reify-finish.js': (npm, arb) => {
30-
t.ok(arb, 'gets arborist tree')
21+
const testTop = {
22+
name: 'test-top',
23+
version: '1.0.0',
24+
dependencies: {
25+
'test-dep-a': '*',
26+
'test-dep-b': '*',
27+
},
28+
}
29+
const testDepA = {
30+
name: 'test-dep-a',
31+
version: '1.0.1',
32+
dependencies: { 'test-sub': '*' },
33+
}
34+
const testDepB = {
35+
name: 'test-dep-b',
36+
version: '1.0.0',
37+
dependencies: { 'test-sub': '*' },
38+
}
39+
const testSub = {
40+
name: 'test-sub',
41+
version: '1.0.0',
42+
}
43+
44+
const treeWithDupes = {
45+
'package.json': JSON.stringify(testTop),
46+
node_modules: {
47+
'test-dep-a': {
48+
'package.json': JSON.stringify(testDepA),
49+
node_modules: {
50+
'test-sub': {
51+
'package.json': JSON.stringify(testSub),
52+
},
3153
},
3254
},
33-
config: {
34-
'dry-run': 'true',
55+
'test-dep-b': {
56+
'package.json': JSON.stringify(testDepB),
57+
node_modules: {
58+
'test-sub': {
59+
'package.json': JSON.stringify(testSub),
60+
},
61+
},
3562
},
63+
},
64+
}
65+
66+
t.test('dedupe', async (t) => {
67+
const { npm, joinedOutput } = await loadMockNpm(t, {
68+
prefixDir: treeWithDupes,
69+
})
70+
const registry = new MockRegistry({
71+
tap: t,
72+
registry: npm.config.get('registry'),
73+
})
74+
const manifestSub = registry.manifest({
75+
name: 'test-sub',
76+
packuments: [{ version: '1.0.0' }],
3677
})
37-
await npm.exec('dedupe', [])
38-
})
3978

40-
t.test('should remove dupes using Arborist - no arguments', async (t) => {
41-
t.plan(2)
42-
const { npm } = await loadMockNpm(t, {
43-
mocks: {
44-
'@npmcli/arborist': function (args) {
45-
t.ok(args.dryRun, 'gets dryRun from config')
46-
t.ok(args.save, 'gets user-set save value from config')
47-
this.dedupe = () => {}
48-
},
49-
'../../lib/utils/reify-output.js': () => {},
50-
'../../lib/utils/reify-finish.js': () => {},
51-
},
52-
config: {
53-
'dry-run': true,
54-
save: true,
79+
await registry.package({
80+
manifest: manifestSub,
81+
tarballs: {
82+
'1.0.0': path.join(npm.prefix, 'node_modules', 'test-dep-a', 'node_modules', 'test-sub'),
5583
},
5684
})
5785
await npm.exec('dedupe', [])
86+
t.match(joinedOutput(), /added 1 package, and removed 2 packages/)
87+
t.ok(fs.existsSync(path.join(npm.prefix, 'node_modules', 'test-sub')), 'test-sub was hoisted')
88+
t.notOk(
89+
fs.existsSync(path.join(npm.prefix, 'node_modules', 'test-dep-a', 'node_modules', 'test-sub')),
90+
'test-dep-a/test-sub was removed'
91+
)
92+
t.notOk(
93+
fs.existsSync(path.join(npm.prefix, 'node_modules', 'test-dep-b', 'node_modules', 'test-sub')),
94+
'test-dep-b/test-sub was removed')
5895
})

0 commit comments

Comments
 (0)