@pnpm/lockfile.filtering 1100.1.14 → 1100.2.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @pnpm/filter-lockfile
|
|
2
2
|
|
|
3
|
+
## 1100.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Fixed an installed optional dependency being left without one of its own required dependencies. When a package reached through `optionalDependencies` is installable on the current system but one of its regular `dependencies` is not, a lockfile-based install skipped that dependency and installed the parent anyway, so importing the parent failed with `MODULE_NOT_FOUND`. The dependency is now installed, and an install-check warning reports the incompatibility. A dependency is still only skipped when every path to it is optional, or when the package that pulls it in was itself skipped [#13286](https://github.com/pnpm/pnpm/issues/13286).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies:
|
|
12
|
+
- @pnpm/config.package-is-installable@1100.1.0
|
|
13
|
+
- @pnpm/deps.path@1100.0.12
|
|
14
|
+
- @pnpm/lockfile.types@1100.0.17
|
|
15
|
+
- @pnpm/lockfile.utils@1100.1.6
|
|
16
|
+
- @pnpm/lockfile.walker@1100.0.17
|
|
17
|
+
- @pnpm/types@1101.7.0
|
|
18
|
+
|
|
3
19
|
## 1100.1.14
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { LockfileObject } from '@pnpm/lockfile.types';
|
|
2
|
-
import type { DependenciesField, ProjectId, SupportedArchitectures } from '@pnpm/types';
|
|
2
|
+
import type { DependenciesField, DepPath, ProjectId, SupportedArchitectures } from '@pnpm/types';
|
|
3
3
|
export interface FilterLockfileResult {
|
|
4
4
|
lockfile: LockfileObject;
|
|
5
5
|
selectedImporterIds: ProjectId[];
|
|
6
|
+
/**
|
|
7
|
+
* Dep paths reached by a non-optional edge from an importer or from a
|
|
8
|
+
* package that is itself part of the install. Their installability is
|
|
9
|
+
* evaluated as non-optional — an incompatible one fails the install under
|
|
10
|
+
* `engineStrict` instead of being skipped — even when the lockfile marks
|
|
11
|
+
* the snapshot `optional: true` because the subtree happens to hang off an
|
|
12
|
+
* optional dependency. Downstream consumers must classify by this set
|
|
13
|
+
* rather than by `pkgSnapshot.optional` so every stage of a headless
|
|
14
|
+
* install agrees with the resolver, which classifies by edge too.
|
|
15
|
+
*/
|
|
16
|
+
requiredDepPaths: Set<DepPath>;
|
|
6
17
|
}
|
|
7
18
|
export declare function filterLockfileByEngine(lockfile: LockfileObject, opts: FilterLockfileOptions): FilterLockfileResult;
|
|
8
19
|
export interface FilterLockfileOptions {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { packageIsInstallable } from '@pnpm/config.package-is-installable';
|
|
1
|
+
import { checkPackageInstallability, packageIsInstallable, } from '@pnpm/config.package-is-installable';
|
|
2
2
|
import { WANTED_LOCKFILE } from '@pnpm/constants';
|
|
3
3
|
import * as dp from '@pnpm/deps.path';
|
|
4
4
|
import { LockfileMissingDependencyError } from '@pnpm/error';
|
|
@@ -13,13 +13,13 @@ export function filterLockfileByEngine(lockfile, opts) {
|
|
|
13
13
|
}
|
|
14
14
|
export function filterLockfileByImportersAndEngine(lockfile, importerIds, opts) {
|
|
15
15
|
const importerIdSet = new Set(importerIds);
|
|
16
|
-
const
|
|
16
|
+
const directDepEdges = toImporterDepPaths(lockfile, importerIds, {
|
|
17
17
|
include: opts.include,
|
|
18
18
|
importerIdSet,
|
|
19
19
|
skipRuntimes: opts.skipRuntimes,
|
|
20
20
|
});
|
|
21
|
-
const packages = lockfile.packages != null
|
|
22
|
-
? pickPkgsWithAllDeps(lockfile,
|
|
21
|
+
const { packages, requiredDepPaths } = lockfile.packages != null
|
|
22
|
+
? pickPkgsWithAllDeps(lockfile, directDepEdges, importerIdSet, {
|
|
23
23
|
currentEngine: opts.currentEngine,
|
|
24
24
|
engineStrict: opts.engineStrict,
|
|
25
25
|
failOnMissingDependencies: opts.failOnMissingDependencies,
|
|
@@ -30,7 +30,7 @@ export function filterLockfileByImportersAndEngine(lockfile, importerIds, opts)
|
|
|
30
30
|
skipRuntimes: opts.skipRuntimes,
|
|
31
31
|
supportedArchitectures: opts.supportedArchitectures,
|
|
32
32
|
})
|
|
33
|
-
: {};
|
|
33
|
+
: { packages: {}, requiredDepPaths: new Set() };
|
|
34
34
|
const importers = mapValues((importer) => {
|
|
35
35
|
const newImporter = filterImporter(importer, opts.include, { skipRuntimes: opts.skipRuntimes });
|
|
36
36
|
if (newImporter.optionalDependencies != null) {
|
|
@@ -48,18 +48,133 @@ export function filterLockfileByImportersAndEngine(lockfile, importerIds, opts)
|
|
|
48
48
|
packages,
|
|
49
49
|
},
|
|
50
50
|
selectedImporterIds: Array.from(importerIdSet),
|
|
51
|
+
requiredDepPaths,
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
|
-
function pickPkgsWithAllDeps(lockfile,
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
function pickPkgsWithAllDeps(lockfile, depEdges, importerIdSet, opts) {
|
|
55
|
+
const ctx = {
|
|
56
|
+
lockfile,
|
|
57
|
+
pickedPackages: {},
|
|
58
|
+
importerIdSet,
|
|
59
|
+
installed: new Set(),
|
|
60
|
+
requiredDepPaths: new Set(),
|
|
61
|
+
evaluated: [],
|
|
62
|
+
edgesByDepPath: new Map(),
|
|
63
|
+
};
|
|
64
|
+
classifyDeps(ctx, depEdges, opts);
|
|
65
|
+
reportInstallability(ctx, opts);
|
|
66
|
+
pickSkippedDeps(ctx, depEdges, opts);
|
|
67
|
+
return { packages: ctx.pickedPackages, requiredDepPaths: ctx.requiredDepPaths };
|
|
57
68
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Work out which packages the install actually reaches, and over which kind
|
|
71
|
+
* of edge.
|
|
72
|
+
*
|
|
73
|
+
* An edge is only expanded once its source is known to be part of the
|
|
74
|
+
* install, so the optionality of the edge — not the `optional` flag the
|
|
75
|
+
* resolver propagated onto the snapshot — decides whether an incompatible
|
|
76
|
+
* package is skipped: only a package whose every inbound edge is optional
|
|
77
|
+
* stays out of the install.
|
|
78
|
+
*
|
|
79
|
+
* Traversal is breadth-first over edges rather than depth-first over
|
|
80
|
+
* packages so the verdict does not depend on which edge happens to reach a
|
|
81
|
+
* package first: a package left out on an optional edge is reconsidered when
|
|
82
|
+
* a non-optional one arrives later. Nothing is reported from here — a
|
|
83
|
+
* package can be visited once per inbound edge, and its installability must
|
|
84
|
+
* be reported exactly once, which [reportInstallability] does afterwards.
|
|
85
|
+
*/
|
|
86
|
+
function classifyDeps(ctx, depEdges, opts) {
|
|
87
|
+
const incompatible = new Map();
|
|
88
|
+
// Walked with a cursor rather than `shift()`, which is O(n) per dequeue on
|
|
89
|
+
// a JS array and would make the walk quadratic on a large lockfile.
|
|
90
|
+
const queue = [...depEdges];
|
|
91
|
+
for (let next = 0; next < queue.length; next++) {
|
|
92
|
+
const { depPath, optional } = queue[next];
|
|
93
|
+
if (!optional)
|
|
94
|
+
ctx.requiredDepPaths.add(depPath);
|
|
95
|
+
if (ctx.installed.has(depPath))
|
|
61
96
|
continue;
|
|
62
97
|
const pkgSnapshot = ctx.lockfile.packages[depPath];
|
|
98
|
+
// Missing entries are reported by the closure pass, which reaches every
|
|
99
|
+
// dep path this one does.
|
|
100
|
+
if (!pkgSnapshot)
|
|
101
|
+
continue;
|
|
102
|
+
if (!incompatible.has(depPath)) {
|
|
103
|
+
ctx.evaluated.push(depPath);
|
|
104
|
+
// TODO: depPath is not the package ID. Should be fixed
|
|
105
|
+
incompatible.set(depPath, !opts.includeIncompatiblePackages && checkPackageInstallability(pkgSnapshot.id ?? depPath, toInstallabilityManifest(depPath, pkgSnapshot), {
|
|
106
|
+
nodeVersion: opts.currentEngine.nodeVersion,
|
|
107
|
+
optional: true,
|
|
108
|
+
supportedArchitectures: opts.supportedArchitectures,
|
|
109
|
+
}) != null);
|
|
110
|
+
}
|
|
111
|
+
if (optional && incompatible.get(depPath))
|
|
112
|
+
continue;
|
|
113
|
+
ctx.installed.add(depPath);
|
|
114
|
+
ctx.pickedPackages[depPath] = pkgSnapshot;
|
|
115
|
+
const edges = nextDepEdges(ctx, pkgSnapshot, opts);
|
|
116
|
+
ctx.edgesByDepPath.set(depPath, edges);
|
|
117
|
+
// Appended one by one: `push(...edges)` passes each edge as its own
|
|
118
|
+
// argument and overflows the engine's argument limit on a wide enough
|
|
119
|
+
// dependency list.
|
|
120
|
+
for (const edge of edges) {
|
|
121
|
+
queue.push(edge);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Report each reached package once, against the edge kind it was classified
|
|
127
|
+
* by. This is where an incompatible package that a non-optional edge reaches
|
|
128
|
+
* fails the install under `engineStrict`, and where a skipped optional gets
|
|
129
|
+
* its `skippedOptionalDependencyLogger` entry.
|
|
130
|
+
*/
|
|
131
|
+
function reportInstallability(ctx, opts) {
|
|
132
|
+
for (const depPath of ctx.evaluated) {
|
|
133
|
+
const pkgSnapshot = ctx.lockfile.packages[depPath];
|
|
134
|
+
const installable = opts.includeIncompatiblePackages ||
|
|
135
|
+
packageIsInstallable(pkgSnapshot.id ?? depPath, toInstallabilityManifest(depPath, pkgSnapshot), {
|
|
136
|
+
// A subtree hanging off an `optionalDependencies` entry stays
|
|
137
|
+
// best-effort: the dependency is installed so its dependent is not
|
|
138
|
+
// left broken, but an incompatibility inside it does not fail the
|
|
139
|
+
// install. Only a package no optional path reaches is fatal here.
|
|
140
|
+
engineStrict: opts.engineStrict && pkgSnapshot.optional !== true,
|
|
141
|
+
lockfileDir: opts.lockfileDir,
|
|
142
|
+
nodeVersion: opts.currentEngine.nodeVersion,
|
|
143
|
+
optional: !ctx.installed.has(depPath) || !ctx.requiredDepPaths.has(depPath),
|
|
144
|
+
supportedArchitectures: opts.supportedArchitectures,
|
|
145
|
+
}) !== false;
|
|
146
|
+
if (installable) {
|
|
147
|
+
opts.skipped.delete(depPath);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
opts.skipped.add(depPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function toInstallabilityManifest(depPath, pkgSnapshot) {
|
|
155
|
+
return {
|
|
156
|
+
...nameVerFromPkgSnapshot(depPath, pkgSnapshot),
|
|
157
|
+
cpu: pkgSnapshot.cpu,
|
|
158
|
+
engines: pkgSnapshot.engines,
|
|
159
|
+
os: pkgSnapshot.os,
|
|
160
|
+
libc: pkgSnapshot.libc,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Extend the picked set over the packages that are reachable but not part of
|
|
165
|
+
* the install — an incompatible optional and everything below it. They stay
|
|
166
|
+
* in the filtered lockfile and are recorded as skipped, so a later install on
|
|
167
|
+
* a host that does support them can pick them up without a re-resolution.
|
|
168
|
+
*/
|
|
169
|
+
function pickSkippedDeps(ctx, depEdges, opts) {
|
|
170
|
+
const queue = depEdges.map(({ depPath }) => depPath);
|
|
171
|
+
const visited = new Set();
|
|
172
|
+
for (let next = 0; next < queue.length; next++) {
|
|
173
|
+
const depPath = queue[next];
|
|
174
|
+
if (visited.has(depPath))
|
|
175
|
+
continue;
|
|
176
|
+
visited.add(depPath);
|
|
177
|
+
const pkgSnapshot = ctx.lockfile.packages[depPath];
|
|
63
178
|
if (!pkgSnapshot && !depPath.startsWith('link:')) {
|
|
64
179
|
if (opts.failOnMissingDependencies) {
|
|
65
180
|
throw new LockfileMissingDependencyError(depPath);
|
|
@@ -67,87 +182,73 @@ function pkgAllDeps(ctx, depPaths, parentIsInstallable, opts) {
|
|
|
67
182
|
lockfileLogger.debug(`No entry for "${depPath}" in ${WANTED_LOCKFILE}`);
|
|
68
183
|
continue;
|
|
69
184
|
}
|
|
70
|
-
|
|
71
|
-
if (!parentIsInstallable) {
|
|
72
|
-
installable = false;
|
|
185
|
+
if (!ctx.installed.has(depPath)) {
|
|
73
186
|
if (!ctx.pickedPackages[depPath] && pkgSnapshot.optional === true) {
|
|
74
187
|
opts.skipped.add(depPath);
|
|
75
188
|
}
|
|
189
|
+
ctx.pickedPackages[depPath] = pkgSnapshot;
|
|
76
190
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
85
|
-
// TODO: depPath is not the package ID. Should be fixed
|
|
86
|
-
installable =
|
|
87
|
-
opts.includeIncompatiblePackages ||
|
|
88
|
-
packageIsInstallable(pkgSnapshot.id ?? depPath, pkg, {
|
|
89
|
-
engineStrict: opts.engineStrict,
|
|
90
|
-
lockfileDir: opts.lockfileDir,
|
|
91
|
-
nodeVersion: opts.currentEngine.nodeVersion,
|
|
92
|
-
optional: pkgSnapshot.optional === true,
|
|
93
|
-
supportedArchitectures: opts.supportedArchitectures,
|
|
94
|
-
}) !== false;
|
|
95
|
-
if (!installable) {
|
|
96
|
-
if (!ctx.pickedPackages[depPath] && pkgSnapshot.optional === true) {
|
|
97
|
-
opts.skipped.add(depPath);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
opts.skipped.delete(depPath);
|
|
102
|
-
}
|
|
191
|
+
// `visited` guarantees one pass per dep path, so a cached entry is
|
|
192
|
+
// released as soon as it is consumed rather than being retained until the
|
|
193
|
+
// whole walk ends.
|
|
194
|
+
const edges = ctx.edgesByDepPath.get(depPath) ?? nextDepEdges(ctx, pkgSnapshot, opts);
|
|
195
|
+
ctx.edgesByDepPath.delete(depPath);
|
|
196
|
+
for (const edge of edges) {
|
|
197
|
+
queue.push(edge.depPath);
|
|
103
198
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** The outbound edges of a package, tagged with the optionality of each. */
|
|
202
|
+
function nextDepEdges(ctx, pkgSnapshot, opts) {
|
|
203
|
+
const { depEdges, importerIds } = parseDepRefs([
|
|
204
|
+
...toDepRefs(pkgSnapshot.dependencies, false),
|
|
205
|
+
...(opts.include.optionalDependencies ? toDepRefs(pkgSnapshot.optionalDependencies, true) : []),
|
|
206
|
+
], ctx.lockfile);
|
|
207
|
+
for (const importerId of importerIds) {
|
|
208
|
+
ctx.importerIdSet.add(importerId);
|
|
209
|
+
}
|
|
210
|
+
return [
|
|
211
|
+
...depEdges,
|
|
212
|
+
...toImporterDepPaths(ctx.lockfile, importerIds, {
|
|
113
213
|
include: opts.include,
|
|
114
214
|
importerIdSet: ctx.importerIdSet,
|
|
115
215
|
skipRuntimes: opts.skipRuntimes,
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
}
|
|
216
|
+
}),
|
|
217
|
+
];
|
|
119
218
|
}
|
|
120
219
|
function toImporterDepPaths(lockfile, importerIds, opts) {
|
|
121
220
|
const importerDeps = importerIds
|
|
122
221
|
.map(importerId => lockfile.importers[importerId])
|
|
123
|
-
.map(importer =>
|
|
124
|
-
...(opts.include.dependencies ? importer.dependencies :
|
|
125
|
-
...(opts.include.devDependencies ? importer.devDependencies :
|
|
126
|
-
...(opts.include.optionalDependencies
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}))
|
|
130
|
-
.map(Object.entries)
|
|
131
|
-
.map(entries => opts.skipRuntimes ? entries.filter(([, ref]) => !ref.startsWith('runtime:')) : entries);
|
|
132
|
-
let { depPaths, importerIds: nextImporterIds } = parseDepRefs(unnest(importerDeps), lockfile);
|
|
222
|
+
.map(importer => [
|
|
223
|
+
...(opts.include.dependencies ? toDepRefs(importer.dependencies, false) : []),
|
|
224
|
+
...(opts.include.devDependencies ? toDepRefs(importer.devDependencies, false) : []),
|
|
225
|
+
...(opts.include.optionalDependencies ? toDepRefs(importer.optionalDependencies, true) : []),
|
|
226
|
+
])
|
|
227
|
+
.map(refs => opts.skipRuntimes ? refs.filter(({ ref }) => !ref.startsWith('runtime:')) : refs);
|
|
228
|
+
let { depEdges, importerIds: nextImporterIds } = parseDepRefs(unnest(importerDeps), lockfile);
|
|
133
229
|
if (!nextImporterIds.length) {
|
|
134
|
-
return
|
|
230
|
+
return depEdges;
|
|
135
231
|
}
|
|
136
232
|
nextImporterIds = nextImporterIds.filter(importerId => !opts.importerIdSet.has(importerId));
|
|
137
233
|
for (const importerId of nextImporterIds) {
|
|
138
234
|
opts.importerIdSet.add(importerId);
|
|
139
235
|
}
|
|
140
236
|
return [
|
|
141
|
-
...
|
|
237
|
+
...depEdges,
|
|
142
238
|
...toImporterDepPaths(lockfile, nextImporterIds, opts),
|
|
143
239
|
];
|
|
144
240
|
}
|
|
145
|
-
function
|
|
241
|
+
function toDepRefs(refsByPkgNames, optional) {
|
|
242
|
+
if (refsByPkgNames == null)
|
|
243
|
+
return [];
|
|
244
|
+
return Object.entries(refsByPkgNames).map(([pkgName, ref]) => ({ pkgName, ref, optional }));
|
|
245
|
+
}
|
|
246
|
+
function parseDepRefs(depRefs, lockfile) {
|
|
146
247
|
const acc = {
|
|
147
|
-
|
|
248
|
+
depEdges: [],
|
|
148
249
|
importerIds: [],
|
|
149
250
|
};
|
|
150
|
-
for (const
|
|
251
|
+
for (const { pkgName, ref, optional } of depRefs) {
|
|
151
252
|
if (ref.startsWith('link:')) {
|
|
152
253
|
const importerId = ref.substring(5);
|
|
153
254
|
if (lockfile.importers[importerId]) {
|
|
@@ -158,7 +259,7 @@ function parseDepRefs(refsByPkgNames, lockfile) {
|
|
|
158
259
|
const depPath = dp.refToRelative(ref, pkgName);
|
|
159
260
|
if (depPath == null)
|
|
160
261
|
continue;
|
|
161
|
-
acc.
|
|
262
|
+
acc.depEdges.push({ depPath, optional });
|
|
162
263
|
}
|
|
163
264
|
return acc;
|
|
164
265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/lockfile.filtering",
|
|
3
|
-
"version": "1100.
|
|
3
|
+
"version": "1100.2.0",
|
|
4
4
|
"description": "Filters a lockfile",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
"!*.map"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pnpm/config.package-is-installable": "1100.0
|
|
32
|
+
"@pnpm/config.package-is-installable": "1100.1.0",
|
|
33
33
|
"@pnpm/constants": "1100.0.1",
|
|
34
|
-
"@pnpm/deps.path": "1100.0.
|
|
34
|
+
"@pnpm/deps.path": "1100.0.12",
|
|
35
35
|
"@pnpm/error": "1100.1.0",
|
|
36
|
-
"@pnpm/lockfile.types": "1100.0.
|
|
37
|
-
"@pnpm/lockfile.utils": "1100.1.
|
|
38
|
-
"@pnpm/lockfile.walker": "1100.0.
|
|
39
|
-
"@pnpm/types": "1101.
|
|
36
|
+
"@pnpm/lockfile.types": "1100.0.17",
|
|
37
|
+
"@pnpm/lockfile.utils": "1100.1.6",
|
|
38
|
+
"@pnpm/lockfile.walker": "1100.0.17",
|
|
39
|
+
"@pnpm/types": "1101.7.0",
|
|
40
40
|
"ramda": "npm:@pnpm/ramda@0.28.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@jest/globals": "30.4.1",
|
|
47
|
-
"@pnpm/lockfile.filtering": "1100.
|
|
47
|
+
"@pnpm/lockfile.filtering": "1100.2.0",
|
|
48
48
|
"@pnpm/logger": "1100.0.0",
|
|
49
|
-
"@types/ramda": "0.
|
|
49
|
+
"@types/ramda": "0.32.0",
|
|
50
50
|
"detect-libc": "^2.1.2",
|
|
51
51
|
"tempy": "3.0.0",
|
|
52
52
|
"write-yaml-file": "^6.0.0",
|