@npmcli/arborist 8.0.1 → 8.0.3
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 +321 -297
- package/lib/arborist/rebuild.js +13 -5
- package/lib/arborist/reify.js +105 -9
- package/lib/diff.js +7 -1
- package/lib/inventory.js +1 -2
- package/lib/isolated-classes.js +138 -0
- package/lib/node.js +39 -19
- package/lib/query-selector-all.js +7 -2
- package/package.json +4 -3
|
@@ -1,72 +1,103 @@
|
|
|
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')
|
|
6
|
+
const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js')
|
|
9
7
|
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
// generate short hash key based on the dependency tree starting at this node
|
|
9
|
+
const getKey = (startNode) => {
|
|
10
|
+
const deps = []
|
|
11
|
+
const branch = []
|
|
12
|
+
depth({
|
|
13
|
+
tree: startNode,
|
|
14
|
+
getChildren: node => node.dependencies,
|
|
15
|
+
visit: node => {
|
|
16
|
+
branch.push(`${node.packageName}@${node.version}`)
|
|
17
|
+
deps.push(`${branch.join('->')}::${node.resolved}`)
|
|
18
|
+
},
|
|
19
|
+
leave: () => {
|
|
20
|
+
branch.pop()
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
deps.sort()
|
|
24
|
+
// TODO these replaces were originally to deal with node 14 not supporting base64url and likely don't need to happen anymore
|
|
25
|
+
// Changing this is a pretty significant breaking change, but removing parts of the hash increases collision possibilities (even if slight).
|
|
26
|
+
const hash = crypto.createHash('shake256', { outputLength: 16 })
|
|
27
|
+
.update(deps.join(','))
|
|
28
|
+
.digest('base64')
|
|
29
|
+
.replace(/\+/g, '-')
|
|
30
|
+
.replace(/\//g, '_')
|
|
31
|
+
.replace(/=+$/m, '')
|
|
32
|
+
return `${startNode.packageName}@${startNode.version}-${hash}`
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
module.exports = cls => class IsolatedReifier extends cls {
|
|
36
|
+
#externalProxies = new Map()
|
|
37
|
+
#omit = new Set()
|
|
38
|
+
#rootDeclaredDeps = new Set()
|
|
39
|
+
#processedEdges = new Set()
|
|
40
|
+
#workspaceProxies = new Map()
|
|
41
|
+
|
|
42
|
+
#generateChild (node, location, pkg, isInStore, root) {
|
|
43
|
+
const newChild = new IsolatedNode({
|
|
44
|
+
isInStore,
|
|
45
|
+
location,
|
|
46
|
+
name: node.packageName || node.name,
|
|
47
|
+
optional: node.optional,
|
|
48
|
+
package: pkg,
|
|
49
|
+
parent: root,
|
|
50
|
+
path: join(this.idealGraph.localPath, location),
|
|
51
|
+
resolved: node.resolved,
|
|
52
|
+
root,
|
|
53
|
+
})
|
|
54
|
+
// XXX top is from place-dep not lib/node.js
|
|
55
|
+
newChild.top = { path: this.idealGraph.localPath }
|
|
56
|
+
root.children.set(newChild.location, newChild)
|
|
57
|
+
root.inventory.set(newChild.location, newChild)
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
/**
|
|
27
61
|
* Create an ideal graph.
|
|
28
62
|
*
|
|
29
63
|
* An implementation of npm RFC-0042
|
|
30
64
|
* https://github.com/npm/rfcs/blob/main/accepted/0042-isolated-mode.md
|
|
31
65
|
*
|
|
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.
|
|
66
|
+
* This entire file should be considered technical debt that will be resolved with an Arborist refactor or rewrite.
|
|
67
|
+
* 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
68
|
*
|
|
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.
|
|
69
|
+
* 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
70
|
*
|
|
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.
|
|
71
|
+
* 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
72
|
*
|
|
44
73
|
**/
|
|
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)
|
|
74
|
+
async makeIdealGraph () {
|
|
54
75
|
const idealTree = this.idealTree
|
|
76
|
+
this.#omit = new Set(this.options.omit)
|
|
77
|
+
const omit = this.#omit
|
|
55
78
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
// npm auto-creates 'workspace' edges from root to all workspaces.
|
|
80
|
+
// For isolated/linked mode, only include workspaces that root explicitly declares as dependencies.
|
|
81
|
+
// When omitting dep types, exclude those from the declared set so their workspaces aren't hoisted.
|
|
82
|
+
const rootPkg = idealTree.package
|
|
83
|
+
this.#rootDeclaredDeps = new Set(Object.keys(Object.assign({},
|
|
84
|
+
rootPkg.dependencies,
|
|
85
|
+
(!omit.has('dev') && rootPkg.devDependencies),
|
|
86
|
+
(!omit.has('optional') && rootPkg.optionalDependencies),
|
|
87
|
+
(!omit.has('peer') && rootPkg.peerDependencies)
|
|
88
|
+
)))
|
|
59
89
|
|
|
60
|
-
//
|
|
61
|
-
this.
|
|
62
|
-
|
|
90
|
+
// XXX this sometimes acts like a node too
|
|
91
|
+
this.idealGraph = {
|
|
92
|
+
external: [],
|
|
93
|
+
isProjectRoot: true,
|
|
94
|
+
localLocation: idealTree.location,
|
|
95
|
+
localPath: idealTree.path,
|
|
96
|
+
path: idealTree.path,
|
|
97
|
+
}
|
|
98
|
+
this.counter = 0
|
|
63
99
|
|
|
64
|
-
|
|
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))
|
|
100
|
+
this.idealGraph.workspaces = await Promise.all(Array.from(idealTree.fsChildren.values(), w => this.#workspaceProxy(w)))
|
|
70
101
|
const processed = new Set()
|
|
71
102
|
const queue = [idealTree, ...idealTree.fsChildren]
|
|
72
103
|
while (queue.length !== 0) {
|
|
@@ -75,42 +106,53 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
75
106
|
continue
|
|
76
107
|
}
|
|
77
108
|
processed.add(next.location)
|
|
78
|
-
next.edgesOut.forEach(
|
|
79
|
-
if (
|
|
80
|
-
|
|
109
|
+
next.edgesOut.forEach(edge => {
|
|
110
|
+
if (edge.to && !(next.package.bundleDependencies || next.package.bundledDependencies || []).includes(edge.to.name) && !edge.to.shouldOmit?.(omit)) {
|
|
111
|
+
queue.push(edge.to)
|
|
81
112
|
}
|
|
82
|
-
queue.push(e.to)
|
|
83
113
|
})
|
|
84
|
-
|
|
85
|
-
|
|
114
|
+
// local `file:` deps are in fsChildren but are not workspaces.
|
|
115
|
+
// they are already handled as workspace-like proxies above and should not go through the external/store extraction path.
|
|
116
|
+
if (!next.isProjectRoot && !next.isWorkspace && !next.inert && !idealTree.fsChildren.has(next) && !idealTree.fsChildren.has(next.target)) {
|
|
117
|
+
this.idealGraph.external.push(await this.#externalProxy(next))
|
|
86
118
|
}
|
|
87
119
|
}
|
|
88
120
|
|
|
89
|
-
await this
|
|
90
|
-
|
|
91
|
-
this.idealGraph = root
|
|
121
|
+
await this.#assignCommonProperties(idealTree, this.idealGraph)
|
|
92
122
|
}
|
|
93
123
|
|
|
94
|
-
async workspaceProxy (
|
|
124
|
+
async #workspaceProxy (node) {
|
|
125
|
+
if (this.#workspaceProxies.has(node)) {
|
|
126
|
+
return this.#workspaceProxies.get(node)
|
|
127
|
+
}
|
|
128
|
+
const result = {}
|
|
129
|
+
// XXX this goes recursive if we don't set here because assignCommonProperties also calls this.#workspaceProxy
|
|
130
|
+
this.#workspaceProxies.set(node, result)
|
|
95
131
|
result.localLocation = node.location
|
|
96
132
|
result.localPath = node.path
|
|
97
133
|
result.isWorkspace = true
|
|
98
134
|
result.resolved = node.resolved
|
|
99
|
-
await this
|
|
135
|
+
await this.#assignCommonProperties(node, result)
|
|
136
|
+
return result
|
|
100
137
|
}
|
|
101
138
|
|
|
102
|
-
async externalProxy (
|
|
103
|
-
|
|
139
|
+
async #externalProxy (node) {
|
|
140
|
+
if (this.#externalProxies.has(node)) {
|
|
141
|
+
return this.#externalProxies.get(node)
|
|
142
|
+
}
|
|
143
|
+
const result = {}
|
|
144
|
+
// XXX this goes recursive if we don't set here because assignCommonProperties also calls this.#externalProxy
|
|
145
|
+
this.#externalProxies.set(node, result)
|
|
146
|
+
await this.#assignCommonProperties(node, result, !node.hasShrinkwrap)
|
|
104
147
|
if (node.hasShrinkwrap) {
|
|
105
148
|
const dir = join(
|
|
106
149
|
node.root.path,
|
|
107
150
|
'node_modules',
|
|
108
151
|
'.store',
|
|
109
|
-
`${node.
|
|
152
|
+
`${node.packageName}@${node.version}`
|
|
110
153
|
)
|
|
111
154
|
mkdirSync(dir, { recursive: true })
|
|
112
|
-
// TODO this approach feels wrong
|
|
113
|
-
// and shouldn't be necessary for shrinkwraps
|
|
155
|
+
// TODO this approach feels wrong and shouldn't be necessary for shrinkwraps
|
|
114
156
|
await pacote.extract(node.resolved, dir, {
|
|
115
157
|
...this.options,
|
|
116
158
|
resolved: node.resolved,
|
|
@@ -118,51 +160,104 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
118
160
|
})
|
|
119
161
|
const Arborist = this.constructor
|
|
120
162
|
const arb = new Arborist({ ...this.options, path: dir })
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
e.id = `${node.id}=>${e.id}`
|
|
163
|
+
// Make sure that the ideal tree is build as the rest of the algorithm depends on it.
|
|
164
|
+
await arb.buildIdealTree({
|
|
165
|
+
complete: false,
|
|
166
|
+
dev: false,
|
|
126
167
|
})
|
|
168
|
+
await arb.makeIdealGraph()
|
|
169
|
+
this.idealGraph.external.push(...arb.idealGraph.external)
|
|
170
|
+
for (const edge of arb.idealGraph.external) {
|
|
171
|
+
edge.root = this.idealGraph
|
|
172
|
+
edge.id = `${node.id}=>${edge.id}`
|
|
173
|
+
}
|
|
127
174
|
result.localDependencies = []
|
|
128
175
|
result.externalDependencies = arb.idealGraph.externalDependencies
|
|
129
176
|
result.externalOptionalDependencies = arb.idealGraph.externalOptionalDependencies
|
|
130
177
|
result.dependencies = [
|
|
131
178
|
...result.externalDependencies,
|
|
132
|
-
...result.localDependencies,
|
|
133
179
|
...result.externalOptionalDependencies,
|
|
134
180
|
]
|
|
135
181
|
}
|
|
136
182
|
result.optional = node.optional
|
|
137
183
|
result.resolved = node.resolved
|
|
138
184
|
result.version = node.version
|
|
185
|
+
return result
|
|
139
186
|
}
|
|
140
187
|
|
|
141
|
-
async assignCommonProperties (node, result) {
|
|
142
|
-
|
|
143
|
-
|
|
188
|
+
async #assignCommonProperties (node, result, populateDeps = true) {
|
|
189
|
+
result.root = this.idealGraph
|
|
190
|
+
// XXX does anything need this?
|
|
191
|
+
result.id = this.counter++
|
|
192
|
+
/* istanbul ignore next - packageName is always set for real packages */
|
|
193
|
+
result.name = result.isWorkspace ? (node.packageName || node.name) : node.name
|
|
194
|
+
/* istanbul ignore next - packageName is always set for real packages */
|
|
195
|
+
result.packageName = node.packageName || node.name
|
|
196
|
+
result.package = { ...node.package }
|
|
197
|
+
result.package.bundleDependencies = undefined
|
|
198
|
+
|
|
199
|
+
if (!populateDeps) {
|
|
200
|
+
return
|
|
144
201
|
}
|
|
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
202
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
203
|
+
let edges = [...node.edgesOut.values()].filter(edge =>
|
|
204
|
+
edge.to?.target &&
|
|
205
|
+
!(node.package.bundledDependencies || node.package.bundleDependencies)?.includes(edge.to.name)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// Only omit edge types for root and workspace nodes (matching shouldOmit scope)
|
|
209
|
+
if ((node.isProjectRoot || node.isWorkspace) && this.#omit.size) {
|
|
210
|
+
edges = edges.filter(edge => {
|
|
211
|
+
if (edge.dev && this.#omit.has('dev')) {
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
if (edge.optional && this.#omit.has('optional')) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
if (edge.peer && this.#omit.has('peer')) {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
return true
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let nonOptionalDeps = edges.filter(edge => !edge.optional).map(edge => edge.to.target)
|
|
225
|
+
|
|
226
|
+
// npm auto-creates 'workspace' edges from root to all workspaces.
|
|
227
|
+
// For isolated/linked mode, only include workspaces that root explicitly declares as dependencies.
|
|
228
|
+
if (node.isProjectRoot) {
|
|
229
|
+
nonOptionalDeps = nonOptionalDeps.filter(n => !n.isWorkspace || this.#rootDeclaredDeps.has(n.packageName))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// When legacyPeerDeps is enabled, peer dep edges are not created on the node.
|
|
233
|
+
// Resolve them from the tree so they get symlinked in the store.
|
|
234
|
+
const peerDeps = node.package.peerDependencies
|
|
235
|
+
if (peerDeps && node.legacyPeerDeps) {
|
|
236
|
+
const edgeNames = new Set(edges.map(edge => edge.name))
|
|
237
|
+
for (const peerName in peerDeps) {
|
|
238
|
+
if (!edgeNames.has(peerName)) {
|
|
239
|
+
const resolved = node.resolve(peerName)
|
|
240
|
+
if (resolved && resolved !== node && !resolved.inert) {
|
|
241
|
+
nonOptionalDeps.push(resolved.target)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 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.
|
|
248
|
+
const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n)
|
|
249
|
+
const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target)
|
|
250
|
+
result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.#workspaceProxy(n)))
|
|
251
|
+
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.#externalProxy(n)))
|
|
252
|
+
result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(n => this.#externalProxy(n)))
|
|
152
253
|
result.dependencies = [
|
|
153
254
|
...result.externalDependencies,
|
|
154
255
|
...result.localDependencies,
|
|
155
256
|
...result.externalOptionalDependencies,
|
|
156
257
|
]
|
|
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
258
|
}
|
|
164
259
|
|
|
165
|
-
async
|
|
260
|
+
async #createBundledTree () {
|
|
166
261
|
// TODO: make sure that idealTree object exists
|
|
167
262
|
const idealTree = this.idealTree
|
|
168
263
|
// TODO: test workspaces having bundled deps
|
|
@@ -201,253 +296,182 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
201
296
|
nodes.set(to.location, { location: to.location, resolved: to.resolved, name: to.name, optional: to.optional, pkg: { ...to.package, bundleDependencies: undefined } })
|
|
202
297
|
edges.push({ from: from.isRoot ? 'root' : from.location, to: to.location })
|
|
203
298
|
|
|
204
|
-
to.edgesOut.forEach(
|
|
299
|
+
to.edgesOut.forEach(edge => {
|
|
205
300
|
// an edge out should always have a to
|
|
206
301
|
/* istanbul ignore else */
|
|
207
|
-
if (
|
|
208
|
-
queue.push({ from:
|
|
302
|
+
if (edge.to) {
|
|
303
|
+
queue.push({ from: edge.from, to: edge.to })
|
|
209
304
|
}
|
|
210
305
|
})
|
|
211
306
|
}
|
|
212
307
|
return { edges, nodes }
|
|
213
308
|
}
|
|
214
309
|
|
|
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
|
-
}
|
|
248
|
-
|
|
249
|
-
const getKey = (idealTreeNode) => {
|
|
250
|
-
return `${idealTreeNode.name}@${idealTreeNode.version}-${treeHash(idealTreeNode)}`
|
|
251
|
-
}
|
|
310
|
+
async createIsolatedTree () {
|
|
311
|
+
await this.makeIdealGraph()
|
|
312
|
+
const bundledTree = await this.#createBundledTree()
|
|
252
313
|
|
|
253
|
-
const root =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
}
|
|
314
|
+
const root = new IsolatedNode(this.idealGraph)
|
|
315
|
+
root.root = root
|
|
316
|
+
root.top = root
|
|
317
|
+
root.inventory.set('', root)
|
|
281
318
|
const processed = new Set()
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
children: [],
|
|
287
|
-
hasInstallScript: c.hasInstallScript,
|
|
288
|
-
binPaths: [],
|
|
289
|
-
package: c.package,
|
|
319
|
+
for (const c of this.idealGraph.workspaces) {
|
|
320
|
+
const wsName = c.packageName
|
|
321
|
+
// XXX parent? root?
|
|
322
|
+
const workspace = new IsolatedNode({
|
|
290
323
|
location: c.localLocation,
|
|
324
|
+
name: wsName,
|
|
325
|
+
package: c.package,
|
|
291
326
|
path: c.localPath,
|
|
292
|
-
realpath: c.localPath,
|
|
293
327
|
resolved: c.resolved,
|
|
294
|
-
}
|
|
295
|
-
|
|
328
|
+
})
|
|
329
|
+
workspace.top = workspace
|
|
330
|
+
workspace.isWorkspace = true
|
|
331
|
+
root.fsChildren.add(workspace)
|
|
296
332
|
root.inventory.set(workspace.location, workspace)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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,
|
|
333
|
+
root.workspaces.set(wsName, workspace.path)
|
|
334
|
+
|
|
335
|
+
// Create workspace Link. For root declared deps, link at root node_modules/. For undeclared deps, link at the workspace's own node_modules/ (self-link).
|
|
336
|
+
const isDeclared = this.#rootDeclaredDeps.has(wsName)
|
|
337
|
+
const wsLink = new IsolatedLink({
|
|
338
|
+
location: isDeclared ? join('node_modules', wsName) : join(c.localLocation, 'node_modules', wsName),
|
|
339
|
+
name: wsName,
|
|
340
|
+
package: workspace.package,
|
|
341
|
+
parent: root,
|
|
342
|
+
path: isDeclared ? join(root.path, 'node_modules', wsName) : join(root.path, c.localLocation, 'node_modules', wsName),
|
|
343
|
+
realpath: workspace.path,
|
|
344
|
+
root,
|
|
345
|
+
target: workspace,
|
|
346
|
+
})
|
|
347
|
+
wsLink.top = root
|
|
348
|
+
if (!isDeclared) {
|
|
349
|
+
workspace.children.set(wsName, wsLink)
|
|
328
350
|
}
|
|
329
|
-
|
|
330
|
-
root.
|
|
331
|
-
|
|
351
|
+
root.children.set(wsName, wsLink)
|
|
352
|
+
root.inventory.set(wsLink.location, wsLink)
|
|
353
|
+
workspace.linksIn.add(wsLink)
|
|
332
354
|
}
|
|
333
|
-
|
|
355
|
+
|
|
356
|
+
this.idealGraph.external.forEach(c => {
|
|
334
357
|
const key = getKey(c)
|
|
335
358
|
if (processed.has(key)) {
|
|
336
359
|
return
|
|
337
360
|
}
|
|
338
361
|
processed.add(key)
|
|
339
|
-
const location = join('node_modules', '.store', key, 'node_modules', c.
|
|
340
|
-
generateChild(c, location, c.package, true)
|
|
362
|
+
const location = join('node_modules', '.store', key, 'node_modules', c.packageName)
|
|
363
|
+
this.#generateChild(c, location, c.package, true, root)
|
|
341
364
|
})
|
|
365
|
+
|
|
342
366
|
bundledTree.nodes.forEach(node => {
|
|
343
|
-
generateChild(node, node.location, node.pkg, false)
|
|
367
|
+
this.#generateChild(node, node.location, node.pkg, false, root)
|
|
344
368
|
})
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
369
|
+
|
|
370
|
+
bundledTree.edges.forEach(edge => {
|
|
371
|
+
const from = edge.from === 'root' ? root : root.inventory.get(edge.from)
|
|
372
|
+
const to = root.inventory.get(edge.to)
|
|
348
373
|
// Maybe optional should be propagated from the original edge
|
|
349
|
-
const
|
|
350
|
-
from.edgesOut.set(to.name,
|
|
351
|
-
to.edgesIn.add(
|
|
374
|
+
const newEdge = { optional: false, from, to }
|
|
375
|
+
from.edgesOut.set(to.name, newEdge)
|
|
376
|
+
to.edgesIn.add(newEdge)
|
|
352
377
|
})
|
|
353
|
-
const memo = new Set()
|
|
354
378
|
|
|
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
|
-
}
|
|
379
|
+
this.#processEdges(this.idealGraph, false, root)
|
|
380
|
+
for (const node of this.idealGraph.workspaces) {
|
|
381
|
+
this.#processEdges(node, false, root)
|
|
382
|
+
}
|
|
383
|
+
return root
|
|
384
|
+
}
|
|
372
385
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
386
|
+
#processEdges (node, externalEdge, root) {
|
|
387
|
+
const key = getKey(node)
|
|
388
|
+
if (this.#processedEdges.has(key)) {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
this.#processedEdges.add(key)
|
|
376
392
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
393
|
+
let from, nmFolder
|
|
394
|
+
if (externalEdge) {
|
|
395
|
+
const fromLocation = join('node_modules', '.store', key, 'node_modules', node.packageName)
|
|
396
|
+
from = root.children.get(fromLocation)
|
|
397
|
+
nmFolder = join('node_modules', '.store', key, 'node_modules')
|
|
398
|
+
} else {
|
|
399
|
+
from = node.isProjectRoot ? root : root.inventory.get(node.localLocation)
|
|
400
|
+
nmFolder = join(node.localLocation, 'node_modules')
|
|
401
|
+
}
|
|
402
|
+
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
|
|
403
|
+
if (!from) {
|
|
404
|
+
return
|
|
405
|
+
}
|
|
380
406
|
|
|
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
|
-
}
|
|
407
|
+
for (const dep of node.localDependencies) {
|
|
408
|
+
this.#processEdges(dep, false, root)
|
|
409
|
+
// nonOptional, local
|
|
410
|
+
this.#processDeps(dep, false, false, root, from, nmFolder)
|
|
411
|
+
}
|
|
412
|
+
for (const dep of node.externalDependencies) {
|
|
413
|
+
this.#processEdges(dep, true, root)
|
|
414
|
+
// nonOptional, external
|
|
415
|
+
this.#processDeps(dep, false, true, root, from, nmFolder)
|
|
416
|
+
}
|
|
417
|
+
for (const dep of node.externalOptionalDependencies) {
|
|
418
|
+
this.#processEdges(dep, true, root)
|
|
419
|
+
// optional, external
|
|
420
|
+
this.#processDeps(dep, true, true, root, from, nmFolder)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
425
423
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
424
|
+
#processDeps (dep, optional, external, root, from, nmFolder) {
|
|
425
|
+
const toKey = getKey(dep)
|
|
426
|
+
|
|
427
|
+
let target
|
|
428
|
+
if (external) {
|
|
429
|
+
const toLocation = join('node_modules', '.store', toKey, 'node_modules', dep.packageName)
|
|
430
|
+
target = root.children.get(toLocation)
|
|
431
|
+
} else {
|
|
432
|
+
target = root.inventory.get(dep.localLocation)
|
|
433
|
+
}
|
|
434
|
+
// TODO: we should no-op is an edge has already been created with the same fromKey and toKey
|
|
435
|
+
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
|
|
436
|
+
if (!target) {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (dep.package.bin) {
|
|
441
|
+
for (const bn in dep.package.bin) {
|
|
442
|
+
target.binPaths.push(join(dep.root.localPath, nmFolder, '.bin', bn))
|
|
440
443
|
}
|
|
441
444
|
}
|
|
442
445
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
+
const pkg = {
|
|
447
|
+
_id: dep.package._id,
|
|
448
|
+
bin: target.package.bin,
|
|
449
|
+
bundleDependencies: undefined,
|
|
450
|
+
deprecated: undefined,
|
|
451
|
+
scripts: dep.package.scripts,
|
|
452
|
+
version: dep.package.version,
|
|
446
453
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
454
|
+
const link = new IsolatedLink({
|
|
455
|
+
isStoreLink: true,
|
|
456
|
+
location: join(nmFolder, dep.name),
|
|
457
|
+
name: toKey,
|
|
458
|
+
optional,
|
|
459
|
+
parent: root,
|
|
460
|
+
package: pkg,
|
|
461
|
+
path: join(dep.root.localPath, nmFolder, dep.name),
|
|
462
|
+
realpath: target.path,
|
|
463
|
+
resolved: external ? `file:.store/${toKey}/node_modules/${dep.packageName}` : dep.resolved,
|
|
464
|
+
root,
|
|
465
|
+
target,
|
|
466
|
+
})
|
|
467
|
+
// XXX top is from place-dep not lib/link.js
|
|
468
|
+
link.top = { path: dep.root.localPath }
|
|
469
|
+
const newEdge1 = { optional, from, to: link }
|
|
470
|
+
from.edgesOut.set(dep.name, newEdge1)
|
|
471
|
+
link.edgesIn.add(newEdge1)
|
|
472
|
+
const newEdge2 = { optional: false, from: link, to: target }
|
|
473
|
+
link.edgesOut.set(dep.name, newEdge2)
|
|
474
|
+
target.edgesIn.add(newEdge2)
|
|
475
|
+
root.children.set(link.location, link)
|
|
452
476
|
}
|
|
453
477
|
}
|
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')
|
|
@@ -34,6 +36,7 @@ const { callLimit: promiseCallLimit } = require('promise-call-limit')
|
|
|
34
36
|
const optionalSet = require('../optional-set.js')
|
|
35
37
|
const calcDepFlags = require('../calc-dep-flags.js')
|
|
36
38
|
const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js')
|
|
39
|
+
const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js')
|
|
37
40
|
|
|
38
41
|
const Shrinkwrap = require('../shrinkwrap.js')
|
|
39
42
|
const { defaultLockfileVersion } = Shrinkwrap
|
|
@@ -77,8 +80,6 @@ const _usePackageLock = Symbol.for('usePackageLock')
|
|
|
77
80
|
// used by build-ideal-tree mixin
|
|
78
81
|
const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
|
|
79
82
|
|
|
80
|
-
const _createIsolatedTree = Symbol.for('createIsolatedTree')
|
|
81
|
-
|
|
82
83
|
module.exports = cls => class Reifier extends cls {
|
|
83
84
|
#bundleMissing = new Set() // child nodes we'd EXPECT to be included in a bundle, but aren't
|
|
84
85
|
#bundleUnpacked = new Set() // the nodes we unpack to read their bundles
|
|
@@ -93,6 +94,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
93
94
|
#shrinkwrapInflated = new Set()
|
|
94
95
|
#sparseTreeDirs = new Set()
|
|
95
96
|
#sparseTreeRoots = new Set()
|
|
97
|
+
#linkedActualForDiff = null
|
|
96
98
|
|
|
97
99
|
constructor (options) {
|
|
98
100
|
super(options)
|
|
@@ -136,16 +138,21 @@ module.exports = cls => class Reifier extends cls {
|
|
|
136
138
|
// this is currently technical debt which will be resolved in a refactor
|
|
137
139
|
// of Node/Link trees
|
|
138
140
|
log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.')
|
|
139
|
-
this.idealTree = await this
|
|
141
|
+
this.idealTree = await this.createIsolatedTree()
|
|
142
|
+
this.#linkedActualForDiff = this.#buildLinkedActualForDiff(
|
|
143
|
+
this.idealTree, this.actualTree
|
|
144
|
+
)
|
|
140
145
|
}
|
|
141
146
|
await this[_diffTrees]()
|
|
142
147
|
await this[_reifyPackages]()
|
|
143
148
|
if (linked) {
|
|
149
|
+
await this.#cleanOrphanedStoreEntries()
|
|
144
150
|
// swap back in the idealTree
|
|
145
151
|
// so that the lockfile is preserved
|
|
146
152
|
this.idealTree = oldTree
|
|
147
153
|
}
|
|
148
154
|
await this[_saveIdealTree](options)
|
|
155
|
+
this.#linkedActualForDiff = null
|
|
149
156
|
// clean up any trash that is still in the tree
|
|
150
157
|
for (const path of this[_trashList]) {
|
|
151
158
|
const loc = relpath(this.idealTree.realpath, path)
|
|
@@ -161,7 +168,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
161
168
|
// was not changed, delete anything in the ideal and not actual.
|
|
162
169
|
// Then we move the entire idealTree over to this.actualTree, and
|
|
163
170
|
// save the hidden lockfile.
|
|
164
|
-
if (this.diff && this.diff.filterSet.size) {
|
|
171
|
+
if (this.diff && this.diff.filterSet.size && !linked) {
|
|
165
172
|
const reroot = new Set()
|
|
166
173
|
|
|
167
174
|
const { filterSet } = this.diff
|
|
@@ -442,9 +449,14 @@ module.exports = cls => class Reifier extends cls {
|
|
|
442
449
|
if (ideal) {
|
|
443
450
|
filterNodes.push(ideal)
|
|
444
451
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
452
|
+
// Skip actual-side filterNodes when using the linked diff wrapper.
|
|
453
|
+
// Those nodes have root===actualTree, not root===linkedActualForDiff, and Diff.calculate requires filterNode.root to match actual.
|
|
454
|
+
// The ideal filterNode alone is sufficient to scope the workspace diff.
|
|
455
|
+
if (!this.#linkedActualForDiff) {
|
|
456
|
+
const actual = this.actualTree.children.get(ws)
|
|
457
|
+
if (actual) {
|
|
458
|
+
filterNodes.push(actual)
|
|
459
|
+
}
|
|
448
460
|
}
|
|
449
461
|
}
|
|
450
462
|
}
|
|
@@ -465,7 +477,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
465
477
|
this.diff = Diff.calculate({
|
|
466
478
|
shrinkwrapInflated: this.#shrinkwrapInflated,
|
|
467
479
|
filterNodes,
|
|
468
|
-
actual: this.actualTree,
|
|
480
|
+
actual: this.#linkedActualForDiff || this.actualTree,
|
|
469
481
|
ideal: this.idealTree,
|
|
470
482
|
})
|
|
471
483
|
|
|
@@ -625,6 +637,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
625
637
|
// if the directory already exists, made will be undefined. if that's the case
|
|
626
638
|
// we don't want to remove it because we aren't the ones who created it so we
|
|
627
639
|
// omit it from the #sparseTreeRoots
|
|
640
|
+
/* istanbul ignore next -- pre-existing: mkdir returns undefined when dir exists, covered in reify tests but lost in aggregate coverage merge */
|
|
628
641
|
if (made) {
|
|
629
642
|
this.#sparseTreeRoots.add(made)
|
|
630
643
|
}
|
|
@@ -824,6 +837,89 @@ module.exports = cls => class Reifier extends cls {
|
|
|
824
837
|
}) : p).then(() => node)
|
|
825
838
|
}
|
|
826
839
|
|
|
840
|
+
// Build a flat actual tree wrapper for linked installs so the diff can
|
|
841
|
+
// correctly match store entries that already exist on disk.
|
|
842
|
+
#buildLinkedActualForDiff (idealTree, actualTree) {
|
|
843
|
+
const combined = new Map()
|
|
844
|
+
|
|
845
|
+
// Create synthetic actual entries for ALL ideal children that exist on disk.
|
|
846
|
+
// The isolated ideal tree is flat (all entries as root children), but loadActual() produces a nested tree where workspace deps are under fsChildren and store entries are deep link targets.
|
|
847
|
+
// Synthetic entries ensure the diff compares matching resolved/integrity values (e.g. workspace links have resolved=undefined in the ideal tree but resolved="file:../packages/..." in the actual tree).
|
|
848
|
+
for (const child of idealTree.children.values()) {
|
|
849
|
+
if (combined.has(child.path) || !existsSync(child.path)) {
|
|
850
|
+
continue
|
|
851
|
+
}
|
|
852
|
+
let entry
|
|
853
|
+
if (child.isLink) {
|
|
854
|
+
entry = new IsolatedLink(child)
|
|
855
|
+
} else {
|
|
856
|
+
entry = new IsolatedNode(child)
|
|
857
|
+
}
|
|
858
|
+
if (child.isLink && combined.has(child.realpath)) {
|
|
859
|
+
entry.target = combined.get(child.realpath)
|
|
860
|
+
}
|
|
861
|
+
combined.set(child.path, entry)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const origGet = actualTree.children.get.bind(actualTree.children)
|
|
865
|
+
const combinedGet = combined.get.bind(combined)
|
|
866
|
+
/* istanbul ignore next -- only reached during scoped workspace installs */
|
|
867
|
+
combined.get = (key) => combinedGet(key) || origGet(key)
|
|
868
|
+
|
|
869
|
+
let wrapper
|
|
870
|
+
/* istanbul ignore next - untested! */
|
|
871
|
+
if (actualTree.isLink) {
|
|
872
|
+
wrapper = new IsolatedLink(actualTree)
|
|
873
|
+
} else {
|
|
874
|
+
wrapper = new IsolatedNode(actualTree)
|
|
875
|
+
}
|
|
876
|
+
wrapper.root = wrapper
|
|
877
|
+
wrapper.binPaths = actualTree.binPaths
|
|
878
|
+
wrapper.children = combined
|
|
879
|
+
wrapper.edgesOut = actualTree.edgesOut
|
|
880
|
+
// Use empty fsChildren so that allChildren() only picks up entries from the combined map.
|
|
881
|
+
// The actual fsChildren have real children with different resolved values (e.g. file:../../../node_modules/.store/... vs file:.store/...) that would overwrite our synthetic entries in allChildren().
|
|
882
|
+
wrapper.fsChildren = new Set()
|
|
883
|
+
wrapper.integrity = actualTree.integrity
|
|
884
|
+
wrapper.inventory = actualTree.inventory
|
|
885
|
+
|
|
886
|
+
return wrapper
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// After a linked install, scan node_modules/.store/ and remove any
|
|
890
|
+
// directories that are not referenced by the current ideal tree.
|
|
891
|
+
async #cleanOrphanedStoreEntries () {
|
|
892
|
+
const storeDir = resolve(this.path, 'node_modules', '.store')
|
|
893
|
+
let entries
|
|
894
|
+
try {
|
|
895
|
+
entries = await readdir(storeDir)
|
|
896
|
+
} catch {
|
|
897
|
+
return
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const validKeys = new Set()
|
|
901
|
+
for (const child of this.idealTree.children.values()) {
|
|
902
|
+
if (child.isInStore) {
|
|
903
|
+
const key = child.location.split(sep)[2]
|
|
904
|
+
validKeys.add(key)
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const orphaned = entries.filter(e => !validKeys.has(e))
|
|
909
|
+
if (!orphaned.length) {
|
|
910
|
+
return
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
log.silly('reify', 'cleaning orphaned store entries', orphaned)
|
|
914
|
+
await promiseAllRejectLate(
|
|
915
|
+
orphaned.map(e =>
|
|
916
|
+
rm(resolve(storeDir, e), { recursive: true, force: true })
|
|
917
|
+
.catch(/* istanbul ignore next -- rm with force rarely fails */
|
|
918
|
+
er => log.warn('cleanup', `Failed to remove orphaned store entry ${e}`, er))
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
|
|
827
923
|
#registryResolved (resolved) {
|
|
828
924
|
// the default registry url is a magic value meaning "the currently
|
|
829
925
|
// 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/lib/inventory.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// a class to manage an inventory and set of indexes of a set of objects based
|
|
2
|
-
// on specific fields.
|
|
1
|
+
// a class to manage an inventory and set of indexes of a set of objects based on specific fields.
|
|
3
2
|
const { hasOwnProperty } = Object.prototype
|
|
4
3
|
const debug = require('./debug.js')
|
|
5
4
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Alternate versions of different classes that we use for isolated mode
|
|
2
|
+
const CaseInsensitiveMap = require('./case-insensitive-map.js')
|
|
3
|
+
const { resolve } = require('node:path')
|
|
4
|
+
|
|
5
|
+
// fake lib/inventory.js
|
|
6
|
+
class IsolatedInventory extends Map {
|
|
7
|
+
query () {
|
|
8
|
+
return []
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// fake lib/node.js
|
|
13
|
+
class IsolatedNode {
|
|
14
|
+
binPaths = []
|
|
15
|
+
children = new CaseInsensitiveMap()
|
|
16
|
+
edgesIn = new Set()
|
|
17
|
+
edgesOut = new CaseInsensitiveMap()
|
|
18
|
+
fsChildren = new Set()
|
|
19
|
+
hasShrinkwrap = false
|
|
20
|
+
integrity = null
|
|
21
|
+
inventory = new IsolatedInventory()
|
|
22
|
+
isInStore = false
|
|
23
|
+
linksIn = new Set()
|
|
24
|
+
meta = { loadedFromDisk: false }
|
|
25
|
+
optional = false
|
|
26
|
+
parent = null
|
|
27
|
+
root = null
|
|
28
|
+
tops = new Set()
|
|
29
|
+
workspaces = new Map()
|
|
30
|
+
|
|
31
|
+
constructor (options) {
|
|
32
|
+
this.location = options.location
|
|
33
|
+
this.name = options.name
|
|
34
|
+
this.package = options.package
|
|
35
|
+
this.path = options.path
|
|
36
|
+
this.realpath = !this.isLink ? this.path : resolve(options.realpath)
|
|
37
|
+
|
|
38
|
+
if (options.parent) {
|
|
39
|
+
this.parent = options.parent
|
|
40
|
+
}
|
|
41
|
+
if (options.resolved) {
|
|
42
|
+
this.resolved = options.resolved
|
|
43
|
+
}
|
|
44
|
+
if (options.root) {
|
|
45
|
+
this.root = options.root
|
|
46
|
+
}
|
|
47
|
+
if (options.isInStore) {
|
|
48
|
+
this.isInStore = true
|
|
49
|
+
}
|
|
50
|
+
if (options.optional) {
|
|
51
|
+
this.optional = true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get isRoot () {
|
|
56
|
+
return this === this.root
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The idealGraph is where this is set to true
|
|
60
|
+
get isProjectRoot () {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get inDepBundle () {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get isLink () {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get isTop () {
|
|
73
|
+
return !this.parent
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* istanbul ignore next -- emulate lib/node.js */
|
|
77
|
+
get global () {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get globalTop () {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* istanbul ignore next -- emulate lib/node.js */
|
|
86
|
+
set target (t) {
|
|
87
|
+
// nop
|
|
88
|
+
// In the real lib/node.js this throws in debug mode
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get target () {
|
|
92
|
+
return this
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* istanbul ignore next -- emulate lib/node.js */
|
|
96
|
+
getBundler () {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* istanbul ignore next -- emulate lib/node.js */
|
|
101
|
+
get hasInstallScript () {
|
|
102
|
+
const { hasInstallScript, scripts } = this.package
|
|
103
|
+
const { install, preinstall, postinstall } = scripts || {}
|
|
104
|
+
return !!(hasInstallScript || install || preinstall || postinstall)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get version () {
|
|
108
|
+
return this.package.version
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// fake lib/link.js
|
|
113
|
+
class IsolatedLink extends IsolatedNode {
|
|
114
|
+
#target
|
|
115
|
+
isStoreLink = false
|
|
116
|
+
|
|
117
|
+
constructor (options) {
|
|
118
|
+
super(options)
|
|
119
|
+
this.#target = options.target
|
|
120
|
+
if (options.isStoreLink) {
|
|
121
|
+
this.isStoreLink = true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get isLink () {
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
set target (t) {
|
|
130
|
+
this.#target = t
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get target () {
|
|
134
|
+
return this.#target
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { IsolatedNode, IsolatedLink }
|
package/lib/node.js
CHANGED
|
@@ -74,33 +74,32 @@ class Node {
|
|
|
74
74
|
constructor (options) {
|
|
75
75
|
// NB: path can be null if it's a link target
|
|
76
76
|
const {
|
|
77
|
-
root,
|
|
78
|
-
path,
|
|
79
|
-
realpath,
|
|
80
|
-
parent,
|
|
81
|
-
error,
|
|
82
|
-
meta,
|
|
83
|
-
fsParent,
|
|
84
|
-
resolved,
|
|
85
|
-
integrity,
|
|
86
|
-
// allow setting name explicitly when we haven't set a path yet
|
|
87
|
-
name,
|
|
88
77
|
children,
|
|
78
|
+
dev = true,
|
|
79
|
+
devOptional = true,
|
|
80
|
+
dummy = false,
|
|
81
|
+
error,
|
|
82
|
+
extraneous = true,
|
|
89
83
|
fsChildren,
|
|
84
|
+
fsParent,
|
|
85
|
+
global = false,
|
|
86
|
+
hasShrinkwrap,
|
|
90
87
|
installLinks = false,
|
|
88
|
+
integrity,
|
|
89
|
+
isInStore = false,
|
|
91
90
|
legacyPeerDeps = false,
|
|
92
91
|
linksIn,
|
|
93
|
-
isInStore = false,
|
|
94
|
-
hasShrinkwrap,
|
|
95
|
-
overrides,
|
|
96
92
|
loadOverrides = false,
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
meta,
|
|
94
|
+
name, // allow setting name explicitly when we haven't set a path yet
|
|
99
95
|
optional = true,
|
|
100
|
-
|
|
96
|
+
overrides,
|
|
97
|
+
parent,
|
|
98
|
+
path,
|
|
101
99
|
peer = true,
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
realpath,
|
|
101
|
+
resolved,
|
|
102
|
+
root,
|
|
104
103
|
sourceReference = null,
|
|
105
104
|
} = options
|
|
106
105
|
// this object gives querySelectorAll somewhere to stash context about a node
|
|
@@ -464,6 +463,27 @@ class Node {
|
|
|
464
463
|
return false
|
|
465
464
|
}
|
|
466
465
|
|
|
466
|
+
shouldOmit (omitSet) {
|
|
467
|
+
if (!omitSet.size) {
|
|
468
|
+
return false
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const { top } = this
|
|
472
|
+
|
|
473
|
+
// if the top is not the root or workspace then we do not want to omit it
|
|
474
|
+
if (!top.isProjectRoot && !top.isWorkspace) {
|
|
475
|
+
return false
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// omit node if the dep type matches any omit flags that were set
|
|
479
|
+
return (
|
|
480
|
+
this.peer && omitSet.has('peer') ||
|
|
481
|
+
this.dev && omitSet.has('dev') ||
|
|
482
|
+
this.optional && omitSet.has('optional') ||
|
|
483
|
+
this.devOptional && omitSet.has('optional') && omitSet.has('dev')
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
467
487
|
getBundler (path = []) {
|
|
468
488
|
// made a cycle, definitely not bundled!
|
|
469
489
|
if (path.includes(this)) {
|
|
@@ -785,9 +785,14 @@ const hasParent = (node, compareNodes) => {
|
|
|
785
785
|
compareNode = compareNode.target
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
-
//
|
|
788
|
+
// Follows logical parent for link ancestors (e.g. workspaces whose target lives outside node_modules).
|
|
789
|
+
// Only match if the node has a link whose parent is the compareNode. Without this check, nodes deep in the store (linked strategy) would incorrectly match as children of root via their fsParent chain.
|
|
789
790
|
if (node.isTop && (node.resolveParent === compareNode)) {
|
|
790
|
-
|
|
791
|
+
for (const link of node.linksIn) {
|
|
792
|
+
if (link.parent === compareNode) {
|
|
793
|
+
return true
|
|
794
|
+
}
|
|
795
|
+
}
|
|
791
796
|
}
|
|
792
797
|
// follows edges-in to check if they match a possible parent
|
|
793
798
|
for (const edge of node.edgesIn) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.3",
|
|
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
|
}
|