isolate-package 1.27.0 → 1.28.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.
Files changed (49) hide show
  1. package/README.md +20 -420
  2. package/dist/index.d.mts +13 -5
  3. package/dist/index.mjs +21 -2
  4. package/dist/index.mjs.map +1 -0
  5. package/dist/{isolate-B11ztsrI.mjs → isolate-D-Qd5BJJ.mjs} +114 -25
  6. package/dist/isolate-D-Qd5BJJ.mjs.map +1 -0
  7. package/dist/isolate-bin.mjs +1 -1
  8. package/dist/isolate-bin.mjs.map +1 -1
  9. package/package.json +37 -32
  10. package/src/get-internal-package-names.test.ts +213 -0
  11. package/src/get-internal-package-names.ts +38 -0
  12. package/src/index.ts +3 -5
  13. package/src/isolate-bin.ts +1 -1
  14. package/src/isolate.ts +21 -31
  15. package/src/lib/cli.test.ts +3 -3
  16. package/src/lib/cli.ts +4 -4
  17. package/src/lib/config.test.ts +163 -0
  18. package/src/lib/config.ts +134 -8
  19. package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +6 -6
  20. package/src/lib/lockfile/helpers/pnpm-map-importer.ts +5 -5
  21. package/src/lib/lockfile/process-lockfile.ts +3 -3
  22. package/src/lib/manifest/adapt-target-package-manifest.ts +2 -2
  23. package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +3 -3
  24. package/src/lib/manifest/helpers/adapt-manifest-internal-deps.ts +2 -2
  25. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +1 -1
  26. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +2 -2
  27. package/src/lib/manifest/helpers/patch-internal-entries.ts +2 -2
  28. package/src/lib/manifest/helpers/resolve-catalog-dependencies.ts +3 -3
  29. package/src/lib/manifest/io.ts +2 -2
  30. package/src/lib/manifest/validate-manifest.test.ts +10 -10
  31. package/src/lib/manifest/validate-manifest.ts +1 -1
  32. package/src/lib/output/unpack-dependencies.ts +4 -4
  33. package/src/lib/package-manager/helpers/infer-from-files.ts +1 -1
  34. package/src/lib/package-manager/helpers/infer-from-manifest.ts +3 -3
  35. package/src/lib/package-manager/index.ts +2 -2
  36. package/src/lib/patches/copy-patches.test.ts +7 -7
  37. package/src/lib/patches/copy-patches.ts +5 -5
  38. package/src/lib/registry/create-packages-registry.ts +12 -14
  39. package/src/lib/registry/helpers/find-packages-globs.ts +7 -7
  40. package/src/lib/registry/list-internal-packages.test.ts +291 -0
  41. package/src/lib/registry/list-internal-packages.ts +70 -18
  42. package/src/lib/utils/filter-object-undefined.test.ts +1 -1
  43. package/src/lib/utils/filter-object-undefined.ts +1 -1
  44. package/src/lib/utils/filter-patched-dependencies.ts +2 -2
  45. package/src/lib/utils/json.ts +4 -4
  46. package/src/lib/utils/pack.ts +3 -3
  47. package/src/lib/utils/yaml.ts +1 -1
  48. package/dist/isolate-B11ztsrI.mjs.map +0 -1
  49. package/docs/firebase.md +0 -144
@@ -0,0 +1,213 @@
1
+ import fs from "fs-extra";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { getInternalPackageNames } from "./get-internal-package-names";
6
+
7
+ vi.mock("~/lib/logger", () => ({
8
+ useLogger: () => ({
9
+ debug: vi.fn(),
10
+ info: vi.fn(),
11
+ warn: vi.fn(),
12
+ error: vi.fn(),
13
+ }),
14
+ setLogLevel: vi.fn(),
15
+ }));
16
+
17
+ const packageManagerResult = {
18
+ name: "pnpm",
19
+ version: "9.0.0",
20
+ majorVersion: 9,
21
+ };
22
+
23
+ const mockDetectPackageManager = vi.fn(
24
+ (_workspaceRootDir: string) => packageManagerResult,
25
+ );
26
+
27
+ vi.mock("~/lib/package-manager", () => ({
28
+ usePackageManager: () => packageManagerResult,
29
+ detectPackageManager: (workspaceRootDir: string) =>
30
+ mockDetectPackageManager(workspaceRootDir),
31
+ }));
32
+
33
+ /**
34
+ * Sets up a minimal workspace file structure with a target package and
35
+ * workspace packages so that getInternalPackageNames can resolve them.
36
+ */
37
+ async function createWorkspace(
38
+ rootDir: string,
39
+ {
40
+ targetDeps = {} as Record<string, string>,
41
+ targetDevDeps = {} as Record<string, string>,
42
+ packages = [] as {
43
+ name: string;
44
+ dir: string;
45
+ deps?: Record<string, string>;
46
+ }[],
47
+ },
48
+ ) {
49
+ const packagesDir = path.join(rootDir, "packages");
50
+ const targetDir = path.join(packagesDir, "target");
51
+
52
+ /** Write pnpm-workspace.yaml so the registry can find packages */
53
+ await fs.writeFile(
54
+ path.join(rootDir, "pnpm-workspace.yaml"),
55
+ "packages:\n - packages/*\n",
56
+ );
57
+
58
+ /** Write a pnpm lockfile so package manager detection works */
59
+ await fs.writeFile(
60
+ path.join(rootDir, "pnpm-lock.yaml"),
61
+ "lockfileVersion: '9.0'\n",
62
+ );
63
+
64
+ /** Write target package manifest */
65
+ await fs.ensureDir(targetDir);
66
+ await fs.writeJson(path.join(targetDir, "package.json"), {
67
+ name: "@test/target",
68
+ version: "0.0.0",
69
+ dependencies: targetDeps,
70
+ devDependencies: targetDevDeps,
71
+ });
72
+
73
+ /** Write workspace package manifests */
74
+ for (const pkg of packages) {
75
+ const pkgDir = path.join(packagesDir, pkg.dir);
76
+ await fs.ensureDir(pkgDir);
77
+ await fs.writeJson(path.join(pkgDir, "package.json"), {
78
+ name: pkg.name,
79
+ version: "0.0.0",
80
+ dependencies: pkg.deps ?? {},
81
+ });
82
+ }
83
+
84
+ return targetDir;
85
+ }
86
+
87
+ describe("getInternalPackageNames", () => {
88
+ let tempDir: string;
89
+ let originalCwd: string;
90
+
91
+ beforeEach(async () => {
92
+ vi.clearAllMocks();
93
+ originalCwd = process.cwd();
94
+ tempDir = await fs.mkdtemp(
95
+ path.join(os.tmpdir(), "isolate-get-internal-test-"),
96
+ );
97
+ });
98
+
99
+ afterEach(async () => {
100
+ process.chdir(originalCwd);
101
+ await fs.remove(tempDir);
102
+ });
103
+
104
+ it("returns internal package names from the target manifest", async () => {
105
+ const targetDir = await createWorkspace(tempDir, {
106
+ targetDeps: { "@test/shared": "0.0.0", lodash: "^4.0.0" },
107
+ packages: [{ name: "@test/shared", dir: "shared" }],
108
+ });
109
+
110
+ process.chdir(targetDir);
111
+
112
+ const result = await getInternalPackageNames({ workspaceRoot: "../.." });
113
+ expect(result).toEqual(["@test/shared"]);
114
+ expect(mockDetectPackageManager).toHaveBeenCalled();
115
+ });
116
+
117
+ it("excludes devDependencies by default", async () => {
118
+ const targetDir = await createWorkspace(tempDir, {
119
+ targetDeps: { "@test/shared": "0.0.0" },
120
+ targetDevDeps: { "@test/dev-tool": "0.0.0" },
121
+ packages: [
122
+ { name: "@test/shared", dir: "shared" },
123
+ { name: "@test/dev-tool", dir: "dev-tool" },
124
+ ],
125
+ });
126
+
127
+ process.chdir(targetDir);
128
+
129
+ const result = await getInternalPackageNames({ workspaceRoot: "../.." });
130
+ expect(result).toEqual(["@test/shared"]);
131
+ });
132
+
133
+ it("includes devDependencies when configured", async () => {
134
+ const targetDir = await createWorkspace(tempDir, {
135
+ targetDeps: { "@test/shared": "0.0.0" },
136
+ targetDevDeps: { "@test/dev-tool": "0.0.0" },
137
+ packages: [
138
+ { name: "@test/shared", dir: "shared" },
139
+ { name: "@test/dev-tool", dir: "dev-tool" },
140
+ ],
141
+ });
142
+
143
+ process.chdir(targetDir);
144
+
145
+ const result = await getInternalPackageNames({
146
+ workspaceRoot: "../..",
147
+ includeDevDependencies: true,
148
+ });
149
+ expect(result).toEqual(
150
+ expect.arrayContaining(["@test/shared", "@test/dev-tool"]),
151
+ );
152
+ expect(result).toHaveLength(2);
153
+ });
154
+
155
+ it("resolves recursive internal dependencies", async () => {
156
+ const targetDir = await createWorkspace(tempDir, {
157
+ targetDeps: { "@test/app-utils": "0.0.0" },
158
+ packages: [
159
+ {
160
+ name: "@test/app-utils",
161
+ dir: "app-utils",
162
+ deps: { "@test/core": "0.0.0" },
163
+ },
164
+ { name: "@test/core", dir: "core" },
165
+ ],
166
+ });
167
+
168
+ process.chdir(targetDir);
169
+
170
+ const result = await getInternalPackageNames({ workspaceRoot: "../.." });
171
+ expect(result).toEqual(
172
+ expect.arrayContaining(["@test/app-utils", "@test/core"]),
173
+ );
174
+ expect(result).toHaveLength(2);
175
+ });
176
+
177
+ it("returns an empty array when there are no internal dependencies", async () => {
178
+ const targetDir = await createWorkspace(tempDir, {
179
+ targetDeps: { lodash: "^4.0.0", express: "^4.0.0" },
180
+ packages: [{ name: "@test/unrelated", dir: "unrelated" }],
181
+ });
182
+
183
+ process.chdir(targetDir);
184
+
185
+ const result = await getInternalPackageNames({ workspaceRoot: "../.." });
186
+ expect(result).toEqual([]);
187
+ });
188
+
189
+ it("loads config from isolate.config.json when no config is passed", async () => {
190
+ const targetDir = await createWorkspace(tempDir, {
191
+ targetDeps: { "@test/shared": "0.0.0" },
192
+ targetDevDeps: { "@test/dev-tool": "0.0.0" },
193
+ packages: [
194
+ { name: "@test/shared", dir: "shared" },
195
+ { name: "@test/dev-tool", dir: "dev-tool" },
196
+ ],
197
+ });
198
+
199
+ /** Write config file with includeDevDependencies enabled */
200
+ await fs.writeJson(path.join(targetDir, "isolate.config.json"), {
201
+ workspaceRoot: "../..",
202
+ includeDevDependencies: true,
203
+ });
204
+
205
+ process.chdir(targetDir);
206
+
207
+ const result = await getInternalPackageNames();
208
+ expect(result).toEqual(
209
+ expect.arrayContaining(["@test/shared", "@test/dev-tool"]),
210
+ );
211
+ expect(result).toHaveLength(2);
212
+ });
213
+ });
@@ -0,0 +1,38 @@
1
+ import path from "node:path";
2
+ import type { IsolateConfig } from "./lib/config";
3
+ import { resolveConfig, resolveWorkspacePaths } from "./lib/config";
4
+ import { detectPackageManager } from "./lib/package-manager";
5
+ import { createPackagesRegistry, listInternalPackages } from "./lib/registry";
6
+ import type { PackageManifest } from "./lib/types";
7
+ import { readTypedJson } from "./lib/utils";
8
+
9
+ /**
10
+ * Get the names of all internal workspace packages that the target package
11
+ * depends on. This is useful for tools like tsup that need a list of internal
12
+ * packages to include in `noExternal`.
13
+ *
14
+ * If no config is passed, it reads from `isolate.config.{ts,js,json}` in the
15
+ * current working directory.
16
+ */
17
+ export async function getInternalPackageNames(
18
+ config?: IsolateConfig,
19
+ ): Promise<string[]> {
20
+ const resolvedConfig = resolveConfig(config);
21
+ const { targetPackageDir, workspaceRootDir } =
22
+ resolveWorkspacePaths(resolvedConfig);
23
+
24
+ detectPackageManager(workspaceRootDir);
25
+
26
+ const targetPackageManifest = await readTypedJson<PackageManifest>(
27
+ path.join(targetPackageDir, "package.json"),
28
+ );
29
+
30
+ const packagesRegistry = await createPackagesRegistry(
31
+ workspaceRootDir,
32
+ resolvedConfig.workspacePackages,
33
+ );
34
+
35
+ return listInternalPackages(targetPackageManifest, packagesRegistry, {
36
+ includeDevDependencies: resolvedConfig.includeDevDependencies,
37
+ });
38
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,5 @@
1
- import type { isolate } from "./isolate";
2
1
  export { isolate } from "./isolate";
2
+ export { getInternalPackageNames } from "./get-internal-package-names";
3
+ export { defineConfig } from "./lib/config";
4
+ export type { IsolateConfig } from "./lib/config";
3
5
  export type { Logger } from "./lib/logger";
4
-
5
- export type IsolateExports = {
6
- isolate: typeof isolate;
7
- };
@@ -87,7 +87,7 @@ const cli = meow(
87
87
  type: "boolean",
88
88
  },
89
89
  },
90
- }
90
+ },
91
91
  );
92
92
 
93
93
  async function run() {
package/src/isolate.ts CHANGED
@@ -4,7 +4,7 @@ import assert from "node:assert";
4
4
  import path from "node:path";
5
5
  import { unique } from "remeda";
6
6
  import type { IsolateConfig } from "./lib/config";
7
- import { resolveConfig } from "./lib/config";
7
+ import { resolveConfig, resolveWorkspacePaths } from "./lib/config";
8
8
  import { processLockfile } from "./lib/lockfile";
9
9
  import { setLogLevel, useLogger } from "./lib/logger";
10
10
  import {
@@ -44,23 +44,13 @@ export function createIsolator(config?: IsolateConfig) {
44
44
  const log = useLogger();
45
45
 
46
46
  const { version: libraryVersion } = await readTypedJson<PackageManifest>(
47
- path.join(path.join(__dirname, "..", "package.json"))
47
+ path.join(path.join(__dirname, "..", "package.json")),
48
48
  );
49
49
 
50
50
  log.debug("Using isolate-package version", libraryVersion);
51
51
 
52
- /**
53
- * If a targetPackagePath is set, we assume the configuration lives in the
54
- * root of the workspace. If targetPackagePath is undefined (the default),
55
- * we assume that the configuration lives in the target package directory.
56
- */
57
- const targetPackageDir = config.targetPackagePath
58
- ? path.join(process.cwd(), config.targetPackagePath)
59
- : process.cwd();
60
-
61
- const workspaceRootDir = config.targetPackagePath
62
- ? process.cwd()
63
- : path.join(targetPackageDir, config.workspaceRoot);
52
+ const { targetPackageDir, workspaceRootDir } =
53
+ resolveWorkspacePaths(config);
64
54
 
65
55
  const buildOutputDir = await getBuildOutputDir({
66
56
  targetPackageDir,
@@ -70,20 +60,20 @@ export function createIsolator(config?: IsolateConfig) {
70
60
 
71
61
  assert(
72
62
  fs.existsSync(buildOutputDir),
73
- `Failed to find build output path at ${buildOutputDir}. Please make sure you build the source before isolating it.`
63
+ `Failed to find build output path at ${buildOutputDir}. Please make sure you build the source before isolating it.`,
74
64
  );
75
65
 
76
66
  log.debug("Workspace root resolved to", workspaceRootDir);
77
67
  log.debug(
78
68
  "Isolate target package",
79
- getRootRelativeLogPath(targetPackageDir, workspaceRootDir)
69
+ getRootRelativeLogPath(targetPackageDir, workspaceRootDir),
80
70
  );
81
71
 
82
72
  const isolateDir = path.join(targetPackageDir, config.isolateDirName);
83
73
 
84
74
  log.debug(
85
75
  "Isolate output directory",
86
- getRootRelativeLogPath(isolateDir, workspaceRootDir)
76
+ getRootRelativeLogPath(isolateDir, workspaceRootDir),
87
77
  );
88
78
 
89
79
  if (fs.existsSync(isolateDir)) {
@@ -97,13 +87,13 @@ export function createIsolator(config?: IsolateConfig) {
97
87
  await fs.ensureDir(tmpDir);
98
88
 
99
89
  const targetPackageManifest = await readTypedJson<PackageManifest>(
100
- path.join(targetPackageDir, "package.json")
90
+ path.join(targetPackageDir, "package.json"),
101
91
  );
102
92
 
103
93
  /** Validate mandatory fields for the target package */
104
94
  validateManifestMandatoryFields(
105
95
  targetPackageManifest,
106
- getRootRelativeLogPath(targetPackageDir, workspaceRootDir)
96
+ getRootRelativeLogPath(targetPackageDir, workspaceRootDir),
107
97
  );
108
98
 
109
99
  const packageManager = detectPackageManager(workspaceRootDir);
@@ -111,7 +101,7 @@ export function createIsolator(config?: IsolateConfig) {
111
101
  log.debug(
112
102
  "Detected package manager",
113
103
  packageManager.name,
114
- packageManager.version
104
+ packageManager.version,
115
105
  );
116
106
 
117
107
  if (shouldUsePnpmPack()) {
@@ -124,7 +114,7 @@ export function createIsolator(config?: IsolateConfig) {
124
114
  */
125
115
  const packagesRegistry = await createPackagesRegistry(
126
116
  workspaceRootDir,
127
- config.workspacePackages
117
+ config.workspacePackages,
128
118
  );
129
119
 
130
120
  const internalPackageNames = listInternalPackages(
@@ -132,7 +122,7 @@ export function createIsolator(config?: IsolateConfig) {
132
122
  packagesRegistry,
133
123
  {
134
124
  includeDevDependencies: config.includeDevDependencies,
135
- }
125
+ },
136
126
  );
137
127
 
138
128
  /**
@@ -144,7 +134,7 @@ export function createIsolator(config?: IsolateConfig) {
144
134
  packagesRegistry,
145
135
  {
146
136
  includeDevDependencies: false,
147
- }
137
+ },
148
138
  );
149
139
 
150
140
  /** Validate mandatory fields for all internal packages that will be isolated */
@@ -155,7 +145,7 @@ export function createIsolator(config?: IsolateConfig) {
155
145
  validateManifestMandatoryFields(
156
146
  packageDef.manifest,
157
147
  getRootRelativeLogPath(packageDef.absoluteDir, workspaceRootDir),
158
- isProductionDependency
148
+ isProductionDependency,
159
149
  );
160
150
  }
161
151
 
@@ -169,7 +159,7 @@ export function createIsolator(config?: IsolateConfig) {
169
159
  packedFilesByName,
170
160
  packagesRegistry,
171
161
  tmpDir,
172
- isolateDir
162
+ isolateDir,
173
163
  );
174
164
 
175
165
  /** Adapt the manifest files for all the unpacked local dependencies */
@@ -250,10 +240,10 @@ export function createIsolator(config?: IsolateConfig) {
250
240
  Object.entries(copiedPatches).map(([spec, patchFile]) => [
251
241
  spec,
252
242
  patchFile.path,
253
- ])
243
+ ]),
254
244
  );
255
245
  log.debug(
256
- `Added ${Object.keys(copiedPatches).length} patches to isolated package.json`
246
+ `Added ${Object.keys(copiedPatches).length} patches to isolated package.json`,
257
247
  );
258
248
  }
259
249
 
@@ -281,8 +271,8 @@ export function createIsolator(config?: IsolateConfig) {
281
271
  const packagesFolderNames = unique(
282
272
  internalPackageNames.map(
283
273
  (name) =>
284
- path.parse(got(packagesRegistry, name).rootRelativeDir).dir
285
- )
274
+ path.parse(got(packagesRegistry, name).rootRelativeDir).dir,
275
+ ),
286
276
  );
287
277
 
288
278
  log.debug("Generating pnpm-workspace.yaml for Rush workspace");
@@ -296,7 +286,7 @@ export function createIsolator(config?: IsolateConfig) {
296
286
  } else {
297
287
  fs.copyFileSync(
298
288
  path.join(workspaceRootDir, "pnpm-workspace.yaml"),
299
- path.join(isolateDir, "pnpm-workspace.yaml")
289
+ path.join(isolateDir, "pnpm-workspace.yaml"),
300
290
  );
301
291
  }
302
292
  }
@@ -321,7 +311,7 @@ export function createIsolator(config?: IsolateConfig) {
321
311
  */
322
312
  log.debug(
323
313
  "Deleting temp directory",
324
- getRootRelativeLogPath(tmpDir, workspaceRootDir)
314
+ getRootRelativeLogPath(tmpDir, workspaceRootDir),
325
315
  );
326
316
  await fs.remove(tmpDir);
327
317
 
@@ -36,7 +36,7 @@ describe("wasFlagExplicitlyPassed", () => {
36
36
  it("detects a short flag", () => {
37
37
  const argv = ["node", "isolate", "-d"];
38
38
  expect(wasFlagExplicitlyPassed("includeDevDependencies", argv, "d")).toBe(
39
- true
39
+ true,
40
40
  );
41
41
  });
42
42
 
@@ -70,7 +70,7 @@ describe("parseLogLevel", () => {
70
70
 
71
71
  it("throws for an invalid log level", () => {
72
72
  expect(() => parseLogLevel("verbose")).toThrow(
73
- 'Invalid log level: "verbose"'
73
+ 'Invalid log level: "verbose"',
74
74
  );
75
75
  });
76
76
  });
@@ -168,7 +168,7 @@ describe("buildCliOverrides", () => {
168
168
  const flags: ParsedFlags = { ...defaultFlags, logLevel: "verbose" };
169
169
  const argv = ["node", "isolate", "--log-level", "verbose"];
170
170
  expect(() => buildCliOverrides(flags, argv)).toThrow(
171
- 'Invalid log level: "verbose"'
171
+ 'Invalid log level: "verbose"',
172
172
  );
173
173
  });
174
174
 
package/src/lib/cli.ts CHANGED
@@ -12,14 +12,14 @@ const validLogLevels: readonly LogLevel[] = ["info", "debug", "warn", "error"];
12
12
  export function wasFlagExplicitlyPassed(
13
13
  flagName: string,
14
14
  argv: string[],
15
- shortFlag?: string
15
+ shortFlag?: string,
16
16
  ): boolean {
17
17
  const kebab = flagName.replace(/[A-Z]/g, (l) => `-${l.toLowerCase()}`);
18
18
  return argv.some(
19
19
  (arg) =>
20
20
  arg === `--${kebab}` ||
21
21
  arg === `--no-${kebab}` ||
22
- (shortFlag !== undefined && arg === `-${shortFlag}`)
22
+ (shortFlag !== undefined && arg === `-${shortFlag}`),
23
23
  );
24
24
  }
25
25
 
@@ -34,7 +34,7 @@ export function parseLogLevel(value: string | undefined): LogLevel | undefined {
34
34
 
35
35
  if (!validLogLevels.includes(value as LogLevel)) {
36
36
  throw new Error(
37
- `Invalid log level: "${value}". Must be one of: ${validLogLevels.join(", ")}`
37
+ `Invalid log level: "${value}". Must be one of: ${validLogLevels.join(", ")}`,
38
38
  );
39
39
  }
40
40
 
@@ -64,7 +64,7 @@ export type ParsedFlags = {
64
64
  */
65
65
  export function buildCliOverrides(
66
66
  flags: ParsedFlags,
67
- argv: string[]
67
+ argv: string[],
68
68
  ): IsolateConfig {
69
69
  const logLevel = parseLogLevel(flags.logLevel);
70
70
 
@@ -0,0 +1,163 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { defineConfig, loadConfigFromFile } from "./config";
6
+
7
+ /** Shared mock logger instance so assertions can check calls. */
8
+ const mockLogger = {
9
+ debug: vi.fn(),
10
+ info: vi.fn(),
11
+ warn: vi.fn(),
12
+ error: vi.fn(),
13
+ };
14
+
15
+ vi.mock("~/lib/logger", () => ({
16
+ useLogger: () => mockLogger,
17
+ }));
18
+
19
+ describe("loadConfigFromFile", () => {
20
+ let tempDir: string;
21
+ let originalCwd: string;
22
+
23
+ beforeEach(async () => {
24
+ vi.clearAllMocks();
25
+ originalCwd = process.cwd();
26
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "isolate-config-test-"));
27
+ process.chdir(tempDir);
28
+ });
29
+
30
+ afterEach(async () => {
31
+ process.chdir(originalCwd);
32
+ await fs.remove(tempDir);
33
+ });
34
+
35
+ it("returns empty object when no config file exists", () => {
36
+ const config = loadConfigFromFile();
37
+ expect(config).toEqual({});
38
+ });
39
+
40
+ it("loads a JSON config file", async () => {
41
+ await fs.writeJson(path.join(tempDir, "isolate.config.json"), {
42
+ isolateDirName: "output",
43
+ workspaceRoot: "../../..",
44
+ });
45
+
46
+ const config = loadConfigFromFile();
47
+ expect(config).toEqual({
48
+ isolateDirName: "output",
49
+ workspaceRoot: "../../..",
50
+ });
51
+ });
52
+
53
+ it("loads a TypeScript config file", async () => {
54
+ await fs.writeFile(
55
+ path.join(tempDir, "isolate.config.ts"),
56
+ `export default { isolateDirName: "from-ts", workspaceRoot: "../.." };`,
57
+ );
58
+
59
+ const config = loadConfigFromFile();
60
+ expect(config).toEqual({
61
+ isolateDirName: "from-ts",
62
+ workspaceRoot: "../..",
63
+ });
64
+ });
65
+
66
+ it("loads a TypeScript config file that uses defineConfig", async () => {
67
+ /**
68
+ * The subprocess can't import from "isolate-package" since it's not
69
+ * installed in the temp dir, so we inline the defineConfig identity
70
+ * function to verify the pattern works end-to-end.
71
+ */
72
+ await fs.writeFile(
73
+ path.join(tempDir, "isolate.config.ts"),
74
+ [
75
+ `const defineConfig = (c: Record<string, unknown>) => c;`,
76
+ `export default defineConfig({ isolateDirName: "defined" });`,
77
+ ].join("\n"),
78
+ );
79
+
80
+ const config = loadConfigFromFile();
81
+ expect(config).toEqual({ isolateDirName: "defined" });
82
+ });
83
+
84
+ it("loads a JavaScript config file", async () => {
85
+ await fs.writeFile(
86
+ path.join(tempDir, "isolate.config.js"),
87
+ `export default { isolateDirName: "from-js", workspaceRoot: "../.." };`,
88
+ );
89
+
90
+ const config = loadConfigFromFile();
91
+ expect(config).toEqual({
92
+ isolateDirName: "from-js",
93
+ workspaceRoot: "../..",
94
+ });
95
+ });
96
+
97
+ it("prefers TypeScript config and warns when multiple exist", async () => {
98
+ await fs.writeJson(path.join(tempDir, "isolate.config.json"), {
99
+ isolateDirName: "from-json",
100
+ });
101
+ await fs.writeFile(
102
+ path.join(tempDir, "isolate.config.ts"),
103
+ `export default { isolateDirName: "from-ts" };`,
104
+ );
105
+
106
+ const config = loadConfigFromFile();
107
+ expect(config).toEqual({ isolateDirName: "from-ts" });
108
+ expect(mockLogger.warn).toHaveBeenCalledWith(
109
+ expect.stringContaining("Found multiple config files"),
110
+ );
111
+ });
112
+
113
+ it("prefers JavaScript config over JSON", async () => {
114
+ await fs.writeJson(path.join(tempDir, "isolate.config.json"), {
115
+ isolateDirName: "from-json",
116
+ });
117
+ await fs.writeFile(
118
+ path.join(tempDir, "isolate.config.js"),
119
+ `export default { isolateDirName: "from-js" };`,
120
+ );
121
+
122
+ const config = loadConfigFromFile();
123
+ expect(config).toEqual({ isolateDirName: "from-js" });
124
+ expect(mockLogger.warn).toHaveBeenCalledWith(
125
+ expect.stringContaining("Found multiple config files"),
126
+ );
127
+ });
128
+
129
+ it("throws when the config file has no default export", async () => {
130
+ await fs.writeFile(
131
+ path.join(tempDir, "isolate.config.ts"),
132
+ `export const config = { isolateDirName: "oops" };`,
133
+ );
134
+
135
+ expect(() => loadConfigFromFile()).toThrow("Failed to load config from");
136
+ });
137
+
138
+ it("throws when the default export is not an object", async () => {
139
+ await fs.writeFile(
140
+ path.join(tempDir, "isolate.config.ts"),
141
+ `export default "not an object";`,
142
+ );
143
+
144
+ expect(() => loadConfigFromFile()).toThrow("Failed to load config from");
145
+ });
146
+
147
+ it("throws when the TypeScript file has a syntax error", async () => {
148
+ await fs.writeFile(
149
+ path.join(tempDir, "isolate.config.ts"),
150
+ `export default {{{`,
151
+ );
152
+
153
+ expect(() => loadConfigFromFile()).toThrow("Failed to load config from");
154
+ });
155
+ });
156
+
157
+ describe("defineConfig", () => {
158
+ it("returns the config object unchanged", () => {
159
+ const input = { isolateDirName: "output", workspaceRoot: "../.." };
160
+ const result = defineConfig(input);
161
+ expect(result).toBe(input);
162
+ });
163
+ });