@npmcli/arborist 2.2.6 → 2.3.0
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/bin/lib/options.js +7 -1
- package/lib/add-rm-pkg-deps.js +1 -1
- package/lib/arborist/build-ideal-tree.js +113 -34
- package/lib/arborist/load-actual.js +28 -2
- package/lib/arborist/reify.js +146 -17
- package/lib/diff.js +59 -10
- package/lib/link.js +13 -7
- package/lib/node.js +5 -2
- package/lib/printable.js +23 -3
- package/lib/shrinkwrap.js +14 -4
- package/lib/tree-check.js +46 -1
- package/package.json +3 -3
package/bin/lib/options.js
CHANGED
|
@@ -33,7 +33,13 @@ for (const arg of process.argv.slice(2)) {
|
|
|
33
33
|
options.omit.push(arg.substr('--omit='.length))
|
|
34
34
|
} else if (/^--before=/.test(arg))
|
|
35
35
|
options.before = new Date(arg.substr('--before='.length))
|
|
36
|
-
else if (
|
|
36
|
+
else if (/^-w.+/.test(arg)) {
|
|
37
|
+
options.workspaces = options.workspaces || []
|
|
38
|
+
options.workspaces.push(arg.replace(/^-w/, ''))
|
|
39
|
+
} else if (/^--workspace=/.test(arg)) {
|
|
40
|
+
options.workspaces = options.workspaces || []
|
|
41
|
+
options.workspaces.push(arg.replace(/^--workspace=/, ''))
|
|
42
|
+
} else if (/^--[^=]+=/.test(arg)) {
|
|
37
43
|
const [key, ...v] = arg.replace(/^--/, '').split('=')
|
|
38
44
|
const val = v.join('=')
|
|
39
45
|
options[key] = val === 'false' ? false : val === 'true' ? true : val
|
package/lib/add-rm-pkg-deps.js
CHANGED
|
@@ -71,7 +71,7 @@ const addSingle = ({pkg, spec, saveBundle, saveType}) => {
|
|
|
71
71
|
pkg.devDependencies[name] = pkg.peerDependencies[name]
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
if (saveBundle) {
|
|
74
|
+
if (saveBundle && saveType !== 'peer' && saveType !== 'peerOptional') {
|
|
75
75
|
// keep it sorted, keep it unique
|
|
76
76
|
const bd = new Set(pkg.bundleDependencies || [])
|
|
77
77
|
bd.add(spec.name)
|
|
@@ -44,12 +44,14 @@ const _currentDep = Symbol('currentDep')
|
|
|
44
44
|
const _updateAll = Symbol('updateAll')
|
|
45
45
|
const _mutateTree = Symbol('mutateTree')
|
|
46
46
|
const _flagsSuspect = Symbol.for('flagsSuspect')
|
|
47
|
+
const _workspaces = Symbol.for('workspaces')
|
|
47
48
|
const _prune = Symbol('prune')
|
|
48
49
|
const _preferDedupe = Symbol('preferDedupe')
|
|
49
50
|
const _legacyBundling = Symbol('legacyBundling')
|
|
50
51
|
const _parseSettings = Symbol('parseSettings')
|
|
51
52
|
const _initTree = Symbol('initTree')
|
|
52
53
|
const _applyUserRequests = Symbol('applyUserRequests')
|
|
54
|
+
const _applyUserRequestsToNode = Symbol('applyUserRequestsToNode')
|
|
53
55
|
const _inflateAncientLockfile = Symbol('inflateAncientLockfile')
|
|
54
56
|
const _buildDeps = Symbol('buildDeps')
|
|
55
57
|
const _buildDepStep = Symbol('buildDepStep')
|
|
@@ -109,7 +111,7 @@ const _peerSetSource = Symbol.for('peerSetSource')
|
|
|
109
111
|
|
|
110
112
|
// used by Reify mixin
|
|
111
113
|
const _force = Symbol.for('force')
|
|
112
|
-
const _explicitRequests = Symbol
|
|
114
|
+
const _explicitRequests = Symbol('explicitRequests')
|
|
113
115
|
const _global = Symbol.for('global')
|
|
114
116
|
const _idealTreePrune = Symbol.for('idealTreePrune')
|
|
115
117
|
|
|
@@ -130,8 +132,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
130
132
|
force = false,
|
|
131
133
|
packageLock = true,
|
|
132
134
|
strictPeerDeps = false,
|
|
135
|
+
workspaces = [],
|
|
133
136
|
} = options
|
|
134
137
|
|
|
138
|
+
this[_workspaces] = workspaces || []
|
|
135
139
|
this[_force] = !!force
|
|
136
140
|
this[_strictPeerDeps] = !!strictPeerDeps
|
|
137
141
|
|
|
@@ -143,6 +147,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
143
147
|
this[_globalStyle] = this[_global] || globalStyle
|
|
144
148
|
this[_follow] = !!follow
|
|
145
149
|
|
|
150
|
+
if (this[_workspaces].length && this[_global])
|
|
151
|
+
throw new Error('Cannot operate on workspaces in global mode')
|
|
152
|
+
|
|
146
153
|
this[_explicitRequests] = new Set()
|
|
147
154
|
this[_preferDedupe] = false
|
|
148
155
|
this[_legacyBundling] = false
|
|
@@ -157,6 +164,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
157
164
|
this[_manifests] = new Map()
|
|
158
165
|
this[_peerConflict] = null
|
|
159
166
|
this[_edgesOverridden] = new Set()
|
|
167
|
+
this[_resolvedAdd] = []
|
|
160
168
|
|
|
161
169
|
// a map of each module in a peer set to the thing that depended on
|
|
162
170
|
// that set of peers in the first place. Use a WeakMap so that we
|
|
@@ -204,8 +212,8 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
204
212
|
|
|
205
213
|
try {
|
|
206
214
|
await this[_initTree]()
|
|
207
|
-
await this[_applyUserRequests](options)
|
|
208
215
|
await this[_inflateAncientLockfile]()
|
|
216
|
+
await this[_applyUserRequests](options)
|
|
209
217
|
await this[_buildDeps]()
|
|
210
218
|
await this[_fixDepFlags]()
|
|
211
219
|
await this[_pruneFailedOptional]()
|
|
@@ -266,6 +274,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
266
274
|
this[_preferDedupe] = !!options.preferDedupe
|
|
267
275
|
this[_legacyBundling] = !!options.legacyBundling
|
|
268
276
|
this[_updateNames] = update.names
|
|
277
|
+
|
|
269
278
|
this[_updateAll] = update.all
|
|
270
279
|
// we prune by default unless explicitly set to boolean false
|
|
271
280
|
this[_prune] = options.prune !== false
|
|
@@ -387,6 +396,42 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
387
396
|
async [_applyUserRequests] (options) {
|
|
388
397
|
process.emit('time', 'idealTree:userRequests')
|
|
389
398
|
const tree = this.idealTree.target || this.idealTree
|
|
399
|
+
|
|
400
|
+
if (!this[_workspaces].length) {
|
|
401
|
+
return this[_applyUserRequestsToNode](tree, options).then(() =>
|
|
402
|
+
process.emit('timeEnd', 'idealTree:userRequests'))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const wsMap = tree.workspaces
|
|
406
|
+
if (!wsMap) {
|
|
407
|
+
this.log.warn('idealTree', 'Workspace filter set, but no workspaces present')
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const promises = []
|
|
412
|
+
for (const name of this[_workspaces]) {
|
|
413
|
+
const path = wsMap.get(name)
|
|
414
|
+
if (!path) {
|
|
415
|
+
this.log.warn('idealTree', `Workspace ${name} in filter set, but not in workspaces`)
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
const loc = relpath(tree.realpath, path)
|
|
419
|
+
const node = tree.inventory.get(loc)
|
|
420
|
+
|
|
421
|
+
/* istanbul ignore if - should be impossible */
|
|
422
|
+
if (!node) {
|
|
423
|
+
this.log.warn('idealTree', `Workspace ${name} in filter set, but no workspace folder present`)
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
promises.push(this[_applyUserRequestsToNode](node, options))
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return Promise.all(promises).then(() =>
|
|
431
|
+
process.emit('timeEnd', 'idealTree:userRequests'))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async [_applyUserRequestsToNode] (tree, options) {
|
|
390
435
|
// If we have a list of package names to update, and we know it's
|
|
391
436
|
// going to update them wherever they are, add any paths into those
|
|
392
437
|
// named nodes to the buildIdealTree queue.
|
|
@@ -395,51 +440,63 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
395
440
|
|
|
396
441
|
// global updates only update the globalTop nodes, but we need to know
|
|
397
442
|
// that they're there, and not reinstall the world unnecessarily.
|
|
443
|
+
const globalExplicitUpdateNames = []
|
|
398
444
|
if (this[_global] && (this[_updateAll] || this[_updateNames].length)) {
|
|
399
445
|
const nm = resolve(this.path, 'node_modules')
|
|
400
446
|
for (const name of await readdir(nm).catch(() => [])) {
|
|
401
|
-
if (this[_updateNames].includes(name))
|
|
402
|
-
this[_explicitRequests].add(name)
|
|
403
447
|
tree.package.dependencies = tree.package.dependencies || {}
|
|
404
|
-
|
|
448
|
+
const updateName = this[_updateNames].includes(name)
|
|
449
|
+
if (this[_updateAll] || updateName) {
|
|
450
|
+
if (updateName)
|
|
451
|
+
globalExplicitUpdateNames.push(name)
|
|
405
452
|
tree.package.dependencies[name] = '*'
|
|
453
|
+
}
|
|
406
454
|
}
|
|
407
455
|
}
|
|
408
456
|
|
|
409
457
|
if (this.auditReport && this.auditReport.size > 0)
|
|
410
458
|
this[_queueVulnDependents](options)
|
|
411
459
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
460
|
+
const { add, rm } = options
|
|
461
|
+
|
|
462
|
+
if (rm && rm.length) {
|
|
463
|
+
addRmPkgDeps.rm(tree.package, rm)
|
|
464
|
+
for (const name of rm)
|
|
465
|
+
this[_explicitRequests].add({ from: tree, name, action: 'DELETE' })
|
|
416
466
|
}
|
|
417
467
|
|
|
418
|
-
if (
|
|
419
|
-
await this[_add](options)
|
|
468
|
+
if (add && add.length)
|
|
469
|
+
await this[_add](tree, options)
|
|
420
470
|
|
|
421
|
-
// triggers a refresh of all edgesOut
|
|
422
|
-
|
|
471
|
+
// triggers a refresh of all edgesOut. this has to be done BEFORE
|
|
472
|
+
// adding the edges to explicitRequests, because the package setter
|
|
473
|
+
// resets all edgesOut.
|
|
474
|
+
if (add && add.length || rm && rm.length || this[_global])
|
|
423
475
|
tree.package = tree.package
|
|
424
|
-
|
|
476
|
+
|
|
477
|
+
for (const spec of this[_resolvedAdd])
|
|
478
|
+
this[_explicitRequests].add(tree.edgesOut.get(spec.name))
|
|
479
|
+
for (const name of globalExplicitUpdateNames)
|
|
480
|
+
this[_explicitRequests].add(tree.edgesOut.get(name))
|
|
425
481
|
}
|
|
426
482
|
|
|
427
483
|
// This returns a promise because we might not have the name yet,
|
|
428
484
|
// and need to call pacote.manifest to find the name.
|
|
429
|
-
[_add] ({add, saveType = null, saveBundle = false}) {
|
|
485
|
+
[_add] (tree, {add, saveType = null, saveBundle = false}) {
|
|
430
486
|
// get the name for each of the specs in the list.
|
|
431
487
|
// ie, doing `foo@bar` we just return foo
|
|
432
488
|
// but if it's a url or git, we don't know the name until we
|
|
433
489
|
// fetch it and look in its manifest.
|
|
434
|
-
return Promise.all(add.map(rawSpec =>
|
|
435
|
-
|
|
490
|
+
return Promise.all(add.map(rawSpec => {
|
|
491
|
+
// We do NOT provide the path here, because user-additions need
|
|
492
|
+
// to be resolved relative to the CWD the user is in.
|
|
493
|
+
return this[_retrieveSpecName](npa(rawSpec))
|
|
436
494
|
.then(add => this[_updateFilePath](add))
|
|
437
495
|
.then(add => this[_followSymlinkPath](add))
|
|
438
|
-
)).then(add => {
|
|
439
|
-
this[_resolvedAdd]
|
|
496
|
+
})).then(add => {
|
|
497
|
+
this[_resolvedAdd].push(...add)
|
|
440
498
|
// now add is a list of spec objects with names.
|
|
441
499
|
// find a home for each of them!
|
|
442
|
-
const tree = this.idealTree.target || this.idealTree
|
|
443
500
|
addRmPkgDeps.add({
|
|
444
501
|
pkg: tree.package,
|
|
445
502
|
add,
|
|
@@ -447,8 +504,6 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
447
504
|
saveType,
|
|
448
505
|
path: this.path,
|
|
449
506
|
})
|
|
450
|
-
for (const spec of add)
|
|
451
|
-
this[_explicitRequests].add(spec.name)
|
|
452
507
|
})
|
|
453
508
|
}
|
|
454
509
|
|
|
@@ -881,6 +936,8 @@ This is a one-time fix-up, please be patient...
|
|
|
881
936
|
// create a virtual root node with the same deps as the node that
|
|
882
937
|
// is requesting this one, so that we can get all the peer deps in
|
|
883
938
|
// a context where they're likely to be resolvable.
|
|
939
|
+
// Note that the virtual root will also have virtual copies of the
|
|
940
|
+
// targets of any child Links, so that they resolve appropriately.
|
|
884
941
|
const parent = parent_ || this[_virtualRoot](edge.from)
|
|
885
942
|
const realParent = edge.peer ? edge.from.resolveParent : edge.from
|
|
886
943
|
|
|
@@ -934,11 +991,23 @@ This is a one-time fix-up, please be patient...
|
|
|
934
991
|
return this[_virtualRoots].get(node)
|
|
935
992
|
|
|
936
993
|
const vr = new Node({
|
|
937
|
-
path:
|
|
994
|
+
path: node.realpath,
|
|
938
995
|
sourceReference: node,
|
|
939
996
|
legacyPeerDeps: this.legacyPeerDeps,
|
|
940
997
|
})
|
|
941
998
|
|
|
999
|
+
// also need to set up any targets from any link deps, so that
|
|
1000
|
+
// they are properly reflected in the virtual environment
|
|
1001
|
+
for (const child of node.children.values()) {
|
|
1002
|
+
if (child.isLink) {
|
|
1003
|
+
new Node({
|
|
1004
|
+
path: child.realpath,
|
|
1005
|
+
sourceReference: child.target,
|
|
1006
|
+
root: vr,
|
|
1007
|
+
})
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
942
1011
|
this[_virtualRoots].set(node, vr)
|
|
943
1012
|
return vr
|
|
944
1013
|
}
|
|
@@ -975,7 +1044,7 @@ This is a one-time fix-up, please be patient...
|
|
|
975
1044
|
// if it's peerOptional and not explicitly requested.
|
|
976
1045
|
if (!edge.to) {
|
|
977
1046
|
return edge.type !== 'peerOptional' ||
|
|
978
|
-
this[_explicitRequests].has(edge
|
|
1047
|
+
this[_explicitRequests].has(edge)
|
|
979
1048
|
}
|
|
980
1049
|
|
|
981
1050
|
// If the edge has an error, there's a problem.
|
|
@@ -991,7 +1060,7 @@ This is a one-time fix-up, please be patient...
|
|
|
991
1060
|
return true
|
|
992
1061
|
|
|
993
1062
|
// If the user has explicitly asked to install this package, it's a problem.
|
|
994
|
-
if (node.isProjectRoot && this[_explicitRequests].has(edge
|
|
1063
|
+
if (node.isProjectRoot && this[_explicitRequests].has(edge))
|
|
995
1064
|
return true
|
|
996
1065
|
|
|
997
1066
|
// No problems!
|
|
@@ -1115,7 +1184,7 @@ This is a one-time fix-up, please be patient...
|
|
|
1115
1184
|
continue
|
|
1116
1185
|
|
|
1117
1186
|
// problem
|
|
1118
|
-
this[_failPeerConflict](edge)
|
|
1187
|
+
this[_failPeerConflict](edge, parentEdge)
|
|
1119
1188
|
}
|
|
1120
1189
|
}
|
|
1121
1190
|
|
|
@@ -1131,17 +1200,17 @@ This is a one-time fix-up, please be patient...
|
|
|
1131
1200
|
continue
|
|
1132
1201
|
|
|
1133
1202
|
// ok, it's the root, or we're in unforced strict mode, so this is bad
|
|
1134
|
-
this[_failPeerConflict](edge)
|
|
1203
|
+
this[_failPeerConflict](edge, parentEdge)
|
|
1135
1204
|
}
|
|
1136
1205
|
return node
|
|
1137
1206
|
}
|
|
1138
1207
|
|
|
1139
|
-
[_failPeerConflict] (edge) {
|
|
1140
|
-
const expl = this[_explainPeerConflict](edge)
|
|
1208
|
+
[_failPeerConflict] (edge, currentEdge) {
|
|
1209
|
+
const expl = this[_explainPeerConflict](edge, currentEdge)
|
|
1141
1210
|
throw Object.assign(new Error('unable to resolve dependency tree'), expl)
|
|
1142
1211
|
}
|
|
1143
1212
|
|
|
1144
|
-
[_explainPeerConflict] (edge) {
|
|
1213
|
+
[_explainPeerConflict] (edge, currentEdge) {
|
|
1145
1214
|
const node = edge.from
|
|
1146
1215
|
const curNode = node.resolve(edge.name)
|
|
1147
1216
|
const pc = this[_peerConflict] || { peer: null, current: null }
|
|
@@ -1150,6 +1219,10 @@ This is a one-time fix-up, please be patient...
|
|
|
1150
1219
|
return {
|
|
1151
1220
|
code: 'ERESOLVE',
|
|
1152
1221
|
current,
|
|
1222
|
+
// it SHOULD be impossible to get here without a current node in place,
|
|
1223
|
+
// but this at least gives us something report on when bugs creep into
|
|
1224
|
+
// the tree handling logic.
|
|
1225
|
+
currentEdge: currentEdge ? currentEdge.explain() : null,
|
|
1153
1226
|
edge: edge.explain(),
|
|
1154
1227
|
peerConflict,
|
|
1155
1228
|
strictPeerDeps: this[_strictPeerDeps],
|
|
@@ -1174,7 +1247,7 @@ This is a one-time fix-up, please be patient...
|
|
|
1174
1247
|
[_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) {
|
|
1175
1248
|
if (edge.to &&
|
|
1176
1249
|
!edge.error &&
|
|
1177
|
-
!this[_explicitRequests].has(edge
|
|
1250
|
+
!this[_explicitRequests].has(edge) &&
|
|
1178
1251
|
!this[_updateNames].includes(edge.name) &&
|
|
1179
1252
|
!this[_isVulnerable](edge.to))
|
|
1180
1253
|
return []
|
|
@@ -1464,9 +1537,15 @@ This is a one-time fix-up, please be patient...
|
|
|
1464
1537
|
if (target.children.has(edge.name)) {
|
|
1465
1538
|
const current = target.children.get(edge.name)
|
|
1466
1539
|
|
|
1467
|
-
// same thing = keep
|
|
1468
|
-
if
|
|
1469
|
-
|
|
1540
|
+
// same thing = keep, UNLESS the current doesn't satisfy and new
|
|
1541
|
+
// one does satisfy. This can happen if it's a link to a matching target
|
|
1542
|
+
// at a different location, which satisfies a version dep, but not a
|
|
1543
|
+
// file: dep. If neither of them satisfy, then we can replace it,
|
|
1544
|
+
// because presumably it's better for a peer or something.
|
|
1545
|
+
if (dep.matches(current)) {
|
|
1546
|
+
if (current.satisfies(edge) || !dep.satisfies(edge))
|
|
1547
|
+
return KEEP
|
|
1548
|
+
}
|
|
1470
1549
|
|
|
1471
1550
|
const { version: curVer } = current
|
|
1472
1551
|
const { version: newVer } = dep
|
|
@@ -32,6 +32,7 @@ const _loadActual = Symbol('loadActual')
|
|
|
32
32
|
const _loadActualVirtually = Symbol('loadActualVirtually')
|
|
33
33
|
const _loadActualActually = Symbol('loadActualActually')
|
|
34
34
|
const _loadWorkspaces = Symbol.for('loadWorkspaces')
|
|
35
|
+
const _loadWorkspaceTargets = Symbol('loadWorkspaceTargets')
|
|
35
36
|
const _actualTreePromise = Symbol('actualTreePromise')
|
|
36
37
|
const _actualTree = Symbol('actualTree')
|
|
37
38
|
const _transplant = Symbol('transplant')
|
|
@@ -150,18 +151,22 @@ module.exports = cls => class ActualLoader extends cls {
|
|
|
150
151
|
await new this.constructor({...this.options}).loadVirtual({
|
|
151
152
|
root: this[_actualTree],
|
|
152
153
|
})
|
|
154
|
+
await this[_loadWorkspaces](this[_actualTree])
|
|
155
|
+
if (this[_actualTree].workspaces && this[_actualTree].workspaces.size)
|
|
156
|
+
calcDepFlags(this[_actualTree], !root)
|
|
153
157
|
this[_transplant](root)
|
|
154
158
|
return this[_actualTree]
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
async [_loadActualActually] ({ root, ignoreMissing, global }) {
|
|
158
162
|
await this[_loadFSTree](this[_actualTree])
|
|
163
|
+
await this[_loadWorkspaces](this[_actualTree])
|
|
164
|
+
await this[_loadWorkspaceTargets](this[_actualTree])
|
|
159
165
|
if (!ignoreMissing)
|
|
160
166
|
await this[_findMissingEdges]()
|
|
161
167
|
this[_findFSParents]()
|
|
162
168
|
this[_transplant](root)
|
|
163
169
|
|
|
164
|
-
await this[_loadWorkspaces](this[_actualTree])
|
|
165
170
|
if (global) {
|
|
166
171
|
// need to depend on the children, or else all of them
|
|
167
172
|
// will end up being flagged as extraneous, since the
|
|
@@ -178,16 +183,37 @@ module.exports = cls => class ActualLoader extends cls {
|
|
|
178
183
|
return this[_actualTree]
|
|
179
184
|
}
|
|
180
185
|
|
|
186
|
+
// if there are workspace targets without Link nodes created, load
|
|
187
|
+
// the targets, so that we know what they are.
|
|
188
|
+
async [_loadWorkspaceTargets] (tree) {
|
|
189
|
+
if (!tree.workspaces || !tree.workspaces.size)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
const promises = []
|
|
193
|
+
for (const path of tree.workspaces.values()) {
|
|
194
|
+
if (!this[_cache].has(path)) {
|
|
195
|
+
const p = this[_loadFSNode]({ path, root: this[_actualTree] })
|
|
196
|
+
.then(node => this[_loadFSTree](node))
|
|
197
|
+
promises.push(p)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
await Promise.all(promises)
|
|
201
|
+
}
|
|
202
|
+
|
|
181
203
|
[_transplant] (root) {
|
|
182
204
|
if (!root || root === this[_actualTree])
|
|
183
205
|
return
|
|
206
|
+
|
|
184
207
|
this[_actualTree][_changePath](root.path)
|
|
185
208
|
for (const node of this[_actualTree].children.values()) {
|
|
186
209
|
if (!this[_transplantFilter](node))
|
|
187
|
-
node.
|
|
210
|
+
node.root = null
|
|
188
211
|
}
|
|
189
212
|
|
|
190
213
|
root.replace(this[_actualTree])
|
|
214
|
+
for (const node of this[_actualTree].fsChildren)
|
|
215
|
+
node.root = this[_transplantFilter](node) ? root : null
|
|
216
|
+
|
|
191
217
|
this[_actualTree] = root
|
|
192
218
|
}
|
|
193
219
|
|
package/lib/arborist/reify.js
CHANGED
|
@@ -6,6 +6,7 @@ const rpj = require('read-package-json-fast')
|
|
|
6
6
|
const { updateDepSpec } = require('../dep-spec.js')
|
|
7
7
|
const AuditReport = require('../audit-report.js')
|
|
8
8
|
const {subset} = require('semver')
|
|
9
|
+
const npa = require('npm-package-arg')
|
|
9
10
|
|
|
10
11
|
const {dirname, resolve, relative} = require('path')
|
|
11
12
|
const {depth: dfwalk} = require('treeverse')
|
|
@@ -13,6 +14,7 @@ const fs = require('fs')
|
|
|
13
14
|
const {promisify} = require('util')
|
|
14
15
|
const symlink = promisify(fs.symlink)
|
|
15
16
|
const mkdirp = require('mkdirp-infer-owner')
|
|
17
|
+
const justMkdirp = require('mkdirp')
|
|
16
18
|
const moveFile = require('@npmcli/move-file')
|
|
17
19
|
const rimraf = promisify(require('rimraf'))
|
|
18
20
|
const packageContents = require('@npmcli/installed-package-contents')
|
|
@@ -25,6 +27,7 @@ const retirePath = require('../retire-path.js')
|
|
|
25
27
|
const promiseAllRejectLate = require('promise-all-reject-late')
|
|
26
28
|
const optionalSet = require('../optional-set.js')
|
|
27
29
|
const updateRootPackageJson = require('../update-root-package-json.js')
|
|
30
|
+
const calcDepFlags = require('../calc-dep-flags.js')
|
|
28
31
|
|
|
29
32
|
const _retiredPaths = Symbol('retiredPaths')
|
|
30
33
|
const _retiredUnchanged = Symbol('retiredUnchanged')
|
|
@@ -35,6 +38,8 @@ const _retireShallowNodes = Symbol.for('retireShallowNodes')
|
|
|
35
38
|
const _getBundlesByDepth = Symbol('getBundlesByDepth')
|
|
36
39
|
const _registryResolved = Symbol('registryResolved')
|
|
37
40
|
const _addNodeToTrashList = Symbol('addNodeToTrashList')
|
|
41
|
+
const _workspaces = Symbol.for('workspaces')
|
|
42
|
+
|
|
38
43
|
// shared by rebuild mixin
|
|
39
44
|
const _trashList = Symbol.for('trashList')
|
|
40
45
|
const _handleOptionalFailure = Symbol.for('handleOptionalFailure')
|
|
@@ -81,7 +86,6 @@ const _global = Symbol.for('global')
|
|
|
81
86
|
|
|
82
87
|
// defined by Ideal mixin
|
|
83
88
|
const _pruneBundledMetadeps = Symbol.for('pruneBundledMetadeps')
|
|
84
|
-
const _explicitRequests = Symbol.for('explicitRequests')
|
|
85
89
|
const _resolvedAdd = Symbol.for('resolvedAdd')
|
|
86
90
|
const _usePackageLock = Symbol.for('usePackageLock')
|
|
87
91
|
const _formatPackageLock = Symbol.for('formatPackageLock')
|
|
@@ -145,7 +149,10 @@ module.exports = cls => class Reifier extends cls {
|
|
|
145
149
|
if (this[_packageLockOnly] || this[_dryRun])
|
|
146
150
|
return
|
|
147
151
|
|
|
148
|
-
|
|
152
|
+
// we do NOT want to set ownership on this folder, especially
|
|
153
|
+
// recursively, because it can have other side effects to do that
|
|
154
|
+
// in a project directory. We just want to make it if it's missing.
|
|
155
|
+
await justMkdirp(resolve(this.path))
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
async [_reifyPackages] () {
|
|
@@ -236,9 +243,25 @@ module.exports = cls => class Reifier extends cls {
|
|
|
236
243
|
const actualOpt = this[_global] ? {
|
|
237
244
|
ignoreMissing: true,
|
|
238
245
|
global: true,
|
|
239
|
-
filter: (node, kid) =>
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
filter: (node, kid) => {
|
|
247
|
+
// if it's not the project root, and we have no explicit requests,
|
|
248
|
+
// then we're already into a nested dep, so we keep it
|
|
249
|
+
if (this.explicitRequests.size === 0 || !node.isProjectRoot)
|
|
250
|
+
return true
|
|
251
|
+
|
|
252
|
+
// if we added it as an edgeOut, then we want it
|
|
253
|
+
if (this.idealTree.edgesOut.has(kid))
|
|
254
|
+
return true
|
|
255
|
+
|
|
256
|
+
// if it's an explicit request, then we want it
|
|
257
|
+
const hasExplicit = [...this.explicitRequests]
|
|
258
|
+
.some(edge => edge.name === kid)
|
|
259
|
+
if (hasExplicit)
|
|
260
|
+
return true
|
|
261
|
+
|
|
262
|
+
// ignore the rest of the global install folder
|
|
263
|
+
return false
|
|
264
|
+
},
|
|
242
265
|
} : { ignoreMissing: true }
|
|
243
266
|
|
|
244
267
|
if (!this[_global]) {
|
|
@@ -265,9 +288,35 @@ module.exports = cls => class Reifier extends cls {
|
|
|
265
288
|
// to just invalidate the parts that changed, but avoid walking the
|
|
266
289
|
// whole tree again.
|
|
267
290
|
|
|
291
|
+
const filterNodes = []
|
|
292
|
+
if (this[_global] && this.explicitRequests.size) {
|
|
293
|
+
const idealTree = this.idealTree.target || this.idealTree
|
|
294
|
+
const actualTree = this.actualTree.target || this.actualTree
|
|
295
|
+
// we ONLY are allowed to make changes in the global top-level
|
|
296
|
+
// children where there's an explicit request.
|
|
297
|
+
for (const { name } of this.explicitRequests) {
|
|
298
|
+
const ideal = idealTree.children.get(name)
|
|
299
|
+
if (ideal)
|
|
300
|
+
filterNodes.push(ideal)
|
|
301
|
+
const actual = actualTree.children.get(name)
|
|
302
|
+
if (actual)
|
|
303
|
+
filterNodes.push(actual)
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
for (const ws of this[_workspaces]) {
|
|
307
|
+
const ideal = this.idealTree.children.get(ws)
|
|
308
|
+
if (ideal)
|
|
309
|
+
filterNodes.push(ideal)
|
|
310
|
+
const actual = this.actualTree.children.get(ws)
|
|
311
|
+
if (actual)
|
|
312
|
+
filterNodes.push(actual)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
268
316
|
// find all the nodes that need to change between the actual
|
|
269
317
|
// and ideal trees.
|
|
270
318
|
this.diff = Diff.calculate({
|
|
319
|
+
filterNodes,
|
|
271
320
|
actual: this.actualTree,
|
|
272
321
|
ideal: this.idealTree,
|
|
273
322
|
})
|
|
@@ -881,11 +930,17 @@ module.exports = cls => class Reifier extends cls {
|
|
|
881
930
|
|
|
882
931
|
process.emit('time', 'reify:save')
|
|
883
932
|
|
|
884
|
-
|
|
933
|
+
// resolvedAdd is the list of user add requests, but with names added
|
|
934
|
+
// to things like git repos and tarball file/urls. However, if the
|
|
935
|
+
// user requested 'foo@', and we have a foo@file:../foo, then we should
|
|
936
|
+
// end up saving the spec we actually used, not whatever they gave us.
|
|
937
|
+
if (this[_resolvedAdd].length) {
|
|
885
938
|
const root = this.idealTree
|
|
886
939
|
const pkg = root.package
|
|
887
|
-
for (const
|
|
888
|
-
const
|
|
940
|
+
for (const { name } of this[_resolvedAdd]) {
|
|
941
|
+
const req = npa.resolve(name, root.edgesOut.get(name).spec, root.realpath)
|
|
942
|
+
const {rawSpec, subSpec} = req
|
|
943
|
+
|
|
889
944
|
const spec = subSpec ? subSpec.rawSpec : rawSpec
|
|
890
945
|
const child = root.children.get(name)
|
|
891
946
|
|
|
@@ -910,6 +965,15 @@ module.exports = cls => class Reifier extends cls {
|
|
|
910
965
|
const save = h.https && h.auth ? `git+${h.https(opt)}`
|
|
911
966
|
: h.shortcut(opt)
|
|
912
967
|
updateDepSpec(pkg, name, save)
|
|
968
|
+
} else if (req.type === 'directory' || req.type === 'file') {
|
|
969
|
+
// save the relative path in package.json
|
|
970
|
+
// Normally saveSpec is updated with the proper relative
|
|
971
|
+
// path already, but it's possible to specify a full absolute
|
|
972
|
+
// path initially, in which case we can end up with the wrong
|
|
973
|
+
// thing, so just get the ultimate fetchSpec and relativize it.
|
|
974
|
+
const p = req.fetchSpec.replace(/^file:/, '')
|
|
975
|
+
const rel = relpath(root.realpath, p)
|
|
976
|
+
updateDepSpec(pkg, name, `file:${rel}`)
|
|
913
977
|
} else
|
|
914
978
|
updateDepSpec(pkg, name, req.saveSpec)
|
|
915
979
|
}
|
|
@@ -950,20 +1014,85 @@ module.exports = cls => class Reifier extends cls {
|
|
|
950
1014
|
return meta.save(saveOpt)
|
|
951
1015
|
}
|
|
952
1016
|
|
|
953
|
-
[_copyIdealToActual] () {
|
|
1017
|
+
async [_copyIdealToActual] () {
|
|
1018
|
+
// clean up any trash that is still in the tree
|
|
1019
|
+
for (const path of this[_trashList]) {
|
|
1020
|
+
const loc = relpath(this.idealTree.realpath, path)
|
|
1021
|
+
const node = this.idealTree.inventory.get(loc)
|
|
1022
|
+
if (node && node.root === this.idealTree)
|
|
1023
|
+
node.parent = null
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// if we filtered to only certain nodes, then anything ELSE needs
|
|
1027
|
+
// to be untouched in the resulting actual tree, even if it differs
|
|
1028
|
+
// in the idealTree. Copy over anything that was in the actual and
|
|
1029
|
+
// was not changed, delete anything in the ideal and not actual.
|
|
1030
|
+
// Then we move the entire idealTree over to this.actualTree, and
|
|
1031
|
+
// save the hidden lockfile.
|
|
1032
|
+
if (this.diff && this.diff.filterSet.size) {
|
|
1033
|
+
const { filterSet } = this.diff
|
|
1034
|
+
const seen = new Set()
|
|
1035
|
+
for (const [loc, ideal] of this.idealTree.inventory.entries()) {
|
|
1036
|
+
if (seen.has(loc))
|
|
1037
|
+
continue
|
|
1038
|
+
seen.add(loc)
|
|
1039
|
+
|
|
1040
|
+
// if it's an ideal node from the filter set, then skip it
|
|
1041
|
+
// because we already made whatever changes were necessary
|
|
1042
|
+
if (filterSet.has(ideal))
|
|
1043
|
+
continue
|
|
1044
|
+
|
|
1045
|
+
// otherwise, if it's not in the actualTree, then it's not a thing
|
|
1046
|
+
// that we actually added. And if it IS in the actualTree, then
|
|
1047
|
+
// it's something that we left untouched, so we need to record
|
|
1048
|
+
// that.
|
|
1049
|
+
const actual = this.actualTree.inventory.get(loc)
|
|
1050
|
+
if (!actual)
|
|
1051
|
+
ideal.root = null
|
|
1052
|
+
else {
|
|
1053
|
+
if ([...actual.linksIn].some(link => filterSet.has(link))) {
|
|
1054
|
+
seen.add(actual.location)
|
|
1055
|
+
continue
|
|
1056
|
+
}
|
|
1057
|
+
const { realpath, isLink } = actual
|
|
1058
|
+
if (isLink && ideal.isLink && ideal.realpath === realpath)
|
|
1059
|
+
continue
|
|
1060
|
+
else
|
|
1061
|
+
actual.root = this.idealTree
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// now find any actual nodes that may not be present in the ideal
|
|
1066
|
+
// tree, but were left behind by virtue of not being in the filter
|
|
1067
|
+
for (const [loc, actual] of this.actualTree.inventory.entries()) {
|
|
1068
|
+
if (seen.has(loc))
|
|
1069
|
+
continue
|
|
1070
|
+
seen.add(loc)
|
|
1071
|
+
if (filterSet.has(actual))
|
|
1072
|
+
continue
|
|
1073
|
+
actual.root = this.idealTree
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// prune out any tops that lack a linkIn
|
|
1077
|
+
for (const top of this.idealTree.tops) {
|
|
1078
|
+
if (top.linksIn.size === 0)
|
|
1079
|
+
top.root = null
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// need to calculate dep flags, since nodes may have been marked
|
|
1083
|
+
// as extraneous or otherwise incorrect during transit.
|
|
1084
|
+
calcDepFlags(this.idealTree)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
954
1087
|
// save the ideal's meta as a hidden lockfile after we actualize it
|
|
955
1088
|
this.idealTree.meta.filename =
|
|
956
|
-
this.
|
|
1089
|
+
this.idealTree.realpath + '/node_modules/.package-lock.json'
|
|
957
1090
|
this.idealTree.meta.hiddenLockfile = true
|
|
1091
|
+
|
|
958
1092
|
this.actualTree = this.idealTree
|
|
959
1093
|
this.idealTree = null
|
|
960
|
-
for (const path of this[_trashList]) {
|
|
961
|
-
const loc = relpath(this.path, path)
|
|
962
|
-
const node = this.actualTree.inventory.get(loc)
|
|
963
|
-
if (node && node.root === this.actualTree)
|
|
964
|
-
node.parent = null
|
|
965
|
-
}
|
|
966
1094
|
|
|
967
|
-
|
|
1095
|
+
if (!this[_global])
|
|
1096
|
+
await this.actualTree.meta.save()
|
|
968
1097
|
}
|
|
969
1098
|
}
|
package/lib/diff.js
CHANGED
|
@@ -11,7 +11,8 @@ const {existsSync} = require('fs')
|
|
|
11
11
|
const ssri = require('ssri')
|
|
12
12
|
|
|
13
13
|
class Diff {
|
|
14
|
-
constructor ({actual, ideal}) {
|
|
14
|
+
constructor ({actual, ideal, filterSet}) {
|
|
15
|
+
this.filterSet = filterSet
|
|
15
16
|
this.children = []
|
|
16
17
|
this.actual = actual
|
|
17
18
|
this.ideal = ideal
|
|
@@ -29,9 +30,54 @@ class Diff {
|
|
|
29
30
|
this.removed = []
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
static calculate ({actual, ideal}) {
|
|
33
|
+
static calculate ({actual, ideal, filterNodes = []}) {
|
|
34
|
+
// if there's a filterNode, then:
|
|
35
|
+
// - get the path from the root to the filterNode. The root or
|
|
36
|
+
// root.target should have an edge either to the filterNode or
|
|
37
|
+
// a link to the filterNode. If not, abort. Add the path to the
|
|
38
|
+
// filterSet.
|
|
39
|
+
// - Add set of Nodes depended on by the filterNode to filterSet.
|
|
40
|
+
// - Anything outside of that set should be ignored by getChildren
|
|
41
|
+
const filterSet = new Set()
|
|
42
|
+
for (const filterNode of filterNodes) {
|
|
43
|
+
const { root } = filterNode
|
|
44
|
+
if (root !== ideal && root !== actual)
|
|
45
|
+
throw new Error('invalid filterNode: outside idealTree/actualTree')
|
|
46
|
+
const { target } = root
|
|
47
|
+
const rootTarget = target || root
|
|
48
|
+
const edge = [...rootTarget.edgesOut.values()].filter(e => {
|
|
49
|
+
return e.to && (e.to === filterNode || e.to.target === filterNode)
|
|
50
|
+
})[0]
|
|
51
|
+
filterSet.add(root)
|
|
52
|
+
filterSet.add(rootTarget)
|
|
53
|
+
filterSet.add(ideal)
|
|
54
|
+
filterSet.add(actual)
|
|
55
|
+
if (edge && edge.to) {
|
|
56
|
+
filterSet.add(edge.to)
|
|
57
|
+
if (edge.to.target)
|
|
58
|
+
filterSet.add(edge.to.target)
|
|
59
|
+
}
|
|
60
|
+
filterSet.add(filterNode)
|
|
61
|
+
|
|
62
|
+
depth({
|
|
63
|
+
tree: filterNode,
|
|
64
|
+
visit: node => filterSet.add(node),
|
|
65
|
+
getChildren: node => {
|
|
66
|
+
node = node.target || node
|
|
67
|
+
const loc = node.location
|
|
68
|
+
const idealNode = ideal.inventory.get(loc)
|
|
69
|
+
const ideals = !idealNode ? []
|
|
70
|
+
: [...idealNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
|
|
71
|
+
const actualNode = actual.inventory.get(loc)
|
|
72
|
+
const actuals = !actualNode ? []
|
|
73
|
+
: [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
|
|
74
|
+
return ideals.concat(actuals)
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
33
79
|
return depth({
|
|
34
|
-
tree: new Diff({actual, ideal}),
|
|
80
|
+
tree: new Diff({actual, ideal, filterSet}),
|
|
35
81
|
getChildren,
|
|
36
82
|
leave,
|
|
37
83
|
})
|
|
@@ -89,20 +135,20 @@ const allChildren = node => {
|
|
|
89
135
|
// to create the diff tree
|
|
90
136
|
const getChildren = diff => {
|
|
91
137
|
const children = []
|
|
92
|
-
const {unchanged, removed} = diff
|
|
138
|
+
const {actual, ideal, unchanged, removed, filterSet} = diff
|
|
93
139
|
|
|
94
140
|
// Note: we DON'T diff fsChildren themselves, because they are either
|
|
95
141
|
// included in the package contents, or part of some other project, and
|
|
96
142
|
// will never appear in legacy shrinkwraps anyway. but we _do_ include the
|
|
97
143
|
// child nodes of fsChildren, because those are nodes that we are typically
|
|
98
144
|
// responsible for installing.
|
|
99
|
-
const actualKids = allChildren(
|
|
100
|
-
const idealKids = allChildren(
|
|
145
|
+
const actualKids = allChildren(actual)
|
|
146
|
+
const idealKids = allChildren(ideal)
|
|
101
147
|
const paths = new Set([...actualKids.keys(), ...idealKids.keys()])
|
|
102
148
|
for (const path of paths) {
|
|
103
149
|
const actual = actualKids.get(path)
|
|
104
150
|
const ideal = idealKids.get(path)
|
|
105
|
-
diffNode(actual, ideal, children, unchanged, removed)
|
|
151
|
+
diffNode(actual, ideal, children, unchanged, removed, filterSet)
|
|
106
152
|
}
|
|
107
153
|
|
|
108
154
|
if (diff.leaves && !children.length)
|
|
@@ -111,7 +157,10 @@ const getChildren = diff => {
|
|
|
111
157
|
return children
|
|
112
158
|
}
|
|
113
159
|
|
|
114
|
-
const diffNode = (actual, ideal, children, unchanged, removed) => {
|
|
160
|
+
const diffNode = (actual, ideal, children, unchanged, removed, filterSet) => {
|
|
161
|
+
if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual)))
|
|
162
|
+
return
|
|
163
|
+
|
|
115
164
|
const action = getAction({actual, ideal})
|
|
116
165
|
|
|
117
166
|
// if it's a match, then get its children
|
|
@@ -119,7 +168,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => {
|
|
|
119
168
|
if (action) {
|
|
120
169
|
if (action === 'REMOVE')
|
|
121
170
|
removed.push(actual)
|
|
122
|
-
children.push(new Diff({actual, ideal}))
|
|
171
|
+
children.push(new Diff({actual, ideal, filterSet}))
|
|
123
172
|
} else {
|
|
124
173
|
unchanged.push(ideal)
|
|
125
174
|
// !*! Weird dirty hack warning !*!
|
|
@@ -150,7 +199,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => {
|
|
|
150
199
|
for (const node of bundledChildren)
|
|
151
200
|
node.parent = ideal
|
|
152
201
|
}
|
|
153
|
-
children.push(...getChildren({actual, ideal, unchanged, removed}))
|
|
202
|
+
children.push(...getChildren({actual, ideal, unchanged, removed, filterSet}))
|
|
154
203
|
}
|
|
155
204
|
}
|
|
156
205
|
|
package/lib/link.js
CHANGED
|
@@ -23,13 +23,19 @@ class Link extends Node {
|
|
|
23
23
|
: null),
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
if (target)
|
|
27
|
+
this.target = target
|
|
28
|
+
else if (this.realpath === this.root.path)
|
|
29
|
+
this.target = this.root
|
|
30
|
+
else {
|
|
31
|
+
this.target = new Node({
|
|
32
|
+
...options,
|
|
33
|
+
path: realpath,
|
|
34
|
+
parent: null,
|
|
35
|
+
fsParent: null,
|
|
36
|
+
root: this.root,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
get version () {
|
package/lib/node.js
CHANGED
|
@@ -685,6 +685,7 @@ class Node {
|
|
|
685
685
|
...this.children.values(),
|
|
686
686
|
...this.inventory.values(),
|
|
687
687
|
].filter(n => n !== this))
|
|
688
|
+
|
|
688
689
|
for (const child of family) {
|
|
689
690
|
if (child.root !== root) {
|
|
690
691
|
child[_delistFromMeta]()
|
|
@@ -704,12 +705,14 @@ class Node {
|
|
|
704
705
|
}
|
|
705
706
|
|
|
706
707
|
// if we had a target, and didn't find one in the new root, then bring
|
|
707
|
-
// it over as well
|
|
708
|
-
|
|
708
|
+
// it over as well, but only if we're setting the link into a new root,
|
|
709
|
+
// as we don't want to lose the target any time we remove a link.
|
|
710
|
+
if (this.isLink && target && !this.target && root !== this)
|
|
709
711
|
target.root = root
|
|
710
712
|
|
|
711
713
|
// tree should always be valid upon root setter completion.
|
|
712
714
|
treeCheck(this)
|
|
715
|
+
treeCheck(root)
|
|
713
716
|
}
|
|
714
717
|
|
|
715
718
|
get root () {
|
package/lib/printable.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// of the current node and its descendents
|
|
3
3
|
|
|
4
4
|
const util = require('util')
|
|
5
|
+
const relpath = require('./relpath.js')
|
|
5
6
|
|
|
6
7
|
class ArboristNode {
|
|
7
8
|
constructor (tree, path) {
|
|
@@ -47,6 +48,11 @@ class ArboristNode {
|
|
|
47
48
|
.map(edge => new EdgeIn(edge)))
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
if (tree.workspaces && tree.workspaces.size) {
|
|
52
|
+
this.workspaces = new Map([...tree.workspaces.entries()]
|
|
53
|
+
.map(([name, path]) => [name, relpath(tree.root.realpath, path)]))
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
// fsChildren sorted by path
|
|
51
57
|
if (tree.fsChildren.size) {
|
|
52
58
|
this.fsChildren = new Set([...tree.fsChildren]
|
|
@@ -63,6 +69,13 @@ class ArboristNode {
|
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
class ArboristVirtualNode extends ArboristNode {
|
|
73
|
+
constructor (tree, path) {
|
|
74
|
+
super(tree, path)
|
|
75
|
+
this.sourceReference = printableTree(tree.sourceReference, path)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
class ArboristLink extends ArboristNode {
|
|
67
80
|
constructor (tree, path) {
|
|
68
81
|
super(tree, path)
|
|
@@ -119,10 +132,17 @@ class EdgeIn extends Edge {
|
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
const printableTree = (tree, path = []) => {
|
|
122
|
-
if (
|
|
123
|
-
return
|
|
135
|
+
if (!tree)
|
|
136
|
+
return tree
|
|
137
|
+
|
|
138
|
+
const Cls = tree.isLink ? ArboristLink
|
|
139
|
+
: tree.sourceReference ? ArboristVirtualNode
|
|
140
|
+
: ArboristNode
|
|
141
|
+
if (path.includes(tree)) {
|
|
142
|
+
const obj = Object.create(Cls.prototype)
|
|
143
|
+
return Object.assign(obj, { location: tree.location })
|
|
144
|
+
}
|
|
124
145
|
path.push(tree)
|
|
125
|
-
const Cls = tree.isLink ? ArboristLink : ArboristNode
|
|
126
146
|
return new Cls(tree, path)
|
|
127
147
|
}
|
|
128
148
|
|
package/lib/shrinkwrap.js
CHANGED
|
@@ -41,6 +41,7 @@ const readFile = promisify(fs.readFile)
|
|
|
41
41
|
const writeFile = promisify(fs.writeFile)
|
|
42
42
|
const stat = promisify(fs.stat)
|
|
43
43
|
const readdir_ = promisify(fs.readdir)
|
|
44
|
+
const readlink = promisify(fs.readlink)
|
|
44
45
|
|
|
45
46
|
// XXX remove when drop support for node v10
|
|
46
47
|
const lstat = promisify(fs.lstat)
|
|
@@ -176,10 +177,19 @@ const assertNoNewer = async (path, data, lockTime, dir = path, seen = null) => {
|
|
|
176
177
|
: readdir(parent, { withFileTypes: true })
|
|
177
178
|
|
|
178
179
|
return children.catch(() => [])
|
|
179
|
-
.then(ents => Promise.all(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
.then(ents => Promise.all(ents.map(async ent => {
|
|
181
|
+
const child = resolve(parent, ent.name)
|
|
182
|
+
if (ent.isDirectory() && !/^\./.test(ent.name))
|
|
183
|
+
await assertNoNewer(path, data, lockTime, child, seen)
|
|
184
|
+
else if (ent.isSymbolicLink()) {
|
|
185
|
+
const target = resolve(parent, await readlink(child))
|
|
186
|
+
const tstat = await stat(target).catch(() => null)
|
|
187
|
+
seen.add(relpath(path, child))
|
|
188
|
+
if (tstat && tstat.isDirectory() && !seen.has(relpath(path, target)))
|
|
189
|
+
await assertNoNewer(path, data, lockTime, target, seen)
|
|
190
|
+
}
|
|
191
|
+
})))
|
|
192
|
+
.then(() => {
|
|
183
193
|
if (dir !== path)
|
|
184
194
|
return
|
|
185
195
|
|
package/lib/tree-check.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const debug = require('./debug.js')
|
|
2
2
|
|
|
3
3
|
const checkTree = (tree, checkUnreachable = true) => {
|
|
4
|
+
const log = [['START TREE CHECK', tree.path]]
|
|
5
|
+
|
|
4
6
|
// this can only happen in tests where we have a "tree" object
|
|
5
7
|
// that isn't actually a tree.
|
|
6
8
|
if (!tree.root || !tree.root.inventory)
|
|
@@ -9,8 +11,21 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
9
11
|
const { inventory } = tree.root
|
|
10
12
|
const seen = new Set()
|
|
11
13
|
const check = (node, via = tree, viaType = 'self') => {
|
|
14
|
+
log.push([
|
|
15
|
+
'CHECK',
|
|
16
|
+
node && node.location,
|
|
17
|
+
via && via.location,
|
|
18
|
+
viaType,
|
|
19
|
+
'seen=' + seen.has(node),
|
|
20
|
+
'promise=' + !!(node && node.then),
|
|
21
|
+
'root=' + !!(node && node.isRoot),
|
|
22
|
+
])
|
|
23
|
+
|
|
12
24
|
if (!node || seen.has(node) || node.then)
|
|
13
25
|
return
|
|
26
|
+
|
|
27
|
+
seen.add(node)
|
|
28
|
+
|
|
14
29
|
if (node.isRoot && node !== tree.root) {
|
|
15
30
|
throw Object.assign(new Error('double root'), {
|
|
16
31
|
node: node.path,
|
|
@@ -19,6 +34,7 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
19
34
|
root: tree.root.path,
|
|
20
35
|
via: via.path,
|
|
21
36
|
viaType,
|
|
37
|
+
log,
|
|
22
38
|
})
|
|
23
39
|
}
|
|
24
40
|
|
|
@@ -31,6 +47,7 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
31
47
|
via: via.path,
|
|
32
48
|
viaType,
|
|
33
49
|
otherRoot: node.root && node.root.path,
|
|
50
|
+
log,
|
|
34
51
|
})
|
|
35
52
|
}
|
|
36
53
|
|
|
@@ -43,6 +60,7 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
43
60
|
viaType,
|
|
44
61
|
inventory: [...node.inventory.values()].map(node =>
|
|
45
62
|
[node.path, node.location]),
|
|
63
|
+
log,
|
|
46
64
|
})
|
|
47
65
|
}
|
|
48
66
|
|
|
@@ -53,6 +71,7 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
53
71
|
root: tree.root.path,
|
|
54
72
|
via: via.path,
|
|
55
73
|
viaType,
|
|
74
|
+
log,
|
|
56
75
|
})
|
|
57
76
|
}
|
|
58
77
|
|
|
@@ -65,14 +84,38 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
65
84
|
via: via.path,
|
|
66
85
|
viaType,
|
|
67
86
|
devEdges: devEdges.map(e => [e.type, e.name, e.spec, e.error]),
|
|
87
|
+
log,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (node.path === tree.root.path && node !== tree.root) {
|
|
92
|
+
throw Object.assign(new Error('node with same path as root'), {
|
|
93
|
+
node: node.path,
|
|
94
|
+
tree: tree.path,
|
|
95
|
+
root: tree.root.path,
|
|
96
|
+
via: via.path,
|
|
97
|
+
viaType,
|
|
98
|
+
log,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!node.isLink && node.path !== node.realpath) {
|
|
103
|
+
throw Object.assign(new Error('non-link with mismatched path/realpath'), {
|
|
104
|
+
node: node.path,
|
|
105
|
+
tree: tree.path,
|
|
106
|
+
realpath: node.realpath,
|
|
107
|
+
root: tree.root.path,
|
|
108
|
+
via: via.path,
|
|
109
|
+
viaType,
|
|
110
|
+
log,
|
|
68
111
|
})
|
|
69
112
|
}
|
|
70
113
|
|
|
71
114
|
const { parent, fsParent, target } = node
|
|
72
|
-
seen.add(node)
|
|
73
115
|
check(parent, node, 'parent')
|
|
74
116
|
check(fsParent, node, 'fsParent')
|
|
75
117
|
check(target, node, 'target')
|
|
118
|
+
log.push(['CHILDREN', node.location, ...node.children.keys()])
|
|
76
119
|
for (const kid of node.children.values())
|
|
77
120
|
check(kid, node, 'children')
|
|
78
121
|
for (const kid of node.fsChildren)
|
|
@@ -81,6 +124,7 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
81
124
|
check(link, node, 'linksIn')
|
|
82
125
|
for (const top of node.tops)
|
|
83
126
|
check(top, node, 'tops')
|
|
127
|
+
log.push(['DONE', node.location])
|
|
84
128
|
}
|
|
85
129
|
check(tree)
|
|
86
130
|
if (checkUnreachable) {
|
|
@@ -92,6 +136,7 @@ const checkTree = (tree, checkUnreachable = true) => {
|
|
|
92
136
|
location: node.location,
|
|
93
137
|
root: tree.root.path,
|
|
94
138
|
tree: tree.path,
|
|
139
|
+
log,
|
|
95
140
|
})
|
|
96
141
|
}
|
|
97
142
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Manage node_modules trees",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@npmcli/installed-package-contents": "^1.0.7",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"cacache": "^15.0.3",
|
|
15
15
|
"common-ancestor-path": "^1.0.1",
|
|
16
16
|
"json-parse-even-better-errors": "^2.3.1",
|
|
17
|
-
"json-stringify-nice": "^1.1.
|
|
17
|
+
"json-stringify-nice": "^1.1.2",
|
|
18
18
|
"mkdirp-infer-owner": "^2.0.0",
|
|
19
19
|
"npm-install-checks": "^4.0.0",
|
|
20
20
|
"npm-package-arg": "^8.1.0",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"promise-call-limit": "^1.0.1",
|
|
27
27
|
"read-package-json-fast": "^2.0.2",
|
|
28
28
|
"readdir-scoped-modules": "^1.1.0",
|
|
29
|
-
"semver": "^7.3.
|
|
29
|
+
"semver": "^7.3.5",
|
|
30
30
|
"tar": "^6.1.0",
|
|
31
31
|
"treeverse": "^1.0.4",
|
|
32
32
|
"walk-up-path": "^1.0.0"
|