isolate-package 1.28.2 → 1.29.0-1

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.
@@ -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,12 @@ 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 the pnpm.overrides field from the root package.json.
59
60
  */
60
61
  await adoptPnpmFieldsFromRoot(
61
62
  manifestWithResolvedCatalogs,
@@ -44,10 +44,11 @@ export async function adaptInternalPackageManifests({
44
44
  };
45
45
 
46
46
  const outputManifest =
47
- packageManager.name === "pnpm" && !forceNpm
47
+ (packageManager.name === "pnpm" || packageManager.name === "bun") &&
48
+ !forceNpm
48
49
  ? /**
49
- * For PNPM the output itself is a workspace so we can preserve the specifiers
50
- * with "workspace:*" in the output manifest.
50
+ * For PNPM and Bun the output itself is a workspace so we can preserve
51
+ * the specifiers with "workspace:*" in the output manifest.
51
52
  */
52
53
  manifestWithResolvedCatalogs
53
54
  : /** For other package managers we replace the links to internal dependencies */
@@ -43,7 +43,7 @@ describe("validateManifestMandatoryFields", () => {
43
43
 
44
44
  expect(() =>
45
45
  validateManifestMandatoryFields(invalidDevManifest, packagePath, false),
46
- ).toThrow(/missing the "version" field/);
46
+ ).toThrow(/missing mandatory fields: version/);
47
47
  });
48
48
 
49
49
  it("should throw error when version field is missing", () => {
@@ -54,7 +54,7 @@ describe("validateManifestMandatoryFields", () => {
54
54
 
55
55
  expect(() =>
56
56
  validateManifestMandatoryFields(invalidManifest, packagePath),
57
- ).toThrow(/missing the "version" field/);
57
+ ).toThrow(/missing mandatory fields: version/);
58
58
  });
59
59
 
60
60
  it("should throw error when files field is missing", () => {
@@ -65,7 +65,7 @@ describe("validateManifestMandatoryFields", () => {
65
65
 
66
66
  expect(() =>
67
67
  validateManifestMandatoryFields(invalidManifest, packagePath),
68
- ).toThrow(/missing the "files" field/);
68
+ ).toThrow(/missing mandatory fields: files/);
69
69
  });
70
70
 
71
71
  it("should throw error when files field is empty array", () => {
@@ -77,7 +77,7 @@ describe("validateManifestMandatoryFields", () => {
77
77
 
78
78
  expect(() =>
79
79
  validateManifestMandatoryFields(invalidManifest, packagePath),
80
- ).toThrow(/missing the "files" field/);
80
+ ).toThrow(/missing mandatory fields: files/);
81
81
  });
82
82
 
83
83
  it("should throw error when files field is not an array", () => {
@@ -89,7 +89,7 @@ describe("validateManifestMandatoryFields", () => {
89
89
 
90
90
  expect(() =>
91
91
  validateManifestMandatoryFields(invalidManifest, packagePath),
92
- ).toThrow(/missing the "files" field/);
92
+ ).toThrow(/missing mandatory fields: files/);
93
93
  });
94
94
 
95
95
  it("should throw error when both fields are missing", () => {
@@ -99,29 +99,20 @@ describe("validateManifestMandatoryFields", () => {
99
99
 
100
100
  expect(() =>
101
101
  validateManifestMandatoryFields(invalidManifest, packagePath),
102
- ).toThrow(/missing mandatory fields in its package\.json: version, files/);
102
+ ).toThrow(/missing mandatory fields: version, files/);
103
103
  });
104
104
 
105
- it("should include documentation URL in error message", () => {
105
+ it("should include helpful error message", () => {
106
106
  const invalidManifest = {
107
107
  name: "test-package",
108
108
  } as PackageManifest;
109
109
 
110
110
  expect(() =>
111
111
  validateManifestMandatoryFields(invalidManifest, packagePath),
112
- ).toThrow(
113
- /See https:\/\/isolate-package\.codecompose\.dev\/getting-started#prerequisites/,
114
- );
115
-
116
- const singleFieldManifest = {
117
- name: "test-package",
118
- version: "1.0.0",
119
- } as PackageManifest;
112
+ ).toThrow(/missing mandatory fields: version, files/);
120
113
 
121
114
  expect(() =>
122
- validateManifestMandatoryFields(singleFieldManifest, packagePath),
123
- ).toThrow(
124
- /See https:\/\/isolate-package\.codecompose\.dev\/getting-started#define-files-field-in-each-package-manifest/,
125
- );
115
+ validateManifestMandatoryFields(invalidManifest, packagePath),
116
+ ).toThrow(/See the documentation for more details/);
126
117
  });
127
118
  });
@@ -1,14 +1,6 @@
1
1
  import { useLogger } from "../logger";
2
2
  import type { PackageManifest } from "../types";
3
3
 
4
- /** Maps field names to their documentation URLs */
5
- const fieldDocUrls: Record<string, string> = {
6
- version:
7
- "https://isolate-package.codecompose.dev/getting-started#define-version-field-in-each-package-manifest",
8
- files:
9
- "https://isolate-package.codecompose.dev/getting-started#define-files-field-in-each-package-manifest",
10
- };
11
-
12
4
  /**
13
5
  * Validate that mandatory fields are present in the package manifest. These
14
6
  * fields are required for the isolate process to work properly.
@@ -46,11 +38,7 @@ export function validateManifestMandatoryFields(
46
38
  }
47
39
 
48
40
  if (missingFields.length > 0) {
49
- const field = missingFields[0]!;
50
- const errorMessage =
51
- missingFields.length === 1
52
- ? `Package at ${packagePath} is missing the "${field}" field in its package.json. See ${fieldDocUrls[field] ?? "https://isolate-package.codecompose.dev/getting-started#prerequisites"}`
53
- : `Package at ${packagePath} is missing mandatory fields in its package.json: ${missingFields.join(", ")}. See https://isolate-package.codecompose.dev/getting-started#prerequisites`;
41
+ const errorMessage = `Package at ${packagePath} is missing mandatory fields: ${missingFields.join(", ")}. See the documentation for more details.`;
54
42
 
55
43
  log.error(errorMessage);
56
44
  throw new Error(errorMessage);
@@ -22,7 +22,7 @@ vi.mock("~/lib/utils", () => ({
22
22
 
23
23
  /** Mock the package manager */
24
24
  vi.mock("~/lib/package-manager", () => ({
25
- usePackageManager: vi.fn(() => ({ majorVersion: 9 })),
25
+ usePackageManager: vi.fn(() => ({ name: "pnpm", majorVersion: 9 })),
26
26
  }));
27
27
 
28
28
  /** Mock the pnpm lockfile readers */
@@ -34,20 +34,11 @@ vi.mock("pnpm_lockfile_file_v9", () => ({
34
34
  readWantedLockfile: vi.fn(() => Promise.resolve(null)),
35
35
  }));
36
36
 
37
- /** Mock the logger */
38
- vi.mock("~/lib/logger", () => ({
39
- useLogger: () => ({
40
- debug: vi.fn(),
41
- info: vi.fn(),
42
- warn: vi.fn(),
43
- error: vi.fn(),
44
- }),
45
- }));
46
-
47
37
  const fs = vi.mocked((await import("fs-extra")).default);
48
38
  const { filterPatchedDependencies, readTypedJson } = vi.mocked(
49
39
  await import("~/lib/utils"),
50
40
  );
41
+ const { usePackageManager } = vi.mocked(await import("~/lib/package-manager"));
51
42
 
52
43
  describe("copyPatches", () => {
53
44
  beforeEach(() => {
@@ -395,4 +386,48 @@ describe("copyPatches", () => {
395
386
  });
396
387
  expect(fs.copy).toHaveBeenCalledTimes(2);
397
388
  });
389
+
390
+ it("should read top-level patchedDependencies for Bun projects", async () => {
391
+ usePackageManager.mockReturnValue({
392
+ name: "bun",
393
+ majorVersion: 1,
394
+ version: "1.2.0",
395
+ packageManagerString: "bun@1.2.0",
396
+ });
397
+
398
+ const targetManifest: PackageManifest = {
399
+ name: "test",
400
+ version: "1.0.0",
401
+ dependencies: { lodash: "^4.0.0" },
402
+ };
403
+
404
+ readTypedJson.mockResolvedValue({
405
+ name: "root",
406
+ version: "1.0.0",
407
+ patchedDependencies: {
408
+ "lodash@4.17.21": "patches/lodash.patch",
409
+ },
410
+ } as PackageManifest);
411
+
412
+ filterPatchedDependencies.mockReturnValue({
413
+ "lodash@4.17.21": "patches/lodash.patch",
414
+ });
415
+
416
+ fs.existsSync.mockReturnValue(true);
417
+
418
+ const result = await copyPatches({
419
+ workspaceRootDir: "/workspace",
420
+ targetPackageManifest: targetManifest,
421
+ isolateDir: "/workspace/isolate",
422
+ includeDevDependencies: false,
423
+ });
424
+
425
+ expect(result).toEqual({
426
+ "lodash@4.17.21": { path: "patches/lodash.patch", hash: "" },
427
+ });
428
+ expect(fs.copy).toHaveBeenCalledWith(
429
+ "/workspace/patches/lodash.patch",
430
+ "/workspace/isolate/patches/lodash.patch",
431
+ );
432
+ });
398
433
  });
@@ -37,7 +37,10 @@ export async function copyPatches({
37
37
  return {};
38
38
  }
39
39
 
40
- const patchedDependencies = workspaceRootManifest.pnpm?.patchedDependencies;
40
+ /** PNPM stores patches under pnpm.patchedDependencies, Bun at the top level */
41
+ const patchedDependencies =
42
+ workspaceRootManifest.pnpm?.patchedDependencies ??
43
+ workspaceRootManifest.patchedDependencies;
41
44
 
42
45
  if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
43
46
  log.debug("No patched dependencies found in workspace root package.json");
@@ -58,9 +61,15 @@ export async function copyPatches({
58
61
  return {};
59
62
  }
60
63
 
61
- /** Read the lockfile to get the hashes for each patch */
64
+ /**
65
+ * Read the pnpm lockfile to get patch hashes. Bun doesn't store hashes in
66
+ * its lockfile so we skip this for Bun.
67
+ */
68
+ const { name: packageManagerName } = usePackageManager();
62
69
  const lockfilePatchedDependencies =
63
- await readLockfilePatchedDependencies(workspaceRootDir);
70
+ packageManagerName === "pnpm"
71
+ ? await readLockfilePatchedDependencies(workspaceRootDir)
72
+ : undefined;
64
73
 
65
74
  const copiedPatches: Record<string, PatchFile> = {};
66
75
 
@@ -84,7 +93,7 @@ export async function copyPatches({
84
93
  const originalPatchFile = lockfilePatchedDependencies?.[packageSpec];
85
94
  const hash = originalPatchFile?.hash ?? "";
86
95
 
87
- if (!hash) {
96
+ if (packageManagerName === "pnpm" && !hash) {
88
97
  log.warn(`No hash found for patch ${packageSpec} in lockfile`);
89
98
  }
90
99
 
package/src/lib/types.ts CHANGED
@@ -11,6 +11,9 @@ export interface PatchFile {
11
11
 
12
12
  export type PackageManifest = PnpmPackageManifest & {
13
13
  packageManager?: string;
14
+ workspaces?: string[];
15
+ /** Bun stores patchedDependencies at the top level */
16
+ patchedDependencies?: Record<string, string>;
14
17
  pnpm?: {
15
18
  patchedDependencies?: Record<string, string>;
16
19
  [key: string]: unknown;
@@ -1,17 +1,7 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import { describe, expect, it } from "vitest";
2
2
  import type { PackageManifest } from "~/lib/types";
3
3
  import { filterPatchedDependencies } from "./filter-patched-dependencies";
4
4
 
5
- /** Mock the logger */
6
- vi.mock("~/lib/logger", () => ({
7
- useLogger: () => ({
8
- debug: vi.fn(),
9
- info: vi.fn(),
10
- warn: vi.fn(),
11
- error: vi.fn(),
12
- }),
13
- }));
14
-
15
5
  describe("filterPatchedDependencies", () => {
16
6
  it("should return undefined when patchedDependencies is undefined", () => {
17
7
  const manifest: PackageManifest = { name: "test", version: "1.0.0" };
@@ -0,0 +1,13 @@
1
+ import { vi } from "vitest";
2
+
3
+ /** Mock the logger for all tests to prevent console output during tests */
4
+ vi.mock("~/lib/logger", () => ({
5
+ useLogger: () => ({
6
+ debug: vi.fn(),
7
+ info: vi.fn(),
8
+ warn: vi.fn(),
9
+ error: vi.fn(),
10
+ }),
11
+ setLogLevel: vi.fn(),
12
+ setLogger: vi.fn(),
13
+ }));