@npmcli/arborist 6.1.6 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/arborist/build-ideal-tree.js +1 -0
- package/lib/arborist/index.js +1 -0
- package/lib/arborist/isolated-reifier.js +453 -0
- package/lib/arborist/rebuild.js +10 -1
- package/lib/arborist/reify.js +98 -86
- package/lib/link.js +3 -1
- package/lib/node.js +2 -0
- package/package.json +5 -3
|
@@ -1232,6 +1232,7 @@ This is a one-time fix-up, please be patient...
|
|
|
1232
1232
|
const isWorkspace = this.idealTree.workspaces && this.idealTree.workspaces.has(spec.name)
|
|
1233
1233
|
|
|
1234
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
|
|
1235
1236
|
if (spec.type === 'directory' && (isWorkspace || !installLinks)) {
|
|
1236
1237
|
return this[_linkFromSpec](name, spec, parent, edge)
|
|
1237
1238
|
}
|
package/lib/arborist/index.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/arborist/rebuild.js
CHANGED
|
@@ -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.
|
|
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
|
|
package/lib/arborist/reify.js
CHANGED
|
@@ -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,
|
|
@@ -82,7 +82,6 @@ const _rollbackRetireShallowNodes = Symbol.for('rollbackRetireShallowNodes')
|
|
|
82
82
|
const _rollbackCreateSparseTree = Symbol.for('rollbackCreateSparseTree')
|
|
83
83
|
const _rollbackMoveBackRetiredUnchanged = Symbol.for('rollbackMoveBackRetiredUnchanged')
|
|
84
84
|
const _saveIdealTree = Symbol.for('saveIdealTree')
|
|
85
|
-
const _saveLockFile = Symbol('saveLockFile')
|
|
86
85
|
const _copyIdealToActual = Symbol('copyIdealToActual')
|
|
87
86
|
const _addOmitsToTrashList = Symbol('addOmitsToTrashList')
|
|
88
87
|
const _packageLockOnly = Symbol('packageLockOnly')
|
|
@@ -106,6 +105,8 @@ const _resolvedAdd = Symbol.for('resolvedAdd')
|
|
|
106
105
|
const _usePackageLock = Symbol.for('usePackageLock')
|
|
107
106
|
const _formatPackageLock = Symbol.for('formatPackageLock')
|
|
108
107
|
|
|
108
|
+
const _createIsolatedTree = Symbol.for('createIsolatedTree')
|
|
109
|
+
|
|
109
110
|
module.exports = cls => class Reifier extends cls {
|
|
110
111
|
constructor (options) {
|
|
111
112
|
super(options)
|
|
@@ -138,6 +139,8 @@ module.exports = cls => class Reifier extends cls {
|
|
|
138
139
|
|
|
139
140
|
// public method
|
|
140
141
|
async reify (options = {}) {
|
|
142
|
+
const linked = (options.installStrategy || this.options.installStrategy) === 'linked'
|
|
143
|
+
|
|
141
144
|
if (this[_packageLockOnly] && this[_global]) {
|
|
142
145
|
const er = new Error('cannot generate lockfile for global packages')
|
|
143
146
|
er.code = 'ESHRINKWRAPGLOBAL'
|
|
@@ -154,8 +157,22 @@ module.exports = cls => class Reifier extends cls {
|
|
|
154
157
|
process.emit('time', 'reify')
|
|
155
158
|
await this[_validatePath]()
|
|
156
159
|
await this[_loadTrees](options)
|
|
160
|
+
|
|
161
|
+
const oldTree = this.idealTree
|
|
162
|
+
if (linked) {
|
|
163
|
+
// swap out the tree with the isolated tree
|
|
164
|
+
// this is currently technical debt which will be resolved in a refactor
|
|
165
|
+
// of Node/Link trees
|
|
166
|
+
log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.')
|
|
167
|
+
this.idealTree = await this[_createIsolatedTree](this.idealTree)
|
|
168
|
+
}
|
|
157
169
|
await this[_diffTrees]()
|
|
158
170
|
await this[_reifyPackages]()
|
|
171
|
+
if (linked) {
|
|
172
|
+
// swap back in the idealTree
|
|
173
|
+
// so that the lockfile is preserved
|
|
174
|
+
this.idealTree = oldTree
|
|
175
|
+
}
|
|
159
176
|
await this[_saveIdealTree](options)
|
|
160
177
|
await this[_copyIdealToActual]()
|
|
161
178
|
// This is a very bad pattern and I can't wait to stop doing it
|
|
@@ -634,44 +651,40 @@ module.exports = cls => class Reifier extends cls {
|
|
|
634
651
|
}
|
|
635
652
|
|
|
636
653
|
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
654
|
const nm = resolve(node.parent.path, 'node_modules')
|
|
669
655
|
await this[_validateNodeModules](nm)
|
|
670
656
|
|
|
671
|
-
if (node.isLink) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
657
|
+
if (!node.isLink) {
|
|
658
|
+
// in normal cases, node.resolved should *always* be set by now.
|
|
659
|
+
// however, it is possible when a lockfile is damaged, or very old,
|
|
660
|
+
// or in some other race condition bugs in npm v6, that a previously
|
|
661
|
+
// bundled dependency will have just a version, but no resolved value,
|
|
662
|
+
// and no 'bundled: true' setting.
|
|
663
|
+
// Do the best with what we have, or else remove it from the tree
|
|
664
|
+
// entirely, since we can't possibly reify it.
|
|
665
|
+
let res = null
|
|
666
|
+
if (node.resolved) {
|
|
667
|
+
const registryResolved = this[_registryResolved](node.resolved)
|
|
668
|
+
if (registryResolved) {
|
|
669
|
+
res = `${node.name}@${registryResolved}`
|
|
670
|
+
}
|
|
671
|
+
} else if (node.package.name && node.version) {
|
|
672
|
+
res = `${node.package.name}@${node.version}`
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// no idea what this thing is. remove it from the tree.
|
|
676
|
+
if (!res) {
|
|
677
|
+
const warning = 'invalid or damaged lockfile detected\n' +
|
|
678
|
+
'please re-try this operation once it completes\n' +
|
|
679
|
+
'so that the damage can be corrected, or perform\n' +
|
|
680
|
+
'a fresh install with no lockfile if the problem persists.'
|
|
681
|
+
log.warn('reify', warning)
|
|
682
|
+
log.verbose('reify', 'unrecognized node in tree', node.path)
|
|
683
|
+
node.parent = null
|
|
684
|
+
node.fsParent = null
|
|
685
|
+
this[_addNodeToTrashList](node)
|
|
686
|
+
return
|
|
687
|
+
}
|
|
675
688
|
await debug(async () => {
|
|
676
689
|
const st = await lstat(node.path).catch(e => null)
|
|
677
690
|
if (st && !st.isDirectory()) {
|
|
@@ -688,7 +701,17 @@ module.exports = cls => class Reifier extends cls {
|
|
|
688
701
|
resolved: node.resolved,
|
|
689
702
|
integrity: node.integrity,
|
|
690
703
|
})
|
|
704
|
+
// store nodes don't use Node class so node.package doesn't get updated
|
|
705
|
+
if (node.isInStore) {
|
|
706
|
+
const pkg = await rpj(join(node.path, 'package.json'))
|
|
707
|
+
node.package.scripts = pkg.scripts
|
|
708
|
+
}
|
|
709
|
+
return
|
|
691
710
|
}
|
|
711
|
+
|
|
712
|
+
// node.isLink
|
|
713
|
+
await rm(node.path, { recursive: true, force: true })
|
|
714
|
+
await this[_symlink](node)
|
|
692
715
|
}
|
|
693
716
|
|
|
694
717
|
async [_symlink] (node) {
|
|
@@ -1380,64 +1403,53 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1380
1403
|
}
|
|
1381
1404
|
}
|
|
1382
1405
|
|
|
1383
|
-
// preserve indentation, if possible
|
|
1384
|
-
const {
|
|
1385
|
-
[Symbol.for('indent')]: indent,
|
|
1386
|
-
} = this.idealTree.package
|
|
1387
|
-
const format = indent === undefined ? ' ' : indent
|
|
1388
|
-
|
|
1389
|
-
const saveOpt = {
|
|
1390
|
-
format: (this[_formatPackageLock] && format) ? format
|
|
1391
|
-
: this[_formatPackageLock],
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
const promises = [this[_saveLockFile](saveOpt)]
|
|
1395
|
-
|
|
1396
|
-
const updatePackageJson = async (tree) => {
|
|
1397
|
-
const pkgJson = await PackageJson.load(tree.path)
|
|
1398
|
-
.catch(() => new PackageJson(tree.path))
|
|
1399
|
-
const {
|
|
1400
|
-
dependencies = {},
|
|
1401
|
-
devDependencies = {},
|
|
1402
|
-
optionalDependencies = {},
|
|
1403
|
-
peerDependencies = {},
|
|
1404
|
-
// bundleDependencies is not required by PackageJson like the other fields here
|
|
1405
|
-
// PackageJson also doesn't omit an empty array for this field so defaulting this
|
|
1406
|
-
// to an empty array would add that field to every package.json file.
|
|
1407
|
-
bundleDependencies,
|
|
1408
|
-
} = tree.package
|
|
1409
|
-
|
|
1410
|
-
pkgJson.update({
|
|
1411
|
-
dependencies,
|
|
1412
|
-
devDependencies,
|
|
1413
|
-
optionalDependencies,
|
|
1414
|
-
peerDependencies,
|
|
1415
|
-
bundleDependencies,
|
|
1416
|
-
})
|
|
1417
|
-
await pkgJson.save()
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
1406
|
if (save) {
|
|
1421
1407
|
for (const tree of updatedTrees) {
|
|
1422
1408
|
// refresh the edges so they have the correct specs
|
|
1423
1409
|
tree.package = tree.package
|
|
1424
|
-
|
|
1410
|
+
const pkgJson = await PackageJson.load(tree.path)
|
|
1411
|
+
.catch(() => new PackageJson(tree.path))
|
|
1412
|
+
const {
|
|
1413
|
+
dependencies = {},
|
|
1414
|
+
devDependencies = {},
|
|
1415
|
+
optionalDependencies = {},
|
|
1416
|
+
peerDependencies = {},
|
|
1417
|
+
// bundleDependencies is not required by PackageJson like the other
|
|
1418
|
+
// fields here PackageJson also doesn't omit an empty array for this
|
|
1419
|
+
// field so defaulting this to an empty array would add that field to
|
|
1420
|
+
// every package.json file.
|
|
1421
|
+
bundleDependencies,
|
|
1422
|
+
} = tree.package
|
|
1423
|
+
|
|
1424
|
+
pkgJson.update({
|
|
1425
|
+
dependencies,
|
|
1426
|
+
devDependencies,
|
|
1427
|
+
optionalDependencies,
|
|
1428
|
+
peerDependencies,
|
|
1429
|
+
bundleDependencies,
|
|
1430
|
+
})
|
|
1431
|
+
await pkgJson.save()
|
|
1425
1432
|
}
|
|
1426
1433
|
}
|
|
1427
1434
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1435
|
+
// before now edge specs could be changing, affecting the `requires` field
|
|
1436
|
+
// in the package lock, so we hold off saving to the very last action
|
|
1437
|
+
if (this[_usePackageLock]) {
|
|
1438
|
+
// preserve indentation, if possible
|
|
1439
|
+
let format = this.idealTree.package[Symbol.for('indent')]
|
|
1440
|
+
if (format === undefined) {
|
|
1441
|
+
format = ' '
|
|
1442
|
+
}
|
|
1432
1443
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1444
|
+
// TODO this ignores options.save
|
|
1445
|
+
await this.idealTree.meta.save({
|
|
1446
|
+
format: (this[_formatPackageLock] && format) ? format
|
|
1447
|
+
: this[_formatPackageLock],
|
|
1448
|
+
})
|
|
1436
1449
|
}
|
|
1437
1450
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
return meta.save(saveOpt)
|
|
1451
|
+
process.emit('timeEnd', 'reify:save')
|
|
1452
|
+
return true
|
|
1441
1453
|
}
|
|
1442
1454
|
|
|
1443
1455
|
async [_copyIdealToActual] () {
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "6.1
|
|
3
|
+
"version": "6.2.1",
|
|
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.
|
|
42
|
+
"@npmcli/template-oss": "4.11.1",
|
|
43
43
|
"benchmark": "^2.1.4",
|
|
44
44
|
"chalk": "^4.1.0",
|
|
45
45
|
"minify-registry-metadata": "^3.0.0",
|
|
46
46
|
"nock": "^13.2.0",
|
|
47
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",
|
|
@@ -100,7 +102,7 @@
|
|
|
100
102
|
},
|
|
101
103
|
"templateOSS": {
|
|
102
104
|
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
|
|
103
|
-
"version": "4.11.
|
|
105
|
+
"version": "4.11.1",
|
|
104
106
|
"content": "../../scripts/template-oss/index.js"
|
|
105
107
|
}
|
|
106
108
|
}
|