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