@rexeus/typeweaver 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -227
- package/dist/cli-B-KPX-Jc.mjs +407 -0
- package/dist/cli.cjs +311 -4337
- package/dist/cli.mjs +289 -4311
- package/dist/cli.mjs.map +1 -1
- package/dist/entry.mjs +3 -5
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -2
- package/dist/{tsx-loader-DUG5if6z.mjs → tsx-loader-DtL8gLLb.mjs} +1 -2
- package/package.json +22 -21
- package/dist/cli-AH4H-B8Q.mjs +0 -4430
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { createPluginContextBuilder, createPluginRegistry, normalizeSpec } from "@rexeus/typeweaver-gen";
|
|
7
|
+
import TypesPlugin from "@rexeus/typeweaver-types";
|
|
8
|
+
import ejs from "ejs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import { Rolldown } from "tsdown";
|
|
11
|
+
import { HttpMethod, HttpStatusCode } from "@rexeus/typeweaver-core";
|
|
12
|
+
//#region src/generators/formatter.ts
|
|
13
|
+
async function formatCode(outputDir, startDir) {
|
|
14
|
+
const format = await loadFormatter();
|
|
15
|
+
if (!format) return;
|
|
16
|
+
await formatDirectory(startDir ?? outputDir, format);
|
|
17
|
+
}
|
|
18
|
+
async function loadFormatter() {
|
|
19
|
+
try {
|
|
20
|
+
return (await import("oxfmt")).format;
|
|
21
|
+
} catch {
|
|
22
|
+
console.warn("oxfmt not installed - skipping formatting. Install with: npm install -D oxfmt");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function formatDirectory(targetDir, format) {
|
|
27
|
+
const contents = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
28
|
+
for (const content of contents) if (content.isFile()) {
|
|
29
|
+
const filePath = path.join(targetDir, content.name);
|
|
30
|
+
const { code } = await format(filePath, fs.readFileSync(filePath, "utf8"));
|
|
31
|
+
fs.writeFileSync(filePath, code);
|
|
32
|
+
} else if (content.isDirectory()) await formatDirectory(path.join(targetDir, content.name), format);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/generators/indexFileGenerator.ts
|
|
36
|
+
function generateIndexFiles(templateDir, context) {
|
|
37
|
+
const templateFilePath = path.join(templateDir, "Index.ejs");
|
|
38
|
+
const template = fs.readFileSync(templateFilePath, "utf8");
|
|
39
|
+
const generatedFiles = context.getGeneratedFiles();
|
|
40
|
+
const groups = /* @__PURE__ */ new Map();
|
|
41
|
+
const rootFiles = /* @__PURE__ */ new Set();
|
|
42
|
+
const existingBarrels = /* @__PURE__ */ new Set();
|
|
43
|
+
for (const file of generatedFiles) {
|
|
44
|
+
const stripped = file.replace(/\.ts$/, "");
|
|
45
|
+
const firstSlash = stripped.indexOf("/");
|
|
46
|
+
if (firstSlash === -1) {
|
|
47
|
+
rootFiles.add(`./${stripped}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const firstSegment = stripped.slice(0, firstSlash);
|
|
51
|
+
if (firstSegment === "lib") {
|
|
52
|
+
const secondSlash = stripped.indexOf("/", firstSlash + 1);
|
|
53
|
+
const groupKey = secondSlash === -1 ? stripped : stripped.slice(0, secondSlash);
|
|
54
|
+
const entryName = stripped.slice(groupKey.length + 1);
|
|
55
|
+
if (entryName === "index") existingBarrels.add(groupKey);
|
|
56
|
+
else {
|
|
57
|
+
if (!groups.has(groupKey)) groups.set(groupKey, /* @__PURE__ */ new Set());
|
|
58
|
+
groups.get(groupKey).add(`./${entryName}`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
const entryName = stripped.slice(firstSlash + 1);
|
|
62
|
+
if (entryName === "index") existingBarrels.add(firstSegment);
|
|
63
|
+
else {
|
|
64
|
+
if (!groups.has(firstSegment)) groups.set(firstSegment, /* @__PURE__ */ new Set());
|
|
65
|
+
groups.get(firstSegment).add(`./${entryName}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const [groupKey, entries] of groups) {
|
|
70
|
+
if (existingBarrels.has(groupKey)) continue;
|
|
71
|
+
const domainBarrelContent = ejs.render(template, { indexPaths: Array.from(entries).sort() });
|
|
72
|
+
const domainIndexPath = path.join(context.outputDir, groupKey, "index.ts");
|
|
73
|
+
fs.mkdirSync(path.dirname(domainIndexPath), { recursive: true });
|
|
74
|
+
fs.writeFileSync(domainIndexPath, domainBarrelContent);
|
|
75
|
+
}
|
|
76
|
+
const rootIndexPaths = new Set(rootFiles);
|
|
77
|
+
for (const groupKey of groups.keys()) rootIndexPaths.add(`./${groupKey}`);
|
|
78
|
+
for (const barrelKey of existingBarrels) rootIndexPaths.add(`./${barrelKey}`);
|
|
79
|
+
const rootContent = ejs.render(template, { indexPaths: Array.from(rootIndexPaths).sort() });
|
|
80
|
+
fs.writeFileSync(path.join(context.outputDir, "index.ts"), rootContent);
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/generators/errors/PluginLoadingFailure.ts
|
|
84
|
+
var PluginLoadingFailure = class PluginLoadingFailure extends Error {
|
|
85
|
+
constructor(pluginName, attempts) {
|
|
86
|
+
super(`Failed to load plugin '${pluginName}'`);
|
|
87
|
+
this.pluginName = pluginName;
|
|
88
|
+
this.attempts = attempts;
|
|
89
|
+
Object.setPrototypeOf(this, PluginLoadingFailure.prototype);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/generators/pluginLoader.ts
|
|
94
|
+
async function loadPlugins(registry, requiredPlugins, strategies, config) {
|
|
95
|
+
for (const requiredPlugin of requiredPlugins) registry.register(requiredPlugin);
|
|
96
|
+
if (!config?.plugins) return;
|
|
97
|
+
const successful = [];
|
|
98
|
+
for (const plugin of config.plugins) {
|
|
99
|
+
let result;
|
|
100
|
+
if (typeof plugin === "string") result = await loadPlugin(plugin, strategies);
|
|
101
|
+
else result = await loadPlugin(plugin[0], strategies);
|
|
102
|
+
if (result.success === false) throw new PluginLoadingFailure(result.error.pluginName, result.error.attempts);
|
|
103
|
+
successful.push(result.value);
|
|
104
|
+
registry.register(result.value.plugin);
|
|
105
|
+
}
|
|
106
|
+
reportSuccessfulLoads(successful);
|
|
107
|
+
}
|
|
108
|
+
async function loadPlugin(pluginName, strategies) {
|
|
109
|
+
const possiblePaths = generatePluginPaths(pluginName, strategies);
|
|
110
|
+
const attempts = [];
|
|
111
|
+
for (const possiblePath of possiblePaths) try {
|
|
112
|
+
const pluginPackage = await import(possiblePath);
|
|
113
|
+
if (pluginPackage.default) return {
|
|
114
|
+
success: true,
|
|
115
|
+
value: {
|
|
116
|
+
plugin: new pluginPackage.default(),
|
|
117
|
+
source: possiblePath
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
attempts.push({
|
|
121
|
+
path: possiblePath,
|
|
122
|
+
error: "No default export found"
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
attempts.push({
|
|
126
|
+
path: possiblePath,
|
|
127
|
+
error: error instanceof Error ? error.message : String(error)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
error: {
|
|
133
|
+
pluginName,
|
|
134
|
+
attempts
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function generatePluginPaths(pluginName, strategies) {
|
|
139
|
+
const paths = [];
|
|
140
|
+
for (const strategy of strategies) switch (strategy) {
|
|
141
|
+
case "npm":
|
|
142
|
+
paths.push(`@rexeus/typeweaver-${pluginName}`);
|
|
143
|
+
paths.push(`@rexeus/${pluginName}`);
|
|
144
|
+
break;
|
|
145
|
+
case "local":
|
|
146
|
+
paths.push(pluginName);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
return paths;
|
|
150
|
+
}
|
|
151
|
+
function reportSuccessfulLoads(successful) {
|
|
152
|
+
if (successful.length > 0) {
|
|
153
|
+
console.info(`Successfully loaded ${successful.length} plugin(s):`);
|
|
154
|
+
for (const result of successful) console.info(` - ${result.plugin.name} (from ${result.source})`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/generators/spec/specBundler.ts
|
|
159
|
+
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
|
|
160
|
+
const WINDOWS_UNC_PATH_PATTERN = /^\\\\/;
|
|
161
|
+
function createWrapperImportSpecifier(wrapperFile, inputFile) {
|
|
162
|
+
const absoluteInputFile = resolveBundledInputFile(inputFile);
|
|
163
|
+
const useWindowsPathSemantics = usesWindowsPathSemantics(wrapperFile, absoluteInputFile);
|
|
164
|
+
const pathModule = useWindowsPathSemantics ? path.win32 : path.posix;
|
|
165
|
+
const wrapperDir = useWindowsPathSemantics ? pathModule.dirname(wrapperFile) : resolveRealFilePath(pathModule.dirname(wrapperFile));
|
|
166
|
+
const resolvedInputFile = useWindowsPathSemantics ? absoluteInputFile : resolveRealFilePath(absoluteInputFile);
|
|
167
|
+
const relativeInputFile = pathModule.relative(wrapperDir, resolvedInputFile).replaceAll(pathModule.sep, "/");
|
|
168
|
+
if (relativeInputFile.startsWith(".") || relativeInputFile.startsWith("..")) return relativeInputFile;
|
|
169
|
+
return `./${relativeInputFile}`;
|
|
170
|
+
}
|
|
171
|
+
async function bundle(config) {
|
|
172
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "typeweaver-spec-loader-"));
|
|
173
|
+
const wrapperFile = path.join(tempDir, "spec-entrypoint.ts");
|
|
174
|
+
const bundledSpecFile = path.join(config.specOutputDir, "spec.js");
|
|
175
|
+
const wrapperImportSpecifier = createWrapperImportSpecifier(wrapperFile, config.inputFile);
|
|
176
|
+
fs.writeFileSync(wrapperFile, [
|
|
177
|
+
`import * as specModule from ${JSON.stringify(wrapperImportSpecifier)};`,
|
|
178
|
+
"const resolvedSpec =",
|
|
179
|
+
" Reflect.get(specModule, \"default\") ??",
|
|
180
|
+
" Reflect.get(specModule, \"spec\") ??",
|
|
181
|
+
" specModule;",
|
|
182
|
+
"",
|
|
183
|
+
"export default resolvedSpec;",
|
|
184
|
+
"export const spec = resolvedSpec;",
|
|
185
|
+
""
|
|
186
|
+
].join("\n"));
|
|
187
|
+
try {
|
|
188
|
+
await Rolldown.build({
|
|
189
|
+
cwd: tempDir,
|
|
190
|
+
input: wrapperFile,
|
|
191
|
+
treeshake: true,
|
|
192
|
+
external: (source) => {
|
|
193
|
+
if (source.startsWith("node:")) return true;
|
|
194
|
+
return !source.startsWith(".") && !path.isAbsolute(source);
|
|
195
|
+
},
|
|
196
|
+
output: {
|
|
197
|
+
file: bundledSpecFile,
|
|
198
|
+
format: "esm"
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
} finally {
|
|
202
|
+
fs.rmSync(tempDir, {
|
|
203
|
+
recursive: true,
|
|
204
|
+
force: true
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (!fs.existsSync(bundledSpecFile)) throw new Error(`Failed to bundle spec entrypoint '${config.inputFile}' to '${bundledSpecFile}'.`);
|
|
208
|
+
return bundledSpecFile;
|
|
209
|
+
}
|
|
210
|
+
function resolveBundledInputFile(inputFile) {
|
|
211
|
+
if (path.isAbsolute(inputFile)) return inputFile;
|
|
212
|
+
if (WINDOWS_ABSOLUTE_PATH_PATTERN.test(inputFile)) return path.win32.normalize(inputFile);
|
|
213
|
+
if (WINDOWS_UNC_PATH_PATTERN.test(inputFile)) return path.win32.normalize(inputFile);
|
|
214
|
+
return path.resolve(inputFile);
|
|
215
|
+
}
|
|
216
|
+
function usesWindowsPathSemantics(...filePaths) {
|
|
217
|
+
return filePaths.some((filePath) => {
|
|
218
|
+
return WINDOWS_ABSOLUTE_PATH_PATTERN.test(filePath) || WINDOWS_UNC_PATH_PATTERN.test(filePath);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function resolveRealFilePath(filePath) {
|
|
222
|
+
if (!fs.existsSync(filePath)) return filePath;
|
|
223
|
+
return fs.realpathSync.native(filePath);
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/generators/spec/InvalidSpecEntrypointError.ts
|
|
227
|
+
var InvalidSpecEntrypointError = class extends Error {
|
|
228
|
+
constructor(specEntrypoint) {
|
|
229
|
+
super(`Spec entrypoint '${specEntrypoint}' must export a SpecDefinition as its default export, named 'spec' export, or module namespace.`);
|
|
230
|
+
this.name = "InvalidSpecEntrypointError";
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/generators/spec/specGuards.ts
|
|
235
|
+
const validHttpStatusCodes = new Set(Object.values(HttpStatusCode).filter((statusCode) => typeof statusCode === "number"));
|
|
236
|
+
const isRecord = (value) => {
|
|
237
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
238
|
+
};
|
|
239
|
+
const isResponseDefinition = (value) => {
|
|
240
|
+
if (!isRecord(value)) return false;
|
|
241
|
+
return typeof value.name === "string" && value.name.length > 0 && typeof value.description === "string" && value.description.length > 0 && validHttpStatusCodes.has(value.statusCode);
|
|
242
|
+
};
|
|
243
|
+
const isOperationDefinition = (value) => {
|
|
244
|
+
if (!isRecord(value) || !Array.isArray(value.responses)) return false;
|
|
245
|
+
return typeof value.operationId === "string" && value.operationId.length > 0 && typeof value.path === "string" && value.path.length > 0 && typeof value.summary === "string" && value.summary.length > 0 && Object.values(HttpMethod).includes(value.method) && isRecord(value.request) && value.responses.length > 0 && value.responses.every((response) => isResponseDefinition(response));
|
|
246
|
+
};
|
|
247
|
+
const isResourceDefinition = (value) => {
|
|
248
|
+
return isRecord(value) && Array.isArray(value.operations) && value.operations.every(isOperationDefinition);
|
|
249
|
+
};
|
|
250
|
+
const isSpecDefinition = (value) => {
|
|
251
|
+
if (!isRecord(value) || !isRecord(value.resources)) return false;
|
|
252
|
+
return Object.values(value.resources).every(isResourceDefinition);
|
|
253
|
+
};
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/generators/spec/specImporter.ts
|
|
256
|
+
async function importDefinition(bundledSpecFile) {
|
|
257
|
+
const specModule = await import(pathToFileURL(bundledSpecFile).toString());
|
|
258
|
+
const definition = specModule.default ?? specModule.spec ?? specModule;
|
|
259
|
+
if (!isSpecDefinition(definition)) throw new InvalidSpecEntrypointError(bundledSpecFile);
|
|
260
|
+
return definition;
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/generators/specLoader.ts
|
|
264
|
+
async function loadSpec(config) {
|
|
265
|
+
fs.mkdirSync(config.specOutputDir, { recursive: true });
|
|
266
|
+
const bundledSpecFile = await bundle(config);
|
|
267
|
+
writeSpecDeclarationFile(config.specOutputDir);
|
|
268
|
+
const definition = await importDefinition(bundledSpecFile);
|
|
269
|
+
return {
|
|
270
|
+
definition,
|
|
271
|
+
normalizedSpec: normalizeSpec(definition)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function writeSpecDeclarationFile(specOutputDir) {
|
|
275
|
+
fs.writeFileSync(`${specOutputDir}/spec.d.ts`, [
|
|
276
|
+
"import type { SpecDefinition } from \"@rexeus/typeweaver-core\";",
|
|
277
|
+
"declare const _default: SpecDefinition;",
|
|
278
|
+
"export default _default;",
|
|
279
|
+
"export declare const spec: SpecDefinition;",
|
|
280
|
+
""
|
|
281
|
+
].join("\n"));
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/generators/Generator.ts
|
|
285
|
+
const moduleDir$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
286
|
+
/**
|
|
287
|
+
* Main generator for typeweaver
|
|
288
|
+
* Uses a plugin-based architecture for extensible code generation
|
|
289
|
+
*/
|
|
290
|
+
var Generator = class {
|
|
291
|
+
coreDir = "@rexeus/typeweaver-core";
|
|
292
|
+
templateDir = path.join(moduleDir$1, "templates");
|
|
293
|
+
registry = createPluginRegistry();
|
|
294
|
+
contextBuilder = createPluginContextBuilder();
|
|
295
|
+
requiredPlugins;
|
|
296
|
+
strategies;
|
|
297
|
+
inputFile = "";
|
|
298
|
+
outputDir = "";
|
|
299
|
+
specOutputDir = "";
|
|
300
|
+
responsesOutputDir = "";
|
|
301
|
+
constructor(requiredPlugins = [new TypesPlugin()], strategies) {
|
|
302
|
+
this.requiredPlugins = requiredPlugins;
|
|
303
|
+
this.strategies = strategies ?? ["npm", "local"];
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Generate code using the plugin system
|
|
307
|
+
*/
|
|
308
|
+
async generate(specFile, outputDir, config) {
|
|
309
|
+
console.info("Starting generation...");
|
|
310
|
+
this.initializeDirectories(specFile, outputDir);
|
|
311
|
+
if (config?.clean ?? true) {
|
|
312
|
+
console.info("Cleaning output directory...");
|
|
313
|
+
fs.rmSync(this.outputDir, {
|
|
314
|
+
recursive: true,
|
|
315
|
+
force: true
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
319
|
+
fs.mkdirSync(this.responsesOutputDir, { recursive: true });
|
|
320
|
+
fs.mkdirSync(this.specOutputDir, { recursive: true });
|
|
321
|
+
await loadPlugins(this.registry, this.requiredPlugins, this.strategies, config);
|
|
322
|
+
console.info(`Bundling spec from '${this.inputFile}' to '${this.specOutputDir}'...`);
|
|
323
|
+
let { normalizedSpec } = await loadSpec({
|
|
324
|
+
inputFile: this.inputFile,
|
|
325
|
+
specOutputDir: this.specOutputDir
|
|
326
|
+
});
|
|
327
|
+
const pluginContext = this.contextBuilder.createPluginContext({
|
|
328
|
+
outputDir: this.outputDir,
|
|
329
|
+
inputDir: path.dirname(this.inputFile),
|
|
330
|
+
config: config ?? {}
|
|
331
|
+
});
|
|
332
|
+
console.info("Initializing plugins...");
|
|
333
|
+
for (const registration of this.registry.getAll()) if (registration.plugin.initialize) await registration.plugin.initialize(pluginContext);
|
|
334
|
+
console.info("Collecting resources...");
|
|
335
|
+
for (const registration of this.registry.getAll()) if (registration.plugin.collectResources) normalizedSpec = await registration.plugin.collectResources(normalizedSpec);
|
|
336
|
+
const generatorContext = this.contextBuilder.createGeneratorContext({
|
|
337
|
+
outputDir: this.outputDir,
|
|
338
|
+
inputDir: path.dirname(this.inputFile),
|
|
339
|
+
config: config ?? {},
|
|
340
|
+
normalizedSpec,
|
|
341
|
+
templateDir: this.templateDir,
|
|
342
|
+
coreDir: this.coreDir,
|
|
343
|
+
responsesOutputDir: this.responsesOutputDir,
|
|
344
|
+
specOutputDir: this.specOutputDir
|
|
345
|
+
});
|
|
346
|
+
console.info("Generating code...");
|
|
347
|
+
for (const registration of this.registry.getAll()) {
|
|
348
|
+
console.info(`Running plugin: ${registration.plugin.name}`);
|
|
349
|
+
if (registration.plugin.generate) await registration.plugin.generate(generatorContext);
|
|
350
|
+
}
|
|
351
|
+
generateIndexFiles(this.templateDir, generatorContext);
|
|
352
|
+
console.info("Finalizing plugins...");
|
|
353
|
+
for (const registration of this.registry.getAll()) if (registration.plugin.finalize) await registration.plugin.finalize(pluginContext);
|
|
354
|
+
if (config?.format ?? true) await formatCode(this.outputDir);
|
|
355
|
+
console.info("Generation complete!");
|
|
356
|
+
console.info(`Generated files: ${this.contextBuilder.getGeneratedFiles().length}`);
|
|
357
|
+
}
|
|
358
|
+
initializeDirectories(specFile, outputDir) {
|
|
359
|
+
this.inputFile = specFile;
|
|
360
|
+
this.outputDir = outputDir;
|
|
361
|
+
this.responsesOutputDir = path.join(outputDir, "responses");
|
|
362
|
+
this.specOutputDir = path.join(this.outputDir, "spec");
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/cli.ts
|
|
367
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
368
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(moduleDir, "../package.json"), "utf-8"));
|
|
369
|
+
const program = new Command();
|
|
370
|
+
const execDir = process.cwd();
|
|
371
|
+
program.name("@rexeus/typeweaver").description("Type-safe API framework with code generation for TypeScript").version(packageJson.version);
|
|
372
|
+
program.command("generate").description("Generate types, validators, and clients from an API spec").option("-i, --input <inputPath>", "path to spec entrypoint file").option("-o, --output <outputDir>", "output directory for generated files").option("-c, --config <configFile>", "path to configuration file").option("-p, --plugins <plugins>", "comma-separated list of plugins to use").option("--format", "format generated code with oxfmt (default: true)").option("--no-format", "disable code formatting").option("--clean", "clean output directory before generation (default: true)").option("--no-clean", "disable cleaning output directory").action(async (options) => {
|
|
373
|
+
let config = {};
|
|
374
|
+
if (options.config) {
|
|
375
|
+
const configPath = path.isAbsolute(options.config) ? options.config : path.join(execDir, options.config);
|
|
376
|
+
try {
|
|
377
|
+
const configModule = await import(pathToFileURL(configPath).toString());
|
|
378
|
+
config = configModule.default ?? configModule;
|
|
379
|
+
console.info(`Loaded configuration from ${configPath}`);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error(`Failed to load configuration file: ${options.config}`);
|
|
382
|
+
console.error(error);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const inputPath = options.input ?? config.input;
|
|
387
|
+
const outputDir = options.output ?? config.output;
|
|
388
|
+
if (!inputPath) throw new Error("No input spec entrypoint provided. Use --input or specify in config file.");
|
|
389
|
+
if (!outputDir) throw new Error("No output directory provided. Use --output or specify in config file.");
|
|
390
|
+
const resolvedInputPath = path.isAbsolute(inputPath) ? inputPath : path.join(execDir, inputPath);
|
|
391
|
+
const resolvedOutputDir = path.isAbsolute(outputDir) ? outputDir : path.join(execDir, outputDir);
|
|
392
|
+
const finalConfig = {
|
|
393
|
+
input: resolvedInputPath,
|
|
394
|
+
output: resolvedOutputDir,
|
|
395
|
+
format: options.format ?? config.format ?? true,
|
|
396
|
+
clean: options.clean ?? config.clean ?? true
|
|
397
|
+
};
|
|
398
|
+
if (options.plugins) finalConfig.plugins = options.plugins.split(",").map((p) => p.trim());
|
|
399
|
+
else if (config.plugins) finalConfig.plugins = config.plugins;
|
|
400
|
+
return new Generator().generate(resolvedInputPath, resolvedOutputDir, finalConfig);
|
|
401
|
+
});
|
|
402
|
+
program.command("init").description("Initialize a new typeweaver project (coming soon)").action(() => {
|
|
403
|
+
console.log("The init command is coming soon!");
|
|
404
|
+
});
|
|
405
|
+
program.parse(process.argv);
|
|
406
|
+
//#endregion
|
|
407
|
+
export {};
|