@typokit/cli 0.1.4
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/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +13 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/build.d.ts +42 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +302 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +106 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +536 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/generate.d.ts +65 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +430 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/inspect.d.ts +26 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +579 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/migrate.d.ts +70 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +570 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/scaffold.d.ts +70 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +483 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/test.d.ts +56 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +248 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +245 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +33 -0
- package/dist/logger.js.map +1 -0
- package/package.json +33 -0
- package/src/bin.ts +22 -0
- package/src/commands/build.ts +433 -0
- package/src/commands/dev.ts +822 -0
- package/src/commands/generate.ts +640 -0
- package/src/commands/inspect.ts +885 -0
- package/src/commands/migrate.ts +800 -0
- package/src/commands/scaffold.ts +627 -0
- package/src/commands/test.ts +353 -0
- package/src/config.ts +93 -0
- package/src/dev.test.ts +285 -0
- package/src/env.d.ts +86 -0
- package/src/generate.test.ts +304 -0
- package/src/index.test.ts +217 -0
- package/src/index.ts +397 -0
- package/src/inspect.test.ts +411 -0
- package/src/logger.ts +49 -0
- package/src/migrate.test.ts +205 -0
- package/src/scaffold.test.ts +256 -0
- package/src/test.test.ts +230 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
// @typokit/cli — Generate Commands
|
|
2
|
+
|
|
3
|
+
import type { CliLogger } from "../logger.js";
|
|
4
|
+
import type { TypoKitConfig } from "../config.js";
|
|
5
|
+
|
|
6
|
+
export interface GenerateCommandOptions {
|
|
7
|
+
/** Project root directory */
|
|
8
|
+
rootDir: string;
|
|
9
|
+
/** Resolved configuration */
|
|
10
|
+
config: Required<TypoKitConfig>;
|
|
11
|
+
/** Logger instance */
|
|
12
|
+
logger: CliLogger;
|
|
13
|
+
/** Generate subcommand: db, client, openapi, tests */
|
|
14
|
+
subcommand: string;
|
|
15
|
+
/** CLI flags */
|
|
16
|
+
flags: Record<string, string | boolean>;
|
|
17
|
+
/** Whether verbose mode is enabled */
|
|
18
|
+
verbose: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GenerateResult {
|
|
22
|
+
/** Whether the command succeeded */
|
|
23
|
+
success: boolean;
|
|
24
|
+
/** Files generated or updated */
|
|
25
|
+
filesWritten: string[];
|
|
26
|
+
/** Duration in milliseconds */
|
|
27
|
+
duration: number;
|
|
28
|
+
/** Errors encountered */
|
|
29
|
+
errors: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve glob patterns to actual file paths.
|
|
34
|
+
* Reuses the same approach as build.ts.
|
|
35
|
+
*/
|
|
36
|
+
async function resolveFilePatterns(
|
|
37
|
+
rootDir: string,
|
|
38
|
+
patterns: string[],
|
|
39
|
+
): Promise<string[]> {
|
|
40
|
+
const { join, resolve } = (await import(/* @vite-ignore */ "path")) as {
|
|
41
|
+
join: (...args: string[]) => string;
|
|
42
|
+
resolve: (...args: string[]) => string;
|
|
43
|
+
};
|
|
44
|
+
const { readdirSync, statSync, existsSync } = (await import(
|
|
45
|
+
/* @vite-ignore */ "fs"
|
|
46
|
+
)) as {
|
|
47
|
+
readdirSync: (p: string) => string[];
|
|
48
|
+
statSync: (p: string) => { isFile(): boolean; isDirectory(): boolean };
|
|
49
|
+
existsSync: (p: string) => boolean;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const files: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const pattern of patterns) {
|
|
55
|
+
if (pattern.includes("*")) {
|
|
56
|
+
const parts = pattern.split("/");
|
|
57
|
+
const hasDoubleGlob = parts.includes("**");
|
|
58
|
+
const lastPart = parts[parts.length - 1];
|
|
59
|
+
|
|
60
|
+
const baseParts: string[] = [];
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
if (part.includes("*")) break;
|
|
63
|
+
baseParts.push(part);
|
|
64
|
+
}
|
|
65
|
+
const baseDir =
|
|
66
|
+
baseParts.length > 0 ? join(rootDir, ...baseParts) : rootDir;
|
|
67
|
+
|
|
68
|
+
if (!existsSync(baseDir)) continue;
|
|
69
|
+
|
|
70
|
+
const entries = hasDoubleGlob
|
|
71
|
+
? listFilesRecursive(baseDir, existsSync, readdirSync, statSync, join)
|
|
72
|
+
: readdirSync(baseDir).map((f) => join(baseDir, f));
|
|
73
|
+
|
|
74
|
+
const filePattern = lastPart.replace(/\*/g, ".*");
|
|
75
|
+
const regex = new RegExp(`^${filePattern}$`);
|
|
76
|
+
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const name = entry.split(/[\\/]/).pop() ?? "";
|
|
79
|
+
if (regex.test(name)) {
|
|
80
|
+
files.push(resolve(entry));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
const fullPath = resolve(join(rootDir, pattern));
|
|
85
|
+
if (existsSync(fullPath)) {
|
|
86
|
+
files.push(fullPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...new Set(files)].sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function listFilesRecursive(
|
|
95
|
+
dir: string,
|
|
96
|
+
existsSync: (p: string) => boolean,
|
|
97
|
+
readdirSync: (p: string) => string[],
|
|
98
|
+
statSync: (p: string) => { isFile(): boolean; isDirectory(): boolean },
|
|
99
|
+
join: (...args: string[]) => string,
|
|
100
|
+
): string[] {
|
|
101
|
+
if (!existsSync(dir)) return [];
|
|
102
|
+
const results: string[] = [];
|
|
103
|
+
const entries = readdirSync(dir);
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const fullPath = join(dir, entry);
|
|
106
|
+
try {
|
|
107
|
+
const stat = statSync(fullPath);
|
|
108
|
+
if (stat.isDirectory()) {
|
|
109
|
+
if (
|
|
110
|
+
entry !== "node_modules" &&
|
|
111
|
+
entry !== "dist" &&
|
|
112
|
+
entry !== ".typokit"
|
|
113
|
+
) {
|
|
114
|
+
results.push(
|
|
115
|
+
...listFilesRecursive(
|
|
116
|
+
fullPath,
|
|
117
|
+
existsSync,
|
|
118
|
+
readdirSync,
|
|
119
|
+
statSync,
|
|
120
|
+
join,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} else if (stat.isFile()) {
|
|
125
|
+
results.push(fullPath);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Skip files that can't be stat'd
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generate database schema artifacts using the configured database adapter.
|
|
136
|
+
*
|
|
137
|
+
* Resolves type files, extracts type metadata, and calls the database adapter's
|
|
138
|
+
* generate() method. If no adapter is configured, reports a helpful error.
|
|
139
|
+
*/
|
|
140
|
+
async function generateDb(
|
|
141
|
+
options: GenerateCommandOptions,
|
|
142
|
+
): Promise<GenerateResult> {
|
|
143
|
+
const startTime = Date.now();
|
|
144
|
+
const { config, rootDir, logger, verbose } = options;
|
|
145
|
+
const filesWritten: string[] = [];
|
|
146
|
+
const errors: string[] = [];
|
|
147
|
+
|
|
148
|
+
logger.step("generate:db", "Resolving type files...");
|
|
149
|
+
const typeFiles = await resolveFilePatterns(rootDir, config.typeFiles);
|
|
150
|
+
|
|
151
|
+
if (typeFiles.length === 0) {
|
|
152
|
+
logger.warn("No type files found matching configured patterns");
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
filesWritten,
|
|
156
|
+
duration: Date.now() - startTime,
|
|
157
|
+
errors,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (verbose) {
|
|
162
|
+
logger.verbose(`Type files: ${typeFiles.length} found`);
|
|
163
|
+
for (const f of typeFiles) logger.verbose(` ${f}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract types using transform-native
|
|
167
|
+
logger.step("generate:db", "Extracting type metadata...");
|
|
168
|
+
try {
|
|
169
|
+
const { parseAndExtractTypes } = (await import(
|
|
170
|
+
/* @vite-ignore */ "@typokit/transform-native"
|
|
171
|
+
)) as {
|
|
172
|
+
parseAndExtractTypes: (files: string[]) => Promise<
|
|
173
|
+
Record<
|
|
174
|
+
string,
|
|
175
|
+
{
|
|
176
|
+
name: string;
|
|
177
|
+
properties: Record<string, { type: string; optional: boolean }>;
|
|
178
|
+
}
|
|
179
|
+
>
|
|
180
|
+
>;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const types = await parseAndExtractTypes(typeFiles);
|
|
184
|
+
const typeCount = Object.keys(types).length;
|
|
185
|
+
|
|
186
|
+
if (typeCount === 0) {
|
|
187
|
+
logger.warn("No types extracted from source files");
|
|
188
|
+
return {
|
|
189
|
+
success: true,
|
|
190
|
+
filesWritten,
|
|
191
|
+
duration: Date.now() - startTime,
|
|
192
|
+
errors,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
logger.step("generate:db", `Extracted ${typeCount} types`);
|
|
197
|
+
|
|
198
|
+
// Generate schema artifacts using diffSchemas (generates DDL from types)
|
|
199
|
+
const { diffSchemas } = (await import(
|
|
200
|
+
/* @vite-ignore */ "@typokit/transform-native"
|
|
201
|
+
)) as {
|
|
202
|
+
diffSchemas: (
|
|
203
|
+
oldTypes: Record<string, unknown>,
|
|
204
|
+
newTypes: Record<string, unknown>,
|
|
205
|
+
name: string,
|
|
206
|
+
) => Promise<{
|
|
207
|
+
name: string;
|
|
208
|
+
sql: string;
|
|
209
|
+
destructive: boolean;
|
|
210
|
+
changes: unknown[];
|
|
211
|
+
}>;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const { join } = (await import(/* @vite-ignore */ "path")) as {
|
|
215
|
+
join: (...args: string[]) => string;
|
|
216
|
+
};
|
|
217
|
+
const nodeFs = (await import(/* @vite-ignore */ "fs")) as {
|
|
218
|
+
mkdirSync: (p: string, opts?: { recursive?: boolean }) => void;
|
|
219
|
+
writeFileSync: (p: string, data: string, encoding?: string) => void;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Diff empty schema against current types to generate full DDL
|
|
223
|
+
const migration = await diffSchemas({}, types, "initial");
|
|
224
|
+
const outputDir = join(rootDir, config.outputDir);
|
|
225
|
+
const schemaDir = join(outputDir, "schemas");
|
|
226
|
+
nodeFs.mkdirSync(schemaDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
// Write migration SQL
|
|
229
|
+
const sqlPath = join(schemaDir, "schema.sql");
|
|
230
|
+
nodeFs.writeFileSync(sqlPath, migration.sql, "utf-8");
|
|
231
|
+
filesWritten.push(sqlPath);
|
|
232
|
+
logger.success(`Generated ${sqlPath}`);
|
|
233
|
+
|
|
234
|
+
// Write schema metadata as JSON
|
|
235
|
+
const metaPath = join(schemaDir, "schema-types.json");
|
|
236
|
+
const metaJson = JSON.stringify(types, null, 2);
|
|
237
|
+
nodeFs.writeFileSync(metaPath, metaJson, "utf-8");
|
|
238
|
+
filesWritten.push(metaPath);
|
|
239
|
+
logger.success(`Generated ${metaPath}`);
|
|
240
|
+
|
|
241
|
+
if (verbose) {
|
|
242
|
+
logger.verbose(`Migration: ${migration.name}`);
|
|
243
|
+
logger.verbose(`Destructive: ${migration.destructive}`);
|
|
244
|
+
logger.verbose(`Changes: ${(migration.changes as unknown[]).length}`);
|
|
245
|
+
}
|
|
246
|
+
} catch (err: unknown) {
|
|
247
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
248
|
+
logger.error(`generate:db failed: ${message}`);
|
|
249
|
+
errors.push(message);
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
filesWritten,
|
|
253
|
+
duration: Date.now() - startTime,
|
|
254
|
+
errors,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const duration = Date.now() - startTime;
|
|
259
|
+
logger.success(
|
|
260
|
+
`generate:db complete — ${filesWritten.length} files written (${duration}ms)`,
|
|
261
|
+
);
|
|
262
|
+
return { success: true, filesWritten, duration, errors };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Generate a type-safe API client from route contracts.
|
|
267
|
+
*
|
|
268
|
+
* Reads compiled routes from the .typokit/ directory and generates
|
|
269
|
+
* a TypeScript client module.
|
|
270
|
+
*/
|
|
271
|
+
async function generateClient(
|
|
272
|
+
options: GenerateCommandOptions,
|
|
273
|
+
): Promise<GenerateResult> {
|
|
274
|
+
const startTime = Date.now();
|
|
275
|
+
const { config, rootDir, logger, verbose } = options;
|
|
276
|
+
const filesWritten: string[] = [];
|
|
277
|
+
const errors: string[] = [];
|
|
278
|
+
|
|
279
|
+
logger.step("generate:client", "Resolving route files...");
|
|
280
|
+
const routeFiles = await resolveFilePatterns(rootDir, config.routeFiles);
|
|
281
|
+
|
|
282
|
+
if (routeFiles.length === 0) {
|
|
283
|
+
logger.warn("No route files found matching configured patterns");
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
filesWritten,
|
|
287
|
+
duration: Date.now() - startTime,
|
|
288
|
+
errors,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (verbose) {
|
|
293
|
+
logger.verbose(`Route files: ${routeFiles.length} found`);
|
|
294
|
+
for (const f of routeFiles) logger.verbose(` ${f}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const { compileRoutes } = (await import(
|
|
299
|
+
/* @vite-ignore */ "@typokit/transform-native"
|
|
300
|
+
)) as {
|
|
301
|
+
compileRoutes: (files: string[]) => Promise<string>;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const { join } = (await import(/* @vite-ignore */ "path")) as {
|
|
305
|
+
join: (...args: string[]) => string;
|
|
306
|
+
};
|
|
307
|
+
const nodeFs = (await import(/* @vite-ignore */ "fs")) as {
|
|
308
|
+
mkdirSync: (p: string, opts?: { recursive?: boolean }) => void;
|
|
309
|
+
writeFileSync: (p: string, data: string, encoding?: string) => void;
|
|
310
|
+
existsSync: (p: string) => boolean;
|
|
311
|
+
readFileSync: (p: string, encoding: string) => string;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
logger.step("generate:client", "Compiling route contracts...");
|
|
315
|
+
const compiledRoutes = await compileRoutes(routeFiles);
|
|
316
|
+
|
|
317
|
+
const outputDir = join(rootDir, config.outputDir);
|
|
318
|
+
const clientDir = join(outputDir, "client");
|
|
319
|
+
nodeFs.mkdirSync(clientDir, { recursive: true });
|
|
320
|
+
|
|
321
|
+
// Generate client code from compiled routes
|
|
322
|
+
const clientCode = generateClientCode(compiledRoutes);
|
|
323
|
+
|
|
324
|
+
const clientPath = join(clientDir, "index.ts");
|
|
325
|
+
nodeFs.writeFileSync(clientPath, clientCode, "utf-8");
|
|
326
|
+
filesWritten.push(clientPath);
|
|
327
|
+
logger.success(`Generated ${clientPath}`);
|
|
328
|
+
|
|
329
|
+
if (verbose) {
|
|
330
|
+
logger.verbose(`Client code: ${clientCode.length} bytes`);
|
|
331
|
+
}
|
|
332
|
+
} catch (err: unknown) {
|
|
333
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
334
|
+
logger.error(`generate:client failed: ${message}`);
|
|
335
|
+
errors.push(message);
|
|
336
|
+
return {
|
|
337
|
+
success: false,
|
|
338
|
+
filesWritten,
|
|
339
|
+
duration: Date.now() - startTime,
|
|
340
|
+
errors,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const duration = Date.now() - startTime;
|
|
345
|
+
logger.success(
|
|
346
|
+
`generate:client complete — ${filesWritten.length} files written (${duration}ms)`,
|
|
347
|
+
);
|
|
348
|
+
return { success: true, filesWritten, duration, errors };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Generate a type-safe fetch client TypeScript module from compiled routes.
|
|
353
|
+
*/
|
|
354
|
+
function generateClientCode(compiledRoutes: string): string {
|
|
355
|
+
const lines: string[] = [];
|
|
356
|
+
lines.push("// Auto-generated by @typokit/cli — do not edit manually");
|
|
357
|
+
lines.push("// Re-run `typokit generate:client` to regenerate");
|
|
358
|
+
lines.push("");
|
|
359
|
+
lines.push("export interface ClientOptions {");
|
|
360
|
+
lines.push(" baseUrl: string;");
|
|
361
|
+
lines.push(" headers?: Record<string, string>;");
|
|
362
|
+
lines.push(" fetch?: typeof fetch;");
|
|
363
|
+
lines.push("}");
|
|
364
|
+
lines.push("");
|
|
365
|
+
lines.push("export interface RequestOptions {");
|
|
366
|
+
lines.push(" params?: Record<string, string>;");
|
|
367
|
+
lines.push(" query?: Record<string, unknown>;");
|
|
368
|
+
lines.push(" body?: unknown;");
|
|
369
|
+
lines.push(" headers?: Record<string, string>;");
|
|
370
|
+
lines.push("}");
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push("export function createClient(options: ClientOptions) {");
|
|
373
|
+
lines.push(
|
|
374
|
+
" const { baseUrl, headers: defaultHeaders, fetch: fetchFn = globalThis.fetch } = options;",
|
|
375
|
+
);
|
|
376
|
+
lines.push("");
|
|
377
|
+
lines.push(
|
|
378
|
+
" async function request(method: string, path: string, opts?: RequestOptions) {",
|
|
379
|
+
);
|
|
380
|
+
lines.push(" let url = baseUrl + path;");
|
|
381
|
+
lines.push(" if (opts?.params) {");
|
|
382
|
+
lines.push(" for (const [key, value] of Object.entries(opts.params)) {");
|
|
383
|
+
lines.push(
|
|
384
|
+
" url = url.replace(`:${key}`, encodeURIComponent(value));",
|
|
385
|
+
);
|
|
386
|
+
lines.push(" }");
|
|
387
|
+
lines.push(" }");
|
|
388
|
+
lines.push(" if (opts?.query) {");
|
|
389
|
+
lines.push(" const qs = Object.entries(opts.query)");
|
|
390
|
+
lines.push(" .filter(([, v]) => v !== undefined)");
|
|
391
|
+
lines.push(
|
|
392
|
+
" .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)",
|
|
393
|
+
);
|
|
394
|
+
lines.push(' .join("&");');
|
|
395
|
+
lines.push(' if (qs) url += "?" + qs;');
|
|
396
|
+
lines.push(" }");
|
|
397
|
+
lines.push(" const res = await fetchFn(url, {");
|
|
398
|
+
lines.push(" method,");
|
|
399
|
+
lines.push(" headers: {");
|
|
400
|
+
lines.push(' "Content-Type": "application/json",');
|
|
401
|
+
lines.push(" ...defaultHeaders,");
|
|
402
|
+
lines.push(" ...opts?.headers,");
|
|
403
|
+
lines.push(" },");
|
|
404
|
+
lines.push(" body: opts?.body ? JSON.stringify(opts.body) : undefined,");
|
|
405
|
+
lines.push(" });");
|
|
406
|
+
lines.push(
|
|
407
|
+
" if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);",
|
|
408
|
+
);
|
|
409
|
+
lines.push(" return res.json();");
|
|
410
|
+
lines.push(" }");
|
|
411
|
+
lines.push("");
|
|
412
|
+
lines.push(" return {");
|
|
413
|
+
lines.push(
|
|
414
|
+
' get: (path: string, opts?: RequestOptions) => request("GET", path, opts),',
|
|
415
|
+
);
|
|
416
|
+
lines.push(
|
|
417
|
+
' post: (path: string, opts?: RequestOptions) => request("POST", path, opts),',
|
|
418
|
+
);
|
|
419
|
+
lines.push(
|
|
420
|
+
' put: (path: string, opts?: RequestOptions) => request("PUT", path, opts),',
|
|
421
|
+
);
|
|
422
|
+
lines.push(
|
|
423
|
+
' patch: (path: string, opts?: RequestOptions) => request("PATCH", path, opts),',
|
|
424
|
+
);
|
|
425
|
+
lines.push(
|
|
426
|
+
' delete: (path: string, opts?: RequestOptions) => request("DELETE", path, opts),',
|
|
427
|
+
);
|
|
428
|
+
lines.push(" };");
|
|
429
|
+
lines.push("}");
|
|
430
|
+
lines.push("");
|
|
431
|
+
lines.push("// Compiled route information (for reference):");
|
|
432
|
+
lines.push("// " + compiledRoutes.split("\n")[0]);
|
|
433
|
+
lines.push("");
|
|
434
|
+
return lines.join("\n");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Generate OpenAPI 3.1 specification.
|
|
439
|
+
* Supports --output <path> flag for custom output location.
|
|
440
|
+
*/
|
|
441
|
+
async function generateOpenapi(
|
|
442
|
+
options: GenerateCommandOptions,
|
|
443
|
+
): Promise<GenerateResult> {
|
|
444
|
+
const startTime = Date.now();
|
|
445
|
+
const { config, rootDir, logger, verbose, flags } = options;
|
|
446
|
+
const filesWritten: string[] = [];
|
|
447
|
+
const errors: string[] = [];
|
|
448
|
+
|
|
449
|
+
logger.step("generate:openapi", "Resolving source files...");
|
|
450
|
+
const routeFiles = await resolveFilePatterns(rootDir, config.routeFiles);
|
|
451
|
+
const typeFiles = await resolveFilePatterns(rootDir, config.typeFiles);
|
|
452
|
+
|
|
453
|
+
if (routeFiles.length === 0) {
|
|
454
|
+
logger.warn("No route files found matching configured patterns");
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
filesWritten,
|
|
458
|
+
duration: Date.now() - startTime,
|
|
459
|
+
errors,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (verbose) {
|
|
464
|
+
logger.verbose(`Route files: ${routeFiles.length} found`);
|
|
465
|
+
logger.verbose(`Type files: ${typeFiles.length} found`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const { generateOpenApi } = (await import(
|
|
470
|
+
/* @vite-ignore */ "@typokit/transform-native"
|
|
471
|
+
)) as {
|
|
472
|
+
generateOpenApi: (
|
|
473
|
+
routeFiles: string[],
|
|
474
|
+
typeFiles: string[],
|
|
475
|
+
) => Promise<string>;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const { join, dirname } = (await import(/* @vite-ignore */ "path")) as {
|
|
479
|
+
join: (...args: string[]) => string;
|
|
480
|
+
dirname: (p: string) => string;
|
|
481
|
+
};
|
|
482
|
+
const nodeFs = (await import(/* @vite-ignore */ "fs")) as {
|
|
483
|
+
mkdirSync: (p: string, opts?: { recursive?: boolean }) => void;
|
|
484
|
+
writeFileSync: (p: string, data: string, encoding?: string) => void;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
logger.step("generate:openapi", "Generating OpenAPI 3.1 specification...");
|
|
488
|
+
const spec = await generateOpenApi(routeFiles, typeFiles);
|
|
489
|
+
|
|
490
|
+
// Determine output path: --output flag or default
|
|
491
|
+
const outputPath =
|
|
492
|
+
typeof flags["output"] === "string"
|
|
493
|
+
? flags["output"]
|
|
494
|
+
: join(rootDir, config.outputDir, "schemas", "openapi.json");
|
|
495
|
+
|
|
496
|
+
const dir = dirname(outputPath);
|
|
497
|
+
nodeFs.mkdirSync(dir, { recursive: true });
|
|
498
|
+
nodeFs.writeFileSync(outputPath, spec, "utf-8");
|
|
499
|
+
filesWritten.push(outputPath);
|
|
500
|
+
logger.success(`Generated ${outputPath}`);
|
|
501
|
+
|
|
502
|
+
if (verbose) {
|
|
503
|
+
logger.verbose(`OpenAPI spec: ${spec.length} bytes`);
|
|
504
|
+
}
|
|
505
|
+
} catch (err: unknown) {
|
|
506
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
507
|
+
logger.error(`generate:openapi failed: ${message}`);
|
|
508
|
+
errors.push(message);
|
|
509
|
+
return {
|
|
510
|
+
success: false,
|
|
511
|
+
filesWritten,
|
|
512
|
+
duration: Date.now() - startTime,
|
|
513
|
+
errors,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const duration = Date.now() - startTime;
|
|
518
|
+
logger.success(
|
|
519
|
+
`generate:openapi complete — ${filesWritten.length} files written (${duration}ms)`,
|
|
520
|
+
);
|
|
521
|
+
return { success: true, filesWritten, duration, errors };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Regenerate contract tests from route schemas.
|
|
526
|
+
*/
|
|
527
|
+
async function generateTests(
|
|
528
|
+
options: GenerateCommandOptions,
|
|
529
|
+
): Promise<GenerateResult> {
|
|
530
|
+
const startTime = Date.now();
|
|
531
|
+
const { config, rootDir, logger, verbose } = options;
|
|
532
|
+
const filesWritten: string[] = [];
|
|
533
|
+
const errors: string[] = [];
|
|
534
|
+
|
|
535
|
+
logger.step("generate:tests", "Resolving route files...");
|
|
536
|
+
const routeFiles = await resolveFilePatterns(rootDir, config.routeFiles);
|
|
537
|
+
|
|
538
|
+
if (routeFiles.length === 0) {
|
|
539
|
+
logger.warn("No route files found matching configured patterns");
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
filesWritten,
|
|
543
|
+
duration: Date.now() - startTime,
|
|
544
|
+
errors,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (verbose) {
|
|
549
|
+
logger.verbose(`Route files: ${routeFiles.length} found`);
|
|
550
|
+
for (const f of routeFiles) logger.verbose(` ${f}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
const { generateTestStubs } = (await import(
|
|
555
|
+
/* @vite-ignore */ "@typokit/transform-native"
|
|
556
|
+
)) as {
|
|
557
|
+
generateTestStubs: (files: string[]) => Promise<string>;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const { join } = (await import(/* @vite-ignore */ "path")) as {
|
|
561
|
+
join: (...args: string[]) => string;
|
|
562
|
+
};
|
|
563
|
+
const nodeFs = (await import(/* @vite-ignore */ "fs")) as {
|
|
564
|
+
mkdirSync: (p: string, opts?: { recursive?: boolean }) => void;
|
|
565
|
+
writeFileSync: (p: string, data: string, encoding?: string) => void;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
logger.step("generate:tests", "Generating contract test stubs...");
|
|
569
|
+
const testCode = await generateTestStubs(routeFiles);
|
|
570
|
+
|
|
571
|
+
const outputDir = join(rootDir, config.outputDir);
|
|
572
|
+
const testsDir = join(outputDir, "tests");
|
|
573
|
+
nodeFs.mkdirSync(testsDir, { recursive: true });
|
|
574
|
+
|
|
575
|
+
const testsPath = join(testsDir, "contract.test.ts");
|
|
576
|
+
nodeFs.writeFileSync(testsPath, testCode, "utf-8");
|
|
577
|
+
filesWritten.push(testsPath);
|
|
578
|
+
logger.success(`Generated ${testsPath}`);
|
|
579
|
+
|
|
580
|
+
if (verbose) {
|
|
581
|
+
logger.verbose(`Test stubs: ${testCode.length} bytes`);
|
|
582
|
+
}
|
|
583
|
+
} catch (err: unknown) {
|
|
584
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
585
|
+
logger.error(`generate:tests failed: ${message}`);
|
|
586
|
+
errors.push(message);
|
|
587
|
+
return {
|
|
588
|
+
success: false,
|
|
589
|
+
filesWritten,
|
|
590
|
+
duration: Date.now() - startTime,
|
|
591
|
+
errors,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const duration = Date.now() - startTime;
|
|
596
|
+
logger.success(
|
|
597
|
+
`generate:tests complete — ${filesWritten.length} files written (${duration}ms)`,
|
|
598
|
+
);
|
|
599
|
+
return { success: true, filesWritten, duration, errors };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Execute a generate subcommand.
|
|
604
|
+
* Dispatches to the appropriate generator based on the subcommand.
|
|
605
|
+
*/
|
|
606
|
+
export async function executeGenerate(
|
|
607
|
+
options: GenerateCommandOptions,
|
|
608
|
+
): Promise<GenerateResult> {
|
|
609
|
+
const { subcommand, logger } = options;
|
|
610
|
+
|
|
611
|
+
switch (subcommand) {
|
|
612
|
+
case "db":
|
|
613
|
+
return generateDb(options);
|
|
614
|
+
case "client":
|
|
615
|
+
return generateClient(options);
|
|
616
|
+
case "openapi":
|
|
617
|
+
return generateOpenapi(options);
|
|
618
|
+
case "tests":
|
|
619
|
+
return generateTests(options);
|
|
620
|
+
default:
|
|
621
|
+
logger.error(`Unknown generate subcommand: ${subcommand}`);
|
|
622
|
+
logger.info("Available subcommands: db, client, openapi, tests");
|
|
623
|
+
return {
|
|
624
|
+
success: false,
|
|
625
|
+
filesWritten: [],
|
|
626
|
+
duration: 0,
|
|
627
|
+
errors: [`Unknown generate subcommand: ${subcommand}`],
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Export individual generators for direct usage
|
|
633
|
+
export {
|
|
634
|
+
generateDb,
|
|
635
|
+
generateClient,
|
|
636
|
+
generateOpenapi,
|
|
637
|
+
generateTests,
|
|
638
|
+
generateClientCode,
|
|
639
|
+
resolveFilePatterns as resolveGenerateFilePatterns,
|
|
640
|
+
};
|