isolate-package 1.30.0-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.
@@ -0,0 +1,181 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import type { PackageManifest, PackagesRegistry } from "~/lib/types";
3
+
4
+ /** Mock dependencies */
5
+ vi.mock("~/lib/package-manager", () => ({
6
+ usePackageManager: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("../io", () => ({
10
+ writeManifest: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("./adapt-manifest-internal-deps", () => ({
14
+ adaptManifestInternalDeps: vi.fn(({ manifest }) => manifest),
15
+ }));
16
+
17
+ vi.mock("./resolve-catalog-dependencies", () => ({
18
+ resolveCatalogDependencies: vi.fn((deps) => Promise.resolve(deps)),
19
+ }));
20
+
21
+ const { usePackageManager } = vi.mocked(await import("~/lib/package-manager"));
22
+
23
+ const { writeManifest } = vi.mocked(await import("../io"));
24
+
25
+ const { adaptInternalPackageManifests } =
26
+ await import("./adapt-internal-package-manifests");
27
+
28
+ describe("adaptInternalPackageManifests", () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ usePackageManager.mockReturnValue({
32
+ name: "pnpm",
33
+ version: "9.0.0",
34
+ majorVersion: 9,
35
+ });
36
+ });
37
+
38
+ function createRegistry(
39
+ entries: Record<
40
+ string,
41
+ { rootRelativeDir: string; manifest: PackageManifest }
42
+ >,
43
+ ): PackagesRegistry {
44
+ const registry: PackagesRegistry = {};
45
+ for (const [name, { rootRelativeDir, manifest }] of Object.entries(
46
+ entries,
47
+ )) {
48
+ registry[name] = {
49
+ absoluteDir: `/workspace/${rootRelativeDir}`,
50
+ rootRelativeDir,
51
+ manifest,
52
+ };
53
+ }
54
+ return registry;
55
+ }
56
+
57
+ it("should preserve scripts in internal dependency manifests", async () => {
58
+ const manifest: PackageManifest = {
59
+ name: "@repo/database",
60
+ version: "1.0.0",
61
+ scripts: {
62
+ postinstall: "prisma generate",
63
+ build: "tsc",
64
+ },
65
+ dependencies: {
66
+ prisma: "^5.0.0",
67
+ },
68
+ devDependencies: {
69
+ typescript: "^5.0.0",
70
+ },
71
+ };
72
+
73
+ const packagesRegistry = createRegistry({
74
+ "@repo/database": {
75
+ rootRelativeDir: "packages/database",
76
+ manifest,
77
+ },
78
+ });
79
+
80
+ await adaptInternalPackageManifests({
81
+ internalPackageNames: ["@repo/database"],
82
+ packagesRegistry,
83
+ isolateDir: "/output",
84
+ forceNpm: false,
85
+ workspaceRootDir: "/workspace",
86
+ });
87
+
88
+ expect(writeManifest).toHaveBeenCalledOnce();
89
+
90
+ const writtenManifest = writeManifest.mock.calls[0]![1];
91
+
92
+ expect(writtenManifest.scripts).toEqual({
93
+ postinstall: "prisma generate",
94
+ build: "tsc",
95
+ });
96
+ });
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
+
147
+ it("should strip devDependencies from internal dependency manifests", async () => {
148
+ const manifest: PackageManifest = {
149
+ name: "@repo/shared",
150
+ version: "1.0.0",
151
+ dependencies: {
152
+ lodash: "^4.0.0",
153
+ },
154
+ devDependencies: {
155
+ vitest: "^1.0.0",
156
+ typescript: "^5.0.0",
157
+ },
158
+ };
159
+
160
+ const packagesRegistry = createRegistry({
161
+ "@repo/shared": {
162
+ rootRelativeDir: "packages/shared",
163
+ manifest,
164
+ },
165
+ });
166
+
167
+ await adaptInternalPackageManifests({
168
+ internalPackageNames: ["@repo/shared"],
169
+ packagesRegistry,
170
+ isolateDir: "/output",
171
+ forceNpm: false,
172
+ workspaceRootDir: "/workspace",
173
+ });
174
+
175
+ expect(writeManifest).toHaveBeenCalledOnce();
176
+
177
+ const writtenManifest = writeManifest.mock.calls[0]![1];
178
+
179
+ expect(writtenManifest.devDependencies).toBeUndefined();
180
+ });
181
+ });
@@ -31,8 +31,19 @@ export async function adaptInternalPackageManifests({
31
31
  internalPackageNames.map(async (packageName) => {
32
32
  const { manifest, rootRelativeDir } = got(packagesRegistry, packageName);
33
33
 
34
- /** Dev dependencies and scripts are never included for internal deps */
35
- const strippedManifest = omit(manifest, ["scripts", "devDependencies"]);
34
+ /** Dev dependencies are never included for internal deps */
35
+ const strippedManifest = omit(manifest, ["devDependencies"]);
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
+ }
36
47
 
37
48
  /** Resolve catalog dependencies before adapting internal deps */
38
49
  const manifestWithResolvedCatalogs = {
@@ -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
 
@@ -43,7 +43,7 @@ describe("validateManifestMandatoryFields", () => {
43
43
 
44
44
  expect(() =>
45
45
  validateManifestMandatoryFields(invalidDevManifest, packagePath, false),
46
- ).toThrow(/missing mandatory fields: version/);
46
+ ).toThrow(/missing the "version" field/);
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 mandatory fields: version/);
57
+ ).toThrow(/missing the "version" field/);
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 mandatory fields: files/);
68
+ ).toThrow(/missing the "files" field/);
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 mandatory fields: files/);
80
+ ).toThrow(/missing the "files" field/);
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 mandatory fields: files/);
92
+ ).toThrow(/missing the "files" field/);
93
93
  });
94
94
 
95
95
  it("should throw error when both fields are missing", () => {
@@ -99,20 +99,29 @@ describe("validateManifestMandatoryFields", () => {
99
99
 
100
100
  expect(() =>
101
101
  validateManifestMandatoryFields(invalidManifest, packagePath),
102
- ).toThrow(/missing mandatory fields: version, files/);
102
+ ).toThrow(/missing mandatory fields in its package\.json: version, files/);
103
103
  });
104
104
 
105
- it("should include helpful error message", () => {
105
+ it("should include documentation URL in 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(/missing mandatory fields: version, files/);
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;
113
120
 
114
121
  expect(() =>
115
- validateManifestMandatoryFields(invalidManifest, packagePath),
116
- ).toThrow(/See the documentation for more details/);
122
+ validateManifestMandatoryFields(singleFieldManifest, packagePath),
123
+ ).toThrow(
124
+ /See https:\/\/isolate-package\.codecompose\.dev\/getting-started#define-files-field-in-each-package-manifest/,
125
+ );
117
126
  });
118
127
  });
@@ -1,6 +1,14 @@
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
+
4
12
  /**
5
13
  * Validate that mandatory fields are present in the package manifest. These
6
14
  * fields are required for the isolate process to work properly.
@@ -38,7 +46,11 @@ export function validateManifestMandatoryFields(
38
46
  }
39
47
 
40
48
  if (missingFields.length > 0) {
41
- const errorMessage = `Package at ${packagePath} is missing mandatory fields: ${missingFields.join(", ")}. See the documentation for more details.`;
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`;
42
54
 
43
55
  log.error(errorMessage);
44
56
  throw new Error(errorMessage);