@npmcli/arborist 6.1.5 → 6.2.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.
@@ -12,6 +12,7 @@ const { readdirScoped } = require('@npmcli/fs')
12
12
  const { lstat, readlink } = require('fs/promises')
13
13
  const { depth } = require('treeverse')
14
14
  const log = require('proc-log')
15
+ const { cleanUrl } = require('npm-registry-fetch')
15
16
 
16
17
  const {
17
18
  OK,
@@ -1210,7 +1211,8 @@ This is a one-time fix-up, please be patient...
1210
1211
  if (this[_manifests].has(spec.raw)) {
1211
1212
  return this[_manifests].get(spec.raw)
1212
1213
  } else {
1213
- log.silly('fetch manifest', spec.raw)
1214
+ const cleanRawSpec = cleanUrl(spec.rawSpec)
1215
+ log.silly('fetch manifest', spec.raw.replace(spec.rawSpec, cleanRawSpec))
1214
1216
  const p = pacote.manifest(spec, options)
1215
1217
  .then(mani => {
1216
1218
  this[_manifests].set(spec.raw, mani)
@@ -1230,6 +1232,7 @@ This is a one-time fix-up, please be patient...
1230
1232
  const isWorkspace = this.idealTree.workspaces && this.idealTree.workspaces.has(spec.name)
1231
1233
 
1232
1234
  // spec is a directory, link it unless installLinks is set or it's a workspace
1235
+ // TODO post arborist refactor, will need to check for installStrategy=linked
1233
1236
  if (spec.type === 'directory' && (isWorkspace || !installLinks)) {
1234
1237
  return this[_linkFromSpec](name, spec, parent, edge)
1235
1238
  }
@@ -42,6 +42,7 @@ const mixins = [
42
42
  require('./load-virtual.js'),
43
43
  require('./rebuild.js'),
44
44
  require('./reify.js'),
45
+ require('./isolated-reifier.js'),
45
46
  ]
46
47
 
47
48
  const _workspacesEnabled = Symbol.for('workspacesEnabled')
@@ -0,0 +1,453 @@
1
+ const _makeIdealGraph = Symbol('makeIdealGraph')
2
+ const _createIsolatedTree = Symbol.for('createIsolatedTree')
3
+ const _createBundledTree = Symbol('createBundledTree')
4
+ const fs = require('fs')
5
+ const pacote = require('pacote')
6
+ const { join } = require('path')
7
+ const { depth } = require('treeverse')
8
+ const crypto = require('crypto')
9
+
10
+ // cache complicated function results
11
+ const memoize = (fn) => {
12
+ const memo = new Map()
13
+ return async function (arg) {
14
+ const key = arg
15
+ if (memo.has(key)) {
16
+ return memo.get(key)
17
+ }
18
+ const result = {}
19
+ memo.set(key, result)
20
+ await fn(result, arg)
21
+ return result
22
+ }
23
+ }
24
+
25
+ module.exports = cls => class IsolatedReifier extends cls {
26
+ /**
27
+ * Create an ideal graph.
28
+ *
29
+ * An implementation of npm RFC-0042
30
+ * https://github.com/npm/rfcs/blob/main/accepted/0042-isolated-mode.md
31
+ *
32
+ * This entire file should be considered technical debt that will be resolved
33
+ * with an Arborist refactor or rewrite. Embedded logic in Nodes and Links,
34
+ * and the incremental state of building trees and reifying contains too many
35
+ * assumptions to do a linked mode properly.
36
+ *
37
+ * Instead, this approach takes a tree built from build-ideal-tree, and
38
+ * returns a new tree-like structure without the embedded logic of Node and
39
+ * Link classes.
40
+ *
41
+ * Since the RFC requires leaving the package-lock in place, this approach
42
+ * temporarily replaces the tree state for a couple of steps of reifying.
43
+ *
44
+ **/
45
+ async [_makeIdealGraph] (options) {
46
+ /* Make sure that the ideal tree is build as the rest of
47
+ * the algorithm depends on it.
48
+ */
49
+ const bitOpt = {
50
+ ...options,
51
+ complete: false,
52
+ }
53
+ await this.buildIdealTree(bitOpt)
54
+ const idealTree = this.idealTree
55
+
56
+ this.rootNode = {}
57
+ const root = this.rootNode
58
+ this.counter = 0
59
+
60
+ // memoize to cache generating proxy Nodes
61
+ this.externalProxyMemo = memoize(this.externalProxy.bind(this))
62
+ this.workspaceProxyMemo = memoize(this.workspaceProxy.bind(this))
63
+
64
+ root.external = []
65
+ root.isProjectRoot = true
66
+ root.localLocation = idealTree.location
67
+ root.localPath = idealTree.path
68
+ root.workspaces = await Promise.all(
69
+ Array.from(idealTree.fsChildren.values(), this.workspaceProxyMemo))
70
+ const processed = new Set()
71
+ const queue = [idealTree, ...idealTree.fsChildren]
72
+ while (queue.length !== 0) {
73
+ const next = queue.pop()
74
+ if (processed.has(next.location)) {
75
+ continue
76
+ }
77
+ processed.add(next.location)
78
+ next.edgesOut.forEach(e => {
79
+ if (!e.to || (next.package.bundleDependencies || next.package.bundledDependencies || []).includes(e.to.name)) {
80
+ return
81
+ }
82
+ queue.push(e.to)
83
+ })
84
+ if (!next.isProjectRoot && !next.isWorkspace) {
85
+ root.external.push(await this.externalProxyMemo(next))
86
+ }
87
+ }
88
+
89
+ await this.assignCommonProperties(idealTree, root)
90
+
91
+ this.idealGraph = root
92
+ }
93
+
94
+ async workspaceProxy (result, node) {
95
+ result.localLocation = node.location
96
+ result.localPath = node.path
97
+ result.isWorkspace = true
98
+ result.resolved = node.resolved
99
+ await this.assignCommonProperties(node, result)
100
+ }
101
+
102
+ async externalProxy (result, node) {
103
+ await this.assignCommonProperties(node, result)
104
+ if (node.hasShrinkwrap) {
105
+ const dir = join(
106
+ node.root.path,
107
+ 'node_modules',
108
+ '.store',
109
+ `${node.name}@${node.version}`
110
+ )
111
+ fs.mkdirSync(dir, { recursive: true })
112
+ // TODO this approach feels wrong
113
+ // and shouldn't be necessary for shrinkwraps
114
+ await pacote.extract(node.resolved, dir, {
115
+ ...this.options,
116
+ resolved: node.resolved,
117
+ integrity: node.integrity,
118
+ })
119
+ const Arborist = this.constructor
120
+ const arb = new Arborist({ ...this.options, path: dir })
121
+ await arb[_makeIdealGraph]({ dev: false })
122
+ this.rootNode.external.push(...arb.idealGraph.external)
123
+ arb.idealGraph.external.forEach(e => {
124
+ e.root = this.rootNode
125
+ e.id = `${node.id}=>${e.id}`
126
+ })
127
+ result.localDependencies = []
128
+ result.externalDependencies = arb.idealGraph.externalDependencies
129
+ result.externalOptionalDependencies = arb.idealGraph.externalOptionalDependencies
130
+ result.dependencies = [
131
+ ...result.externalDependencies,
132
+ ...result.localDependencies,
133
+ ...result.externalOptionalDependencies,
134
+ ]
135
+ }
136
+ result.optional = node.optional
137
+ result.resolved = node.resolved
138
+ result.version = node.version
139
+ }
140
+
141
+ async assignCommonProperties (node, result) {
142
+ function validEdgesOut (node) {
143
+ return [...node.edgesOut.values()].filter(e => e.to && e.to.target && !(node.package.bundledDepenedencies || node.package.bundleDependencies || []).includes(e.to.name))
144
+ }
145
+ const edges = validEdgesOut(node)
146
+ const optionalDeps = edges.filter(e => e.optional).map(e => e.to.target)
147
+ const nonOptionalDeps = edges.filter(e => !e.optional).map(e => e.to.target)
148
+
149
+ result.localDependencies = await Promise.all(nonOptionalDeps.filter(n => n.isWorkspace).map(this.workspaceProxyMemo))
150
+ result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace).map(this.externalProxyMemo))
151
+ result.externalOptionalDependencies = await Promise.all(optionalDeps.map(this.externalProxyMemo))
152
+ result.dependencies = [
153
+ ...result.externalDependencies,
154
+ ...result.localDependencies,
155
+ ...result.externalOptionalDependencies,
156
+ ]
157
+ result.root = this.rootNode
158
+ result.id = this.counter++
159
+ result.name = node.name
160
+ result.package = { ...node.package }
161
+ result.package.bundleDependencies = undefined
162
+ result.hasInstallScript = node.hasInstallScript
163
+ }
164
+
165
+ async [_createBundledTree] () {
166
+ // TODO: make sure that idealTree object exists
167
+ const idealTree = this.idealTree
168
+ // TODO: test workspaces having bundled deps
169
+ const queue = []
170
+
171
+ for (const [, edge] of idealTree.edgesOut) {
172
+ if (edge.to && (idealTree.package.bundleDependencies || idealTree.package.bundledDependencies || []).includes(edge.to.name)) {
173
+ queue.push({ from: idealTree, to: edge.to })
174
+ }
175
+ }
176
+ for (const child of idealTree.fsChildren) {
177
+ for (const [, edge] of child.edgesOut) {
178
+ if (edge.to && (child.package.bundleDependencies || child.package.bundledDependencies || []).includes(edge.to.name)) {
179
+ queue.push({ from: child, to: edge.to })
180
+ }
181
+ }
182
+ }
183
+
184
+ const processed = new Set()
185
+ const nodes = new Map()
186
+ const edges = []
187
+ while (queue.length !== 0) {
188
+ const nextEdge = queue.pop()
189
+ const key = `${nextEdge.from.location}=>${nextEdge.to.location}`
190
+ // should be impossible, unless bundled is duped
191
+ /* istanbul ignore next */
192
+ if (processed.has(key)) {
193
+ continue
194
+ }
195
+ processed.add(key)
196
+ const from = nextEdge.from
197
+ if (!from.isRoot && !from.isWorkspace) {
198
+ nodes.set(from.location, { location: from.location, resolved: from.resolved, name: from.name, optional: from.optional, pkg: { ...from.package, bundleDependencies: undefined } })
199
+ }
200
+ const to = nextEdge.to
201
+ nodes.set(to.location, { location: to.location, resolved: to.resolved, name: to.name, optional: to.optional, pkg: { ...to.package, bundleDependencies: undefined } })
202
+ edges.push({ from: from.isRoot ? 'root' : from.location, to: to.location })
203
+
204
+ to.edgesOut.forEach(e => {
205
+ // an edge out should always have a to
206
+ /* istanbul ignore else */
207
+ if (e.to) {
208
+ queue.push({ from: e.from, to: e.to })
209
+ }
210
+ })
211
+ }
212
+ return { edges, nodes }
213
+ }
214
+
215
+ async [_createIsolatedTree] (idealTree) {
216
+ await this[_makeIdealGraph](this.options)
217
+
218
+ const proxiedIdealTree = this.idealGraph
219
+
220
+ const bundledTree = await this[_createBundledTree]()
221
+
222
+ const treeHash = (startNode) => {
223
+ // generate short hash based on the dependency tree
224
+ // starting at this node
225
+ const deps = []
226
+ const branch = []
227
+ depth({
228
+ tree: startNode,
229
+ getChildren: node => node.dependencies,
230
+ filter: node => node,
231
+ visit: node => {
232
+ branch.push(`${node.name}@${node.version}`)
233
+ deps.push(`${branch.join('->')}::${node.resolved}`)
234
+ },
235
+ leave: () => {
236
+ branch.pop()
237
+ },
238
+ })
239
+ deps.sort()
240
+ return crypto.createHash('shake256', { outputLength: 16 })
241
+ .update(deps.join(','))
242
+ .digest('base64')
243
+ // Node v14 doesn't support base64url
244
+ .replace(/\+/g, '-')
245
+ .replace(/\//g, '_')
246
+ .replace(/=+$/m, '')
247
+ }
248
+
249
+ const getKey = (idealTreeNode) => {
250
+ return `${idealTreeNode.name}@${idealTreeNode.version}-${treeHash(idealTreeNode)}`
251
+ }
252
+
253
+ const root = {
254
+ fsChildren: [],
255
+ integrity: null,
256
+ inventory: new Map(),
257
+ isLink: false,
258
+ isRoot: true,
259
+ binPaths: [],
260
+ edgesIn: new Set(),
261
+ edgesOut: new Map(),
262
+ hasShrinkwrap: false,
263
+ parent: null,
264
+ // TODO: we should probably not reference this.idealTree
265
+ resolved: this.idealTree.resolved,
266
+ isTop: true,
267
+ path: proxiedIdealTree.root.localPath,
268
+ realpath: proxiedIdealTree.root.localPath,
269
+ package: proxiedIdealTree.root.package,
270
+ meta: { loadedFromDisk: false },
271
+ global: false,
272
+ isProjectRoot: true,
273
+ children: [],
274
+ }
275
+ // root.inventory.set('', t)
276
+ // root.meta = this.idealTree.meta
277
+ // TODO We should mock better the inventory object because it is used by audit-report.js ... maybe
278
+ root.inventory.query = () => {
279
+ return []
280
+ }
281
+ const processed = new Set()
282
+ proxiedIdealTree.workspaces.forEach(c => {
283
+ const workspace = {
284
+ edgesIn: new Set(),
285
+ edgesOut: new Map(),
286
+ children: [],
287
+ hasInstallScript: c.hasInstallScript,
288
+ binPaths: [],
289
+ package: c.package,
290
+ location: c.localLocation,
291
+ path: c.localPath,
292
+ realpath: c.localPath,
293
+ resolved: c.resolved,
294
+ }
295
+ root.fsChildren.push(workspace)
296
+ root.inventory.set(workspace.location, workspace)
297
+ })
298
+ const generateChild = (node, location, pkg, inStore) => {
299
+ const newChild = {
300
+ global: false,
301
+ globalTop: false,
302
+ isProjectRoot: false,
303
+ isTop: false,
304
+ location,
305
+ name: node.name,
306
+ optional: node.optional,
307
+ top: { path: proxiedIdealTree.root.localPath },
308
+ children: [],
309
+ edgesIn: new Set(),
310
+ edgesOut: new Map(),
311
+ binPaths: [],
312
+ fsChildren: [],
313
+ /* istanbul ignore next -- emulate Node */
314
+ getBundler () {
315
+ return null
316
+ },
317
+ hasShrinkwrap: false,
318
+ inDepBundle: false,
319
+ integrity: null,
320
+ isLink: false,
321
+ isRoot: false,
322
+ isInStore: inStore,
323
+ path: join(proxiedIdealTree.root.localPath, location),
324
+ realpath: join(proxiedIdealTree.root.localPath, location),
325
+ resolved: node.resolved,
326
+ version: pkg.version,
327
+ package: pkg,
328
+ }
329
+ newChild.target = newChild
330
+ root.children.push(newChild)
331
+ root.inventory.set(newChild.location, newChild)
332
+ }
333
+ proxiedIdealTree.external.forEach(c => {
334
+ const key = getKey(c)
335
+ if (processed.has(key)) {
336
+ return
337
+ }
338
+ processed.add(key)
339
+ const location = join('node_modules', '.store', key, 'node_modules', c.name)
340
+ generateChild(c, location, c.package, true)
341
+ })
342
+ bundledTree.nodes.forEach(node => {
343
+ generateChild(node, node.location, node.pkg, false)
344
+ })
345
+ bundledTree.edges.forEach(e => {
346
+ const from = e.from === 'root' ? root : root.inventory.get(e.from)
347
+ const to = root.inventory.get(e.to)
348
+ // Maybe optional should be propagated from the original edge
349
+ const edge = { optional: false, from, to }
350
+ from.edgesOut.set(to.name, edge)
351
+ to.edgesIn.add(edge)
352
+ })
353
+ const memo = new Set()
354
+
355
+ function processEdges (node, externalEdge) {
356
+ externalEdge = !!externalEdge
357
+ const key = getKey(node)
358
+ if (memo.has(key)) {
359
+ return
360
+ }
361
+ memo.add(key)
362
+
363
+ let from, nmFolder
364
+ if (externalEdge) {
365
+ const fromLocation = join('node_modules', '.store', key, 'node_modules', node.name)
366
+ from = root.children.find(c => c.location === fromLocation)
367
+ nmFolder = join('node_modules', '.store', key, 'node_modules')
368
+ } else {
369
+ from = node.isProjectRoot ? root : root.fsChildren.find(c => c.location === node.localLocation)
370
+ nmFolder = join(node.localLocation, 'node_modules')
371
+ }
372
+
373
+ const processDeps = (dep, optional, external) => {
374
+ optional = !!optional
375
+ external = !!external
376
+
377
+ const location = join(nmFolder, dep.name)
378
+ const binNames = dep.package.bin && Object.keys(dep.package.bin) || []
379
+ const toKey = getKey(dep)
380
+
381
+ let target
382
+ if (external) {
383
+ const toLocation = join('node_modules', '.store', toKey, 'node_modules', dep.name)
384
+ target = root.children.find(c => c.location === toLocation)
385
+ } else {
386
+ target = root.fsChildren.find(c => c.location === dep.localLocation)
387
+ }
388
+ // TODO: we should no-op is an edge has already been created with the same fromKey and toKey
389
+
390
+ binNames.forEach(bn => {
391
+ target.binPaths.push(join(from.realpath, 'node_modules', '.bin', bn))
392
+ })
393
+
394
+ const link = {
395
+ global: false,
396
+ globalTop: false,
397
+ isProjectRoot: false,
398
+ edgesIn: new Set(),
399
+ edgesOut: new Map(),
400
+ binPaths: [],
401
+ isTop: false,
402
+ optional,
403
+ location: location,
404
+ path: join(dep.root.localPath, nmFolder, dep.name),
405
+ realpath: target.path,
406
+ name: toKey,
407
+ resolved: dep.resolved,
408
+ top: { path: dep.root.localPath },
409
+ children: [],
410
+ fsChildren: [],
411
+ isLink: true,
412
+ isStoreLink: true,
413
+ isRoot: false,
414
+ package: { _id: 'abc', bundleDependencies: undefined, deprecated: undefined, bin: target.package.bin, scripts: dep.package.scripts },
415
+ target,
416
+ }
417
+ const newEdge1 = { optional, from, to: link }
418
+ from.edgesOut.set(dep.name, newEdge1)
419
+ link.edgesIn.add(newEdge1)
420
+ const newEdge2 = { optional: false, from: link, to: target }
421
+ link.edgesOut.set(dep.name, newEdge2)
422
+ target.edgesIn.add(newEdge2)
423
+ root.children.push(link)
424
+ }
425
+
426
+ for (const dep of node.localDependencies) {
427
+ processEdges(dep, false)
428
+ // nonOptional, local
429
+ processDeps(dep, false, false)
430
+ }
431
+ for (const dep of node.externalDependencies) {
432
+ processEdges(dep, true)
433
+ // nonOptional, external
434
+ processDeps(dep, false, true)
435
+ }
436
+ for (const dep of node.externalOptionalDependencies) {
437
+ processEdges(dep, true)
438
+ // optional, external
439
+ processDeps(dep, true, true)
440
+ }
441
+ }
442
+
443
+ processEdges(proxiedIdealTree, false)
444
+ for (const node of proxiedIdealTree.workspaces) {
445
+ processEdges(node, false)
446
+ }
447
+ root.children.forEach(c => c.parent = root)
448
+ root.children.forEach(c => c.root = root)
449
+ root.root = root
450
+ root.target = root
451
+ return root
452
+ }
453
+ }
@@ -1,5 +1,6 @@
1
1
  // mixin providing the loadVirtual method
2
2
  const localeCompare = require('@isaacs/string-locale-compare')('en')
3
+ const mapWorkspaces = require('@npmcli/map-workspaces')
3
4
 
4
5
  const { resolve } = require('path')
5
6
 
@@ -21,7 +22,6 @@ const loadRoot = Symbol('loadRoot')
21
22
  const loadNode = Symbol('loadVirtualNode')
22
23
  const loadLink = Symbol('loadVirtualLink')
23
24
  const loadWorkspaces = Symbol.for('loadWorkspaces')
24
- const loadWorkspacesVirtual = Symbol.for('loadWorkspacesVirtual')
25
25
  const flagsSuspect = Symbol.for('flagsSuspect')
26
26
  const reCalcDepFlags = Symbol('reCalcDepFlags')
27
27
  const checkRootEdges = Symbol('checkRootEdges')
@@ -157,7 +157,7 @@ module.exports = cls => class VirtualLoader extends cls {
157
157
  }
158
158
 
159
159
  const lockWS = []
160
- const workspaces = this[loadWorkspacesVirtual]({
160
+ const workspaces = mapWorkspaces.virtual({
161
161
  cwd: this.path,
162
162
  lockfile: s.data,
163
163
  })
@@ -1,33 +1,19 @@
1
1
  const mapWorkspaces = require('@npmcli/map-workspaces')
2
2
 
3
- const _appendWorkspaces = Symbol('appendWorkspaces')
4
3
  // shared ref used by other mixins/Arborist
5
4
  const _loadWorkspaces = Symbol.for('loadWorkspaces')
6
- const _loadWorkspacesVirtual = Symbol.for('loadWorkspacesVirtual')
7
5
 
8
6
  module.exports = cls => class MapWorkspaces extends cls {
9
- [_appendWorkspaces] (node, workspaces) {
10
- if (node && workspaces.size) {
11
- node.workspaces = workspaces
12
- }
13
-
14
- return node
15
- }
16
-
17
7
  async [_loadWorkspaces] (node) {
18
- if (node.workspaces) {
19
- return node
20
- }
21
-
22
8
  const workspaces = await mapWorkspaces({
23
9
  cwd: node.path,
24
10
  pkg: node.package,
25
11
  })
26
12
 
27
- return this[_appendWorkspaces](node, workspaces)
28
- }
13
+ if (node && workspaces.size) {
14
+ node.workspaces = workspaces
15
+ }
29
16
 
30
- [_loadWorkspacesVirtual] (opts) {
31
- return mapWorkspaces.virtual(opts)
17
+ return node
32
18
  }
33
19
  }
@@ -89,6 +89,7 @@ module.exports = cls => class Builder extends cls {
89
89
  const {
90
90
  depNodes,
91
91
  linkNodes,
92
+ storeNodes,
92
93
  } = this[_retrieveNodesByType](nodes)
93
94
 
94
95
  // build regular deps
@@ -99,6 +100,10 @@ module.exports = cls => class Builder extends cls {
99
100
  this[_resetQueues]()
100
101
  await this[_build](linkNodes, { type: 'links' })
101
102
  }
103
+ if (storeNodes.size) {
104
+ this[_resetQueues]()
105
+ await this[_build](storeNodes, { type: 'storelinks' })
106
+ }
102
107
 
103
108
  process.emit('timeEnd', 'build')
104
109
  }
@@ -130,9 +135,12 @@ module.exports = cls => class Builder extends cls {
130
135
  [_retrieveNodesByType] (nodes) {
131
136
  const depNodes = new Set()
132
137
  const linkNodes = new Set()
138
+ const storeNodes = new Set()
133
139
 
134
140
  for (const node of nodes) {
135
- if (node.isLink) {
141
+ if (node.isStoreLink) {
142
+ storeNodes.add(node)
143
+ } else if (node.isLink) {
136
144
  linkNodes.add(node)
137
145
  } else {
138
146
  depNodes.add(node)
@@ -154,6 +162,7 @@ module.exports = cls => class Builder extends cls {
154
162
  return {
155
163
  depNodes,
156
164
  linkNodes,
165
+ storeNodes,
157
166
  }
158
167
  }
159
168
 
@@ -1,5 +1,4 @@
1
1
  // mixin implementing the reify method
2
-
3
2
  const onExit = require('../signal-handling.js')
4
3
  const pacote = require('pacote')
5
4
  const AuditReport = require('../audit-report.js')
@@ -10,8 +9,9 @@ const debug = require('../debug.js')
10
9
  const walkUp = require('walk-up-path')
11
10
  const log = require('proc-log')
12
11
  const hgi = require('hosted-git-info')
12
+ const rpj = require('read-package-json-fast')
13
13
 
14
- const { dirname, resolve, relative } = require('path')
14
+ const { dirname, resolve, relative, join } = require('path')
15
15
  const { depth: dfwalk } = require('treeverse')
16
16
  const {
17
17
  lstat,
@@ -106,6 +106,8 @@ const _resolvedAdd = Symbol.for('resolvedAdd')
106
106
  const _usePackageLock = Symbol.for('usePackageLock')
107
107
  const _formatPackageLock = Symbol.for('formatPackageLock')
108
108
 
109
+ const _createIsolatedTree = Symbol.for('createIsolatedTree')
110
+
109
111
  module.exports = cls => class Reifier extends cls {
110
112
  constructor (options) {
111
113
  super(options)
@@ -138,6 +140,8 @@ module.exports = cls => class Reifier extends cls {
138
140
 
139
141
  // public method
140
142
  async reify (options = {}) {
143
+ const linked = (options.installStrategy || this.options.installStrategy) === 'linked'
144
+
141
145
  if (this[_packageLockOnly] && this[_global]) {
142
146
  const er = new Error('cannot generate lockfile for global packages')
143
147
  er.code = 'ESHRINKWRAPGLOBAL'
@@ -154,8 +158,22 @@ module.exports = cls => class Reifier extends cls {
154
158
  process.emit('time', 'reify')
155
159
  await this[_validatePath]()
156
160
  await this[_loadTrees](options)
161
+
162
+ const oldTree = this.idealTree
163
+ if (linked) {
164
+ // swap out the tree with the isolated tree
165
+ // this is currently technical debt which will be resolved in a refactor
166
+ // of Node/Link trees
167
+ log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.')
168
+ this.idealTree = await this[_createIsolatedTree](this.idealTree)
169
+ }
157
170
  await this[_diffTrees]()
158
171
  await this[_reifyPackages]()
172
+ if (linked) {
173
+ // swap back in the idealTree
174
+ // so that the lockfile is preserved
175
+ this.idealTree = oldTree
176
+ }
159
177
  await this[_saveIdealTree](options)
160
178
  await this[_copyIdealToActual]()
161
179
  // This is a very bad pattern and I can't wait to stop doing it
@@ -634,44 +652,40 @@ module.exports = cls => class Reifier extends cls {
634
652
  }
635
653
 
636
654
  async [_extractOrLink] (node) {
637
- // in normal cases, node.resolved should *always* be set by now.
638
- // however, it is possible when a lockfile is damaged, or very old,
639
- // or in some other race condition bugs in npm v6, that a previously
640
- // bundled dependency will have just a version, but no resolved value,
641
- // and no 'bundled: true' setting.
642
- // Do the best with what we have, or else remove it from the tree
643
- // entirely, since we can't possibly reify it.
644
- let res = null
645
- if (node.resolved) {
646
- const registryResolved = this[_registryResolved](node.resolved)
647
- if (registryResolved) {
648
- res = `${node.name}@${registryResolved}`
649
- }
650
- } else if (node.packageName && node.version) {
651
- res = `${node.packageName}@${node.version}`
652
- }
653
-
654
- // no idea what this thing is. remove it from the tree.
655
- if (!res) {
656
- const warning = 'invalid or damaged lockfile detected\n' +
657
- 'please re-try this operation once it completes\n' +
658
- 'so that the damage can be corrected, or perform\n' +
659
- 'a fresh install with no lockfile if the problem persists.'
660
- log.warn('reify', warning)
661
- log.verbose('reify', 'unrecognized node in tree', node.path)
662
- node.parent = null
663
- node.fsParent = null
664
- this[_addNodeToTrashList](node)
665
- return
666
- }
667
-
668
655
  const nm = resolve(node.parent.path, 'node_modules')
669
656
  await this[_validateNodeModules](nm)
670
657
 
671
- if (node.isLink) {
672
- await rm(node.path, { recursive: true, force: true })
673
- await this[_symlink](node)
674
- } else {
658
+ if (!node.isLink) {
659
+ // in normal cases, node.resolved should *always* be set by now.
660
+ // however, it is possible when a lockfile is damaged, or very old,
661
+ // or in some other race condition bugs in npm v6, that a previously
662
+ // bundled dependency will have just a version, but no resolved value,
663
+ // and no 'bundled: true' setting.
664
+ // Do the best with what we have, or else remove it from the tree
665
+ // entirely, since we can't possibly reify it.
666
+ let res = null
667
+ if (node.resolved) {
668
+ const registryResolved = this[_registryResolved](node.resolved)
669
+ if (registryResolved) {
670
+ res = `${node.name}@${registryResolved}`
671
+ }
672
+ } else if (node.package.name && node.version) {
673
+ res = `${node.package.name}@${node.version}`
674
+ }
675
+
676
+ // no idea what this thing is. remove it from the tree.
677
+ if (!res) {
678
+ const warning = 'invalid or damaged lockfile detected\n' +
679
+ 'please re-try this operation once it completes\n' +
680
+ 'so that the damage can be corrected, or perform\n' +
681
+ 'a fresh install with no lockfile if the problem persists.'
682
+ log.warn('reify', warning)
683
+ log.verbose('reify', 'unrecognized node in tree', node.path)
684
+ node.parent = null
685
+ node.fsParent = null
686
+ this[_addNodeToTrashList](node)
687
+ return
688
+ }
675
689
  await debug(async () => {
676
690
  const st = await lstat(node.path).catch(e => null)
677
691
  if (st && !st.isDirectory()) {
@@ -688,7 +702,17 @@ module.exports = cls => class Reifier extends cls {
688
702
  resolved: node.resolved,
689
703
  integrity: node.integrity,
690
704
  })
705
+ // store nodes don't use Node class so node.package doesn't get updated
706
+ if (node.isInStore) {
707
+ const pkg = await rpj(join(node.path, 'package.json'))
708
+ node.package.scripts = pkg.scripts
709
+ }
710
+ return
691
711
  }
712
+
713
+ // node.isLink
714
+ await rm(node.path, { recursive: true, force: true })
715
+ await this[_symlink](node)
692
716
  }
693
717
 
694
718
  async [_symlink] (node) {
package/lib/link.js CHANGED
@@ -8,7 +8,7 @@ const _delistFromMeta = Symbol.for('_delistFromMeta')
8
8
  const _refreshLocation = Symbol.for('_refreshLocation')
9
9
  class Link extends Node {
10
10
  constructor (options) {
11
- const { root, realpath, target, parent, fsParent } = options
11
+ const { root, realpath, target, parent, fsParent, isStoreLink } = options
12
12
 
13
13
  if (!realpath && !(target && target.path)) {
14
14
  throw new TypeError('must provide realpath for Link node')
@@ -23,6 +23,8 @@ class Link extends Node {
23
23
  : null),
24
24
  })
25
25
 
26
+ this.isStoreLink = isStoreLink || false
27
+
26
28
  if (target) {
27
29
  this.target = target
28
30
  } else if (this.realpath === this.root.path) {
package/lib/node.js CHANGED
@@ -91,6 +91,7 @@ class Node {
91
91
  installLinks = false,
92
92
  legacyPeerDeps = false,
93
93
  linksIn,
94
+ isInStore = false,
94
95
  hasShrinkwrap,
95
96
  overrides,
96
97
  loadOverrides = false,
@@ -113,6 +114,7 @@ class Node {
113
114
  this[_workspaces] = null
114
115
 
115
116
  this.errors = error ? [error] : []
117
+ this.isInStore = isInStore
116
118
 
117
119
  // this will usually be null, except when modeling a
118
120
  // package's dependencies in a virtual root.
@@ -1,4 +1,4 @@
1
- function overrideResolves (resolved, opts = {}) {
1
+ function overrideResolves (resolved, opts) {
2
2
  const { omitLockfileRegistryResolved = false } = opts
3
3
 
4
4
  if (omitLockfileRegistryResolved) {
@@ -50,9 +50,36 @@ class OverrideSet {
50
50
  continue
51
51
  }
52
52
 
53
- if (semver.intersects(edge.spec, rule.keySpec)) {
53
+ // if keySpec is * we found our override
54
+ if (rule.keySpec === '*') {
54
55
  return rule
55
56
  }
57
+
58
+ let spec = npa(`${edge.name}@${edge.spec}`)
59
+ if (spec.type === 'alias') {
60
+ spec = spec.subSpec
61
+ }
62
+
63
+ if (spec.type === 'git') {
64
+ if (spec.gitRange && semver.intersects(spec.gitRange, rule.keySpec)) {
65
+ return rule
66
+ }
67
+
68
+ continue
69
+ }
70
+
71
+ if (spec.type === 'range' || spec.type === 'version') {
72
+ if (semver.intersects(spec.fetchSpec, rule.keySpec)) {
73
+ return rule
74
+ }
75
+
76
+ continue
77
+ }
78
+
79
+ // if we got this far, the spec type is one of tag, directory or file
80
+ // which means we have no real way to make version comparisons, so we
81
+ // just accept the override
82
+ return rule
56
83
  }
57
84
 
58
85
  return this
package/lib/place-dep.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  const localeCompare = require('@isaacs/string-locale-compare')('en')
11
11
  const log = require('proc-log')
12
+ const { cleanUrl } = require('npm-registry-fetch')
12
13
  const deepestNestingTarget = require('./deepest-nesting-target.js')
13
14
  const CanPlaceDep = require('./can-place-dep.js')
14
15
  const {
@@ -187,7 +188,7 @@ class PlaceDep {
187
188
  `${this.dep.name}@${this.dep.version}`,
188
189
  this.canPlace.description,
189
190
  `for: ${this.edge.from.package._id || this.edge.from.location}`,
190
- `want: ${this.edge.spec || '*'}`
191
+ `want: ${cleanUrl(this.edge.spec || '*')}`
191
192
  )
192
193
 
193
194
  const placementType = this.canPlace.canPlace === CONFLICT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "6.1.5",
3
+ "version": "6.2.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -39,16 +39,18 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@npmcli/eslint-config": "^4.0.0",
42
- "@npmcli/template-oss": "4.11.0",
42
+ "@npmcli/template-oss": "4.11.1",
43
43
  "benchmark": "^2.1.4",
44
44
  "chalk": "^4.1.0",
45
- "minify-registry-metadata": "^2.1.0",
45
+ "minify-registry-metadata": "^3.0.0",
46
46
  "nock": "^13.2.0",
47
- "tap": "^16.0.1",
47
+ "tap": "^16.3.2",
48
+ "tar-stream": "^3.0.0",
48
49
  "tcompare": "^5.0.6"
49
50
  },
50
51
  "scripts": {
51
52
  "test": "tap",
53
+ "test-only": "tap --only",
52
54
  "posttest": "node ../.. run lint",
53
55
  "snap": "tap",
54
56
  "postsnap": "npm run lintfix",
@@ -81,7 +83,6 @@
81
83
  "tap": {
82
84
  "color": true,
83
85
  "after": "test/fixtures/cleanup.js",
84
- "coverage-map": "map.js",
85
86
  "test-env": [
86
87
  "NODE_OPTIONS=--no-warnings",
87
88
  "LC_ALL=sk"
@@ -101,7 +102,7 @@
101
102
  },
102
103
  "templateOSS": {
103
104
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
104
- "version": "4.11.0",
105
+ "version": "4.11.1",
105
106
  "content": "../../scripts/template-oss/index.js"
106
107
  }
107
108
  }