isolate-package 1.33.0 → 1.34.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 (67) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/index.mjs.map +1 -1
  3. package/dist/{isolate-DyRD5Zd_.mjs → isolate-DI3eUTci.mjs} +862 -263
  4. package/dist/isolate-DI3eUTci.mjs.map +1 -0
  5. package/dist/isolate-bin.mjs +5 -6
  6. package/dist/isolate-bin.mjs.map +1 -1
  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 +2 -2
  21. package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +6 -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/helpers/adapt-internal-package-manifests.test.ts +3 -3
  25. package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +2 -2
  26. package/src/lib/manifest/helpers/adapt-manifest-internal-deps.ts +1 -1
  27. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +4 -4
  28. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +7 -7
  29. package/src/lib/manifest/helpers/resolve-catalog-dependencies.test.ts +410 -0
  30. package/src/lib/manifest/helpers/resolve-catalog-dependencies.ts +115 -27
  31. package/src/lib/manifest/io.ts +6 -2
  32. package/src/lib/manifest/validate-manifest.ts +2 -2
  33. package/src/lib/output/get-build-output-dir.ts +1 -1
  34. package/src/lib/output/pack-dependencies.ts +1 -1
  35. package/src/lib/output/process-build-output-files.ts +6 -17
  36. package/src/lib/package-manager/helpers/infer-from-files.ts +5 -5
  37. package/src/lib/package-manager/helpers/infer-from-manifest.ts +7 -8
  38. package/src/lib/package-manager/index.ts +1 -1
  39. package/src/lib/package-manager/names.ts +8 -10
  40. package/src/lib/patches/collect-installed-names-bun.test.ts +154 -0
  41. package/src/lib/patches/collect-installed-names-bun.ts +87 -0
  42. package/src/lib/patches/collect-installed-names-pnpm.test.ts +316 -0
  43. package/src/lib/patches/collect-installed-names-pnpm.ts +365 -0
  44. package/src/lib/patches/copy-patches.test.ts +130 -13
  45. package/src/lib/patches/copy-patches.ts +47 -10
  46. package/src/lib/patches/write-isolate-pnpm-workspace.test.ts +83 -3
  47. package/src/lib/patches/write-isolate-pnpm-workspace.ts +4 -4
  48. package/src/lib/registry/collect-reachable-package-names.test.ts +1 -1
  49. package/src/lib/registry/create-packages-registry.ts +34 -31
  50. package/src/lib/registry/helpers/find-packages-globs.ts +23 -19
  51. package/src/lib/registry/list-internal-packages.test.ts +2 -2
  52. package/src/lib/types.ts +2 -2
  53. package/src/lib/utils/filter-patched-dependencies.test.ts +1 -1
  54. package/src/lib/utils/filter-patched-dependencies.ts +2 -2
  55. package/src/lib/utils/get-dirname.ts +1 -1
  56. package/src/lib/utils/index.ts +1 -1
  57. package/src/lib/utils/json.ts +12 -14
  58. package/src/lib/utils/pack.ts +32 -22
  59. package/src/lib/utils/reset-isolate-dir.test.ts +165 -0
  60. package/src/lib/utils/reset-isolate-dir.ts +147 -0
  61. package/src/lib/utils/unpack.test.ts +76 -0
  62. package/src/lib/utils/unpack.ts +16 -10
  63. package/src/lib/utils/wait-for-complete-file.test.ts +105 -0
  64. package/src/lib/utils/wait-for-complete-file.ts +44 -0
  65. package/src/lib/utils/yaml.ts +8 -9
  66. package/src/testing/setup.ts +1 -1
  67. package/dist/isolate-DyRD5Zd_.mjs.map +0 -1
@@ -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
  });
@@ -2,39 +2,45 @@ import fs from "fs-extra";
2
2
  import path from "node:path";
3
3
  import { readWantedLockfile as readWantedLockfile_v8 } from "pnpm_lockfile_file_v8";
4
4
  import { readWantedLockfile as readWantedLockfile_v9 } from "pnpm_lockfile_file_v9";
5
- import { useLogger } from "~/lib/logger";
6
- import { usePackageManager } from "~/lib/package-manager";
7
- import { collectReachablePackageNames } from "~/lib/registry";
5
+ import { useLogger } from "#/lib/logger";
6
+ import { usePackageManager } from "#/lib/package-manager";
7
+ import { collectReachablePackageNames } from "#/lib/registry";
8
8
  import type {
9
9
  PackageManifest,
10
10
  PackagesRegistry,
11
11
  PatchFile,
12
12
  PnpmSettings,
13
- } from "~/lib/types";
13
+ } from "#/lib/types";
14
14
  import {
15
15
  filterPatchedDependencies,
16
16
  getRootRelativeLogPath,
17
17
  isRushWorkspace,
18
18
  readTypedJson,
19
19
  readTypedYamlSync,
20
- } from "~/lib/utils";
20
+ } from "#/lib/utils";
21
+ import { collectInstalledNamesFromBunLockfile } from "./collect-installed-names-bun";
22
+ import { collectInstalledNamesFromPnpmLockfile } from "./collect-installed-names-pnpm";
21
23
 
22
24
  export async function copyPatches({
23
25
  workspaceRootDir,
26
+ targetPackageDir,
24
27
  targetPackageManifest,
25
28
  packagesRegistry,
29
+ internalDepPackageNames,
26
30
  isolateDir,
27
31
  includeDevDependencies,
28
32
  }: {
29
33
  workspaceRootDir: string;
34
+ targetPackageDir: string;
30
35
  targetPackageManifest: PackageManifest;
31
36
  packagesRegistry: PackagesRegistry;
37
+ internalDepPackageNames: string[];
32
38
  isolateDir: string;
33
39
  includeDevDependencies: boolean;
34
40
  }): Promise<Record<string, PatchFile>> {
35
41
  const log = useLogger();
36
42
 
37
- const { name: packageManagerName } = usePackageManager();
43
+ const { name: packageManagerName, majorVersion } = usePackageManager();
38
44
 
39
45
  let patchedDependencies: Record<string, string> | undefined;
40
46
 
@@ -44,9 +50,9 @@ export async function copyPatches({
44
50
  */
45
51
  if (packageManagerName === "pnpm") {
46
52
  try {
47
- const pnpmSettings = readTypedYamlSync<PnpmSettings>(
53
+ const pnpmSettings = readTypedYamlSync(
48
54
  path.join(workspaceRootDir, "pnpm-workspace.yaml"),
49
- );
55
+ ) as PnpmSettings | undefined;
50
56
  patchedDependencies = pnpmSettings?.patchedDependencies;
51
57
  } catch (error) {
52
58
  log.warn(
@@ -67,9 +73,9 @@ export async function copyPatches({
67
73
  }
68
74
 
69
75
  try {
70
- const workspaceRootManifest = await readTypedJson<PackageManifest>(
76
+ const workspaceRootManifest = (await readTypedJson(
71
77
  path.join(workspaceRootDir, "package.json"),
72
- );
78
+ )) as PackageManifest;
73
79
  /** PNPM stores patches under pnpm.patchedDependencies, Bun at the top level */
74
80
  patchedDependencies =
75
81
  workspaceRootManifest?.pnpm?.patchedDependencies ??
@@ -102,6 +108,37 @@ export async function copyPatches({
102
108
  includeDevDependencies,
103
109
  });
104
110
 
111
+ /**
112
+ * Manifest-based reachability misses external→external transitives because
113
+ * external manifests aren't loaded here. Walk the package-manager's
114
+ * lockfile to also pick up those names, so a patch for a deeply-nested
115
+ * external dep (e.g. `@react-pdf/render` reached via `@react-pdf/renderer`)
116
+ * survives isolation.
117
+ */
118
+ const lockfileInstalledNames =
119
+ packageManagerName === "pnpm"
120
+ ? await collectInstalledNamesFromPnpmLockfile({
121
+ workspaceRootDir,
122
+ targetPackageDir,
123
+ internalDepPackageNames,
124
+ packagesRegistry,
125
+ majorVersion,
126
+ includeDevDependencies,
127
+ })
128
+ : packageManagerName === "bun"
129
+ ? collectInstalledNamesFromBunLockfile({
130
+ workspaceRootDir,
131
+ targetPackageDir,
132
+ internalDepPackageNames,
133
+ packagesRegistry,
134
+ includeDevDependencies,
135
+ })
136
+ : new Set<string>();
137
+
138
+ for (const name of lockfileInstalledNames) {
139
+ reachableDependencyNames.add(name);
140
+ }
141
+
105
142
  const filteredPatches = filterPatchedDependencies({
106
143
  patchedDependencies,
107
144
  targetPackageManifest,
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import type { PatchFile } from "~/lib/types";
2
+ import type { PatchFile } from "#/lib/types";
3
3
  import { writeIsolatePnpmWorkspace } from "./write-isolate-pnpm-workspace";
4
4
 
5
5
  vi.mock("fs-extra", () => ({
@@ -8,14 +8,14 @@ vi.mock("fs-extra", () => ({
8
8
  },
9
9
  }));
10
10
 
11
- vi.mock("~/lib/utils", () => ({
11
+ vi.mock("#/lib/utils", () => ({
12
12
  readTypedYamlSync: vi.fn(),
13
13
  writeTypedYamlSync: vi.fn(),
14
14
  }));
15
15
 
16
16
  const fs = vi.mocked((await import("fs-extra")).default);
17
17
  const { readTypedYamlSync, writeTypedYamlSync } = vi.mocked(
18
- await import("~/lib/utils"),
18
+ await import("#/lib/utils"),
19
19
  );
20
20
 
21
21
  const workspaceRootDir = "/workspace";
@@ -139,6 +139,86 @@ describe("writeIsolatePnpmWorkspace", () => {
139
139
  );
140
140
  });
141
141
 
142
+ /**
143
+ * Regression test for issue #189: pnpm 11 expresses the build-script policy
144
+ * via `allowBuilds` in pnpm-workspace.yaml (and removes the older
145
+ * `pnpm.onlyBuiltDependencies` / `ignoredBuiltDependencies` fields from
146
+ * package.json). The verbatim copy must carry that field — along with other
147
+ * workspace-level settings like `minimumReleaseAge` — into the isolate
148
+ * output so downstream `pnpm install` honors the same policy.
149
+ */
150
+ it("preserves pnpm 11 workspace settings (allowBuilds, minimumReleaseAge) when no patches are involved", () => {
151
+ readTypedYamlSync.mockReturnValue({
152
+ packages: ["apps/*", "packages/*"],
153
+ allowBuilds: {
154
+ puppeteer: true,
155
+ esbuild: true,
156
+ },
157
+ minimumReleaseAge: 10_080,
158
+ });
159
+
160
+ writeIsolatePnpmWorkspace({
161
+ workspaceRootDir,
162
+ isolateDir,
163
+ copiedPatches: {},
164
+ });
165
+
166
+ /**
167
+ * With no patchedDependencies in the source yaml, the file is copied
168
+ * verbatim — preserving `allowBuilds` and any other top-level settings.
169
+ */
170
+ expect(writeTypedYamlSync).not.toHaveBeenCalled();
171
+ expect(fs.copyFileSync).toHaveBeenCalledWith(
172
+ "/workspace/pnpm-workspace.yaml",
173
+ "/workspace/isolate/pnpm-workspace.yaml",
174
+ );
175
+ });
176
+
177
+ /**
178
+ * When patches are being filtered, the rewrite path must still carry
179
+ * `allowBuilds` into the output yaml — otherwise pnpm 11's build-script
180
+ * policy is silently dropped.
181
+ */
182
+ it("preserves allowBuilds when rewriting to filter patchedDependencies", () => {
183
+ readTypedYamlSync.mockReturnValue({
184
+ packages: ["apps/*", "packages/*"],
185
+ allowBuilds: {
186
+ puppeteer: true,
187
+ },
188
+ patchedDependencies: {
189
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
190
+ "axios@1.6.0": "patches/axios@1.6.0.patch",
191
+ },
192
+ });
193
+
194
+ const copiedPatches: Record<string, PatchFile> = {
195
+ "lodash@4.17.21": {
196
+ path: "patches/lodash@4.17.21.patch",
197
+ hash: "abc",
198
+ },
199
+ };
200
+
201
+ writeIsolatePnpmWorkspace({
202
+ workspaceRootDir,
203
+ isolateDir,
204
+ copiedPatches,
205
+ });
206
+
207
+ expect(fs.copyFileSync).not.toHaveBeenCalled();
208
+ expect(writeTypedYamlSync).toHaveBeenCalledWith(
209
+ "/workspace/isolate/pnpm-workspace.yaml",
210
+ {
211
+ packages: ["apps/*", "packages/*"],
212
+ allowBuilds: {
213
+ puppeteer: true,
214
+ },
215
+ patchedDependencies: {
216
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
217
+ },
218
+ },
219
+ );
220
+ });
221
+
142
222
  it("copies verbatim when every patch is kept (preserving comments and order)", () => {
143
223
  readTypedYamlSync.mockReturnValue({
144
224
  packages: ["packages/*"],
@@ -1,8 +1,8 @@
1
1
  import fs from "fs-extra";
2
2
  import path from "node:path";
3
- import { useLogger } from "~/lib/logger";
4
- import type { PatchFile, PnpmSettings } from "~/lib/types";
5
- import { readTypedYamlSync, writeTypedYamlSync } from "~/lib/utils";
3
+ import { useLogger } from "#/lib/logger";
4
+ import type { PatchFile, PnpmSettings } from "#/lib/types";
5
+ import { readTypedYamlSync, writeTypedYamlSync } from "#/lib/utils";
6
6
 
7
7
  /**
8
8
  * Copy `pnpm-workspace.yaml` from the workspace root to the isolate directory,
@@ -39,7 +39,7 @@ export function writeIsolatePnpmWorkspace({
39
39
  let settings: PnpmSettings | undefined;
40
40
 
41
41
  try {
42
- settings = readTypedYamlSync<PnpmSettings>(sourcePath);
42
+ settings = readTypedYamlSync(sourcePath) as PnpmSettings | undefined;
43
43
  } catch (error) {
44
44
  log.warn(
45
45
  `Could not read pnpm-workspace.yaml, falling back to verbatim copy: ${error instanceof Error ? error.message : String(error)}`,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { PackageManifest, PackagesRegistry } from "~/lib/types";
2
+ import type { PackageManifest, PackagesRegistry } from "#/lib/types";
3
3
  import { collectReachablePackageNames } from "./collect-reachable-package-names";
4
4
 
5
5
  function entry(manifest: PackageManifest) {
@@ -28,38 +28,41 @@ export async function createPackagesRegistry(
28
28
  workspaceRootDir,
29
29
  );
30
30
 
31
- const registry: PackagesRegistry = (
32
- await Promise.all(
33
- allPackages.map(async (rootRelativeDir) => {
34
- const absoluteDir = path.join(workspaceRootDir, rootRelativeDir);
35
- const manifestPath = path.join(absoluteDir, "package.json");
31
+ const entries = await Promise.all(
32
+ allPackages.map(async (rootRelativeDir) => {
33
+ const absoluteDir = path.join(workspaceRootDir, rootRelativeDir);
34
+ const manifestPath = path.join(absoluteDir, "package.json");
36
35
 
37
- if (!fs.existsSync(manifestPath)) {
38
- log.warn(
39
- `Ignoring directory ${rootRelativeDir} because it does not contain a package.json file`,
40
- );
41
- return;
42
- } else {
43
- log.debug(`Registering package ${rootRelativeDir}`);
36
+ if (!fs.existsSync(manifestPath)) {
37
+ log.warn(
38
+ `Ignoring directory ${rootRelativeDir} because it does not contain a package.json file`,
39
+ );
40
+ return null;
41
+ }
44
42
 
45
- const manifest = await readTypedJson<PackageManifest>(
46
- path.join(absoluteDir, "package.json"),
47
- );
43
+ log.debug(`Registering package ${rootRelativeDir}`);
48
44
 
49
- return {
50
- manifest,
51
- rootRelativeDir,
52
- absoluteDir,
53
- };
54
- }
55
- }),
56
- )
57
- ).reduce<PackagesRegistry>((acc, info) => {
58
- if (info) {
59
- acc[info.manifest.name] = info;
60
- }
61
- return acc;
62
- }, {});
45
+ const manifest = (await readTypedJson(
46
+ path.join(absoluteDir, "package.json"),
47
+ )) as PackageManifest;
48
+
49
+ return {
50
+ manifest,
51
+ rootRelativeDir,
52
+ absoluteDir,
53
+ };
54
+ }),
55
+ );
56
+
57
+ const registry: PackagesRegistry = entries.reduce<PackagesRegistry>(
58
+ (acc, info) => {
59
+ if (info) {
60
+ acc[info.manifest.name] = info;
61
+ }
62
+ return acc;
63
+ },
64
+ {},
65
+ );
63
66
 
64
67
  return registry;
65
68
  }
@@ -73,9 +76,9 @@ function listWorkspacePackages(
73
76
  workspaceRootDir: string,
74
77
  ) {
75
78
  if (isRushWorkspace(workspaceRootDir)) {
76
- const rushConfig = readTypedJsonSync<RushConfig>(
79
+ const rushConfig = readTypedJsonSync(
77
80
  path.join(workspaceRootDir, "rush.json"),
78
- );
81
+ ) as RushConfig;
79
82
 
80
83
  return rushConfig.projects.map(({ projectFolder }) => projectFolder);
81
84
  } else {
@@ -13,16 +13,16 @@ import {
13
13
  * monorepo. This configuration is dependent on the package manager used, and I
14
14
  * don't know if we're covering all cases yet...
15
15
  */
16
- export function findPackagesGlobs(workspaceRootDir: string) {
16
+ export function findPackagesGlobs(workspaceRootDir: string): string[] {
17
17
  const log = useLogger();
18
18
 
19
19
  const packageManager = usePackageManager();
20
20
 
21
21
  switch (packageManager.name) {
22
22
  case "pnpm": {
23
- const workspaceConfig = readTypedYamlSync<{ packages: string[] }>(
23
+ const workspaceConfig = readTypedYamlSync(
24
24
  path.join(workspaceRootDir, "pnpm-workspace.yaml"),
25
- );
25
+ ) as { packages: string[] } | undefined;
26
26
 
27
27
  if (!workspaceConfig) {
28
28
  throw new Error(
@@ -48,9 +48,9 @@ export function findPackagesGlobs(workspaceRootDir: string) {
48
48
  "package.json",
49
49
  );
50
50
 
51
- const { workspaces } = readTypedJsonSync<{ workspaces: string[] }>(
52
- workspaceRootManifestPath,
53
- );
51
+ const { workspaces } = readTypedJsonSync(workspaceRootManifestPath) as {
52
+ workspaces: string[];
53
+ };
54
54
 
55
55
  if (!workspaces) {
56
56
  throw new Error(
@@ -60,21 +60,25 @@ export function findPackagesGlobs(workspaceRootDir: string) {
60
60
 
61
61
  if (Array.isArray(workspaces)) {
62
62
  return workspaces;
63
- } else {
64
- /**
65
- * For Yarn, workspaces could be defined as an object with { packages:
66
- * [], nohoist: [] }. See
67
- * https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
68
- */
69
- const workspacesObject = workspaces as { packages?: string[] };
63
+ }
70
64
 
71
- assert(
72
- workspacesObject.packages,
73
- "workspaces.packages must be an array",
74
- );
65
+ /**
66
+ * For Yarn, workspaces could be defined as an object with { packages: [],
67
+ * nohoist: [] }. See
68
+ * https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
69
+ */
70
+ const workspacesObject = workspaces as { packages?: string[] };
75
71
 
76
- return workspacesObject.packages;
77
- }
72
+ assert(
73
+ Array.isArray(workspacesObject.packages),
74
+ "workspaces.packages must be an array",
75
+ );
76
+
77
+ return workspacesObject.packages;
78
78
  }
79
+ default:
80
+ throw new Error(
81
+ `Unsupported package manager: ${packageManager.name as string}`,
82
+ );
79
83
  }
80
84
  }