@npmcli/arborist 0.0.33 → 1.0.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.
@@ -70,7 +70,7 @@ const _queueNamedUpdates = Symbol('queueNamedUpdates')
70
70
  const _queueVulnDependents = Symbol('queueVulnDependents')
71
71
  const _avoidRange = Symbol('avoidRange')
72
72
  const _shouldUpdateNode = Symbol('shouldUpdateNode')
73
- const _resetDepFlags = Symbol('resetDepFlags')
73
+ const resetDepFlags = require('../reset-dep-flags.js')
74
74
  const _loadFailures = Symbol('loadFailures')
75
75
  const _pruneFailedOptional = Symbol('pruneFailedOptional')
76
76
  const _linkNodes = Symbol('linkNodes')
@@ -89,10 +89,19 @@ const _strictPeerDeps = Symbol('strictPeerDeps')
89
89
  const _checkEngineAndPlatform = Symbol('checkEngineAndPlatform')
90
90
  const _checkEngine = Symbol('checkEngine')
91
91
  const _checkPlatform = Symbol('checkPlatform')
92
+ const _virtualRoots = Symbol('virtualRoots')
93
+ const _virtualRoot = Symbol('virtualRoot')
92
94
 
93
95
  // used for the ERESOLVE error to show the last peer conflict encountered
94
96
  const _peerConflict = Symbol('peerConflict')
95
97
 
98
+ const _failPeerConflict = Symbol('failPeerConflict')
99
+ const _explainPeerConflict = Symbol('explainPeerConflict')
100
+ const _warnPeerConflict = Symbol('warnPeerConflict')
101
+ const _edgesOverridden = Symbol('edgesOverridden')
102
+ // exposed symbol for unit testing the placeDep method directly
103
+ const _peerSetSource = Symbol.for('peerSetSource')
104
+
96
105
  // used by Reify mixin
97
106
  const _force = Symbol.for('force')
98
107
  const _explicitRequests = Symbol.for('explicitRequests')
@@ -142,6 +151,13 @@ module.exports = cls => class IdealTreeBuilder extends cls {
142
151
  this[_linkNodes] = new Set()
143
152
  this[_manifests] = new Map()
144
153
  this[_peerConflict] = null
154
+ this[_edgesOverridden] = new Set()
155
+
156
+ // a map of each module in a peer set to the thing that depended on
157
+ // that set of peers in the first place. Use a WeakMap so that we
158
+ // don't hold onto references for nodes that are garbage collected.
159
+ this[_peerSetSource] = new WeakMap()
160
+ this[_virtualRoots] = new Map()
145
161
  }
146
162
 
147
163
  get explicitRequests () {
@@ -294,6 +310,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
294
310
  await new this.constructor(this.options).loadActual({ root })
295
311
  return root
296
312
  })
313
+
297
314
  .then(tree => {
298
315
  // null the virtual tree, because we're about to hack away at it
299
316
  // if you want another one, load another copy.
@@ -707,16 +724,42 @@ This is a one-time fix-up, please be patient...
707
724
  // Set `preferDedupe: true` in the options to replace the shallower
708
725
  // dep if allowed.
709
726
 
710
- const tasks = await Promise.all(
711
- // resolve all the edges into nodes using pacote.manifest
712
- // return a {dep,edge} object so that we can track the reason
713
- // for this node through the parallelized async operation.
714
- // note that dep.edgesOut will have all its peer deps resolved,
715
- // since they're relevant in the calculation about where to place
716
- // the new and/or updated dependency.
717
- this[_problemEdges](node).map(edge => this[_nodeFromEdge](edge)
718
- .then(dep => ({edge, dep})))
719
- )
727
+ const tasks = []
728
+ const peerSource = this[_peerSetSource].get(node) || node
729
+ for (const edge of this[_problemEdges](node)) {
730
+ if (this[_edgesOverridden].has(edge))
731
+ continue
732
+
733
+ // peerSetSource is only relevant when we have a peerEntryEdge
734
+ // otherwise we're setting regular non-peer deps as if they have
735
+ // a virtual root of whatever brought in THIS node.
736
+ // so we VR the node itself if the edge is not a peer
737
+ const source = edge.peer ? peerSource : node
738
+ const virtualRoot = this[_virtualRoot](source, true)
739
+ // reuse virtual root if we already have one, but don't
740
+ // try to do the override ahead of time, since we MAY be able
741
+ // to create a more correct tree than the virtual root could.
742
+ const vrEdge = virtualRoot && virtualRoot.edgesOut.get(edge.name)
743
+ const vrDep = vrEdge && vrEdge.valid && vrEdge.to
744
+ // only re-use the virtualRoot if it's a peer edge we're placing.
745
+ // otherwise, we end up in situations where we override peer deps that
746
+ // we could have otherwise found homes for. Eg:
747
+ // xy -> (x, y)
748
+ // x -> PEER(z@1)
749
+ // y -> PEER(z@2)
750
+ // If xy is a dependency, we can resolve this like:
751
+ // project
752
+ // +-- xy
753
+ // | +-- y
754
+ // | +-- z@2
755
+ // +-- x
756
+ // +-- z@1
757
+ // But if x and y are loaded in the same virtual root, then they will
758
+ // be forced to agree on a version of z.
759
+ const dep = vrDep && vrDep.satisfies(edge) ? vrDep
760
+ : await this[_nodeFromEdge](edge, edge.peer ? virtualRoot : null)
761
+ tasks.push({edge, dep})
762
+ }
720
763
 
721
764
  const placed = tasks
722
765
  .sort((a, b) => a.edge.name.localeCompare(b.edge.name))
@@ -746,33 +789,45 @@ This is a one-time fix-up, please be patient...
746
789
 
747
790
  // loads a node from an edge, and then loads its peer deps (and their
748
791
  // peer deps, on down the line) into a virtual root parent.
749
- [_nodeFromEdge] (edge, parent) {
792
+ [_nodeFromEdge] (edge, parent_) {
750
793
  // create a virtual root node with the same deps as the node that
751
794
  // is requesting this one, so that we can get all the peer deps in
752
795
  // a context where they're likely to be resolvable.
753
- const { legacyPeerDeps } = this
754
- parent = parent || new Node({
755
- path: '/virtual-root',
756
- sourceReference: edge.from,
757
- legacyPeerDeps,
758
- })
796
+ const parent = parent_ || this[_virtualRoot](edge.from)
759
797
 
760
798
  const spec = npa.resolve(edge.name, edge.spec, edge.from.path)
761
799
  return this[_nodeFromSpec](edge.name, spec, parent, edge)
762
800
  .then(node => {
763
- // handle otherwise unresolvable dependency nesting loops by
764
- // creating a symbolic link
765
- // a1 -> b1 -> a2 -> b2 -> a1 -> ...
766
- // instead of nesting forever, when the loop occurs, create
767
- // a symbolic link to the earlier instance
801
+ // handle otherwise unresolvable dependency nesting loops by
802
+ // creating a symbolic link
803
+ // a1 -> b1 -> a2 -> b2 -> a1 -> ...
804
+ // instead of nesting forever, when the loop occurs, create
805
+ // a symbolic link to the earlier instance
768
806
  for (let p = edge.from.resolveParent; p; p = p.resolveParent) {
769
807
  if (p.matches(node) && !p.isRoot)
770
808
  return new Link({ parent, target: p })
771
809
  }
810
+ // keep track of the thing that caused this node to be included.
811
+ const src = parent.sourceReference
812
+ this[_peerSetSource].set(node, src)
772
813
  return this[_loadPeerSet](node)
773
814
  })
774
815
  }
775
816
 
817
+ [_virtualRoot] (node, reuse = false) {
818
+ if (reuse && this[_virtualRoots].has(node))
819
+ return this[_virtualRoots].get(node)
820
+
821
+ const vr = new Node({
822
+ path: '/virtual-root',
823
+ sourceReference: node,
824
+ legacyPeerDeps: this.legacyPeerDeps,
825
+ })
826
+
827
+ this[_virtualRoots].set(node, vr)
828
+ return vr
829
+ }
830
+
776
831
  [_problemEdges] (node) {
777
832
  // skip over any bundled deps, they're not our problem.
778
833
  // Note that this WILL fetch bundled meta-deps which are also dependencies
@@ -801,14 +856,14 @@ This is a one-time fix-up, please be patient...
801
856
  if (edge.to && edge.to.inShrinkwrap)
802
857
  return false
803
858
 
804
- // If the edge has an error, there's a problem.
805
- if (!edge.valid)
806
- return true
807
-
808
859
  // If the edge has no destination, that's a problem.
809
860
  if (!edge.to)
810
861
  return edge.type !== 'peerOptional'
811
862
 
863
+ // If the edge has an error, there's a problem.
864
+ if (!edge.valid)
865
+ return true
866
+
812
867
  // If user has explicitly asked to update this package by name, it's a problem.
813
868
  if (this[_updateNames].includes(edge.name))
814
869
  return true
@@ -891,19 +946,100 @@ This is a one-time fix-up, please be patient...
891
946
  // We prefer to get peer deps that meet the requiring node's dependency,
892
947
  // if possible, since that almost certainly works (since that package was
893
948
  // developed with this set of deps) and will typically be more restrictive.
894
- [_loadPeerSet] (node) {
949
+ // Note that the peers in the set can conflict either with each other,
950
+ // or with a direct dependency from the virtual root parent! In strict
951
+ // mode, this is always an error. In force mode, it never is, and we
952
+ // prefer the parent's non-peer dep over a peer dep, or the version that
953
+ // gets placed first. In non-strict mode, we behave strictly if the
954
+ // virtual root is based on the root project, and allow non-peer parent
955
+ // deps to override, but throw if no preference can be determined.
956
+ async [_loadPeerSet] (node) {
895
957
  const peerEdges = [...node.edgesOut.values()]
896
- .filter(e => e.peer && !e.valid)
897
- .map(e => node.parent && node.parent.edgesOut.get(e.name) || e)
898
- return Promise.all(
899
- peerEdges.map(edge => this[_nodeFromEdge](edge, node.parent))
900
- ).then(() => node)
958
+ // we only care about peers here, and don't install peerOptionals
959
+ .filter(e => e.peer && !e.valid && !e.optional)
960
+ .sort(({name: a}, {name: b}) => a.localeCompare(b))
961
+
962
+ for (const edge of peerEdges) {
963
+ // already placed this one, and we're happy with it.
964
+ if (edge.valid)
965
+ continue
966
+
967
+ const parentEdge = node.parent.edgesOut.get(edge.name)
968
+ const {isRoot, isWorkspace} = node.parent.sourceReference
969
+ const isMine = isRoot || isWorkspace
970
+ if (edge.missing) {
971
+ if (!parentEdge) {
972
+ // easy, just put the thing there
973
+ await this[_nodeFromEdge](edge, node.parent)
974
+ continue
975
+ } else {
976
+ // try to put the parent's preference, and make sure that satisfies.
977
+ // if so, we're good.
978
+ // if it does not, then we have a problem in strict mode, no problem
979
+ // in force mode, and a problem in non-strict mode if this isn't
980
+ // on behalf of the root node. In all such cases, we warn at least.
981
+ await this[_nodeFromEdge](parentEdge, node.parent)
982
+
983
+ // hooray! that worked!
984
+ if (edge.valid)
985
+ continue
986
+
987
+ // allow it
988
+ if (this[_force] || !isMine && !this[_strictPeerDeps])
989
+ continue
990
+ else
991
+ this[_failPeerConflict](edge)
992
+ }
993
+ }
994
+
995
+ // at this point we know that there is a dep there, and
996
+ // we don't like it. always fail strictly, always allow forcibly or
997
+ // in non-strict mode if it's not our fault. don't warn here, because
998
+ // we are going to warn again when we place the deps, if we end up
999
+ // overriding for something else.
1000
+ if (this[_force] || !isMine && !this[_strictPeerDeps])
1001
+ continue
1002
+
1003
+ // ok, it's the root, or we're in unforced strict mode, so this is bad
1004
+ this[_failPeerConflict](edge)
1005
+ }
1006
+ return node
1007
+ }
1008
+
1009
+ [_failPeerConflict] (edge) {
1010
+ const expl = this[_explainPeerConflict](edge)
1011
+ throw Object.assign(new Error('unable to resolve dependency tree'), expl)
1012
+ }
1013
+
1014
+ [_explainPeerConflict] (edge) {
1015
+ const node = edge.from
1016
+ const curNode = node.resolve(edge.name)
1017
+ const pc = this[_peerConflict] || { peer: null, current: null }
1018
+ const current = curNode ? curNode.explain() : pc.current
1019
+ const peerConflict = pc.peer
1020
+ return {
1021
+ code: 'ERESOLVE',
1022
+ current,
1023
+ edge: edge.explain(),
1024
+ peerConflict,
1025
+ strictPeerDeps: this[_strictPeerDeps],
1026
+ }
1027
+ }
1028
+
1029
+ [_warnPeerConflict] (edge) {
1030
+ // track that we've overridden this edge, so that we don't keep trying
1031
+ // to re-resolve it in an infinite loop.
1032
+ this[_edgesOverridden].add(edge)
1033
+ const expl = this[_explainPeerConflict](edge)
1034
+ this.log.warn('ERESOLVE', 'overriding peer dependency', expl)
901
1035
  }
902
1036
 
903
1037
  // starting from either node, or in the case of non-root peer deps,
904
1038
  // the node's parent, walk up the tree until we find the first spot
905
1039
  // where this dep cannot be placed, and use the one right before that.
906
1040
  // place dep, requested by node, to satisfy edge
1041
+ // XXX split this out into a separate method or mixin? It's quite a lot
1042
+ // of functionality that ought to have its own unit tests more conveniently.
907
1043
  [_placeDep] (dep, node, edge, peerEntryEdge = null) {
908
1044
  if (edge.to &&
909
1045
  !edge.error &&
@@ -911,16 +1047,15 @@ This is a one-time fix-up, please be patient...
911
1047
  !this[_isVulnerable](edge.to))
912
1048
  return []
913
1049
 
914
- // top nodes should still get peer deps from their parent or fsParent
915
- // if possible, and only install locally if there's no other option,
916
- // eg for a link outside of the project root.
1050
+ // top nodes should still get peer deps from their fsParent if possible,
1051
+ // and only install locally if there's no other option, eg for a link
1052
+ // outside of the project root, or for a conflicted dep.
917
1053
  const start = edge.peer && !node.isRoot
918
1054
  ? node.resolveParent || node
919
1055
  : node
920
1056
 
921
1057
  let target
922
1058
  let canPlace = null
923
- let warnPeer = false
924
1059
  for (let check = start; check; check = check.resolveParent) {
925
1060
  const cp = this[_canPlaceDep](dep, check, edge, peerEntryEdge)
926
1061
 
@@ -928,17 +1063,8 @@ This is a one-time fix-up, please be patient...
928
1063
  if (cp !== CONFLICT) {
929
1064
  canPlace = cp
930
1065
  target = check
931
- } else {
932
- if (check === start) {
933
- // if it's a peer dep, and the first place we're putting it conflicts
934
- // because the node has a direct dependency on the pkg in question,
935
- // then we treat that as an override when --force is applied, and
936
- // just warn about it.
937
- const checkEdge = check.edgesOut.get(edge.name)
938
- warnPeer = check === start && edge.peer && checkEdge
939
- }
1066
+ } else
940
1067
  break
941
- }
942
1068
 
943
1069
  // nest packages like npm v1 and v2
944
1070
  // very disk-inefficient
@@ -951,38 +1077,16 @@ This is a one-time fix-up, please be patient...
951
1077
  break
952
1078
  }
953
1079
 
954
- if (!target) {
955
- const curNode = node.resolve(edge.name)
956
- const pc = this[_peerConflict] || { peer: null, current: null }
957
- // we'll only get one of these
958
- const current = curNode ? curNode.explain() : pc.current
959
- const peerConflict = pc.peer
960
- const expl = {
961
- code: 'ERESOLVE',
962
- dep: dep.explain(edge),
963
- current,
964
- peerConflict,
965
- fixWithForce: edge.peer && !!warnPeer,
966
- type: edge.type,
967
- isPeer: edge.peer,
968
- }
969
- const override = this[_force] || !this[_strictPeerDeps]
970
-
971
- if (override && expl.fixWithForce) {
972
- this.log.warn('ERESOLVE', 'overriding peer dependency', expl)
973
- return []
974
- } else {
975
- const er = new Error('unable to resolve dependency tree')
976
- throw Object.assign(er, expl)
977
- }
978
- }
1080
+ if (!target)
1081
+ this[_failPeerConflict](edge)
979
1082
 
980
1083
  this.log.silly(
981
1084
  'placeDep',
982
1085
  target.location || 'ROOT',
983
- `${edge.name}@${edge.spec}`,
984
- canPlace,
985
- `for: ${node.package._id || node.location}`
1086
+ `${dep.name}@${dep.version}`,
1087
+ canPlace.description || /* istanbul ignore next */ canPlace,
1088
+ `for: ${node.package._id || node.location}`,
1089
+ `want: ${edge.spec || '*'}`
986
1090
  )
987
1091
 
988
1092
  // it worked, so we clearly have no peer conflicts at this point.
@@ -1034,10 +1138,12 @@ This is a one-time fix-up, please be patient...
1034
1138
  edge.to.parent = null
1035
1139
 
1036
1140
  // visit any dependents who are upset by this change
1037
- for (const edge of dep.edgesIn) {
1038
- if (!edge.valid) {
1039
- this.addTracker('idealTree', edge.from.name, edge.from.location)
1040
- this[_depsQueue].push(edge.from)
1141
+ // if it's an angry overridden peer edge, however, make sure we
1142
+ // skip over it!
1143
+ for (const edgeIn of dep.edgesIn) {
1144
+ if (edgeIn !== edge && !edgeIn.valid && !this[_depsSeen].has(edge.from)) {
1145
+ this.addTracker('idealTree', edgeIn.from.name, edgeIn.from.location)
1146
+ this[_depsQueue].push(edgeIn.from)
1041
1147
  }
1042
1148
  }
1043
1149
 
@@ -1073,28 +1179,30 @@ This is a one-time fix-up, please be patient...
1073
1179
  // also place its unmet or invalid peer deps at this location
1074
1180
  // note that dep has now been removed from the virtualRoot set
1075
1181
  // by virtue of being placed in the target's node_modules.
1076
- if (virtualRoot) {
1077
- const peers = []
1078
- // double loop so that we don't yank things out and then fail to find
1079
- // them in the virtualRoot's children.
1080
- for (const peerEdge of dep.edgesOut.values()) {
1081
- // XXX needs some rework
1082
- if (peerEdge.peer && !peerEdge.valid) {
1083
- const peer = virtualRoot.children.get(peerEdge.name) /* istanbul ignore next - should be impossible */ ||
1084
- peerEdge.to
1085
- /* istanbul ignore else - should be impossible */
1086
- if (peer)
1087
- peers.push([peer, peerEdge])
1088
- }
1089
- }
1182
+ const peers = []
1183
+ // double loop so that we don't yank things out and then fail to find
1184
+ // them in the virtualRoot's children.
1185
+ for (const peerEdge of dep.edgesOut.values()) {
1186
+ if (!peerEdge.peer || peerEdge.valid)
1187
+ continue
1188
+ const peer = virtualRoot.children.get(peerEdge.name)
1189
+ // since we re-use virtualRoots, it's possible that the node was
1190
+ // already placed somewhere in the tree, and thus plucked off the
1191
+ // virtual root. however, in that case, it should have been no
1192
+ // longer a missing/invalid peer dep, so something is messed up.
1193
+ if (peer)
1194
+ peers.push([peer, peerEdge])
1195
+ }
1090
1196
 
1091
- for (const [peer, peerEdge] of peers) {
1092
- const peerPlaced = this[_placeDep](
1093
- peer, dep, peerEdge, peerEntryEdge || edge)
1094
- placed.push(...peerPlaced)
1095
- }
1197
+ for (const [peer, peerEdge] of peers) {
1198
+ const peerPlaced = this[_placeDep](
1199
+ peer, dep, peerEdge, peerEntryEdge || edge)
1200
+ placed.push(...peerPlaced)
1096
1201
  }
1097
1202
 
1203
+ // we're done with this now, clean it up.
1204
+ this[_virtualRoots].delete(virtualRoot.sourceReference)
1205
+
1098
1206
  return placed
1099
1207
  }
1100
1208
 
@@ -1138,43 +1246,41 @@ This is a one-time fix-up, please be patient...
1138
1246
  // When we check peers, we pass along the peerEntryEdge to track the
1139
1247
  // original edge that caused us to load the family of peer dependencies.
1140
1248
  [_canPlaceDep] (dep, target, edge, peerEntryEdge = null) {
1141
- // peer deps of root deps are effectively root deps
1142
- const isRootDep = target.isRoot && (
1143
- // a direct dependency from the root node
1144
- edge.from === target ||
1145
- // a member of the peer set of a direct root dependency
1146
- peerEntryEdge && peerEntryEdge.from === target
1147
- )
1148
-
1149
1249
  const entryEdge = peerEntryEdge || edge
1250
+ const source = this[_peerSetSource].get(dep)
1251
+ const virtualRoot = dep.parent
1252
+ const vrEdge = virtualRoot.edgesOut.get(edge.name)
1150
1253
 
1151
- // has child by that name already
1152
- if (target.children.has(dep.name)) {
1153
- const current = target.children.get(dep.name)
1154
- // if the integrities match, then it's literally the same exact bytes,
1155
- // even if it came from somewhere else.
1156
- if (dep.integrity && dep.integrity === current.integrity)
1157
- return KEEP
1254
+ const isSource = target === source
1255
+ const { isRoot, isWorkspace } = source || {}
1256
+ const isMine = isRoot || isWorkspace
1158
1257
 
1159
- // we can always place the root's deps in the root nm folder
1160
- if (isRootDep)
1161
- return this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge)
1258
+ if (target.children.has(edge.name)) {
1259
+ const current = target.children.get(edge.name)
1162
1260
 
1163
- // if the version is greater, try to use the new one
1164
- const curVer = current.version
1165
- const newVer = dep.version
1166
- // always try to replace if the version is greater
1261
+ // same thing = keep
1262
+ if (dep.matches(current))
1263
+ return KEEP
1264
+
1265
+ const { version: curVer } = current
1266
+ const { version: newVer } = dep
1167
1267
  const tryReplace = curVer && newVer && semver.gte(newVer, curVer)
1168
- if (tryReplace && current.canReplaceWith(dep))
1169
- return this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge)
1268
+ if (tryReplace && dep.canReplace(current)) {
1269
+ const res = this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge)
1270
+ /* istanbul ignore else - It's extremely rare that a replaceable
1271
+ * node would be a conflict, if the current one wasn't a conflict,
1272
+ * but it is theoretically possible if peer deps are pinned. In
1273
+ * that case we treat it like any other conflict, and keep trying */
1274
+ if (res !== CONFLICT)
1275
+ return res
1276
+ }
1170
1277
 
1171
- // ok, see if the current one satisfies the edge we're working on then
1278
+ // ok, can't replace the current with new one, but maybe current is ok?
1172
1279
  if (edge.satisfiedBy(current))
1173
1280
  return KEEP
1174
1281
 
1175
- // last try, if we prefer deduplication over novelty, check to see if
1176
- // this (older) dep can satisfy the needs of the less nested instance
1177
- if (this[_preferDedupe] && current.canReplaceWith(dep)) {
1282
+ // if we prefer deduping, then try replacing newer with older
1283
+ if (this[_preferDedupe] && !tryReplace && dep.canReplace(current)) {
1178
1284
  const res = this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge)
1179
1285
  /* istanbul ignore else - It's extremely rare that a replaceable
1180
1286
  * node would be a conflict, if the current one wasn't a conflict,
@@ -1184,50 +1290,90 @@ This is a one-time fix-up, please be patient...
1184
1290
  return res
1185
1291
  }
1186
1292
 
1187
- // if this is a peer dep, AND target is the resolveParent of the edge,
1188
- // then this is the only place it can go. If the current node is not
1189
- // a non-peer dependency of this specific target, then it can replace
1190
- // and dupe it deeper in the tree. If the current node is a peer dep
1191
- // in a set that is a non-peer dep of a deeper target, then replace
1192
- // the whole peer set and the module bringing it in, and add the
1193
- // dependent to the queue for re-evaluation.
1194
- if (edge.peer && target === edge.from.resolveParent && !peerEntryEdge) {
1293
+ // check for conflict override cases.
1294
+ // first: is this the only place this thing can go? If the target is
1295
+ // the source, then one of these things are true.
1296
+ //
1297
+ // 1. the conflicted dep was deduped up to here from a lower dependency
1298
+ // w -> (x,y)
1299
+ // x -> (z)
1300
+ // y -> PEER(p@1)
1301
+ // z -> (q)
1302
+ // q -> (p@2)
1303
+ //
1304
+ // When building, let's say that x is fully placed, with all of its
1305
+ // deps, and we're _adding_ y. Since the peer on p@1 was not initially
1306
+ // present, it's been deduped up to w, and now needs to be pushed out.
1307
+ // Replace it, and potentially also replace its peer set (though that'll
1308
+ // be accomplished by making the same determination when we call
1309
+ // _canPlacePeers)
1310
+ //
1311
+ // 2. the dep we're TRYING to place here ought to be overridden by the
1312
+ // one that's here now, because current is (a) a direct dep of the
1313
+ // source, or (b) an already-placed peer in a conflicted peer set, or
1314
+ // (c) an already-placed peer in a different peer set at the same level.
1315
+ // If strict or ours, conflict. Otherwise, keep.
1316
+ if (isSource) {
1317
+ // check to see if the current module could go deeper in the tree
1195
1318
  const peerSet = getPeerSet(current)
1196
- for (const p of peerSet) {
1319
+ let canReplace = true
1320
+ OUTER: for (const p of peerSet) {
1197
1321
  // if any have a non-peer dep from the target, or a peer dep if
1198
- // the target is root, then we can't safely replace.
1322
+ // the target is root, then cannot safely replace and dupe deeper.
1199
1323
  for (const edge of p.edgesIn) {
1200
- if (edge.peer) {
1201
- // root deps take precedence always.
1202
- // in case this is an edge coming from a link it's also
1203
- // going to conflict since deps are effectively relative
1204
- // to its link node parent
1205
- if (edge.from.isTop)
1206
- return CONFLICT
1207
-
1208
- // other peer deps on this node are irrelevant though.
1324
+ if (peerSet.has(edge.from))
1209
1325
  continue
1326
+
1327
+ // only respect valid edges, however, since we're likely trying
1328
+ // to fix the very one that's currently broken!
1329
+ if (edge.from === target && edge.valid) {
1330
+ canReplace = false
1331
+ break OUTER
1210
1332
  }
1211
- // note that we MAY resolve this conflict by using the target's
1212
- // conflicting dep on the peer, if --force is set.
1213
- if (edge.from === target)
1214
- return CONFLICT
1215
1333
  }
1216
1334
  }
1217
- // all peers could be nested deeper in the tree, so replace
1218
- // adding to the queue will happen later when we scan dep's edgesIn
1219
- return REPLACE
1335
+ if (canReplace) {
1336
+ const ret = this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge)
1337
+ /* istanbul ignore else - extremely rare that the peer set would
1338
+ * conflict if we can replace the node in question, but theoretically
1339
+ * possible, if peer deps are pinned aggressively. */
1340
+ if (ret !== CONFLICT)
1341
+ return ret
1342
+ }
1343
+
1344
+ // so it's not a deeper dep that's been deduped. That means that the
1345
+ // only way it could have ended up here is if it's a conflicted peer.
1346
+ /* istanbul ignore else - would have already crashed if not forced,
1347
+ * and either mine or strict, when creating the peerSet. Keeping this
1348
+ * check so that we're not only relying on action at a distance. */
1349
+ if (!this[_strictPeerDeps] && !isMine || this[_force]) {
1350
+ this[_warnPeerConflict](edge, dep)
1351
+ return KEEP
1352
+ }
1353
+ }
1354
+
1355
+ if (vrEdge && vrEdge.satisfiedBy(current)) {
1356
+ /* istanbul ignore else - If the virtual root was satisfied, in
1357
+ * such a way that it was an override, and it's NOT forced, and is
1358
+ * ours, or is in strict mode, then it would have crashed during
1359
+ * the creation of the peerSet. Nevertheless, this is a good check
1360
+ * to ensure we're not warning when we should be conflicting. */
1361
+ if (this[_force] || !isMine && !this[_strictPeerDeps]) {
1362
+ this[_warnPeerConflict](edge)
1363
+ return KEEP
1364
+ }
1220
1365
  }
1221
1366
 
1222
- // no agreement could be reached :(
1367
+ // no justification for overriding, and no agreement possible.
1223
1368
  return CONFLICT
1224
1369
  }
1225
1370
 
1226
- // check to see if the target DOESN'T have a child by that name,
1227
- // but DOES have a conflicting dependency of its own. no need to check
1228
- // if this is the edge we're already looking to resolve!
1371
+ // no existing node at this location!
1372
+ // check to see if the target doesn't have a child by that name,
1373
+ // but WANTS one, and won't be happy with this one. if this is the
1374
+ // edge we're looking to resolve, then not relevant, of course.
1229
1375
  if (target !== entryEdge.from && target.edgesOut.has(dep.name)) {
1230
- const edge = target.edgesOut.get(dep.name)
1376
+ const targetEdge = target.edgesOut.get(dep.name)
1231
1377
  // It might be that the dep would not be valid here, BUT some other
1232
1378
  // version would. Could to try to resolve that, but that makes this no
1233
1379
  // longer a pure synchronous function. ugh.
@@ -1240,15 +1386,28 @@ This is a one-time fix-up, please be patient...
1240
1386
  // a specific name, however, or if a dep makes an incompatible change
1241
1387
  // to its peer dep in a non-semver-major version bump, or if the parent
1242
1388
  // is unbounded in its dependency list.
1243
- if (!edge.satisfiedBy(dep))
1389
+ if (!targetEdge.satisfiedBy(dep)) {
1390
+ if (isSource) {
1391
+ // conflicted peer dep. accept what's there, if overriding
1392
+ /* istanbul ignore else - If it's the source, and the source's edge
1393
+ * is not valid, then either we crashed when creating the peer set,
1394
+ * or it's forced, or it's not ours and not strict. Keep this check
1395
+ * to avoid relying on action at a distance, however. */
1396
+ if (this[_force] || !isMine && !this[_strictPeerDeps]) {
1397
+ this[_warnPeerConflict](edge)
1398
+ return KEEP
1399
+ }
1400
+ }
1401
+
1244
1402
  return CONFLICT
1403
+ }
1245
1404
  }
1246
1405
 
1247
- // check to see what the name resolves to here, and who depends on it
1248
- // and if they'd be ok with the new dep being there instead. we know
1249
- // at this point that it's not the target's direct child node. this is
1250
- // only a check we do when deduping. if it's a direct dep of the target,
1251
- // then we just make the invalid edge and resolve it later.
1406
+ // check to see what that name resolves to here, and who may depend on
1407
+ // being able to reach it by crawling up past this parent. we know
1408
+ // at this point that it's not the target's direct child node. if it's
1409
+ // a direct dep of the target, we just make the invalid edge and
1410
+ // resolve it later.
1252
1411
  const current = target !== entryEdge.from && target.resolve(dep.name)
1253
1412
  if (current) {
1254
1413
  for (const edge of current.edgesIn.values()) {
@@ -1259,6 +1418,7 @@ This is a one-time fix-up, please be patient...
1259
1418
  }
1260
1419
  }
1261
1420
 
1421
+ // no objections! ok to place here
1262
1422
  return this[_canPlacePeers](dep, target, edge, OK, peerEntryEdge)
1263
1423
  }
1264
1424
 
@@ -1271,25 +1431,28 @@ This is a one-time fix-up, please be patient...
1271
1431
  return ret
1272
1432
 
1273
1433
  for (const peer of dep.parent.children.values()) {
1274
- if (peer !== dep) {
1275
- const peerEdge = dep.edgesOut.get(peer.name) ||
1276
- [...peer.edgesIn].find(e => e.peer)
1277
- /* istanbul ignore else - pretty sure this is impossible, but just
1278
- being cautious */
1279
- if (peerEdge) {
1280
- const canPlacePeer = this[_canPlaceDep](peer, target, peerEdge, edge)
1281
- if (canPlacePeer === CONFLICT) {
1282
- const current = target.resolve(peer.name)
1283
- this[_peerConflict] = {
1284
- peer: peer.explain(peerEdge),
1285
- current: current && current.explain(),
1286
- }
1287
- return CONFLICT
1288
- }
1289
- }
1434
+ if (peer === dep)
1435
+ continue
1436
+
1437
+ const peerEdge = dep.edgesOut.get(peer.name) ||
1438
+ [...peer.edgesIn].find(e => e.peer)
1439
+
1440
+ /* istanbul ignore if - pretty sure this is impossible, but just
1441
+ being cautious */
1442
+ if (!peerEdge)
1443
+ continue
1444
+
1445
+ const canPlacePeer = this[_canPlaceDep](peer, target, peerEdge, edge)
1446
+ if (canPlacePeer !== CONFLICT)
1447
+ continue
1448
+
1449
+ const current = target.resolve(peer.name)
1450
+ this[_peerConflict] = {
1451
+ peer: peer.explain(peerEdge),
1452
+ current: current && current.explain(),
1290
1453
  }
1454
+ return CONFLICT
1291
1455
  }
1292
-
1293
1456
  return ret
1294
1457
  }
1295
1458
 
@@ -1362,7 +1525,7 @@ This is a one-time fix-up, please be patient...
1362
1525
  // nothing to prune, because we built it from scratch. if we didn't
1363
1526
  // add or remove anything, then also nothing to do.
1364
1527
  if (metaFromDisk && mutateTree)
1365
- this[_resetDepFlags]()
1528
+ resetDepFlags(this.idealTree)
1366
1529
 
1367
1530
  // update all the dev/optional/etc flags in the tree
1368
1531
  // either we started with a fresh tree, or we
@@ -1397,22 +1560,6 @@ This is a one-time fix-up, please be patient...
1397
1560
  node.parent = null
1398
1561
  }
1399
1562
 
1400
- // we'll need to actually do a walk from the root, because you can have
1401
- // a cycle of deps that all depend on each other, but no path from root.
1402
- // Also, since the ideal tree is loaded from the shrinkwrap, it had
1403
- // extraneous flags set false that might now be actually extraneous, and
1404
- // dev/optional flags that are also now incorrect. This method sets
1405
- // all flags to true, so we can find the set that is actually extraneous.
1406
- [_resetDepFlags] () {
1407
- for (const node of this.idealTree.inventory.values()) {
1408
- node.extraneous = true
1409
- node.dev = true
1410
- node.devOptional = true
1411
- node.peer = true
1412
- node.optional = true
1413
- }
1414
- }
1415
-
1416
1563
  [_pruneFailedOptional] () {
1417
1564
  for (const node of this[_loadFailures]) {
1418
1565
  if (!node.optional)
package/lib/edge.js CHANGED
@@ -12,6 +12,8 @@ const _name = Symbol('_name')
12
12
  const _error = Symbol('_error')
13
13
  const _loadError = Symbol('_loadError')
14
14
  const _setFrom = Symbol('_setFrom')
15
+ const _explain = Symbol('_explain')
16
+ const _explanation = Symbol('_explanation')
15
17
 
16
18
  const types = new Set([
17
19
  'prod',
@@ -60,6 +62,25 @@ class Edge {
60
62
  return depValid(node, this.spec, this.accept, this.from)
61
63
  }
62
64
 
65
+ explain (seen = []) {
66
+ if (this[_explanation])
67
+ return this[_explanation]
68
+
69
+ return this[_explanation] = this[_explain](seen)
70
+ }
71
+
72
+ // return the edge data, and an explanation of how that edge came to be here
73
+ [_explain] (seen) {
74
+ const { error, from } = this
75
+ return {
76
+ type: this.type,
77
+ name: this.name,
78
+ spec: this.spec,
79
+ ...(error ? { error } : {}),
80
+ ...(from ? { from: from.explain(null, seen) } : {}),
81
+ }
82
+ }
83
+
63
84
  get workspace () {
64
85
  return this[_type] === 'workspace'
65
86
  }
@@ -125,6 +146,7 @@ class Edge {
125
146
  }
126
147
 
127
148
  reload (hard = false) {
149
+ this[_explanation] = null
128
150
  const newTo = this[_from].resolve(this.name)
129
151
  if (newTo !== this[_to]) {
130
152
  if (this[_to])
@@ -138,6 +160,7 @@ class Edge {
138
160
  }
139
161
 
140
162
  detach () {
163
+ this[_explanation] = null
141
164
  if (this[_to])
142
165
  this[_to].edgesIn.delete(this)
143
166
  this[_from].edgesOut.delete(this.name)
@@ -147,6 +170,7 @@ class Edge {
147
170
  }
148
171
 
149
172
  [_setFrom] (node) {
173
+ this[_explanation] = null
150
174
  this[_from] = node
151
175
  if (node.edgesOut.has(this.name))
152
176
  node.edgesOut.get(this.name).detach()
package/lib/node.js CHANGED
@@ -57,7 +57,6 @@ const _delistFromMeta = Symbol('_delistFromMeta')
57
57
  const _global = Symbol.for('global')
58
58
  const _workspaces = Symbol('_workspaces')
59
59
  const _explain = Symbol('_explain')
60
- const _explainEdge = Symbol('_explainEdge')
61
60
  const _explanation = Symbol('_explanation')
62
61
 
63
62
  const relpath = require('./relpath.js')
@@ -340,9 +339,8 @@ class Node {
340
339
  why.whileInstalling = {
341
340
  name,
342
341
  version,
342
+ path: this.root.sourceReference.path,
343
343
  }
344
- if (edge)
345
- this[_explainEdge](edge, seen)
346
344
  }
347
345
 
348
346
  if (this.sourceReference)
@@ -358,7 +356,7 @@ class Node {
358
356
 
359
357
  why.dependents = []
360
358
  if (edge)
361
- why.dependents.push(this[_explainEdge](edge, seen))
359
+ why.dependents.push(edge.explain(seen))
362
360
  else {
363
361
  // if we have an edge from the root, just show that, and stop there
364
362
  // no need to go deeper, because it doesn't provide much more value.
@@ -376,21 +374,11 @@ class Node {
376
374
  edges.push(edge)
377
375
  }
378
376
  for (const edge of edges)
379
- why.dependents.push(this[_explainEdge](edge, seen))
377
+ why.dependents.push(edge.explain(seen))
380
378
  }
381
379
  return why
382
380
  }
383
381
 
384
- // return the edge data, and an explanation of how that edge came to be here
385
- [_explainEdge] (edge, seen) {
386
- return {
387
- type: edge.type,
388
- spec: edge.spec,
389
- ...(edge.error ? { error: edge.error } : {}),
390
- from: edge.from.explain(null, seen),
391
- }
392
- }
393
-
394
382
  isDescendantOf (node) {
395
383
  for (let p = this; p; p = p.parent) {
396
384
  if (p === node)
@@ -448,6 +436,14 @@ class Node {
448
436
  return !!bundler && bundler !== this.root
449
437
  }
450
438
 
439
+ get isWorkspace () {
440
+ if (this.isRoot)
441
+ return false
442
+ const { root } = this
443
+ const { type, to } = root.edgesOut.get(this.package.name) || {}
444
+ return type === 'workspace' && to && (to.target === this || to === this)
445
+ }
446
+
451
447
  get isRoot () {
452
448
  return this === this.root
453
449
  }
@@ -0,0 +1,15 @@
1
+ // Sometimes we need to actually do a walk from the root, because you can
2
+ // have a cycle of deps that all depend on each other, but no path from root.
3
+ // Also, since the ideal tree is loaded from the shrinkwrap, it had extraneous
4
+ // flags set false that might now be actually extraneous, and dev/optional
5
+ // flags that are also now incorrect. This method sets all flags to true, so
6
+ // we can find the set that is actually extraneous.
7
+ module.exports = tree => {
8
+ for (const node of tree.inventory.values()) {
9
+ node.extraneous = true
10
+ node.dev = true
11
+ node.devOptional = true
12
+ node.peer = true
13
+ node.optional = true
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "0.0.33",
3
+ "version": "1.0.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@npmcli/installed-package-contents": "^1.0.5",
@@ -8,7 +8,7 @@
8
8
  "@npmcli/metavuln-calculator": "^1.0.0",
9
9
  "@npmcli/name-from-folder": "^1.0.1",
10
10
  "@npmcli/node-gyp": "^1.0.0",
11
- "@npmcli/run-script": "^1.7.0",
11
+ "@npmcli/run-script": "^1.7.2",
12
12
  "bin-links": "^2.2.1",
13
13
  "cacache": "^15.0.3",
14
14
  "common-ancestor-path": "^1.0.1",