@npmcli/arborist 8.0.1 → 8.0.2
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/isolated-reifier.js +326 -273
- package/lib/arborist/rebuild.js +13 -5
- package/lib/arborist/reify.js +140 -9
- package/lib/diff.js +7 -1
- package/package.json +4 -3
|
@@ -1,72 +1,105 @@
|
|
|
1
|
-
const _makeIdealGraph = Symbol('makeIdealGraph')
|
|
2
|
-
const _createIsolatedTree = Symbol.for('createIsolatedTree')
|
|
3
|
-
const _createBundledTree = Symbol('createBundledTree')
|
|
4
1
|
const { mkdirSync } = require('node:fs')
|
|
5
2
|
const pacote = require('pacote')
|
|
6
3
|
const { join } = require('node:path')
|
|
7
4
|
const { depth } = require('treeverse')
|
|
8
5
|
const crypto = require('node:crypto')
|
|
9
6
|
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
// generate short hash key based on the dependency tree starting at this node
|
|
8
|
+
const getKey = (startNode) => {
|
|
9
|
+
const deps = []
|
|
10
|
+
const branch = []
|
|
11
|
+
depth({
|
|
12
|
+
tree: startNode,
|
|
13
|
+
getChildren: node => node.dependencies,
|
|
14
|
+
visit: node => {
|
|
15
|
+
branch.push(`${node.packageName}@${node.version}`)
|
|
16
|
+
deps.push(`${branch.join('->')}::${node.resolved}`)
|
|
17
|
+
},
|
|
18
|
+
leave: () => {
|
|
19
|
+
branch.pop()
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
deps.sort()
|
|
23
|
+
// TODO these replaces were originally to deal with node 14 not supporting base64url and likely don't need to happen anymore
|
|
24
|
+
// Changing this is a pretty significant breaking change, but removing parts of the hash increases collision possibilities (even if slight).
|
|
25
|
+
const hash = crypto.createHash('shake256', { outputLength: 16 })
|
|
26
|
+
.update(deps.join(','))
|
|
27
|
+
.digest('base64')
|
|
28
|
+
.replace(/\+/g, '-')
|
|
29
|
+
.replace(/\//g, '_')
|
|
30
|
+
.replace(/=+$/m, '')
|
|
31
|
+
return `${startNode.packageName}@${startNode.version}-${hash}`
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
module.exports = cls => class IsolatedReifier extends cls {
|
|
35
|
+
#externalProxies = new Map()
|
|
36
|
+
#processedEdges = new Set()
|
|
37
|
+
#workspaceProxies = new Map()
|
|
38
|
+
|
|
39
|
+
#generateChild (node, location, pkg, inStore, root) {
|
|
40
|
+
const newChild = {
|
|
41
|
+
binPaths: [],
|
|
42
|
+
children: new Map(),
|
|
43
|
+
edgesIn: new Set(),
|
|
44
|
+
edgesOut: new Map(),
|
|
45
|
+
fsChildren: new Set(),
|
|
46
|
+
/* istanbul ignore next -- emulate Node */
|
|
47
|
+
getBundler () {
|
|
48
|
+
return null
|
|
49
|
+
},
|
|
50
|
+
global: false,
|
|
51
|
+
globalTop: false,
|
|
52
|
+
hasShrinkwrap: false,
|
|
53
|
+
inDepBundle: false,
|
|
54
|
+
integrity: null,
|
|
55
|
+
isInStore: inStore,
|
|
56
|
+
isLink: false,
|
|
57
|
+
isProjectRoot: false,
|
|
58
|
+
isRoot: false,
|
|
59
|
+
isTop: false,
|
|
60
|
+
location,
|
|
61
|
+
name: node.packageName || node.name,
|
|
62
|
+
optional: node.optional,
|
|
63
|
+
package: pkg,
|
|
64
|
+
parent: root,
|
|
65
|
+
path: join(this.idealGraph.root.localPath, location),
|
|
66
|
+
realpath: join(this.idealGraph.root.localPath, location),
|
|
67
|
+
resolved: node.resolved,
|
|
68
|
+
root,
|
|
69
|
+
top: { path: this.idealGraph.root.localPath },
|
|
70
|
+
version: pkg.version,
|
|
71
|
+
}
|
|
72
|
+
newChild.target = newChild
|
|
73
|
+
root.children.set(newChild.location, newChild)
|
|
74
|
+
root.inventory.set(newChild.location, newChild)
|
|
75
|
+
}
|
|
76
|
+
|
|
26
77
|
/**
|
|
27
78
|
* Create an ideal graph.
|
|
28
79
|
*
|
|
29
80
|
* An implementation of npm RFC-0042
|
|
30
81
|
* https://github.com/npm/rfcs/blob/main/accepted/0042-isolated-mode.md
|
|
31
82
|
*
|
|
32
|
-
* This entire file should be considered technical debt that will be resolved
|
|
33
|
-
*
|
|
34
|
-
* and the incremental state of building trees and reifying contains too many
|
|
35
|
-
* assumptions to do a linked mode properly.
|
|
83
|
+
* This entire file should be considered technical debt that will be resolved with an Arborist refactor or rewrite.
|
|
84
|
+
* Embedded logic in Nodes and Links, and the incremental state of building trees and reifying contains too many assumptions to do a linked mode properly.
|
|
36
85
|
*
|
|
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.
|
|
86
|
+
* Instead, this approach takes a tree built from build-ideal-tree, and returns a new tree-like structure without the embedded logic of Node and Link classes.
|
|
40
87
|
*
|
|
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.
|
|
88
|
+
* Since the RFC requires leaving the package-lock in place, this approach temporarily replaces the tree state for a couple of steps of reifying.
|
|
43
89
|
*
|
|
44
90
|
**/
|
|
45
|
-
async
|
|
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)
|
|
91
|
+
async makeIdealGraph () {
|
|
54
92
|
const idealTree = this.idealTree
|
|
55
93
|
|
|
56
|
-
this.
|
|
57
|
-
|
|
94
|
+
this.idealGraph = {
|
|
95
|
+
external: [],
|
|
96
|
+
isProjectRoot: true,
|
|
97
|
+
localLocation: idealTree.location,
|
|
98
|
+
localPath: idealTree.path,
|
|
99
|
+
}
|
|
58
100
|
this.counter = 0
|
|
59
101
|
|
|
60
|
-
|
|
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))
|
|
102
|
+
this.idealGraph.workspaces = await Promise.all(Array.from(idealTree.fsChildren.values(), w => this.workspaceProxy(w)))
|
|
70
103
|
const processed = new Set()
|
|
71
104
|
const queue = [idealTree, ...idealTree.fsChildren]
|
|
72
105
|
while (queue.length !== 0) {
|
|
@@ -75,42 +108,53 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
75
108
|
continue
|
|
76
109
|
}
|
|
77
110
|
processed.add(next.location)
|
|
78
|
-
next.edgesOut.forEach(
|
|
79
|
-
if (
|
|
80
|
-
|
|
111
|
+
next.edgesOut.forEach(edge => {
|
|
112
|
+
if (edge.to && !(next.package.bundleDependencies || next.package.bundledDependencies || []).includes(edge.to.name)) {
|
|
113
|
+
queue.push(edge.to)
|
|
81
114
|
}
|
|
82
|
-
queue.push(e.to)
|
|
83
115
|
})
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
// local `file:` deps are in fsChildren but are not workspaces.
|
|
117
|
+
// they are already handled as workspace-like proxies above and should not go through the external/store extraction path.
|
|
118
|
+
if (!next.isProjectRoot && !next.isWorkspace && !next.inert && !idealTree.fsChildren.has(next) && !idealTree.fsChildren.has(next.target)) {
|
|
119
|
+
this.idealGraph.external.push(await this.externalProxy(next))
|
|
86
120
|
}
|
|
87
121
|
}
|
|
88
122
|
|
|
89
|
-
await this.assignCommonProperties(idealTree,
|
|
90
|
-
|
|
91
|
-
this.idealGraph = root
|
|
123
|
+
await this.assignCommonProperties(idealTree, this.idealGraph)
|
|
92
124
|
}
|
|
93
125
|
|
|
94
|
-
async workspaceProxy (
|
|
126
|
+
async workspaceProxy (node) {
|
|
127
|
+
if (this.#workspaceProxies.has(node)) {
|
|
128
|
+
return this.#workspaceProxies.get(node)
|
|
129
|
+
}
|
|
130
|
+
const result = {}
|
|
131
|
+
// XXX this goes recursive if we don't set here because assignCommonProperties also calls this.workspaceProxy
|
|
132
|
+
this.#workspaceProxies.set(node, result)
|
|
95
133
|
result.localLocation = node.location
|
|
96
134
|
result.localPath = node.path
|
|
97
135
|
result.isWorkspace = true
|
|
98
136
|
result.resolved = node.resolved
|
|
99
137
|
await this.assignCommonProperties(node, result)
|
|
138
|
+
return result
|
|
100
139
|
}
|
|
101
140
|
|
|
102
|
-
async externalProxy (
|
|
103
|
-
|
|
141
|
+
async externalProxy (node) {
|
|
142
|
+
if (this.#externalProxies.has(node)) {
|
|
143
|
+
return this.#externalProxies.get(node)
|
|
144
|
+
}
|
|
145
|
+
const result = {}
|
|
146
|
+
// XXX this goes recursive if we don't set here because assignCommonProperties also calls this.externalProxy
|
|
147
|
+
this.#externalProxies.set(node, result)
|
|
148
|
+
await this.assignCommonProperties(node, result, !node.hasShrinkwrap)
|
|
104
149
|
if (node.hasShrinkwrap) {
|
|
105
150
|
const dir = join(
|
|
106
151
|
node.root.path,
|
|
107
152
|
'node_modules',
|
|
108
153
|
'.store',
|
|
109
|
-
`${node.
|
|
154
|
+
`${node.packageName}@${node.version}`
|
|
110
155
|
)
|
|
111
156
|
mkdirSync(dir, { recursive: true })
|
|
112
|
-
// TODO this approach feels wrong
|
|
113
|
-
// and shouldn't be necessary for shrinkwraps
|
|
157
|
+
// TODO this approach feels wrong and shouldn't be necessary for shrinkwraps
|
|
114
158
|
await pacote.extract(node.resolved, dir, {
|
|
115
159
|
...this.options,
|
|
116
160
|
resolved: node.resolved,
|
|
@@ -118,51 +162,81 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
118
162
|
})
|
|
119
163
|
const Arborist = this.constructor
|
|
120
164
|
const arb = new Arborist({ ...this.options, path: dir })
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
e.id = `${node.id}=>${e.id}`
|
|
165
|
+
// Make sure that the ideal tree is build as the rest of the algorithm depends on it.
|
|
166
|
+
await arb.buildIdealTree({
|
|
167
|
+
complete: false,
|
|
168
|
+
dev: false,
|
|
126
169
|
})
|
|
170
|
+
await arb.makeIdealGraph()
|
|
171
|
+
this.idealGraph.external.push(...arb.idealGraph.external)
|
|
172
|
+
for (const edge of arb.idealGraph.external) {
|
|
173
|
+
edge.root = this.idealGraph
|
|
174
|
+
edge.id = `${node.id}=>${edge.id}`
|
|
175
|
+
}
|
|
127
176
|
result.localDependencies = []
|
|
128
177
|
result.externalDependencies = arb.idealGraph.externalDependencies
|
|
129
178
|
result.externalOptionalDependencies = arb.idealGraph.externalOptionalDependencies
|
|
130
179
|
result.dependencies = [
|
|
131
180
|
...result.externalDependencies,
|
|
132
|
-
...result.localDependencies,
|
|
133
181
|
...result.externalOptionalDependencies,
|
|
134
182
|
]
|
|
135
183
|
}
|
|
136
184
|
result.optional = node.optional
|
|
137
185
|
result.resolved = node.resolved
|
|
138
186
|
result.version = node.version
|
|
187
|
+
return result
|
|
139
188
|
}
|
|
140
189
|
|
|
141
|
-
async assignCommonProperties (node, result) {
|
|
142
|
-
|
|
143
|
-
|
|
190
|
+
async assignCommonProperties (node, result, populateDeps = true) {
|
|
191
|
+
result.root = this.idealGraph
|
|
192
|
+
result.id = this.counter++
|
|
193
|
+
/* istanbul ignore next - packageName is always set for real packages */
|
|
194
|
+
result.name = result.isWorkspace ? (node.packageName || node.name) : node.name
|
|
195
|
+
/* istanbul ignore next - packageName is always set for real packages */
|
|
196
|
+
result.packageName = node.packageName || node.name
|
|
197
|
+
result.package = { ...node.package }
|
|
198
|
+
result.package.bundleDependencies = undefined
|
|
199
|
+
result.hasInstallScript = node.hasInstallScript
|
|
200
|
+
|
|
201
|
+
if (!populateDeps) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const edges = [...node.edgesOut.values()].filter(edge =>
|
|
206
|
+
edge.to?.target &&
|
|
207
|
+
!(node.package.bundledDependencies || node.package.bundleDependencies)?.includes(edge.to.name)
|
|
208
|
+
)
|
|
209
|
+
const nonOptionalDeps = edges.filter(edge => !edge.optional).map(edge => edge.to.target)
|
|
210
|
+
|
|
211
|
+
// When legacyPeerDeps is enabled, peer dep edges are not created on the node.
|
|
212
|
+
// Resolve them from the tree so they get symlinked in the store.
|
|
213
|
+
const peerDeps = node.package.peerDependencies
|
|
214
|
+
if (peerDeps && node.legacyPeerDeps) {
|
|
215
|
+
const edgeNames = new Set(edges.map(edge => edge.name))
|
|
216
|
+
for (const peerName in peerDeps) {
|
|
217
|
+
if (!edgeNames.has(peerName)) {
|
|
218
|
+
const resolved = node.resolve(peerName)
|
|
219
|
+
if (resolved && resolved !== node && !resolved.inert) {
|
|
220
|
+
nonOptionalDeps.push(resolved.target)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
144
224
|
}
|
|
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
225
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
226
|
+
// local `file:` deps (non-workspace fsChildren) should be treated as local dependencies, not external, so they get symlinked directly instead of being extracted into the store.
|
|
227
|
+
const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n)
|
|
228
|
+
const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target)
|
|
229
|
+
result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.workspaceProxy(n)))
|
|
230
|
+
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.externalProxy(n)))
|
|
231
|
+
result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(n => this.externalProxy(n)))
|
|
152
232
|
result.dependencies = [
|
|
153
233
|
...result.externalDependencies,
|
|
154
234
|
...result.localDependencies,
|
|
155
235
|
...result.externalOptionalDependencies,
|
|
156
236
|
]
|
|
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
237
|
}
|
|
164
238
|
|
|
165
|
-
async
|
|
239
|
+
async #createBundledTree () {
|
|
166
240
|
// TODO: make sure that idealTree object exists
|
|
167
241
|
const idealTree = this.idealTree
|
|
168
242
|
// TODO: test workspaces having bundled deps
|
|
@@ -201,253 +275,232 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
201
275
|
nodes.set(to.location, { location: to.location, resolved: to.resolved, name: to.name, optional: to.optional, pkg: { ...to.package, bundleDependencies: undefined } })
|
|
202
276
|
edges.push({ from: from.isRoot ? 'root' : from.location, to: to.location })
|
|
203
277
|
|
|
204
|
-
to.edgesOut.forEach(
|
|
278
|
+
to.edgesOut.forEach(edge => {
|
|
205
279
|
// an edge out should always have a to
|
|
206
280
|
/* istanbul ignore else */
|
|
207
|
-
if (
|
|
208
|
-
queue.push({ from:
|
|
281
|
+
if (edge.to) {
|
|
282
|
+
queue.push({ from: edge.from, to: edge.to })
|
|
209
283
|
}
|
|
210
284
|
})
|
|
211
285
|
}
|
|
212
286
|
return { edges, nodes }
|
|
213
287
|
}
|
|
214
288
|
|
|
215
|
-
async
|
|
216
|
-
await this
|
|
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
|
-
}
|
|
289
|
+
async createIsolatedTree () {
|
|
290
|
+
await this.makeIdealGraph()
|
|
248
291
|
|
|
249
|
-
const
|
|
250
|
-
return `${idealTreeNode.name}@${idealTreeNode.version}-${treeHash(idealTreeNode)}`
|
|
251
|
-
}
|
|
292
|
+
const bundledTree = await this.#createBundledTree()
|
|
252
293
|
|
|
253
294
|
const root = {
|
|
254
|
-
fsChildren: [],
|
|
255
|
-
integrity: null,
|
|
256
|
-
inventory: new Map(),
|
|
257
|
-
isLink: false,
|
|
258
|
-
isRoot: true,
|
|
259
295
|
binPaths: [],
|
|
296
|
+
children: new Map(),
|
|
260
297
|
edgesIn: new Set(),
|
|
261
298
|
edgesOut: new Map(),
|
|
299
|
+
fsChildren: new Set(),
|
|
300
|
+
global: false,
|
|
262
301
|
hasShrinkwrap: false,
|
|
302
|
+
integrity: null,
|
|
303
|
+
inventory: new Map(),
|
|
304
|
+
isLink: false,
|
|
305
|
+
isProjectRoot: true,
|
|
306
|
+
isRoot: true,
|
|
307
|
+
isTop: true,
|
|
308
|
+
linksIn: new Set(),
|
|
309
|
+
meta: { loadedFromDisk: false },
|
|
310
|
+
package: this.idealGraph.root.package,
|
|
263
311
|
parent: null,
|
|
312
|
+
path: this.idealGraph.root.localPath,
|
|
313
|
+
realpath: this.idealGraph.root.localPath,
|
|
264
314
|
// TODO: we should probably not reference this.idealTree
|
|
265
315
|
resolved: this.idealTree.resolved,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
realpath: proxiedIdealTree.root.localPath,
|
|
269
|
-
package: proxiedIdealTree.root.package,
|
|
270
|
-
meta: { loadedFromDisk: false },
|
|
271
|
-
global: false,
|
|
272
|
-
isProjectRoot: true,
|
|
273
|
-
children: [],
|
|
316
|
+
tops: new Set(),
|
|
317
|
+
workspaces: new Map(),
|
|
274
318
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
319
|
+
root.inventory.set('', root)
|
|
320
|
+
root.root = root
|
|
321
|
+
root.target = root
|
|
322
|
+
// TODO inventory.query is a stub; audit-report needs 'packageName' support
|
|
278
323
|
root.inventory.query = () => {
|
|
279
324
|
return []
|
|
280
325
|
}
|
|
281
326
|
const processed = new Set()
|
|
282
|
-
|
|
327
|
+
for (const c of this.idealGraph.workspaces) {
|
|
328
|
+
const wsName = c.packageName
|
|
283
329
|
const workspace = {
|
|
330
|
+
binPaths: [],
|
|
331
|
+
children: new Map(),
|
|
284
332
|
edgesIn: new Set(),
|
|
285
333
|
edgesOut: new Map(),
|
|
286
|
-
|
|
334
|
+
fsChildren: new Set(),
|
|
287
335
|
hasInstallScript: c.hasInstallScript,
|
|
288
|
-
|
|
289
|
-
|
|
336
|
+
isLink: false,
|
|
337
|
+
isRoot: false,
|
|
338
|
+
linksIn: new Set(),
|
|
290
339
|
location: c.localLocation,
|
|
340
|
+
name: wsName,
|
|
341
|
+
package: c.package,
|
|
291
342
|
path: c.localPath,
|
|
292
343
|
realpath: c.localPath,
|
|
293
344
|
resolved: c.resolved,
|
|
294
345
|
}
|
|
295
|
-
|
|
346
|
+
workspace.target = workspace
|
|
347
|
+
root.fsChildren.add(workspace)
|
|
296
348
|
root.inventory.set(workspace.location, workspace)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
349
|
+
|
|
350
|
+
// Create workspace Link entry in children for _diffTrees lookup
|
|
351
|
+
const wsLink = {
|
|
352
|
+
binPaths: [],
|
|
353
|
+
children: new Map(),
|
|
354
|
+
edgesIn: new Set(),
|
|
355
|
+
edgesOut: new Map(),
|
|
356
|
+
fsChildren: new Set(),
|
|
300
357
|
global: false,
|
|
301
358
|
globalTop: false,
|
|
359
|
+
isLink: true,
|
|
302
360
|
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
361
|
isRoot: false,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
362
|
+
isTop: false,
|
|
363
|
+
linksIn: new Set(),
|
|
364
|
+
location: join('node_modules', wsName),
|
|
365
|
+
name: wsName,
|
|
366
|
+
package: workspace.package,
|
|
367
|
+
parent: root,
|
|
368
|
+
path: join(root.path, 'node_modules', wsName),
|
|
369
|
+
realpath: workspace.path,
|
|
370
|
+
root,
|
|
371
|
+
target: workspace,
|
|
328
372
|
}
|
|
329
|
-
|
|
330
|
-
root.
|
|
331
|
-
root.
|
|
373
|
+
root.children.set(wsLink.name, wsLink)
|
|
374
|
+
root.inventory.set(wsLink.location, wsLink)
|
|
375
|
+
root.workspaces.set(wsName, workspace.path)
|
|
376
|
+
workspace.linksIn.add(wsLink)
|
|
332
377
|
}
|
|
333
|
-
|
|
378
|
+
|
|
379
|
+
this.idealGraph.external.forEach(c => {
|
|
334
380
|
const key = getKey(c)
|
|
335
381
|
if (processed.has(key)) {
|
|
336
382
|
return
|
|
337
383
|
}
|
|
338
384
|
processed.add(key)
|
|
339
|
-
const location = join('node_modules', '.store', key, 'node_modules', c.
|
|
340
|
-
generateChild(c, location, c.package, true)
|
|
385
|
+
const location = join('node_modules', '.store', key, 'node_modules', c.packageName)
|
|
386
|
+
this.#generateChild(c, location, c.package, true, root)
|
|
341
387
|
})
|
|
388
|
+
|
|
342
389
|
bundledTree.nodes.forEach(node => {
|
|
343
|
-
generateChild(node, node.location, node.pkg, false)
|
|
390
|
+
this.#generateChild(node, node.location, node.pkg, false, root)
|
|
344
391
|
})
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
392
|
+
|
|
393
|
+
bundledTree.edges.forEach(edge => {
|
|
394
|
+
const from = edge.from === 'root' ? root : root.inventory.get(edge.from)
|
|
395
|
+
const to = root.inventory.get(edge.to)
|
|
348
396
|
// Maybe optional should be propagated from the original edge
|
|
349
|
-
const
|
|
350
|
-
from.edgesOut.set(to.name,
|
|
351
|
-
to.edgesIn.add(
|
|
397
|
+
const newEdge = { optional: false, from, to }
|
|
398
|
+
from.edgesOut.set(to.name, newEdge)
|
|
399
|
+
to.edgesIn.add(newEdge)
|
|
352
400
|
})
|
|
353
|
-
const memo = new Set()
|
|
354
401
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
}
|
|
402
|
+
this.#processEdges(this.idealGraph, false, root)
|
|
403
|
+
for (const node of this.idealGraph.workspaces) {
|
|
404
|
+
this.#processEdges(node, false, root)
|
|
405
|
+
}
|
|
406
|
+
return root
|
|
407
|
+
}
|
|
372
408
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
409
|
+
#processEdges (node, externalEdge, root) {
|
|
410
|
+
const key = getKey(node)
|
|
411
|
+
if (this.#processedEdges.has(key)) {
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
this.#processedEdges.add(key)
|
|
376
415
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
416
|
+
let from, nmFolder
|
|
417
|
+
if (externalEdge) {
|
|
418
|
+
const fromLocation = join('node_modules', '.store', key, 'node_modules', node.packageName)
|
|
419
|
+
from = root.children.get(fromLocation)
|
|
420
|
+
nmFolder = join('node_modules', '.store', key, 'node_modules')
|
|
421
|
+
} else {
|
|
422
|
+
from = node.isProjectRoot ? root : root.inventory.get(node.localLocation)
|
|
423
|
+
nmFolder = join(node.localLocation, 'node_modules')
|
|
424
|
+
}
|
|
425
|
+
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
|
|
426
|
+
if (!from) {
|
|
427
|
+
return
|
|
428
|
+
}
|
|
380
429
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
}
|
|
430
|
+
for (const dep of node.localDependencies) {
|
|
431
|
+
this.#processEdges(dep, false, root)
|
|
432
|
+
// nonOptional, local
|
|
433
|
+
this.#processDeps(dep, false, false, root, from, nmFolder)
|
|
434
|
+
}
|
|
435
|
+
for (const dep of node.externalDependencies) {
|
|
436
|
+
this.#processEdges(dep, true, root)
|
|
437
|
+
// nonOptional, external
|
|
438
|
+
this.#processDeps(dep, false, true, root, from, nmFolder)
|
|
439
|
+
}
|
|
440
|
+
for (const dep of node.externalOptionalDependencies) {
|
|
441
|
+
this.#processEdges(dep, true, root)
|
|
442
|
+
// optional, external
|
|
443
|
+
this.#processDeps(dep, true, true, root, from, nmFolder)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
425
446
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
447
|
+
#processDeps (dep, optional, external, root, from, nmFolder) {
|
|
448
|
+
const toKey = getKey(dep)
|
|
449
|
+
|
|
450
|
+
let target
|
|
451
|
+
if (external) {
|
|
452
|
+
const toLocation = join('node_modules', '.store', toKey, 'node_modules', dep.packageName)
|
|
453
|
+
target = root.children.get(toLocation)
|
|
454
|
+
} else {
|
|
455
|
+
target = root.inventory.get(dep.localLocation)
|
|
456
|
+
}
|
|
457
|
+
// TODO: we should no-op is an edge has already been created with the same fromKey and toKey
|
|
458
|
+
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
|
|
459
|
+
if (!target) {
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (dep.package.bin) {
|
|
464
|
+
for (const bn in dep.package.bin) {
|
|
465
|
+
target.binPaths.push(join(dep.root.localPath, nmFolder, '.bin', bn))
|
|
440
466
|
}
|
|
441
467
|
}
|
|
442
468
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
469
|
+
const link = {
|
|
470
|
+
binPaths: [],
|
|
471
|
+
children: new Map(),
|
|
472
|
+
edgesIn: new Set(),
|
|
473
|
+
edgesOut: new Map(),
|
|
474
|
+
fsChildren: new Set(),
|
|
475
|
+
global: false,
|
|
476
|
+
globalTop: false,
|
|
477
|
+
isLink: true,
|
|
478
|
+
isProjectRoot: false,
|
|
479
|
+
isRoot: false,
|
|
480
|
+
isStoreLink: true,
|
|
481
|
+
isTop: false,
|
|
482
|
+
location: join(nmFolder, dep.name),
|
|
483
|
+
name: toKey,
|
|
484
|
+
optional,
|
|
485
|
+
// TODO _id: 'abc' ?
|
|
486
|
+
package: { _id: 'abc', bundleDependencies: undefined, deprecated: undefined, bin: target.package.bin, scripts: dep.package.scripts },
|
|
487
|
+
parent: root,
|
|
488
|
+
path: join(dep.root.localPath, nmFolder, dep.name),
|
|
489
|
+
realpath: target.path,
|
|
490
|
+
resolved: external
|
|
491
|
+
? `file:.store/${toKey}/node_modules/${dep.packageName}`
|
|
492
|
+
: dep.resolved,
|
|
493
|
+
root,
|
|
494
|
+
target,
|
|
495
|
+
version: dep.version,
|
|
496
|
+
top: { path: dep.root.localPath },
|
|
446
497
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
498
|
+
const newEdge1 = { optional, from, to: link }
|
|
499
|
+
from.edgesOut.set(dep.name, newEdge1)
|
|
500
|
+
link.edgesIn.add(newEdge1)
|
|
501
|
+
const newEdge2 = { optional: false, from: link, to: target }
|
|
502
|
+
link.edgesOut.set(dep.name, newEdge2)
|
|
503
|
+
target.edgesIn.add(newEdge2)
|
|
504
|
+
root.children.set(link.location, link)
|
|
452
505
|
}
|
|
453
506
|
}
|
package/lib/arborist/rebuild.js
CHANGED
|
@@ -297,12 +297,12 @@ module.exports = cls => class Builder extends cls {
|
|
|
297
297
|
devOptional,
|
|
298
298
|
package: pkg,
|
|
299
299
|
location,
|
|
300
|
-
isStoreLink,
|
|
301
300
|
} = node.target
|
|
302
301
|
|
|
303
302
|
// skip any that we know we'll be deleting
|
|
304
|
-
// or
|
|
305
|
-
|
|
303
|
+
// or links to store entries (their scripts run on the store
|
|
304
|
+
// entry itself, not through the link)
|
|
305
|
+
if (this[_trashList].has(path) || (node.isLink && node.target?.isInStore)) {
|
|
306
306
|
return
|
|
307
307
|
}
|
|
308
308
|
|
|
@@ -383,13 +383,21 @@ module.exports = cls => class Builder extends cls {
|
|
|
383
383
|
|
|
384
384
|
const timeEnd = time.start(`build:link:${node.location}`)
|
|
385
385
|
|
|
386
|
-
|
|
386
|
+
// On Windows, antivirus/indexer can transiently lock files, causing EPERM/EACCES/EBUSY on the rename inside write-file-atomic (used by bin-links/fix-bin.js), so, retry with backoff.
|
|
387
|
+
const promiseRetry = require('promise-retry')
|
|
388
|
+
const p = promiseRetry((retry) => binLinks({
|
|
387
389
|
pkg: node.package,
|
|
388
390
|
path: node.path,
|
|
389
391
|
top: !!(node.isTop || node.globalTop),
|
|
390
392
|
force: this.options.force,
|
|
391
393
|
global: !!node.globalTop,
|
|
392
|
-
})
|
|
394
|
+
}).catch(/* istanbul ignore next - Windows-only transient antivirus locks */ err => {
|
|
395
|
+
if (process.platform === 'win32' &&
|
|
396
|
+
(err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'EBUSY')) {
|
|
397
|
+
return retry(err)
|
|
398
|
+
}
|
|
399
|
+
throw err
|
|
400
|
+
}), { retries: 5, minTimeout: 500 })
|
|
393
401
|
|
|
394
402
|
await (this.#doHandleOptionalFailure
|
|
395
403
|
? this[_handleOptionalFailure](node, p)
|
package/lib/arborist/reify.js
CHANGED
|
@@ -11,11 +11,13 @@ const { log, time } = require('proc-log')
|
|
|
11
11
|
const hgi = require('hosted-git-info')
|
|
12
12
|
const rpj = require('read-package-json-fast')
|
|
13
13
|
|
|
14
|
-
const { dirname, resolve, relative, join } = require('node:path')
|
|
14
|
+
const { dirname, resolve, relative, join, sep } = require('node:path')
|
|
15
15
|
const { depth: dfwalk } = require('treeverse')
|
|
16
|
+
const { existsSync } = require('node:fs')
|
|
16
17
|
const {
|
|
17
18
|
lstat,
|
|
18
19
|
mkdir,
|
|
20
|
+
readdir,
|
|
19
21
|
rm,
|
|
20
22
|
symlink,
|
|
21
23
|
} = require('node:fs/promises')
|
|
@@ -77,8 +79,6 @@ const _usePackageLock = Symbol.for('usePackageLock')
|
|
|
77
79
|
// used by build-ideal-tree mixin
|
|
78
80
|
const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
|
|
79
81
|
|
|
80
|
-
const _createIsolatedTree = Symbol.for('createIsolatedTree')
|
|
81
|
-
|
|
82
82
|
module.exports = cls => class Reifier extends cls {
|
|
83
83
|
#bundleMissing = new Set() // child nodes we'd EXPECT to be included in a bundle, but aren't
|
|
84
84
|
#bundleUnpacked = new Set() // the nodes we unpack to read their bundles
|
|
@@ -93,6 +93,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
93
93
|
#shrinkwrapInflated = new Set()
|
|
94
94
|
#sparseTreeDirs = new Set()
|
|
95
95
|
#sparseTreeRoots = new Set()
|
|
96
|
+
#linkedActualForDiff = null
|
|
96
97
|
|
|
97
98
|
constructor (options) {
|
|
98
99
|
super(options)
|
|
@@ -136,16 +137,21 @@ module.exports = cls => class Reifier extends cls {
|
|
|
136
137
|
// this is currently technical debt which will be resolved in a refactor
|
|
137
138
|
// of Node/Link trees
|
|
138
139
|
log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.')
|
|
139
|
-
this.idealTree = await this
|
|
140
|
+
this.idealTree = await this.createIsolatedTree()
|
|
141
|
+
this.#linkedActualForDiff = this.#buildLinkedActualForDiff(
|
|
142
|
+
this.idealTree, this.actualTree
|
|
143
|
+
)
|
|
140
144
|
}
|
|
141
145
|
await this[_diffTrees]()
|
|
142
146
|
await this[_reifyPackages]()
|
|
143
147
|
if (linked) {
|
|
148
|
+
await this.#cleanOrphanedStoreEntries()
|
|
144
149
|
// swap back in the idealTree
|
|
145
150
|
// so that the lockfile is preserved
|
|
146
151
|
this.idealTree = oldTree
|
|
147
152
|
}
|
|
148
153
|
await this[_saveIdealTree](options)
|
|
154
|
+
this.#linkedActualForDiff = null
|
|
149
155
|
// clean up any trash that is still in the tree
|
|
150
156
|
for (const path of this[_trashList]) {
|
|
151
157
|
const loc = relpath(this.idealTree.realpath, path)
|
|
@@ -161,7 +167,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
161
167
|
// was not changed, delete anything in the ideal and not actual.
|
|
162
168
|
// Then we move the entire idealTree over to this.actualTree, and
|
|
163
169
|
// save the hidden lockfile.
|
|
164
|
-
if (this.diff && this.diff.filterSet.size) {
|
|
170
|
+
if (this.diff && this.diff.filterSet.size && !linked) {
|
|
165
171
|
const reroot = new Set()
|
|
166
172
|
|
|
167
173
|
const { filterSet } = this.diff
|
|
@@ -442,9 +448,14 @@ module.exports = cls => class Reifier extends cls {
|
|
|
442
448
|
if (ideal) {
|
|
443
449
|
filterNodes.push(ideal)
|
|
444
450
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
451
|
+
// Skip actual-side filterNodes when using the linked diff wrapper.
|
|
452
|
+
// Those nodes have root===actualTree, not root===linkedActualForDiff, and Diff.calculate requires filterNode.root to match actual.
|
|
453
|
+
// The ideal filterNode alone is sufficient to scope the workspace diff.
|
|
454
|
+
if (!this.#linkedActualForDiff) {
|
|
455
|
+
const actual = this.actualTree.children.get(ws)
|
|
456
|
+
if (actual) {
|
|
457
|
+
filterNodes.push(actual)
|
|
458
|
+
}
|
|
448
459
|
}
|
|
449
460
|
}
|
|
450
461
|
}
|
|
@@ -465,7 +476,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
465
476
|
this.diff = Diff.calculate({
|
|
466
477
|
shrinkwrapInflated: this.#shrinkwrapInflated,
|
|
467
478
|
filterNodes,
|
|
468
|
-
actual: this.actualTree,
|
|
479
|
+
actual: this.#linkedActualForDiff || this.actualTree,
|
|
469
480
|
ideal: this.idealTree,
|
|
470
481
|
})
|
|
471
482
|
|
|
@@ -625,6 +636,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
625
636
|
// if the directory already exists, made will be undefined. if that's the case
|
|
626
637
|
// we don't want to remove it because we aren't the ones who created it so we
|
|
627
638
|
// omit it from the #sparseTreeRoots
|
|
639
|
+
/* istanbul ignore next -- pre-existing: mkdir returns undefined when dir exists, covered in reify tests but lost in aggregate coverage merge */
|
|
628
640
|
if (made) {
|
|
629
641
|
this.#sparseTreeRoots.add(made)
|
|
630
642
|
}
|
|
@@ -824,6 +836,125 @@ module.exports = cls => class Reifier extends cls {
|
|
|
824
836
|
}) : p).then(() => node)
|
|
825
837
|
}
|
|
826
838
|
|
|
839
|
+
// Build a flat actual tree wrapper for linked installs so the diff can
|
|
840
|
+
// correctly match store entries that already exist on disk.
|
|
841
|
+
#buildLinkedActualForDiff (idealTree, actualTree) {
|
|
842
|
+
const combined = new Map()
|
|
843
|
+
|
|
844
|
+
for (const child of actualTree.children.values()) {
|
|
845
|
+
combined.set(child.path, child)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
for (const child of idealTree.children.values()) {
|
|
849
|
+
if (!combined.has(child.path) && (child.isInStore || child.isStoreLink) &&
|
|
850
|
+
existsSync(child.path)) {
|
|
851
|
+
const entry = {
|
|
852
|
+
global: false,
|
|
853
|
+
globalTop: false,
|
|
854
|
+
isProjectRoot: false,
|
|
855
|
+
isTop: false,
|
|
856
|
+
location: child.location,
|
|
857
|
+
name: child.name,
|
|
858
|
+
optional: child.optional,
|
|
859
|
+
top: child.top,
|
|
860
|
+
children: [],
|
|
861
|
+
edgesIn: new Set(),
|
|
862
|
+
edgesOut: new Map(),
|
|
863
|
+
binPaths: [],
|
|
864
|
+
fsChildren: [],
|
|
865
|
+
/* istanbul ignore next -- emulate Node */
|
|
866
|
+
getBundler () {
|
|
867
|
+
return null
|
|
868
|
+
},
|
|
869
|
+
hasShrinkwrap: false,
|
|
870
|
+
inDepBundle: false,
|
|
871
|
+
integrity: null,
|
|
872
|
+
isLink: Boolean(child.isLink),
|
|
873
|
+
isRoot: false,
|
|
874
|
+
isInStore: Boolean(child.isInStore),
|
|
875
|
+
path: child.path,
|
|
876
|
+
realpath: child.realpath,
|
|
877
|
+
resolved: child.resolved,
|
|
878
|
+
version: child.version,
|
|
879
|
+
package: child.package,
|
|
880
|
+
}
|
|
881
|
+
entry.target = entry
|
|
882
|
+
if (child.isLink && combined.has(child.realpath)) {
|
|
883
|
+
entry.target = combined.get(child.realpath)
|
|
884
|
+
}
|
|
885
|
+
combined.set(child.path, entry)
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const origGet = actualTree.children.get.bind(actualTree.children)
|
|
890
|
+
const combinedGet = combined.get.bind(combined)
|
|
891
|
+
/* istanbul ignore next -- only reached during scoped workspace installs */
|
|
892
|
+
combined.get = (key) => combinedGet(key) || origGet(key)
|
|
893
|
+
|
|
894
|
+
const wrapper = {
|
|
895
|
+
isRoot: true,
|
|
896
|
+
isLink: actualTree.isLink,
|
|
897
|
+
target: actualTree.target,
|
|
898
|
+
fsChildren: actualTree.fsChildren,
|
|
899
|
+
path: actualTree.path,
|
|
900
|
+
realpath: actualTree.realpath,
|
|
901
|
+
edgesOut: actualTree.edgesOut,
|
|
902
|
+
inventory: actualTree.inventory,
|
|
903
|
+
package: actualTree.package,
|
|
904
|
+
resolved: actualTree.resolved,
|
|
905
|
+
version: actualTree.version,
|
|
906
|
+
integrity: actualTree.integrity,
|
|
907
|
+
binPaths: actualTree.binPaths,
|
|
908
|
+
hasShrinkwrap: false,
|
|
909
|
+
inDepBundle: false,
|
|
910
|
+
parent: null,
|
|
911
|
+
children: combined,
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
for (const child of combined.values()) {
|
|
915
|
+
if (!child.parent) {
|
|
916
|
+
child.parent = wrapper
|
|
917
|
+
child.root = wrapper
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return wrapper
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// After a linked install, scan node_modules/.store/ and remove any
|
|
925
|
+
// directories that are not referenced by the current ideal tree.
|
|
926
|
+
async #cleanOrphanedStoreEntries () {
|
|
927
|
+
const storeDir = resolve(this.path, 'node_modules', '.store')
|
|
928
|
+
let entries
|
|
929
|
+
try {
|
|
930
|
+
entries = await readdir(storeDir)
|
|
931
|
+
} catch {
|
|
932
|
+
return
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const validKeys = new Set()
|
|
936
|
+
for (const child of this.idealTree.children.values()) {
|
|
937
|
+
if (child.isInStore) {
|
|
938
|
+
const key = child.location.split(sep)[2]
|
|
939
|
+
validKeys.add(key)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const orphaned = entries.filter(e => !validKeys.has(e))
|
|
944
|
+
if (!orphaned.length) {
|
|
945
|
+
return
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
log.silly('reify', 'cleaning orphaned store entries', orphaned)
|
|
949
|
+
await promiseAllRejectLate(
|
|
950
|
+
orphaned.map(e =>
|
|
951
|
+
rm(resolve(storeDir, e), { recursive: true, force: true })
|
|
952
|
+
.catch(/* istanbul ignore next -- rm with force rarely fails */
|
|
953
|
+
er => log.warn('cleanup', `Failed to remove orphaned store entry ${e}`, er))
|
|
954
|
+
)
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
|
|
827
958
|
#registryResolved (resolved) {
|
|
828
959
|
// the default registry url is a magic value meaning "the currently
|
|
829
960
|
// configured registry".
|
package/lib/diff.js
CHANGED
|
@@ -69,6 +69,7 @@ class Diff {
|
|
|
69
69
|
tree: filterNode,
|
|
70
70
|
visit: node => filterSet.add(node),
|
|
71
71
|
getChildren: node => {
|
|
72
|
+
const orig = node
|
|
72
73
|
node = node.target
|
|
73
74
|
const loc = node.location
|
|
74
75
|
const idealNode = ideal.inventory.get(loc)
|
|
@@ -85,7 +86,12 @@ class Diff {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
const result = ideals.concat(actuals)
|
|
90
|
+
// Include link targets so store entries end up in filterSet
|
|
91
|
+
if (orig.isLink) {
|
|
92
|
+
result.push(node)
|
|
93
|
+
}
|
|
94
|
+
return result
|
|
89
95
|
},
|
|
90
96
|
})
|
|
91
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.2",
|
|
4
4
|
"description": "Manage node_modules trees",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@isaacs/string-locale-compare": "^1.1.0",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"proggy": "^3.0.0",
|
|
34
34
|
"promise-all-reject-late": "^1.0.0",
|
|
35
35
|
"promise-call-limit": "^3.0.1",
|
|
36
|
+
"promise-retry": "^2.0.1",
|
|
36
37
|
"read-package-json-fast": "^4.0.0",
|
|
37
38
|
"semver": "^7.3.7",
|
|
38
39
|
"ssri": "^12.0.0",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@npmcli/eslint-config": "^5.0.1",
|
|
44
|
-
"@npmcli/template-oss": "4.
|
|
45
|
+
"@npmcli/template-oss": "4.29.0",
|
|
45
46
|
"benchmark": "^2.1.4",
|
|
46
47
|
"minify-registry-metadata": "^4.0.0",
|
|
47
48
|
"nock": "^13.3.3",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
},
|
|
94
95
|
"templateOSS": {
|
|
95
96
|
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
|
|
96
|
-
"version": "4.
|
|
97
|
+
"version": "4.29.0",
|
|
97
98
|
"content": "../../scripts/template-oss/index.js"
|
|
98
99
|
}
|
|
99
100
|
}
|