@npmcli/arborist 9.3.1 → 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 +314 -294
- package/lib/arborist/rebuild.js +13 -5
- package/lib/arborist/reify.js +110 -10
- 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 +16 -3
- package/lib/shrinkwrap.js +1 -0
- 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,42 +106,53 @@ 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,
|
|
106
150
|
'node_modules',
|
|
107
151
|
'.store',
|
|
108
|
-
`${node.
|
|
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,48 +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
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
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.
|
|
233
|
+
const peerDeps = node.package.peerDependencies
|
|
234
|
+
if (peerDeps && node.legacyPeerDeps) {
|
|
235
|
+
const edgeNames = new Set(edges.map(edge => edge.name))
|
|
236
|
+
for (const peerName in peerDeps) {
|
|
237
|
+
if (!edgeNames.has(peerName)) {
|
|
238
|
+
const resolved = node.resolve(peerName)
|
|
239
|
+
if (resolved && resolved !== node && !resolved.inert) {
|
|
240
|
+
nonOptionalDeps.push(resolved.target)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
143
244
|
}
|
|
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
245
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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)))
|
|
151
252
|
result.dependencies = [
|
|
152
253
|
...result.externalDependencies,
|
|
153
254
|
...result.localDependencies,
|
|
154
255
|
...result.externalOptionalDependencies,
|
|
155
256
|
]
|
|
156
|
-
result.root = this.rootNode
|
|
157
|
-
result.id = this.counter++
|
|
158
|
-
result.name = node.name
|
|
159
|
-
result.package = { ...node.package }
|
|
160
|
-
result.package.bundleDependencies = undefined
|
|
161
|
-
result.hasInstallScript = node.hasInstallScript
|
|
162
257
|
}
|
|
163
258
|
|
|
164
259
|
async #createBundledTree () {
|
|
@@ -200,253 +295,178 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
200
295
|
nodes.set(to.location, { location: to.location, resolved: to.resolved, name: to.name, optional: to.optional, pkg: { ...to.package, bundleDependencies: undefined } })
|
|
201
296
|
edges.push({ from: from.isRoot ? 'root' : from.location, to: to.location })
|
|
202
297
|
|
|
203
|
-
to.edgesOut.forEach(
|
|
298
|
+
to.edgesOut.forEach(edge => {
|
|
204
299
|
// an edge out should always have a to
|
|
205
300
|
/* istanbul ignore else */
|
|
206
|
-
if (
|
|
207
|
-
queue.push({ from:
|
|
301
|
+
if (edge.to) {
|
|
302
|
+
queue.push({ from: edge.from, to: edge.to })
|
|
208
303
|
}
|
|
209
304
|
})
|
|
210
305
|
}
|
|
211
306
|
return { edges, nodes }
|
|
212
307
|
}
|
|
213
308
|
|
|
214
|
-
async
|
|
215
|
-
await this
|
|
216
|
-
|
|
217
|
-
const proxiedIdealTree = this.idealGraph
|
|
218
|
-
|
|
309
|
+
async createIsolatedTree () {
|
|
310
|
+
await this.makeIdealGraph()
|
|
219
311
|
const bundledTree = await this.#createBundledTree()
|
|
220
312
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const deps = []
|
|
225
|
-
const branch = []
|
|
226
|
-
depth({
|
|
227
|
-
tree: startNode,
|
|
228
|
-
getChildren: node => node.dependencies,
|
|
229
|
-
filter: node => node,
|
|
230
|
-
visit: node => {
|
|
231
|
-
branch.push(`${node.name}@${node.version}`)
|
|
232
|
-
deps.push(`${branch.join('->')}::${node.resolved}`)
|
|
233
|
-
},
|
|
234
|
-
leave: () => {
|
|
235
|
-
branch.pop()
|
|
236
|
-
},
|
|
237
|
-
})
|
|
238
|
-
deps.sort()
|
|
239
|
-
return crypto.createHash('shake256', { outputLength: 16 })
|
|
240
|
-
.update(deps.join(','))
|
|
241
|
-
.digest('base64')
|
|
242
|
-
// Node v14 doesn't support base64url
|
|
243
|
-
.replace(/\+/g, '-')
|
|
244
|
-
.replace(/\//g, '_')
|
|
245
|
-
.replace(/=+$/m, '')
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const getKey = (idealTreeNode) => {
|
|
249
|
-
return `${idealTreeNode.name}@${idealTreeNode.version}-${treeHash(idealTreeNode)}`
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const root = {
|
|
253
|
-
fsChildren: [],
|
|
254
|
-
integrity: null,
|
|
255
|
-
inventory: new Map(),
|
|
256
|
-
isLink: false,
|
|
257
|
-
isRoot: true,
|
|
258
|
-
binPaths: [],
|
|
259
|
-
edgesIn: new Set(),
|
|
260
|
-
edgesOut: new Map(),
|
|
261
|
-
hasShrinkwrap: false,
|
|
262
|
-
parent: null,
|
|
263
|
-
// TODO: we should probably not reference this.idealTree
|
|
264
|
-
resolved: this.idealTree.resolved,
|
|
265
|
-
isTop: true,
|
|
266
|
-
path: proxiedIdealTree.root.localPath,
|
|
267
|
-
realpath: proxiedIdealTree.root.localPath,
|
|
268
|
-
package: proxiedIdealTree.root.package,
|
|
269
|
-
meta: { loadedFromDisk: false },
|
|
270
|
-
global: false,
|
|
271
|
-
isProjectRoot: true,
|
|
272
|
-
children: [],
|
|
273
|
-
}
|
|
274
|
-
// root.inventory.set('', t)
|
|
275
|
-
// root.meta = this.idealTree.meta
|
|
276
|
-
// TODO We should mock better the inventory object because it is used by audit-report.js ... maybe
|
|
277
|
-
root.inventory.query = () => {
|
|
278
|
-
return []
|
|
279
|
-
}
|
|
313
|
+
const root = new IsolatedNode(this.idealGraph)
|
|
314
|
+
root.root = root
|
|
315
|
+
root.inventory.set('', root)
|
|
280
316
|
const processed = new Set()
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
children: [],
|
|
286
|
-
hasInstallScript: c.hasInstallScript,
|
|
287
|
-
binPaths: [],
|
|
288
|
-
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({
|
|
289
321
|
location: c.localLocation,
|
|
322
|
+
name: wsName,
|
|
323
|
+
package: c.package,
|
|
290
324
|
path: c.localPath,
|
|
291
|
-
realpath: c.localPath,
|
|
292
325
|
resolved: c.resolved,
|
|
293
|
-
}
|
|
294
|
-
root.fsChildren.
|
|
326
|
+
})
|
|
327
|
+
root.fsChildren.add(workspace)
|
|
295
328
|
root.inventory.set(workspace.location, workspace)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
/* istanbul ignore next -- emulate Node */
|
|
313
|
-
getBundler () {
|
|
314
|
-
return null
|
|
315
|
-
},
|
|
316
|
-
hasShrinkwrap: false,
|
|
317
|
-
inDepBundle: false,
|
|
318
|
-
integrity: null,
|
|
319
|
-
isLink: false,
|
|
320
|
-
isRoot: false,
|
|
321
|
-
isInStore: inStore,
|
|
322
|
-
path: join(proxiedIdealTree.root.localPath, location),
|
|
323
|
-
realpath: join(proxiedIdealTree.root.localPath, location),
|
|
324
|
-
resolved: node.resolved,
|
|
325
|
-
version: pkg.version,
|
|
326
|
-
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)
|
|
327
345
|
}
|
|
328
|
-
|
|
329
|
-
root.
|
|
330
|
-
|
|
346
|
+
root.children.set(wsName, wsLink)
|
|
347
|
+
root.inventory.set(wsLink.location, wsLink)
|
|
348
|
+
workspace.linksIn.add(wsLink)
|
|
331
349
|
}
|
|
332
|
-
|
|
350
|
+
|
|
351
|
+
this.idealGraph.external.forEach(c => {
|
|
333
352
|
const key = getKey(c)
|
|
334
353
|
if (processed.has(key)) {
|
|
335
354
|
return
|
|
336
355
|
}
|
|
337
356
|
processed.add(key)
|
|
338
|
-
const location = join('node_modules', '.store', key, 'node_modules', c.
|
|
339
|
-
generateChild(c, location, c.package, true)
|
|
357
|
+
const location = join('node_modules', '.store', key, 'node_modules', c.packageName)
|
|
358
|
+
this.#generateChild(c, location, c.package, true, root)
|
|
340
359
|
})
|
|
360
|
+
|
|
341
361
|
bundledTree.nodes.forEach(node => {
|
|
342
|
-
generateChild(node, node.location, node.pkg, false)
|
|
362
|
+
this.#generateChild(node, node.location, node.pkg, false, root)
|
|
343
363
|
})
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
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)
|
|
347
368
|
// Maybe optional should be propagated from the original edge
|
|
348
|
-
const
|
|
349
|
-
from.edgesOut.set(to.name,
|
|
350
|
-
to.edgesIn.add(
|
|
369
|
+
const newEdge = { optional: false, from, to }
|
|
370
|
+
from.edgesOut.set(to.name, newEdge)
|
|
371
|
+
to.edgesIn.add(newEdge)
|
|
351
372
|
})
|
|
352
|
-
const memo = new Set()
|
|
353
373
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
memo.add(key)
|
|
361
|
-
|
|
362
|
-
let from, nmFolder
|
|
363
|
-
if (externalEdge) {
|
|
364
|
-
const fromLocation = join('node_modules', '.store', key, 'node_modules', node.name)
|
|
365
|
-
from = root.children.find(c => c.location === fromLocation)
|
|
366
|
-
nmFolder = join('node_modules', '.store', key, 'node_modules')
|
|
367
|
-
} else {
|
|
368
|
-
from = node.isProjectRoot ? root : root.fsChildren.find(c => c.location === node.localLocation)
|
|
369
|
-
nmFolder = join(node.localLocation, 'node_modules')
|
|
370
|
-
}
|
|
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
|
+
}
|
|
371
380
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
381
|
+
#processEdges (node, externalEdge, root) {
|
|
382
|
+
const key = getKey(node)
|
|
383
|
+
if (this.#processedEdges.has(key)) {
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
this.#processedEdges.add(key)
|
|
375
387
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
401
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
isProjectRoot: false,
|
|
397
|
-
edgesIn: new Set(),
|
|
398
|
-
edgesOut: new Map(),
|
|
399
|
-
binPaths: [],
|
|
400
|
-
isTop: false,
|
|
401
|
-
optional,
|
|
402
|
-
location: location,
|
|
403
|
-
path: join(dep.root.localPath, nmFolder, dep.name),
|
|
404
|
-
realpath: target.path,
|
|
405
|
-
name: toKey,
|
|
406
|
-
resolved: dep.resolved,
|
|
407
|
-
top: { path: dep.root.localPath },
|
|
408
|
-
children: [],
|
|
409
|
-
fsChildren: [],
|
|
410
|
-
isLink: true,
|
|
411
|
-
isStoreLink: true,
|
|
412
|
-
isRoot: false,
|
|
413
|
-
package: { _id: 'abc', bundleDependencies: undefined, deprecated: undefined, bin: target.package.bin, scripts: dep.package.scripts },
|
|
414
|
-
target,
|
|
415
|
-
}
|
|
416
|
-
const newEdge1 = { optional, from, to: link }
|
|
417
|
-
from.edgesOut.set(dep.name, newEdge1)
|
|
418
|
-
link.edgesIn.add(newEdge1)
|
|
419
|
-
const newEdge2 = { optional: false, from: link, to: target }
|
|
420
|
-
link.edgesOut.set(dep.name, newEdge2)
|
|
421
|
-
target.edgesIn.add(newEdge2)
|
|
422
|
-
root.children.push(link)
|
|
423
|
-
}
|
|
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
|
+
}
|
|
424
418
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
419
|
+
#processDeps (dep, optional, external, root, from, nmFolder) {
|
|
420
|
+
const toKey = getKey(dep)
|
|
421
|
+
|
|
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))
|
|
439
438
|
}
|
|
440
439
|
}
|
|
441
440
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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,
|
|
445
448
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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)
|
|
451
471
|
}
|
|
452
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
|
|
|
@@ -295,12 +296,12 @@ module.exports = cls => class Builder extends cls {
|
|
|
295
296
|
devOptional,
|
|
296
297
|
package: pkg,
|
|
297
298
|
location,
|
|
298
|
-
isStoreLink,
|
|
299
299
|
} = node.target
|
|
300
300
|
|
|
301
301
|
// skip any that we know we'll be deleting
|
|
302
|
-
// or
|
|
303
|
-
|
|
302
|
+
// or links to store entries (their scripts run on the store
|
|
303
|
+
// entry itself, not through the link)
|
|
304
|
+
if (this[_trashList].has(path) || (node.isLink && node.target?.isInStore)) {
|
|
304
305
|
return
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -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
|
|
@@ -426,9 +432,14 @@ module.exports = cls => class Reifier extends cls {
|
|
|
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
|
|
@@ -427,11 +427,19 @@ class Results {
|
|
|
427
427
|
if (!this.currentAstNode.typeValue) {
|
|
428
428
|
return this.initialItems
|
|
429
429
|
}
|
|
430
|
+
// TODO this differs subtly with `:type()` because it now iterates on edgesIn, which means extraneous deps won't show up
|
|
431
|
+
// note how "@npmcli/abbrev@2.0.0-beta.45" is in the `:type()` results in the test but not in any of the other results.
|
|
430
432
|
return this.initialItems
|
|
431
433
|
.flatMap(node => {
|
|
432
434
|
const found = []
|
|
435
|
+
const { typeValue } = this.currentAstNode
|
|
433
436
|
for (const edge of node.edgesIn) {
|
|
434
|
-
|
|
437
|
+
const parsedArg = npa(`${edge.name}@${edge.spec}`)
|
|
438
|
+
if (typeValue === 'registry') {
|
|
439
|
+
if (parsedArg.registry) {
|
|
440
|
+
found.push(edge.to)
|
|
441
|
+
}
|
|
442
|
+
} else if (parsedArg.type === typeValue) {
|
|
435
443
|
found.push(edge.to)
|
|
436
444
|
}
|
|
437
445
|
}
|
|
@@ -785,9 +793,14 @@ const hasParent = (node, compareNodes) => {
|
|
|
785
793
|
compareNode = compareNode.target
|
|
786
794
|
}
|
|
787
795
|
|
|
788
|
-
//
|
|
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.
|
|
789
798
|
if (node.isTop && (node.resolveParent === compareNode)) {
|
|
790
|
-
|
|
799
|
+
for (const link of node.linksIn) {
|
|
800
|
+
if (link.parent === compareNode) {
|
|
801
|
+
return true
|
|
802
|
+
}
|
|
803
|
+
}
|
|
791
804
|
}
|
|
792
805
|
// follows edges-in to check if they match a possible parent
|
|
793
806
|
for (const edge of node.edgesIn) {
|
package/lib/shrinkwrap.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "9.
|
|
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",
|