@rexeus/typeweaver 0.10.3 → 0.10.5

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.
@@ -7,42 +7,83 @@ import { createPluginContextBuilder, createPluginRegistry, normalizeSpec, render
7
7
  import { TypesPlugin } from "@rexeus/typeweaver-types";
8
8
  import os from "node:os";
9
9
  import { build } from "rolldown";
10
+ import { createHash } from "node:crypto";
10
11
  import { HttpMethod, HttpStatusCode } from "@rexeus/typeweaver-core";
12
+ //#region src/errors/InvalidConfigExportError.ts
13
+ var InvalidConfigExportError = class extends Error {
14
+ name = "InvalidConfigExportError";
15
+ constructor(configPath, reason) {
16
+ super(getInvalidConfigExportMessage(configPath, reason));
17
+ this.configPath = configPath;
18
+ this.reason = reason;
19
+ }
20
+ };
21
+ const getInvalidConfigExportMessage = (configPath, reason) => {
22
+ switch (reason) {
23
+ case "both-default-and-named-config": return `Configuration file '${configPath}' must choose one export style: use either 'export default' or 'export const config = ...', but not both.`;
24
+ case "default-namespace-wrapper": return `Configuration file '${configPath}' default export must be the config object itself, not a module namespace-like wrapper. Export the config directly with 'export default { ... }' or use 'export const config = ...'.`;
25
+ case "missing-config-export": return `Configuration file '${configPath}' must export its config via 'export default' or 'export const config = ...'.`;
26
+ case "non-object-config": return `Configuration file '${configPath}' must export a config object via 'export default' or 'export const config = ...'.`;
27
+ }
28
+ };
29
+ //#endregion
30
+ //#region src/errors/UnsupportedConfigExtensionError.ts
31
+ var UnsupportedConfigExtensionError = class extends Error {
32
+ name = "UnsupportedConfigExtensionError";
33
+ constructor(configPath, extension, supportedExtensions) {
34
+ super(`Unsupported config file extension '${extension}' for '${configPath}'. TypeWeaver accepts only these config extensions: ${supportedExtensions.join(", ")}.`);
35
+ this.configPath = configPath;
36
+ this.extension = extension;
37
+ this.supportedExtensions = supportedExtensions;
38
+ }
39
+ };
40
+ //#endregion
41
+ //#region src/errors/UnsupportedTypeScriptConfigError.ts
42
+ var UnsupportedTypeScriptConfigError = class extends Error {
43
+ name = "UnsupportedTypeScriptConfigError";
44
+ constructor(configPath, extension) {
45
+ super(`TypeScript config files are not supported: '${configPath}' uses '${extension}'. Use a JavaScript config file with one of these extensions: .js, .mjs, or .cjs.`);
46
+ this.configPath = configPath;
47
+ this.extension = extension;
48
+ }
49
+ };
50
+ //#endregion
11
51
  //#region src/configLoader.ts
12
- const SUPPORTED_CONFIG_EXTENSIONS = new Set([
52
+ const SUPPORTED_CONFIG_EXTENSIONS = [
13
53
  ".js",
14
54
  ".mjs",
15
55
  ".cjs"
16
- ]);
56
+ ];
57
+ const SUPPORTED_CONFIG_EXTENSION_SET = new Set(SUPPORTED_CONFIG_EXTENSIONS);
17
58
  const UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS = new Set([
18
59
  ".ts",
19
60
  ".mts",
20
61
  ".cts"
21
62
  ]);
22
63
  const getResolvedConfigPath = (configPath, currentWorkingDirectory = process.cwd()) => {
23
- return path.isAbsolute(configPath) ? configPath : path.join(currentWorkingDirectory, configPath);
64
+ return path.isAbsolute(configPath) ? configPath : path.resolve(currentWorkingDirectory, configPath);
24
65
  };
25
66
  const assertSupportedConfigPath = (configPath) => {
26
67
  const extension = path.extname(configPath).toLowerCase();
27
- if (UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS.has(extension)) throw new Error(`TypeScript config files are no longer supported: '${configPath}'. Convert the config to .js, .mjs, or .cjs, or compile it before passing --config.`);
28
- if (!SUPPORTED_CONFIG_EXTENSIONS.has(extension)) throw new Error(`Unsupported config file extension for '${configPath}'. TypeWeaver only accepts .js, .mjs, or .cjs config files.`);
68
+ if (UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS.has(extension)) throw new UnsupportedTypeScriptConfigError(configPath, extension);
69
+ if (!SUPPORTED_CONFIG_EXTENSION_SET.has(extension)) throw new UnsupportedConfigExtensionError(configPath, extension, SUPPORTED_CONFIG_EXTENSIONS);
29
70
  };
30
71
  const loadConfig = async (configPath) => {
31
72
  assertSupportedConfigPath(configPath);
32
73
  const loadedConfig = getConfigExport(await import(pathToFileURL(path.resolve(configPath)).toString()), configPath);
33
- if (!isConfigObject(loadedConfig)) throw new Error(`Configuration file '${configPath}' must export a config object via 'export default' or 'export const config = ...'.`);
74
+ if (!isConfigObject(loadedConfig)) throw new InvalidConfigExportError(configPath, "non-object-config");
34
75
  return loadedConfig;
35
76
  };
36
77
  const getConfigExport = (configModule, configPath) => {
37
78
  const hasDefaultExport = Object.hasOwn(configModule, "default");
38
79
  const hasNamedConfigExport = Object.hasOwn(configModule, "config");
39
- if (hasDefaultExport && hasNamedConfigExport) throw new Error(`Configuration file '${configPath}' must choose a single export style: either 'export default' or 'export const config = ...', but not both.`);
80
+ if (hasDefaultExport && hasNamedConfigExport) throw new InvalidConfigExportError(configPath, "both-default-and-named-config");
40
81
  if (hasDefaultExport) {
41
- if (isNamespaceLikeConfigExport(configModule.default)) throw new Error(`Configuration file '${configPath}' default export must be the config object itself, not a module namespace-like wrapper. Export the config directly with 'export default { ... }' or use 'export const config = ...'.`);
82
+ if (isNamespaceLikeConfigExport(configModule.default)) throw new InvalidConfigExportError(configPath, "default-namespace-wrapper");
42
83
  return configModule.default;
43
84
  }
44
85
  if (hasNamedConfigExport) return configModule.config;
45
- throw new Error(`Configuration file '${configPath}' must export its config via 'export default' or 'export const config = ...'.`);
86
+ throw new InvalidConfigExportError(configPath, "missing-config-export");
46
87
  };
47
88
  const isConfigObject = (value) => {
48
89
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -52,6 +93,35 @@ const isNamespaceLikeConfigExport = (value) => {
52
93
  return Object.hasOwn(value, "default") || Object.hasOwn(value, "config");
53
94
  };
54
95
  //#endregion
96
+ //#region src/generators/errors/UnsafeCleanTargetError.ts
97
+ var UnsafeCleanTargetError = class extends Error {
98
+ name = "UnsafeCleanTargetError";
99
+ resolvedOutputDir;
100
+ currentWorkingDirectory;
101
+ protectedWorkspaceRoot;
102
+ filesystemRoot;
103
+ constructor(outputDir, reason, diagnostics = {}) {
104
+ super(getUnsafeCleanTargetMessage(outputDir, reason, diagnostics));
105
+ this.outputDir = outputDir;
106
+ this.reason = reason;
107
+ this.resolvedOutputDir = diagnostics.resolvedOutputDir;
108
+ this.currentWorkingDirectory = diagnostics.currentWorkingDirectory;
109
+ this.protectedWorkspaceRoot = diagnostics.protectedWorkspaceRoot;
110
+ this.filesystemRoot = diagnostics.filesystemRoot;
111
+ }
112
+ };
113
+ const getUnsafeCleanTargetMessage = (outputDir, reason, diagnostics) => {
114
+ const targetDescription = `Refusing to clean '${outputDir}'`;
115
+ const suffix = "Use a dedicated generated output directory instead.";
116
+ switch (reason) {
117
+ case "empty-path": return `Refusing to clean an empty output directory path. ${suffix}`;
118
+ case "filesystem-root": return `${targetDescription} because it resolves to the filesystem root '${diagnostics.filesystemRoot ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
119
+ case "current-working-directory": return `${targetDescription} because it resolves to the current working directory '${diagnostics.currentWorkingDirectory ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
120
+ case "workspace-root": return `${targetDescription} because it resolves to the protected workspace root '${diagnostics.protectedWorkspaceRoot ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
121
+ case "ancestor-of-current-working-directory": return `${targetDescription} because it resolves to an ancestor directory of the current working directory '${diagnostics.currentWorkingDirectory ?? ""}'. ${suffix}`;
122
+ }
123
+ };
124
+ //#endregion
55
125
  //#region src/generators/formatter.ts
56
126
  async function formatCode(outputDir, startDir) {
57
127
  const format = await loadFormatter();
@@ -88,11 +158,16 @@ function generateIndexFiles(templateDir, context) {
88
158
  const withJsExt = normalizedFile.replace(/\.ts$/, ".js");
89
159
  const stripped = normalizedFile.replace(/\.ts$/, "");
90
160
  const firstSlash = stripped.indexOf("/");
161
+ if (stripped === "index") continue;
91
162
  if (firstSlash === -1) {
92
163
  rootFiles.add(`./${withJsExt}`);
93
164
  continue;
94
165
  }
95
166
  const firstSegment = stripped.slice(0, firstSlash);
167
+ if (stripped === "lib/index") {
168
+ existingBarrels.add("lib");
169
+ continue;
170
+ }
96
171
  if (firstSegment === "lib") {
97
172
  const secondSlash = stripped.indexOf("/", firstSlash + 1);
98
173
  const groupKey = secondSlash === -1 ? stripped : stripped.slice(0, secondSlash);
@@ -141,30 +216,29 @@ async function loadPlugins(registry, requiredPlugins, strategies, config) {
141
216
  if (!config?.plugins) return;
142
217
  const successful = [];
143
218
  for (const plugin of config.plugins) {
144
- let result;
145
- if (typeof plugin === "string") result = await loadPlugin(plugin, strategies);
146
- else result = await loadPlugin(plugin[0], strategies);
219
+ const result = await loadPlugin(typeof plugin === "string" ? plugin : plugin[0], strategies, typeof plugin === "string" ? void 0 : plugin[1]);
147
220
  if (result.success === false) throw new PluginLoadingFailure(result.error.pluginName, result.error.attempts);
148
221
  successful.push(result.value);
149
- registry.register(result.value.plugin);
222
+ registry.register(result.value.plugin, result.value.config);
150
223
  }
151
224
  reportSuccessfulLoads(successful);
152
225
  }
153
- async function loadPlugin(pluginName, strategies) {
226
+ async function loadPlugin(pluginName, strategies, pluginConfig) {
154
227
  const possiblePaths = generatePluginPaths(pluginName, strategies);
155
228
  const attempts = [];
156
229
  for (const possiblePath of possiblePaths) try {
157
- const PluginClass = findPluginConstructor(await import(possiblePath));
158
- if (PluginClass) return {
230
+ const plugin = createPluginInstance(await import(possiblePath), pluginConfig);
231
+ if (plugin.success) return {
159
232
  success: true,
160
233
  value: {
161
- plugin: new PluginClass(),
162
- source: possiblePath
234
+ plugin: plugin.value,
235
+ source: possiblePath,
236
+ config: pluginConfig
163
237
  }
164
238
  };
165
239
  attempts.push({
166
240
  path: possiblePath,
167
- error: "No plugin class export found"
241
+ error: plugin.error
168
242
  });
169
243
  } catch (error) {
170
244
  attempts.push({
@@ -180,10 +254,46 @@ async function loadPlugin(pluginName, strategies) {
180
254
  }
181
255
  };
182
256
  }
183
- function findPluginConstructor(pluginModule) {
184
- for (const [key, value] of Object.entries(pluginModule)) if (key !== "default" && typeof value === "function") return value;
257
+ function createPluginInstance(pluginModule, pluginConfig) {
258
+ const candidates = findPluginConstructorCandidates(pluginModule);
259
+ if (candidates.length === 0) return {
260
+ success: false,
261
+ error: "No plugin constructor export found"
262
+ };
263
+ const errors = [];
264
+ for (const candidate of candidates) try {
265
+ const plugin = new candidate.constructor(pluginConfig);
266
+ if (isTypeweaverPlugin(plugin)) return {
267
+ success: true,
268
+ value: plugin
269
+ };
270
+ errors.push(`Export '${candidate.exportName}' did not produce a valid plugin with a string name`);
271
+ } catch (error) {
272
+ errors.push(`Export '${candidate.exportName}' could not be instantiated: ${formatError(error)}`);
273
+ }
274
+ return {
275
+ success: false,
276
+ error: errors.join("; ")
277
+ };
278
+ }
279
+ function findPluginConstructorCandidates(pluginModule) {
280
+ const candidates = [];
281
+ for (const [key, value] of Object.entries(pluginModule)) if (key !== "default" && typeof value === "function") candidates.push({
282
+ exportName: key,
283
+ constructor: value
284
+ });
185
285
  const defaultExport = pluginModule.default;
186
- if (typeof defaultExport === "function") return defaultExport;
286
+ if (typeof defaultExport === "function") candidates.push({
287
+ exportName: "default",
288
+ constructor: defaultExport
289
+ });
290
+ return candidates;
291
+ }
292
+ function isTypeweaverPlugin(value) {
293
+ return typeof value === "object" && value !== null && typeof value.name === "string" && value.name.length > 0;
294
+ }
295
+ function formatError(error) {
296
+ return error instanceof Error ? error.message : String(error);
187
297
  }
188
298
  function generatePluginPaths(pluginName, strategies) {
189
299
  const paths = [];
@@ -193,11 +303,19 @@ function generatePluginPaths(pluginName, strategies) {
193
303
  paths.push(`@rexeus/${pluginName}`);
194
304
  break;
195
305
  case "local":
306
+ paths.push(toLocalImportSpecifier(pluginName));
307
+ break;
308
+ case "scoped":
196
309
  paths.push(pluginName);
197
310
  break;
198
311
  }
199
312
  return paths;
200
313
  }
314
+ function toLocalImportSpecifier(pluginName) {
315
+ if (pluginName.startsWith("file:")) return pluginName;
316
+ if (path.isAbsolute(pluginName)) return pathToFileURL(pluginName).href;
317
+ return pluginName;
318
+ }
201
319
  function reportSuccessfulLoads(successful) {
202
320
  if (successful.length > 0) {
203
321
  console.info(`Successfully loaded ${successful.length} plugin(s):`);
@@ -205,6 +323,17 @@ function reportSuccessfulLoads(successful) {
205
323
  }
206
324
  }
207
325
  //#endregion
326
+ //#region src/generators/spec/errors/SpecBundleOutputMissingError.ts
327
+ var SpecBundleOutputMissingError = class extends Error {
328
+ name = "SpecBundleOutputMissingError";
329
+ constructor(inputFile, bundledSpecFile, specOutputDir) {
330
+ super(`Spec bundling completed but did not create the expected output '${bundledSpecFile}' for entrypoint '${inputFile}'. Expected the bundle inside '${specOutputDir}'.`);
331
+ this.inputFile = inputFile;
332
+ this.bundledSpecFile = bundledSpecFile;
333
+ this.specOutputDir = specOutputDir;
334
+ }
335
+ };
336
+ //#endregion
208
337
  //#region src/generators/spec/specBundler.ts
209
338
  const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
210
339
  const WINDOWS_UNC_PATH_PATTERN = /^\\\\/;
@@ -218,7 +347,7 @@ function createWrapperImportSpecifier(wrapperFile, inputFile) {
218
347
  if (relativeInputFile.startsWith(".") || relativeInputFile.startsWith("..")) return relativeInputFile;
219
348
  return `./${relativeInputFile}`;
220
349
  }
221
- async function bundle(config) {
350
+ async function bundle(config, deps = {}) {
222
351
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "typeweaver-spec-loader-"));
223
352
  const wrapperFile = path.join(tempDir, "spec-entrypoint.ts");
224
353
  const bundledSpecFile = path.join(config.specOutputDir, "spec.js");
@@ -234,7 +363,7 @@ async function bundle(config) {
234
363
  ""
235
364
  ].join("\n"));
236
365
  try {
237
- await build({
366
+ await (deps.build ?? build)({
238
367
  cwd: tempDir,
239
368
  input: wrapperFile,
240
369
  treeshake: true,
@@ -254,7 +383,7 @@ async function bundle(config) {
254
383
  force: true
255
384
  });
256
385
  }
257
- if (!fs.existsSync(bundledSpecFile)) throw new Error(`Failed to bundle spec entrypoint '${config.inputFile}' to '${bundledSpecFile}'.`);
386
+ if (!(deps.existsSync ?? fs.existsSync)(bundledSpecFile)) throw new SpecBundleOutputMissingError(config.inputFile, bundledSpecFile, config.specOutputDir);
258
387
  return bundledSpecFile;
259
388
  }
260
389
  function resolveBundledInputFile(inputFile) {
@@ -304,7 +433,10 @@ const isSpecDefinition = (value) => {
304
433
  //#endregion
305
434
  //#region src/generators/spec/specImporter.ts
306
435
  async function importDefinition(bundledSpecFile) {
307
- const specModule = await import(pathToFileURL(bundledSpecFile).toString());
436
+ const contentHash = createHash("sha256").update(fs.readFileSync(bundledSpecFile)).digest("hex");
437
+ const moduleUrl = pathToFileURL(bundledSpecFile);
438
+ moduleUrl.searchParams.set("content", contentHash);
439
+ const specModule = await import(moduleUrl.toString());
308
440
  const definition = specModule.spec ?? specModule.default ?? specModule;
309
441
  if (!isSpecDefinition(definition)) throw new InvalidSpecEntrypointError(bundledSpecFile);
310
442
  return definition;
@@ -333,13 +465,32 @@ function writeSpecDeclarationFile(specOutputDir) {
333
465
  const moduleDir$1 = path.dirname(fileURLToPath(import.meta.url));
334
466
  const assertSafeCleanTarget = (outputDir, currentWorkingDirectory) => {
335
467
  const trimmedOutputDir = outputDir.trim();
336
- if (trimmedOutputDir.length === 0) throw new Error("Refusing to clean an empty output directory path. Pass a dedicated generated output directory instead.");
468
+ if (trimmedOutputDir.length === 0) throw new UnsafeCleanTargetError(outputDir, "empty-path");
337
469
  const resolvedWorkingDirectory = path.resolve(currentWorkingDirectory);
470
+ const canonicalWorkingDirectory = fs.realpathSync.native(resolvedWorkingDirectory);
338
471
  const resolvedOutputDir = path.resolve(resolvedWorkingDirectory, trimmedOutputDir);
339
- if (resolvedOutputDir === path.parse(resolvedOutputDir).root) throw new Error(`Refusing to clean '${outputDir}' because it resolves to the filesystem root.`);
340
- if (resolvedOutputDir === resolvedWorkingDirectory) throw new Error(`Refusing to clean '${outputDir}' because it resolves to the current working directory.`);
341
- const protectedWorkspaceRoot = findProtectedWorkspaceRoot(resolvedWorkingDirectory);
342
- if (protectedWorkspaceRoot !== void 0 && resolvedOutputDir === protectedWorkspaceRoot) throw new Error(`Refusing to clean '${outputDir}' because it resolves to the inferred workspace root '${protectedWorkspaceRoot}'. Choose a dedicated output subdirectory instead.`);
472
+ const canonicalOutputDir = canonicalizePathForContainment(resolvedOutputDir);
473
+ const filesystemRoot = path.parse(canonicalOutputDir).root;
474
+ if (canonicalOutputDir === filesystemRoot) throw new UnsafeCleanTargetError(outputDir, "filesystem-root", {
475
+ resolvedOutputDir,
476
+ currentWorkingDirectory: resolvedWorkingDirectory,
477
+ filesystemRoot
478
+ });
479
+ if (resolvedOutputDir === resolvedWorkingDirectory || canonicalOutputDir === canonicalWorkingDirectory) throw new UnsafeCleanTargetError(outputDir, "current-working-directory", {
480
+ resolvedOutputDir,
481
+ currentWorkingDirectory: resolvedWorkingDirectory
482
+ });
483
+ const protectedWorkspaceRoots = [findProtectedWorkspaceRoot(resolvedWorkingDirectory), findProtectedWorkspaceRoot(canonicalWorkingDirectory)].filter((root) => root !== void 0);
484
+ const protectedWorkspaceRootTarget = protectedWorkspaceRoots.find((protectedWorkspaceRoot) => resolvedOutputDir === protectedWorkspaceRoot || canonicalOutputDir === fs.realpathSync.native(protectedWorkspaceRoot));
485
+ if (protectedWorkspaceRootTarget !== void 0) throw new UnsafeCleanTargetError(outputDir, "workspace-root", {
486
+ resolvedOutputDir,
487
+ currentWorkingDirectory: resolvedWorkingDirectory,
488
+ protectedWorkspaceRoot: protectedWorkspaceRootTarget
489
+ });
490
+ if (protectedWorkspaceRoots.length > 0 && (isSameOrDescendantOf(resolvedWorkingDirectory, resolvedOutputDir) || isSameOrDescendantOf(canonicalWorkingDirectory, canonicalOutputDir))) throw new UnsafeCleanTargetError(outputDir, "ancestor-of-current-working-directory", {
491
+ resolvedOutputDir,
492
+ currentWorkingDirectory: resolvedWorkingDirectory
493
+ });
343
494
  };
344
495
  /**
345
496
  * Main generator for typeweaver
@@ -433,6 +584,58 @@ const findProtectedWorkspaceRoot = (startDirectory) => {
433
584
  const hasWorkspaceMarker = (directory) => {
434
585
  return ["pnpm-workspace.yaml", ".git"].some((marker) => fs.existsSync(path.join(directory, marker)));
435
586
  };
587
+ const canonicalizePathForContainment = (targetPath) => {
588
+ const remainingSegments = [];
589
+ let nearestExistingPath = path.resolve(targetPath);
590
+ while (!fs.existsSync(nearestExistingPath)) {
591
+ const parentPath = path.dirname(nearestExistingPath);
592
+ if (parentPath === nearestExistingPath) break;
593
+ remainingSegments.unshift(path.basename(nearestExistingPath));
594
+ nearestExistingPath = parentPath;
595
+ }
596
+ const canonicalExistingPath = fs.realpathSync.native(nearestExistingPath);
597
+ return path.join(canonicalExistingPath, ...remainingSegments);
598
+ };
599
+ const isSameOrDescendantOf = (directory, ancestor) => {
600
+ const relativePath = path.relative(ancestor, directory);
601
+ const parentTraversalPrefix = `..${path.sep}`;
602
+ const escapesAncestor = relativePath === ".." || relativePath.startsWith(parentTraversalPrefix);
603
+ return relativePath === "" || !escapesAncestor && !path.isAbsolute(relativePath);
604
+ };
605
+ //#endregion
606
+ //#region src/errors/MissingGenerateOptionError.ts
607
+ var MissingGenerateOptionError = class extends Error {
608
+ name = "MissingGenerateOptionError";
609
+ constructor(optionName, flag, configKey) {
610
+ super(`Missing required generate option '${optionName}'. Pass ${flag} or set '${configKey}' in the TypeWeaver config file.`);
611
+ this.optionName = optionName;
612
+ this.flag = flag;
613
+ this.configKey = configKey;
614
+ }
615
+ };
616
+ //#endregion
617
+ //#region src/resolveGenerateOptions.ts
618
+ const resolveGenerateOptions = (options, config, currentWorkingDirectory) => {
619
+ const inputPath = options.input ?? config.input;
620
+ const outputDir = options.output ?? config.output;
621
+ if (!inputPath) throw new MissingGenerateOptionError("input", "--input", "input");
622
+ if (!outputDir) throw new MissingGenerateOptionError("output", "--output", "output");
623
+ const resolvedInputPath = path.isAbsolute(inputPath) ? inputPath : path.join(currentWorkingDirectory, inputPath);
624
+ const resolvedOutputDir = path.isAbsolute(outputDir) ? outputDir : path.join(currentWorkingDirectory, outputDir);
625
+ const finalConfig = {
626
+ input: resolvedInputPath,
627
+ output: resolvedOutputDir,
628
+ format: options.format ?? config.format ?? true,
629
+ clean: options.clean ?? config.clean ?? true
630
+ };
631
+ if (options.plugins) finalConfig.plugins = options.plugins.split(",").map((plugin) => plugin.trim());
632
+ else if (config.plugins) finalConfig.plugins = config.plugins;
633
+ return {
634
+ inputPath: resolvedInputPath,
635
+ outputDir: resolvedOutputDir,
636
+ config: finalConfig
637
+ };
638
+ };
436
639
  //#endregion
437
640
  //#region src/cli.ts
438
641
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -453,21 +656,8 @@ program.command("generate").description("Generate types, validators, and clients
453
656
  process.exit(1);
454
657
  }
455
658
  }
456
- const inputPath = options.input ?? config.input;
457
- const outputDir = options.output ?? config.output;
458
- if (!inputPath) throw new Error("No input spec entrypoint provided. Use --input or specify in config file.");
459
- if (!outputDir) throw new Error("No output directory provided. Use --output or specify in config file.");
460
- const resolvedInputPath = path.isAbsolute(inputPath) ? inputPath : path.join(execDir, inputPath);
461
- const resolvedOutputDir = path.isAbsolute(outputDir) ? outputDir : path.join(execDir, outputDir);
462
- const finalConfig = {
463
- input: resolvedInputPath,
464
- output: resolvedOutputDir,
465
- format: options.format ?? config.format ?? true,
466
- clean: options.clean ?? config.clean ?? true
467
- };
468
- if (options.plugins) finalConfig.plugins = options.plugins.split(",").map((p) => p.trim());
469
- else if (config.plugins) finalConfig.plugins = config.plugins;
470
- return new Generator().generate(resolvedInputPath, resolvedOutputDir, finalConfig, execDir);
659
+ const resolvedGenerateOptions = resolveGenerateOptions(options, config, execDir);
660
+ return new Generator().generate(resolvedGenerateOptions.inputPath, resolvedGenerateOptions.outputDir, resolvedGenerateOptions.config, execDir);
471
661
  });
472
662
  program.command("init").description("Initialize a new typeweaver project (coming soon)").action(() => {
473
663
  console.log("The init command is coming soon!");