pnpm-dedup 2.1.0 → 2.2.2

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.
Files changed (89) hide show
  1. package/README.md +15 -5
  2. package/dist/applyClusterFixes.js +103 -80
  3. package/dist/applyClusterFixes.js.map +1 -1
  4. package/dist/applyClusterFixes.test.js +257 -29
  5. package/dist/applyClusterFixes.test.js.map +1 -1
  6. package/dist/bin/pnpm-dedupe.js +0 -0
  7. package/dist/bin/pnpm-why-duplicate.js +0 -0
  8. package/dist/bin/pnpm-why-duplicate.test.js +17 -8
  9. package/dist/bin/pnpm-why-duplicate.test.js.map +1 -1
  10. package/dist/dedupe.js +15 -4
  11. package/dist/dedupe.js.map +1 -1
  12. package/dist/dedupeCheckConsistency.test.js +53 -59
  13. package/dist/dedupeCheckConsistency.test.js.map +1 -1
  14. package/dist/dedupeEndToEnd.test.js +3 -3
  15. package/dist/dedupeEndToEnd.test.js.map +1 -1
  16. package/dist/dedupePeers.test.js +1 -1
  17. package/dist/dedupePeers.test.js.map +1 -1
  18. package/dist/definitions/applyClusterFixes.d.ts +6 -3
  19. package/dist/definitions/applyClusterFixes.d.ts.map +1 -1
  20. package/dist/definitions/dedupe.d.ts.map +1 -1
  21. package/dist/definitions/helpers/describeUnheldOverride.d.ts +10 -0
  22. package/dist/definitions/helpers/describeUnheldOverride.d.ts.map +1 -0
  23. package/dist/definitions/helpers/describeUnheldOverride.test.d.ts +2 -0
  24. package/dist/definitions/helpers/describeUnheldOverride.test.d.ts.map +1 -0
  25. package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts +1 -5
  26. package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts.map +1 -1
  27. package/dist/definitions/pnpmLockTypes.d.ts +1 -0
  28. package/dist/definitions/pnpmLockTypes.d.ts.map +1 -1
  29. package/dist/definitions/readPnpmLock.d.ts +7 -0
  30. package/dist/definitions/readPnpmLock.d.ts.map +1 -1
  31. package/dist/definitions/readPnpmLock.test.d.ts +2 -0
  32. package/dist/definitions/readPnpmLock.test.d.ts.map +1 -0
  33. package/dist/displayMany.test.js +3 -4
  34. package/dist/displayMany.test.js.map +1 -1
  35. package/dist/helpers/buildIdentifiedFixesMap.test.js +1 -1
  36. package/dist/helpers/buildIdentifiedFixesMap.test.js.map +1 -1
  37. package/dist/helpers/buildPnpmPackagesMap.test.js +1 -1
  38. package/dist/helpers/buildPnpmPackagesMap.test.js.map +1 -1
  39. package/dist/helpers/collectDependentRanges.test.js +1 -1
  40. package/dist/helpers/collectDependentRanges.test.js.map +1 -1
  41. package/dist/helpers/collectPnpmDependents.test.js +1 -1
  42. package/dist/helpers/collectPnpmDependents.test.js.map +1 -1
  43. package/dist/helpers/describeUnheldOverride.js +30 -0
  44. package/dist/helpers/describeUnheldOverride.js.map +1 -0
  45. package/dist/helpers/describeUnheldOverride.test.js +80 -0
  46. package/dist/helpers/describeUnheldOverride.test.js.map +1 -0
  47. package/dist/helpers/parsePnpmLockPackages.test.js +1 -1
  48. package/dist/helpers/parsePnpmLockPackages.test.js.map +1 -1
  49. package/dist/helpers/pnpmWorkspaceYaml.js +2 -17
  50. package/dist/helpers/pnpmWorkspaceYaml.js.map +1 -1
  51. package/dist/helpers/pnpmWorkspaceYaml.test.js +3 -5
  52. package/dist/helpers/pnpmWorkspaceYaml.test.js.map +1 -1
  53. package/dist/helpers/readInstalledManifest.test.js +1 -1
  54. package/dist/helpers/readInstalledManifest.test.js.map +1 -1
  55. package/dist/helpers/toLockstepGraph.test.js +1 -1
  56. package/dist/helpers/toLockstepGraph.test.js.map +1 -1
  57. package/dist/identifyClusterFixes.test.js +22 -41
  58. package/dist/identifyClusterFixes.test.js.map +1 -1
  59. package/dist/identifyResolutionFixes.test.js +8 -7
  60. package/dist/identifyResolutionFixes.test.js.map +1 -1
  61. package/dist/readPnpmLock.js +21 -2
  62. package/dist/readPnpmLock.js.map +1 -1
  63. package/dist/readPnpmLock.test.js +43 -0
  64. package/dist/readPnpmLock.test.js.map +1 -0
  65. package/package.json +4 -3
  66. package/src/applyClusterFixes.test.ts +398 -36
  67. package/src/applyClusterFixes.ts +178 -125
  68. package/src/bin/pnpm-why-duplicate.test.ts +31 -8
  69. package/src/dedupe.ts +22 -7
  70. package/src/dedupeCheckConsistency.test.ts +84 -100
  71. package/src/dedupeEndToEnd.test.ts +84 -77
  72. package/src/dedupePeers.test.ts +1 -1
  73. package/src/displayMany.test.ts +4 -4
  74. package/src/helpers/buildIdentifiedFixesMap.test.ts +1 -1
  75. package/src/helpers/buildPnpmPackagesMap.test.ts +1 -1
  76. package/src/helpers/collectDependentRanges.test.ts +1 -1
  77. package/src/helpers/collectPnpmDependents.test.ts +1 -1
  78. package/src/helpers/describeUnheldOverride.test.ts +100 -0
  79. package/src/helpers/describeUnheldOverride.ts +53 -0
  80. package/src/helpers/parsePnpmLockPackages.test.ts +1 -1
  81. package/src/helpers/pnpmWorkspaceYaml.test.ts +2 -5
  82. package/src/helpers/pnpmWorkspaceYaml.ts +5 -20
  83. package/src/helpers/readInstalledManifest.test.ts +1 -1
  84. package/src/helpers/toLockstepGraph.test.ts +1 -1
  85. package/src/identifyClusterFixes.test.ts +27 -58
  86. package/src/identifyResolutionFixes.test.ts +8 -7
  87. package/src/pnpmLockTypes.ts +1 -0
  88. package/src/readPnpmLock.test.ts +65 -0
  89. package/src/readPnpmLock.ts +26 -2
@@ -1,5 +1,5 @@
1
- import { describe, it } from "bun:test";
2
1
  import { deepStrictEqual, strictEqual } from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
3
  import {
4
4
  addOverrides,
5
5
  overrideKey,
@@ -94,7 +94,7 @@ describe("pnpmWorkspaceYaml", () => {
94
94
  );
95
95
  });
96
96
 
97
- it("puts the comment above the first entry it adds, not above the block", () => {
97
+ it("appends to an existing block, leaving the user's comment in place", () => {
98
98
  strictEqual(
99
99
  addOverrides(
100
100
  'overrides:\n # a decision taken elsewhere\n "foo@": "1.0.0"\n',
@@ -102,14 +102,11 @@ describe("pnpmWorkspaceYaml", () => {
102
102
  ["leaf", "2.0.0"],
103
103
  ["other", "1.0.0"],
104
104
  ]),
105
- { comment: "Added by pnpm-dedup.\nsee https://example.test/issues" },
106
105
  ),
107
106
  [
108
107
  "overrides:",
109
108
  " # a decision taken elsewhere",
110
109
  ' "foo@": "1.0.0"',
111
- " # Added by pnpm-dedup.",
112
- " # see https://example.test/issues",
113
110
  ' "leaf@": "2.0.0"',
114
111
  ' "other@": "1.0.0"',
115
112
  "",
@@ -12,14 +12,6 @@ export const overrideKey = (
12
12
  convergence: boolean,
13
13
  ): string => (convergence ? `${packageName}@` : packageName);
14
14
 
15
- // `yaml` writes `commentBefore` straight after the `#`, so the leading space
16
- // that makes it read as prose has to be part of every line.
17
- const spaced = (comment: string): string =>
18
- comment
19
- .split("\n")
20
- .map((line) => (line.startsWith(" ") ? line : ` ${line}`))
21
- .join("\n");
22
-
23
15
  const quoted = (value: string): Scalar<string> => {
24
16
  const scalar = new Scalar(value);
25
17
  scalar.type = Scalar.QUOTE_DOUBLE;
@@ -29,32 +21,25 @@ const quoted = (value: string): Scalar<string> => {
29
21
  export interface AddOverridesOptions {
30
22
  // false writes plain keys, which pnpm applies to every requester
31
23
  convergence?: boolean;
32
- comment?: string;
33
24
  }
34
25
 
35
26
  /**
36
27
  * Add overrides to a `pnpm-workspace.yaml`, editing the document rather than
37
28
  * reserializing it: the user's comments, key order and formatting have to
38
29
  * survive. Pass `undefined` for a file that does not exist yet.
39
- *
40
- * `comment` is attached above the first entry added, not above the `overrides`
41
- * key, so an existing block and whatever the user wrote about it stay untouched.
42
30
  */
43
31
  export const addOverrides = (
44
32
  content: string | undefined,
45
33
  overrides: Map<string, string>,
46
- { convergence = true, comment }: AddOverridesOptions = {},
34
+ { convergence = true }: AddOverridesOptions = {},
47
35
  ): string => {
48
36
  const doc = parseDocument(content ?? "");
49
- let isFirst = true;
50
37
 
51
38
  for (const [packageName, version] of overrides) {
52
- const key = quoted(overrideKey(packageName, convergence));
53
- if (isFirst && comment !== undefined) {
54
- key.commentBefore = spaced(comment);
55
- }
56
- doc.setIn(["overrides", key], quoted(version));
57
- isFirst = false;
39
+ doc.setIn(
40
+ ["overrides", quoted(overrideKey(packageName, convergence))],
41
+ quoted(version),
42
+ );
58
43
  }
59
44
 
60
45
  return doc.toString();
@@ -1,8 +1,8 @@
1
- import { describe, it } from "bun:test";
2
1
  import { deepStrictEqual, strictEqual } from "node:assert/strict";
3
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
3
  import { tmpdir } from "node:os";
5
4
  import { join } from "node:path";
5
+ import { describe, it } from "node:test";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createManifestReaderWithStats } from "./readInstalledManifest.ts";
8
8
 
@@ -1,5 +1,5 @@
1
- import { describe, it } from "bun:test";
2
1
  import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import type { PnpmLockFile } from "../pnpmLockTypes.ts";
5
5
  import { readPnpmLock } from "../readPnpmLock.ts";
@@ -1,6 +1,6 @@
1
- import { describe, it } from "bun:test";
2
1
  import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
3
2
  import { join } from "node:path";
3
+ import { describe, it } from "node:test";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import type { ClusterFix } from "pm-dedup-core";
6
6
  import { buildPnpmPackagesMap } from "./helpers/buildPnpmPackagesMap.ts";
@@ -304,17 +304,29 @@ describe("identifyClusterFixes", () => {
304
304
  strictEqual(requesterNames.has("metro-config"), false);
305
305
  });
306
306
 
307
- it("converges the family on the pinned version, without touching the pin", () => {
307
+ it("proposes the metro pin upgrade, since nothing moves react-native", () => {
308
308
  const fix = metroCluster();
309
309
 
310
- // The root pins metro at 0.84.5 on purpose, so the pin is a constraint to
311
- // resolve around, not something to upgrade: the family collapses onto
312
- // 0.84.5 and `workspaceChanges` stays empty.
310
+ // react-native@0.87.0 and @react-native/metro-config@0.87.0 hold the 0.87.0
311
+ // metro subtree. Nothing pins them exactly — they arrive through `*` /
312
+ // `0.83 - 0.86` peer ranges — but a resolver keeps a locked version its
313
+ // requesters accept, so they stay, and their `^0.87.0` on metro keeps the
314
+ // family off 0.84.5. The root's pin is the only thing left to move: a last
315
+ // resort, proposed because nothing else deduplicates the family.
313
316
  strictEqual(fix.applicable, true);
314
- strictEqual(fix.target, "0.84.5");
315
- strictEqual(fix.direction, "down");
317
+ strictEqual(fix.target, "0.87.0");
318
+ strictEqual(fix.direction, "up");
316
319
  strictEqual(fix.convergentMembers.length, 15);
317
- deepStrictEqual(fix.workspaceChanges, []);
320
+ deepStrictEqual(
321
+ fix.workspaceChanges.map(
322
+ (change) => `${change.packageName} ${change.range} -> ${change.to}`,
323
+ ),
324
+ ["metro 0.84.5 -> 0.87.0"],
325
+ );
326
+ strictEqual(fix.needsRoundTrip, true);
327
+ // react-native is only asked for through open ranges: it is never planned
328
+ // onto a version number that only metro carries
329
+ deepStrictEqual(fix.reResolutionSet, []);
318
330
 
319
331
  // metro is the only member a real range applies to (the pin): the 14
320
332
  // metro-* packages are requested through `*` or through metro's own exact
@@ -322,60 +334,17 @@ describe("identifyClusterFixes", () => {
322
334
  deepStrictEqual(fix.driverMembers, ["metro"]);
323
335
  });
324
336
 
325
- it("leaves react-native's version to the resolver", () => {
337
+ it("leaves the wildcards alone once the pin they ignored moves to them", () => {
326
338
  const fix = metroCluster();
327
339
 
328
- // react-native@0.87.0 and @react-native/metro-config@0.87.0 are what hold
329
- // the 0.87.0 metro subtree, and nothing pins either of them there — both
330
- // arrive through `*` / `0.83 - 0.86` peer ranges (0.87.0 does not even
331
- // satisfy the latter). They have to move, but which version they land on is
332
- // the resolver's call, so no version is asserted for them.
333
- deepStrictEqual(fix.floatingMembers, [
334
- "@react-native/metro-config",
335
- "react-native",
336
- ]);
337
- deepStrictEqual(fix.reResolutionSet, []);
338
- strictEqual(fix.needsRoundTrip, true);
339
- });
340
-
341
- it("repoints the wildcards that ignored the pinned version", () => {
342
- const fix = metroCluster();
343
-
344
- // What the fixture is actually about: `@tamagui/metro-plugin` declares
340
+ // What the fixture is about: `@tamagui/metro-plugin` declares
345
341
  // `metro-config: "*"` / `metro-transform-worker: "*"`, and pnpm resolved
346
- // both to 0.87.0 although the pinned 0.84.5 was already in the tree and the
347
- // range accepts it — so the plugin runs against a different metro than the
348
- // app's.
342
+ // both to 0.87.0 beside the pinned 0.84.5 — the plugin runs against a
343
+ // different metro than the app's. The fix moves the pin to 0.87.0, which
344
+ // puts the app on the plugin's copy: sending the plugin back to 0.84.5
345
+ // would split the family again.
349
346
  strictEqual(fix.anchor, "0.84.5");
350
- deepStrictEqual(
351
- fix.reuseFixes.map(
352
- (reuse) =>
353
- `${reuse.requesterName}>${reuse.packageName} "${reuse.range}" ${reuse.from} -> ${reuse.to}`,
354
- ),
355
- [
356
- '@tamagui/metro-plugin>metro-config "*" 0.87.0 -> 0.84.5',
357
- '@tamagui/metro-plugin>metro-transform-worker "*" 0.87.0 -> 0.84.5',
358
- ],
359
- );
360
- });
361
-
362
- it("leaves alone the ranges that cannot accept the pinned version", () => {
363
- const repointed = new Set(
364
- metroCluster().reuseFixes.map((reuse) => reuse.packageName),
365
- );
366
-
367
- // `@react-native/metro-config` requires `metro-config: ^0.87.0`, and
368
- // `react-native-web` requires `@react-native/normalize-colors: ^0.74.1`:
369
- // neither accepts 0.84.5, so neither is repointed.
370
- strictEqual(repointed.has("@react-native/normalize-colors"), false);
371
- deepStrictEqual(
372
- metroCluster()
373
- .reuseFixes.filter(
374
- (reuse) => reuse.requesterName === "@react-native/metro-config",
375
- )
376
- .map((reuse) => reuse.packageName),
377
- [],
378
- );
347
+ deepStrictEqual(fix.reuseFixes, []);
379
348
  });
380
349
 
381
350
  // `aliased-swapped-names`: the importer pins the real `typescript` at 7.0.2
@@ -1,5 +1,6 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { deepStrictEqual } from "node:assert/strict";
2
2
  import fs from "node:fs";
3
+ import { describe, it } from "node:test";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { identifyResolutionFixes } from "pm-dedup-core";
5
6
  import type { PackageResolution } from "./helpers/buildPnpmPackagesMap.ts";
@@ -11,7 +12,7 @@ const loadResolutionsFixture = (fileName: string): PackageResolution[] => {
11
12
  fileURLToPath(
12
13
  new URL(`../test/fixtures/resolutions/${fileName}`, import.meta.url),
13
14
  ),
14
- // eslint-disable-next-line unicorn/prefer-json-parse-buffer
15
+ // oxlint-disable-next-line unicorn-js/prefer-json-parse-buffer
15
16
  "utf8",
16
17
  ),
17
18
  );
@@ -25,7 +26,7 @@ const loadDependentsFixture = (
25
26
  fileURLToPath(
26
27
  new URL(`../test/fixtures/dependents/${fileName}`, import.meta.url),
27
28
  ),
28
- // eslint-disable-next-line unicorn/prefer-json-parse-buffer
29
+ // oxlint-disable-next-line unicorn-js/prefer-json-parse-buffer
29
30
  "utf8",
30
31
  ),
31
32
  );
@@ -39,14 +40,14 @@ describe("identifyResolutionFixes", () => {
39
40
  const resolutions: PackageResolution[] = [];
40
41
 
41
42
  const fixes = identifyResolutionFixes(resolutions, objetToMap({}));
42
- expect(fixes).toEqual([]);
43
+ deepStrictEqual(fixes, []);
43
44
  });
44
45
 
45
46
  it("should return an empty array when there is only one resolution", () => {
46
47
  const resolutions = loadResolutionsFixture("semver-7.7.3.json");
47
48
  const dependents = loadDependentsFixture("semver-7.7.3.json");
48
49
  const fixes = identifyResolutionFixes(resolutions, objetToMap(dependents));
49
- expect(fixes).toEqual([]);
50
+ deepStrictEqual(fixes, []);
50
51
  });
51
52
 
52
53
  it("should not identify fixes when dependencies are not compatible", () => {
@@ -57,7 +58,7 @@ describe("identifyResolutionFixes", () => {
57
58
  "babel-code-frame-7.26.2-7.27.1.json",
58
59
  );
59
60
  const fixes = identifyResolutionFixes(resolutions, objetToMap(dependents));
60
- expect(fixes).toEqual([]);
61
+ deepStrictEqual(fixes, []);
61
62
  });
62
63
 
63
64
  it("should identify resolution fixes when dependencies are compatible", () => {
@@ -69,7 +70,7 @@ describe("identifyResolutionFixes", () => {
69
70
  );
70
71
 
71
72
  const fixes = identifyResolutionFixes(resolutions, objetToMap(dependents));
72
- expect(fixes).toEqual([
73
+ deepStrictEqual(fixes, [
73
74
  {
74
75
  mergeableResolutions: [
75
76
  "printable-shell-command@5.0.7",
@@ -13,6 +13,7 @@ export interface ProjectSnapshot {
13
13
  dependencies?: Record<string, PnpmImporterDependency>;
14
14
  devDependencies?: Record<string, PnpmImporterDependency>;
15
15
  optionalDependencies?: Record<string, PnpmImporterDependency>;
16
+ packageManagerDependencies?: Record<string, PnpmImporterDependency>;
16
17
  }
17
18
 
18
19
  export interface PackageMeta {
@@ -0,0 +1,65 @@
1
+ import { deepStrictEqual, ok, throws } from "node:assert/strict";
2
+ import { writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, it } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createTempProjects } from "./helpers/tempProjects.ts";
7
+ import { readPnpmLock } from "./readPnpmLock.ts";
8
+
9
+ const fixtureLock = (scenario: string): string =>
10
+ fileURLToPath(
11
+ new URL(`../test/fixtures/${scenario}/pnpm-lock.yaml`, import.meta.url),
12
+ );
13
+
14
+ const projects = createTempProjects("pnpm-dedup-read-lock-");
15
+
16
+ afterEach(projects.cleanup);
17
+
18
+ const writeLock = (content: string): string => {
19
+ const path = join(projects.create(), "pnpm-lock.yaml");
20
+ writeFileSync(path, content);
21
+ return path;
22
+ };
23
+
24
+ describe("readPnpmLock", () => {
25
+ it("returns the project document of a lockfile that also resolves the package manager", () => {
26
+ const lock = readPnpmLock(fixtureLock("package-manager-document"));
27
+
28
+ deepStrictEqual(Object.keys(lock.importers?.["."] ?? {}), ["dependencies"]);
29
+ const packageIds = Object.keys(lock.packages ?? {});
30
+ ok(packageIds.includes("printable-shell-command@5.0.7"));
31
+ ok(!packageIds.some((id) => id.startsWith("@pnpm/exe")));
32
+ });
33
+
34
+ it("reads the same project as the single-document lockfile", () => {
35
+ deepStrictEqual(
36
+ readPnpmLock(fixtureLock("package-manager-document")),
37
+ readPnpmLock(fixtureLock("duplicated-printable-shell-command")),
38
+ );
39
+ });
40
+
41
+ it("throws on malformed yaml", () => {
42
+ throws(
43
+ () => readPnpmLock(writeLock("lockfileVersion: '9.0'\nimporters: [\n")),
44
+ { name: "YAMLParseError" },
45
+ );
46
+ });
47
+
48
+ it("throws when only the package-manager document is present", () => {
49
+ const path = writeLock(
50
+ "lockfileVersion: '9.0'\n\nimporters:\n\n .:\n packageManagerDependencies:\n pnpm:\n specifier: ^12.0.0\n version: 12.10.1\n",
51
+ );
52
+
53
+ throws(() => readPnpmLock(path), {
54
+ message: `No project lockfile document found in ${path}`,
55
+ });
56
+ });
57
+
58
+ it("throws on an empty file", () => {
59
+ const path = writeLock("");
60
+
61
+ throws(() => readPnpmLock(path), {
62
+ message: `No project lockfile document found in ${path}`,
63
+ });
64
+ });
65
+ });
@@ -1,7 +1,31 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { parse } from "yaml";
2
+ import { parseAllDocuments } from "yaml";
3
3
  import type { PnpmLockFile } from "./pnpmLockTypes.ts";
4
4
 
5
+ const isPackageManagerDocument = (lock: PnpmLockFile): boolean =>
6
+ Object.values(lock.importers ?? {}).some(
7
+ (importer) => importer.packageManagerDependencies !== undefined,
8
+ );
9
+
10
+ /**
11
+ * A project declaring `devEngines.packageManager` gets a lockfile of two YAML
12
+ * documents from pnpm 12: the first resolves pnpm itself
13
+ * (`packageManagerDependencies`, `@pnpm/exe.*`), the second is the project's.
14
+ * Only the project document is returned — the other one would report pnpm as a
15
+ * dependency of the project.
16
+ */
5
17
  export const readPnpmLock = (filepath: string): PnpmLockFile => {
6
- return parse(readFileSync(filepath, "utf8")) as PnpmLockFile;
18
+ const documents = parseAllDocuments(readFileSync(filepath, "utf8"));
19
+ const locks = documents.map((document) => {
20
+ const [error] = document.errors;
21
+ if (error) throw error;
22
+ return document.toJS() as PnpmLockFile | null;
23
+ });
24
+ const projectLock = locks.findLast(
25
+ (lock) => lock !== null && !isPackageManagerDocument(lock),
26
+ );
27
+ if (!projectLock) {
28
+ throw new Error(`No project lockfile document found in ${filepath}`);
29
+ }
30
+ return projectLock;
7
31
  };