@npmcli/arborist 2.2.7 → 2.4.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.
@@ -20,13 +20,21 @@ const levelMap = new Map(levels.reduce((set, level, index) => {
20
20
  }, []))
21
21
 
22
22
  const { inspect, format } = require('util')
23
+ const colors = process.stderr.isTTY
24
+ const magenta = colors ? msg => `\x1B[35m${msg}\x1B[39m` : m => m
23
25
  if (loglevel !== 'silent') {
24
26
  process.on('log', (level, ...args) => {
25
27
  if (levelMap.get(level) < levelMap.get(loglevel))
26
28
  return
27
- const pref = `${process.pid} ${level} `
29
+ const pref = `${process.pid} ${magenta(level)} `
28
30
  if (level === 'warn' && args[0] === 'ERESOLVE')
29
- args[2] = inspect(args[2], { depth: 10 })
31
+ args[2] = inspect(args[2], { depth: 10, colors })
32
+ else {
33
+ args = args.map(a => {
34
+ return typeof a === 'string' ? a
35
+ : inspect(a, { depth: 10, colors })
36
+ })
37
+ }
30
38
  const msg = pref + format(...args).trim().split('\n').join(`\n${pref}`)
31
39
  console.error(msg)
32
40
  })
@@ -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 (/^--[^=]+=/.test(arg)) {
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/bin/lib/timers.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const timers = Object.create(null)
2
+ const { format } = require('util')
2
3
 
3
4
  process.on('time', name => {
4
5
  if (timers[name])
@@ -6,17 +7,20 @@ process.on('time', name => {
6
7
  timers[name] = process.hrtime()
7
8
  })
8
9
 
10
+ const dim = process.stderr.isTTY ? msg => `\x1B[2m${msg}\x1B[22m` : m => m
11
+ const red = process.stderr.isTTY ? msg => `\x1B[31m${msg}\x1B[39m` : m => m
9
12
  process.on('timeEnd', name => {
10
13
  if (!timers[name])
11
14
  throw new Error('timer not started! ' + name)
12
15
  const res = process.hrtime(timers[name])
13
16
  delete timers[name]
14
- console.error(`${process.pid} ${name}`, res[0] * 1e3 + res[1] / 1e6)
17
+ const msg = format(`${process.pid} ${name}`, res[0] * 1e3 + res[1] / 1e6)
18
+ console.error(dim(msg))
15
19
  })
16
20
 
17
21
  process.on('exit', () => {
18
22
  for (const name of Object.keys(timers)) {
19
- console.error('Dangling timer: ', name)
23
+ console.error(red('Dangling timer:'), name)
20
24
  process.exitCode = 1
21
25
  }
22
26
  })
package/bin/virtual.js CHANGED
@@ -8,7 +8,8 @@ require('./lib/timers.js')
8
8
  const start = process.hrtime()
9
9
  new Arborist(options).loadVirtual().then(tree => {
10
10
  const end = process.hrtime(start)
11
- print(tree)
11
+ if (!options.quiet)
12
+ print(tree)
12
13
  if (options.save)
13
14
  tree.meta.save()
14
15
  console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
@@ -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.for('explicitRequests')
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,38 +440,49 @@ 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
- if (this[_updateAll] || this[_updateNames].includes(name))
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
- if (options.rm && options.rm.length) {
413
- addRmPkgDeps.rm(tree.package, options.rm)
414
- for (const name of options.rm)
415
- this[_explicitRequests].add(name)
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 (options.add)
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
- if (options.add && options.add.length || options.rm && options.rm.length || this[_global])
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
- process.emit('timeEnd', 'idealTree:userRequests')
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
@@ -438,10 +494,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
438
494
  .then(add => this[_updateFilePath](add))
439
495
  .then(add => this[_followSymlinkPath](add))
440
496
  })).then(add => {
441
- this[_resolvedAdd] = add
497
+ this[_resolvedAdd].push(...add)
442
498
  // now add is a list of spec objects with names.
443
499
  // find a home for each of them!
444
- const tree = this.idealTree.target || this.idealTree
445
500
  addRmPkgDeps.add({
446
501
  pkg: tree.package,
447
502
  add,
@@ -449,8 +504,6 @@ module.exports = cls => class IdealTreeBuilder extends cls {
449
504
  saveType,
450
505
  path: this.path,
451
506
  })
452
- for (const spec of add)
453
- this[_explicitRequests].add(spec.name)
454
507
  })
455
508
  }
456
509
 
@@ -883,6 +936,8 @@ This is a one-time fix-up, please be patient...
883
936
  // create a virtual root node with the same deps as the node that
884
937
  // is requesting this one, so that we can get all the peer deps in
885
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.
886
941
  const parent = parent_ || this[_virtualRoot](edge.from)
887
942
  const realParent = edge.peer ? edge.from.resolveParent : edge.from
888
943
 
@@ -936,11 +991,23 @@ This is a one-time fix-up, please be patient...
936
991
  return this[_virtualRoots].get(node)
937
992
 
938
993
  const vr = new Node({
939
- path: '/virtual-root',
994
+ path: node.realpath,
940
995
  sourceReference: node,
941
996
  legacyPeerDeps: this.legacyPeerDeps,
942
997
  })
943
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
+
944
1011
  this[_virtualRoots].set(node, vr)
945
1012
  return vr
946
1013
  }
@@ -977,7 +1044,7 @@ This is a one-time fix-up, please be patient...
977
1044
  // if it's peerOptional and not explicitly requested.
978
1045
  if (!edge.to) {
979
1046
  return edge.type !== 'peerOptional' ||
980
- this[_explicitRequests].has(edge.name)
1047
+ this[_explicitRequests].has(edge)
981
1048
  }
982
1049
 
983
1050
  // If the edge has an error, there's a problem.
@@ -993,7 +1060,7 @@ This is a one-time fix-up, please be patient...
993
1060
  return true
994
1061
 
995
1062
  // If the user has explicitly asked to install this package, it's a problem.
996
- if (node.isProjectRoot && this[_explicitRequests].has(edge.name))
1063
+ if (node.isProjectRoot && this[_explicitRequests].has(edge))
997
1064
  return true
998
1065
 
999
1066
  // No problems!
@@ -1117,10 +1184,20 @@ This is a one-time fix-up, please be patient...
1117
1184
  continue
1118
1185
 
1119
1186
  // problem
1120
- this[_failPeerConflict](edge)
1187
+ this[_failPeerConflict](edge, parentEdge)
1121
1188
  }
1122
1189
  }
1123
1190
 
1191
+ // There is something present already, and we're not happy about it
1192
+ // See if the thing we WOULD be happy with is also going to satisfy
1193
+ // the other dependents on the current node.
1194
+ const current = edge.to
1195
+ const dep = await this[_nodeFromEdge](edge, null, null, required)
1196
+ if (dep.canReplace(current)) {
1197
+ await this[_nodeFromEdge](edge, node.parent, null, required)
1198
+ continue
1199
+ }
1200
+
1124
1201
  // at this point we know that there is a dep there, and
1125
1202
  // we don't like it. always fail strictly, always allow forcibly or
1126
1203
  // in non-strict mode if it's not our fault. don't warn here, because
@@ -1133,17 +1210,17 @@ This is a one-time fix-up, please be patient...
1133
1210
  continue
1134
1211
 
1135
1212
  // ok, it's the root, or we're in unforced strict mode, so this is bad
1136
- this[_failPeerConflict](edge)
1213
+ this[_failPeerConflict](edge, parentEdge)
1137
1214
  }
1138
1215
  return node
1139
1216
  }
1140
1217
 
1141
- [_failPeerConflict] (edge) {
1142
- const expl = this[_explainPeerConflict](edge)
1218
+ [_failPeerConflict] (edge, currentEdge) {
1219
+ const expl = this[_explainPeerConflict](edge, currentEdge)
1143
1220
  throw Object.assign(new Error('unable to resolve dependency tree'), expl)
1144
1221
  }
1145
1222
 
1146
- [_explainPeerConflict] (edge) {
1223
+ [_explainPeerConflict] (edge, currentEdge) {
1147
1224
  const node = edge.from
1148
1225
  const curNode = node.resolve(edge.name)
1149
1226
  const pc = this[_peerConflict] || { peer: null, current: null }
@@ -1152,6 +1229,10 @@ This is a one-time fix-up, please be patient...
1152
1229
  return {
1153
1230
  code: 'ERESOLVE',
1154
1231
  current,
1232
+ // it SHOULD be impossible to get here without a current node in place,
1233
+ // but this at least gives us something report on when bugs creep into
1234
+ // the tree handling logic.
1235
+ currentEdge: currentEdge ? currentEdge.explain() : null,
1155
1236
  edge: edge.explain(),
1156
1237
  peerConflict,
1157
1238
  strictPeerDeps: this[_strictPeerDeps],
@@ -1176,7 +1257,7 @@ This is a one-time fix-up, please be patient...
1176
1257
  [_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) {
1177
1258
  if (edge.to &&
1178
1259
  !edge.error &&
1179
- !this[_explicitRequests].has(edge.name) &&
1260
+ !this[_explicitRequests].has(edge) &&
1180
1261
  !this[_updateNames].includes(edge.name) &&
1181
1262
  !this[_isVulnerable](edge.to))
1182
1263
  return []
@@ -1466,9 +1547,15 @@ This is a one-time fix-up, please be patient...
1466
1547
  if (target.children.has(edge.name)) {
1467
1548
  const current = target.children.get(edge.name)
1468
1549
 
1469
- // same thing = keep
1470
- if (dep.matches(current))
1471
- return KEEP
1550
+ // same thing = keep, UNLESS the current doesn't satisfy and new
1551
+ // one does satisfy. This can happen if it's a link to a matching target
1552
+ // at a different location, which satisfies a version dep, but not a
1553
+ // file: dep. If neither of them satisfy, then we can replace it,
1554
+ // because presumably it's better for a peer or something.
1555
+ if (dep.matches(current)) {
1556
+ if (current.satisfies(edge) || !dep.satisfies(edge))
1557
+ return KEEP
1558
+ }
1472
1559
 
1473
1560
  const { version: curVer } = current
1474
1561
  const { version: newVer } = dep
@@ -54,7 +54,7 @@ class Arborist extends Base {
54
54
  ...options,
55
55
  path: options.path || '.',
56
56
  cache: options.cache || `${homedir()}/.npm/_cacache`,
57
- packumentCache: new Map(),
57
+ packumentCache: options.packumentCache || new Map(),
58
58
  log: options.log || procLog,
59
59
  }
60
60
  this.cache = resolve(this.options.cache)
@@ -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.parent = null
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
 
@@ -93,7 +93,8 @@ module.exports = cls => class VirtualLoader extends cls {
93
93
  this.virtualTree = root
94
94
  const {links, nodes} = this[resolveNodes](s, root)
95
95
  await this[resolveLinks](links, nodes)
96
- this[assignBundles](nodes)
96
+ if (!(s.originalLockfileVersion >= 2))
97
+ this[assignBundles](nodes)
97
98
  if (this[flagsSuspect])
98
99
  this[reCalcDepFlags](nodes.values())
99
100
  return root
@@ -220,22 +221,24 @@ module.exports = cls => class VirtualLoader extends cls {
220
221
  [assignBundles] (nodes) {
221
222
  for (const [location, node] of nodes) {
222
223
  // Skip assignment of parentage for the root package
223
- if (!location)
224
+ if (!location || node.target && !node.target.location)
224
225
  continue
225
226
  const { name, parent, package: { inBundle }} = node
227
+
226
228
  if (!parent)
227
229
  continue
228
230
 
229
231
  // read inBundle from package because 'package' here is
230
232
  // actually a v2 lockfile metadata entry.
231
- // If the *parent* is also bundled, though, then we assume
232
- // that it's being pulled in just by virtue of that.
233
+ // If the *parent* is also bundled, though, or if the parent has
234
+ // no dependency on it, then we assume that it's being pulled in
235
+ // just by virtue of its parent or a transitive dep being bundled.
233
236
  const { package: ppkg } = parent
234
237
  const { inBundle: parentBundled } = ppkg
235
- if (inBundle && !parentBundled) {
238
+ if (inBundle && !parentBundled && parent.edgesOut.has(node.name)) {
236
239
  if (!ppkg.bundleDependencies)
237
240
  ppkg.bundleDependencies = [name]
238
- else if (!ppkg.bundleDependencies.includes(name))
241
+ else
239
242
  ppkg.bundleDependencies.push(name)
240
243
  }
241
244
  }
@@ -115,10 +115,6 @@ module.exports = cls => class Builder extends cls {
115
115
  await this[_runScripts]('preinstall')
116
116
  if (this[_binLinks] && type !== 'links')
117
117
  await this[_linkAllBins]()
118
- if (!this[_ignoreScripts]) {
119
- await this[_runScripts]('install')
120
- await this[_runScripts]('postinstall')
121
- }
122
118
 
123
119
  // links should also run prepare scripts and only link bins after that
124
120
  if (type === 'links') {
@@ -128,6 +124,11 @@ module.exports = cls => class Builder extends cls {
128
124
  await this[_linkAllBins]()
129
125
  }
130
126
 
127
+ if (!this[_ignoreScripts]) {
128
+ await this[_runScripts]('install')
129
+ await this[_runScripts]('postinstall')
130
+ }
131
+
131
132
  process.emit('timeEnd', `build:${type}`)
132
133
  }
133
134