@npmcli/arborist 9.5.0 → 10.0.0-pre.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/index.js +1 -2
- package/lib/arborist/build-ideal-tree.js +89 -44
- package/lib/arborist/isolated-reifier.js +5 -45
- package/lib/arborist/load-virtual.js +1 -2
- package/lib/arborist/reify.js +27 -41
- package/lib/diff.js +7 -22
- package/lib/edge.js +2 -2
- package/lib/isolated-classes.js +0 -1
- package/lib/link.js +16 -4
- package/lib/node.js +2 -9
- package/lib/printable.js +0 -5
- package/lib/shrinkwrap.js +23 -33
- package/package.json +1 -1
- package/bin/shrinkwrap.js +0 -7
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ arb.loadActual().then(tree => {
|
|
|
60
60
|
// tree is also stored at arb.virtualTree
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
// read just what the package-lock.json
|
|
63
|
+
// read just what the package-lock.json says
|
|
64
64
|
// This *also* loads the yarn.lock file, but that's only relevant
|
|
65
65
|
// when building the ideal tree.
|
|
66
66
|
arb.loadVirtual().then(tree => {
|
|
@@ -301,7 +301,7 @@ pruning nodes from the tree.
|
|
|
301
301
|
that the dep is brought in by a peer dep at some point, rather than a
|
|
302
302
|
normal non-peer dependency.
|
|
303
303
|
|
|
304
|
-
Note: `devOptional` is only set in the
|
|
304
|
+
Note: `devOptional` is only set in the package-lock file if
|
|
305
305
|
_neither_ `dev` nor `optional` are set, as it would be redundant.
|
|
306
306
|
|
|
307
307
|
## BIN
|
package/bin/index.js
CHANGED
|
@@ -20,8 +20,7 @@ ${message && '\n' + message + '\n'}
|
|
|
20
20
|
* prune: prune the ideal tree and reify (like npm prune)
|
|
21
21
|
* ideal: generate and print the ideal tree
|
|
22
22
|
* actual: read and print the actual tree in node_modules
|
|
23
|
-
* virtual: read and print the virtual tree in the local
|
|
24
|
-
* shrinkwrap: load a local shrinkwrap and print its data
|
|
23
|
+
* virtual: read and print the virtual tree in the local package-lock.json
|
|
25
24
|
* audit: perform a security audit on project dependencies
|
|
26
25
|
* funding: query funding information in the local package tree. A second
|
|
27
26
|
positional argument after the path name can limit to a package name.
|
|
@@ -28,6 +28,15 @@ const Shrinkwrap = require('../shrinkwrap.js')
|
|
|
28
28
|
const { defaultLockfileVersion } = Shrinkwrap
|
|
29
29
|
const Node = require('../node.js')
|
|
30
30
|
const Link = require('../link.js')
|
|
31
|
+
|
|
32
|
+
// Maps a parsed spec.type to the corresponding allow-* arborist option name.
|
|
33
|
+
// Hoisted to module scope so #checkAllow doesn't re-allocate it per call.
|
|
34
|
+
const ALLOW_OPTION_FOR_TYPE = {
|
|
35
|
+
git: 'allowGit',
|
|
36
|
+
remote: 'allowRemote',
|
|
37
|
+
file: 'allowFile',
|
|
38
|
+
directory: 'allowDirectory',
|
|
39
|
+
}
|
|
31
40
|
const addRmPkgDeps = require('../add-rm-pkg-deps.js')
|
|
32
41
|
const optionalSet = require('../optional-set.js')
|
|
33
42
|
const { checkEngine, checkPlatform } = require('npm-install-checks')
|
|
@@ -649,6 +658,45 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
649
658
|
return vuln.range
|
|
650
659
|
}
|
|
651
660
|
|
|
661
|
+
// Enforces the allow-git / allow-file / allow-directory / allow-remote configs at the arborist resolution layer, before any branching into the symlink (Link) path or the manifest-fetch path.
|
|
662
|
+
// Pacote also enforces these inside FetcherBase.get() as defense-in-depth, but the symlink branch never reaches pacote, and the manifest cache here would bypass pacote on a cached hit.
|
|
663
|
+
// Throws the same { code: EALLOW${TYPE} } shape pacote uses, so callers and downstream consumers stay consistent.
|
|
664
|
+
#checkAllow (spec, edge) {
|
|
665
|
+
const optName = ALLOW_OPTION_FOR_TYPE[spec.type]
|
|
666
|
+
if (!optName) {
|
|
667
|
+
return
|
|
668
|
+
}
|
|
669
|
+
const allow = this.options[optName] ?? 'all'
|
|
670
|
+
if (allow === 'all') {
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
const isRoot = !!(edge?.from?.isProjectRoot || edge?.from?.isWorkspace)
|
|
674
|
+
if (allow !== 'none' && isRoot) {
|
|
675
|
+
return
|
|
676
|
+
}
|
|
677
|
+
throw Object.assign(
|
|
678
|
+
new Error(`Fetching${allow === 'root' ? ' non-root' : ''} packages of type "${spec.type}" have been disabled`),
|
|
679
|
+
{
|
|
680
|
+
code: `EALLOW${spec.type.toUpperCase()}`,
|
|
681
|
+
package: spec.toString(),
|
|
682
|
+
}
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Builds a Node representing a spec we failed to load (allow-* gate, network failure, ENOTARGET, etc.) and records it in #loadFailures so #pruneFailedOptional can later decide whether the failure is fatal or silently dropped for optional deps.
|
|
687
|
+
#failureNode (name, parent, error, edge) {
|
|
688
|
+
error.requiredBy = edge?.from?.location || '.'
|
|
689
|
+
const n = new Node({
|
|
690
|
+
name,
|
|
691
|
+
parent,
|
|
692
|
+
error,
|
|
693
|
+
installLinks: this.installLinks,
|
|
694
|
+
legacyPeerDeps: this.legacyPeerDeps,
|
|
695
|
+
})
|
|
696
|
+
this.#loadFailures.add(n)
|
|
697
|
+
return n
|
|
698
|
+
}
|
|
699
|
+
|
|
652
700
|
#queueNamedUpdates () {
|
|
653
701
|
// ignore top nodes, since they are not loaded the same way, and
|
|
654
702
|
// probably have their own project associated with them.
|
|
@@ -663,7 +711,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
663
711
|
for (const node of this.idealTree.inventory.values()) {
|
|
664
712
|
// XXX add any invalid edgesOut to the queue
|
|
665
713
|
if (this[_updateNames].includes(node.name) &&
|
|
666
|
-
!node.isTop && !node.inDepBundle
|
|
714
|
+
!node.isTop && !node.inDepBundle) {
|
|
667
715
|
for (const edge of node.edgesIn) {
|
|
668
716
|
this.addTracker('idealTree', edge.from.name, edge.from.location)
|
|
669
717
|
this.#depsQueue.push(edge.from)
|
|
@@ -786,15 +834,11 @@ This is a one-time fix-up, please be patient...
|
|
|
786
834
|
const node = this.#depsQueue.pop()
|
|
787
835
|
const bd = node.package.bundleDependencies
|
|
788
836
|
const hasBundle = bd && Array.isArray(bd) && bd.length
|
|
789
|
-
const { hasShrinkwrap } = node
|
|
790
837
|
|
|
791
838
|
// if the node was already visited, or has since been removed from the
|
|
792
|
-
// tree, skip over it and process the rest of the queue.
|
|
793
|
-
// a shrinkwrap, also skip it, because it's going to get its deps
|
|
794
|
-
// satisfied by whatever's in that file anyway.
|
|
839
|
+
// tree, skip over it and process the rest of the queue.
|
|
795
840
|
if (this.#depsSeen.has(node) ||
|
|
796
|
-
node.root !== this.idealTree
|
|
797
|
-
hasShrinkwrap && !this.#complete) {
|
|
841
|
+
node.root !== this.idealTree) {
|
|
798
842
|
return this.#buildDepStep()
|
|
799
843
|
}
|
|
800
844
|
|
|
@@ -804,15 +848,15 @@ This is a one-time fix-up, please be patient...
|
|
|
804
848
|
|
|
805
849
|
// if we're loading a _complete_ ideal tree, for a --package-lock-only
|
|
806
850
|
// installation for example, we have to crack open the tarball and
|
|
807
|
-
// look inside if it has bundle deps
|
|
851
|
+
// look inside if it has bundle deps. note that this is
|
|
808
852
|
// not necessary during a reification, because we just update the
|
|
809
|
-
// ideal tree by reading bundles
|
|
853
|
+
// ideal tree by reading bundles in place.
|
|
810
854
|
// Don't bother if the node is from the actual tree and hasn't
|
|
811
855
|
// been resolved, because we can't fetch it anyway, could be anything!
|
|
812
856
|
const crackOpen = this.#complete &&
|
|
813
857
|
node !== this.idealTree &&
|
|
814
858
|
node.resolved &&
|
|
815
|
-
|
|
859
|
+
hasBundle &&
|
|
816
860
|
!node.inert
|
|
817
861
|
if (crackOpen) {
|
|
818
862
|
const Arborist = this.constructor
|
|
@@ -825,15 +869,8 @@ This is a one-time fix-up, please be patient...
|
|
|
825
869
|
integrity: node.integrity,
|
|
826
870
|
})
|
|
827
871
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
.loadVirtual({ root: node })
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
if (hasBundle) {
|
|
834
|
-
await new Arborist({ ...this.options, path })
|
|
835
|
-
.loadActual({ root: node, ignoreMissing: true })
|
|
836
|
-
}
|
|
872
|
+
await new Arborist({ ...this.options, path })
|
|
873
|
+
.loadActual({ root: node, ignoreMissing: true })
|
|
837
874
|
})
|
|
838
875
|
}
|
|
839
876
|
|
|
@@ -1040,7 +1077,7 @@ This is a one-time fix-up, please be patient...
|
|
|
1040
1077
|
// This can't be changed or removed till we figure out why
|
|
1041
1078
|
// The test is named "tarball deps with transitive tarball deps"
|
|
1042
1079
|
promises.push(() =>
|
|
1043
|
-
this.#fetchManifest(npa.resolve(e.name, e.spec, fromPath(placed, e)), parent)
|
|
1080
|
+
this.#fetchManifest(npa.resolve(e.name, e.spec, fromPath(placed, e)), parent, e)
|
|
1044
1081
|
.catch(() => null)
|
|
1045
1082
|
)
|
|
1046
1083
|
}
|
|
@@ -1180,11 +1217,6 @@ This is a one-time fix-up, please be patient...
|
|
|
1180
1217
|
continue
|
|
1181
1218
|
}
|
|
1182
1219
|
|
|
1183
|
-
// If it's shrinkwrapped, we use what the shrinkwap wants.
|
|
1184
|
-
if (edge.to && edge.to.inShrinkwrap) {
|
|
1185
|
-
continue
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
1220
|
// If the edge has no destination, that's a problem, unless
|
|
1189
1221
|
// if it's peerOptional and not explicitly requested.
|
|
1190
1222
|
if (!edge.to) {
|
|
@@ -1231,12 +1263,14 @@ This is a one-time fix-up, please be patient...
|
|
|
1231
1263
|
return problems
|
|
1232
1264
|
}
|
|
1233
1265
|
|
|
1234
|
-
async #fetchManifest (spec, parent) {
|
|
1266
|
+
async #fetchManifest (spec, parent, edge) {
|
|
1267
|
+
// Enforce allow-* gates before consulting the manifest cache so a cached entry from a different edge cannot bypass the policy.
|
|
1268
|
+
this.#checkAllow(spec, edge)
|
|
1235
1269
|
const options = {
|
|
1236
1270
|
...this.options,
|
|
1237
1271
|
avoid: this.#avoidRange(spec.name),
|
|
1238
1272
|
fullMetadata: true,
|
|
1239
|
-
_isRoot:
|
|
1273
|
+
_isRoot: !!(edge?.from?.isProjectRoot || edge?.from?.isWorkspace),
|
|
1240
1274
|
}
|
|
1241
1275
|
// get the intended spec and stored metadata from yarn.lock file,
|
|
1242
1276
|
// if available and valid.
|
|
@@ -1253,6 +1287,14 @@ This is a one-time fix-up, please be patient...
|
|
|
1253
1287
|
}
|
|
1254
1288
|
|
|
1255
1289
|
async #nodeFromSpec (name, spec, parent, edge) {
|
|
1290
|
+
// Enforce allow-git / allow-file / allow-directory / allow-remote before any branching, so the symlink (Link) path is enforced as well as the manifest-fetch path.
|
|
1291
|
+
// Route the failure through #loadFailures so optional-dep semantics apply (e.g. a transitive optionalDependencies entry that resolves to a disallowed git URL is silently dropped rather than failing the install).
|
|
1292
|
+
try {
|
|
1293
|
+
this.#checkAllow(spec, edge)
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
return this.#failureNode(name, parent, error, edge)
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1256
1298
|
// pacote will slap integrity on its options, so we have to clone the object so it doesn't get mutated.
|
|
1257
1299
|
// Don't bother to load the manifest for link deps, because the target might be within another package that doesn't exist yet.
|
|
1258
1300
|
const { installLinks, legacyPeerDeps } = this
|
|
@@ -1307,23 +1349,26 @@ This is a one-time fix-up, please be patient...
|
|
|
1307
1349
|
|
|
1308
1350
|
// spec isn't a directory, and either isn't a workspace or the workspace we have
|
|
1309
1351
|
// doesn't satisfy the edge. try to fetch a manifest and build a node from that.
|
|
1310
|
-
return this.#fetchManifest(spec, parent)
|
|
1311
|
-
.then(
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1352
|
+
return this.#fetchManifest(spec, parent, edge)
|
|
1353
|
+
.then(
|
|
1354
|
+
pkg => {
|
|
1355
|
+
// When a proxy/upstream registry returns an incomplete manifest
|
|
1356
|
+
// (e.g. missing version field for platform-specific packages it
|
|
1357
|
+
// hasn't cached), treat it as a load failure so that optional deps
|
|
1358
|
+
// are properly pruned instead of written to the lockfile without
|
|
1359
|
+
// version metadata. Only apply to registry specs — file: deps
|
|
1360
|
+
// legitimately omit version.
|
|
1361
|
+
if (!pkg.version && spec.registry) {
|
|
1362
|
+
const error = Object.assign(
|
|
1363
|
+
new Error(`incomplete manifest for ${name}, missing version`),
|
|
1364
|
+
{ code: 'EINCOMPLETEMANIFEST' }
|
|
1365
|
+
)
|
|
1366
|
+
return this.#failureNode(name, parent, error, edge)
|
|
1367
|
+
}
|
|
1368
|
+
return new Node({ name, pkg, parent, installLinks, legacyPeerDeps })
|
|
1369
|
+
},
|
|
1370
|
+
error => this.#failureNode(name, parent, error, edge)
|
|
1371
|
+
)
|
|
1327
1372
|
}
|
|
1328
1373
|
|
|
1329
1374
|
// load all peer deps and meta-peer deps into the node's parent
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const { mkdirSync } = require('node:fs')
|
|
2
|
-
const pacote = require('pacote')
|
|
3
1
|
const { join } = require('node:path')
|
|
4
2
|
const { depth } = require('treeverse')
|
|
5
3
|
const crypto = require('node:crypto')
|
|
@@ -97,7 +95,9 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
97
95
|
}
|
|
98
96
|
this.counter = 0
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
// Skip extraneous fsChildren: workspaces removed from the root manifest can linger in fsChildren via the lockfile, and re-materializing them here would re-create a directory the user just deleted.
|
|
99
|
+
const fsChildren = Array.from(idealTree.fsChildren.values()).filter(w => !w.extraneous)
|
|
100
|
+
this.idealGraph.workspaces = await Promise.all(fsChildren.map(w => this.#workspaceProxy(w)))
|
|
101
101
|
const processed = new Set()
|
|
102
102
|
const queue = [idealTree, ...idealTree.fsChildren]
|
|
103
103
|
while (queue.length !== 0) {
|
|
@@ -147,50 +147,14 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
147
147
|
const result = {}
|
|
148
148
|
// XXX this goes recursive if we don't set here because assignCommonProperties also calls this.#externalProxy
|
|
149
149
|
this.#externalProxies.set(node, result)
|
|
150
|
-
await this.#assignCommonProperties(node, result
|
|
151
|
-
if (node.hasShrinkwrap) {
|
|
152
|
-
const dir = join(
|
|
153
|
-
node.root.path,
|
|
154
|
-
'node_modules',
|
|
155
|
-
'.store',
|
|
156
|
-
`${node.packageName}@${node.version}`
|
|
157
|
-
)
|
|
158
|
-
mkdirSync(dir, { recursive: true })
|
|
159
|
-
// TODO this approach feels wrong and shouldn't be necessary for shrinkwraps
|
|
160
|
-
await pacote.extract(node.resolved, dir, {
|
|
161
|
-
...this.options,
|
|
162
|
-
resolved: node.resolved,
|
|
163
|
-
integrity: node.integrity,
|
|
164
|
-
// TODO _isRoot
|
|
165
|
-
})
|
|
166
|
-
const Arborist = this.constructor
|
|
167
|
-
const arb = new Arborist({ ...this.options, path: dir })
|
|
168
|
-
// Make sure that the ideal tree is build as the rest of the algorithm depends on it.
|
|
169
|
-
await arb.buildIdealTree({
|
|
170
|
-
complete: false,
|
|
171
|
-
dev: false,
|
|
172
|
-
})
|
|
173
|
-
await arb.makeIdealGraph()
|
|
174
|
-
this.idealGraph.external.push(...arb.idealGraph.external)
|
|
175
|
-
for (const edge of arb.idealGraph.external) {
|
|
176
|
-
edge.root = this.idealGraph
|
|
177
|
-
edge.id = `${node.id}=>${edge.id}`
|
|
178
|
-
}
|
|
179
|
-
result.localDependencies = []
|
|
180
|
-
result.externalDependencies = arb.idealGraph.externalDependencies
|
|
181
|
-
result.externalOptionalDependencies = arb.idealGraph.externalOptionalDependencies
|
|
182
|
-
result.dependencies = [
|
|
183
|
-
...result.externalDependencies,
|
|
184
|
-
...result.externalOptionalDependencies,
|
|
185
|
-
]
|
|
186
|
-
}
|
|
150
|
+
await this.#assignCommonProperties(node, result)
|
|
187
151
|
result.optional = node.optional
|
|
188
152
|
result.resolved = node.resolved
|
|
189
153
|
result.version = node.version
|
|
190
154
|
return result
|
|
191
155
|
}
|
|
192
156
|
|
|
193
|
-
async #assignCommonProperties (node, result
|
|
157
|
+
async #assignCommonProperties (node, result) {
|
|
194
158
|
result.root = this.idealGraph
|
|
195
159
|
// XXX does anything need this?
|
|
196
160
|
result.id = this.counter++
|
|
@@ -200,10 +164,6 @@ module.exports = cls => class IsolatedReifier extends cls {
|
|
|
200
164
|
result.package = { ...node.package }
|
|
201
165
|
result.package.bundleDependencies = undefined
|
|
202
166
|
|
|
203
|
-
if (!populateDeps) {
|
|
204
|
-
return
|
|
205
|
-
}
|
|
206
|
-
|
|
207
167
|
let edges = [...node.edgesOut.values()].filter(edge =>
|
|
208
168
|
edge.to?.target &&
|
|
209
169
|
!(node.package.bundledDependencies || node.package.bundleDependencies)?.includes(edge.to.name)
|
|
@@ -39,7 +39,7 @@ module.exports = cls => class VirtualLoader extends cls {
|
|
|
39
39
|
resolveOptions: this.options,
|
|
40
40
|
})
|
|
41
41
|
if (!s.loadedFromDisk && !options.root) {
|
|
42
|
-
const er = new Error('loadVirtual requires existing
|
|
42
|
+
const er = new Error('loadVirtual requires existing package-lock.json file')
|
|
43
43
|
throw Object.assign(er, { code: 'ENOLOCK' })
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -244,7 +244,6 @@ To fix:
|
|
|
244
244
|
integrity: sw.integrity,
|
|
245
245
|
resolved: consistentResolve(sw.resolved, this.path, path),
|
|
246
246
|
pkg: sw,
|
|
247
|
-
hasShrinkwrap: sw.hasShrinkwrap,
|
|
248
247
|
loadOverrides,
|
|
249
248
|
// cast to boolean because they're undefined in the lock file when false
|
|
250
249
|
extraneous: !!sw.extraneous,
|
package/lib/arborist/reify.js
CHANGED
|
@@ -4,6 +4,7 @@ const hgi = require('hosted-git-info')
|
|
|
4
4
|
const npa = require('npm-package-arg')
|
|
5
5
|
const packageContents = require('@npmcli/installed-package-contents')
|
|
6
6
|
const pacote = require('pacote')
|
|
7
|
+
const { pickRegistry } = require('npm-registry-fetch')
|
|
7
8
|
const promiseAllRejectLate = require('promise-all-reject-late')
|
|
8
9
|
const runScript = require('@npmcli/run-script')
|
|
9
10
|
const { callLimit: promiseCallLimit } = require('promise-call-limit')
|
|
@@ -47,7 +48,6 @@ const _checkBins = Symbol.for('checkBins')
|
|
|
47
48
|
// TODO tests should not be this deep into internals
|
|
48
49
|
const _diffTrees = Symbol.for('diffTrees')
|
|
49
50
|
const _createSparseTree = Symbol.for('createSparseTree')
|
|
50
|
-
const _loadShrinkwrapsAndUpdateTrees = Symbol.for('loadShrinkwrapsAndUpdateTrees')
|
|
51
51
|
const _reifyNode = Symbol.for('reifyNode')
|
|
52
52
|
const _updateAll = Symbol.for('updateAll')
|
|
53
53
|
const _updateNames = Symbol.for('updateNames')
|
|
@@ -72,7 +72,6 @@ module.exports = cls => class Reifier extends cls {
|
|
|
72
72
|
#omit
|
|
73
73
|
#retiredPaths = {}
|
|
74
74
|
#retiredUnchanged = {}
|
|
75
|
-
#shrinkwrapInflated = new Set()
|
|
76
75
|
#sparseTreeDirs = new Set()
|
|
77
76
|
#sparseTreeRoots = new Set()
|
|
78
77
|
#linkedActualForDiff = null
|
|
@@ -305,7 +304,6 @@ module.exports = cls => class Reifier extends cls {
|
|
|
305
304
|
]],
|
|
306
305
|
[_rollbackCreateSparseTree, [
|
|
307
306
|
_createSparseTree,
|
|
308
|
-
_loadShrinkwrapsAndUpdateTrees,
|
|
309
307
|
_loadBundlesAndUpdateTrees,
|
|
310
308
|
_submitQuickAudit,
|
|
311
309
|
_unpackNewModules,
|
|
@@ -465,7 +463,6 @@ module.exports = cls => class Reifier extends cls {
|
|
|
465
463
|
// and ideal trees.
|
|
466
464
|
this.diff = Diff.calculate({
|
|
467
465
|
omit: this.#omit,
|
|
468
|
-
shrinkwrapInflated: this.#shrinkwrapInflated,
|
|
469
466
|
filterNodes,
|
|
470
467
|
actual: this.#linkedActualForDiff || this.actualTree,
|
|
471
468
|
ideal: this.idealTree,
|
|
@@ -616,39 +613,6 @@ module.exports = cls => class Reifier extends cls {
|
|
|
616
613
|
.then(() => this[_rollbackRetireShallowNodes](er))
|
|
617
614
|
}
|
|
618
615
|
|
|
619
|
-
// shrinkwrap nodes define their dependency branches with a file, so
|
|
620
|
-
// we need to unpack them, read that shrinkwrap file, and then update
|
|
621
|
-
// the tree by calling loadVirtual with the node as the root.
|
|
622
|
-
[_loadShrinkwrapsAndUpdateTrees] () {
|
|
623
|
-
const seen = this.#shrinkwrapInflated
|
|
624
|
-
const shrinkwraps = this.diff.leaves
|
|
625
|
-
.filter(d => (d.action === 'CHANGE' || d.action === 'ADD' || !d.action) &&
|
|
626
|
-
d.ideal.hasShrinkwrap && !seen.has(d.ideal) &&
|
|
627
|
-
!this[_trashList].has(d.ideal.path))
|
|
628
|
-
|
|
629
|
-
if (!shrinkwraps.length) {
|
|
630
|
-
return
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const timeEnd = time.start('reify:loadShrinkwraps')
|
|
634
|
-
|
|
635
|
-
const Arborist = this.constructor
|
|
636
|
-
return promiseAllRejectLate(shrinkwraps.map(diff => {
|
|
637
|
-
const node = diff.ideal
|
|
638
|
-
seen.add(node)
|
|
639
|
-
return diff.action ? this[_reifyNode](node) : node
|
|
640
|
-
}))
|
|
641
|
-
.then(nodes => promiseAllRejectLate(nodes.map(node => new Arborist({
|
|
642
|
-
...this.options,
|
|
643
|
-
path: node.path,
|
|
644
|
-
}).loadVirtual({ root: node }))))
|
|
645
|
-
// reload the diff and sparse tree because the ideal tree changed
|
|
646
|
-
.then(() => this[_diffTrees]())
|
|
647
|
-
.then(() => this[_createSparseTree]())
|
|
648
|
-
.then(() => this[_loadShrinkwrapsAndUpdateTrees]())
|
|
649
|
-
.then(timeEnd)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
616
|
// create a symlink for Links, extract for Nodes
|
|
653
617
|
// return the node object, since we usually want that
|
|
654
618
|
// handle optional dep failures here
|
|
@@ -741,7 +705,14 @@ module.exports = cls => class Reifier extends cls {
|
|
|
741
705
|
...this.options,
|
|
742
706
|
resolved: node.resolved,
|
|
743
707
|
integrity: node.integrity,
|
|
744
|
-
|
|
708
|
+
// A node counts as "root" for allow-* enforcement if it satisfies at least one valid dependency edge declared by the project root or a workspace.
|
|
709
|
+
// node.parent is unsafe here: after hoisting, transitive packages can have the project root as their tree parent.
|
|
710
|
+
_isRoot: [...node.edgesIn].some(e =>
|
|
711
|
+
e.valid && (e.from?.isProjectRoot || e.from?.isWorkspace)
|
|
712
|
+
),
|
|
713
|
+
// pacote's npa re-parses our `name@URL` spec as type=remote, so allowRemote would mis-fire on registry tarballs.
|
|
714
|
+
// Override only when we can prove the URL is registry-mediated; see #isRegistryResolvedTarball.
|
|
715
|
+
...(this.#isRegistryResolvedTarball(node) ? { allowRemote: 'all' } : {}),
|
|
745
716
|
})
|
|
746
717
|
// store nodes don't use Node class so node.package doesn't get updated
|
|
747
718
|
if (node.isInStore) {
|
|
@@ -865,6 +836,24 @@ module.exports = cls => class Reifier extends cls {
|
|
|
865
836
|
return wrapper
|
|
866
837
|
}
|
|
867
838
|
|
|
839
|
+
// When extracting a registry-resolved package, the spec we hand to pacote is name@URL.
|
|
840
|
+
// pacote re-parses that with npa and gets spec.type === 'remote', so without an override the allow-remote gate would fire on every registry tarball (both =none and =root mis-fire).
|
|
841
|
+
// Returns true only when we are confident this is a registry-mediated install: the node's inbound edges must all be registry-typed (no exotic spec smuggled the URL in) AND the resolved URL's host must match the registry npm-registry-fetch selected for this spec, so a tampered lockfile pointing at an attacker host still hits the gate.
|
|
842
|
+
#isRegistryResolvedTarball (node) {
|
|
843
|
+
if (!node.resolved || !node.isRegistryDependency) {
|
|
844
|
+
return false
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
// Hostnames are case-insensitive; lowercase both sides for safety even though WHATWG URL already normalizes.
|
|
848
|
+
const resolvedHost = new URL(node.resolved).hostname.toLowerCase()
|
|
849
|
+
// pickRegistry only consults spec.scope, so a bare-name (tag) parse is sufficient and avoids a node.version dependency.
|
|
850
|
+
const registryHost = new URL(pickRegistry(npa(node.name), this.options)).hostname.toLowerCase()
|
|
851
|
+
return resolvedHost === registryHost
|
|
852
|
+
} catch {
|
|
853
|
+
return false
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
868
857
|
#registryResolved (resolved) {
|
|
869
858
|
// the default registry url is a magic value meaning "the currently
|
|
870
859
|
// configured registry".
|
|
@@ -1150,7 +1139,6 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1150
1139
|
|
|
1151
1140
|
const node = diff.ideal
|
|
1152
1141
|
const bd = this.#bundleUnpacked.has(node)
|
|
1153
|
-
const sw = this.#shrinkwrapInflated.has(node)
|
|
1154
1142
|
const bundleMissing = this.#bundleMissing.has(node)
|
|
1155
1143
|
|
|
1156
1144
|
// check whether we still need to unpack this one.
|
|
@@ -1160,8 +1148,6 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1160
1148
|
!node.isRoot &&
|
|
1161
1149
|
// already unpacked to read bundle
|
|
1162
1150
|
!bd &&
|
|
1163
|
-
// already unpacked to read sw
|
|
1164
|
-
!sw &&
|
|
1165
1151
|
// already unpacked by another dep's bundle
|
|
1166
1152
|
(bundleMissing || !node.inDepBundle)
|
|
1167
1153
|
|
package/lib/diff.js
CHANGED
|
@@ -11,10 +11,9 @@ const { existsSync } = require('node:fs')
|
|
|
11
11
|
const ssri = require('ssri')
|
|
12
12
|
|
|
13
13
|
class Diff {
|
|
14
|
-
constructor ({ actual, ideal, filterSet,
|
|
14
|
+
constructor ({ actual, ideal, filterSet, omit }) {
|
|
15
15
|
this.omit = omit
|
|
16
16
|
this.filterSet = filterSet
|
|
17
|
-
this.shrinkwrapInflated = shrinkwrapInflated
|
|
18
17
|
this.children = []
|
|
19
18
|
this.actual = actual
|
|
20
19
|
this.ideal = ideal
|
|
@@ -36,7 +35,6 @@ class Diff {
|
|
|
36
35
|
actual,
|
|
37
36
|
ideal,
|
|
38
37
|
filterNodes = [],
|
|
39
|
-
shrinkwrapInflated = new Set(),
|
|
40
38
|
omit = new Set(),
|
|
41
39
|
}) {
|
|
42
40
|
// if there's a filterNode, then:
|
|
@@ -102,7 +100,7 @@ class Diff {
|
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
return depth({
|
|
105
|
-
tree: new Diff({ actual, ideal, filterSet,
|
|
103
|
+
tree: new Diff({ actual, ideal, filterSet, omit }),
|
|
106
104
|
getChildren,
|
|
107
105
|
leave,
|
|
108
106
|
})
|
|
@@ -191,26 +189,16 @@ const getChildren = diff => {
|
|
|
191
189
|
unchanged,
|
|
192
190
|
removed,
|
|
193
191
|
filterSet,
|
|
194
|
-
shrinkwrapInflated,
|
|
195
192
|
omit,
|
|
196
193
|
} = diff
|
|
197
194
|
|
|
198
195
|
// Note: we DON'T diff fsChildren themselves, because they are either
|
|
199
|
-
// included in the package contents, or part of some other project
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// responsible for installing.
|
|
196
|
+
// included in the package contents, or part of some other project.
|
|
197
|
+
// But we _do_ include the child nodes of fsChildren, because those are
|
|
198
|
+
// nodes that we are typically responsible for installing.
|
|
203
199
|
const actualKids = allChildren(actual)
|
|
204
200
|
const idealKids = allChildren(ideal)
|
|
205
201
|
|
|
206
|
-
if (ideal && ideal.hasShrinkwrap && !shrinkwrapInflated.has(ideal)) {
|
|
207
|
-
// Guaranteed to get a diff.leaves here, because we always
|
|
208
|
-
// be called with a proper Diff object when ideal has a shrinkwrap
|
|
209
|
-
// that has not been inflated.
|
|
210
|
-
diff.leaves.push(diff)
|
|
211
|
-
return children
|
|
212
|
-
}
|
|
213
|
-
|
|
214
202
|
const paths = new Set([...actualKids.keys(), ...idealKids.keys()])
|
|
215
203
|
for (const path of paths) {
|
|
216
204
|
const actual = actualKids.get(path)
|
|
@@ -222,7 +210,6 @@ const getChildren = diff => {
|
|
|
222
210
|
unchanged,
|
|
223
211
|
removed,
|
|
224
212
|
filterSet,
|
|
225
|
-
shrinkwrapInflated,
|
|
226
213
|
omit,
|
|
227
214
|
})
|
|
228
215
|
}
|
|
@@ -241,7 +228,6 @@ const diffNode = ({
|
|
|
241
228
|
unchanged,
|
|
242
229
|
removed,
|
|
243
230
|
filterSet,
|
|
244
|
-
shrinkwrapInflated,
|
|
245
231
|
omit,
|
|
246
232
|
}) => {
|
|
247
233
|
if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual))) {
|
|
@@ -264,11 +250,11 @@ const diffNode = ({
|
|
|
264
250
|
|
|
265
251
|
// if it's a match, then get its children
|
|
266
252
|
// otherwise, this is the child diff node
|
|
267
|
-
if (action
|
|
253
|
+
if (action) {
|
|
268
254
|
if (action === 'REMOVE') {
|
|
269
255
|
removed.push(actual)
|
|
270
256
|
}
|
|
271
|
-
children.push(new Diff({ actual, ideal, filterSet,
|
|
257
|
+
children.push(new Diff({ actual, ideal, filterSet, omit }))
|
|
272
258
|
} else {
|
|
273
259
|
unchanged.push(ideal)
|
|
274
260
|
// !*! Weird dirty hack warning !*!
|
|
@@ -307,7 +293,6 @@ const diffNode = ({
|
|
|
307
293
|
unchanged,
|
|
308
294
|
removed,
|
|
309
295
|
filterSet,
|
|
310
|
-
shrinkwrapInflated,
|
|
311
296
|
omit,
|
|
312
297
|
}))
|
|
313
298
|
}
|
package/lib/edge.js
CHANGED
|
@@ -109,8 +109,8 @@ class Edge {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// NOTE: this condition means we explicitly do not support overriding
|
|
112
|
-
// bundled
|
|
113
|
-
if (node.
|
|
112
|
+
// bundled dependencies
|
|
113
|
+
if (node.inDepBundle) {
|
|
114
114
|
return depValid(node, this.rawSpec, this.#accept, this.#from)
|
|
115
115
|
}
|
|
116
116
|
|
package/lib/isolated-classes.js
CHANGED
package/lib/link.js
CHANGED
|
@@ -109,12 +109,24 @@ class Link extends Node {
|
|
|
109
109
|
// so this is a no-op
|
|
110
110
|
[_loadDeps] () {}
|
|
111
111
|
|
|
112
|
-
// When a Link receives overrides (via edgesIn), forward them to the target node which holds the actual edgesOut.
|
|
113
|
-
// Without this,
|
|
112
|
+
// When a Link receives overrides (via edgesIn), forward them to the target node which holds the actual edgesOut — but only when the OverrideSet has at least one rule that names a dep the target actually depends on.
|
|
113
|
+
// Without this scope, the link forwards a generic ancestor OverrideSet that has no real effect on the target's edges, but still flips the target to "has overrides", which changes downstream `canReplaceWith` / placement decisions and causes `npm ci` to re-resolve lockfile-pinned edges from the registry.
|
|
114
|
+
// See npm/cli#9357.
|
|
114
115
|
recalculateOutEdgesOverrides () {
|
|
115
|
-
if (this.target) {
|
|
116
|
-
|
|
116
|
+
if (!this.target || !this.overrides) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
let hasMatchingRule = false
|
|
120
|
+
for (const rule of this.overrides.ruleset.values()) {
|
|
121
|
+
if (this.target.edgesOut.has(rule.name)) {
|
|
122
|
+
hasMatchingRule = true
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!hasMatchingRule) {
|
|
127
|
+
return
|
|
117
128
|
}
|
|
129
|
+
this.target.updateOverridesEdgeInAdded(this.overrides)
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
// links can't have children, only their targets can
|
package/lib/node.js
CHANGED
|
@@ -82,7 +82,6 @@ class Node {
|
|
|
82
82
|
fsChildren,
|
|
83
83
|
fsParent,
|
|
84
84
|
global = false,
|
|
85
|
-
hasShrinkwrap,
|
|
86
85
|
inert = false,
|
|
87
86
|
installLinks = false,
|
|
88
87
|
integrity,
|
|
@@ -170,7 +169,6 @@ class Node {
|
|
|
170
169
|
}
|
|
171
170
|
}
|
|
172
171
|
this.integrity = integrity || this.package._integrity || null
|
|
173
|
-
this.hasShrinkwrap = hasShrinkwrap || this.package._hasShrinkwrap || false
|
|
174
172
|
this.installLinks = installLinks
|
|
175
173
|
this.legacyPeerDeps = legacyPeerDeps
|
|
176
174
|
|
|
@@ -1101,8 +1099,8 @@ class Node {
|
|
|
1101
1099
|
// is depending on it would be fine with the thing that they would resolve
|
|
1102
1100
|
// to if it was removed, or nothing is depending on it in the first place.
|
|
1103
1101
|
canDedupe (preferDedupe = false, explicitRequest = false) {
|
|
1104
|
-
// not allowed to mess with
|
|
1105
|
-
if (this.inDepBundle
|
|
1102
|
+
// not allowed to mess with bundles
|
|
1103
|
+
if (this.inDepBundle) {
|
|
1106
1104
|
return false
|
|
1107
1105
|
}
|
|
1108
1106
|
|
|
@@ -1249,11 +1247,6 @@ class Node {
|
|
|
1249
1247
|
treeCheck(this)
|
|
1250
1248
|
}
|
|
1251
1249
|
|
|
1252
|
-
get inShrinkwrap () {
|
|
1253
|
-
return this.parent &&
|
|
1254
|
-
(this.parent.hasShrinkwrap || this.parent.inShrinkwrap)
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
1250
|
get parent () {
|
|
1258
1251
|
// setter prevents _parent from being this
|
|
1259
1252
|
return this[_parent]
|
package/lib/printable.js
CHANGED
|
@@ -52,11 +52,6 @@ class ArboristNode {
|
|
|
52
52
|
if (bd && bd.length) {
|
|
53
53
|
this.bundleDependencies = bd
|
|
54
54
|
}
|
|
55
|
-
if (tree.inShrinkwrap) {
|
|
56
|
-
this.inShrinkwrap = true
|
|
57
|
-
} else if (tree.hasShrinkwrap) {
|
|
58
|
-
this.hasShrinkwrap = true
|
|
59
|
-
}
|
|
60
55
|
if (tree.error) {
|
|
61
56
|
this.error = treeError(tree.error)
|
|
62
57
|
}
|
package/lib/shrinkwrap.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// a module that manages a
|
|
2
|
-
// package-lock.json).
|
|
1
|
+
// a module that manages a lockfile (package-lock.json).
|
|
3
2
|
|
|
4
3
|
// Increment whenever the lockfile version updates
|
|
5
4
|
// v1 - npm <=6
|
|
@@ -98,7 +97,6 @@ const pkgMetaKeys = [
|
|
|
98
97
|
'libc',
|
|
99
98
|
'_integrity',
|
|
100
99
|
'license',
|
|
101
|
-
'_hasShrinkwrap',
|
|
102
100
|
'hasInstallScript',
|
|
103
101
|
'bin',
|
|
104
102
|
'deprecated',
|
|
@@ -108,7 +106,6 @@ const pkgMetaKeys = [
|
|
|
108
106
|
const nodeMetaKeys = [
|
|
109
107
|
'integrity',
|
|
110
108
|
'inBundle',
|
|
111
|
-
'hasShrinkwrap',
|
|
112
109
|
'hasInstallScript',
|
|
113
110
|
]
|
|
114
111
|
|
|
@@ -199,17 +196,14 @@ class Shrinkwrap {
|
|
|
199
196
|
const s = new Shrinkwrap(options)
|
|
200
197
|
s.reset()
|
|
201
198
|
|
|
202
|
-
const [
|
|
199
|
+
const [lock] = await s.resetFiles
|
|
203
200
|
|
|
204
|
-
// XXX this is duplicated in this.load(), but using loadFiles instead of resetFiles
|
|
205
201
|
if (s.hiddenLockfile) {
|
|
206
202
|
s.filename = resolve(s.path, 'node_modules/.package-lock.json')
|
|
207
|
-
} else if (s.shrinkwrapOnly || sw) {
|
|
208
|
-
s.filename = resolve(s.path, 'npm-shrinkwrap.json')
|
|
209
203
|
} else {
|
|
210
204
|
s.filename = resolve(s.path, 'package-lock.json')
|
|
211
205
|
}
|
|
212
|
-
s.loadedFromDisk = !!
|
|
206
|
+
s.loadedFromDisk = !!lock
|
|
213
207
|
// TODO what uses this?
|
|
214
208
|
s.type = basename(s.filename)
|
|
215
209
|
|
|
@@ -286,7 +280,6 @@ class Shrinkwrap {
|
|
|
286
280
|
path,
|
|
287
281
|
indent = 2,
|
|
288
282
|
newline = '\n',
|
|
289
|
-
shrinkwrapOnly = false,
|
|
290
283
|
hiddenLockfile = false,
|
|
291
284
|
lockfileVersion,
|
|
292
285
|
resolveOptions = {},
|
|
@@ -312,8 +305,6 @@ class Shrinkwrap {
|
|
|
312
305
|
this.hiddenLockfile = hiddenLockfile
|
|
313
306
|
this.loadingError = null
|
|
314
307
|
this.resolveOptions = resolveOptions
|
|
315
|
-
// only load npm-shrinkwrap.json in dep trees, not package-lock
|
|
316
|
-
this.shrinkwrapOnly = shrinkwrapOnly
|
|
317
308
|
}
|
|
318
309
|
|
|
319
310
|
// check to see if a spec is present in the yarn.lock file, and if so,
|
|
@@ -369,14 +360,10 @@ class Shrinkwrap {
|
|
|
369
360
|
|
|
370
361
|
// files to potentially read from and write to, in order of priority
|
|
371
362
|
get #filenameSet () {
|
|
372
|
-
if (this.shrinkwrapOnly) {
|
|
373
|
-
return [`${this.path}/npm-shrinkwrap.json`]
|
|
374
|
-
}
|
|
375
363
|
if (this.hiddenLockfile) {
|
|
376
364
|
return [`${this.path}/node_modules/.package-lock.json`]
|
|
377
365
|
}
|
|
378
366
|
return [
|
|
379
|
-
`${this.path}/npm-shrinkwrap.json`,
|
|
380
367
|
`${this.path}/package-lock.json`,
|
|
381
368
|
`${this.path}/yarn.lock`,
|
|
382
369
|
]
|
|
@@ -396,9 +383,9 @@ class Shrinkwrap {
|
|
|
396
383
|
}
|
|
397
384
|
|
|
398
385
|
get resetFiles () {
|
|
399
|
-
// slice out yarn, we only care about lock
|
|
386
|
+
// slice out yarn, we only care about the package-lock when checking
|
|
400
387
|
// this way, since we're not actually loading the full lock metadata
|
|
401
|
-
return Promise.all(this.#filenameSet.slice(0,
|
|
388
|
+
return Promise.all(this.#filenameSet.slice(0, 1)
|
|
402
389
|
.map(file => file && stat(file).then(st => st.isFile(), er => {
|
|
403
390
|
/* istanbul ignore else - can't test without breaking module itself */
|
|
404
391
|
if (er.code === 'ENOENT') {
|
|
@@ -425,25 +412,18 @@ class Shrinkwrap {
|
|
|
425
412
|
}
|
|
426
413
|
|
|
427
414
|
async load () {
|
|
428
|
-
// we don't need to load package-lock.json except for top of tree nodes,
|
|
429
|
-
// only npm-shrinkwrap.json.
|
|
430
415
|
let data
|
|
431
416
|
try {
|
|
432
|
-
const [
|
|
433
|
-
data =
|
|
417
|
+
const [lock, yarn] = await this.loadFiles
|
|
418
|
+
data = lock || '{}'
|
|
434
419
|
|
|
435
|
-
// use shrinkwrap only for deps; otherwise, prefer package-lock
|
|
436
|
-
// and ignore npm-shrinkwrap if both are present.
|
|
437
|
-
// TODO: emit a warning here or something if both are present.
|
|
438
420
|
if (this.hiddenLockfile) {
|
|
439
421
|
this.filename = resolve(this.path, 'node_modules/.package-lock.json')
|
|
440
|
-
} else if (this.shrinkwrapOnly || sw) {
|
|
441
|
-
this.filename = resolve(this.path, 'npm-shrinkwrap.json')
|
|
442
422
|
} else {
|
|
443
423
|
this.filename = resolve(this.path, 'package-lock.json')
|
|
444
424
|
}
|
|
445
425
|
this.type = basename(this.filename)
|
|
446
|
-
this.loadedFromDisk = Boolean(
|
|
426
|
+
this.loadedFromDisk = Boolean(lock)
|
|
447
427
|
|
|
448
428
|
if (yarn) {
|
|
449
429
|
this.yarnLock = new YarnLock()
|
|
@@ -809,7 +789,6 @@ class Shrinkwrap {
|
|
|
809
789
|
const {
|
|
810
790
|
resolved,
|
|
811
791
|
integrity,
|
|
812
|
-
hasShrinkwrap,
|
|
813
792
|
version,
|
|
814
793
|
} = this.get(node.path)
|
|
815
794
|
|
|
@@ -836,17 +815,14 @@ class Shrinkwrap {
|
|
|
836
815
|
if (allOk) {
|
|
837
816
|
node.resolved = node.resolved || pathFixed || null
|
|
838
817
|
node.integrity = node.integrity || integrity || null
|
|
839
|
-
node.hasShrinkwrap = node.hasShrinkwrap || hasShrinkwrap || false
|
|
840
818
|
} else {
|
|
841
819
|
// try to read off the package or node itself
|
|
842
820
|
const {
|
|
843
821
|
resolved,
|
|
844
822
|
integrity,
|
|
845
|
-
hasShrinkwrap,
|
|
846
823
|
} = Shrinkwrap.metaFromNode(node, this.path, this.resolveOptions)
|
|
847
824
|
node.resolved = node.resolved || resolved || null
|
|
848
825
|
node.integrity = node.integrity || integrity || null
|
|
849
|
-
node.hasShrinkwrap = node.hasShrinkwrap || hasShrinkwrap || false
|
|
850
826
|
}
|
|
851
827
|
}
|
|
852
828
|
this.#awaitingUpdate.set(loc, node)
|
|
@@ -929,10 +905,24 @@ class Shrinkwrap {
|
|
|
929
905
|
continue
|
|
930
906
|
}
|
|
931
907
|
const loc = relpath(this.path, node.path)
|
|
932
|
-
|
|
908
|
+
// Drop lockfile entries for extraneous nodes outside node_modules. These are stale workspace entries: the workspace was removed from package.json or its directory was deleted, so it should not be tracked in package-lock.json.
|
|
909
|
+
if (node.extraneous && !/(^|\/)node_modules\//.test(loc) && loc !== 'node_modules') {
|
|
910
|
+
continue
|
|
911
|
+
}
|
|
912
|
+
const meta = Shrinkwrap.metaFromNode(
|
|
933
913
|
node,
|
|
934
914
|
this.path,
|
|
935
915
|
this.resolveOptions)
|
|
916
|
+
// Skip inert nodes — these are optional deps that failed to load
|
|
917
|
+
// (e.g. 404 from a proxy registry that hasn't cached the package,
|
|
918
|
+
// or incomplete manifest missing version field).
|
|
919
|
+
// #pruneFailedOptional marks them inert so they won't be reified;
|
|
920
|
+
// writing them to the lockfile produces invalid entries like
|
|
921
|
+
// {"optional": true} that cause "Invalid Version:" errors.
|
|
922
|
+
if (node.inert && !node.package.version) {
|
|
923
|
+
continue
|
|
924
|
+
}
|
|
925
|
+
this.data.packages[loc] = meta
|
|
936
926
|
}
|
|
937
927
|
} else if (this.#awaitingUpdate.size > 0) {
|
|
938
928
|
for (const loc of this.#awaitingUpdate.keys()) {
|
package/package.json
CHANGED