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
@@ -0,0 +1,365 @@
1
+ import path from "node:path";
2
+ import {
3
+ getLockfileImporterId as getLockfileImporterId_v8,
4
+ readWantedLockfile as readWantedLockfile_v8,
5
+ } from "pnpm_lockfile_file_v8";
6
+ import {
7
+ getLockfileImporterId as getLockfileImporterId_v9,
8
+ readWantedLockfile as readWantedLockfile_v9,
9
+ } from "pnpm_lockfile_file_v9";
10
+ import { useLogger } from "#/lib/logger";
11
+ import type { PackagesRegistry } from "#/lib/types";
12
+ import { getPackageName, isRushWorkspace } from "#/lib/utils";
13
+
14
+ /**
15
+ * Walk the workspace pnpm lockfile starting from the target package and its
16
+ * internal workspace dependencies, returning the set of every package name
17
+ * that will end up installed in the isolate (including deep
18
+ * external-to-external transitives).
19
+ *
20
+ * Used by `copyPatches` to preserve patches for transitive deps that aren't
21
+ * directly listed on any internal manifest. Returns an empty set on any
22
+ * failure so the caller falls back to manifest-based reachability. When the
23
+ * lockfile is present but lacks a `packages` section, returns just the
24
+ * direct importer dep names.
25
+ */
26
+ export async function collectInstalledNamesFromPnpmLockfile({
27
+ workspaceRootDir,
28
+ targetPackageDir,
29
+ internalDepPackageNames,
30
+ packagesRegistry,
31
+ majorVersion,
32
+ includeDevDependencies,
33
+ }: {
34
+ workspaceRootDir: string;
35
+ targetPackageDir: string;
36
+ internalDepPackageNames: string[];
37
+ packagesRegistry: PackagesRegistry;
38
+ majorVersion: number;
39
+ includeDevDependencies: boolean;
40
+ }): Promise<Set<string>> {
41
+ const log = useLogger();
42
+
43
+ try {
44
+ const useVersion9 = majorVersion >= 9;
45
+ const isRush = isRushWorkspace(workspaceRootDir);
46
+ const lockfileDir = isRush
47
+ ? path.join(workspaceRootDir, "common/config/rush")
48
+ : workspaceRootDir;
49
+
50
+ const lockfile = useVersion9
51
+ ? await readWantedLockfile_v9(lockfileDir, { ignoreIncompatible: false })
52
+ : await readWantedLockfile_v8(lockfileDir, { ignoreIncompatible: false });
53
+
54
+ if (!lockfile) {
55
+ log.debug("No pnpm lockfile available for installed-names walk");
56
+ return new Set();
57
+ }
58
+
59
+ const rawTargetImporterId = useVersion9
60
+ ? getLockfileImporterId_v9(workspaceRootDir, targetPackageDir)
61
+ : getLockfileImporterId_v8(workspaceRootDir, targetPackageDir);
62
+
63
+ /**
64
+ * Normalize separators to POSIX so Windows callers match the lockfile's
65
+ * importer keys (mirrors generate-pnpm-lockfile.ts). Applied once here so
66
+ * the `isTarget` equality check below compares apples-to-apples — without
67
+ * this, on Windows the raw id with backslashes wouldn't match the
68
+ * normalized id used as the importers map key.
69
+ */
70
+ const targetImporterId = toLockfileImporterKey(rawTargetImporterId, isRush);
71
+
72
+ const importerIds = [
73
+ targetImporterId,
74
+ ...(
75
+ internalDepPackageNames
76
+ .map((name) => packagesRegistry[name]?.rootRelativeDir)
77
+ .filter(Boolean) as string[]
78
+ ).map((dir) => toLockfileImporterKey(dir, isRush)),
79
+ ];
80
+
81
+ const packages = (lockfile as { packages?: Record<string, PnpmPackage> })
82
+ .packages;
83
+
84
+ if (!packages) {
85
+ log.debug("Lockfile has no packages section to walk");
86
+ return collectImporterDirectNames(
87
+ lockfile.importers,
88
+ importerIds,
89
+ targetImporterId,
90
+ includeDevDependencies,
91
+ );
92
+ }
93
+
94
+ const names = new Set<string>();
95
+ const seen = new Set<string>();
96
+ const queue: string[] = [];
97
+
98
+ for (const importerId of importerIds) {
99
+ const importer = lockfile.importers[importerId];
100
+ if (!importer) continue;
101
+
102
+ const isTarget = importerId === targetImporterId;
103
+
104
+ enqueueImporterDeps({
105
+ importer,
106
+ names,
107
+ queue,
108
+ useVersion9,
109
+ includeDevDependencies: isTarget && includeDevDependencies,
110
+ });
111
+ }
112
+
113
+ let depPath: string | undefined;
114
+ while ((depPath = queue.pop()) !== undefined) {
115
+ if (seen.has(depPath)) continue;
116
+ seen.add(depPath);
117
+
118
+ names.add(extractPackageName(depPath));
119
+
120
+ const pkg = packages[depPath];
121
+ if (!pkg) continue;
122
+
123
+ enqueueResolvedDeps(pkg.dependencies, names, queue, useVersion9, seen);
124
+ enqueueResolvedDeps(
125
+ pkg.optionalDependencies,
126
+ names,
127
+ queue,
128
+ useVersion9,
129
+ seen,
130
+ );
131
+
132
+ /**
133
+ * Peer requirement values are name → semver-range, not resolved depPaths.
134
+ * Just record the names so a patch on a peer-only external transitive
135
+ * survives filtering (mirrors the bun walker and the sister manifest
136
+ * walker, which both include peerDependencies).
137
+ */
138
+ collectNames(pkg.peerDependencies, names);
139
+ }
140
+
141
+ return names;
142
+ } catch (error) {
143
+ log.debug(
144
+ `Failed to walk pnpm lockfile for installed names: ${error instanceof Error ? error.message : String(error)}`,
145
+ );
146
+ return new Set();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Convert a raw importer id (as returned by `getLockfileImporterId` or a
152
+ * package's rootRelativeDir) to the form actually used as a key in
153
+ * `lockfile.importers`: POSIX separators, with the Rush `../../` prefix when
154
+ * the workspace lives under `common/config/rush`. Lockfile keys are always
155
+ * POSIX regardless of the host OS, so backslashes are normalized
156
+ * unconditionally rather than relying on `path.sep`.
157
+ */
158
+ function toLockfileImporterKey(importerId: string, isRush: boolean): string {
159
+ const posix = importerId
160
+ .split(path.sep)
161
+ .join(path.posix.sep)
162
+ .replace(/\\/g, "/");
163
+ return isRush ? `../../${posix}` : posix;
164
+ }
165
+
166
+ type ResolvedDeps = Record<string, string>;
167
+
168
+ type PnpmImporter = {
169
+ dependencies?: ResolvedDeps;
170
+ optionalDependencies?: ResolvedDeps;
171
+ devDependencies?: ResolvedDeps;
172
+ peerDependencies?: ResolvedDeps;
173
+ };
174
+
175
+ type PnpmPackage = {
176
+ dependencies?: ResolvedDeps;
177
+ optionalDependencies?: ResolvedDeps;
178
+ peerDependencies?: ResolvedDeps;
179
+ };
180
+
181
+ function enqueueImporterDeps({
182
+ importer,
183
+ names,
184
+ queue,
185
+ useVersion9,
186
+ includeDevDependencies,
187
+ }: {
188
+ importer: PnpmImporter;
189
+ names: Set<string>;
190
+ queue: string[];
191
+ useVersion9: boolean;
192
+ includeDevDependencies: boolean;
193
+ }): void {
194
+ enqueueResolvedDeps(importer.dependencies, names, queue, useVersion9);
195
+ enqueueResolvedDeps(importer.optionalDependencies, names, queue, useVersion9);
196
+ if (includeDevDependencies) {
197
+ enqueueResolvedDeps(importer.devDependencies, names, queue, useVersion9);
198
+ }
199
+ /**
200
+ * Importer peerDependencies usually aren't a separate map in the lockfile
201
+ * (autoInstallPeers folds them into `dependencies`), but record names if
202
+ * they happen to be present.
203
+ */
204
+ collectNames(importer.peerDependencies, names);
205
+ }
206
+
207
+ function enqueueResolvedDeps(
208
+ deps: ResolvedDeps | undefined,
209
+ names: Set<string>,
210
+ queue: string[],
211
+ useVersion9: boolean,
212
+ seen?: Set<string>,
213
+ ): void {
214
+ if (!deps) return;
215
+
216
+ for (const [alias, ref] of Object.entries(deps)) {
217
+ /**
218
+ * The alias is the name as listed in the parent's dependencies map. For
219
+ * non-aliased installs this is also the resolved package name. We add it
220
+ * to the set as a candidate name; visiting the actual depPath below
221
+ * refines this with the true installed name.
222
+ */
223
+ names.add(alias);
224
+
225
+ const depPath = refToRelative(ref, alias, useVersion9);
226
+ if (depPath && !seen?.has(depPath)) {
227
+ queue.push(depPath);
228
+ }
229
+ }
230
+ }
231
+
232
+ function collectNames(
233
+ deps: ResolvedDeps | undefined,
234
+ names: Set<string>,
235
+ ): void {
236
+ if (!deps) return;
237
+ for (const name of Object.keys(deps)) {
238
+ names.add(name);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Mirrors `@pnpm/dependency-path`'s `refToRelative`. The depPath shape differs
244
+ * between pnpm 8 (lockfile v6, normalized to v5 keys like `/foo/1.0.0`) and
245
+ * pnpm 9 (lockfile v9 keys like `foo@1.0.0`). Returns the depPath used as a
246
+ * key in `lockfile.packages`, or null if the ref points to a workspace link.
247
+ */
248
+ function refToRelative(
249
+ reference: string,
250
+ pkgName: string,
251
+ useVersion9: boolean,
252
+ ): string | null {
253
+ if (!reference) return null;
254
+ if (reference.startsWith("link:")) return null;
255
+ return useVersion9
256
+ ? refToRelativeV9(reference, pkgName)
257
+ : refToRelativeV8(reference, pkgName);
258
+ }
259
+
260
+ function refToRelativeV9(reference: string, pkgName: string): string | null {
261
+ if (reference.startsWith("@")) return reference;
262
+ const atIndex = reference.indexOf("@");
263
+ if (atIndex === -1) return `${pkgName}@${reference}`;
264
+ const colonIndex = reference.indexOf(":");
265
+ const bracketIndex = reference.indexOf("(");
266
+ if (
267
+ (colonIndex === -1 || atIndex < colonIndex) &&
268
+ (bracketIndex === -1 || atIndex < bracketIndex)
269
+ ) {
270
+ return reference;
271
+ }
272
+ return `${pkgName}@${reference}`;
273
+ }
274
+
275
+ /**
276
+ * v8 form: pnpm 8 (lockfile v6) is normalized on read to v5-style depPaths
277
+ * with leading slash and `/` separator between name and version. Plain
278
+ * version refs build that key; refs already containing a `/` (peer-suffixed
279
+ * or pre-formed) are returned verbatim. Mirrors `@pnpm/dependency-path@2.x`.
280
+ */
281
+ function refToRelativeV8(reference: string, pkgName: string): string | null {
282
+ if (reference.startsWith("file:")) return reference;
283
+ const slashIndex = reference.indexOf("/");
284
+ const bracketIndex = reference.indexOf("(");
285
+ const noSlashBeforeBracket =
286
+ bracketIndex !== -1 && reference.lastIndexOf("/", bracketIndex) === -1;
287
+ if (slashIndex === -1 || noSlashBeforeBracket) {
288
+ return `/${pkgName}/${reference}`;
289
+ }
290
+ return reference;
291
+ }
292
+
293
+ /**
294
+ * Extract the bare package name from a pnpm depPath. Strips the optional
295
+ * peer-resolution suffix (e.g. `(react@18.0.0)`) before parsing. Handles
296
+ * both v9 (`@scope/foo@1.0.0`) and v8 (`/@scope/foo/1.0.0`) shapes.
297
+ */
298
+ function extractPackageName(depPath: string): string {
299
+ const peerStart = indexOfPeersSuffix(depPath);
300
+ const trimmed = peerStart === -1 ? depPath : depPath.slice(0, peerStart);
301
+
302
+ if (trimmed.startsWith("/")) {
303
+ /** v8 v5-style: `/<name>/<version>` */
304
+ const stripped = trimmed.slice(1);
305
+ if (stripped.startsWith("@")) {
306
+ const secondSlash = stripped.indexOf("/", stripped.indexOf("/") + 1);
307
+ return secondSlash === -1 ? stripped : stripped.slice(0, secondSlash);
308
+ }
309
+ const firstSlash = stripped.indexOf("/");
310
+ return firstSlash === -1 ? stripped : stripped.slice(0, firstSlash);
311
+ }
312
+
313
+ return getPackageName(trimmed);
314
+ }
315
+
316
+ /**
317
+ * Mirrors `@pnpm/dependency-path`'s `indexOfPeersSuffix`. Returns the index
318
+ * where the peer-resolution suffix starts, or -1 if there is none.
319
+ */
320
+ function indexOfPeersSuffix(depPath: string): number {
321
+ if (!depPath.endsWith(")")) return -1;
322
+ let open = 1;
323
+ for (let i = depPath.length - 2; i >= 0; i--) {
324
+ if (depPath[i] === "(") {
325
+ open--;
326
+ } else if (depPath[i] === ")") {
327
+ open++;
328
+ } else if (!open) {
329
+ return i + 1;
330
+ }
331
+ }
332
+ return -1;
333
+ }
334
+
335
+ /**
336
+ * Fallback when the lockfile is missing `packages`: just return importer
337
+ * direct dep names so we at least cover some of the graph.
338
+ */
339
+ function collectImporterDirectNames(
340
+ importers: Record<string, PnpmImporter>,
341
+ importerIds: string[],
342
+ targetImporterId: string,
343
+ includeDevDependencies: boolean,
344
+ ): Set<string> {
345
+ const names = new Set<string>();
346
+ for (const importerId of importerIds) {
347
+ const importer = importers[importerId];
348
+ if (!importer) continue;
349
+ const isTarget = importerId === targetImporterId;
350
+ for (const name of Object.keys(importer.dependencies ?? {}))
351
+ names.add(name);
352
+ for (const name of Object.keys(importer.optionalDependencies ?? {})) {
353
+ names.add(name);
354
+ }
355
+ for (const name of Object.keys(importer.peerDependencies ?? {})) {
356
+ names.add(name);
357
+ }
358
+ if (isTarget && includeDevDependencies) {
359
+ for (const name of Object.keys(importer.devDependencies ?? {})) {
360
+ names.add(name);
361
+ }
362
+ }
363
+ }
364
+ return names;
365
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import type { PackageManifest, PnpmSettings } from "~/lib/types";
2
+ import type { PackageManifest, PnpmSettings } from "#/lib/types";
3
3
  import { copyPatches } from "./copy-patches";
4
4
 
5
5
  /** Mock fs-extra */
@@ -12,33 +12,50 @@ vi.mock("fs-extra", () => ({
12
12
  }));
13
13
 
14
14
  /** Mock the utils */
15
- vi.mock("~/lib/utils", () => ({
15
+ vi.mock("#/lib/utils", () => ({
16
16
  filterPatchedDependencies: vi.fn(),
17
17
  getIsolateRelativeLogPath: vi.fn((p: string) => p),
18
+ getPackageName: vi.fn((spec: string) => {
19
+ if (spec.startsWith("@")) {
20
+ const parts = spec.split("@");
21
+ return `@${parts[1] ?? ""}`;
22
+ }
23
+ return spec.split("@")[0] ?? "";
24
+ }),
18
25
  getRootRelativeLogPath: vi.fn((p: string) => p),
19
26
  isRushWorkspace: vi.fn(() => false),
20
27
  readTypedJson: vi.fn(),
28
+ readTypedJsonSync: vi.fn(),
21
29
  readTypedYamlSync: vi.fn(),
22
30
  }));
23
31
 
24
32
  /** Mock the package manager */
25
- vi.mock("~/lib/package-manager", () => ({
33
+ vi.mock("#/lib/package-manager", () => ({
26
34
  usePackageManager: vi.fn(() => ({ name: "pnpm", majorVersion: 9 })),
27
35
  }));
28
36
 
29
37
  /** Mock the pnpm lockfile readers */
30
38
  vi.mock("pnpm_lockfile_file_v8", () => ({
31
39
  readWantedLockfile: vi.fn(() => Promise.resolve(null)),
40
+ getLockfileImporterId: vi.fn(
41
+ (root: string, dir: string) => dir.replace(`${root}/`, "") || ".",
42
+ ),
32
43
  }));
33
44
 
34
45
  vi.mock("pnpm_lockfile_file_v9", () => ({
35
46
  readWantedLockfile: vi.fn(() => Promise.resolve(null)),
47
+ getLockfileImporterId: vi.fn(
48
+ (root: string, dir: string) => dir.replace(`${root}/`, "") || ".",
49
+ ),
36
50
  }));
37
51
 
38
52
  const fs = vi.mocked((await import("fs-extra")).default);
39
53
  const { filterPatchedDependencies, readTypedJson, readTypedYamlSync } =
40
- vi.mocked(await import("~/lib/utils"));
41
- const { usePackageManager } = vi.mocked(await import("~/lib/package-manager"));
54
+ vi.mocked(await import("#/lib/utils"));
55
+ const { usePackageManager } = vi.mocked(await import("#/lib/package-manager"));
56
+ const { readWantedLockfile: readWantedLockfile_v9 } = vi.mocked(
57
+ await import("pnpm_lockfile_file_v9"),
58
+ );
42
59
 
43
60
  describe("copyPatches", () => {
44
61
  beforeEach(() => {
@@ -49,14 +66,6 @@ describe("copyPatches", () => {
49
66
  vi.restoreAllMocks();
50
67
  });
51
68
 
52
- const mockJsonSettings = (settings: PnpmSettings | undefined) => {
53
- readTypedJson.mockResolvedValue({
54
- name: "root",
55
- version: "1.0.0",
56
- pnpm: settings,
57
- });
58
- };
59
-
60
69
  it("should return empty object when workspace root package.json cannot be read", async () => {
61
70
  readTypedYamlSync.mockImplementation(() => {
62
71
  throw new Error("File not found");
@@ -65,6 +74,8 @@ describe("copyPatches", () => {
65
74
 
66
75
  const result = await copyPatches({
67
76
  workspaceRootDir: "/workspace",
77
+ targetPackageDir: "/workspace/packages/test",
78
+ internalDepPackageNames: [],
68
79
  targetPackageManifest: { name: "test", version: "1.0.0" },
69
80
  isolateDir: "/workspace/isolate",
70
81
  packagesRegistry: {},
@@ -126,6 +137,8 @@ describe("copyPatches", () => {
126
137
 
127
138
  const result = await copyPatches({
128
139
  workspaceRootDir: "/workspace",
140
+ targetPackageDir: "/workspace/packages/test",
141
+ internalDepPackageNames: [],
129
142
  targetPackageManifest: { name: "test", version: "1.0.0" },
130
143
  isolateDir: "/workspace/isolate",
131
144
  packagesRegistry: {},
@@ -146,6 +159,8 @@ describe("copyPatches", () => {
146
159
 
147
160
  const result = await copyPatches({
148
161
  workspaceRootDir: "/workspace",
162
+ targetPackageDir: "/workspace/packages/test",
163
+ internalDepPackageNames: [],
149
164
  targetPackageManifest: { name: "test", version: "1.0.0" },
150
165
  isolateDir: "/workspace/isolate",
151
166
  packagesRegistry: {},
@@ -176,6 +191,8 @@ describe("copyPatches", () => {
176
191
 
177
192
  const result = await copyPatches({
178
193
  workspaceRootDir: "/workspace",
194
+ targetPackageDir: "/workspace/packages/test",
195
+ internalDepPackageNames: [],
179
196
  targetPackageManifest: targetManifest,
180
197
  isolateDir: "/workspace/isolate",
181
198
  packagesRegistry: {},
@@ -214,6 +231,8 @@ describe("copyPatches", () => {
214
231
 
215
232
  const result = await copyPatches({
216
233
  workspaceRootDir: "/workspace",
234
+ targetPackageDir: "/workspace/packages/test",
235
+ internalDepPackageNames: [],
217
236
  targetPackageManifest: targetManifest,
218
237
  isolateDir: "/workspace/isolate",
219
238
  packagesRegistry: {},
@@ -256,6 +275,8 @@ describe("copyPatches", () => {
256
275
 
257
276
  const result = await copyPatches({
258
277
  workspaceRootDir: "/workspace",
278
+ targetPackageDir: "/workspace/packages/test",
279
+ internalDepPackageNames: [],
259
280
  targetPackageManifest: targetManifest,
260
281
  isolateDir: "/workspace/isolate",
261
282
  packagesRegistry: {},
@@ -287,6 +308,8 @@ describe("copyPatches", () => {
287
308
 
288
309
  const result = await copyPatches({
289
310
  workspaceRootDir: "/workspace",
311
+ targetPackageDir: "/workspace/packages/test",
312
+ internalDepPackageNames: [],
290
313
  targetPackageManifest: targetManifest,
291
314
  isolateDir: "/workspace/isolate",
292
315
  packagesRegistry: {},
@@ -321,6 +344,8 @@ describe("copyPatches", () => {
321
344
 
322
345
  const result = await copyPatches({
323
346
  workspaceRootDir: "/workspace",
347
+ targetPackageDir: "/workspace/packages/test",
348
+ internalDepPackageNames: [],
324
349
  targetPackageManifest: targetManifest,
325
350
  isolateDir: "/workspace/isolate",
326
351
  packagesRegistry: {},
@@ -364,6 +389,8 @@ describe("copyPatches", () => {
364
389
 
365
390
  const result = await copyPatches({
366
391
  workspaceRootDir: "/workspace",
392
+ targetPackageDir: "/workspace/packages/test",
393
+ internalDepPackageNames: [],
367
394
  targetPackageManifest: targetManifest,
368
395
  isolateDir: "/workspace/isolate",
369
396
  packagesRegistry: {},
@@ -409,6 +436,8 @@ describe("copyPatches", () => {
409
436
 
410
437
  const result = await copyPatches({
411
438
  workspaceRootDir: "/workspace",
439
+ targetPackageDir: "/workspace/packages/test",
440
+ internalDepPackageNames: [],
412
441
  targetPackageManifest: targetManifest,
413
442
  isolateDir: "/workspace/isolate",
414
443
  packagesRegistry: {},
@@ -456,6 +485,8 @@ describe("copyPatches", () => {
456
485
 
457
486
  const result = await copyPatches({
458
487
  workspaceRootDir: "/workspace",
488
+ targetPackageDir: "/workspace/packages/test",
489
+ internalDepPackageNames: [],
459
490
  targetPackageManifest: targetManifest,
460
491
  isolateDir: "/workspace/isolate",
461
492
  packagesRegistry: {},
@@ -509,6 +540,8 @@ describe("copyPatches", () => {
509
540
 
510
541
  const result = await copyPatches({
511
542
  workspaceRootDir: "/workspace",
543
+ targetPackageDir: "/workspace/packages/test",
544
+ internalDepPackageNames: [],
512
545
  targetPackageManifest: consumerManifest,
513
546
  isolateDir: "/workspace/isolate",
514
547
  packagesRegistry: {
@@ -536,4 +569,88 @@ describe("copyPatches", () => {
536
569
  expect(reachable!.has("firebase-package")).toBe(true);
537
570
  expect(reachable!.has("tslib")).toBe(true);
538
571
  });
572
+
573
+ it("should pick up deep external-to-external transitives from the pnpm lockfile (regression: issue #167 follow-up)", async () => {
574
+ /**
575
+ * Target depends on `@react-pdf/renderer` (external). The patched
576
+ * `@react-pdf/render` is only a transitive of `@react-pdf/renderer`. The
577
+ * manifest walker can't see it because it can't open external manifests,
578
+ * so the lockfile walker has to surface it.
579
+ */
580
+ const targetManifest: PackageManifest = {
581
+ name: "consumer",
582
+ version: "1.0.0",
583
+ dependencies: { "@react-pdf/renderer": "^4.0.0" },
584
+ };
585
+
586
+ readTypedYamlSync.mockReturnValue({
587
+ patchedDependencies: {
588
+ "@react-pdf/render@4.3.0": "patches/@react-pdf__render@4.3.0.patch",
589
+ },
590
+ });
591
+ readTypedJson.mockResolvedValue({
592
+ name: "root",
593
+ version: "1.0.0",
594
+ } as PackageManifest);
595
+
596
+ filterPatchedDependencies.mockReturnValue({
597
+ "@react-pdf/render@4.3.0": "patches/@react-pdf__render@4.3.0.patch",
598
+ });
599
+
600
+ fs.existsSync.mockReturnValue(true);
601
+
602
+ usePackageManager.mockReturnValue({
603
+ name: "pnpm",
604
+ majorVersion: 9,
605
+ version: "9.0.0",
606
+ packageManagerString: "pnpm@9.0.0",
607
+ });
608
+
609
+ /**
610
+ * Fake v9 lockfile: target importer depends on @react-pdf/renderer, which
611
+ * has @react-pdf/render as its only resolved dep.
612
+ */
613
+ readWantedLockfile_v9.mockResolvedValue({
614
+ lockfileVersion: "9.0",
615
+ importers: {
616
+ "packages/consumer": {
617
+ specifiers: { "@react-pdf/renderer": "^4.0.0" },
618
+ dependencies: { "@react-pdf/renderer": "4.0.0" },
619
+ },
620
+ },
621
+ packages: {
622
+ "@react-pdf/renderer@4.0.0": {
623
+ resolution: { integrity: "sha512-x" },
624
+ dependencies: { "@react-pdf/render": "4.3.0" },
625
+ },
626
+ "@react-pdf/render@4.3.0": {
627
+ resolution: { integrity: "sha512-y" },
628
+ },
629
+ },
630
+ } as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
631
+
632
+ const result = await copyPatches({
633
+ workspaceRootDir: "/workspace",
634
+ targetPackageDir: "/workspace/packages/consumer",
635
+ internalDepPackageNames: [],
636
+ targetPackageManifest: targetManifest,
637
+ isolateDir: "/workspace/isolate",
638
+ packagesRegistry: {},
639
+ includeDevDependencies: false,
640
+ });
641
+
642
+ expect(result).toEqual({
643
+ "@react-pdf/render@4.3.0": {
644
+ path: "patches/@react-pdf__render@4.3.0.patch",
645
+ hash: "",
646
+ },
647
+ });
648
+
649
+ const filterCall = filterPatchedDependencies.mock.calls[0]?.[0];
650
+ expect(filterCall).toBeDefined();
651
+ const reachable = filterCall!.reachableDependencyNames;
652
+ expect(reachable).toBeInstanceOf(Set);
653
+ expect(reachable!.has("@react-pdf/renderer")).toBe(true);
654
+ expect(reachable!.has("@react-pdf/render")).toBe(true);
655
+ });
539
656
  });