@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.cjs CHANGED
@@ -31,42 +31,83 @@ let _rexeus_typeweaver_types = require("@rexeus/typeweaver-types");
31
31
  let node_os = require("node:os");
32
32
  node_os = __toESM(node_os);
33
33
  let rolldown = require("rolldown");
34
+ let node_crypto = require("node:crypto");
34
35
  let _rexeus_typeweaver_core = require("@rexeus/typeweaver-core");
36
+ //#region src/errors/InvalidConfigExportError.ts
37
+ var InvalidConfigExportError = class extends Error {
38
+ name = "InvalidConfigExportError";
39
+ constructor(configPath, reason) {
40
+ super(getInvalidConfigExportMessage(configPath, reason));
41
+ this.configPath = configPath;
42
+ this.reason = reason;
43
+ }
44
+ };
45
+ const getInvalidConfigExportMessage = (configPath, reason) => {
46
+ switch (reason) {
47
+ 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.`;
48
+ 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 = ...'.`;
49
+ case "missing-config-export": return `Configuration file '${configPath}' must export its config via 'export default' or 'export const config = ...'.`;
50
+ case "non-object-config": return `Configuration file '${configPath}' must export a config object via 'export default' or 'export const config = ...'.`;
51
+ }
52
+ };
53
+ //#endregion
54
+ //#region src/errors/UnsupportedConfigExtensionError.ts
55
+ var UnsupportedConfigExtensionError = class extends Error {
56
+ name = "UnsupportedConfigExtensionError";
57
+ constructor(configPath, extension, supportedExtensions) {
58
+ super(`Unsupported config file extension '${extension}' for '${configPath}'. TypeWeaver accepts only these config extensions: ${supportedExtensions.join(", ")}.`);
59
+ this.configPath = configPath;
60
+ this.extension = extension;
61
+ this.supportedExtensions = supportedExtensions;
62
+ }
63
+ };
64
+ //#endregion
65
+ //#region src/errors/UnsupportedTypeScriptConfigError.ts
66
+ var UnsupportedTypeScriptConfigError = class extends Error {
67
+ name = "UnsupportedTypeScriptConfigError";
68
+ constructor(configPath, extension) {
69
+ super(`TypeScript config files are not supported: '${configPath}' uses '${extension}'. Use a JavaScript config file with one of these extensions: .js, .mjs, or .cjs.`);
70
+ this.configPath = configPath;
71
+ this.extension = extension;
72
+ }
73
+ };
74
+ //#endregion
35
75
  //#region src/configLoader.ts
36
- const SUPPORTED_CONFIG_EXTENSIONS = new Set([
76
+ const SUPPORTED_CONFIG_EXTENSIONS = [
37
77
  ".js",
38
78
  ".mjs",
39
79
  ".cjs"
40
- ]);
80
+ ];
81
+ const SUPPORTED_CONFIG_EXTENSION_SET = new Set(SUPPORTED_CONFIG_EXTENSIONS);
41
82
  const UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS = new Set([
42
83
  ".ts",
43
84
  ".mts",
44
85
  ".cts"
45
86
  ]);
46
87
  const getResolvedConfigPath = (configPath, currentWorkingDirectory = process.cwd()) => {
47
- return node_path.default.isAbsolute(configPath) ? configPath : node_path.default.join(currentWorkingDirectory, configPath);
88
+ return node_path.default.isAbsolute(configPath) ? configPath : node_path.default.resolve(currentWorkingDirectory, configPath);
48
89
  };
49
90
  const assertSupportedConfigPath = (configPath) => {
50
91
  const extension = node_path.default.extname(configPath).toLowerCase();
51
- 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.`);
52
- if (!SUPPORTED_CONFIG_EXTENSIONS.has(extension)) throw new Error(`Unsupported config file extension for '${configPath}'. TypeWeaver only accepts .js, .mjs, or .cjs config files.`);
92
+ if (UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS.has(extension)) throw new UnsupportedTypeScriptConfigError(configPath, extension);
93
+ if (!SUPPORTED_CONFIG_EXTENSION_SET.has(extension)) throw new UnsupportedConfigExtensionError(configPath, extension, SUPPORTED_CONFIG_EXTENSIONS);
53
94
  };
54
95
  const loadConfig = async (configPath) => {
55
96
  assertSupportedConfigPath(configPath);
56
97
  const loadedConfig = getConfigExport(await import((0, node_url.pathToFileURL)(node_path.default.resolve(configPath)).toString()), configPath);
57
- if (!isConfigObject(loadedConfig)) throw new Error(`Configuration file '${configPath}' must export a config object via 'export default' or 'export const config = ...'.`);
98
+ if (!isConfigObject(loadedConfig)) throw new InvalidConfigExportError(configPath, "non-object-config");
58
99
  return loadedConfig;
59
100
  };
60
101
  const getConfigExport = (configModule, configPath) => {
61
102
  const hasDefaultExport = Object.hasOwn(configModule, "default");
62
103
  const hasNamedConfigExport = Object.hasOwn(configModule, "config");
63
- 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.`);
104
+ if (hasDefaultExport && hasNamedConfigExport) throw new InvalidConfigExportError(configPath, "both-default-and-named-config");
64
105
  if (hasDefaultExport) {
65
- 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 = ...'.`);
106
+ if (isNamespaceLikeConfigExport(configModule.default)) throw new InvalidConfigExportError(configPath, "default-namespace-wrapper");
66
107
  return configModule.default;
67
108
  }
68
109
  if (hasNamedConfigExport) return configModule.config;
69
- throw new Error(`Configuration file '${configPath}' must export its config via 'export default' or 'export const config = ...'.`);
110
+ throw new InvalidConfigExportError(configPath, "missing-config-export");
70
111
  };
71
112
  const isConfigObject = (value) => {
72
113
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -76,6 +117,35 @@ const isNamespaceLikeConfigExport = (value) => {
76
117
  return Object.hasOwn(value, "default") || Object.hasOwn(value, "config");
77
118
  };
78
119
  //#endregion
120
+ //#region src/generators/errors/UnsafeCleanTargetError.ts
121
+ var UnsafeCleanTargetError = class extends Error {
122
+ name = "UnsafeCleanTargetError";
123
+ resolvedOutputDir;
124
+ currentWorkingDirectory;
125
+ protectedWorkspaceRoot;
126
+ filesystemRoot;
127
+ constructor(outputDir, reason, diagnostics = {}) {
128
+ super(getUnsafeCleanTargetMessage(outputDir, reason, diagnostics));
129
+ this.outputDir = outputDir;
130
+ this.reason = reason;
131
+ this.resolvedOutputDir = diagnostics.resolvedOutputDir;
132
+ this.currentWorkingDirectory = diagnostics.currentWorkingDirectory;
133
+ this.protectedWorkspaceRoot = diagnostics.protectedWorkspaceRoot;
134
+ this.filesystemRoot = diagnostics.filesystemRoot;
135
+ }
136
+ };
137
+ const getUnsafeCleanTargetMessage = (outputDir, reason, diagnostics) => {
138
+ const targetDescription = `Refusing to clean '${outputDir}'`;
139
+ const suffix = "Use a dedicated generated output directory instead.";
140
+ switch (reason) {
141
+ case "empty-path": return `Refusing to clean an empty output directory path. ${suffix}`;
142
+ case "filesystem-root": return `${targetDescription} because it resolves to the filesystem root '${diagnostics.filesystemRoot ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
143
+ case "current-working-directory": return `${targetDescription} because it resolves to the current working directory '${diagnostics.currentWorkingDirectory ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
144
+ case "workspace-root": return `${targetDescription} because it resolves to the protected workspace root '${diagnostics.protectedWorkspaceRoot ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
145
+ case "ancestor-of-current-working-directory": return `${targetDescription} because it resolves to an ancestor directory of the current working directory '${diagnostics.currentWorkingDirectory ?? ""}'. ${suffix}`;
146
+ }
147
+ };
148
+ //#endregion
79
149
  //#region src/generators/formatter.ts
80
150
  async function formatCode(outputDir, startDir) {
81
151
  const format = await loadFormatter();
@@ -112,11 +182,16 @@ function generateIndexFiles(templateDir, context) {
112
182
  const withJsExt = normalizedFile.replace(/\.ts$/, ".js");
113
183
  const stripped = normalizedFile.replace(/\.ts$/, "");
114
184
  const firstSlash = stripped.indexOf("/");
185
+ if (stripped === "index") continue;
115
186
  if (firstSlash === -1) {
116
187
  rootFiles.add(`./${withJsExt}`);
117
188
  continue;
118
189
  }
119
190
  const firstSegment = stripped.slice(0, firstSlash);
191
+ if (stripped === "lib/index") {
192
+ existingBarrels.add("lib");
193
+ continue;
194
+ }
120
195
  if (firstSegment === "lib") {
121
196
  const secondSlash = stripped.indexOf("/", firstSlash + 1);
122
197
  const groupKey = secondSlash === -1 ? stripped : stripped.slice(0, secondSlash);
@@ -165,30 +240,29 @@ async function loadPlugins(registry, requiredPlugins, strategies, config) {
165
240
  if (!config?.plugins) return;
166
241
  const successful = [];
167
242
  for (const plugin of config.plugins) {
168
- let result;
169
- if (typeof plugin === "string") result = await loadPlugin(plugin, strategies);
170
- else result = await loadPlugin(plugin[0], strategies);
243
+ const result = await loadPlugin(typeof plugin === "string" ? plugin : plugin[0], strategies, typeof plugin === "string" ? void 0 : plugin[1]);
171
244
  if (result.success === false) throw new PluginLoadingFailure(result.error.pluginName, result.error.attempts);
172
245
  successful.push(result.value);
173
- registry.register(result.value.plugin);
246
+ registry.register(result.value.plugin, result.value.config);
174
247
  }
175
248
  reportSuccessfulLoads(successful);
176
249
  }
177
- async function loadPlugin(pluginName, strategies) {
250
+ async function loadPlugin(pluginName, strategies, pluginConfig) {
178
251
  const possiblePaths = generatePluginPaths(pluginName, strategies);
179
252
  const attempts = [];
180
253
  for (const possiblePath of possiblePaths) try {
181
- const PluginClass = findPluginConstructor(await import(possiblePath));
182
- if (PluginClass) return {
254
+ const plugin = createPluginInstance(await import(possiblePath), pluginConfig);
255
+ if (plugin.success) return {
183
256
  success: true,
184
257
  value: {
185
- plugin: new PluginClass(),
186
- source: possiblePath
258
+ plugin: plugin.value,
259
+ source: possiblePath,
260
+ config: pluginConfig
187
261
  }
188
262
  };
189
263
  attempts.push({
190
264
  path: possiblePath,
191
- error: "No plugin class export found"
265
+ error: plugin.error
192
266
  });
193
267
  } catch (error) {
194
268
  attempts.push({
@@ -204,10 +278,46 @@ async function loadPlugin(pluginName, strategies) {
204
278
  }
205
279
  };
206
280
  }
207
- function findPluginConstructor(pluginModule) {
208
- for (const [key, value] of Object.entries(pluginModule)) if (key !== "default" && typeof value === "function") return value;
281
+ function createPluginInstance(pluginModule, pluginConfig) {
282
+ const candidates = findPluginConstructorCandidates(pluginModule);
283
+ if (candidates.length === 0) return {
284
+ success: false,
285
+ error: "No plugin constructor export found"
286
+ };
287
+ const errors = [];
288
+ for (const candidate of candidates) try {
289
+ const plugin = new candidate.constructor(pluginConfig);
290
+ if (isTypeweaverPlugin(plugin)) return {
291
+ success: true,
292
+ value: plugin
293
+ };
294
+ errors.push(`Export '${candidate.exportName}' did not produce a valid plugin with a string name`);
295
+ } catch (error) {
296
+ errors.push(`Export '${candidate.exportName}' could not be instantiated: ${formatError(error)}`);
297
+ }
298
+ return {
299
+ success: false,
300
+ error: errors.join("; ")
301
+ };
302
+ }
303
+ function findPluginConstructorCandidates(pluginModule) {
304
+ const candidates = [];
305
+ for (const [key, value] of Object.entries(pluginModule)) if (key !== "default" && typeof value === "function") candidates.push({
306
+ exportName: key,
307
+ constructor: value
308
+ });
209
309
  const defaultExport = pluginModule.default;
210
- if (typeof defaultExport === "function") return defaultExport;
310
+ if (typeof defaultExport === "function") candidates.push({
311
+ exportName: "default",
312
+ constructor: defaultExport
313
+ });
314
+ return candidates;
315
+ }
316
+ function isTypeweaverPlugin(value) {
317
+ return typeof value === "object" && value !== null && typeof value.name === "string" && value.name.length > 0;
318
+ }
319
+ function formatError(error) {
320
+ return error instanceof Error ? error.message : String(error);
211
321
  }
212
322
  function generatePluginPaths(pluginName, strategies) {
213
323
  const paths = [];
@@ -217,11 +327,19 @@ function generatePluginPaths(pluginName, strategies) {
217
327
  paths.push(`@rexeus/${pluginName}`);
218
328
  break;
219
329
  case "local":
330
+ paths.push(toLocalImportSpecifier(pluginName));
331
+ break;
332
+ case "scoped":
220
333
  paths.push(pluginName);
221
334
  break;
222
335
  }
223
336
  return paths;
224
337
  }
338
+ function toLocalImportSpecifier(pluginName) {
339
+ if (pluginName.startsWith("file:")) return pluginName;
340
+ if (node_path.default.isAbsolute(pluginName)) return (0, node_url.pathToFileURL)(pluginName).href;
341
+ return pluginName;
342
+ }
225
343
  function reportSuccessfulLoads(successful) {
226
344
  if (successful.length > 0) {
227
345
  console.info(`Successfully loaded ${successful.length} plugin(s):`);
@@ -229,6 +347,17 @@ function reportSuccessfulLoads(successful) {
229
347
  }
230
348
  }
231
349
  //#endregion
350
+ //#region src/generators/spec/errors/SpecBundleOutputMissingError.ts
351
+ var SpecBundleOutputMissingError = class extends Error {
352
+ name = "SpecBundleOutputMissingError";
353
+ constructor(inputFile, bundledSpecFile, specOutputDir) {
354
+ super(`Spec bundling completed but did not create the expected output '${bundledSpecFile}' for entrypoint '${inputFile}'. Expected the bundle inside '${specOutputDir}'.`);
355
+ this.inputFile = inputFile;
356
+ this.bundledSpecFile = bundledSpecFile;
357
+ this.specOutputDir = specOutputDir;
358
+ }
359
+ };
360
+ //#endregion
232
361
  //#region src/generators/spec/specBundler.ts
233
362
  const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
234
363
  const WINDOWS_UNC_PATH_PATTERN = /^\\\\/;
@@ -242,7 +371,7 @@ function createWrapperImportSpecifier(wrapperFile, inputFile) {
242
371
  if (relativeInputFile.startsWith(".") || relativeInputFile.startsWith("..")) return relativeInputFile;
243
372
  return `./${relativeInputFile}`;
244
373
  }
245
- async function bundle(config) {
374
+ async function bundle(config, deps = {}) {
246
375
  const tempDir = node_fs.default.mkdtempSync(node_path.default.join(node_os.default.tmpdir(), "typeweaver-spec-loader-"));
247
376
  const wrapperFile = node_path.default.join(tempDir, "spec-entrypoint.ts");
248
377
  const bundledSpecFile = node_path.default.join(config.specOutputDir, "spec.js");
@@ -258,7 +387,7 @@ async function bundle(config) {
258
387
  ""
259
388
  ].join("\n"));
260
389
  try {
261
- await (0, rolldown.build)({
390
+ await (deps.build ?? rolldown.build)({
262
391
  cwd: tempDir,
263
392
  input: wrapperFile,
264
393
  treeshake: true,
@@ -278,7 +407,7 @@ async function bundle(config) {
278
407
  force: true
279
408
  });
280
409
  }
281
- if (!node_fs.default.existsSync(bundledSpecFile)) throw new Error(`Failed to bundle spec entrypoint '${config.inputFile}' to '${bundledSpecFile}'.`);
410
+ if (!(deps.existsSync ?? node_fs.default.existsSync)(bundledSpecFile)) throw new SpecBundleOutputMissingError(config.inputFile, bundledSpecFile, config.specOutputDir);
282
411
  return bundledSpecFile;
283
412
  }
284
413
  function resolveBundledInputFile(inputFile) {
@@ -328,7 +457,10 @@ const isSpecDefinition = (value) => {
328
457
  //#endregion
329
458
  //#region src/generators/spec/specImporter.ts
330
459
  async function importDefinition(bundledSpecFile) {
331
- const specModule = await import((0, node_url.pathToFileURL)(bundledSpecFile).toString());
460
+ const contentHash = (0, node_crypto.createHash)("sha256").update(node_fs.default.readFileSync(bundledSpecFile)).digest("hex");
461
+ const moduleUrl = (0, node_url.pathToFileURL)(bundledSpecFile);
462
+ moduleUrl.searchParams.set("content", contentHash);
463
+ const specModule = await import(moduleUrl.toString());
332
464
  const definition = specModule.spec ?? specModule.default ?? specModule;
333
465
  if (!isSpecDefinition(definition)) throw new InvalidSpecEntrypointError(bundledSpecFile);
334
466
  return definition;
@@ -357,13 +489,32 @@ function writeSpecDeclarationFile(specOutputDir) {
357
489
  const moduleDir$1 = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
358
490
  const assertSafeCleanTarget = (outputDir, currentWorkingDirectory) => {
359
491
  const trimmedOutputDir = outputDir.trim();
360
- if (trimmedOutputDir.length === 0) throw new Error("Refusing to clean an empty output directory path. Pass a dedicated generated output directory instead.");
492
+ if (trimmedOutputDir.length === 0) throw new UnsafeCleanTargetError(outputDir, "empty-path");
361
493
  const resolvedWorkingDirectory = node_path.default.resolve(currentWorkingDirectory);
494
+ const canonicalWorkingDirectory = node_fs.default.realpathSync.native(resolvedWorkingDirectory);
362
495
  const resolvedOutputDir = node_path.default.resolve(resolvedWorkingDirectory, trimmedOutputDir);
363
- if (resolvedOutputDir === node_path.default.parse(resolvedOutputDir).root) throw new Error(`Refusing to clean '${outputDir}' because it resolves to the filesystem root.`);
364
- if (resolvedOutputDir === resolvedWorkingDirectory) throw new Error(`Refusing to clean '${outputDir}' because it resolves to the current working directory.`);
365
- const protectedWorkspaceRoot = findProtectedWorkspaceRoot(resolvedWorkingDirectory);
366
- 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.`);
496
+ const canonicalOutputDir = canonicalizePathForContainment(resolvedOutputDir);
497
+ const filesystemRoot = node_path.default.parse(canonicalOutputDir).root;
498
+ if (canonicalOutputDir === filesystemRoot) throw new UnsafeCleanTargetError(outputDir, "filesystem-root", {
499
+ resolvedOutputDir,
500
+ currentWorkingDirectory: resolvedWorkingDirectory,
501
+ filesystemRoot
502
+ });
503
+ if (resolvedOutputDir === resolvedWorkingDirectory || canonicalOutputDir === canonicalWorkingDirectory) throw new UnsafeCleanTargetError(outputDir, "current-working-directory", {
504
+ resolvedOutputDir,
505
+ currentWorkingDirectory: resolvedWorkingDirectory
506
+ });
507
+ const protectedWorkspaceRoots = [findProtectedWorkspaceRoot(resolvedWorkingDirectory), findProtectedWorkspaceRoot(canonicalWorkingDirectory)].filter((root) => root !== void 0);
508
+ const protectedWorkspaceRootTarget = protectedWorkspaceRoots.find((protectedWorkspaceRoot) => resolvedOutputDir === protectedWorkspaceRoot || canonicalOutputDir === node_fs.default.realpathSync.native(protectedWorkspaceRoot));
509
+ if (protectedWorkspaceRootTarget !== void 0) throw new UnsafeCleanTargetError(outputDir, "workspace-root", {
510
+ resolvedOutputDir,
511
+ currentWorkingDirectory: resolvedWorkingDirectory,
512
+ protectedWorkspaceRoot: protectedWorkspaceRootTarget
513
+ });
514
+ if (protectedWorkspaceRoots.length > 0 && (isSameOrDescendantOf(resolvedWorkingDirectory, resolvedOutputDir) || isSameOrDescendantOf(canonicalWorkingDirectory, canonicalOutputDir))) throw new UnsafeCleanTargetError(outputDir, "ancestor-of-current-working-directory", {
515
+ resolvedOutputDir,
516
+ currentWorkingDirectory: resolvedWorkingDirectory
517
+ });
367
518
  };
368
519
  /**
369
520
  * Main generator for typeweaver
@@ -457,6 +608,58 @@ const findProtectedWorkspaceRoot = (startDirectory) => {
457
608
  const hasWorkspaceMarker = (directory) => {
458
609
  return ["pnpm-workspace.yaml", ".git"].some((marker) => node_fs.default.existsSync(node_path.default.join(directory, marker)));
459
610
  };
611
+ const canonicalizePathForContainment = (targetPath) => {
612
+ const remainingSegments = [];
613
+ let nearestExistingPath = node_path.default.resolve(targetPath);
614
+ while (!node_fs.default.existsSync(nearestExistingPath)) {
615
+ const parentPath = node_path.default.dirname(nearestExistingPath);
616
+ if (parentPath === nearestExistingPath) break;
617
+ remainingSegments.unshift(node_path.default.basename(nearestExistingPath));
618
+ nearestExistingPath = parentPath;
619
+ }
620
+ const canonicalExistingPath = node_fs.default.realpathSync.native(nearestExistingPath);
621
+ return node_path.default.join(canonicalExistingPath, ...remainingSegments);
622
+ };
623
+ const isSameOrDescendantOf = (directory, ancestor) => {
624
+ const relativePath = node_path.default.relative(ancestor, directory);
625
+ const parentTraversalPrefix = `..${node_path.default.sep}`;
626
+ const escapesAncestor = relativePath === ".." || relativePath.startsWith(parentTraversalPrefix);
627
+ return relativePath === "" || !escapesAncestor && !node_path.default.isAbsolute(relativePath);
628
+ };
629
+ //#endregion
630
+ //#region src/errors/MissingGenerateOptionError.ts
631
+ var MissingGenerateOptionError = class extends Error {
632
+ name = "MissingGenerateOptionError";
633
+ constructor(optionName, flag, configKey) {
634
+ super(`Missing required generate option '${optionName}'. Pass ${flag} or set '${configKey}' in the TypeWeaver config file.`);
635
+ this.optionName = optionName;
636
+ this.flag = flag;
637
+ this.configKey = configKey;
638
+ }
639
+ };
640
+ //#endregion
641
+ //#region src/resolveGenerateOptions.ts
642
+ const resolveGenerateOptions = (options, config, currentWorkingDirectory) => {
643
+ const inputPath = options.input ?? config.input;
644
+ const outputDir = options.output ?? config.output;
645
+ if (!inputPath) throw new MissingGenerateOptionError("input", "--input", "input");
646
+ if (!outputDir) throw new MissingGenerateOptionError("output", "--output", "output");
647
+ const resolvedInputPath = node_path.default.isAbsolute(inputPath) ? inputPath : node_path.default.join(currentWorkingDirectory, inputPath);
648
+ const resolvedOutputDir = node_path.default.isAbsolute(outputDir) ? outputDir : node_path.default.join(currentWorkingDirectory, outputDir);
649
+ const finalConfig = {
650
+ input: resolvedInputPath,
651
+ output: resolvedOutputDir,
652
+ format: options.format ?? config.format ?? true,
653
+ clean: options.clean ?? config.clean ?? true
654
+ };
655
+ if (options.plugins) finalConfig.plugins = options.plugins.split(",").map((plugin) => plugin.trim());
656
+ else if (config.plugins) finalConfig.plugins = config.plugins;
657
+ return {
658
+ inputPath: resolvedInputPath,
659
+ outputDir: resolvedOutputDir,
660
+ config: finalConfig
661
+ };
662
+ };
460
663
  //#endregion
461
664
  //#region src/cli.ts
462
665
  const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
@@ -477,21 +680,8 @@ program.command("generate").description("Generate types, validators, and clients
477
680
  process.exit(1);
478
681
  }
479
682
  }
480
- const inputPath = options.input ?? config.input;
481
- const outputDir = options.output ?? config.output;
482
- if (!inputPath) throw new Error("No input spec entrypoint provided. Use --input or specify in config file.");
483
- if (!outputDir) throw new Error("No output directory provided. Use --output or specify in config file.");
484
- const resolvedInputPath = node_path.default.isAbsolute(inputPath) ? inputPath : node_path.default.join(execDir, inputPath);
485
- const resolvedOutputDir = node_path.default.isAbsolute(outputDir) ? outputDir : node_path.default.join(execDir, outputDir);
486
- const finalConfig = {
487
- input: resolvedInputPath,
488
- output: resolvedOutputDir,
489
- format: options.format ?? config.format ?? true,
490
- clean: options.clean ?? config.clean ?? true
491
- };
492
- if (options.plugins) finalConfig.plugins = options.plugins.split(",").map((p) => p.trim());
493
- else if (config.plugins) finalConfig.plugins = config.plugins;
494
- return new Generator().generate(resolvedInputPath, resolvedOutputDir, finalConfig, execDir);
683
+ const resolvedGenerateOptions = resolveGenerateOptions(options, config, execDir);
684
+ return new Generator().generate(resolvedGenerateOptions.inputPath, resolvedGenerateOptions.outputDir, resolvedGenerateOptions.config, execDir);
495
685
  });
496
686
  program.command("init").description("Initialize a new typeweaver project (coming soon)").action(() => {
497
687
  console.log("The init command is coming soon!");