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.
- package/dist/index.d.mts +7 -1
- package/dist/index.mjs +1 -1
- package/dist/{isolate-CJy3YyKG.mjs → isolate-BRD2AgVJ.mjs} +422 -125
- package/dist/isolate-BRD2AgVJ.mjs.map +1 -0
- package/dist/isolate-bin.mjs +3 -3
- package/dist/isolate-bin.mjs.map +1 -1
- package/package.json +11 -2
- package/src/isolate-bin.ts +2 -2
- package/src/isolate.ts +30 -0
- package/src/lib/config.ts +31 -6
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package-lock.json +82 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package.json +8 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/api/package.json +12 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/shared/package.json +12 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/utils/package.json +11 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package-lock.json +56 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package.json +8 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/api/package.json +11 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/other/package.json +11 -0
- package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +243 -0
- package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +604 -0
- package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +417 -21
- package/src/lib/lockfile/process-lockfile.test.ts +4 -0
- package/src/lib/lockfile/process-lockfile.ts +14 -16
- package/src/lib/patches/copy-patches.test.ts +78 -0
- package/src/lib/patches/copy-patches.ts +22 -1
- package/src/lib/registry/collect-reachable-package-names.test.ts +239 -0
- package/src/lib/registry/collect-reachable-package-names.ts +60 -0
- package/src/lib/registry/index.ts +1 -0
- package/src/lib/utils/filter-patched-dependencies.test.ts +77 -0
- package/src/lib/utils/filter-patched-dependencies.ts +41 -17
- package/src/lib/utils/is-rush-workspace.ts +6 -0
- 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
|
|
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
|
+
}
|
|
@@ -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
|
|
7
|
-
* present in the
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
`Excluding patch: ${packageSpec}
|
|
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
|
|