@npmcli/arborist 9.4.0 → 9.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/arborist/isolated-reifier.js +301 -306
- package/lib/arborist/rebuild.js +10 -2
- package/lib/arborist/reify.js +111 -11
- package/lib/audit-report.js +2 -0
- package/lib/diff.js +7 -1
- package/lib/inventory.js +1 -2
- package/lib/isolated-classes.js +138 -0
- package/lib/node.js +19 -20
- package/lib/query-selector-all.js +7 -2
- package/package.json +2 -1
|
@@ -1,71 +1,103 @@
|
|
|
1
|
-
const _makeIdealGraph = Symbol('makeIdealGraph')
|
|
2
|
-
const _createIsolatedTree = Symbol.for('createIsolatedTree')
|
|
3
1
|
const { mkdirSync } = require('node:fs')
|
|
4
2
|
const pacote = require('pacote')
|
|
5
3
|
const { join } = require('node:path')
|
|
6
4
|
const { depth } = require('treeverse')
|
|
7
5
|
const crypto = require('node:crypto')
|
|
6
|
+
const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js')
|
|
8
7
|
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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}`
|
|
22
33
|
}
|
|
23
34
|
|
|
24
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
|
+
|
|
25
60
|
/**
|
|
26
61
|
* Create an ideal graph.
|
|
27
62
|
*
|
|
28
63
|
* An implementation of npm RFC-0042
|
|
29
64
|
* https://github.com/npm/rfcs/blob/main/accepted/0042-isolated-mode.md
|
|
30
65
|
*
|
|
31
|
-
* This entire file should be considered technical debt that will be resolved
|
|
32
|
-
*
|
|
33
|
-
* and the incremental state of building trees and reifying contains too many
|
|
34
|
-
* 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.
|
|
35
68
|
*
|
|
36
|
-
* Instead, this approach takes a tree built from build-ideal-tree, and
|
|
37
|
-
* returns a new tree-like structure without the embedded logic of Node and
|
|
38
|
-
* 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.
|
|
39
70
|
*
|
|
40
|
-
* Since the RFC requires leaving the package-lock in place, this approach
|
|
41
|
-
* 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.
|
|
42
72
|
*
|
|
43
73
|
**/
|
|
44
|
-
async
|
|
45
|
-
/* Make sure that the ideal tree is build as the rest of
|
|
46
|
-
* the algorithm depends on it.
|
|
47
|
-
*/
|
|
48
|
-
const bitOpt = {
|
|
49
|
-
...options,
|
|
50
|
-
complete: false,
|
|
51
|
-
}
|
|
52
|
-
await this.buildIdealTree(bitOpt)
|
|
74
|
+
async makeIdealGraph () {
|
|
53
75
|
const idealTree = this.idealTree
|
|
76
|
+
this.#omit = new Set(this.options.omit)
|
|
77
|
+
const omit = this.#omit
|
|
54
78
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
)))
|
|
58
89
|
|
|
59
|
-
//
|
|
60
|
-
this.
|
|
61
|
-
|
|
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
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
root.isProjectRoot = true
|
|
65
|
-
root.localLocation = idealTree.location
|
|
66
|
-
root.localPath = idealTree.path
|
|
67
|
-
root.workspaces = await Promise.all(
|
|
68
|
-
Array.from(idealTree.fsChildren.values(), this.workspaceProxyMemo))
|
|
100
|
+
this.idealGraph.workspaces = await Promise.all(Array.from(idealTree.fsChildren.values(), w => this.#workspaceProxy(w)))
|
|
69
101
|
const processed = new Set()
|
|
70
102
|
const queue = [idealTree, ...idealTree.fsChildren]
|
|
71
103
|
while (queue.length !== 0) {
|
|
@@ -74,32 +106,44 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
74
106
|
continue
|
|
75
107
|
}
|
|
76
108
|
processed.add(next.location)
|
|
77
|
-
next.edgesOut.forEach(
|
|
78
|
-
if (
|
|
79
|
-
|
|
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)
|
|
80
112
|
}
|
|
81
|
-
queue.push(e.to)
|
|
82
113
|
})
|
|
83
|
-
|
|
84
|
-
|
|
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))
|
|
85
118
|
}
|
|
86
119
|
}
|
|
87
120
|
|
|
88
|
-
await this
|
|
89
|
-
|
|
90
|
-
this.idealGraph = root
|
|
121
|
+
await this.#assignCommonProperties(idealTree, this.idealGraph)
|
|
91
122
|
}
|
|
92
123
|
|
|
93
|
-
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)
|
|
94
131
|
result.localLocation = node.location
|
|
95
132
|
result.localPath = node.path
|
|
96
133
|
result.isWorkspace = true
|
|
97
134
|
result.resolved = node.resolved
|
|
98
|
-
await this
|
|
135
|
+
await this.#assignCommonProperties(node, result)
|
|
136
|
+
return result
|
|
99
137
|
}
|
|
100
138
|
|
|
101
|
-
async externalProxy (
|
|
102
|
-
|
|
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)
|
|
103
147
|
if (node.hasShrinkwrap) {
|
|
104
148
|
const dir = join(
|
|
105
149
|
node.root.path,
|
|
@@ -108,8 +152,7 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
108
152
|
`${node.packageName}@${node.version}`
|
|
109
153
|
)
|
|
110
154
|
mkdirSync(dir, { recursive: true })
|
|
111
|
-
// TODO this approach feels wrong
|
|
112
|
-
// and shouldn't be necessary for shrinkwraps
|
|
155
|
+
// TODO this approach feels wrong and shouldn't be necessary for shrinkwraps
|
|
113
156
|
await pacote.extract(node.resolved, dir, {
|
|
114
157
|
...this.options,
|
|
115
158
|
resolved: node.resolved,
|
|
@@ -117,65 +160,100 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
117
160
|
})
|
|
118
161
|
const Arborist = this.constructor
|
|
119
162
|
const arb = new Arborist({ ...this.options, path: dir })
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
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,
|
|
125
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
|
+
}
|
|
126
174
|
result.localDependencies = []
|
|
127
175
|
result.externalDependencies = arb.idealGraph.externalDependencies
|
|
128
176
|
result.externalOptionalDependencies = arb.idealGraph.externalOptionalDependencies
|
|
129
177
|
result.dependencies = [
|
|
130
178
|
...result.externalDependencies,
|
|
131
|
-
...result.localDependencies,
|
|
132
179
|
...result.externalOptionalDependencies,
|
|
133
180
|
]
|
|
134
181
|
}
|
|
135
182
|
result.optional = node.optional
|
|
136
183
|
result.resolved = node.resolved
|
|
137
184
|
result.version = node.version
|
|
185
|
+
return result
|
|
138
186
|
}
|
|
139
187
|
|
|
140
|
-
async assignCommonProperties (node, result) {
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
result.packageName = node.packageName || node.name
|
|
195
|
+
result.package = { ...node.package }
|
|
196
|
+
result.package.bundleDependencies = undefined
|
|
197
|
+
|
|
198
|
+
if (!populateDeps) {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let edges = [...node.edgesOut.values()].filter(edge =>
|
|
203
|
+
edge.to?.target &&
|
|
204
|
+
!(node.package.bundledDependencies || node.package.bundleDependencies)?.includes(edge.to.name)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// Only omit edge types for root and workspace nodes (matching shouldOmit scope)
|
|
208
|
+
if ((node.isProjectRoot || node.isWorkspace) && this.#omit.size) {
|
|
209
|
+
edges = edges.filter(edge => {
|
|
210
|
+
if (edge.dev && this.#omit.has('dev')) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
if (edge.optional && this.#omit.has('optional')) {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
if (edge.peer && this.#omit.has('peer')) {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
return true
|
|
220
|
+
})
|
|
143
221
|
}
|
|
144
|
-
const edges = validEdgesOut(node)
|
|
145
|
-
const optionalDeps = edges.filter(e => e.optional).map(e => e.to.target)
|
|
146
|
-
const nonOptionalDeps = edges.filter(e => !e.optional).map(e => e.to.target)
|
|
147
222
|
|
|
148
|
-
|
|
149
|
-
|
|
223
|
+
let nonOptionalDeps = edges.filter(edge => !edge.optional).map(edge => edge.to.target)
|
|
224
|
+
|
|
225
|
+
// npm auto-creates 'workspace' edges from root to all workspaces.
|
|
226
|
+
// For isolated/linked mode, only include workspaces that root explicitly declares as dependencies.
|
|
227
|
+
if (node.isProjectRoot) {
|
|
228
|
+
nonOptionalDeps = nonOptionalDeps.filter(n => !n.isWorkspace || this.#rootDeclaredDeps.has(n.packageName))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// When legacyPeerDeps is enabled, peer dep edges are not created on the node.
|
|
232
|
+
// Resolve them from the tree so they get symlinked in the store.
|
|
150
233
|
const peerDeps = node.package.peerDependencies
|
|
151
234
|
if (peerDeps && node.legacyPeerDeps) {
|
|
152
|
-
const edgeNames = new Set(edges.map(
|
|
153
|
-
for (const peerName
|
|
235
|
+
const edgeNames = new Set(edges.map(edge => edge.name))
|
|
236
|
+
for (const peerName in peerDeps) {
|
|
154
237
|
if (!edgeNames.has(peerName)) {
|
|
155
238
|
const resolved = node.resolve(peerName)
|
|
156
239
|
if (resolved && resolved !== node && !resolved.inert) {
|
|
157
|
-
nonOptionalDeps.push(resolved)
|
|
240
|
+
nonOptionalDeps.push(resolved.target)
|
|
158
241
|
}
|
|
159
242
|
}
|
|
160
243
|
}
|
|
161
244
|
}
|
|
162
245
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
246
|
+
// 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.
|
|
247
|
+
const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n)
|
|
248
|
+
const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target)
|
|
249
|
+
result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.#workspaceProxy(n)))
|
|
250
|
+
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.#externalProxy(n)))
|
|
251
|
+
result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(n => this.#externalProxy(n)))
|
|
166
252
|
result.dependencies = [
|
|
167
253
|
...result.externalDependencies,
|
|
168
254
|
...result.localDependencies,
|
|
169
255
|
...result.externalOptionalDependencies,
|
|
170
256
|
]
|
|
171
|
-
result.root = this.rootNode
|
|
172
|
-
result.id = this.counter++
|
|
173
|
-
/* istanbul ignore next - packageName is always set for real packages */
|
|
174
|
-
result.name = result.isWorkspace ? (node.packageName || node.name) : node.name
|
|
175
|
-
result.packageName = node.packageName || node.name
|
|
176
|
-
result.package = { ...node.package }
|
|
177
|
-
result.package.bundleDependencies = undefined
|
|
178
|
-
result.hasInstallScript = node.hasInstallScript
|
|
179
257
|
}
|
|
180
258
|
|
|
181
259
|
async #createBundledTree () {
|
|
@@ -217,261 +295,178 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
217
295
|
nodes.set(to.location, { location: to.location, resolved: to.resolved, name: to.name, optional: to.optional, pkg: { ...to.package, bundleDependencies: undefined } })
|
|
218
296
|
edges.push({ from: from.isRoot ? 'root' : from.location, to: to.location })
|
|
219
297
|
|
|
220
|
-
to.edgesOut.forEach(
|
|
298
|
+
to.edgesOut.forEach(edge => {
|
|
221
299
|
// an edge out should always have a to
|
|
222
300
|
/* istanbul ignore else */
|
|
223
|
-
if (
|
|
224
|
-
queue.push({ from:
|
|
301
|
+
if (edge.to) {
|
|
302
|
+
queue.push({ from: edge.from, to: edge.to })
|
|
225
303
|
}
|
|
226
304
|
})
|
|
227
305
|
}
|
|
228
306
|
return { edges, nodes }
|
|
229
307
|
}
|
|
230
308
|
|
|
231
|
-
async
|
|
232
|
-
await this
|
|
233
|
-
|
|
234
|
-
const proxiedIdealTree = this.idealGraph
|
|
235
|
-
|
|
309
|
+
async createIsolatedTree () {
|
|
310
|
+
await this.makeIdealGraph()
|
|
236
311
|
const bundledTree = await this.#createBundledTree()
|
|
237
312
|
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const deps = []
|
|
242
|
-
const branch = []
|
|
243
|
-
depth({
|
|
244
|
-
tree: startNode,
|
|
245
|
-
getChildren: node => node.dependencies,
|
|
246
|
-
filter: node => node,
|
|
247
|
-
visit: node => {
|
|
248
|
-
branch.push(`${node.packageName}@${node.version}`)
|
|
249
|
-
deps.push(`${branch.join('->')}::${node.resolved}`)
|
|
250
|
-
},
|
|
251
|
-
leave: () => {
|
|
252
|
-
branch.pop()
|
|
253
|
-
},
|
|
254
|
-
})
|
|
255
|
-
deps.sort()
|
|
256
|
-
return crypto.createHash('shake256', { outputLength: 16 })
|
|
257
|
-
.update(deps.join(','))
|
|
258
|
-
.digest('base64')
|
|
259
|
-
// Node v14 doesn't support base64url
|
|
260
|
-
.replace(/\+/g, '-')
|
|
261
|
-
.replace(/\//g, '_')
|
|
262
|
-
.replace(/=+$/m, '')
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const getKey = (idealTreeNode) => {
|
|
266
|
-
return `${idealTreeNode.packageName}@${idealTreeNode.version}-${treeHash(idealTreeNode)}`
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const root = {
|
|
270
|
-
fsChildren: [],
|
|
271
|
-
integrity: null,
|
|
272
|
-
inventory: new Map(),
|
|
273
|
-
isLink: false,
|
|
274
|
-
isRoot: true,
|
|
275
|
-
binPaths: [],
|
|
276
|
-
edgesIn: new Set(),
|
|
277
|
-
edgesOut: new Map(),
|
|
278
|
-
hasShrinkwrap: false,
|
|
279
|
-
parent: null,
|
|
280
|
-
// TODO: we should probably not reference this.idealTree
|
|
281
|
-
resolved: this.idealTree.resolved,
|
|
282
|
-
isTop: true,
|
|
283
|
-
path: proxiedIdealTree.root.localPath,
|
|
284
|
-
realpath: proxiedIdealTree.root.localPath,
|
|
285
|
-
package: proxiedIdealTree.root.package,
|
|
286
|
-
meta: { loadedFromDisk: false },
|
|
287
|
-
global: false,
|
|
288
|
-
isProjectRoot: true,
|
|
289
|
-
children: [],
|
|
290
|
-
}
|
|
291
|
-
// root.inventory.set('', t)
|
|
292
|
-
// root.meta = this.idealTree.meta
|
|
293
|
-
// TODO We should mock better the inventory object because it is used by audit-report.js ... maybe
|
|
294
|
-
root.inventory.query = () => {
|
|
295
|
-
return []
|
|
296
|
-
}
|
|
313
|
+
const root = new IsolatedNode(this.idealGraph)
|
|
314
|
+
root.root = root
|
|
315
|
+
root.inventory.set('', root)
|
|
297
316
|
const processed = new Set()
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
children: [],
|
|
303
|
-
hasInstallScript: c.hasInstallScript,
|
|
304
|
-
binPaths: [],
|
|
305
|
-
package: c.package,
|
|
317
|
+
for (const c of this.idealGraph.workspaces) {
|
|
318
|
+
const wsName = c.packageName
|
|
319
|
+
// XXX parent? root?
|
|
320
|
+
const workspace = new IsolatedNode({
|
|
306
321
|
location: c.localLocation,
|
|
322
|
+
name: wsName,
|
|
323
|
+
package: c.package,
|
|
307
324
|
path: c.localPath,
|
|
308
|
-
realpath: c.localPath,
|
|
309
325
|
resolved: c.resolved,
|
|
310
|
-
}
|
|
311
|
-
root.fsChildren.
|
|
326
|
+
})
|
|
327
|
+
root.fsChildren.add(workspace)
|
|
312
328
|
root.inventory.set(workspace.location, workspace)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
/* istanbul ignore next -- emulate Node */
|
|
330
|
-
getBundler () {
|
|
331
|
-
return null
|
|
332
|
-
},
|
|
333
|
-
hasShrinkwrap: false,
|
|
334
|
-
inDepBundle: false,
|
|
335
|
-
integrity: null,
|
|
336
|
-
isLink: false,
|
|
337
|
-
isRoot: false,
|
|
338
|
-
isInStore: inStore,
|
|
339
|
-
path: join(proxiedIdealTree.root.localPath, location),
|
|
340
|
-
realpath: join(proxiedIdealTree.root.localPath, location),
|
|
341
|
-
resolved: node.resolved,
|
|
342
|
-
version: pkg.version,
|
|
343
|
-
package: pkg,
|
|
329
|
+
root.workspaces.set(wsName, workspace.path)
|
|
330
|
+
|
|
331
|
+
// 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).
|
|
332
|
+
const isDeclared = this.#rootDeclaredDeps.has(wsName)
|
|
333
|
+
const wsLink = new IsolatedLink({
|
|
334
|
+
location: isDeclared ? join('node_modules', wsName) : join(c.localLocation, 'node_modules', wsName),
|
|
335
|
+
name: wsName,
|
|
336
|
+
package: workspace.package,
|
|
337
|
+
parent: root,
|
|
338
|
+
path: isDeclared ? join(root.path, 'node_modules', wsName) : join(root.path, c.localLocation, 'node_modules', wsName),
|
|
339
|
+
realpath: workspace.path,
|
|
340
|
+
root,
|
|
341
|
+
target: workspace,
|
|
342
|
+
})
|
|
343
|
+
if (!isDeclared) {
|
|
344
|
+
workspace.children.set(wsName, wsLink)
|
|
344
345
|
}
|
|
345
|
-
|
|
346
|
-
root.
|
|
347
|
-
|
|
346
|
+
root.children.set(wsName, wsLink)
|
|
347
|
+
root.inventory.set(wsLink.location, wsLink)
|
|
348
|
+
workspace.linksIn.add(wsLink)
|
|
348
349
|
}
|
|
349
|
-
|
|
350
|
+
|
|
351
|
+
this.idealGraph.external.forEach(c => {
|
|
350
352
|
const key = getKey(c)
|
|
351
353
|
if (processed.has(key)) {
|
|
352
354
|
return
|
|
353
355
|
}
|
|
354
356
|
processed.add(key)
|
|
355
357
|
const location = join('node_modules', '.store', key, 'node_modules', c.packageName)
|
|
356
|
-
generateChild(c, location, c.package, true)
|
|
358
|
+
this.#generateChild(c, location, c.package, true, root)
|
|
357
359
|
})
|
|
360
|
+
|
|
358
361
|
bundledTree.nodes.forEach(node => {
|
|
359
|
-
generateChild(node, node.location, node.pkg, false)
|
|
362
|
+
this.#generateChild(node, node.location, node.pkg, false, root)
|
|
360
363
|
})
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
+
|
|
365
|
+
bundledTree.edges.forEach(edge => {
|
|
366
|
+
const from = edge.from === 'root' ? root : root.inventory.get(edge.from)
|
|
367
|
+
const to = root.inventory.get(edge.to)
|
|
364
368
|
// Maybe optional should be propagated from the original edge
|
|
365
|
-
const
|
|
366
|
-
from.edgesOut.set(to.name,
|
|
367
|
-
to.edgesIn.add(
|
|
369
|
+
const newEdge = { optional: false, from, to }
|
|
370
|
+
from.edgesOut.set(to.name, newEdge)
|
|
371
|
+
to.edgesIn.add(newEdge)
|
|
368
372
|
})
|
|
369
|
-
const memo = new Set()
|
|
370
373
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
memo.add(key)
|
|
378
|
-
|
|
379
|
-
let from, nmFolder
|
|
380
|
-
if (externalEdge) {
|
|
381
|
-
const fromLocation = join('node_modules', '.store', key, 'node_modules', node.packageName)
|
|
382
|
-
from = root.children.find(c => c.location === fromLocation)
|
|
383
|
-
nmFolder = join('node_modules', '.store', key, 'node_modules')
|
|
384
|
-
} else {
|
|
385
|
-
from = node.isProjectRoot ? root : root.fsChildren.find(c => c.location === node.localLocation)
|
|
386
|
-
nmFolder = join(node.localLocation, 'node_modules')
|
|
387
|
-
}
|
|
388
|
-
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
|
|
389
|
-
if (!from) {
|
|
390
|
-
return
|
|
391
|
-
}
|
|
374
|
+
this.#processEdges(this.idealGraph, false, root)
|
|
375
|
+
for (const node of this.idealGraph.workspaces) {
|
|
376
|
+
this.#processEdges(node, false, root)
|
|
377
|
+
}
|
|
378
|
+
return root
|
|
379
|
+
}
|
|
392
380
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
381
|
+
#processEdges (node, externalEdge, root) {
|
|
382
|
+
const key = getKey(node)
|
|
383
|
+
if (this.#processedEdges.has(key)) {
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
this.#processedEdges.add(key)
|
|
396
387
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
388
|
+
let from, nmFolder
|
|
389
|
+
if (externalEdge) {
|
|
390
|
+
const fromLocation = join('node_modules', '.store', key, 'node_modules', node.packageName)
|
|
391
|
+
from = root.children.get(fromLocation)
|
|
392
|
+
nmFolder = join('node_modules', '.store', key, 'node_modules')
|
|
393
|
+
} else {
|
|
394
|
+
from = node.isProjectRoot ? root : root.inventory.get(node.localLocation)
|
|
395
|
+
nmFolder = join(node.localLocation, 'node_modules')
|
|
396
|
+
}
|
|
397
|
+
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
|
|
398
|
+
if (!from) {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
400
401
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
402
|
+
for (const dep of node.localDependencies) {
|
|
403
|
+
this.#processEdges(dep, false, root)
|
|
404
|
+
// nonOptional, local
|
|
405
|
+
this.#processDeps(dep, false, false, root, from, nmFolder)
|
|
406
|
+
}
|
|
407
|
+
for (const dep of node.externalDependencies) {
|
|
408
|
+
this.#processEdges(dep, true, root)
|
|
409
|
+
// nonOptional, external
|
|
410
|
+
this.#processDeps(dep, false, true, root, from, nmFolder)
|
|
411
|
+
}
|
|
412
|
+
for (const dep of node.externalOptionalDependencies) {
|
|
413
|
+
this.#processEdges(dep, true, root)
|
|
414
|
+
// optional, external
|
|
415
|
+
this.#processDeps(dep, true, true, root, from, nmFolder)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
413
418
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
const link = {
|
|
419
|
-
global: false,
|
|
420
|
-
globalTop: false,
|
|
421
|
-
isProjectRoot: false,
|
|
422
|
-
edgesIn: new Set(),
|
|
423
|
-
edgesOut: new Map(),
|
|
424
|
-
binPaths: [],
|
|
425
|
-
isTop: false,
|
|
426
|
-
optional,
|
|
427
|
-
location: location,
|
|
428
|
-
path: join(dep.root.localPath, nmFolder, dep.name),
|
|
429
|
-
realpath: target.path,
|
|
430
|
-
name: toKey,
|
|
431
|
-
resolved: dep.resolved,
|
|
432
|
-
top: { path: dep.root.localPath },
|
|
433
|
-
children: [],
|
|
434
|
-
fsChildren: [],
|
|
435
|
-
isLink: true,
|
|
436
|
-
isStoreLink: true,
|
|
437
|
-
isRoot: false,
|
|
438
|
-
package: { _id: 'abc', bundleDependencies: undefined, deprecated: undefined, bin: target.package.bin, scripts: dep.package.scripts },
|
|
439
|
-
target,
|
|
440
|
-
}
|
|
441
|
-
const newEdge1 = { optional, from, to: link }
|
|
442
|
-
from.edgesOut.set(dep.name, newEdge1)
|
|
443
|
-
link.edgesIn.add(newEdge1)
|
|
444
|
-
const newEdge2 = { optional: false, from: link, to: target }
|
|
445
|
-
link.edgesOut.set(dep.name, newEdge2)
|
|
446
|
-
target.edgesIn.add(newEdge2)
|
|
447
|
-
root.children.push(link)
|
|
448
|
-
}
|
|
419
|
+
#processDeps (dep, optional, external, root, from, nmFolder) {
|
|
420
|
+
const toKey = getKey(dep)
|
|
449
421
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
422
|
+
let target
|
|
423
|
+
if (external) {
|
|
424
|
+
const toLocation = join('node_modules', '.store', toKey, 'node_modules', dep.packageName)
|
|
425
|
+
target = root.children.get(toLocation)
|
|
426
|
+
} else {
|
|
427
|
+
target = root.inventory.get(dep.localLocation)
|
|
428
|
+
}
|
|
429
|
+
// TODO: we should no-op is an edge has already been created with the same fromKey and toKey
|
|
430
|
+
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
|
|
431
|
+
if (!target) {
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (dep.package.bin) {
|
|
436
|
+
for (const bn in dep.package.bin) {
|
|
437
|
+
target.binPaths.push(join(dep.root.localPath, nmFolder, '.bin', bn))
|
|
464
438
|
}
|
|
465
439
|
}
|
|
466
440
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
441
|
+
const pkg = {
|
|
442
|
+
_id: dep.package._id,
|
|
443
|
+
bin: target.package.bin,
|
|
444
|
+
bundleDependencies: undefined,
|
|
445
|
+
deprecated: undefined,
|
|
446
|
+
scripts: dep.package.scripts,
|
|
447
|
+
version: dep.package.version,
|
|
470
448
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
449
|
+
const link = new IsolatedLink({
|
|
450
|
+
isStoreLink: true,
|
|
451
|
+
location: join(nmFolder, dep.name),
|
|
452
|
+
name: toKey,
|
|
453
|
+
optional,
|
|
454
|
+
parent: root,
|
|
455
|
+
package: pkg,
|
|
456
|
+
path: join(dep.root.localPath, nmFolder, dep.name),
|
|
457
|
+
realpath: target.path,
|
|
458
|
+
resolved: external ? `file:.store/${toKey}/node_modules/${dep.packageName}` : dep.resolved,
|
|
459
|
+
root,
|
|
460
|
+
target,
|
|
461
|
+
})
|
|
462
|
+
// XXX top is from place-dep not lib/link.js
|
|
463
|
+
link.top = { path: dep.root.localPath }
|
|
464
|
+
const newEdge1 = { optional, from, to: link }
|
|
465
|
+
from.edgesOut.set(dep.name, newEdge1)
|
|
466
|
+
link.edgesIn.add(newEdge1)
|
|
467
|
+
const newEdge2 = { optional: false, from: link, to: target }
|
|
468
|
+
link.edgesOut.set(dep.name, newEdge2)
|
|
469
|
+
target.edgesIn.add(newEdge2)
|
|
470
|
+
root.children.set(link.location, link)
|
|
476
471
|
}
|
|
477
472
|
}
|
package/lib/arborist/rebuild.js
CHANGED
|
@@ -9,6 +9,7 @@ const runScript = require('@npmcli/run-script')
|
|
|
9
9
|
const { callLimit: promiseCallLimit } = require('promise-call-limit')
|
|
10
10
|
const { depth: dfwalk } = require('treeverse')
|
|
11
11
|
const { isNodeGypPackage, defaultGypInstallScript } = require('@npmcli/node-gyp')
|
|
12
|
+
const { promiseRetry } = require('@gar/promise-retry')
|
|
12
13
|
const { log, time } = require('proc-log')
|
|
13
14
|
const { resolve } = require('node:path')
|
|
14
15
|
|
|
@@ -381,13 +382,20 @@ module.exports = cls => class Builder extends cls {
|
|
|
381
382
|
|
|
382
383
|
const timeEnd = time.start(`build:link:${node.location}`)
|
|
383
384
|
|
|
384
|
-
|
|
385
|
+
// 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.
|
|
386
|
+
const p = promiseRetry((retry) => binLinks({
|
|
385
387
|
pkg: node.package,
|
|
386
388
|
path: node.path,
|
|
387
389
|
top: !!(node.isTop || node.globalTop),
|
|
388
390
|
force: this.options.force,
|
|
389
391
|
global: !!node.globalTop,
|
|
390
|
-
})
|
|
392
|
+
}).catch(/* istanbul ignore next - Windows-only transient antivirus locks */ err => {
|
|
393
|
+
if (process.platform === 'win32' &&
|
|
394
|
+
(err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'EBUSY')) {
|
|
395
|
+
return retry(err)
|
|
396
|
+
}
|
|
397
|
+
throw err
|
|
398
|
+
}), { retries: 5, minTimeout: 500 })
|
|
391
399
|
|
|
392
400
|
await (this.#doHandleOptionalFailure
|
|
393
401
|
? this[_handleOptionalFailure](node, p)
|
package/lib/arborist/reify.js
CHANGED
|
@@ -8,9 +8,10 @@ const promiseAllRejectLate = require('promise-all-reject-late')
|
|
|
8
8
|
const runScript = require('@npmcli/run-script')
|
|
9
9
|
const { callLimit: promiseCallLimit } = require('promise-call-limit')
|
|
10
10
|
const { depth: dfwalk } = require('treeverse')
|
|
11
|
-
const { dirname, resolve, relative, join } = require('node:path')
|
|
11
|
+
const { dirname, resolve, relative, join, sep } = require('node:path')
|
|
12
12
|
const { log, time } = require('proc-log')
|
|
13
|
-
const {
|
|
13
|
+
const { existsSync } = require('node:fs')
|
|
14
|
+
const { lstat, mkdir, readdir, rm, symlink } = require('node:fs/promises')
|
|
14
15
|
const { moveFile } = require('@npmcli/fs')
|
|
15
16
|
const { subset, intersects } = require('semver')
|
|
16
17
|
const { walkUp } = require('walk-up-path')
|
|
@@ -26,6 +27,7 @@ const retirePath = require('../retire-path.js')
|
|
|
26
27
|
const treeCheck = require('../tree-check.js')
|
|
27
28
|
const { defaultLockfileVersion } = require('../shrinkwrap.js')
|
|
28
29
|
const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js')
|
|
30
|
+
const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js')
|
|
29
31
|
|
|
30
32
|
// Part of steps (steps need refactoring before we can do anything about these)
|
|
31
33
|
const _retireShallowNodes = Symbol.for('retireShallowNodes')
|
|
@@ -63,8 +65,6 @@ const _resolvedAdd = Symbol.for('resolvedAdd')
|
|
|
63
65
|
// used by build-ideal-tree mixin
|
|
64
66
|
const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
|
|
65
67
|
|
|
66
|
-
const _createIsolatedTree = Symbol.for('createIsolatedTree')
|
|
67
|
-
|
|
68
68
|
module.exports = cls => class Reifier extends cls {
|
|
69
69
|
#bundleMissing = new Set() // child nodes we'd EXPECT to be included in a bundle, but aren't
|
|
70
70
|
#bundleUnpacked = new Set() // the nodes we unpack to read their bundles
|
|
@@ -75,6 +75,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
75
75
|
#shrinkwrapInflated = new Set()
|
|
76
76
|
#sparseTreeDirs = new Set()
|
|
77
77
|
#sparseTreeRoots = new Set()
|
|
78
|
+
#linkedActualForDiff = null
|
|
78
79
|
|
|
79
80
|
constructor (options) {
|
|
80
81
|
super(options)
|
|
@@ -115,16 +116,21 @@ module.exports = cls => class Reifier extends cls {
|
|
|
115
116
|
// this is currently technical debt which will be resolved in a refactor
|
|
116
117
|
// of Node/Link trees
|
|
117
118
|
log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.')
|
|
118
|
-
this.idealTree = await this
|
|
119
|
+
this.idealTree = await this.createIsolatedTree()
|
|
120
|
+
this.#linkedActualForDiff = this.#buildLinkedActualForDiff(
|
|
121
|
+
this.idealTree, this.actualTree
|
|
122
|
+
)
|
|
119
123
|
}
|
|
120
124
|
await this[_diffTrees]()
|
|
121
125
|
await this.#reifyPackages()
|
|
122
126
|
if (linked) {
|
|
127
|
+
await this.#cleanOrphanedStoreEntries()
|
|
123
128
|
// swap back in the idealTree
|
|
124
129
|
// so that the lockfile is preserved
|
|
125
130
|
this.idealTree = oldTree
|
|
126
131
|
}
|
|
127
132
|
await this[_saveIdealTree](options)
|
|
133
|
+
this.#linkedActualForDiff = null
|
|
128
134
|
// clean inert
|
|
129
135
|
for (const node of this.idealTree.inventory.values()) {
|
|
130
136
|
if (node.inert) {
|
|
@@ -146,7 +152,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
146
152
|
// was not changed, delete anything in the ideal and not actual.
|
|
147
153
|
// Then we move the entire idealTree over to this.actualTree, and
|
|
148
154
|
// save the hidden lockfile.
|
|
149
|
-
if (this.diff && this.diff.filterSet.size) {
|
|
155
|
+
if (this.diff && this.diff.filterSet.size && !linked) {
|
|
150
156
|
const reroot = new Set()
|
|
151
157
|
|
|
152
158
|
const { filterSet } = this.diff
|
|
@@ -422,13 +428,18 @@ module.exports = cls => class Reifier extends cls {
|
|
|
422
428
|
if (includeWorkspaces) {
|
|
423
429
|
// add all ws nodes to filterNodes
|
|
424
430
|
for (const ws of this.options.workspaces) {
|
|
425
|
-
const ideal = this.idealTree.children.get
|
|
431
|
+
const ideal = this.idealTree.children.get(ws)
|
|
426
432
|
if (ideal) {
|
|
427
433
|
filterNodes.push(ideal)
|
|
428
434
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
435
|
+
// Skip actual-side filterNodes when using the linked diff wrapper.
|
|
436
|
+
// Those nodes have root===actualTree, not root===linkedActualForDiff, and Diff.calculate requires filterNode.root to match actual.
|
|
437
|
+
// The ideal filterNode alone is sufficient to scope the workspace diff.
|
|
438
|
+
if (!this.#linkedActualForDiff) {
|
|
439
|
+
const actual = this.actualTree.children.get(ws)
|
|
440
|
+
if (actual) {
|
|
441
|
+
filterNodes.push(actual)
|
|
442
|
+
}
|
|
432
443
|
}
|
|
433
444
|
}
|
|
434
445
|
}
|
|
@@ -450,7 +461,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
450
461
|
omit: this.#omit,
|
|
451
462
|
shrinkwrapInflated: this.#shrinkwrapInflated,
|
|
452
463
|
filterNodes,
|
|
453
|
-
actual: this.actualTree,
|
|
464
|
+
actual: this.#linkedActualForDiff || this.actualTree,
|
|
454
465
|
ideal: this.idealTree,
|
|
455
466
|
})
|
|
456
467
|
|
|
@@ -573,6 +584,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
573
584
|
// if the directory already exists, made will be undefined. if that's the case
|
|
574
585
|
// we don't want to remove it because we aren't the ones who created it so we
|
|
575
586
|
// omit it from the #sparseTreeRoots
|
|
587
|
+
/* istanbul ignore next -- pre-existing: mkdir returns undefined when dir exists, covered in reify tests but lost in aggregate coverage merge */
|
|
576
588
|
if (made) {
|
|
577
589
|
this.#sparseTreeRoots.add(made)
|
|
578
590
|
}
|
|
@@ -789,6 +801,59 @@ module.exports = cls => class Reifier extends cls {
|
|
|
789
801
|
return join(filePath)
|
|
790
802
|
}
|
|
791
803
|
|
|
804
|
+
// Build a flat actual tree wrapper for linked installs so the diff can correctly match store entries that already exist on disk.
|
|
805
|
+
// The proxy tree from createIsolatedTree() is flat (all children on root), but loadActual() produces a nested tree where store entries are deep link targets.
|
|
806
|
+
// This wrapper surfaces them at the root level for comparison.
|
|
807
|
+
#buildLinkedActualForDiff (idealTree, actualTree) {
|
|
808
|
+
// Combined Map keyed by path (how allChildren() in diff.js keys)
|
|
809
|
+
const combined = new Map()
|
|
810
|
+
|
|
811
|
+
// Create synthetic actual entries for ALL ideal children that exist on disk.
|
|
812
|
+
// 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.
|
|
813
|
+
// 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).
|
|
814
|
+
for (const child of idealTree.children.values()) {
|
|
815
|
+
if (combined.has(child.path) || !existsSync(child.path)) {
|
|
816
|
+
continue
|
|
817
|
+
}
|
|
818
|
+
let entry
|
|
819
|
+
if (child.isLink) {
|
|
820
|
+
entry = new IsolatedLink(child)
|
|
821
|
+
} else {
|
|
822
|
+
entry = new IsolatedNode(child)
|
|
823
|
+
}
|
|
824
|
+
if (child.isLink && combined.has(child.realpath)) {
|
|
825
|
+
entry.target = combined.get(child.realpath)
|
|
826
|
+
}
|
|
827
|
+
combined.set(child.path, entry)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Proxy .get(name) to original actual tree for filterNodes compatibility
|
|
831
|
+
// (scoped workspace installs use .get(name), allChildren uses .values())
|
|
832
|
+
const origGet = actualTree.children.get.bind(actualTree.children)
|
|
833
|
+
const combinedGet = combined.get.bind(combined)
|
|
834
|
+
/* istanbul ignore next -- only reached during scoped workspace installs */
|
|
835
|
+
combined.get = (key) => combinedGet(key) || origGet(key)
|
|
836
|
+
|
|
837
|
+
let wrapper
|
|
838
|
+
/* istanbul ignore next - untested! */
|
|
839
|
+
if (actualTree.isLink) {
|
|
840
|
+
wrapper = new IsolatedLink(actualTree)
|
|
841
|
+
} else {
|
|
842
|
+
wrapper = new IsolatedNode(actualTree)
|
|
843
|
+
}
|
|
844
|
+
wrapper.root = wrapper
|
|
845
|
+
wrapper.binPaths = actualTree.binPaths
|
|
846
|
+
wrapper.children = combined
|
|
847
|
+
wrapper.edgesOut = actualTree.edgesOut
|
|
848
|
+
// Use empty fsChildren so that allChildren() only picks up entries from the combined map.
|
|
849
|
+
// 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().
|
|
850
|
+
wrapper.fsChildren = new Set()
|
|
851
|
+
wrapper.integrity = actualTree.integrity
|
|
852
|
+
wrapper.inventory = actualTree.inventory
|
|
853
|
+
|
|
854
|
+
return wrapper
|
|
855
|
+
}
|
|
856
|
+
|
|
792
857
|
#registryResolved (resolved) {
|
|
793
858
|
// the default registry url is a magic value meaning "the currently
|
|
794
859
|
// configured registry".
|
|
@@ -1247,6 +1312,41 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1247
1312
|
timeEnd()
|
|
1248
1313
|
}
|
|
1249
1314
|
|
|
1315
|
+
// After a linked install, scan node_modules/.store/ and remove any directories that are not referenced by the current ideal tree.
|
|
1316
|
+
// Store entries become orphaned when dependencies are updated or removed, because the diff never sees the old store keys.
|
|
1317
|
+
async #cleanOrphanedStoreEntries () {
|
|
1318
|
+
const storeDir = resolve(this.path, 'node_modules', '.store')
|
|
1319
|
+
let entries
|
|
1320
|
+
try {
|
|
1321
|
+
entries = await readdir(storeDir)
|
|
1322
|
+
} catch {
|
|
1323
|
+
return
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Collect valid store keys from the isolated ideal tree (location: node_modules/.store/{key}/node_modules/{pkg})
|
|
1327
|
+
const validKeys = new Set()
|
|
1328
|
+
for (const child of this.idealTree.children.values()) {
|
|
1329
|
+
if (child.isInStore) {
|
|
1330
|
+
const key = child.location.split(sep)[2]
|
|
1331
|
+
validKeys.add(key)
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const orphaned = entries.filter(e => !validKeys.has(e))
|
|
1336
|
+
if (!orphaned.length) {
|
|
1337
|
+
return
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
log.silly('reify', 'cleaning orphaned store entries', orphaned)
|
|
1341
|
+
await promiseAllRejectLate(
|
|
1342
|
+
orphaned.map(e =>
|
|
1343
|
+
rm(resolve(storeDir, e), { recursive: true, force: true })
|
|
1344
|
+
.catch(/* istanbul ignore next -- rm with force rarely fails */
|
|
1345
|
+
er => log.warn('cleanup', `Failed to remove orphaned store entry ${e}`, er))
|
|
1346
|
+
)
|
|
1347
|
+
)
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1250
1350
|
// last but not least, we save the ideal tree metadata to the package-lock
|
|
1251
1351
|
// or shrinkwrap file, and any additions or removals to package.json
|
|
1252
1352
|
async [_saveIdealTree] (options) {
|
package/lib/audit-report.js
CHANGED
package/lib/diff.js
CHANGED
|
@@ -71,6 +71,7 @@ class Diff {
|
|
|
71
71
|
tree: filterNode,
|
|
72
72
|
visit: node => filterSet.add(node),
|
|
73
73
|
getChildren: node => {
|
|
74
|
+
const orig = node
|
|
74
75
|
node = node.target
|
|
75
76
|
const loc = node.location
|
|
76
77
|
const idealNode = ideal.inventory.get(loc)
|
|
@@ -87,7 +88,12 @@ class Diff {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
const result = ideals.concat(actuals)
|
|
92
|
+
// Include link targets so store entries end up in filterSet
|
|
93
|
+
if (orig.isLink) {
|
|
94
|
+
result.push(node)
|
|
95
|
+
}
|
|
96
|
+
return result
|
|
91
97
|
},
|
|
92
98
|
})
|
|
93
99
|
}
|
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
|
@@ -73,35 +73,34 @@ class Node {
|
|
|
73
73
|
constructor (options) {
|
|
74
74
|
// NB: path can be null if it's a link target
|
|
75
75
|
const {
|
|
76
|
-
root,
|
|
77
|
-
path,
|
|
78
|
-
realpath,
|
|
79
|
-
parent,
|
|
80
|
-
error,
|
|
81
|
-
meta,
|
|
82
|
-
fsParent,
|
|
83
|
-
resolved,
|
|
84
|
-
integrity,
|
|
85
|
-
// allow setting name explicitly when we haven't set a path yet
|
|
86
|
-
name,
|
|
87
76
|
children,
|
|
77
|
+
dev = true,
|
|
78
|
+
devOptional = true,
|
|
79
|
+
dummy = false,
|
|
80
|
+
error,
|
|
81
|
+
extraneous = true,
|
|
88
82
|
fsChildren,
|
|
83
|
+
fsParent,
|
|
84
|
+
global = false,
|
|
85
|
+
hasShrinkwrap,
|
|
86
|
+
inert = false,
|
|
89
87
|
installLinks = false,
|
|
88
|
+
integrity,
|
|
89
|
+
isInStore = false,
|
|
90
90
|
legacyPeerDeps = false,
|
|
91
91
|
linksIn,
|
|
92
|
-
isInStore = false,
|
|
93
|
-
hasShrinkwrap,
|
|
94
|
-
overrides,
|
|
95
92
|
loadOverrides = false,
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
meta,
|
|
94
|
+
name, // allow setting name explicitly when we haven't set a path yet
|
|
98
95
|
optional = true,
|
|
99
|
-
|
|
96
|
+
overrides,
|
|
97
|
+
parent,
|
|
98
|
+
path,
|
|
100
99
|
peer = true,
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
realpath,
|
|
101
|
+
resolved,
|
|
102
|
+
root,
|
|
103
103
|
sourceReference = null,
|
|
104
|
-
inert = false,
|
|
105
104
|
} = options
|
|
106
105
|
// this object gives querySelectorAll somewhere to stash context about a node
|
|
107
106
|
// while processing a query
|
|
@@ -793,9 +793,14 @@ const hasParent = (node, compareNodes) => {
|
|
|
793
793
|
compareNode = compareNode.target
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
-
//
|
|
796
|
+
// Follows logical parent for link ancestors (e.g. workspaces whose target lives outside node_modules).
|
|
797
|
+
// 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.
|
|
797
798
|
if (node.isTop && (node.resolveParent === compareNode)) {
|
|
798
|
-
|
|
799
|
+
for (const link of node.linksIn) {
|
|
800
|
+
if (link.parent === compareNode) {
|
|
801
|
+
return true
|
|
802
|
+
}
|
|
803
|
+
}
|
|
799
804
|
}
|
|
800
805
|
// follows edges-in to check if they match a possible parent
|
|
801
806
|
for (const edge of node.edgesIn) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "9.4.
|
|
3
|
+
"version": "9.4.1",
|
|
4
4
|
"description": "Manage node_modules trees",
|
|
5
5
|
"dependencies": {
|
|
6
|
+
"@gar/promise-retry": "^1.0.0",
|
|
6
7
|
"@isaacs/string-locale-compare": "^1.1.0",
|
|
7
8
|
"@npmcli/fs": "^5.0.0",
|
|
8
9
|
"@npmcli/installed-package-contents": "^4.0.0",
|