isolate-package 1.29.0 → 1.30.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 (29) hide show
  1. package/dist/index.d.mts +7 -1
  2. package/dist/index.mjs +2 -3
  3. package/dist/index.mjs.map +1 -1
  4. package/dist/{isolate-3GcdAuUK.mjs → isolate-CJy3YyKG.mjs} +261 -64
  5. package/dist/isolate-CJy3YyKG.mjs.map +1 -0
  6. package/dist/isolate-bin.mjs +3 -5
  7. package/dist/isolate-bin.mjs.map +1 -1
  8. package/package.json +21 -20
  9. package/src/get-internal-package-names.test.ts +0 -10
  10. package/src/index.ts +6 -0
  11. package/src/isolate.ts +38 -8
  12. package/src/lib/lockfile/helpers/generate-bun-lockfile.test.ts +619 -0
  13. package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +354 -0
  14. package/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +387 -0
  15. package/src/lib/lockfile/helpers/index.ts +1 -0
  16. package/src/lib/lockfile/helpers/pnpm-map-importer.test.ts +183 -0
  17. package/src/lib/lockfile/process-lockfile.test.ts +238 -0
  18. package/src/lib/lockfile/process-lockfile.ts +6 -6
  19. package/src/lib/manifest/adapt-target-package-manifest.ts +6 -4
  20. package/src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts +49 -0
  21. package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +15 -3
  22. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +61 -0
  23. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +42 -3
  24. package/src/lib/patches/copy-patches.test.ts +49 -11
  25. package/src/lib/patches/copy-patches.ts +38 -17
  26. package/src/lib/types.ts +5 -0
  27. package/src/lib/utils/filter-patched-dependencies.test.ts +1 -11
  28. package/src/testing/setup.ts +13 -0
  29. package/dist/isolate-3GcdAuUK.mjs.map +0 -1
@@ -0,0 +1,238 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { processLockfile } from "./process-lockfile";
3
+ import type { IsolateConfigResolved } from "../config";
4
+
5
+ /** Mock the package manager detection */
6
+ vi.mock("~/lib/package-manager", () => ({
7
+ usePackageManager: vi.fn(),
8
+ }));
9
+
10
+ /** Mock all lockfile generators */
11
+ vi.mock("./helpers", () => ({
12
+ generateBunLockfile: vi.fn(),
13
+ generateNpmLockfile: vi.fn(),
14
+ generatePnpmLockfile: vi.fn(),
15
+ generateYarnLockfile: vi.fn(),
16
+ }));
17
+
18
+ const { usePackageManager } = vi.mocked(await import("~/lib/package-manager"));
19
+ const {
20
+ generateBunLockfile,
21
+ generateNpmLockfile,
22
+ generatePnpmLockfile,
23
+ generateYarnLockfile,
24
+ } = vi.mocked(await import("./helpers"));
25
+
26
+ /** Minimal config for testing */
27
+ function createConfig(
28
+ overrides?: Partial<IsolateConfigResolved>,
29
+ ): IsolateConfigResolved {
30
+ return {
31
+ buildDirName: "dist",
32
+ forceNpm: false,
33
+ includeDevDependencies: false,
34
+ isolateDirName: "isolate",
35
+ logLevel: "info",
36
+ omitPackageManager: false,
37
+ tsconfigPath: undefined,
38
+ workspacePackages: undefined,
39
+ workspaceRoot: undefined,
40
+ ...overrides,
41
+ } as IsolateConfigResolved;
42
+ }
43
+
44
+ describe("processLockfile", () => {
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.restoreAllMocks();
51
+ });
52
+
53
+ it("should route to npm generator for npm package manager", async () => {
54
+ usePackageManager.mockReturnValue({
55
+ name: "npm",
56
+ majorVersion: 10,
57
+ version: "10.0.0",
58
+ packageManagerString: "npm@10.0.0",
59
+ });
60
+
61
+ const result = await processLockfile({
62
+ workspaceRootDir: "/workspace",
63
+ isolateDir: "/workspace/apps/my-app/isolate",
64
+ packagesRegistry: {},
65
+ internalDepPackageNames: [],
66
+ targetPackageDir: "/workspace/apps/my-app",
67
+ targetPackageName: "my-app",
68
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
69
+ config: createConfig(),
70
+ });
71
+
72
+ expect(generateNpmLockfile).toHaveBeenCalledWith({
73
+ workspaceRootDir: "/workspace",
74
+ isolateDir: "/workspace/apps/my-app/isolate",
75
+ });
76
+ expect(result).toBe(false);
77
+ });
78
+
79
+ it("should route to pnpm generator for pnpm package manager", async () => {
80
+ usePackageManager.mockReturnValue({
81
+ name: "pnpm",
82
+ majorVersion: 9,
83
+ version: "9.0.0",
84
+ packageManagerString: "pnpm@9.0.0",
85
+ });
86
+
87
+ const config = createConfig({ includeDevDependencies: true });
88
+
89
+ await processLockfile({
90
+ workspaceRootDir: "/workspace",
91
+ isolateDir: "/workspace/apps/my-app/isolate",
92
+ packagesRegistry: { shared: {} as never },
93
+ internalDepPackageNames: ["shared"],
94
+ targetPackageDir: "/workspace/apps/my-app",
95
+ targetPackageName: "my-app",
96
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
97
+ config,
98
+ });
99
+
100
+ expect(generatePnpmLockfile).toHaveBeenCalledWith({
101
+ workspaceRootDir: "/workspace",
102
+ targetPackageDir: "/workspace/apps/my-app",
103
+ isolateDir: "/workspace/apps/my-app/isolate",
104
+ internalDepPackageNames: ["shared"],
105
+ packagesRegistry: { shared: {} as never },
106
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
107
+ majorVersion: 9,
108
+ includeDevDependencies: true,
109
+ patchedDependencies: undefined,
110
+ });
111
+ });
112
+
113
+ it("should route to yarn generator for yarn v1", async () => {
114
+ usePackageManager.mockReturnValue({
115
+ name: "yarn",
116
+ majorVersion: 1,
117
+ version: "1.22.0",
118
+ packageManagerString: "yarn@1.22.0",
119
+ });
120
+
121
+ const result = await processLockfile({
122
+ workspaceRootDir: "/workspace",
123
+ isolateDir: "/workspace/apps/my-app/isolate",
124
+ packagesRegistry: {},
125
+ internalDepPackageNames: [],
126
+ targetPackageDir: "/workspace/apps/my-app",
127
+ targetPackageName: "my-app",
128
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
129
+ config: createConfig(),
130
+ });
131
+
132
+ expect(generateYarnLockfile).toHaveBeenCalledWith({
133
+ workspaceRootDir: "/workspace",
134
+ isolateDir: "/workspace/apps/my-app/isolate",
135
+ });
136
+ expect(result).toBe(false);
137
+ });
138
+
139
+ it("should fall back to npm for modern yarn (v2+)", async () => {
140
+ usePackageManager.mockReturnValue({
141
+ name: "yarn",
142
+ majorVersion: 4,
143
+ version: "4.0.0",
144
+ packageManagerString: "yarn@4.0.0",
145
+ });
146
+
147
+ const result = await processLockfile({
148
+ workspaceRootDir: "/workspace",
149
+ isolateDir: "/workspace/apps/my-app/isolate",
150
+ packagesRegistry: {},
151
+ internalDepPackageNames: [],
152
+ targetPackageDir: "/workspace/apps/my-app",
153
+ targetPackageName: "my-app",
154
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
155
+ config: createConfig(),
156
+ });
157
+
158
+ expect(generateNpmLockfile).toHaveBeenCalled();
159
+ expect(result).toBe(true);
160
+ });
161
+
162
+ it("should route to bun generator for bun package manager", async () => {
163
+ usePackageManager.mockReturnValue({
164
+ name: "bun",
165
+ majorVersion: 1,
166
+ version: "1.2.0",
167
+ packageManagerString: "bun@1.2.0",
168
+ });
169
+
170
+ const result = await processLockfile({
171
+ workspaceRootDir: "/workspace",
172
+ isolateDir: "/workspace/apps/my-app/isolate",
173
+ packagesRegistry: {},
174
+ internalDepPackageNames: ["shared"],
175
+ targetPackageDir: "/workspace/apps/my-app",
176
+ targetPackageName: "my-app",
177
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
178
+ config: createConfig(),
179
+ });
180
+
181
+ expect(generateBunLockfile).toHaveBeenCalledWith({
182
+ workspaceRootDir: "/workspace",
183
+ targetPackageDir: "/workspace/apps/my-app",
184
+ isolateDir: "/workspace/apps/my-app/isolate",
185
+ internalDepPackageNames: ["shared"],
186
+ packagesRegistry: {},
187
+ includeDevDependencies: false,
188
+ });
189
+ expect(result).toBe(false);
190
+ });
191
+
192
+ it("should use npm when forceNpm is true regardless of package manager", async () => {
193
+ usePackageManager.mockReturnValue({
194
+ name: "pnpm",
195
+ majorVersion: 9,
196
+ version: "9.0.0",
197
+ packageManagerString: "pnpm@9.0.0",
198
+ });
199
+
200
+ const result = await processLockfile({
201
+ workspaceRootDir: "/workspace",
202
+ isolateDir: "/workspace/apps/my-app/isolate",
203
+ packagesRegistry: {},
204
+ internalDepPackageNames: [],
205
+ targetPackageDir: "/workspace/apps/my-app",
206
+ targetPackageName: "my-app",
207
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
208
+ config: createConfig({ forceNpm: true }),
209
+ });
210
+
211
+ expect(generateNpmLockfile).toHaveBeenCalled();
212
+ expect(generatePnpmLockfile).not.toHaveBeenCalled();
213
+ expect(result).toBe(true);
214
+ });
215
+
216
+ it("should fall back to npm for unknown package manager", async () => {
217
+ usePackageManager.mockReturnValue({
218
+ name: "unknown" as never,
219
+ majorVersion: 1,
220
+ version: "1.0.0",
221
+ packageManagerString: "unknown@1.0.0",
222
+ });
223
+
224
+ const result = await processLockfile({
225
+ workspaceRootDir: "/workspace",
226
+ isolateDir: "/workspace/apps/my-app/isolate",
227
+ packagesRegistry: {},
228
+ internalDepPackageNames: [],
229
+ targetPackageDir: "/workspace/apps/my-app",
230
+ targetPackageName: "my-app",
231
+ targetPackageManifest: { name: "my-app", version: "1.0.0" },
232
+ config: createConfig(),
233
+ });
234
+
235
+ expect(generateNpmLockfile).toHaveBeenCalled();
236
+ expect(result).toBe(true);
237
+ });
238
+ });
@@ -3,6 +3,7 @@ import { useLogger } from "../logger";
3
3
  import { usePackageManager } from "../package-manager";
4
4
  import type { PackageManifest, PackagesRegistry, PatchFile } from "../types";
5
5
  import {
6
+ generateBunLockfile,
6
7
  generateNpmLockfile,
7
8
  generatePnpmLockfile,
8
9
  generateYarnLockfile,
@@ -97,15 +98,14 @@ export async function processLockfile({
97
98
  break;
98
99
  }
99
100
  case "bun": {
100
- log.warn(
101
- `Ouput lockfiles for Bun are not yet supported. Using NPM for output`,
102
- );
103
- await generateNpmLockfile({
101
+ await generateBunLockfile({
104
102
  workspaceRootDir,
103
+ targetPackageDir,
105
104
  isolateDir,
105
+ internalDepPackageNames,
106
+ packagesRegistry,
107
+ includeDevDependencies: config.includeDevDependencies,
106
108
  });
107
-
108
- usedFallbackToNpm = true;
109
109
  break;
110
110
  }
111
111
  default:
@@ -51,11 +51,13 @@ export async function adaptTargetPackageManifest({
51
51
  };
52
52
 
53
53
  const adaptedManifest =
54
- packageManager.name === "pnpm" && !forceNpm
54
+ (packageManager.name === "pnpm" || packageManager.name === "bun") &&
55
+ !forceNpm
55
56
  ? /**
56
- * For PNPM the output itself is a workspace so we can preserve the specifiers
57
- * with "workspace:*" in the output manifest, but we do want to adopt the
58
- * pnpm.overrides field from the root package.json.
57
+ * For PNPM and Bun the output itself is a workspace so we can preserve
58
+ * the specifiers with "workspace:*" in the output manifest, but we do
59
+ * want to adopt workspace-level fields from the root package.json
60
+ * (pnpm.overrides for PNPM, top-level overrides for Bun).
59
61
  */
60
62
  await adoptPnpmFieldsFromRoot(
61
63
  manifestWithResolvedCatalogs,
@@ -95,6 +95,55 @@ describe("adaptInternalPackageManifests", () => {
95
95
  });
96
96
  });
97
97
 
98
+ it("should strip the prepare script from internal dependency manifests", async () => {
99
+ const manifest: PackageManifest = {
100
+ name: "@repo/common",
101
+ version: "1.0.0",
102
+ scripts: {
103
+ prepare: "npm run clean && npm run build",
104
+ clean: "del-cli dist",
105
+ build: "tsdown",
106
+ postinstall: "prisma generate",
107
+ },
108
+ dependencies: {
109
+ ky: "^1.0.0",
110
+ },
111
+ devDependencies: {
112
+ "del-cli": "^7.0.0",
113
+ tsdown: "^0.20.0",
114
+ },
115
+ };
116
+
117
+ const packagesRegistry = createRegistry({
118
+ "@repo/common": {
119
+ rootRelativeDir: "packages/common",
120
+ manifest,
121
+ },
122
+ });
123
+
124
+ await adaptInternalPackageManifests({
125
+ internalPackageNames: ["@repo/common"],
126
+ packagesRegistry,
127
+ isolateDir: "/output",
128
+ forceNpm: false,
129
+ workspaceRootDir: "/workspace",
130
+ });
131
+
132
+ expect(writeManifest).toHaveBeenCalledOnce();
133
+
134
+ const writtenManifest = writeManifest.mock.calls[0]![1];
135
+
136
+ /** prepare should be stripped because it depends on devDependency binaries */
137
+ expect(writtenManifest.scripts?.prepare).toBeUndefined();
138
+
139
+ /** Other scripts should be preserved */
140
+ expect(writtenManifest.scripts).toEqual({
141
+ clean: "del-cli dist",
142
+ build: "tsdown",
143
+ postinstall: "prisma generate",
144
+ });
145
+ });
146
+
98
147
  it("should strip devDependencies from internal dependency manifests", async () => {
99
148
  const manifest: PackageManifest = {
100
149
  name: "@repo/shared",
@@ -34,6 +34,17 @@ export async function adaptInternalPackageManifests({
34
34
  /** Dev dependencies are never included for internal deps */
35
35
  const strippedManifest = omit(manifest, ["devDependencies"]);
36
36
 
37
+ /**
38
+ * Strip the `prepare` script because it runs during `pnpm install` and
39
+ * typically depends on devDependency binaries (e.g. tsdown, del-cli)
40
+ * which are not available in the isolated output. Other lifecycle
41
+ * scripts like `postinstall` are preserved because they handle runtime
42
+ * setup (e.g. Prisma client generation).
43
+ */
44
+ if (strippedManifest.scripts) {
45
+ strippedManifest.scripts = omit(strippedManifest.scripts, ["prepare"]);
46
+ }
47
+
37
48
  /** Resolve catalog dependencies before adapting internal deps */
38
49
  const manifestWithResolvedCatalogs = {
39
50
  ...strippedManifest,
@@ -44,10 +55,11 @@ export async function adaptInternalPackageManifests({
44
55
  };
45
56
 
46
57
  const outputManifest =
47
- packageManager.name === "pnpm" && !forceNpm
58
+ (packageManager.name === "pnpm" || packageManager.name === "bun") &&
59
+ !forceNpm
48
60
  ? /**
49
- * For PNPM the output itself is a workspace so we can preserve the specifiers
50
- * with "workspace:*" in the output manifest.
61
+ * For PNPM and Bun the output itself is a workspace so we can preserve
62
+ * the specifiers with "workspace:*" in the output manifest.
51
63
  */
52
64
  manifestWithResolvedCatalogs
53
65
  : /** For other package managers we replace the links to internal dependencies */
@@ -12,10 +12,16 @@ vi.mock("~/lib/utils", () => ({
12
12
  readTypedJson: vi.fn(),
13
13
  }));
14
14
 
15
+ vi.mock("~/lib/package-manager", () => ({
16
+ usePackageManager: vi.fn(() => ({ name: "pnpm", majorVersion: 9 })),
17
+ }));
18
+
15
19
  const { isRushWorkspace, readTypedJson } = vi.mocked(
16
20
  await import("~/lib/utils"),
17
21
  );
18
22
 
23
+ const { usePackageManager } = vi.mocked(await import("~/lib/package-manager"));
24
+
19
25
  describe("adoptPnpmFieldsFromRoot", () => {
20
26
  beforeEach(() => {
21
27
  vi.clearAllMocks();
@@ -209,4 +215,59 @@ describe("adoptPnpmFieldsFromRoot", () => {
209
215
  },
210
216
  });
211
217
  });
218
+
219
+ it("should adopt top-level overrides for Bun", async () => {
220
+ usePackageManager.mockReturnValue({
221
+ name: "bun",
222
+ majorVersion: 1,
223
+ version: "1.0.0",
224
+ packageManagerString: "bun@1.0.0",
225
+ } as ReturnType<typeof usePackageManager>);
226
+ isRushWorkspace.mockReturnValue(false);
227
+ readTypedJson.mockResolvedValue({
228
+ name: "root",
229
+ version: "1.0.0",
230
+ overrides: {
231
+ foo: "^1.0.0",
232
+ },
233
+ } as unknown as ProjectManifest);
234
+
235
+ const targetManifest: PackageManifest = {
236
+ name: "test-package",
237
+ version: "1.0.0",
238
+ };
239
+
240
+ const result = await adoptPnpmFieldsFromRoot(targetManifest, "/workspace");
241
+
242
+ expect(result).toEqual({
243
+ name: "test-package",
244
+ version: "1.0.0",
245
+ overrides: {
246
+ foo: "^1.0.0",
247
+ },
248
+ });
249
+ });
250
+
251
+ it("should return original manifest for Bun when no overrides are present", async () => {
252
+ usePackageManager.mockReturnValue({
253
+ name: "bun",
254
+ majorVersion: 1,
255
+ version: "1.0.0",
256
+ packageManagerString: "bun@1.0.0",
257
+ } as ReturnType<typeof usePackageManager>);
258
+ isRushWorkspace.mockReturnValue(false);
259
+ readTypedJson.mockResolvedValue({
260
+ name: "root",
261
+ version: "1.0.0",
262
+ } as ProjectManifest);
263
+
264
+ const targetManifest: PackageManifest = {
265
+ name: "test-package",
266
+ version: "1.0.0",
267
+ };
268
+
269
+ const result = await adoptPnpmFieldsFromRoot(targetManifest, "/workspace");
270
+
271
+ expect(result).toEqual(targetManifest);
272
+ });
212
273
  });
@@ -1,12 +1,13 @@
1
1
  import type { ProjectManifest, PnpmSettings } from "@pnpm/types";
2
2
  import path from "path";
3
+ import { usePackageManager } from "~/lib/package-manager";
3
4
  import type { PackageManifest } from "~/lib/types";
4
5
  import { isRushWorkspace, readTypedJson } from "~/lib/utils";
5
6
 
6
7
  /**
7
- * Adopts the `pnpm` fields from the root package manifest. Currently it takes
8
- * overrides, onlyBuiltDependencies, and ignoredBuiltDependencies, because these
9
- * are typically workspace-level configuration settings.
8
+ * Adopts workspace-level fields from the root package manifest. For pnpm this
9
+ * reads overrides, onlyBuiltDependencies, and ignoredBuiltDependencies from the
10
+ * `pnpm` key. For Bun it reads `overrides` from the top level.
10
11
  */
11
12
  export async function adoptPnpmFieldsFromRoot(
12
13
  targetPackageManifest: PackageManifest,
@@ -20,6 +21,44 @@ export async function adoptPnpmFieldsFromRoot(
20
21
  path.join(workspaceRootDir, "package.json"),
21
22
  );
22
23
 
24
+ const packageManager = usePackageManager();
25
+
26
+ if (packageManager.name === "bun") {
27
+ return adoptBunFieldsFromRoot(targetPackageManifest, rootPackageManifest);
28
+ }
29
+
30
+ return adoptPnpmFieldsOnly(targetPackageManifest, rootPackageManifest);
31
+ }
32
+
33
+ /** Adopt Bun's top-level overrides from the root manifest */
34
+ function adoptBunFieldsFromRoot(
35
+ targetPackageManifest: PackageManifest,
36
+ rootPackageManifest: ProjectManifest,
37
+ ): PackageManifest {
38
+ /**
39
+ * Bun supports `overrides` at the top level of package.json (same as npm).
40
+ * Read from the root manifest and set them on the output manifest so that
41
+ * `bun install --frozen-lockfile` succeeds.
42
+ */
43
+ const overrides = (rootPackageManifest as Record<string, unknown>)[
44
+ "overrides"
45
+ ] as Record<string, string> | undefined;
46
+
47
+ if (!overrides) {
48
+ return targetPackageManifest;
49
+ }
50
+
51
+ return {
52
+ ...targetPackageManifest,
53
+ overrides,
54
+ } as PackageManifest;
55
+ }
56
+
57
+ /** Adopt pnpm-specific fields from the root manifest */
58
+ function adoptPnpmFieldsOnly(
59
+ targetPackageManifest: PackageManifest,
60
+ rootPackageManifest: ProjectManifest,
61
+ ): PackageManifest {
23
62
  const { overrides, onlyBuiltDependencies, ignoredBuiltDependencies } =
24
63
  rootPackageManifest.pnpm || {};
25
64
 
@@ -23,7 +23,7 @@ vi.mock("~/lib/utils", () => ({
23
23
 
24
24
  /** Mock the package manager */
25
25
  vi.mock("~/lib/package-manager", () => ({
26
- usePackageManager: vi.fn(() => ({ majorVersion: 9 })),
26
+ usePackageManager: vi.fn(() => ({ name: "pnpm", majorVersion: 9 })),
27
27
  }));
28
28
 
29
29
  /** Mock the pnpm lockfile readers */
@@ -35,19 +35,10 @@ vi.mock("pnpm_lockfile_file_v9", () => ({
35
35
  readWantedLockfile: vi.fn(() => Promise.resolve(null)),
36
36
  }));
37
37
 
38
- /** Mock the logger */
39
- vi.mock("~/lib/logger", () => ({
40
- useLogger: () => ({
41
- debug: vi.fn(),
42
- info: vi.fn(),
43
- warn: vi.fn(),
44
- error: vi.fn(),
45
- }),
46
- }));
47
-
48
38
  const fs = vi.mocked((await import("fs-extra")).default);
49
39
  const { filterPatchedDependencies, readTypedJson, readTypedYamlSync } =
50
40
  vi.mocked(await import("~/lib/utils"));
41
+ const { usePackageManager } = vi.mocked(await import("~/lib/package-manager"));
51
42
 
52
43
  describe("copyPatches", () => {
53
44
  beforeEach(() => {
@@ -420,4 +411,51 @@ describe("copyPatches", () => {
420
411
  expect(fs.copy).toHaveBeenCalledTimes(2);
421
412
  });
422
413
  });
414
+
415
+ it("should read top-level patchedDependencies for Bun projects", async () => {
416
+ usePackageManager.mockReturnValue({
417
+ name: "bun",
418
+ majorVersion: 1,
419
+ version: "1.2.0",
420
+ packageManagerString: "bun@1.2.0",
421
+ });
422
+
423
+ const targetManifest: PackageManifest = {
424
+ name: "test",
425
+ version: "1.0.0",
426
+ dependencies: { lodash: "^4.0.0" },
427
+ };
428
+
429
+ /** No patches in pnpm-workspace.yaml, so it falls back to package.json */
430
+ readTypedYamlSync.mockReturnValue({});
431
+
432
+ readTypedJson.mockResolvedValue({
433
+ name: "root",
434
+ version: "1.0.0",
435
+ patchedDependencies: {
436
+ "lodash@4.17.21": "patches/lodash.patch",
437
+ },
438
+ } as PackageManifest);
439
+
440
+ filterPatchedDependencies.mockReturnValue({
441
+ "lodash@4.17.21": "patches/lodash.patch",
442
+ });
443
+
444
+ fs.existsSync.mockReturnValue(true);
445
+
446
+ const result = await copyPatches({
447
+ workspaceRootDir: "/workspace",
448
+ targetPackageManifest: targetManifest,
449
+ isolateDir: "/workspace/isolate",
450
+ includeDevDependencies: false,
451
+ });
452
+
453
+ expect(result).toEqual({
454
+ "lodash@4.17.21": { path: "patches/lodash.patch", hash: "" },
455
+ });
456
+ expect(fs.copy).toHaveBeenCalledWith(
457
+ "/workspace/patches/lodash.patch",
458
+ "/workspace/isolate/patches/lodash.patch",
459
+ );
460
+ });
423
461
  });
@@ -26,30 +26,46 @@ export async function copyPatches({
26
26
  }): Promise<Record<string, PatchFile>> {
27
27
  const log = useLogger();
28
28
 
29
+ const { name: packageManagerName } = usePackageManager();
30
+
29
31
  let patchedDependencies: Record<string, string> | undefined;
30
32
 
31
- // First try reading the pnpm-workspace.yaml file
32
- try {
33
- const pnpmSettings = readTypedYamlSync<PnpmSettings>(
34
- path.join(workspaceRootDir, "pnpm-workspace.yaml"),
35
- );
36
- patchedDependencies = pnpmSettings?.patchedDependencies;
37
- } catch (error) {
38
- log.warn(
39
- `Could not read pnpm-workspace.yaml: ${error instanceof Error ? error.message : String(error)}`,
40
- );
33
+ /**
34
+ * Only try reading pnpm-workspace.yaml for pnpm workspaces. Bun workspaces
35
+ * don't have this file and the warning would be noisy.
36
+ */
37
+ if (packageManagerName === "pnpm") {
38
+ try {
39
+ const pnpmSettings = readTypedYamlSync<PnpmSettings>(
40
+ path.join(workspaceRootDir, "pnpm-workspace.yaml"),
41
+ );
42
+ patchedDependencies = pnpmSettings?.patchedDependencies;
43
+ } catch (error) {
44
+ log.warn(
45
+ `Could not read pnpm-workspace.yaml: ${error instanceof Error ? error.message : String(error)}`,
46
+ );
47
+ }
41
48
  }
42
49
 
43
50
  if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
44
- log.debug(
45
- "No patched dependencies found in pnpm-workspace.yaml; Falling back to workspace root package.json",
46
- );
51
+ if (packageManagerName === "pnpm") {
52
+ log.debug(
53
+ "No patched dependencies found in pnpm-workspace.yaml; Falling back to workspace root package.json",
54
+ );
55
+ } else {
56
+ log.debug(
57
+ "Reading patched dependencies from workspace root package.json",
58
+ );
59
+ }
47
60
 
48
61
  try {
49
62
  const workspaceRootManifest = await readTypedJson<PackageManifest>(
50
63
  path.join(workspaceRootDir, "package.json"),
51
64
  );
52
- patchedDependencies = workspaceRootManifest?.pnpm?.patchedDependencies;
65
+ /** PNPM stores patches under pnpm.patchedDependencies, Bun at the top level */
66
+ patchedDependencies =
67
+ workspaceRootManifest?.pnpm?.patchedDependencies ??
68
+ workspaceRootManifest?.patchedDependencies;
53
69
  } catch (error) {
54
70
  log.warn(
55
71
  `Could not read workspace root package.json: ${error instanceof Error ? error.message : String(error)}`,
@@ -76,9 +92,14 @@ export async function copyPatches({
76
92
  return {};
77
93
  }
78
94
 
79
- /** Read the lockfile to get the hashes for each patch */
95
+ /**
96
+ * Read the pnpm lockfile to get patch hashes. Bun doesn't store hashes in
97
+ * its lockfile so we skip this for Bun.
98
+ */
80
99
  const lockfilePatchedDependencies =
81
- await readLockfilePatchedDependencies(workspaceRootDir);
100
+ packageManagerName === "pnpm"
101
+ ? await readLockfilePatchedDependencies(workspaceRootDir)
102
+ : undefined;
82
103
 
83
104
  const copiedPatches: Record<string, PatchFile> = {};
84
105
 
@@ -102,7 +123,7 @@ export async function copyPatches({
102
123
  const originalPatchFile = lockfilePatchedDependencies?.[packageSpec];
103
124
  const hash = originalPatchFile?.hash ?? "";
104
125
 
105
- if (!hash) {
126
+ if (packageManagerName === "pnpm" && !hash) {
106
127
  log.warn(`No hash found for patch ${packageSpec} in lockfile`);
107
128
  }
108
129