@npmcli/arborist 2.8.1 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/arborist/build-ideal-tree.js +20 -1
- package/lib/arborist/load-actual.js +4 -2
- package/lib/arborist/reify.js +79 -19
- package/lib/case-insensitive-map.js +48 -0
- package/lib/deepest-nesting-target.js +1 -1
- package/lib/edge.js +2 -2
- package/lib/node.js +31 -9
- package/package.json +1 -2
|
@@ -9,6 +9,9 @@ const { resolve, dirname } = require('path')
|
|
|
9
9
|
const { promisify } = require('util')
|
|
10
10
|
const treeCheck = require('../tree-check.js')
|
|
11
11
|
const readdir = promisify(require('readdir-scoped-modules'))
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
const lstat = promisify(fs.lstat)
|
|
14
|
+
const readlink = promisify(fs.readlink)
|
|
12
15
|
const { depth } = require('treeverse')
|
|
13
16
|
|
|
14
17
|
const {
|
|
@@ -407,7 +410,14 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
407
410
|
if (this[_updateAll] || updateName) {
|
|
408
411
|
if (updateName)
|
|
409
412
|
globalExplicitUpdateNames.push(name)
|
|
410
|
-
|
|
413
|
+
const dir = resolve(nm, name)
|
|
414
|
+
const st = await lstat(dir).catch(/* istanbul ignore next */ er => null)
|
|
415
|
+
if (st && st.isSymbolicLink()) {
|
|
416
|
+
const target = await readlink(dir)
|
|
417
|
+
const real = resolve(dirname(dir), target)
|
|
418
|
+
tree.package.dependencies[name] = `file:${real}`
|
|
419
|
+
} else
|
|
420
|
+
tree.package.dependencies[name] = '*'
|
|
411
421
|
}
|
|
412
422
|
}
|
|
413
423
|
}
|
|
@@ -1015,6 +1025,15 @@ This is a one-time fix-up, please be patient...
|
|
|
1015
1025
|
// keep track of the thing that caused this node to be included.
|
|
1016
1026
|
const src = parent.sourceReference
|
|
1017
1027
|
this[_peerSetSource].set(node, src)
|
|
1028
|
+
|
|
1029
|
+
// do not load the peers along with the set if this is a global top pkg
|
|
1030
|
+
// otherwise we'll be tempted to put peers as other top-level installed
|
|
1031
|
+
// things, potentially clobbering what's there already, which is not
|
|
1032
|
+
// what we want. the missing edges will be picked up on the next pass.
|
|
1033
|
+
if (this[_global] && edge.from.isProjectRoot)
|
|
1034
|
+
return node
|
|
1035
|
+
|
|
1036
|
+
// otherwise, we have to make sure that our peers can go along with us.
|
|
1018
1037
|
return this[_loadPeerSet](node, required)
|
|
1019
1038
|
}
|
|
1020
1039
|
|
|
@@ -188,8 +188,10 @@ module.exports = cls => class ActualLoader extends cls {
|
|
|
188
188
|
const tree = this[_actualTree]
|
|
189
189
|
const actualRoot = tree.isLink ? tree.target : tree
|
|
190
190
|
const { dependencies = {} } = actualRoot.package
|
|
191
|
-
for (const name of actualRoot.children.
|
|
192
|
-
|
|
191
|
+
for (const [name, kid] of actualRoot.children.entries()) {
|
|
192
|
+
const def = kid.isLink ? `file:${kid.realpath}` : '*'
|
|
193
|
+
dependencies[name] = dependencies[name] || def
|
|
194
|
+
}
|
|
193
195
|
actualRoot.package = { ...actualRoot.package, dependencies }
|
|
194
196
|
}
|
|
195
197
|
return this[_actualTree]
|
package/lib/arborist/reify.js
CHANGED
|
@@ -6,11 +6,13 @@ const AuditReport = require('../audit-report.js')
|
|
|
6
6
|
const {subset, intersects} = require('semver')
|
|
7
7
|
const npa = require('npm-package-arg')
|
|
8
8
|
const debug = require('../debug.js')
|
|
9
|
+
const walkUp = require('walk-up-path')
|
|
9
10
|
|
|
10
11
|
const {dirname, resolve, relative} = require('path')
|
|
11
12
|
const {depth: dfwalk} = require('treeverse')
|
|
12
13
|
const fs = require('fs')
|
|
13
14
|
const {promisify} = require('util')
|
|
15
|
+
const lstat = promisify(fs.lstat)
|
|
14
16
|
const symlink = promisify(fs.symlink)
|
|
15
17
|
const mkdirp = require('mkdirp-infer-owner')
|
|
16
18
|
const justMkdirp = require('mkdirp')
|
|
@@ -19,6 +21,7 @@ const rimraf = promisify(require('rimraf'))
|
|
|
19
21
|
const PackageJson = require('@npmcli/package-json')
|
|
20
22
|
const packageContents = require('@npmcli/installed-package-contents')
|
|
21
23
|
const { checkEngine, checkPlatform } = require('npm-install-checks')
|
|
24
|
+
const _force = Symbol.for('force')
|
|
22
25
|
|
|
23
26
|
const treeCheck = require('../tree-check.js')
|
|
24
27
|
const relpath = require('../relpath.js')
|
|
@@ -76,8 +79,10 @@ const _copyIdealToActual = Symbol('copyIdealToActual')
|
|
|
76
79
|
const _addOmitsToTrashList = Symbol('addOmitsToTrashList')
|
|
77
80
|
const _packageLockOnly = Symbol('packageLockOnly')
|
|
78
81
|
const _dryRun = Symbol('dryRun')
|
|
82
|
+
const _validateNodeModules = Symbol('validateNodeModules')
|
|
83
|
+
const _nmValidated = Symbol('nmValidated')
|
|
79
84
|
const _validatePath = Symbol('validatePath')
|
|
80
|
-
const _reifyPackages = Symbol('reifyPackages')
|
|
85
|
+
const _reifyPackages = Symbol.for('reifyPackages')
|
|
81
86
|
|
|
82
87
|
const _omitDev = Symbol('omitDev')
|
|
83
88
|
const _omitOptional = Symbol('omitOptional')
|
|
@@ -119,6 +124,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
119
124
|
this[_bundleUnpacked] = new Set()
|
|
120
125
|
// child nodes we'd EXPECT to be included in a bundle, but aren't
|
|
121
126
|
this[_bundleMissing] = new Set()
|
|
127
|
+
this[_nmValidated] = new Set()
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
// public method
|
|
@@ -159,6 +165,9 @@ module.exports = cls => class Reifier extends cls {
|
|
|
159
165
|
// recursively, because it can have other side effects to do that
|
|
160
166
|
// in a project directory. We just want to make it if it's missing.
|
|
161
167
|
await justMkdirp(resolve(this.path))
|
|
168
|
+
|
|
169
|
+
// do not allow the top-level node_modules to be a symlink
|
|
170
|
+
await this[_validateNodeModules](resolve(this.path, 'node_modules'))
|
|
162
171
|
}
|
|
163
172
|
|
|
164
173
|
async [_reifyPackages] () {
|
|
@@ -328,12 +337,13 @@ module.exports = cls => class Reifier extends cls {
|
|
|
328
337
|
ideal: this.idealTree,
|
|
329
338
|
})
|
|
330
339
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
340
|
+
// we don't have to add 'removed' folders to the trashlist, because
|
|
341
|
+
// they'll be moved aside to a retirement folder, and then the retired
|
|
342
|
+
// folder will be deleted at the end. This is important when we have
|
|
343
|
+
// a folder like FOO being "removed" in favor of a folder like "foo",
|
|
344
|
+
// because if we remove node_modules/FOO on case-insensitive systems,
|
|
345
|
+
// it will remove the dep that we *want* at node_modules/foo.
|
|
346
|
+
|
|
337
347
|
process.emit('timeEnd', 'reify:diffTrees')
|
|
338
348
|
}
|
|
339
349
|
|
|
@@ -429,19 +439,40 @@ module.exports = cls => class Reifier extends cls {
|
|
|
429
439
|
process.emit('time', 'reify:createSparse')
|
|
430
440
|
// if we call this fn again, we look for the previous list
|
|
431
441
|
// so that we can avoid making the same directory multiple times
|
|
432
|
-
const
|
|
442
|
+
const leaves = this.diff.leaves
|
|
433
443
|
.filter(diff => {
|
|
434
444
|
return (diff.action === 'ADD' || diff.action === 'CHANGE') &&
|
|
435
445
|
!this[_sparseTreeDirs].has(diff.ideal.path) &&
|
|
436
446
|
!diff.ideal.isLink
|
|
437
447
|
})
|
|
438
|
-
.map(diff => diff.ideal
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
448
|
+
.map(diff => diff.ideal)
|
|
449
|
+
|
|
450
|
+
// we check this in parallel, so guard against multiple attempts to
|
|
451
|
+
// retire the same path at the same time.
|
|
452
|
+
const dirsChecked = new Set()
|
|
453
|
+
return promiseAllRejectLate(leaves.map(async node => {
|
|
454
|
+
for (const d of walkUp(node.path)) {
|
|
455
|
+
if (d === node.top.path)
|
|
456
|
+
break
|
|
457
|
+
if (dirsChecked.has(d))
|
|
458
|
+
continue
|
|
459
|
+
dirsChecked.add(d)
|
|
460
|
+
const st = await lstat(d).catch(er => null)
|
|
461
|
+
// this can happen if we have a link to a package with a name
|
|
462
|
+
// that the filesystem treats as if it is the same thing.
|
|
463
|
+
// would be nice to have conditional istanbul ignores here...
|
|
464
|
+
/* istanbul ignore next - defense in depth */
|
|
465
|
+
if (st && !st.isDirectory()) {
|
|
466
|
+
const retired = retirePath(d)
|
|
467
|
+
this[_retiredPaths][d] = retired
|
|
468
|
+
this[_trashList].add(retired)
|
|
469
|
+
await this[_renamePath](d, retired)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const made = await mkdirp(node.path)
|
|
473
|
+
this[_sparseTreeDirs].add(node.path)
|
|
474
|
+
this[_sparseTreeRoots].add(made)
|
|
475
|
+
}))
|
|
445
476
|
.then(() => process.emit('timeEnd', 'reify:createSparse'))
|
|
446
477
|
}
|
|
447
478
|
|
|
@@ -536,7 +567,20 @@ module.exports = cls => class Reifier extends cls {
|
|
|
536
567
|
})
|
|
537
568
|
}
|
|
538
569
|
|
|
539
|
-
|
|
570
|
+
// do not allow node_modules to be a symlink
|
|
571
|
+
async [_validateNodeModules] (nm) {
|
|
572
|
+
if (this[_force] || this[_nmValidated].has(nm))
|
|
573
|
+
return
|
|
574
|
+
const st = await lstat(nm).catch(() => null)
|
|
575
|
+
if (!st || st.isDirectory()) {
|
|
576
|
+
this[_nmValidated].add(nm)
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
this.log.warn('reify', 'Removing non-directory', nm)
|
|
580
|
+
await rimraf(nm)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async [_extractOrLink] (node) {
|
|
540
584
|
// in normal cases, node.resolved should *always* be set by now.
|
|
541
585
|
// however, it is possible when a lockfile is damaged, or very old,
|
|
542
586
|
// or in some other race condition bugs in npm v6, that a previously
|
|
@@ -563,13 +607,29 @@ module.exports = cls => class Reifier extends cls {
|
|
|
563
607
|
return
|
|
564
608
|
}
|
|
565
609
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
610
|
+
const nm = resolve(node.parent.path, 'node_modules')
|
|
611
|
+
await this[_validateNodeModules](nm)
|
|
612
|
+
|
|
613
|
+
if (node.isLink) {
|
|
614
|
+
await rimraf(node.path)
|
|
615
|
+
await this[_symlink](node)
|
|
616
|
+
} else {
|
|
617
|
+
await debug(async () => {
|
|
618
|
+
const st = await lstat(node.path).catch(e => null)
|
|
619
|
+
if (st && !st.isDirectory()) {
|
|
620
|
+
debug.log('unpacking into a non-directory', node)
|
|
621
|
+
throw Object.assign(new Error('ENOTDIR: not a directory'), {
|
|
622
|
+
code: 'ENOTDIR',
|
|
623
|
+
path: node.path,
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
})
|
|
627
|
+
await pacote.extract(res, node.path, {
|
|
569
628
|
...this.options,
|
|
570
629
|
resolved: node.resolved,
|
|
571
630
|
integrity: node.integrity,
|
|
572
631
|
})
|
|
632
|
+
}
|
|
573
633
|
}
|
|
574
634
|
|
|
575
635
|
async [_symlink] (node) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// package children are represented with a Map object, but many file systems
|
|
2
|
+
// are case-insensitive and unicode-normalizing, so we need to treat
|
|
3
|
+
// node.children.get('FOO') and node.children.get('foo') as the same thing.
|
|
4
|
+
|
|
5
|
+
const _keys = Symbol('keys')
|
|
6
|
+
const _normKey = Symbol('normKey')
|
|
7
|
+
const normalize = s => s.normalize('NFKD').toLowerCase()
|
|
8
|
+
const OGMap = Map
|
|
9
|
+
module.exports = class Map extends OGMap {
|
|
10
|
+
constructor (items = []) {
|
|
11
|
+
super()
|
|
12
|
+
this[_keys] = new OGMap()
|
|
13
|
+
for (const [key, val] of items)
|
|
14
|
+
this.set(key, val)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[_normKey] (key) {
|
|
18
|
+
return typeof key === 'string' ? normalize(key) : key
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get (key) {
|
|
22
|
+
const normKey = this[_normKey](key)
|
|
23
|
+
return this[_keys].has(normKey) ? super.get(this[_keys].get(normKey))
|
|
24
|
+
: undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set (key, val) {
|
|
28
|
+
const normKey = this[_normKey](key)
|
|
29
|
+
if (this[_keys].has(normKey))
|
|
30
|
+
super.delete(this[_keys].get(normKey))
|
|
31
|
+
this[_keys].set(normKey, key)
|
|
32
|
+
return super.set(key, val)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
delete (key) {
|
|
36
|
+
const normKey = this[_normKey](key)
|
|
37
|
+
if (this[_keys].has(normKey)) {
|
|
38
|
+
const prevKey = this[_keys].get(normKey)
|
|
39
|
+
this[_keys].delete(normKey)
|
|
40
|
+
return super.delete(prevKey)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
has (key) {
|
|
45
|
+
const normKey = this[_normKey](key)
|
|
46
|
+
return this[_keys].has(normKey) && super.has(this[_keys].get(normKey))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const deepestNestingTarget = (start, name) => {
|
|
6
6
|
for (const target of start.ancestry()) {
|
|
7
7
|
// note: this will skip past the first target if edge is peer
|
|
8
|
-
if (target.isProjectRoot || !target.resolveParent)
|
|
8
|
+
if (target.isProjectRoot || !target.resolveParent || target.globalTop)
|
|
9
9
|
return target
|
|
10
10
|
const targetEdge = target.edgesOut.get(name)
|
|
11
11
|
if (!targetEdge || !targetEdge.peer)
|
package/lib/edge.js
CHANGED
|
@@ -77,7 +77,7 @@ class Edge {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
satisfiedBy (node) {
|
|
80
|
-
return depValid(node, this.spec, this.accept, this.from)
|
|
80
|
+
return node.name === this.name && depValid(node, this.spec, this.accept, this.from)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
explain (seen = []) {
|
|
@@ -167,7 +167,7 @@ class Edge {
|
|
|
167
167
|
[_loadError] () {
|
|
168
168
|
return !this[_to] ? (this.optional ? null : 'MISSING')
|
|
169
169
|
: this.peer && this.from === this.to.parent && !this.from.isTop ? 'PEER LOCAL'
|
|
170
|
-
: !
|
|
170
|
+
: !this.satisfiedBy(this.to) ? 'INVALID'
|
|
171
171
|
: 'OK'
|
|
172
172
|
}
|
|
173
173
|
|
package/lib/node.js
CHANGED
|
@@ -66,6 +66,7 @@ const relpath = require('./relpath.js')
|
|
|
66
66
|
const consistentResolve = require('./consistent-resolve.js')
|
|
67
67
|
|
|
68
68
|
const printableTree = require('./printable.js')
|
|
69
|
+
const CaseInsensitiveMap = require('./case-insensitive-map.js')
|
|
69
70
|
|
|
70
71
|
class Node {
|
|
71
72
|
constructor (options) {
|
|
@@ -148,7 +149,7 @@ class Node {
|
|
|
148
149
|
this.hasShrinkwrap = hasShrinkwrap || pkg._hasShrinkwrap || false
|
|
149
150
|
this.legacyPeerDeps = legacyPeerDeps
|
|
150
151
|
|
|
151
|
-
this.children = new
|
|
152
|
+
this.children = new CaseInsensitiveMap()
|
|
152
153
|
this.fsChildren = new Set()
|
|
153
154
|
this.inventory = new Inventory({})
|
|
154
155
|
this.tops = new Set()
|
|
@@ -181,7 +182,7 @@ class Node {
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
this.edgesIn = new Set()
|
|
184
|
-
this.edgesOut = new
|
|
185
|
+
this.edgesOut = new CaseInsensitiveMap()
|
|
185
186
|
|
|
186
187
|
// have to set the internal package ref before assigning the parent,
|
|
187
188
|
// because this.package is read when adding to inventory
|
|
@@ -248,7 +249,7 @@ class Node {
|
|
|
248
249
|
|
|
249
250
|
// true for packages installed directly in the global node_modules folder
|
|
250
251
|
get globalTop () {
|
|
251
|
-
return this.global && this.parent.isProjectRoot
|
|
252
|
+
return this.global && this.parent && this.parent.isProjectRoot
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
get workspaces () {
|
|
@@ -478,6 +479,9 @@ class Node {
|
|
|
478
479
|
}
|
|
479
480
|
|
|
480
481
|
get isProjectRoot () {
|
|
482
|
+
// only treat as project root if it's the actual link that is the root,
|
|
483
|
+
// or the target of the root link, but NOT if it's another link to the
|
|
484
|
+
// same root that happens to be somewhere else.
|
|
481
485
|
return this === this.root || this === this.root.target
|
|
482
486
|
}
|
|
483
487
|
|
|
@@ -772,9 +776,15 @@ class Node {
|
|
|
772
776
|
this[_loadDepType](this.package.dependencies, 'prod')
|
|
773
777
|
this[_loadDepType](this.package.optionalDependencies, 'optional')
|
|
774
778
|
|
|
775
|
-
const { isTop, path, sourceReference } = this
|
|
776
|
-
const {
|
|
777
|
-
|
|
779
|
+
const { globalTop, isTop, path, sourceReference } = this
|
|
780
|
+
const {
|
|
781
|
+
globalTop: srcGlobalTop,
|
|
782
|
+
isTop: srcTop,
|
|
783
|
+
path: srcPath,
|
|
784
|
+
} = sourceReference || {}
|
|
785
|
+
const thisDev = isTop && !globalTop && path
|
|
786
|
+
const srcDev = !sourceReference || srcTop && !srcGlobalTop && srcPath
|
|
787
|
+
if (thisDev && srcDev)
|
|
778
788
|
this[_loadDepType](this.package.devDependencies, 'dev')
|
|
779
789
|
}
|
|
780
790
|
|
|
@@ -1014,8 +1024,20 @@ class Node {
|
|
|
1014
1024
|
|
|
1015
1025
|
replace (node) {
|
|
1016
1026
|
this[_delistFromMeta]()
|
|
1017
|
-
|
|
1018
|
-
|
|
1027
|
+
|
|
1028
|
+
// if the name matches, but is not identical, we are intending to clobber
|
|
1029
|
+
// something case-insensitively, so merely setting name and path won't
|
|
1030
|
+
// have the desired effect. just set the path so it'll collide in the
|
|
1031
|
+
// parent's children map, and leave it at that.
|
|
1032
|
+
const nameMatch = node.parent &&
|
|
1033
|
+
node.parent.children.get(this.name) === node
|
|
1034
|
+
if (nameMatch)
|
|
1035
|
+
this.path = resolve(node.parent.path, 'node_modules', this.name)
|
|
1036
|
+
else {
|
|
1037
|
+
this.path = node.path
|
|
1038
|
+
this.name = node.name
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1019
1041
|
if (!this.isLink)
|
|
1020
1042
|
this.realpath = this.path
|
|
1021
1043
|
this[_refreshLocation]()
|
|
@@ -1210,7 +1232,7 @@ class Node {
|
|
|
1210
1232
|
}
|
|
1211
1233
|
|
|
1212
1234
|
get isTop () {
|
|
1213
|
-
return !this.parent
|
|
1235
|
+
return !this.parent || this.globalTop
|
|
1214
1236
|
}
|
|
1215
1237
|
|
|
1216
1238
|
get top () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "Manage node_modules trees",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@npmcli/installed-package-contents": "^1.0.7",
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"rimraf": "^3.0.2",
|
|
33
33
|
"semver": "^7.3.5",
|
|
34
34
|
"ssri": "^8.0.1",
|
|
35
|
-
"tar": "^6.1.0",
|
|
36
35
|
"treeverse": "^1.0.4",
|
|
37
36
|
"walk-up-path": "^1.0.0"
|
|
38
37
|
},
|