isolate-package 1.30.0 → 1.32.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 (33) hide show
  1. package/dist/index.d.mts +7 -1
  2. package/dist/index.mjs +1 -1
  3. package/dist/{isolate-CJy3YyKG.mjs → isolate-BRD2AgVJ.mjs} +422 -125
  4. package/dist/isolate-BRD2AgVJ.mjs.map +1 -0
  5. package/dist/isolate-bin.mjs +3 -3
  6. package/dist/isolate-bin.mjs.map +1 -1
  7. package/package.json +11 -2
  8. package/src/isolate-bin.ts +2 -2
  9. package/src/isolate.ts +30 -0
  10. package/src/lib/config.ts +31 -6
  11. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package-lock.json +82 -0
  12. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package.json +8 -0
  13. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/api/package.json +12 -0
  14. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/shared/package.json +12 -0
  15. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/utils/package.json +11 -0
  16. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package-lock.json +56 -0
  17. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package.json +8 -0
  18. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/api/package.json +11 -0
  19. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/other/package.json +11 -0
  20. package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +243 -0
  21. package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +604 -0
  22. package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +417 -21
  23. package/src/lib/lockfile/process-lockfile.test.ts +4 -0
  24. package/src/lib/lockfile/process-lockfile.ts +14 -16
  25. package/src/lib/patches/copy-patches.test.ts +78 -0
  26. package/src/lib/patches/copy-patches.ts +22 -1
  27. package/src/lib/registry/collect-reachable-package-names.test.ts +239 -0
  28. package/src/lib/registry/collect-reachable-package-names.ts +60 -0
  29. package/src/lib/registry/index.ts +1 -0
  30. package/src/lib/utils/filter-patched-dependencies.test.ts +77 -0
  31. package/src/lib/utils/filter-patched-dependencies.ts +41 -17
  32. package/src/lib/utils/is-rush-workspace.ts +6 -0
  33. package/dist/isolate-CJy3YyKG.mjs.map +0 -1
@@ -67,6 +67,7 @@ describe("copyPatches", () => {
67
67
  workspaceRootDir: "/workspace",
68
68
  targetPackageManifest: { name: "test", version: "1.0.0" },
69
69
  isolateDir: "/workspace/isolate",
70
+ packagesRegistry: {},
70
71
  includeDevDependencies: false,
71
72
  });
72
73
 
@@ -127,6 +128,7 @@ describe("copyPatches", () => {
127
128
  workspaceRootDir: "/workspace",
128
129
  targetPackageManifest: { name: "test", version: "1.0.0" },
129
130
  isolateDir: "/workspace/isolate",
131
+ packagesRegistry: {},
130
132
  includeDevDependencies: false,
131
133
  });
132
134
 
@@ -146,6 +148,7 @@ describe("copyPatches", () => {
146
148
  workspaceRootDir: "/workspace",
147
149
  targetPackageManifest: { name: "test", version: "1.0.0" },
148
150
  isolateDir: "/workspace/isolate",
151
+ packagesRegistry: {},
149
152
  includeDevDependencies: false,
150
153
  });
151
154
 
@@ -175,6 +178,7 @@ describe("copyPatches", () => {
175
178
  workspaceRootDir: "/workspace",
176
179
  targetPackageManifest: targetManifest,
177
180
  isolateDir: "/workspace/isolate",
181
+ packagesRegistry: {},
178
182
  includeDevDependencies: false,
179
183
  });
180
184
 
@@ -212,6 +216,7 @@ describe("copyPatches", () => {
212
216
  workspaceRootDir: "/workspace",
213
217
  targetPackageManifest: targetManifest,
214
218
  isolateDir: "/workspace/isolate",
219
+ packagesRegistry: {},
215
220
  includeDevDependencies: true,
216
221
  });
217
222
 
@@ -222,6 +227,7 @@ describe("copyPatches", () => {
222
227
  patchedDependencies: { "vitest@1.0.0": "patches/vitest.patch" },
223
228
  targetPackageManifest: targetManifest,
224
229
  includeDevDependencies: true,
230
+ reachableDependencyNames: expect.any(Set),
225
231
  });
226
232
  expect(fs.copy).toHaveBeenCalledWith(
227
233
  "/workspace/patches/vitest.patch",
@@ -252,6 +258,7 @@ describe("copyPatches", () => {
252
258
  workspaceRootDir: "/workspace",
253
259
  targetPackageManifest: targetManifest,
254
260
  isolateDir: "/workspace/isolate",
261
+ packagesRegistry: {},
255
262
  includeDevDependencies: false,
256
263
  });
257
264
 
@@ -282,6 +289,7 @@ describe("copyPatches", () => {
282
289
  workspaceRootDir: "/workspace",
283
290
  targetPackageManifest: targetManifest,
284
291
  isolateDir: "/workspace/isolate",
292
+ packagesRegistry: {},
285
293
  includeDevDependencies: false,
286
294
  });
287
295
 
@@ -315,6 +323,7 @@ describe("copyPatches", () => {
315
323
  workspaceRootDir: "/workspace",
316
324
  targetPackageManifest: targetManifest,
317
325
  isolateDir: "/workspace/isolate",
326
+ packagesRegistry: {},
318
327
  includeDevDependencies: false,
319
328
  });
320
329
 
@@ -357,6 +366,7 @@ describe("copyPatches", () => {
357
366
  workspaceRootDir: "/workspace",
358
367
  targetPackageManifest: targetManifest,
359
368
  isolateDir: "/workspace/isolate",
369
+ packagesRegistry: {},
360
370
  includeDevDependencies: false,
361
371
  });
362
372
 
@@ -401,6 +411,7 @@ describe("copyPatches", () => {
401
411
  workspaceRootDir: "/workspace",
402
412
  targetPackageManifest: targetManifest,
403
413
  isolateDir: "/workspace/isolate",
414
+ packagesRegistry: {},
404
415
  includeDevDependencies: false,
405
416
  });
406
417
 
@@ -447,6 +458,7 @@ describe("copyPatches", () => {
447
458
  workspaceRootDir: "/workspace",
448
459
  targetPackageManifest: targetManifest,
449
460
  isolateDir: "/workspace/isolate",
461
+ packagesRegistry: {},
450
462
  includeDevDependencies: false,
451
463
  });
452
464
 
@@ -458,4 +470,70 @@ describe("copyPatches", () => {
458
470
  "/workspace/isolate/patches/lodash.patch",
459
471
  );
460
472
  });
473
+
474
+ it("should pass reachable transitive dep names from internal packages to the filter (regression: issue #167)", async () => {
475
+ /**
476
+ * Target `consumer` depends on internal `firebase-package`, which in turn
477
+ * depends on `tslib`. A patch for `tslib@2.0.0` declared at the workspace
478
+ * root must reach the filter with `tslib` in `reachableDependencyNames`
479
+ * so it can be preserved even though `consumer` doesn't list it directly.
480
+ */
481
+ const consumerManifest: PackageManifest = {
482
+ name: "consumer",
483
+ version: "1.0.0",
484
+ dependencies: { "firebase-package": "file:./packages/firebase-package" },
485
+ };
486
+
487
+ readTypedYamlSync.mockReturnValue({
488
+ patchedDependencies: {
489
+ "tslib@2.0.0": "patches/tslib@2.0.0.patch",
490
+ },
491
+ });
492
+ readTypedJson.mockResolvedValue({
493
+ name: "root",
494
+ version: "1.0.0",
495
+ } as PackageManifest);
496
+
497
+ filterPatchedDependencies.mockReturnValue({
498
+ "tslib@2.0.0": "patches/tslib@2.0.0.patch",
499
+ });
500
+
501
+ fs.existsSync.mockReturnValue(true);
502
+
503
+ usePackageManager.mockReturnValue({
504
+ name: "pnpm",
505
+ majorVersion: 9,
506
+ version: "9.0.0",
507
+ packageManagerString: "pnpm@9.0.0",
508
+ });
509
+
510
+ const result = await copyPatches({
511
+ workspaceRootDir: "/workspace",
512
+ targetPackageManifest: consumerManifest,
513
+ isolateDir: "/workspace/isolate",
514
+ packagesRegistry: {
515
+ "firebase-package": {
516
+ absoluteDir: "/workspace/packages/firebase-package",
517
+ rootRelativeDir: "packages/firebase-package",
518
+ manifest: {
519
+ name: "firebase-package",
520
+ version: "1.0.0",
521
+ dependencies: { tslib: "^2.0.0" },
522
+ },
523
+ },
524
+ },
525
+ includeDevDependencies: false,
526
+ });
527
+
528
+ expect(result).toEqual({
529
+ "tslib@2.0.0": { path: "patches/tslib@2.0.0.patch", hash: "" },
530
+ });
531
+
532
+ const filterCall = filterPatchedDependencies.mock.calls[0]?.[0];
533
+ expect(filterCall).toBeDefined();
534
+ const reachable = filterCall!.reachableDependencyNames;
535
+ expect(reachable).toBeInstanceOf(Set);
536
+ expect(reachable!.has("firebase-package")).toBe(true);
537
+ expect(reachable!.has("tslib")).toBe(true);
538
+ });
461
539
  });
@@ -4,7 +4,13 @@ import { readWantedLockfile as readWantedLockfile_v8 } from "pnpm_lockfile_file_
4
4
  import { readWantedLockfile as readWantedLockfile_v9 } from "pnpm_lockfile_file_v9";
5
5
  import { useLogger } from "~/lib/logger";
6
6
  import { usePackageManager } from "~/lib/package-manager";
7
- import type { PackageManifest, PatchFile, PnpmSettings } from "~/lib/types";
7
+ import { collectReachablePackageNames } from "~/lib/registry";
8
+ import type {
9
+ PackageManifest,
10
+ PackagesRegistry,
11
+ PatchFile,
12
+ PnpmSettings,
13
+ } from "~/lib/types";
8
14
  import {
9
15
  filterPatchedDependencies,
10
16
  getRootRelativeLogPath,
@@ -16,11 +22,13 @@ import {
16
22
  export async function copyPatches({
17
23
  workspaceRootDir,
18
24
  targetPackageManifest,
25
+ packagesRegistry,
19
26
  isolateDir,
20
27
  includeDevDependencies,
21
28
  }: {
22
29
  workspaceRootDir: string;
23
30
  targetPackageManifest: PackageManifest;
31
+ packagesRegistry: PackagesRegistry;
24
32
  isolateDir: string;
25
33
  includeDevDependencies: boolean;
26
34
  }): Promise<Record<string, PatchFile>> {
@@ -82,10 +90,23 @@ export async function copyPatches({
82
90
  `Found ${Object.keys(patchedDependencies).length} patched dependencies in workspace`,
83
91
  );
84
92
 
93
+ /**
94
+ * Collect the set of dependency names reachable from the target (direct deps
95
+ * plus deps introduced by internal workspace packages). Patches for names in
96
+ * this set are preserved even when the target doesn't list them directly —
97
+ * see issue #167.
98
+ */
99
+ const reachableDependencyNames = collectReachablePackageNames({
100
+ targetPackageManifest,
101
+ packagesRegistry,
102
+ includeDevDependencies,
103
+ });
104
+
85
105
  const filteredPatches = filterPatchedDependencies({
86
106
  patchedDependencies,
87
107
  targetPackageManifest,
88
108
  includeDevDependencies,
109
+ reachableDependencyNames,
89
110
  });
90
111
 
91
112
  if (!filteredPatches) {
@@ -0,0 +1,239 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { PackageManifest, PackagesRegistry } from "~/lib/types";
3
+ import { collectReachablePackageNames } from "./collect-reachable-package-names";
4
+
5
+ function entry(manifest: PackageManifest) {
6
+ return {
7
+ absoluteDir: `/workspace/packages/${manifest.name}`,
8
+ rootRelativeDir: `packages/${manifest.name}`,
9
+ manifest,
10
+ };
11
+ }
12
+
13
+ describe("collectReachablePackageNames", () => {
14
+ it("returns target direct deps", () => {
15
+ const manifest: PackageManifest = {
16
+ name: "app",
17
+ version: "1.0.0",
18
+ dependencies: { lodash: "^4.0.0", tslib: "^2.0.0" },
19
+ };
20
+
21
+ const result = collectReachablePackageNames({
22
+ targetPackageManifest: manifest,
23
+ packagesRegistry: {},
24
+ includeDevDependencies: false,
25
+ });
26
+
27
+ expect([...result].sort()).toEqual(["lodash", "tslib"]);
28
+ });
29
+
30
+ it("excludes target devDependencies when includeDevDependencies is false", () => {
31
+ const manifest: PackageManifest = {
32
+ name: "app",
33
+ version: "1.0.0",
34
+ dependencies: { lodash: "^4.0.0" },
35
+ devDependencies: { vitest: "^1.0.0" },
36
+ };
37
+
38
+ const result = collectReachablePackageNames({
39
+ targetPackageManifest: manifest,
40
+ packagesRegistry: {},
41
+ includeDevDependencies: false,
42
+ });
43
+
44
+ expect(result.has("lodash")).toBe(true);
45
+ expect(result.has("vitest")).toBe(false);
46
+ });
47
+
48
+ it("includes target devDependencies when includeDevDependencies is true", () => {
49
+ const manifest: PackageManifest = {
50
+ name: "app",
51
+ version: "1.0.0",
52
+ dependencies: { lodash: "^4.0.0" },
53
+ devDependencies: { vitest: "^1.0.0" },
54
+ };
55
+
56
+ const result = collectReachablePackageNames({
57
+ targetPackageManifest: manifest,
58
+ packagesRegistry: {},
59
+ includeDevDependencies: true,
60
+ });
61
+
62
+ expect([...result].sort()).toEqual(["lodash", "vitest"]);
63
+ });
64
+
65
+ it("recurses through internal workspace packages to pick up their deps", () => {
66
+ /** Mirrors issue #167: consumer → firebase-package (internal) → tslib */
67
+ const consumerManifest: PackageManifest = {
68
+ name: "consumer",
69
+ version: "1.0.0",
70
+ dependencies: { "firebase-package": "workspace:*" },
71
+ };
72
+ const firebaseManifest: PackageManifest = {
73
+ name: "firebase-package",
74
+ version: "1.0.0",
75
+ dependencies: { tslib: "^2.0.0" },
76
+ };
77
+
78
+ const registry: PackagesRegistry = {
79
+ "firebase-package": entry(firebaseManifest),
80
+ };
81
+
82
+ const result = collectReachablePackageNames({
83
+ targetPackageManifest: consumerManifest,
84
+ packagesRegistry: registry,
85
+ includeDevDependencies: false,
86
+ });
87
+
88
+ expect(result.has("firebase-package")).toBe(true);
89
+ expect(result.has("tslib")).toBe(true);
90
+ });
91
+
92
+ it("does not include devDependencies of internal packages", () => {
93
+ const consumerManifest: PackageManifest = {
94
+ name: "consumer",
95
+ version: "1.0.0",
96
+ dependencies: { "firebase-package": "workspace:*" },
97
+ };
98
+ const firebaseManifest: PackageManifest = {
99
+ name: "firebase-package",
100
+ version: "1.0.0",
101
+ dependencies: { tslib: "^2.0.0" },
102
+ devDependencies: { vitest: "^1.0.0" },
103
+ };
104
+
105
+ const registry: PackagesRegistry = {
106
+ "firebase-package": entry(firebaseManifest),
107
+ };
108
+
109
+ const result = collectReachablePackageNames({
110
+ targetPackageManifest: consumerManifest,
111
+ packagesRegistry: registry,
112
+ /**
113
+ * Even with includeDevDependencies true for the target, internal
114
+ * packages' devDependencies stay out — they aren't installed in the
115
+ * isolate.
116
+ */
117
+ includeDevDependencies: true,
118
+ });
119
+
120
+ expect(result.has("tslib")).toBe(true);
121
+ expect(result.has("vitest")).toBe(false);
122
+ });
123
+
124
+ it("handles multi-level internal chains", () => {
125
+ const appManifest: PackageManifest = {
126
+ name: "app",
127
+ version: "1.0.0",
128
+ dependencies: { "pkg-a": "workspace:*" },
129
+ };
130
+ const pkgAManifest: PackageManifest = {
131
+ name: "pkg-a",
132
+ version: "1.0.0",
133
+ dependencies: { "pkg-b": "workspace:*" },
134
+ };
135
+ const pkgBManifest: PackageManifest = {
136
+ name: "pkg-b",
137
+ version: "1.0.0",
138
+ dependencies: { "@scope/leaf": "^1.0.0" },
139
+ };
140
+
141
+ const registry: PackagesRegistry = {
142
+ "pkg-a": entry(pkgAManifest),
143
+ "pkg-b": entry(pkgBManifest),
144
+ };
145
+
146
+ const result = collectReachablePackageNames({
147
+ targetPackageManifest: appManifest,
148
+ packagesRegistry: registry,
149
+ includeDevDependencies: false,
150
+ });
151
+
152
+ expect([...result].sort()).toEqual(["@scope/leaf", "pkg-a", "pkg-b"]);
153
+ });
154
+
155
+ it("walks optionalDependencies of target and internal packages", () => {
156
+ const appManifest: PackageManifest = {
157
+ name: "app",
158
+ version: "1.0.0",
159
+ dependencies: { "pkg-a": "workspace:*" },
160
+ optionalDependencies: { "optional-on-target": "^1.0.0" },
161
+ };
162
+ const pkgAManifest: PackageManifest = {
163
+ name: "pkg-a",
164
+ version: "1.0.0",
165
+ optionalDependencies: { "optional-on-internal": "^1.0.0" },
166
+ };
167
+
168
+ const registry: PackagesRegistry = {
169
+ "pkg-a": entry(pkgAManifest),
170
+ };
171
+
172
+ const result = collectReachablePackageNames({
173
+ targetPackageManifest: appManifest,
174
+ packagesRegistry: registry,
175
+ includeDevDependencies: false,
176
+ });
177
+
178
+ expect(result.has("optional-on-target")).toBe(true);
179
+ expect(result.has("optional-on-internal")).toBe(true);
180
+ });
181
+
182
+ it("walks peerDependencies of target and internal packages", () => {
183
+ /**
184
+ * With pnpm's default autoInstallPeers, peer deps typically end up
185
+ * installed in the isolate and may carry patches.
186
+ */
187
+ const appManifest: PackageManifest = {
188
+ name: "app",
189
+ version: "1.0.0",
190
+ dependencies: { "pkg-a": "workspace:*" },
191
+ peerDependencies: { "peer-on-target": "^1.0.0" },
192
+ };
193
+ const pkgAManifest: PackageManifest = {
194
+ name: "pkg-a",
195
+ version: "1.0.0",
196
+ peerDependencies: { "peer-on-internal": "^1.0.0" },
197
+ };
198
+
199
+ const registry: PackagesRegistry = {
200
+ "pkg-a": entry(pkgAManifest),
201
+ };
202
+
203
+ const result = collectReachablePackageNames({
204
+ targetPackageManifest: appManifest,
205
+ packagesRegistry: registry,
206
+ includeDevDependencies: false,
207
+ });
208
+
209
+ expect(result.has("peer-on-target")).toBe(true);
210
+ expect(result.has("peer-on-internal")).toBe(true);
211
+ });
212
+
213
+ it("tolerates cycles between internal packages", () => {
214
+ /** Each package already visited is skipped on re-entry */
215
+ const aManifest: PackageManifest = {
216
+ name: "pkg-a",
217
+ version: "1.0.0",
218
+ dependencies: { "pkg-b": "workspace:*", tslib: "^2.0.0" },
219
+ };
220
+ const bManifest: PackageManifest = {
221
+ name: "pkg-b",
222
+ version: "1.0.0",
223
+ dependencies: { "pkg-a": "workspace:*" },
224
+ };
225
+
226
+ const registry: PackagesRegistry = {
227
+ "pkg-a": entry(aManifest),
228
+ "pkg-b": entry(bManifest),
229
+ };
230
+
231
+ const result = collectReachablePackageNames({
232
+ targetPackageManifest: aManifest,
233
+ packagesRegistry: registry,
234
+ includeDevDependencies: false,
235
+ });
236
+
237
+ expect([...result].sort()).toEqual(["pkg-a", "pkg-b", "tslib"]);
238
+ });
239
+ });
@@ -0,0 +1,60 @@
1
+ import type { PackageManifest, PackagesRegistry } from "../types";
2
+
3
+ /**
4
+ * Walk the target manifest and the manifests of any internal (workspace)
5
+ * packages reachable from it, collecting every dependency name encountered
6
+ * (both internal and external).
7
+ *
8
+ * The resulting set is a superset of the target's direct dependencies: it also
9
+ * includes dependencies of internal workspace packages that will end up in the
10
+ * isolated output. This is used to filter workspace-level
11
+ * `patchedDependencies` so that patches for deps introduced via internal
12
+ * packages aren't dropped.
13
+ *
14
+ * `dependencies`, `optionalDependencies`, and `peerDependencies` are all
15
+ * walked — any of them can lead to a package being installed in the isolate
16
+ * (pnpm installs peers by default via `autoInstallPeers`). devDependencies of
17
+ * internal packages are never followed, and devDependencies of the *target*
18
+ * are followed only when `includeDevDependencies` is true.
19
+ *
20
+ * Note: only recurses through internal packages — manifests of external deps
21
+ * aren't available here. Deep external→external transitives therefore won't
22
+ * appear in the set.
23
+ */
24
+ export function collectReachablePackageNames({
25
+ targetPackageManifest,
26
+ packagesRegistry,
27
+ includeDevDependencies,
28
+ }: {
29
+ targetPackageManifest: PackageManifest;
30
+ packagesRegistry: PackagesRegistry;
31
+ includeDevDependencies: boolean;
32
+ }): Set<string> {
33
+ const names = new Set<string>();
34
+ const visitedInternal = new Set<string>();
35
+
36
+ walk(targetPackageManifest, true);
37
+
38
+ return names;
39
+
40
+ function walk(manifest: PackageManifest, isTarget: boolean) {
41
+ const depNames = [
42
+ ...Object.keys(manifest.dependencies ?? {}),
43
+ ...Object.keys(manifest.optionalDependencies ?? {}),
44
+ ...Object.keys(manifest.peerDependencies ?? {}),
45
+ ...(isTarget && includeDevDependencies
46
+ ? Object.keys(manifest.devDependencies ?? {})
47
+ : []),
48
+ ];
49
+
50
+ for (const name of depNames) {
51
+ names.add(name);
52
+
53
+ const internalPkg = packagesRegistry[name];
54
+ if (internalPkg && !visitedInternal.has(name)) {
55
+ visitedInternal.add(name);
56
+ walk(internalPkg.manifest, false);
57
+ }
58
+ }
59
+ }
60
+ }
@@ -1,2 +1,3 @@
1
+ export * from "./collect-reachable-package-names";
1
2
  export * from "./create-packages-registry";
2
3
  export * from "./list-internal-packages";
@@ -182,6 +182,83 @@ describe("filterPatchedDependencies", () => {
182
182
  expect(result).toBeUndefined();
183
183
  });
184
184
 
185
+ it("should include patches for packages reachable via internal workspace packages", () => {
186
+ /** Issue #167: patch targets a transitive dep via an internal package */
187
+ const manifest: PackageManifest = {
188
+ name: "consumer",
189
+ version: "1.0.0",
190
+ dependencies: { "firebase-package": "file:./packages/firebase-package" },
191
+ };
192
+
193
+ const result = filterPatchedDependencies({
194
+ patchedDependencies: { "tslib@2.0.0": "patches/tslib.patch" },
195
+ targetPackageManifest: manifest,
196
+ includeDevDependencies: false,
197
+ reachableDependencyNames: new Set(["firebase-package", "tslib"]),
198
+ });
199
+
200
+ expect(result).toEqual({ "tslib@2.0.0": "patches/tslib.patch" });
201
+ });
202
+
203
+ it("should exclude patches for packages not in direct deps nor the reachable set", () => {
204
+ const manifest: PackageManifest = {
205
+ name: "consumer",
206
+ version: "1.0.0",
207
+ dependencies: { "firebase-package": "file:./packages/firebase-package" },
208
+ };
209
+
210
+ const result = filterPatchedDependencies({
211
+ patchedDependencies: { "unrelated@1.0.0": "patches/unrelated.patch" },
212
+ targetPackageManifest: manifest,
213
+ includeDevDependencies: false,
214
+ reachableDependencyNames: new Set(["firebase-package", "tslib"]),
215
+ });
216
+
217
+ expect(result).toBeUndefined();
218
+ });
219
+
220
+ it("should include a patch when a target devDep is also reachable as a prod transitive", () => {
221
+ /**
222
+ * The target lists `tslib` as a devDep and runs with
223
+ * includeDevDependencies=false, but `tslib` is also a prod dep of an
224
+ * internal workspace package that IS installed in the isolate. The
225
+ * patch must be preserved because tslib will be present at install
226
+ * time through the internal package.
227
+ */
228
+ const manifest: PackageManifest = {
229
+ name: "app",
230
+ version: "1.0.0",
231
+ dependencies: { "firebase-package": "file:./packages/firebase-package" },
232
+ devDependencies: { tslib: "^2.0.0" },
233
+ };
234
+
235
+ const result = filterPatchedDependencies({
236
+ patchedDependencies: { "tslib@2.0.0": "patches/tslib.patch" },
237
+ targetPackageManifest: manifest,
238
+ includeDevDependencies: false,
239
+ reachableDependencyNames: new Set(["firebase-package", "tslib"]),
240
+ });
241
+
242
+ expect(result).toEqual({ "tslib@2.0.0": "patches/tslib.patch" });
243
+ });
244
+
245
+ it("should still exclude a pure target devDep patch when not reachable and dev deps are off", () => {
246
+ const manifest: PackageManifest = {
247
+ name: "app",
248
+ version: "1.0.0",
249
+ devDependencies: { vitest: "^1.0.0" },
250
+ };
251
+
252
+ const result = filterPatchedDependencies({
253
+ patchedDependencies: { "vitest@1.0.0": "patches/vitest.patch" },
254
+ targetPackageManifest: manifest,
255
+ includeDevDependencies: false,
256
+ reachableDependencyNames: new Set(),
257
+ });
258
+
259
+ expect(result).toBeUndefined();
260
+ });
261
+
185
262
  it("should preserve patch value types", () => {
186
263
  const manifest: PackageManifest = {
187
264
  name: "test",
@@ -3,17 +3,26 @@ import type { PackageManifest } from "~/lib/types";
3
3
  import { getPackageName } from "./get-package-name";
4
4
 
5
5
  /**
6
- * Filters patched dependencies to only include patches for packages that are
7
- * present in the target package's dependencies based on dependency type.
6
+ * Filters patched dependencies to only include patches for packages that will
7
+ * be present in the isolated output, either as a direct dependency of the
8
+ * target or as a transitive dependency reachable through internal workspace
9
+ * packages.
8
10
  */
9
11
  export function filterPatchedDependencies<T>({
10
12
  patchedDependencies,
11
13
  targetPackageManifest,
12
14
  includeDevDependencies,
15
+ reachableDependencyNames,
13
16
  }: {
14
17
  patchedDependencies: Record<string, T> | undefined;
15
18
  targetPackageManifest: PackageManifest;
16
19
  includeDevDependencies: boolean;
20
+ /**
21
+ * Additional set of dependency names reachable from the target (e.g. via
22
+ * internal workspace packages). Used to preserve patches for transitive
23
+ * deps that are not listed directly on the target manifest.
24
+ */
25
+ reachableDependencyNames?: Set<string>;
17
26
  }): Record<string, T> | undefined {
18
27
  const log = useLogger();
19
28
  if (!patchedDependencies || typeof patchedDependencies !== "object") {
@@ -27,7 +36,7 @@ export function filterPatchedDependencies<T>({
27
36
  for (const [packageSpec, patchInfo] of Object.entries(patchedDependencies)) {
28
37
  const packageName = getPackageName(packageSpec);
29
38
 
30
- /** Check if it's a production dependency */
39
+ /** Direct production dependency */
31
40
  if (targetPackageManifest.dependencies?.[packageName]) {
32
41
  filteredPatches[packageSpec] = patchInfo;
33
42
  includedCount++;
@@ -35,23 +44,38 @@ export function filterPatchedDependencies<T>({
35
44
  continue;
36
45
  }
37
46
 
38
- /** Check if it's a dev dependency and we should include dev dependencies */
39
- if (targetPackageManifest.devDependencies?.[packageName]) {
40
- if (includeDevDependencies) {
41
- filteredPatches[packageSpec] = patchInfo;
42
- includedCount++;
43
- log.debug(`Including dev dependency patch: ${packageSpec}`);
44
- } else {
45
- excludedCount++;
46
- log.debug(`Excluding dev dependency patch: ${packageSpec}`);
47
- }
47
+ /** Direct dev dependency (respects the dev-deps flag) */
48
+ if (
49
+ includeDevDependencies &&
50
+ targetPackageManifest.devDependencies?.[packageName]
51
+ ) {
52
+ filteredPatches[packageSpec] = patchInfo;
53
+ includedCount++;
54
+ log.debug(`Including dev dependency patch: ${packageSpec}`);
55
+ continue;
56
+ }
57
+
58
+ /**
59
+ * Reachable via an internal workspace package. This fires even when the
60
+ * package is also listed in the target's devDependencies with
61
+ * `includeDevDependencies=false`, because the package is still installed
62
+ * in the isolate as a prod transitive.
63
+ */
64
+ if (reachableDependencyNames?.has(packageName)) {
65
+ filteredPatches[packageSpec] = patchInfo;
66
+ includedCount++;
67
+ log.debug(`Including transitive dependency patch: ${packageSpec}`);
48
68
  continue;
49
69
  }
50
70
 
51
- /** Package not found in dependencies or devDependencies */
52
- log.debug(
53
- `Excluding patch: ${packageSpec} (package "${packageName}" not in target dependencies)`,
54
- );
71
+ /** Package won't be installed in the isolate */
72
+ if (targetPackageManifest.devDependencies?.[packageName]) {
73
+ log.debug(`Excluding dev dependency patch: ${packageSpec}`);
74
+ } else {
75
+ log.debug(
76
+ `Excluding patch: ${packageSpec} (package "${packageName}" not reachable from target)`,
77
+ );
78
+ }
55
79
  excludedCount++;
56
80
  }
57
81