isolate-package 1.33.0 → 1.35.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.
Files changed (68) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/index.mjs.map +1 -1
  3. package/dist/isolate-bin.mjs +5 -6
  4. package/dist/isolate-bin.mjs.map +1 -1
  5. package/dist/{isolate-DyRD5Zd_.mjs → isolate-ts-Igq7C.mjs} +888 -271
  6. package/dist/isolate-ts-Igq7C.mjs.map +1 -0
  7. package/package.json +23 -19
  8. package/src/get-internal-package-names.test.ts +1 -1
  9. package/src/get-internal-package-names.ts +2 -2
  10. package/src/isolate-bin.ts +5 -5
  11. package/src/isolate.ts +22 -17
  12. package/src/lib/config.test.ts +1 -1
  13. package/src/lib/config.ts +3 -3
  14. package/src/lib/lockfile/helpers/bun-lockfile.ts +153 -0
  15. package/src/lib/lockfile/helpers/generate-bun-lockfile.test.ts +3 -3
  16. package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +14 -146
  17. package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +1 -5
  18. package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +311 -16
  19. package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +193 -22
  20. package/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +83 -2
  21. package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +33 -6
  22. package/src/lib/lockfile/helpers/generate-yarn-lockfile.ts +5 -5
  23. package/src/lib/lockfile/process-lockfile.test.ts +2 -2
  24. package/src/lib/manifest/adapt-target-package-manifest.ts +22 -13
  25. package/src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts +72 -3
  26. package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +22 -12
  27. package/src/lib/manifest/helpers/adapt-manifest-internal-deps.ts +1 -1
  28. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +4 -4
  29. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +7 -7
  30. package/src/lib/manifest/helpers/resolve-catalog-dependencies.test.ts +410 -0
  31. package/src/lib/manifest/helpers/resolve-catalog-dependencies.ts +115 -27
  32. package/src/lib/manifest/io.ts +6 -2
  33. package/src/lib/manifest/validate-manifest.ts +2 -2
  34. package/src/lib/output/get-build-output-dir.ts +1 -1
  35. package/src/lib/output/pack-dependencies.ts +1 -1
  36. package/src/lib/output/process-build-output-files.ts +6 -17
  37. package/src/lib/package-manager/helpers/infer-from-files.ts +5 -5
  38. package/src/lib/package-manager/helpers/infer-from-manifest.ts +7 -8
  39. package/src/lib/package-manager/index.ts +1 -1
  40. package/src/lib/package-manager/names.ts +8 -10
  41. package/src/lib/patches/collect-installed-names-bun.test.ts +154 -0
  42. package/src/lib/patches/collect-installed-names-bun.ts +87 -0
  43. package/src/lib/patches/collect-installed-names-pnpm.test.ts +316 -0
  44. package/src/lib/patches/collect-installed-names-pnpm.ts +365 -0
  45. package/src/lib/patches/copy-patches.test.ts +130 -13
  46. package/src/lib/patches/copy-patches.ts +47 -10
  47. package/src/lib/patches/write-isolate-pnpm-workspace.test.ts +83 -3
  48. package/src/lib/patches/write-isolate-pnpm-workspace.ts +4 -4
  49. package/src/lib/registry/collect-reachable-package-names.test.ts +1 -1
  50. package/src/lib/registry/create-packages-registry.ts +34 -31
  51. package/src/lib/registry/helpers/find-packages-globs.ts +23 -19
  52. package/src/lib/registry/list-internal-packages.test.ts +2 -2
  53. package/src/lib/types.ts +2 -2
  54. package/src/lib/utils/filter-patched-dependencies.test.ts +1 -1
  55. package/src/lib/utils/filter-patched-dependencies.ts +2 -2
  56. package/src/lib/utils/get-dirname.ts +1 -1
  57. package/src/lib/utils/index.ts +1 -1
  58. package/src/lib/utils/json.ts +12 -14
  59. package/src/lib/utils/pack.ts +32 -22
  60. package/src/lib/utils/reset-isolate-dir.test.ts +165 -0
  61. package/src/lib/utils/reset-isolate-dir.ts +147 -0
  62. package/src/lib/utils/unpack.test.ts +76 -0
  63. package/src/lib/utils/unpack.ts +16 -10
  64. package/src/lib/utils/wait-for-complete-file.test.ts +105 -0
  65. package/src/lib/utils/wait-for-complete-file.ts +44 -0
  66. package/src/lib/utils/yaml.ts +8 -9
  67. package/src/testing/setup.ts +1 -1
  68. package/dist/isolate-DyRD5Zd_.mjs.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import fs from "fs-extra";
2
2
  import assert from "node:assert";
3
3
  import path from "node:path";
4
- import { useLogger } from "~/lib/logger";
5
- import { getMajorVersion } from "~/lib/utils/get-major-version";
4
+ import { useLogger } from "#/lib/logger";
5
+ import { getMajorVersion } from "#/lib/utils/get-major-version";
6
6
  import type { PackageManifest } from "../../types";
7
7
  import { readTypedJsonSync } from "../../utils";
8
8
  import type { PackageManagerName } from "../names";
@@ -11,19 +11,18 @@ import { getLockfileFileName, supportedPackageManagerNames } from "../names";
11
11
  export function inferFromManifest(workspaceRoot: string) {
12
12
  const log = useLogger();
13
13
 
14
- const { packageManager: packageManagerString } =
15
- readTypedJsonSync<PackageManifest>(
16
- path.join(workspaceRoot, "package.json"),
17
- );
14
+ const { packageManager: packageManagerString } = readTypedJsonSync(
15
+ path.join(workspaceRoot, "package.json"),
16
+ ) as PackageManifest;
18
17
 
19
18
  if (!packageManagerString) {
20
19
  log.debug("No packageManager field found in root manifest");
21
- return;
20
+ return null;
22
21
  }
23
22
 
24
23
  const [name, version = "*"] = packageManagerString.split("@") as [
25
24
  PackageManagerName,
26
- string,
25
+ string | undefined,
27
26
  ];
28
27
 
29
28
  assert(
@@ -9,7 +9,7 @@ let packageManager: PackageManager | undefined;
9
9
 
10
10
  export function usePackageManager() {
11
11
  if (!packageManager) {
12
- throw Error(
12
+ throw new Error(
13
13
  "No package manager detected. Make sure to call detectPackageManager() before usePackageManager()",
14
14
  );
15
15
  }
@@ -14,15 +14,13 @@ export type PackageManager = {
14
14
  packageManagerString?: string;
15
15
  };
16
16
 
17
+ const lockfileFileNamesByPackageManager: Record<PackageManagerName, string> = {
18
+ bun: "bun.lock",
19
+ pnpm: "pnpm-lock.yaml",
20
+ yarn: "yarn.lock",
21
+ npm: "package-lock.json",
22
+ };
23
+
17
24
  export function getLockfileFileName(name: PackageManagerName) {
18
- switch (name) {
19
- case "bun":
20
- return "bun.lock";
21
- case "pnpm":
22
- return "pnpm-lock.yaml";
23
- case "yarn":
24
- return "yarn.lock";
25
- case "npm":
26
- return "package-lock.json";
27
- }
25
+ return lockfileFileNamesByPackageManager[name];
28
26
  }
@@ -0,0 +1,154 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { collectInstalledNamesFromBunLockfile } from "./collect-installed-names-bun";
3
+
4
+ vi.mock("fs-extra", () => ({
5
+ default: {
6
+ existsSync: vi.fn(),
7
+ },
8
+ }));
9
+
10
+ vi.mock("#/lib/utils", () => ({
11
+ readTypedJsonSync: vi.fn(),
12
+ }));
13
+
14
+ const fs = vi.mocked((await import("fs-extra")).default);
15
+ const { readTypedJsonSync } = vi.mocked(await import("#/lib/utils"));
16
+
17
+ const baseArgs = {
18
+ workspaceRootDir: "/workspace",
19
+ targetPackageDir: "/workspace/packages/consumer",
20
+ internalDepPackageNames: [],
21
+ packagesRegistry: {},
22
+ includeDevDependencies: false,
23
+ };
24
+
25
+ describe("collectInstalledNamesFromBunLockfile", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ it("returns an empty set when bun.lock is missing", () => {
35
+ fs.existsSync.mockReturnValue(false);
36
+
37
+ const result = collectInstalledNamesFromBunLockfile({
38
+ ...baseArgs,
39
+ });
40
+
41
+ expect(result).toEqual(new Set());
42
+ });
43
+
44
+ it("walks external-to-external transitives from the target workspace entry", () => {
45
+ fs.existsSync.mockReturnValue(true);
46
+ readTypedJsonSync.mockReturnValue({
47
+ lockfileVersion: 1,
48
+ workspaces: {
49
+ "packages/consumer": {
50
+ name: "consumer",
51
+ dependencies: { "@react-pdf/renderer": "^4.0.0" },
52
+ },
53
+ },
54
+ packages: {
55
+ "@react-pdf/renderer": [
56
+ "@react-pdf/renderer@4.0.0",
57
+ "https://registry/...",
58
+ { dependencies: { "@react-pdf/render": "4.3.0" } },
59
+ "checksum",
60
+ ],
61
+ "@react-pdf/render": [
62
+ "@react-pdf/render@4.3.0",
63
+ "https://registry/...",
64
+ {},
65
+ "checksum",
66
+ ],
67
+ },
68
+ });
69
+
70
+ const result = collectInstalledNamesFromBunLockfile({
71
+ ...baseArgs,
72
+ });
73
+
74
+ expect(result.has("@react-pdf/renderer")).toBe(true);
75
+ expect(result.has("@react-pdf/render")).toBe(true);
76
+ });
77
+
78
+ it("walks transitives reachable through internal workspace entries", () => {
79
+ fs.existsSync.mockReturnValue(true);
80
+ readTypedJsonSync.mockReturnValue({
81
+ lockfileVersion: 1,
82
+ workspaces: {
83
+ "packages/consumer": {
84
+ name: "consumer",
85
+ dependencies: { "firebase-package": "workspace:*" },
86
+ },
87
+ "packages/firebase-package": {
88
+ name: "firebase-package",
89
+ dependencies: { tslib: "^2.0.0" },
90
+ },
91
+ },
92
+ packages: {
93
+ tslib: ["tslib@2.0.0", "https://registry/...", {}, "checksum"],
94
+ },
95
+ });
96
+
97
+ const result = collectInstalledNamesFromBunLockfile({
98
+ ...baseArgs,
99
+ internalDepPackageNames: ["firebase-package"],
100
+ packagesRegistry: {
101
+ "firebase-package": {
102
+ absoluteDir: "/workspace/packages/firebase-package",
103
+ rootRelativeDir: "packages/firebase-package",
104
+ manifest: { name: "firebase-package", version: "1.0.0" },
105
+ },
106
+ },
107
+ });
108
+
109
+ expect(result.has("tslib")).toBe(true);
110
+ });
111
+
112
+ it("skips devDependencies of the target when includeDevDependencies is false", () => {
113
+ fs.existsSync.mockReturnValue(true);
114
+ readTypedJsonSync.mockReturnValue({
115
+ lockfileVersion: 1,
116
+ workspaces: {
117
+ "packages/consumer": {
118
+ name: "consumer",
119
+ dependencies: { lodash: "^4.0.0" },
120
+ devDependencies: { typescript: "^5.0.0" },
121
+ },
122
+ },
123
+ packages: {
124
+ lodash: ["lodash@4.17.21", "https://registry/...", {}, "checksum"],
125
+ typescript: [
126
+ "typescript@5.5.0",
127
+ "https://registry/...",
128
+ {},
129
+ "checksum",
130
+ ],
131
+ },
132
+ });
133
+
134
+ const result = collectInstalledNamesFromBunLockfile({
135
+ ...baseArgs,
136
+ });
137
+
138
+ expect(result.has("lodash")).toBe(true);
139
+ expect(result.has("typescript")).toBe(false);
140
+ });
141
+
142
+ it("returns an empty set when the lockfile read throws", () => {
143
+ fs.existsSync.mockReturnValue(true);
144
+ readTypedJsonSync.mockImplementation(() => {
145
+ throw new Error("invalid json");
146
+ });
147
+
148
+ const result = collectInstalledNamesFromBunLockfile({
149
+ ...baseArgs,
150
+ });
151
+
152
+ expect(result).toEqual(new Set());
153
+ });
154
+ });
@@ -0,0 +1,87 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import {
4
+ type BunLockfile,
5
+ collectDependencyNames,
6
+ collectRequiredPackages,
7
+ } from "#/lib/lockfile/helpers/bun-lockfile";
8
+ import { useLogger } from "#/lib/logger";
9
+ import type { PackagesRegistry } from "#/lib/types";
10
+ import { readTypedJsonSync } from "#/lib/utils";
11
+
12
+ /**
13
+ * Walk the workspace bun.lock starting from the target package and its
14
+ * internal workspace dependencies, returning the set of every package name
15
+ * that will end up installed in the isolate (including deep
16
+ * external-to-external transitives).
17
+ *
18
+ * Used by `copyPatches` to preserve patches for transitive deps that aren't
19
+ * directly listed on any internal manifest. Returns an empty set on any
20
+ * failure so the caller falls back to manifest-based reachability.
21
+ */
22
+ export function collectInstalledNamesFromBunLockfile({
23
+ workspaceRootDir,
24
+ targetPackageDir,
25
+ internalDepPackageNames,
26
+ packagesRegistry,
27
+ includeDevDependencies,
28
+ }: {
29
+ workspaceRootDir: string;
30
+ targetPackageDir: string;
31
+ internalDepPackageNames: string[];
32
+ packagesRegistry: PackagesRegistry;
33
+ includeDevDependencies: boolean;
34
+ }): Set<string> {
35
+ const log = useLogger();
36
+
37
+ try {
38
+ const lockfilePath = path.join(workspaceRootDir, "bun.lock");
39
+ if (!fs.existsSync(lockfilePath)) {
40
+ log.debug("No bun.lock available for installed-names walk");
41
+ return new Set();
42
+ }
43
+
44
+ const lockfile = readTypedJsonSync(lockfilePath) as BunLockfile;
45
+
46
+ const targetWorkspaceKey = path
47
+ .relative(workspaceRootDir, targetPackageDir)
48
+ .split(path.sep)
49
+ .join(path.posix.sep);
50
+
51
+ const internalWorkspaceKeys = internalDepPackageNames
52
+ .map((name) => {
53
+ const pkg = packagesRegistry[name];
54
+ if (!pkg) return null;
55
+ return pkg.rootRelativeDir.split(path.sep).join(path.posix.sep);
56
+ })
57
+ .filter(Boolean) as string[];
58
+
59
+ const directDependencyNames = new Set<string>();
60
+
61
+ const targetEntry = lockfile.workspaces[targetWorkspaceKey];
62
+ if (targetEntry) {
63
+ for (const name of collectDependencyNames(
64
+ targetEntry,
65
+ includeDevDependencies,
66
+ )) {
67
+ directDependencyNames.add(name);
68
+ }
69
+ }
70
+
71
+ for (const workspaceKey of internalWorkspaceKeys) {
72
+ const entry = lockfile.workspaces[workspaceKey];
73
+ if (!entry) continue;
74
+ /** Internal workspace deps never bring in their devDependencies */
75
+ for (const name of collectDependencyNames(entry, false)) {
76
+ directDependencyNames.add(name);
77
+ }
78
+ }
79
+
80
+ return collectRequiredPackages(directDependencyNames, lockfile.packages);
81
+ } catch (error) {
82
+ log.debug(
83
+ `Failed to walk bun.lock for installed names: ${error instanceof Error ? error.message : String(error)}`,
84
+ );
85
+ return new Set();
86
+ }
87
+ }
@@ -0,0 +1,316 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { collectInstalledNamesFromPnpmLockfile } from "./collect-installed-names-pnpm";
3
+
4
+ vi.mock("pnpm_lockfile_file_v8", () => ({
5
+ readWantedLockfile: vi.fn(() => Promise.resolve(null)),
6
+ getLockfileImporterId: vi.fn(
7
+ (root: string, dir: string) => dir.replace(`${root}/`, "") || ".",
8
+ ),
9
+ }));
10
+
11
+ vi.mock("pnpm_lockfile_file_v9", () => ({
12
+ readWantedLockfile: vi.fn(() => Promise.resolve(null)),
13
+ getLockfileImporterId: vi.fn(
14
+ (root: string, dir: string) => dir.replace(`${root}/`, "") || ".",
15
+ ),
16
+ }));
17
+
18
+ vi.mock("#/lib/utils", () => ({
19
+ getPackageName: vi.fn((spec: string) => {
20
+ if (spec.startsWith("@")) {
21
+ const parts = spec.split("@");
22
+ return `@${parts[1] ?? ""}`;
23
+ }
24
+ return spec.split("@")[0] ?? "";
25
+ }),
26
+ isRushWorkspace: vi.fn(() => false),
27
+ }));
28
+
29
+ const { readWantedLockfile: readWantedLockfile_v9 } = vi.mocked(
30
+ await import("pnpm_lockfile_file_v9"),
31
+ );
32
+ const { readWantedLockfile: readWantedLockfile_v8 } = vi.mocked(
33
+ await import("pnpm_lockfile_file_v8"),
34
+ );
35
+
36
+ const baseArgs = {
37
+ workspaceRootDir: "/workspace",
38
+ targetPackageDir: "/workspace/packages/consumer",
39
+ internalDepPackageNames: [],
40
+ packagesRegistry: {},
41
+ includeDevDependencies: false,
42
+ };
43
+
44
+ describe("collectInstalledNamesFromPnpmLockfile", () => {
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.restoreAllMocks();
51
+ });
52
+
53
+ it("returns an empty set when the lockfile is missing", async () => {
54
+ readWantedLockfile_v9.mockResolvedValue(null);
55
+
56
+ const result = await collectInstalledNamesFromPnpmLockfile({
57
+ ...baseArgs,
58
+ majorVersion: 9,
59
+ });
60
+
61
+ expect(result).toEqual(new Set());
62
+ });
63
+
64
+ it("walks external-to-external transitives from the target importer", async () => {
65
+ readWantedLockfile_v9.mockResolvedValue({
66
+ lockfileVersion: "9.0",
67
+ importers: {
68
+ "packages/consumer": {
69
+ specifiers: { "@react-pdf/renderer": "^4.0.0" },
70
+ dependencies: { "@react-pdf/renderer": "4.0.0" },
71
+ },
72
+ },
73
+ packages: {
74
+ "@react-pdf/renderer@4.0.0": {
75
+ resolution: { integrity: "sha512-x" },
76
+ dependencies: { "@react-pdf/render": "4.3.0" },
77
+ },
78
+ "@react-pdf/render@4.3.0": {
79
+ resolution: { integrity: "sha512-y" },
80
+ },
81
+ },
82
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
83
+
84
+ const result = await collectInstalledNamesFromPnpmLockfile({
85
+ ...baseArgs,
86
+ majorVersion: 9,
87
+ });
88
+
89
+ expect(result.has("@react-pdf/renderer")).toBe(true);
90
+ expect(result.has("@react-pdf/render")).toBe(true);
91
+ });
92
+
93
+ it("walks transitives reachable through internal workspace importers", async () => {
94
+ readWantedLockfile_v9.mockResolvedValue({
95
+ lockfileVersion: "9.0",
96
+ importers: {
97
+ "packages/consumer": {
98
+ specifiers: { "firebase-package": "workspace:*" },
99
+ dependencies: { "firebase-package": "link:../firebase-package" },
100
+ },
101
+ "packages/firebase-package": {
102
+ specifiers: { tslib: "^2.0.0" },
103
+ dependencies: { tslib: "2.0.0" },
104
+ },
105
+ },
106
+ packages: {
107
+ "tslib@2.0.0": { resolution: { integrity: "sha512-z" } },
108
+ },
109
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
110
+
111
+ const result = await collectInstalledNamesFromPnpmLockfile({
112
+ ...baseArgs,
113
+ internalDepPackageNames: ["firebase-package"],
114
+ packagesRegistry: {
115
+ "firebase-package": {
116
+ absoluteDir: "/workspace/packages/firebase-package",
117
+ rootRelativeDir: "packages/firebase-package",
118
+ manifest: { name: "firebase-package", version: "1.0.0" },
119
+ },
120
+ },
121
+ majorVersion: 9,
122
+ });
123
+
124
+ expect(result.has("tslib")).toBe(true);
125
+ });
126
+
127
+ it("does not include the target's devDependencies when includeDevDependencies is false", async () => {
128
+ readWantedLockfile_v9.mockResolvedValue({
129
+ lockfileVersion: "9.0",
130
+ importers: {
131
+ "packages/consumer": {
132
+ specifiers: { lodash: "^4.0.0", typescript: "^5.0.0" },
133
+ dependencies: { lodash: "4.17.21" },
134
+ devDependencies: { typescript: "5.5.0" },
135
+ },
136
+ },
137
+ packages: {
138
+ "lodash@4.17.21": { resolution: { integrity: "sha512-a" } },
139
+ "typescript@5.5.0": { resolution: { integrity: "sha512-b" } },
140
+ },
141
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
142
+
143
+ const result = await collectInstalledNamesFromPnpmLockfile({
144
+ ...baseArgs,
145
+ majorVersion: 9,
146
+ });
147
+
148
+ expect(result.has("lodash")).toBe(true);
149
+ expect(result.has("typescript")).toBe(false);
150
+ });
151
+
152
+ it("includes the target's devDependencies when includeDevDependencies is true", async () => {
153
+ readWantedLockfile_v9.mockResolvedValue({
154
+ lockfileVersion: "9.0",
155
+ importers: {
156
+ "packages/consumer": {
157
+ specifiers: { typescript: "^5.0.0" },
158
+ devDependencies: { typescript: "5.5.0" },
159
+ },
160
+ },
161
+ packages: {
162
+ "typescript@5.5.0": { resolution: { integrity: "sha512-b" } },
163
+ },
164
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
165
+
166
+ const result = await collectInstalledNamesFromPnpmLockfile({
167
+ ...baseArgs,
168
+ majorVersion: 9,
169
+ includeDevDependencies: true,
170
+ });
171
+
172
+ expect(result.has("typescript")).toBe(true);
173
+ });
174
+
175
+ it("walks transitives via v8 v5-style depPath keys for pnpm major < 9", async () => {
176
+ /**
177
+ * After `readWantedLockfile_v8` normalizes a pnpm 8 lockfile (lockfile
178
+ * version 6.x), `lockfile.packages` is keyed in v5 form: leading slash
179
+ * with `/` separator between name and version, e.g. `/foo/1.0.0` and
180
+ * `/@scope/foo/1.0.0`.
181
+ */
182
+ readWantedLockfile_v8.mockResolvedValue({
183
+ lockfileVersion: 6.1,
184
+ importers: {
185
+ "packages/consumer": {
186
+ specifiers: { "@react-pdf/renderer": "^4.0.0" },
187
+ dependencies: { "@react-pdf/renderer": "4.0.0" },
188
+ },
189
+ },
190
+ packages: {
191
+ "/@react-pdf/renderer/4.0.0": {
192
+ resolution: { integrity: "sha512-x" },
193
+ dependencies: { "@react-pdf/render": "4.3.0" },
194
+ },
195
+ "/@react-pdf/render/4.3.0": {
196
+ resolution: { integrity: "sha512-y" },
197
+ },
198
+ },
199
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v8>>);
200
+
201
+ const result = await collectInstalledNamesFromPnpmLockfile({
202
+ ...baseArgs,
203
+ majorVersion: 8,
204
+ });
205
+
206
+ expect(readWantedLockfile_v8).toHaveBeenCalled();
207
+ expect(readWantedLockfile_v9).not.toHaveBeenCalled();
208
+ expect(result.has("@react-pdf/renderer")).toBe(true);
209
+ expect(result.has("@react-pdf/render")).toBe(true);
210
+ });
211
+
212
+ it("includes peerDependencies of package snapshots in the name set", async () => {
213
+ /**
214
+ * Peer requirement values aren't resolved depPaths, so we just collect
215
+ * the names. This mirrors `collectReachablePackageNames` and the bun
216
+ * walker, both of which include peerDependencies.
217
+ */
218
+ readWantedLockfile_v9.mockResolvedValue({
219
+ lockfileVersion: "9.0",
220
+ importers: {
221
+ "packages/consumer": {
222
+ specifiers: { "some-pkg": "^1.0.0" },
223
+ dependencies: { "some-pkg": "1.0.0" },
224
+ },
225
+ },
226
+ packages: {
227
+ "some-pkg@1.0.0": {
228
+ resolution: { integrity: "sha512-p" },
229
+ peerDependencies: { "peer-only-dep": ">=1" },
230
+ },
231
+ },
232
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
233
+
234
+ const result = await collectInstalledNamesFromPnpmLockfile({
235
+ ...baseArgs,
236
+ majorVersion: 9,
237
+ });
238
+
239
+ expect(result.has("some-pkg")).toBe(true);
240
+ expect(result.has("peer-only-dep")).toBe(true);
241
+ });
242
+
243
+ it("strips peer-resolution suffixes when extracting package names", async () => {
244
+ readWantedLockfile_v9.mockResolvedValue({
245
+ lockfileVersion: "9.0",
246
+ importers: {
247
+ "packages/consumer": {
248
+ specifiers: { "react-dom": "^18.0.0" },
249
+ dependencies: {
250
+ "react-dom": "18.2.0(react@18.2.0)",
251
+ },
252
+ },
253
+ },
254
+ packages: {
255
+ "react-dom@18.2.0(react@18.2.0)": {
256
+ resolution: { integrity: "sha512-d" },
257
+ dependencies: { react: "18.2.0" },
258
+ },
259
+ "react@18.2.0": { resolution: { integrity: "sha512-e" } },
260
+ },
261
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
262
+
263
+ const result = await collectInstalledNamesFromPnpmLockfile({
264
+ ...baseArgs,
265
+ majorVersion: 9,
266
+ });
267
+
268
+ expect(result.has("react-dom")).toBe(true);
269
+ expect(result.has("react")).toBe(true);
270
+ });
271
+
272
+ it("returns an empty set when the lockfile read throws", async () => {
273
+ readWantedLockfile_v9.mockRejectedValueOnce(new Error("boom"));
274
+
275
+ const result = await collectInstalledNamesFromPnpmLockfile({
276
+ ...baseArgs,
277
+ majorVersion: 9,
278
+ });
279
+
280
+ expect(result).toEqual(new Set());
281
+ });
282
+
283
+ it("normalizes the target importer id before the isTarget check (Windows)", async () => {
284
+ /**
285
+ * Simulate Windows: getLockfileImporterId returns a backslash-separated
286
+ * id, but the lockfile's importer keys use POSIX separators. Without
287
+ * normalizing the id used in the isTarget comparison, the target's
288
+ * devDependencies would be skipped even with includeDevDependencies=true.
289
+ */
290
+ const { getLockfileImporterId } = vi.mocked(
291
+ await import("pnpm_lockfile_file_v9"),
292
+ );
293
+ getLockfileImporterId.mockReturnValueOnce("packages\\consumer");
294
+
295
+ readWantedLockfile_v9.mockResolvedValue({
296
+ lockfileVersion: "9.0",
297
+ importers: {
298
+ "packages/consumer": {
299
+ specifiers: { typescript: "^5.0.0" },
300
+ devDependencies: { typescript: "5.5.0" },
301
+ },
302
+ },
303
+ packages: {
304
+ "typescript@5.5.0": { resolution: { integrity: "sha512-w" } },
305
+ },
306
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
307
+
308
+ const result = await collectInstalledNamesFromPnpmLockfile({
309
+ ...baseArgs,
310
+ majorVersion: 9,
311
+ includeDevDependencies: true,
312
+ });
313
+
314
+ expect(result.has("typescript")).toBe(true);
315
+ });
316
+ });