@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.
- package/lib/arborist/build-ideal-tree.js +4 -1
- package/lib/arborist/index.js +1 -0
- package/lib/arborist/isolated-reifier.js +453 -0
- package/lib/arborist/load-virtual.js +2 -2
- package/lib/arborist/load-workspaces.js +4 -18
- package/lib/arborist/rebuild.js +10 -1
- package/lib/arborist/reify.js +61 -37
- package/lib/link.js +3 -1
- package/lib/node.js +2 -0
- package/lib/override-resolves.js +1 -1
- package/lib/override-set.js +28 -1
- package/lib/place-dep.js +2 -1
- package/package.json +7 -6
|
@@ -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
|
-
|
|
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
|
}
|
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
|
+
}
|
|
@@ -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 =
|
|
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
|
-
|
|
28
|
-
|
|
13
|
+
if (node && workspaces.size) {
|
|
14
|
+
node.workspaces = workspaces
|
|
15
|
+
}
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
return mapWorkspaces.virtual(opts)
|
|
17
|
+
return node
|
|
32
18
|
}
|
|
33
19
|
}
|
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,
|
|
@@ -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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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.
|
package/lib/override-resolves.js
CHANGED
package/lib/override-set.js
CHANGED
|
@@ -50,9 +50,36 @@ class OverrideSet {
|
|
|
50
50
|
continue
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
if
|
|
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.
|
|
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.
|
|
42
|
+
"@npmcli/template-oss": "4.11.1",
|
|
43
43
|
"benchmark": "^2.1.4",
|
|
44
44
|
"chalk": "^4.1.0",
|
|
45
|
-
"minify-registry-metadata": "^
|
|
45
|
+
"minify-registry-metadata": "^3.0.0",
|
|
46
46
|
"nock": "^13.2.0",
|
|
47
|
-
"tap": "^16.
|
|
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.
|
|
105
|
+
"version": "4.11.1",
|
|
105
106
|
"content": "../../scripts/template-oss/index.js"
|
|
106
107
|
}
|
|
107
108
|
}
|