@pnpm/lockfile.filtering 1100.1.13 → 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,37 @@
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
+
19
+ ## 1100.1.14
20
+
21
+ ### Patch Changes
22
+
23
+ - Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug [#13164](https://github.com/pnpm/pnpm/issues/13164).
24
+
25
+ - Updated dependencies:
26
+ - @pnpm/config.package-is-installable@1100.0.16
27
+ - @pnpm/constants@1100.0.1
28
+ - @pnpm/deps.path@1100.0.11
29
+ - @pnpm/error@1100.1.0
30
+ - @pnpm/lockfile.types@1100.0.16
31
+ - @pnpm/lockfile.utils@1100.1.5
32
+ - @pnpm/lockfile.walker@1100.0.16
33
+ - @pnpm/types@1101.6.0
34
+
3
35
  ## 1100.1.13
4
36
 
5
37
  ### Patch Changes
@@ -0,0 +1,7 @@
1
+ import type { ProjectSnapshot } from '@pnpm/lockfile.types';
2
+ import type { DependenciesField } from '@pnpm/types';
3
+ export declare function filterImporter(importer: ProjectSnapshot, include: {
4
+ [dependenciesField in DependenciesField]: boolean;
5
+ }, opts?: {
6
+ skipRuntimes?: boolean;
7
+ }): ProjectSnapshot;
@@ -0,0 +1,23 @@
1
+ export function filterImporter(importer, include, opts) {
2
+ const skipRuntimes = opts?.skipRuntimes === true;
3
+ return {
4
+ dependencies: !include.dependencies ? {} : pickNonRuntime(importer.dependencies, skipRuntimes),
5
+ devDependencies: !include.devDependencies ? {} : pickNonRuntime(importer.devDependencies, skipRuntimes),
6
+ optionalDependencies: !include.optionalDependencies ? {} : pickNonRuntime(importer.optionalDependencies, skipRuntimes),
7
+ specifiers: pickNonRuntime(importer.specifiers, skipRuntimes),
8
+ };
9
+ }
10
+ function pickNonRuntime(deps, skipRuntimes) {
11
+ if (!deps)
12
+ return {};
13
+ if (!skipRuntimes)
14
+ return deps;
15
+ const result = {};
16
+ for (const [name, ref] of Object.entries(deps)) {
17
+ if (!ref.startsWith('runtime:')) {
18
+ result[name] = ref;
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+ //# sourceMappingURL=filterImporter.js.map
@@ -0,0 +1,9 @@
1
+ import type { LockfileObject } from '@pnpm/lockfile.types';
2
+ import type { DependenciesField, DepPath } from '@pnpm/types';
3
+ export declare function filterLockfile(lockfile: LockfileObject, opts: {
4
+ include: {
5
+ [dependenciesField in DependenciesField]: boolean;
6
+ };
7
+ skipped: Set<DepPath>;
8
+ skipRuntimes?: boolean;
9
+ }): LockfileObject;
@@ -0,0 +1,8 @@
1
+ import { filterLockfileByImporters } from './filterLockfileByImporters.js';
2
+ export function filterLockfile(lockfile, opts) {
3
+ return filterLockfileByImporters(lockfile, Object.keys(lockfile.importers), {
4
+ ...opts,
5
+ failOnMissingDependencies: false,
6
+ });
7
+ }
8
+ //# sourceMappingURL=filterLockfile.js.map
@@ -0,0 +1,10 @@
1
+ import type { LockfileObject } from '@pnpm/lockfile.types';
2
+ import type { DependenciesField, DepPath, ProjectId } from '@pnpm/types';
3
+ export declare function filterLockfileByImporters(lockfile: LockfileObject, importerIds: ProjectId[], opts: {
4
+ include: {
5
+ [dependenciesField in DependenciesField]: boolean;
6
+ };
7
+ skipped: Set<DepPath>;
8
+ skipRuntimes?: boolean;
9
+ failOnMissingDependencies: boolean;
10
+ }): LockfileObject;
@@ -0,0 +1,36 @@
1
+ import { WANTED_LOCKFILE } from '@pnpm/constants';
2
+ import { LockfileMissingDependencyError } from '@pnpm/error';
3
+ import { lockfileWalker } from '@pnpm/lockfile.walker';
4
+ import { logger } from '@pnpm/logger';
5
+ import { filterImporter } from './filterImporter.js';
6
+ const lockfileLogger = logger('lockfile');
7
+ export function filterLockfileByImporters(lockfile, importerIds, opts) {
8
+ const importers = { ...lockfile.importers };
9
+ for (const importerId of importerIds) {
10
+ importers[importerId] = filterImporter(lockfile.importers[importerId], opts.include, { skipRuntimes: opts.skipRuntimes });
11
+ }
12
+ const packages = {};
13
+ if (lockfile.packages != null) {
14
+ pkgAllDeps(lockfileWalker({ ...lockfile, importers }, importerIds, { include: opts.include, skipped: opts.skipped }).step, packages, {
15
+ failOnMissingDependencies: opts.failOnMissingDependencies,
16
+ });
17
+ }
18
+ return {
19
+ ...lockfile,
20
+ importers,
21
+ packages,
22
+ };
23
+ }
24
+ function pkgAllDeps(step, pickedPackages, opts) {
25
+ for (const { pkgSnapshot, depPath, next } of step.dependencies) {
26
+ pickedPackages[depPath] = pkgSnapshot;
27
+ pkgAllDeps(next(), pickedPackages, opts);
28
+ }
29
+ for (const depPath of step.missing) {
30
+ if (opts.failOnMissingDependencies) {
31
+ throw new LockfileMissingDependencyError(depPath);
32
+ }
33
+ lockfileLogger.debug(`No entry for "${depPath}" in ${WANTED_LOCKFILE}`);
34
+ }
35
+ }
36
+ //# sourceMappingURL=filterLockfileByImporters.js.map
@@ -0,0 +1,35 @@
1
+ import type { LockfileObject } from '@pnpm/lockfile.types';
2
+ import type { DependenciesField, DepPath, ProjectId, SupportedArchitectures } from '@pnpm/types';
3
+ export interface FilterLockfileResult {
4
+ lockfile: LockfileObject;
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>;
17
+ }
18
+ export declare function filterLockfileByEngine(lockfile: LockfileObject, opts: FilterLockfileOptions): FilterLockfileResult;
19
+ export interface FilterLockfileOptions {
20
+ currentEngine: {
21
+ nodeVersion?: string;
22
+ pnpmVersion: string;
23
+ };
24
+ engineStrict: boolean;
25
+ include: {
26
+ [dependenciesField in DependenciesField]: boolean;
27
+ };
28
+ includeIncompatiblePackages?: boolean;
29
+ failOnMissingDependencies: boolean;
30
+ lockfileDir: string;
31
+ skipped: Set<string>;
32
+ skipRuntimes?: boolean;
33
+ supportedArchitectures?: SupportedArchitectures;
34
+ }
35
+ export declare function filterLockfileByImportersAndEngine(lockfile: LockfileObject, importerIds: ProjectId[], opts: FilterLockfileOptions): FilterLockfileResult;
@@ -0,0 +1,266 @@
1
+ import { checkPackageInstallability, packageIsInstallable, } from '@pnpm/config.package-is-installable';
2
+ import { WANTED_LOCKFILE } from '@pnpm/constants';
3
+ import * as dp from '@pnpm/deps.path';
4
+ import { LockfileMissingDependencyError } from '@pnpm/error';
5
+ import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
6
+ import { logger } from '@pnpm/logger';
7
+ import { map as mapValues, pickBy, unnest } from 'ramda';
8
+ import { filterImporter } from './filterImporter.js';
9
+ const lockfileLogger = logger('lockfile');
10
+ export function filterLockfileByEngine(lockfile, opts) {
11
+ const importerIds = Object.keys(lockfile.importers);
12
+ return filterLockfileByImportersAndEngine(lockfile, importerIds, opts);
13
+ }
14
+ export function filterLockfileByImportersAndEngine(lockfile, importerIds, opts) {
15
+ const importerIdSet = new Set(importerIds);
16
+ const directDepEdges = toImporterDepPaths(lockfile, importerIds, {
17
+ include: opts.include,
18
+ importerIdSet,
19
+ skipRuntimes: opts.skipRuntimes,
20
+ });
21
+ const { packages, requiredDepPaths } = lockfile.packages != null
22
+ ? pickPkgsWithAllDeps(lockfile, directDepEdges, importerIdSet, {
23
+ currentEngine: opts.currentEngine,
24
+ engineStrict: opts.engineStrict,
25
+ failOnMissingDependencies: opts.failOnMissingDependencies,
26
+ include: opts.include,
27
+ includeIncompatiblePackages: opts.includeIncompatiblePackages === true,
28
+ lockfileDir: opts.lockfileDir,
29
+ skipped: opts.skipped,
30
+ skipRuntimes: opts.skipRuntimes,
31
+ supportedArchitectures: opts.supportedArchitectures,
32
+ })
33
+ : { packages: {}, requiredDepPaths: new Set() };
34
+ const importers = mapValues((importer) => {
35
+ const newImporter = filterImporter(importer, opts.include, { skipRuntimes: opts.skipRuntimes });
36
+ if (newImporter.optionalDependencies != null) {
37
+ newImporter.optionalDependencies = pickBy((ref, depName) => {
38
+ const depPath = dp.refToRelative(ref, depName);
39
+ return !depPath || packages[depPath] != null;
40
+ }, newImporter.optionalDependencies);
41
+ }
42
+ return newImporter;
43
+ }, lockfile.importers);
44
+ return {
45
+ lockfile: {
46
+ ...lockfile,
47
+ importers,
48
+ packages,
49
+ },
50
+ selectedImporterIds: Array.from(importerIdSet),
51
+ requiredDepPaths,
52
+ };
53
+ }
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 };
68
+ }
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))
96
+ continue;
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];
178
+ if (!pkgSnapshot && !depPath.startsWith('link:')) {
179
+ if (opts.failOnMissingDependencies) {
180
+ throw new LockfileMissingDependencyError(depPath);
181
+ }
182
+ lockfileLogger.debug(`No entry for "${depPath}" in ${WANTED_LOCKFILE}`);
183
+ continue;
184
+ }
185
+ if (!ctx.installed.has(depPath)) {
186
+ if (!ctx.pickedPackages[depPath] && pkgSnapshot.optional === true) {
187
+ opts.skipped.add(depPath);
188
+ }
189
+ ctx.pickedPackages[depPath] = pkgSnapshot;
190
+ }
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);
198
+ }
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, {
213
+ include: opts.include,
214
+ importerIdSet: ctx.importerIdSet,
215
+ skipRuntimes: opts.skipRuntimes,
216
+ }),
217
+ ];
218
+ }
219
+ function toImporterDepPaths(lockfile, importerIds, opts) {
220
+ const importerDeps = importerIds
221
+ .map(importerId => lockfile.importers[importerId])
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);
229
+ if (!nextImporterIds.length) {
230
+ return depEdges;
231
+ }
232
+ nextImporterIds = nextImporterIds.filter(importerId => !opts.importerIdSet.has(importerId));
233
+ for (const importerId of nextImporterIds) {
234
+ opts.importerIdSet.add(importerId);
235
+ }
236
+ return [
237
+ ...depEdges,
238
+ ...toImporterDepPaths(lockfile, nextImporterIds, opts),
239
+ ];
240
+ }
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) {
247
+ const acc = {
248
+ depEdges: [],
249
+ importerIds: [],
250
+ };
251
+ for (const { pkgName, ref, optional } of depRefs) {
252
+ if (ref.startsWith('link:')) {
253
+ const importerId = ref.substring(5);
254
+ if (lockfile.importers[importerId]) {
255
+ acc.importerIds.push(importerId);
256
+ }
257
+ continue;
258
+ }
259
+ const depPath = dp.refToRelative(ref, pkgName);
260
+ if (depPath == null)
261
+ continue;
262
+ acc.depEdges.push({ depPath, optional });
263
+ }
264
+ return acc;
265
+ }
266
+ //# sourceMappingURL=filterLockfileByImportersAndEngine.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { filterLockfile } from './filterLockfile.js';
2
+ export { filterLockfileByImporters } from './filterLockfileByImporters.js';
3
+ export { filterLockfileByEngine, filterLockfileByImportersAndEngine } from './filterLockfileByImportersAndEngine.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/lockfile.filtering",
3
- "version": "1100.1.13",
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.15",
33
- "@pnpm/constants": "1100.0.0",
34
- "@pnpm/deps.path": "1100.0.10",
35
- "@pnpm/error": "1100.0.1",
36
- "@pnpm/lockfile.types": "1100.0.15",
37
- "@pnpm/lockfile.utils": "1100.1.4",
38
- "@pnpm/lockfile.walker": "1100.0.15",
39
- "@pnpm/types": "1101.5.0",
32
+ "@pnpm/config.package-is-installable": "1100.1.0",
33
+ "@pnpm/constants": "1100.0.1",
34
+ "@pnpm/deps.path": "1100.0.12",
35
+ "@pnpm/error": "1100.1.0",
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.1.13",
47
+ "@pnpm/lockfile.filtering": "1100.2.0",
48
48
  "@pnpm/logger": "1100.0.0",
49
- "@types/ramda": "0.31.1",
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",