@vertz/codegen 0.2.0 → 0.2.3
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 +16 -227
- package/dist/index.d.ts +69 -54
- package/dist/index.js +714 -989
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
|
-
var VALID_GENERATORS = new Set(["typescript"
|
|
2
|
+
var VALID_GENERATORS = new Set(["typescript"]);
|
|
3
3
|
function defineCodegenConfig(config) {
|
|
4
4
|
return config;
|
|
5
5
|
}
|
|
@@ -9,8 +9,7 @@ function resolveCodegenConfig(config) {
|
|
|
9
9
|
outputDir: config?.outputDir ?? ".vertz/generated",
|
|
10
10
|
format: config?.format,
|
|
11
11
|
incremental: config?.incremental,
|
|
12
|
-
typescript: config?.typescript
|
|
13
|
-
cli: config?.cli
|
|
12
|
+
typescript: config?.typescript
|
|
14
13
|
};
|
|
15
14
|
}
|
|
16
15
|
function validateCodegenConfig(config) {
|
|
@@ -32,18 +31,6 @@ function validateCodegenConfig(config) {
|
|
|
32
31
|
errors.push("codegen.typescript.publishable.outputDir is required");
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
|
-
if (config.cli?.publishable) {
|
|
36
|
-
const pub = config.cli.publishable;
|
|
37
|
-
if (!pub.name) {
|
|
38
|
-
errors.push("codegen.cli.publishable.name is required");
|
|
39
|
-
}
|
|
40
|
-
if (!pub.outputDir) {
|
|
41
|
-
errors.push("codegen.cli.publishable.outputDir is required");
|
|
42
|
-
}
|
|
43
|
-
if (!pub.binName) {
|
|
44
|
-
errors.push("codegen.cli.publishable.binName is required");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
34
|
return errors;
|
|
48
35
|
}
|
|
49
36
|
// src/format.ts
|
|
@@ -128,8 +115,11 @@ async function formatWithBiome(files) {
|
|
|
128
115
|
}
|
|
129
116
|
}
|
|
130
117
|
// src/generate.ts
|
|
131
|
-
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
132
|
-
import { dirname as dirname3, join as join3 } from "node:path";
|
|
118
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
119
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
|
|
120
|
+
|
|
121
|
+
// src/generators/client-generator.ts
|
|
122
|
+
import { posix } from "node:path";
|
|
133
123
|
|
|
134
124
|
// src/utils/naming.ts
|
|
135
125
|
function splitWords(input) {
|
|
@@ -149,890 +139,472 @@ function toSnakeCase(input) {
|
|
|
149
139
|
return splitWords(input).map((w) => w.toLowerCase()).join("_");
|
|
150
140
|
}
|
|
151
141
|
|
|
152
|
-
// src/generators/
|
|
142
|
+
// src/generators/client-generator.ts
|
|
153
143
|
var FILE_HEADER = `// Generated by @vertz/codegen — do not edit
|
|
154
|
-
`;
|
|
155
|
-
function mapJsonSchemaTypeToCLI(type) {
|
|
156
|
-
if (type === "number" || type === "integer")
|
|
157
|
-
return "number";
|
|
158
|
-
if (type === "boolean")
|
|
159
|
-
return "boolean";
|
|
160
|
-
return "string";
|
|
161
|
-
}
|
|
162
|
-
function emitFieldDefinitions(schema, indent) {
|
|
163
|
-
const properties = schema.properties;
|
|
164
|
-
if (!properties)
|
|
165
|
-
return [];
|
|
166
|
-
const required = schema.required ?? [];
|
|
167
|
-
const lines = [];
|
|
168
|
-
for (const [key, propSchema] of Object.entries(properties)) {
|
|
169
|
-
const cliType = mapJsonSchemaTypeToCLI(propSchema.type);
|
|
170
|
-
const isRequired = required.includes(key);
|
|
171
|
-
const parts = [];
|
|
172
|
-
parts.push(`type: '${cliType}'`);
|
|
173
|
-
if (propSchema.description) {
|
|
174
|
-
parts.push(`description: '${propSchema.description}'`);
|
|
175
|
-
}
|
|
176
|
-
parts.push(`required: ${isRequired}`);
|
|
177
|
-
if (propSchema.enum) {
|
|
178
|
-
const enumValues = propSchema.enum.map((v) => `'${v}'`).join(", ");
|
|
179
|
-
parts.push(`enum: [${enumValues}]`);
|
|
180
|
-
}
|
|
181
|
-
lines.push(`${indent} ${key}: { ${parts.join(", ")} },`);
|
|
182
|
-
}
|
|
183
|
-
return lines;
|
|
184
|
-
}
|
|
185
|
-
function emitCommandDefinition(op) {
|
|
186
|
-
const lines = [];
|
|
187
|
-
lines.push("{");
|
|
188
|
-
lines.push(` method: '${op.method}',`);
|
|
189
|
-
lines.push(` path: '${op.path}',`);
|
|
190
|
-
lines.push(` description: '${op.description ?? op.operationId}',`);
|
|
191
|
-
if (op.params) {
|
|
192
|
-
const fields = emitFieldDefinitions(op.params, " ");
|
|
193
|
-
if (fields.length > 0) {
|
|
194
|
-
lines.push(" params: {");
|
|
195
|
-
lines.push(...fields);
|
|
196
|
-
lines.push(" },");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (op.query) {
|
|
200
|
-
const fields = emitFieldDefinitions(op.query, " ");
|
|
201
|
-
if (fields.length > 0) {
|
|
202
|
-
lines.push(" query: {");
|
|
203
|
-
lines.push(...fields);
|
|
204
|
-
lines.push(" },");
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (op.body) {
|
|
208
|
-
const fields = emitFieldDefinitions(op.body, " ");
|
|
209
|
-
if (fields.length > 0) {
|
|
210
|
-
lines.push(" body: {");
|
|
211
|
-
lines.push(...fields);
|
|
212
|
-
lines.push(" },");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
lines.push("}");
|
|
216
|
-
return lines.join(`
|
|
217
|
-
`);
|
|
218
|
-
}
|
|
219
|
-
function emitModuleCommands(module) {
|
|
220
|
-
if (module.operations.length === 0) {
|
|
221
|
-
return "{}";
|
|
222
|
-
}
|
|
223
|
-
const entries = [];
|
|
224
|
-
for (const op of module.operations) {
|
|
225
|
-
const commandName = toKebabCase(op.operationId);
|
|
226
|
-
const def = emitCommandDefinition(op);
|
|
227
|
-
const indented = def.split(`
|
|
228
|
-
`).map((line, i) => i === 0 ? line : ` ${line}`).join(`
|
|
229
|
-
`);
|
|
230
|
-
entries.push(` '${commandName}': ${indented},`);
|
|
231
|
-
}
|
|
232
|
-
return `{
|
|
233
|
-
${entries.join(`
|
|
234
|
-
`)}
|
|
235
|
-
}`;
|
|
236
|
-
}
|
|
237
|
-
function emitManifestFile(ir) {
|
|
238
|
-
const sections = [FILE_HEADER];
|
|
239
|
-
sections.push("import type { CommandManifest } from '@vertz/cli-runtime';");
|
|
240
|
-
sections.push("");
|
|
241
|
-
if (ir.modules.length === 0) {
|
|
242
|
-
sections.push("export const commands: CommandManifest = {};");
|
|
243
|
-
} else {
|
|
244
|
-
const namespaceEntries = [];
|
|
245
|
-
for (const mod of ir.modules) {
|
|
246
|
-
const cmds = emitModuleCommands(mod);
|
|
247
|
-
const indented = cmds.split(`
|
|
248
|
-
`).map((line, i) => i === 0 ? line : ` ${line}`).join(`
|
|
249
|
-
`);
|
|
250
|
-
namespaceEntries.push(` ${mod.name}: ${indented},`);
|
|
251
|
-
}
|
|
252
|
-
sections.push(`export const commands: CommandManifest = {
|
|
253
|
-
${namespaceEntries.join(`
|
|
254
|
-
`)}
|
|
255
|
-
};`);
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
path: "cli/manifest.ts",
|
|
259
|
-
content: sections.join(`
|
|
260
|
-
`)
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
function emitBinEntryPoint(options) {
|
|
264
|
-
const lines = [];
|
|
265
|
-
lines.push("#!/usr/bin/env node");
|
|
266
|
-
lines.push(FILE_HEADER);
|
|
267
|
-
lines.push("import { createCLI } from '@vertz/cli-runtime';");
|
|
268
|
-
lines.push("import { commands } from './manifest';");
|
|
269
|
-
lines.push("");
|
|
270
|
-
lines.push("const cli = createCLI({");
|
|
271
|
-
lines.push(` name: '${options.cliName}',`);
|
|
272
|
-
lines.push(` version: '${options.cliVersion}',`);
|
|
273
|
-
lines.push(" commands,");
|
|
274
|
-
lines.push("});");
|
|
275
|
-
lines.push("");
|
|
276
|
-
lines.push("cli.run(process.argv.slice(2));");
|
|
277
|
-
return {
|
|
278
|
-
path: "cli/bin.ts",
|
|
279
|
-
content: lines.join(`
|
|
280
|
-
`)
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function scaffoldCLIPackageJson(options) {
|
|
284
|
-
const pkg = {
|
|
285
|
-
name: options.packageName,
|
|
286
|
-
version: options.packageVersion ?? "0.0.0",
|
|
287
|
-
description: `CLI for ${options.cliName} — generated by @vertz/codegen`,
|
|
288
|
-
private: true,
|
|
289
|
-
bin: {
|
|
290
|
-
[options.cliName]: "./cli/bin.ts"
|
|
291
|
-
},
|
|
292
|
-
dependencies: {
|
|
293
|
-
"@vertz/cli-runtime": "*",
|
|
294
|
-
"@vertz/fetch": "*"
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
return {
|
|
298
|
-
path: "package.json",
|
|
299
|
-
content: JSON.stringify(pkg, null, 2)
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
function scaffoldCLIRootIndex() {
|
|
303
|
-
const lines = [FILE_HEADER];
|
|
304
|
-
lines.push("export { commands } from './cli/manifest';");
|
|
305
|
-
return {
|
|
306
|
-
path: "index.ts",
|
|
307
|
-
content: lines.join(`
|
|
308
|
-
`)
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// src/utils/imports.ts
|
|
313
|
-
function mergeImports(imports) {
|
|
314
|
-
const seen = new Map;
|
|
315
|
-
for (const imp of imports) {
|
|
316
|
-
const key = `${imp.from}::${imp.name}::${imp.isType}::${imp.alias ?? ""}`;
|
|
317
|
-
if (!seen.has(key)) {
|
|
318
|
-
seen.set(key, imp);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return [...seen.values()].sort((a, b) => {
|
|
322
|
-
const fromCmp = a.from.localeCompare(b.from);
|
|
323
|
-
if (fromCmp !== 0)
|
|
324
|
-
return fromCmp;
|
|
325
|
-
return a.name.localeCompare(b.name);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
function renderImports(imports) {
|
|
329
|
-
const grouped = new Map;
|
|
330
|
-
for (const imp of imports) {
|
|
331
|
-
let group = grouped.get(imp.from);
|
|
332
|
-
if (!group) {
|
|
333
|
-
group = { types: [], values: [] };
|
|
334
|
-
grouped.set(imp.from, group);
|
|
335
|
-
}
|
|
336
|
-
const nameStr = imp.alias ? `${imp.name} as ${imp.alias}` : imp.name;
|
|
337
|
-
if (imp.isType) {
|
|
338
|
-
group.types.push(nameStr);
|
|
339
|
-
} else {
|
|
340
|
-
group.values.push(nameStr);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
const lines = [];
|
|
344
|
-
for (const [from, group] of grouped) {
|
|
345
|
-
if (group.types.length > 0) {
|
|
346
|
-
lines.push(`import type { ${group.types.join(", ")} } from '${from}';`);
|
|
347
|
-
}
|
|
348
|
-
if (group.values.length > 0) {
|
|
349
|
-
lines.push(`import { ${group.values.join(", ")} } from '${from}';`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return lines.join(`
|
|
353
|
-
`);
|
|
354
|
-
}
|
|
355
144
|
|
|
356
|
-
// src/generators/typescript/emit-client.ts
|
|
357
|
-
var FILE_HEADER2 = `// Generated by @vertz/codegen — do not edit
|
|
358
145
|
`;
|
|
359
|
-
function emitSDKConfig(auth) {
|
|
360
|
-
const imports = [{ from: "@vertz/fetch", name: "FetchClientConfig", isType: true }];
|
|
361
|
-
const fields = [];
|
|
362
|
-
for (const scheme of auth.schemes) {
|
|
363
|
-
if (scheme.type === "bearer") {
|
|
364
|
-
fields.push(` /** Bearer token or function returning a token. */
|
|
365
|
-
token?: string | (() => string | Promise<string>);`);
|
|
366
|
-
} else if (scheme.type === "apiKey") {
|
|
367
|
-
fields.push(` /** API key or function returning a key. */
|
|
368
|
-
apiKey?: string | (() => string | Promise<string>);`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (fields.length === 0) {
|
|
372
|
-
return {
|
|
373
|
-
content: "export interface SDKConfig extends FetchClientConfig {}",
|
|
374
|
-
imports
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
const content = `export interface SDKConfig extends FetchClientConfig {
|
|
378
|
-
${fields.join(`
|
|
379
|
-
`)}
|
|
380
|
-
}`;
|
|
381
|
-
return { content, imports };
|
|
382
|
-
}
|
|
383
|
-
function emitAuthStrategyBuilder(auth) {
|
|
384
|
-
const imports = [{ from: "@vertz/fetch", name: "AuthStrategy", isType: true }];
|
|
385
|
-
const lines = [];
|
|
386
|
-
lines.push("const authStrategies: AuthStrategy[] = [...(config.authStrategies ?? [])];");
|
|
387
|
-
for (const scheme of auth.schemes) {
|
|
388
|
-
if (scheme.type === "bearer") {
|
|
389
|
-
lines.push(`if (config.token) {
|
|
390
|
-
authStrategies.push({ type: 'bearer', token: config.token });
|
|
391
|
-
}`);
|
|
392
|
-
} else if (scheme.type === "apiKey") {
|
|
393
|
-
lines.push(`if (config.apiKey) {
|
|
394
|
-
authStrategies.push({ type: 'apiKey', key: config.apiKey, location: '${scheme.in}', name: '${scheme.paramName}' });
|
|
395
|
-
}`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return { content: lines.join(`
|
|
399
|
-
`), imports };
|
|
400
|
-
}
|
|
401
|
-
function buildPathExpression(path) {
|
|
402
|
-
if (!path.includes(":")) {
|
|
403
|
-
return `'${path}'`;
|
|
404
|
-
}
|
|
405
|
-
const interpolated = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "${input.params.$1}");
|
|
406
|
-
return `\`${interpolated}\``;
|
|
407
|
-
}
|
|
408
|
-
function buildRequestOptions(op) {
|
|
409
|
-
const opts = [];
|
|
410
|
-
if (op.query) {
|
|
411
|
-
opts.push("query: input?.query");
|
|
412
|
-
}
|
|
413
|
-
if (op.body) {
|
|
414
|
-
opts.push("body: input.body");
|
|
415
|
-
}
|
|
416
|
-
if (op.headers) {
|
|
417
|
-
opts.push("headers: input?.headers");
|
|
418
|
-
}
|
|
419
|
-
if (opts.length === 0) {
|
|
420
|
-
return "";
|
|
421
|
-
}
|
|
422
|
-
return `, { ${opts.join(", ")} }`;
|
|
423
|
-
}
|
|
424
|
-
function hasOperationInput(op) {
|
|
425
|
-
return !!(op.params || op.query || op.body || op.headers);
|
|
426
|
-
}
|
|
427
|
-
function isInputOptional(op) {
|
|
428
|
-
return !op.params && !op.body;
|
|
429
|
-
}
|
|
430
|
-
function emitOperationMethod(op) {
|
|
431
|
-
const imports = [];
|
|
432
|
-
const methodName = toCamelCase(op.operationId);
|
|
433
|
-
const inputTypeName = `${toPascalCase(op.operationId)}Input`;
|
|
434
|
-
const responseTypeName = `${toPascalCase(op.operationId)}Response`;
|
|
435
|
-
const hasInput = hasOperationInput(op);
|
|
436
|
-
if (hasInput) {
|
|
437
|
-
const optional = isInputOptional(op);
|
|
438
|
-
const inputParam = optional ? `input?: ${inputTypeName}` : `input: ${inputTypeName}`;
|
|
439
|
-
imports.push({ from: "../types", name: inputTypeName, isType: true });
|
|
440
|
-
imports.push({ from: "../types", name: responseTypeName, isType: true });
|
|
441
|
-
const pathExpr2 = buildPathExpression(op.path);
|
|
442
|
-
const reqOpts = buildRequestOptions(op);
|
|
443
|
-
const content2 = `${methodName}(${inputParam}): Promise<SDKResult<${responseTypeName}>> {
|
|
444
|
-
return client.request('${op.method}', ${pathExpr2}${reqOpts});
|
|
445
|
-
}`;
|
|
446
|
-
return { content: content2, imports };
|
|
447
|
-
}
|
|
448
|
-
imports.push({ from: "../types", name: responseTypeName, isType: true });
|
|
449
|
-
const pathExpr = buildPathExpression(op.path);
|
|
450
|
-
const content = `${methodName}(): Promise<SDKResult<${responseTypeName}>> {
|
|
451
|
-
return client.request('${op.method}', ${pathExpr});
|
|
452
|
-
}`;
|
|
453
|
-
return { content, imports };
|
|
454
|
-
}
|
|
455
|
-
function emitStreamingMethod(op) {
|
|
456
|
-
const imports = [];
|
|
457
|
-
const methodName = toCamelCase(op.operationId);
|
|
458
|
-
const inputTypeName = `${toPascalCase(op.operationId)}Input`;
|
|
459
|
-
const eventTypeName = `${toPascalCase(op.operationId)}Event`;
|
|
460
|
-
const format = op.streaming?.format ?? "sse";
|
|
461
|
-
imports.push({ from: "../types", name: eventTypeName, isType: true });
|
|
462
|
-
const hasInput = hasOperationInput(op);
|
|
463
|
-
const pathExpr = buildPathExpression(op.path);
|
|
464
|
-
const streamOpts = [];
|
|
465
|
-
streamOpts.push(`method: '${op.method}'`);
|
|
466
|
-
streamOpts.push(`path: ${pathExpr}`);
|
|
467
|
-
if (op.query) {
|
|
468
|
-
streamOpts.push("query: input?.query");
|
|
469
|
-
}
|
|
470
|
-
streamOpts.push(`format: '${format}'`);
|
|
471
|
-
const optsStr = streamOpts.join(`,
|
|
472
|
-
`);
|
|
473
|
-
if (hasInput) {
|
|
474
|
-
const optional = isInputOptional(op);
|
|
475
|
-
const inputParam = optional ? `input?: ${inputTypeName}` : `input: ${inputTypeName}`;
|
|
476
|
-
imports.push({ from: "../types", name: inputTypeName, isType: true });
|
|
477
|
-
const content2 = `async *${methodName}(${inputParam}): AsyncGenerator<${eventTypeName}> {
|
|
478
|
-
yield* client.requestStream<${eventTypeName}>({
|
|
479
|
-
${optsStr},
|
|
480
|
-
});
|
|
481
|
-
}`;
|
|
482
|
-
return { content: content2, imports };
|
|
483
|
-
}
|
|
484
|
-
const content = `async *${methodName}(): AsyncGenerator<${eventTypeName}> {
|
|
485
|
-
yield* client.requestStream<${eventTypeName}>({
|
|
486
|
-
${optsStr},
|
|
487
|
-
});
|
|
488
|
-
}`;
|
|
489
|
-
return { content, imports };
|
|
490
|
-
}
|
|
491
|
-
function emitModuleFile(module) {
|
|
492
|
-
const fragments = [];
|
|
493
|
-
for (const op of module.operations) {
|
|
494
|
-
if (op.streaming) {
|
|
495
|
-
fragments.push(emitStreamingMethod(op));
|
|
496
|
-
} else {
|
|
497
|
-
fragments.push(emitOperationMethod(op));
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
const allImports = mergeImports(fragments.flatMap((f) => f.imports));
|
|
501
|
-
const importBlock = renderImports(allImports);
|
|
502
|
-
const factoryName = `create${toPascalCase(module.name)}Module`;
|
|
503
|
-
const methods = fragments.map((f) => ` ${f.content}`).join(`,
|
|
504
|
-
`);
|
|
505
|
-
const sections = [FILE_HEADER2];
|
|
506
|
-
if (importBlock) {
|
|
507
|
-
sections.push(importBlock);
|
|
508
|
-
sections.push("");
|
|
509
|
-
}
|
|
510
|
-
sections.push(`export function ${factoryName}(client: FetchClient) {
|
|
511
|
-
return {
|
|
512
|
-
${methods},
|
|
513
|
-
};
|
|
514
|
-
}`);
|
|
515
|
-
const fetchClientImport = "import { FetchClient } from '@vertz/fetch';";
|
|
516
|
-
const content = sections.join(`
|
|
517
|
-
`);
|
|
518
|
-
const withFetchImport = content.replace(FILE_HEADER2, `${FILE_HEADER2}
|
|
519
|
-
${fetchClientImport}
|
|
520
|
-
`);
|
|
521
|
-
return {
|
|
522
|
-
path: `modules/${module.name}.ts`,
|
|
523
|
-
content: withFetchImport
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
function emitClientFile(ir) {
|
|
527
|
-
const sections = [FILE_HEADER2];
|
|
528
|
-
const imports = [];
|
|
529
|
-
imports.push({ from: "@vertz/fetch", name: "FetchClient", isType: false });
|
|
530
|
-
const configFragment = emitSDKConfig(ir.auth);
|
|
531
|
-
imports.push(...configFragment.imports);
|
|
532
|
-
const authFragment = emitAuthStrategyBuilder(ir.auth);
|
|
533
|
-
imports.push(...authFragment.imports);
|
|
534
|
-
for (const mod of ir.modules) {
|
|
535
|
-
const factoryName = `create${toPascalCase(mod.name)}Module`;
|
|
536
|
-
imports.push({ from: `./modules/${mod.name}`, name: factoryName, isType: false });
|
|
537
|
-
}
|
|
538
|
-
const allImports = mergeImports(imports);
|
|
539
|
-
const importBlock = renderImports(allImports);
|
|
540
|
-
if (importBlock) {
|
|
541
|
-
sections.push(importBlock);
|
|
542
|
-
sections.push("");
|
|
543
|
-
}
|
|
544
|
-
sections.push(`export interface SDKResult<T> {
|
|
545
|
-
data: T;
|
|
546
|
-
status: number;
|
|
547
|
-
headers: Headers;
|
|
548
|
-
}`);
|
|
549
|
-
sections.push("");
|
|
550
|
-
sections.push(configFragment.content);
|
|
551
|
-
sections.push("");
|
|
552
|
-
const moduleEntries = ir.modules.map((mod) => ` ${toCamelCase(mod.name)}: create${toPascalCase(mod.name)}Module(client)`).join(`,
|
|
553
|
-
`);
|
|
554
|
-
const createClientBody = [
|
|
555
|
-
"export function createClient(config: SDKConfig) {",
|
|
556
|
-
` ${authFragment.content.split(`
|
|
557
|
-
`).join(`
|
|
558
|
-
`)}`,
|
|
559
|
-
"",
|
|
560
|
-
" const client = new FetchClient({",
|
|
561
|
-
" ...config,",
|
|
562
|
-
" authStrategies,",
|
|
563
|
-
" });",
|
|
564
|
-
"",
|
|
565
|
-
" return {",
|
|
566
|
-
moduleEntries,
|
|
567
|
-
" };",
|
|
568
|
-
"}"
|
|
569
|
-
].join(`
|
|
570
|
-
`);
|
|
571
|
-
sections.push(createClientBody);
|
|
572
|
-
return {
|
|
573
|
-
path: "client.ts",
|
|
574
|
-
content: sections.join(`
|
|
575
|
-
`)
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
146
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
147
|
+
class ClientGenerator {
|
|
148
|
+
name = "client";
|
|
149
|
+
generate(ir, config) {
|
|
150
|
+
return [
|
|
151
|
+
this.generateClient(ir),
|
|
152
|
+
this.generatePackageJson(config.outputDir),
|
|
153
|
+
this.generateReadme(ir)
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
generateClient(ir) {
|
|
157
|
+
const entities = ir.entities ?? [];
|
|
158
|
+
const lines = [FILE_HEADER];
|
|
159
|
+
if (entities.length > 0) {
|
|
160
|
+
lines.push("import { FetchClient } from '@vertz/fetch';");
|
|
161
|
+
for (const entity of entities) {
|
|
162
|
+
const pascal = toPascalCase(entity.entityName);
|
|
163
|
+
lines.push(`import { create${pascal}Sdk } from './entities/${entity.entityName}';`);
|
|
164
|
+
}
|
|
165
|
+
lines.push("");
|
|
596
166
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
167
|
+
lines.push("export interface ClientOptions {");
|
|
168
|
+
lines.push(" baseURL?: string;");
|
|
169
|
+
lines.push(" headers?: Record<string, string>;");
|
|
170
|
+
lines.push(" timeoutMs?: number;");
|
|
171
|
+
lines.push("}");
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push("export function createClient(options: ClientOptions = {}) {");
|
|
174
|
+
if (entities.length > 0) {
|
|
175
|
+
lines.push(` const client = new FetchClient({ baseURL: options.baseURL ?? '/api', headers: options.headers, timeoutMs: options.timeoutMs });`);
|
|
176
|
+
lines.push(" return {");
|
|
177
|
+
for (const entity of entities) {
|
|
178
|
+
const pascal = toPascalCase(entity.entityName);
|
|
179
|
+
const camel = toCamelCase(entity.entityName);
|
|
180
|
+
lines.push(` ${camel}: create${pascal}Sdk(client),`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(" };");
|
|
183
|
+
} else {
|
|
184
|
+
lines.push(" return {};");
|
|
603
185
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
186
|
+
lines.push("}");
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("export type Client = ReturnType<typeof createClient>;");
|
|
189
|
+
return { path: "client.ts", content: lines.join(`
|
|
190
|
+
`) };
|
|
191
|
+
}
|
|
192
|
+
generatePackageJson(outputDir) {
|
|
193
|
+
const dir = `./${posix.normalize(outputDir.replace(/\\/g, "/"))}`;
|
|
194
|
+
const pkg = {
|
|
195
|
+
imports: {
|
|
196
|
+
"#generated": `${dir}/client.ts`,
|
|
197
|
+
"#generated/types": `${dir}/types/index.ts`
|
|
609
198
|
}
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
${fields.join(`,
|
|
614
|
-
`)},
|
|
615
|
-
})`;
|
|
616
|
-
}
|
|
617
|
-
return "s.unknown()";
|
|
618
|
-
}
|
|
619
|
-
function emitSchemaReExports(schemas) {
|
|
620
|
-
const sections = [FILE_HEADER3];
|
|
621
|
-
if (schemas.length > 0) {
|
|
622
|
-
sections.push("import { s } from '@vertz/schema';");
|
|
623
|
-
sections.push("");
|
|
624
|
-
}
|
|
625
|
-
for (const schema of schemas) {
|
|
626
|
-
const validatorName = `${schema.name}Schema`;
|
|
627
|
-
const schemaCall = jsonSchemaToSchemaCall(schema.jsonSchema);
|
|
628
|
-
sections.push(`export const ${validatorName} = ${schemaCall};`);
|
|
629
|
-
}
|
|
630
|
-
return {
|
|
631
|
-
path: "schemas.ts",
|
|
632
|
-
content: sections.join(`
|
|
633
|
-
`)
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
function emitBarrelIndex(ir) {
|
|
637
|
-
const lines = [FILE_HEADER3];
|
|
638
|
-
lines.push("export { createClient } from './client';");
|
|
639
|
-
lines.push("export type { SDKConfig, SDKResult } from './client';");
|
|
640
|
-
for (const mod of ir.modules) {
|
|
641
|
-
lines.push(`export * from './types/${mod.name}';`);
|
|
642
|
-
}
|
|
643
|
-
if (ir.schemas.length > 0) {
|
|
644
|
-
lines.push("export * from './types/shared';");
|
|
645
|
-
}
|
|
646
|
-
if (ir.schemas.length > 0) {
|
|
647
|
-
lines.push("export * from './schemas';");
|
|
648
|
-
}
|
|
649
|
-
return {
|
|
650
|
-
path: "index.ts",
|
|
651
|
-
content: lines.join(`
|
|
652
|
-
`)
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
function emitPackageJson(ir, options) {
|
|
656
|
-
const dependencies = {
|
|
657
|
-
"@vertz/fetch": "*"
|
|
658
|
-
};
|
|
659
|
-
if (ir.schemas.length > 0) {
|
|
660
|
-
dependencies["@vertz/schema"] = "*";
|
|
199
|
+
};
|
|
200
|
+
return { path: "package.json", content: `${JSON.stringify(pkg, null, 2)}
|
|
201
|
+
` };
|
|
661
202
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const type = convert(schema, context);
|
|
691
|
-
return { type, extractedTypes: context.namedTypes };
|
|
692
|
-
}
|
|
693
|
-
function convert(schema, _ctx) {
|
|
694
|
-
if (schema.$defs && typeof schema.$defs === "object") {
|
|
695
|
-
const defs = schema.$defs;
|
|
696
|
-
for (const [name, defSchema] of Object.entries(defs)) {
|
|
697
|
-
if (!_ctx.namedTypes.has(name)) {
|
|
698
|
-
_ctx.resolving.add(name);
|
|
699
|
-
const typeStr = convert(defSchema, _ctx);
|
|
700
|
-
_ctx.resolving.delete(name);
|
|
701
|
-
_ctx.namedTypes.set(name, typeStr);
|
|
203
|
+
generateReadme(ir) {
|
|
204
|
+
const entities = ir.entities ?? [];
|
|
205
|
+
const lines = [];
|
|
206
|
+
lines.push("# Generated Client");
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push("Auto-generated by `@vertz/codegen`. Do not edit manually.");
|
|
209
|
+
lines.push("");
|
|
210
|
+
lines.push("## Usage");
|
|
211
|
+
lines.push("");
|
|
212
|
+
lines.push("```typescript");
|
|
213
|
+
lines.push("import { createClient } from '#generated';");
|
|
214
|
+
lines.push("");
|
|
215
|
+
lines.push("const api = createClient();");
|
|
216
|
+
lines.push("```");
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push("## Type Imports");
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push("```typescript");
|
|
221
|
+
lines.push("import type { ... } from '#generated/types';");
|
|
222
|
+
lines.push("```");
|
|
223
|
+
if (entities.length > 0) {
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push("## Available Resources");
|
|
226
|
+
lines.push("");
|
|
227
|
+
for (const entity of entities) {
|
|
228
|
+
const camel = toCamelCase(entity.entityName);
|
|
229
|
+
const methods = this.getEntityMethods(entity);
|
|
230
|
+
lines.push(`- \`client.${camel}\` — ${methods}`);
|
|
702
231
|
}
|
|
703
232
|
}
|
|
233
|
+
lines.push("");
|
|
234
|
+
return { path: "README.md", content: lines.join(`
|
|
235
|
+
`) };
|
|
704
236
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
237
|
+
getEntityMethods(entity) {
|
|
238
|
+
const methods = [];
|
|
239
|
+
for (const op of entity.operations) {
|
|
240
|
+
methods.push(op.kind);
|
|
708
241
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (schema.const !== undefined) {
|
|
712
|
-
return toLiteral(schema.const);
|
|
713
|
-
}
|
|
714
|
-
if (Array.isArray(schema.enum)) {
|
|
715
|
-
return schema.enum.map((v) => toLiteral(v)).join(" | ");
|
|
716
|
-
}
|
|
717
|
-
if (Array.isArray(schema.oneOf)) {
|
|
718
|
-
return schema.oneOf.map((s) => convert(s, _ctx)).join(" | ");
|
|
719
|
-
}
|
|
720
|
-
if (Array.isArray(schema.anyOf)) {
|
|
721
|
-
return schema.anyOf.map((s) => convert(s, _ctx)).join(" | ");
|
|
722
|
-
}
|
|
723
|
-
if (Array.isArray(schema.allOf)) {
|
|
724
|
-
return schema.allOf.map((s) => convert(s, _ctx)).join(" & ");
|
|
725
|
-
}
|
|
726
|
-
if (Array.isArray(schema.type)) {
|
|
727
|
-
return schema.type.map((t) => PRIMITIVE_MAP[t] ?? t).join(" | ");
|
|
728
|
-
}
|
|
729
|
-
if (typeof schema.type === "string") {
|
|
730
|
-
const type = schema.type;
|
|
731
|
-
if (type === "array") {
|
|
732
|
-
if (Array.isArray(schema.prefixItems)) {
|
|
733
|
-
const items = schema.prefixItems.map((s) => convert(s, _ctx));
|
|
734
|
-
return `[${items.join(", ")}]`;
|
|
735
|
-
}
|
|
736
|
-
if (schema.items && typeof schema.items === "object") {
|
|
737
|
-
const itemType = convert(schema.items, _ctx);
|
|
738
|
-
return itemType.includes(" | ") ? `(${itemType})[]` : `${itemType}[]`;
|
|
739
|
-
}
|
|
740
|
-
return "unknown[]";
|
|
741
|
-
}
|
|
742
|
-
if (type === "object") {
|
|
743
|
-
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && !schema.properties) {
|
|
744
|
-
const valueType = convert(schema.additionalProperties, _ctx);
|
|
745
|
-
return `Record<string, ${valueType}>`;
|
|
746
|
-
}
|
|
747
|
-
if (schema.properties && typeof schema.properties === "object") {
|
|
748
|
-
const props = schema.properties;
|
|
749
|
-
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
750
|
-
const parts = [];
|
|
751
|
-
for (const [key, propSchema] of Object.entries(props)) {
|
|
752
|
-
const propType = convert(propSchema, _ctx);
|
|
753
|
-
const optional = required.has(key) ? "" : "?";
|
|
754
|
-
parts.push(`${key}${optional}: ${propType}`);
|
|
755
|
-
}
|
|
756
|
-
return `{ ${parts.join("; ")} }`;
|
|
757
|
-
}
|
|
758
|
-
return "Record<string, unknown>";
|
|
242
|
+
for (const action of entity.actions) {
|
|
243
|
+
methods.push(action.name);
|
|
759
244
|
}
|
|
760
|
-
return
|
|
245
|
+
return methods.length > 0 ? methods.join(", ") : "no operations";
|
|
761
246
|
}
|
|
762
|
-
return "unknown";
|
|
763
|
-
}
|
|
764
|
-
function refToName(ref) {
|
|
765
|
-
const segments = ref.split("/");
|
|
766
|
-
return segments[segments.length - 1] ?? "unknown";
|
|
767
|
-
}
|
|
768
|
-
function toLiteral(value) {
|
|
769
|
-
if (typeof value === "string")
|
|
770
|
-
return `'${value}'`;
|
|
771
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
772
|
-
return String(value);
|
|
773
|
-
if (value === null)
|
|
774
|
-
return "null";
|
|
775
|
-
return "unknown";
|
|
776
247
|
}
|
|
777
248
|
|
|
778
|
-
// src/generators/
|
|
779
|
-
var
|
|
249
|
+
// src/generators/entity-schema-generator.ts
|
|
250
|
+
var FILE_HEADER2 = `// Generated by @vertz/codegen — do not edit
|
|
251
|
+
|
|
780
252
|
`;
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
for (const [name, tsType] of result.extractedTypes) {
|
|
791
|
-
lines.push(emitTypeDeclaration(name, tsType));
|
|
792
|
-
lines.push("");
|
|
793
|
-
}
|
|
794
|
-
const jsdocParts = [];
|
|
795
|
-
if (schema.annotations.description) {
|
|
796
|
-
jsdocParts.push(schema.annotations.description);
|
|
797
|
-
}
|
|
798
|
-
if (schema.annotations.deprecated) {
|
|
799
|
-
jsdocParts.push("@deprecated");
|
|
800
|
-
}
|
|
801
|
-
if (jsdocParts.length > 0) {
|
|
802
|
-
lines.push(`/** ${jsdocParts.join(`
|
|
803
|
-
* `)} */`);
|
|
804
|
-
}
|
|
805
|
-
lines.push(emitTypeDeclaration(schema.name, result.type));
|
|
806
|
-
return { content: lines.join(`
|
|
807
|
-
`), imports: [] };
|
|
253
|
+
var TYPE_MAP = {
|
|
254
|
+
string: "s.string()",
|
|
255
|
+
number: "s.number()",
|
|
256
|
+
boolean: "s.boolean()",
|
|
257
|
+
date: "s.string()",
|
|
258
|
+
unknown: "s.unknown()"
|
|
259
|
+
};
|
|
260
|
+
function toLowerFirst(s) {
|
|
261
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
808
262
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const ref = op.schemaRefs.query;
|
|
825
|
-
if (ref) {
|
|
826
|
-
slots.push(`query?: ${ref}`);
|
|
827
|
-
imports.push({ from: "", name: ref, isType: true });
|
|
828
|
-
} else {
|
|
829
|
-
const result = jsonSchemaToTS(op.query);
|
|
830
|
-
slots.push(`query?: ${result.type}`);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
if (op.body) {
|
|
834
|
-
const ref = op.schemaRefs.body;
|
|
835
|
-
if (ref) {
|
|
836
|
-
slots.push(`body: ${ref}`);
|
|
837
|
-
imports.push({ from: "", name: ref, isType: true });
|
|
838
|
-
} else {
|
|
839
|
-
const result = jsonSchemaToTS(op.body);
|
|
840
|
-
slots.push(`body: ${result.type}`);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
if (op.headers) {
|
|
844
|
-
const ref = op.schemaRefs.headers;
|
|
845
|
-
if (ref) {
|
|
846
|
-
slots.push(`headers?: ${ref}`);
|
|
847
|
-
imports.push({ from: "", name: ref, isType: true });
|
|
848
|
-
} else {
|
|
849
|
-
const result = jsonSchemaToTS(op.headers);
|
|
850
|
-
slots.push(`headers?: ${result.type}`);
|
|
263
|
+
|
|
264
|
+
class EntitySchemaGenerator {
|
|
265
|
+
name = "entity-schema";
|
|
266
|
+
generate(ir, _config) {
|
|
267
|
+
if (!ir.entities?.length)
|
|
268
|
+
return [];
|
|
269
|
+
const files = [];
|
|
270
|
+
const entitiesWithSchemas = [];
|
|
271
|
+
for (const entity of ir.entities) {
|
|
272
|
+
const schemaOps = entity.operations.filter((op) => (op.kind === "create" || op.kind === "update") && op.resolvedFields && op.resolvedFields.length > 0);
|
|
273
|
+
const schemaActions = entity.actions.filter((a) => a.resolvedInputFields && a.resolvedInputFields.length > 0);
|
|
274
|
+
if (schemaOps.length === 0 && schemaActions.length === 0)
|
|
275
|
+
continue;
|
|
276
|
+
entitiesWithSchemas.push(entity);
|
|
277
|
+
files.push(this.generateEntitySchema(entity, schemaOps, schemaActions));
|
|
851
278
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return { content: "", imports: [] };
|
|
855
|
-
}
|
|
856
|
-
const lines = [];
|
|
857
|
-
lines.push(`/** Input for ${op.operationId} */`);
|
|
858
|
-
lines.push(`export interface ${typeName} { ${slots.join("; ")} }`);
|
|
859
|
-
return { content: lines.join(`
|
|
860
|
-
`), imports };
|
|
861
|
-
}
|
|
862
|
-
function emitOperationResponseType(op) {
|
|
863
|
-
const typeName = `${toPascalCase(op.operationId)}Response`;
|
|
864
|
-
const imports = [];
|
|
865
|
-
if (!op.response) {
|
|
866
|
-
return {
|
|
867
|
-
content: `export type ${typeName} = void;`,
|
|
868
|
-
imports: []
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
const ref = op.schemaRefs.response;
|
|
872
|
-
if (ref) {
|
|
873
|
-
imports.push({ from: "", name: ref, isType: true });
|
|
874
|
-
return {
|
|
875
|
-
content: `export type ${typeName} = ${ref};`,
|
|
876
|
-
imports
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
const result = jsonSchemaToTS(op.response);
|
|
880
|
-
return {
|
|
881
|
-
content: emitTypeDeclaration(typeName, result.type),
|
|
882
|
-
imports: []
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
function emitStreamingEventType(op) {
|
|
886
|
-
if (!op.streaming) {
|
|
887
|
-
return { content: "", imports: [] };
|
|
888
|
-
}
|
|
889
|
-
const typeName = `${toPascalCase(op.operationId)}Event`;
|
|
890
|
-
if (!op.streaming.eventSchema) {
|
|
891
|
-
return {
|
|
892
|
-
content: `export type ${typeName} = unknown;`,
|
|
893
|
-
imports: []
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
const result = jsonSchemaToTS(op.streaming.eventSchema);
|
|
897
|
-
return {
|
|
898
|
-
content: emitTypeDeclaration(typeName, result.type),
|
|
899
|
-
imports: []
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
function emitModuleTypesFile(module, schemas) {
|
|
903
|
-
const sections = [FILE_HEADER4];
|
|
904
|
-
for (const schema of schemas) {
|
|
905
|
-
const fragment = emitInterfaceFromSchema(schema);
|
|
906
|
-
if (fragment.content) {
|
|
907
|
-
sections.push(fragment.content);
|
|
279
|
+
if (entitiesWithSchemas.length > 0) {
|
|
280
|
+
files.push(this.generateIndex(entitiesWithSchemas));
|
|
908
281
|
}
|
|
282
|
+
return files;
|
|
909
283
|
}
|
|
910
|
-
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
284
|
+
generateEntitySchema(entity, schemaOps, schemaActions) {
|
|
285
|
+
const lines = [FILE_HEADER2];
|
|
286
|
+
lines.push("import { s } from '@vertz/schema';");
|
|
287
|
+
lines.push("");
|
|
288
|
+
for (const op of schemaOps) {
|
|
289
|
+
if (!op.resolvedFields)
|
|
290
|
+
continue;
|
|
291
|
+
const varName = `${toLowerFirst(op.inputSchema ?? `${op.kind}Input`)}Schema`;
|
|
292
|
+
const fields = op.resolvedFields.map((f) => {
|
|
293
|
+
const baseCall = TYPE_MAP[f.tsType] ?? "s.unknown()";
|
|
294
|
+
const call = f.optional ? `${baseCall}.optional()` : baseCall;
|
|
295
|
+
return ` ${f.name}: ${call}`;
|
|
296
|
+
}).join(`,
|
|
297
|
+
`);
|
|
298
|
+
lines.push(`export const ${varName} = s.object({`);
|
|
299
|
+
lines.push(`${fields},`);
|
|
300
|
+
lines.push("});");
|
|
301
|
+
lines.push("");
|
|
918
302
|
}
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
303
|
+
for (const action of schemaActions) {
|
|
304
|
+
if (!action.resolvedInputFields)
|
|
305
|
+
continue;
|
|
306
|
+
const varName = `${toLowerFirst(action.inputSchema ?? `${action.name}Input`)}Schema`;
|
|
307
|
+
const fields = action.resolvedInputFields.map((f) => {
|
|
308
|
+
const baseCall = TYPE_MAP[f.tsType] ?? "s.unknown()";
|
|
309
|
+
const call = f.optional ? `${baseCall}.optional()` : baseCall;
|
|
310
|
+
return ` ${f.name}: ${call}`;
|
|
311
|
+
}).join(`,
|
|
312
|
+
`);
|
|
313
|
+
lines.push(`export const ${varName} = s.object({`);
|
|
314
|
+
lines.push(`${fields},`);
|
|
315
|
+
lines.push("});");
|
|
316
|
+
lines.push("");
|
|
922
317
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
content: sections.join(`
|
|
927
|
-
|
|
318
|
+
return {
|
|
319
|
+
path: `schemas/${entity.entityName}.ts`,
|
|
320
|
+
content: lines.join(`
|
|
928
321
|
`)
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
generateIndex(entities) {
|
|
325
|
+
const lines = [FILE_HEADER2];
|
|
326
|
+
for (const entity of entities) {
|
|
327
|
+
const exports = [];
|
|
328
|
+
const schemaOps = entity.operations.filter((op) => (op.kind === "create" || op.kind === "update") && op.resolvedFields && op.resolvedFields.length > 0);
|
|
329
|
+
for (const op of schemaOps) {
|
|
330
|
+
const varName = `${toLowerFirst(op.inputSchema ?? `${op.kind}Input`)}Schema`;
|
|
331
|
+
exports.push(varName);
|
|
332
|
+
}
|
|
333
|
+
const schemaActions = entity.actions.filter((a) => a.resolvedInputFields && a.resolvedInputFields.length > 0);
|
|
334
|
+
for (const action of schemaActions) {
|
|
335
|
+
const varName = `${toLowerFirst(action.inputSchema ?? `${action.name}Input`)}Schema`;
|
|
336
|
+
exports.push(varName);
|
|
337
|
+
}
|
|
338
|
+
if (exports.length > 0) {
|
|
339
|
+
lines.push(`export { ${exports.join(", ")} } from './${entity.entityName}';`);
|
|
340
|
+
}
|
|
937
341
|
}
|
|
342
|
+
return { path: "schemas/index.ts", content: lines.join(`
|
|
343
|
+
`) };
|
|
938
344
|
}
|
|
939
|
-
return {
|
|
940
|
-
path: "types/shared.ts",
|
|
941
|
-
content: sections.join(`
|
|
942
|
-
|
|
943
|
-
`)
|
|
944
|
-
};
|
|
945
345
|
}
|
|
946
346
|
|
|
947
|
-
// src/generators/
|
|
948
|
-
var
|
|
347
|
+
// src/generators/entity-sdk-generator.ts
|
|
348
|
+
var FILE_HEADER3 = `// Generated by @vertz/codegen — do not edit
|
|
349
|
+
|
|
949
350
|
`;
|
|
950
|
-
function
|
|
951
|
-
|
|
952
|
-
return "Record<string, never>";
|
|
953
|
-
}
|
|
954
|
-
const ref = op.schemaRefs.params;
|
|
955
|
-
if (ref) {
|
|
956
|
-
return ref;
|
|
957
|
-
}
|
|
958
|
-
const result = jsonSchemaToTS(op.params);
|
|
959
|
-
return result.type;
|
|
960
|
-
}
|
|
961
|
-
function queryToTS(op) {
|
|
962
|
-
if (!op.query) {
|
|
963
|
-
return "Record<string, never>";
|
|
964
|
-
}
|
|
965
|
-
const ref = op.schemaRefs.query;
|
|
966
|
-
if (ref) {
|
|
967
|
-
return ref;
|
|
968
|
-
}
|
|
969
|
-
const result = jsonSchemaToTS(op.query);
|
|
970
|
-
return result.type;
|
|
351
|
+
function toPascalCase2(s) {
|
|
352
|
+
return s.split("-").map((w) => w[0]?.toUpperCase() + w.slice(1)).join("");
|
|
971
353
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
354
|
+
|
|
355
|
+
class EntitySdkGenerator {
|
|
356
|
+
name = "entity-sdk";
|
|
357
|
+
generate(ir, _config) {
|
|
358
|
+
if (!ir.entities?.length)
|
|
359
|
+
return [];
|
|
360
|
+
const files = [];
|
|
361
|
+
for (const entity of ir.entities) {
|
|
362
|
+
files.push(this.generateEntitySdk(entity, ir.basePath));
|
|
363
|
+
}
|
|
364
|
+
files.push(this.generateIndex(ir.entities));
|
|
365
|
+
return files;
|
|
366
|
+
}
|
|
367
|
+
generateEntitySdk(entity, _basePath) {
|
|
368
|
+
const pascal = toPascalCase2(entity.entityName);
|
|
369
|
+
const lines = [FILE_HEADER3];
|
|
370
|
+
const createOpsWithMeta = entity.operations.filter((op) => op.kind === "create" && op.resolvedFields && op.resolvedFields.length > 0);
|
|
371
|
+
const hasSchemaImports = createOpsWithMeta.length > 0;
|
|
372
|
+
if (hasSchemaImports) {
|
|
373
|
+
const schemaImports = [];
|
|
374
|
+
for (const op of createOpsWithMeta) {
|
|
375
|
+
const schemaVarName = `${(op.inputSchema ?? "createInput").charAt(0).toLowerCase()}${(op.inputSchema ?? "createInput").slice(1)}Schema`;
|
|
376
|
+
schemaImports.push(schemaVarName);
|
|
377
|
+
}
|
|
378
|
+
lines.push(`import { ${schemaImports.join(", ")} } from '../schemas/${entity.entityName}';`);
|
|
379
|
+
}
|
|
380
|
+
const hasTypes = entity.operations.some((op) => op.outputSchema || op.inputSchema);
|
|
381
|
+
const hasListOp = entity.operations.some((op) => op.kind === "list");
|
|
382
|
+
if (hasTypes) {
|
|
383
|
+
const typeImports = new Set;
|
|
384
|
+
for (const op of entity.operations) {
|
|
385
|
+
if (op.outputSchema)
|
|
386
|
+
typeImports.add(op.outputSchema);
|
|
387
|
+
if (op.inputSchema)
|
|
388
|
+
typeImports.add(op.inputSchema);
|
|
389
|
+
}
|
|
390
|
+
for (const action of entity.actions) {
|
|
391
|
+
if (action.inputSchema)
|
|
392
|
+
typeImports.add(action.inputSchema);
|
|
393
|
+
if (action.outputSchema)
|
|
394
|
+
typeImports.add(action.outputSchema);
|
|
395
|
+
}
|
|
396
|
+
lines.push(`import type { ${[...typeImports].join(", ")} } from '../types/${entity.entityName}';`);
|
|
397
|
+
const fetchImports = hasListOp ? "type FetchClient, type ListResponse, createDescriptor" : "type FetchClient, createDescriptor";
|
|
398
|
+
lines.push(`import { ${fetchImports} } from '@vertz/fetch';`);
|
|
399
|
+
lines.push("");
|
|
400
|
+
}
|
|
401
|
+
lines.push(`export function create${pascal}Sdk(client: FetchClient) {`);
|
|
402
|
+
lines.push(" return {");
|
|
403
|
+
for (const op of entity.operations) {
|
|
404
|
+
const inputType = op.inputSchema ?? "unknown";
|
|
405
|
+
const outputType = op.outputSchema ?? "unknown";
|
|
406
|
+
const listOutput = op.kind === "list" ? `ListResponse<${outputType}>` : outputType;
|
|
407
|
+
switch (op.kind) {
|
|
408
|
+
case "list":
|
|
409
|
+
lines.push(` list: Object.assign(`);
|
|
410
|
+
lines.push(` (query?: Record<string, unknown>) => createDescriptor('GET', '${op.path}', () => client.get<${listOutput}>('${op.path}', { query }), query),`);
|
|
411
|
+
lines.push(` { url: '${op.path}', method: 'GET' as const },`);
|
|
412
|
+
lines.push(` ),`);
|
|
413
|
+
break;
|
|
414
|
+
case "get":
|
|
415
|
+
lines.push(` get: Object.assign(`);
|
|
416
|
+
lines.push(` (id: string) => createDescriptor('GET', \`${op.path.replace(":id", "${id}")}\`, () => client.get<${outputType}>(\`${op.path.replace(":id", "${id}")}\`)),`);
|
|
417
|
+
lines.push(` { url: '${op.path}', method: 'GET' as const },`);
|
|
418
|
+
lines.push(` ),`);
|
|
419
|
+
break;
|
|
420
|
+
case "create":
|
|
421
|
+
if (op.resolvedFields && op.resolvedFields.length > 0) {
|
|
422
|
+
const schemaVarName = `${(op.inputSchema ?? "createInput").charAt(0).toLowerCase()}${(op.inputSchema ?? "createInput").slice(1)}Schema`;
|
|
423
|
+
lines.push(` create: Object.assign(`);
|
|
424
|
+
lines.push(` (body: ${inputType}) => createDescriptor('POST', '${op.path}', () => client.post<${outputType}>('${op.path}', body)),`);
|
|
425
|
+
lines.push(` {`);
|
|
426
|
+
lines.push(` url: '${op.path}',`);
|
|
427
|
+
lines.push(` method: 'POST' as const,`);
|
|
428
|
+
lines.push(` meta: { bodySchema: ${schemaVarName} },`);
|
|
429
|
+
lines.push(` },`);
|
|
430
|
+
lines.push(` ),`);
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(` create: Object.assign(`);
|
|
433
|
+
lines.push(` (body: ${inputType}) => createDescriptor('POST', '${op.path}', () => client.post<${outputType}>('${op.path}', body)),`);
|
|
434
|
+
lines.push(` { url: '${op.path}', method: 'POST' as const },`);
|
|
435
|
+
lines.push(` ),`);
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
case "update":
|
|
439
|
+
lines.push(` update: Object.assign(`);
|
|
440
|
+
lines.push(` (id: string, body: ${inputType}) => createDescriptor('PATCH', \`${op.path.replace(":id", "${id}")}\`, () => client.patch<${outputType}>(\`${op.path.replace(":id", "${id}")}\`, body)),`);
|
|
441
|
+
lines.push(` { url: '${op.path}', method: 'PATCH' as const },`);
|
|
442
|
+
lines.push(` ),`);
|
|
443
|
+
break;
|
|
444
|
+
case "delete":
|
|
445
|
+
lines.push(` delete: Object.assign(`);
|
|
446
|
+
lines.push(` (id: string) => createDescriptor('DELETE', \`${op.path.replace(":id", "${id}")}\`, () => client.delete<${outputType}>(\`${op.path.replace(":id", "${id}")}\`)),`);
|
|
447
|
+
lines.push(` { url: '${op.path}', method: 'DELETE' as const },`);
|
|
448
|
+
lines.push(` ),`);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const action of entity.actions) {
|
|
453
|
+
const inputType = action.inputSchema ?? "unknown";
|
|
454
|
+
const outputType = action.outputSchema ?? "unknown";
|
|
455
|
+
const method = action.method ?? "POST";
|
|
456
|
+
const methodLower = method.toLowerCase();
|
|
457
|
+
const hasId = action.hasId ?? action.path.includes(":id");
|
|
458
|
+
const pathExpr = hasId ? `\`${action.path.replace(":id", "${id}")}\`` : `'${action.path}'`;
|
|
459
|
+
const hasBody = method === "POST" || method === "PUT" || method === "PATCH";
|
|
460
|
+
const params = [];
|
|
461
|
+
if (hasId)
|
|
462
|
+
params.push("id: string");
|
|
463
|
+
if (hasBody)
|
|
464
|
+
params.push(`body: ${inputType}`);
|
|
465
|
+
const paramStr = params.join(", ");
|
|
466
|
+
const clientArgs = hasBody ? `${pathExpr}, body` : pathExpr;
|
|
467
|
+
const clientCall = `client.${methodLower}<${outputType}>(${clientArgs})`;
|
|
468
|
+
lines.push(` ${action.name}: Object.assign(`);
|
|
469
|
+
lines.push(` (${paramStr}) => createDescriptor('${method}', ${pathExpr}, () => ${clientCall}${hasBody ? ", body" : ""}),`);
|
|
470
|
+
lines.push(` { url: '${action.path}', method: '${method}' as const },`);
|
|
471
|
+
lines.push(" ),");
|
|
472
|
+
}
|
|
473
|
+
lines.push(" };");
|
|
474
|
+
lines.push("}");
|
|
475
|
+
return {
|
|
476
|
+
path: `entities/${entity.entityName}.ts`,
|
|
477
|
+
content: lines.join(`
|
|
478
|
+
`)
|
|
479
|
+
};
|
|
975
480
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
481
|
+
generateIndex(entities) {
|
|
482
|
+
const lines = [FILE_HEADER3];
|
|
483
|
+
for (const entity of entities) {
|
|
484
|
+
const pascal = toPascalCase2(entity.entityName);
|
|
485
|
+
lines.push(`export { create${pascal}Sdk } from './${entity.entityName}';`);
|
|
486
|
+
}
|
|
487
|
+
return { path: "entities/index.ts", content: lines.join(`
|
|
488
|
+
`) };
|
|
979
489
|
}
|
|
980
|
-
const result = jsonSchemaToTS(op.body);
|
|
981
|
-
return result.type;
|
|
982
490
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
491
|
+
|
|
492
|
+
// src/generators/entity-types-generator.ts
|
|
493
|
+
var FILE_HEADER4 = `// Generated by @vertz/codegen — do not edit
|
|
494
|
+
|
|
495
|
+
`;
|
|
496
|
+
var TS_TYPE_MAP = {
|
|
497
|
+
string: "string",
|
|
498
|
+
number: "number",
|
|
499
|
+
boolean: "boolean",
|
|
500
|
+
date: "string",
|
|
501
|
+
unknown: "unknown"
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
class EntityTypesGenerator {
|
|
505
|
+
name = "entity-types";
|
|
506
|
+
generate(ir, _config) {
|
|
507
|
+
if (!ir.entities?.length)
|
|
508
|
+
return [];
|
|
509
|
+
const files = [];
|
|
510
|
+
const entitiesWithTypes = [];
|
|
511
|
+
for (const entity of ir.entities) {
|
|
512
|
+
const hasOpTypes = entity.operations.some((op) => op.inputSchema || op.outputSchema);
|
|
513
|
+
const hasActionTypes = entity.actions.some((a) => a.resolvedInputFields?.length || a.resolvedOutputFields?.length);
|
|
514
|
+
if (!hasOpTypes && !hasActionTypes)
|
|
515
|
+
continue;
|
|
516
|
+
entitiesWithTypes.push(entity);
|
|
517
|
+
files.push(this.generateEntityTypes(entity));
|
|
518
|
+
}
|
|
519
|
+
if (entitiesWithTypes.length > 0) {
|
|
520
|
+
files.push(this.generateIndex(entitiesWithTypes));
|
|
521
|
+
}
|
|
522
|
+
return files;
|
|
523
|
+
}
|
|
524
|
+
generateEntityTypes(entity) {
|
|
525
|
+
const lines = [FILE_HEADER4];
|
|
526
|
+
const emitted = new Set;
|
|
527
|
+
for (const op of entity.operations) {
|
|
528
|
+
if (op.inputSchema && !emitted.has(op.inputSchema)) {
|
|
529
|
+
const inputType = this.emitBodyType(op.inputSchema, op.resolvedFields);
|
|
530
|
+
if (inputType) {
|
|
531
|
+
lines.push(inputType);
|
|
532
|
+
lines.push("");
|
|
533
|
+
emitted.add(op.inputSchema);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (op.outputSchema && !emitted.has(op.outputSchema)) {
|
|
537
|
+
const outputType = this.emitResponseType(op.outputSchema, op.responseFields);
|
|
538
|
+
if (outputType) {
|
|
539
|
+
lines.push(outputType);
|
|
540
|
+
lines.push("");
|
|
541
|
+
emitted.add(op.outputSchema);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const action of entity.actions) {
|
|
546
|
+
if (action.inputSchema && !emitted.has(action.inputSchema)) {
|
|
547
|
+
const inputType = this.emitBodyType(action.inputSchema, action.resolvedInputFields);
|
|
548
|
+
if (inputType) {
|
|
549
|
+
lines.push(inputType);
|
|
550
|
+
lines.push("");
|
|
551
|
+
emitted.add(action.inputSchema);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (action.outputSchema && !emitted.has(action.outputSchema)) {
|
|
555
|
+
const outputType = this.emitResponseType(action.outputSchema, action.resolvedOutputFields);
|
|
556
|
+
if (outputType) {
|
|
557
|
+
lines.push(outputType);
|
|
558
|
+
lines.push("");
|
|
559
|
+
emitted.add(action.outputSchema);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
path: `types/${entity.entityName}.ts`,
|
|
565
|
+
content: lines.join(`
|
|
566
|
+
`)
|
|
567
|
+
};
|
|
990
568
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
569
|
+
emitBodyType(typeName, fields) {
|
|
570
|
+
if (!fields || fields.length === 0)
|
|
571
|
+
return;
|
|
572
|
+
const props = fields.map((f) => {
|
|
573
|
+
const tsType = TS_TYPE_MAP[f.tsType] ?? "unknown";
|
|
574
|
+
const optional = f.optional ? "?" : "";
|
|
575
|
+
return ` ${f.name}${optional}: ${tsType}`;
|
|
576
|
+
}).join(`;
|
|
577
|
+
`);
|
|
578
|
+
return `export interface ${typeName} {
|
|
579
|
+
${props};
|
|
580
|
+
}`;
|
|
997
581
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
582
|
+
emitResponseType(typeName, fields) {
|
|
583
|
+
if (!fields || fields.length === 0)
|
|
584
|
+
return;
|
|
585
|
+
const props = fields.map((f) => {
|
|
586
|
+
const tsType = TS_TYPE_MAP[f.tsType] ?? "unknown";
|
|
587
|
+
const optional = f.optional ? "?" : "";
|
|
588
|
+
return ` ${f.name}${optional}: ${tsType}`;
|
|
589
|
+
}).join(`;
|
|
590
|
+
`);
|
|
591
|
+
return `export interface ${typeName} {
|
|
592
|
+
${props};
|
|
593
|
+
}`;
|
|
1001
594
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const body = bodyToTS(op);
|
|
1010
|
-
const headers = headersToTS(op);
|
|
1011
|
-
const response = responseToTS(op);
|
|
1012
|
-
return ` '${routeKey}': { params: ${params}; query: ${query}; body: ${body}; headers: ${headers}; response: ${response} };`;
|
|
1013
|
-
}
|
|
1014
|
-
function emitRouteMapType(ir) {
|
|
1015
|
-
const sections = [FILE_HEADER5];
|
|
1016
|
-
sections.push("export interface AppRouteMap {");
|
|
1017
|
-
const operations = ir.modules.flatMap((mod) => mod.operations);
|
|
1018
|
-
if (operations.length === 0) {
|
|
1019
|
-
sections.push(" [key: string]: never;");
|
|
1020
|
-
} else {
|
|
1021
|
-
const entries = operations.map((op) => emitRouteEntry(op));
|
|
1022
|
-
sections.push(entries.join(`
|
|
1023
|
-
`));
|
|
595
|
+
generateIndex(entities) {
|
|
596
|
+
const lines = [FILE_HEADER4];
|
|
597
|
+
for (const entity of entities) {
|
|
598
|
+
lines.push(`export * from './${entity.entityName}';`);
|
|
599
|
+
}
|
|
600
|
+
return { path: "types/index.ts", content: lines.join(`
|
|
601
|
+
`) };
|
|
1024
602
|
}
|
|
1025
|
-
sections.push("}");
|
|
1026
|
-
return {
|
|
1027
|
-
path: "types/routes.ts",
|
|
1028
|
-
content: sections.join(`
|
|
1029
|
-
`)
|
|
1030
|
-
};
|
|
1031
603
|
}
|
|
1032
604
|
|
|
1033
605
|
// src/incremental.ts
|
|
1034
606
|
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1035
|
-
import { dirname as dirname2, join as join2, relative } from "node:path";
|
|
607
|
+
import { dirname as dirname2, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
1036
608
|
|
|
1037
609
|
// src/hasher.ts
|
|
1038
610
|
import { createHash } from "node:crypto";
|
|
@@ -1067,6 +639,10 @@ async function writeIncremental(files, outputDir, options) {
|
|
|
1067
639
|
const generatedPaths = new Set(files.map((f) => f.path));
|
|
1068
640
|
for (const file of files) {
|
|
1069
641
|
const filePath = join2(outputDir, file.path);
|
|
642
|
+
const resolvedPath = resolve2(filePath);
|
|
643
|
+
if (!resolvedPath.startsWith(resolve2(outputDir))) {
|
|
644
|
+
throw new Error(`Generated file path "${file.path}" escapes output directory`);
|
|
645
|
+
}
|
|
1070
646
|
const dir = dirname2(filePath);
|
|
1071
647
|
await mkdir2(dir, { recursive: true });
|
|
1072
648
|
let existingContent;
|
|
@@ -1116,122 +692,107 @@ function adaptIR(appIR) {
|
|
|
1116
692
|
}
|
|
1117
693
|
};
|
|
1118
694
|
});
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
headers: route.headers?.jsonSchema,
|
|
1144
|
-
response: route.response?.jsonSchema,
|
|
1145
|
-
schemaRefs
|
|
1146
|
-
};
|
|
1147
|
-
}))
|
|
1148
|
-
}));
|
|
1149
|
-
const slotNames = {
|
|
1150
|
-
params: "Params",
|
|
1151
|
-
query: "Query",
|
|
1152
|
-
body: "Body",
|
|
1153
|
-
headers: "Headers",
|
|
1154
|
-
response: "Response"
|
|
1155
|
-
};
|
|
1156
|
-
const inlineSchemas = [];
|
|
1157
|
-
for (const mod of appIR.modules) {
|
|
1158
|
-
for (const router of mod.routers) {
|
|
1159
|
-
for (const route of router.routes) {
|
|
1160
|
-
for (const [slot, suffix] of Object.entries(slotNames)) {
|
|
1161
|
-
const ref = route[slot];
|
|
1162
|
-
if (ref && typeof ref === "object" && "kind" in ref && ref.kind === "inline" && ref.jsonSchema) {
|
|
1163
|
-
const name = `${toPascalCase(route.operationId)}${suffix}`;
|
|
1164
|
-
inlineSchemas.push({
|
|
1165
|
-
name,
|
|
1166
|
-
jsonSchema: ref.jsonSchema,
|
|
1167
|
-
annotations: { namingParts: {} }
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
695
|
+
const allSchemas = [...schemas].sort((a, b) => a.name.localeCompare(b.name));
|
|
696
|
+
const entities = (appIR.entities ?? []).map((entity) => {
|
|
697
|
+
const entityPascal = toPascalCase(entity.name);
|
|
698
|
+
const operations = [];
|
|
699
|
+
const crudOps = [
|
|
700
|
+
{ kind: "list", method: "GET", path: `/${entity.name}`, schema: "response" },
|
|
701
|
+
{ kind: "get", method: "GET", path: `/${entity.name}/:id`, schema: "response" },
|
|
702
|
+
{ kind: "create", method: "POST", path: `/${entity.name}`, schema: "createInput" },
|
|
703
|
+
{ kind: "update", method: "PATCH", path: `/${entity.name}/:id`, schema: "updateInput" },
|
|
704
|
+
{ kind: "delete", method: "DELETE", path: `/${entity.name}/:id`, schema: "response" }
|
|
705
|
+
];
|
|
706
|
+
for (const op of crudOps) {
|
|
707
|
+
const accessKind = entity.access[op.kind];
|
|
708
|
+
if (accessKind === "false")
|
|
709
|
+
continue;
|
|
710
|
+
let resolvedFields;
|
|
711
|
+
if (op.kind === "create" || op.kind === "update") {
|
|
712
|
+
const schemaRef = entity.modelRef.schemaRefs[op.schema];
|
|
713
|
+
if (schemaRef?.kind === "inline") {
|
|
714
|
+
resolvedFields = schemaRef.resolvedFields?.map((f) => ({
|
|
715
|
+
name: f.name,
|
|
716
|
+
tsType: f.tsType,
|
|
717
|
+
optional: f.optional
|
|
718
|
+
}));
|
|
1170
719
|
}
|
|
1171
720
|
}
|
|
721
|
+
let responseFields;
|
|
722
|
+
const responseRef = entity.modelRef.schemaRefs.response;
|
|
723
|
+
if (responseRef?.kind === "inline") {
|
|
724
|
+
responseFields = responseRef.resolvedFields?.map((f) => ({
|
|
725
|
+
name: f.name,
|
|
726
|
+
tsType: f.tsType,
|
|
727
|
+
optional: f.optional
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
730
|
+
operations.push({
|
|
731
|
+
kind: op.kind,
|
|
732
|
+
method: op.method,
|
|
733
|
+
path: op.path,
|
|
734
|
+
operationId: `${op.kind}${entityPascal}`,
|
|
735
|
+
outputSchema: entity.modelRef.schemaRefs.resolved ? `${entityPascal}Response` : undefined,
|
|
736
|
+
inputSchema: (op.kind === "create" || op.kind === "update") && entity.modelRef.schemaRefs.resolved ? `${op.kind === "create" ? "Create" : "Update"}${entityPascal}Input` : undefined,
|
|
737
|
+
resolvedFields,
|
|
738
|
+
responseFields
|
|
739
|
+
});
|
|
1172
740
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
741
|
+
const actions = entity.actions.filter((a) => entity.access.custom[a.name] !== "false").map((a) => {
|
|
742
|
+
const actionPascal = toPascalCase(a.name);
|
|
743
|
+
const path = a.path ? `/${entity.name}/${a.path}` : `/${entity.name}/:id/${a.name}`;
|
|
744
|
+
let resolvedInputFields;
|
|
745
|
+
if (a.body?.kind === "inline") {
|
|
746
|
+
resolvedInputFields = a.body.resolvedFields?.map((f) => ({
|
|
747
|
+
name: f.name,
|
|
748
|
+
tsType: f.tsType,
|
|
749
|
+
optional: f.optional
|
|
750
|
+
}));
|
|
751
|
+
}
|
|
752
|
+
let resolvedOutputFields;
|
|
753
|
+
if (a.response?.kind === "inline") {
|
|
754
|
+
resolvedOutputFields = a.response.resolvedFields?.map((f) => ({
|
|
755
|
+
name: f.name,
|
|
756
|
+
tsType: f.tsType,
|
|
757
|
+
optional: f.optional
|
|
758
|
+
}));
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
name: a.name,
|
|
762
|
+
method: a.method,
|
|
763
|
+
operationId: `${a.name}${entityPascal}`,
|
|
764
|
+
path,
|
|
765
|
+
hasId: path.includes(":id"),
|
|
766
|
+
inputSchema: a.body ? `${actionPascal}${entityPascal}Input` : undefined,
|
|
767
|
+
outputSchema: a.response ? `${actionPascal}${entityPascal}Output` : undefined,
|
|
768
|
+
resolvedInputFields,
|
|
769
|
+
resolvedOutputFields
|
|
770
|
+
};
|
|
771
|
+
});
|
|
772
|
+
return { entityName: entity.name, operations, actions };
|
|
773
|
+
});
|
|
1179
774
|
return {
|
|
1180
775
|
basePath: appIR.app.basePath,
|
|
1181
776
|
version: appIR.app.version,
|
|
1182
|
-
modules:
|
|
777
|
+
modules: [],
|
|
1183
778
|
schemas: allSchemas,
|
|
779
|
+
entities,
|
|
1184
780
|
auth: { schemes: [] }
|
|
1185
781
|
};
|
|
1186
782
|
}
|
|
1187
783
|
|
|
1188
784
|
// src/generate.ts
|
|
1189
|
-
function runTypescriptGenerator(ir,
|
|
1190
|
-
const files = [];
|
|
1191
|
-
const moduleSchemaNames = new Set;
|
|
1192
|
-
for (const mod of ir.modules) {
|
|
1193
|
-
for (const op of mod.operations) {
|
|
1194
|
-
for (const ref of Object.values(op.schemaRefs)) {
|
|
1195
|
-
if (ref)
|
|
1196
|
-
moduleSchemaNames.add(ref);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
const sharedSchemas = ir.schemas.filter((s) => !moduleSchemaNames.has(s.name));
|
|
1201
|
-
for (const mod of ir.modules) {
|
|
1202
|
-
const moduleRefNames = new Set;
|
|
1203
|
-
for (const op of mod.operations) {
|
|
1204
|
-
for (const ref of Object.values(op.schemaRefs)) {
|
|
1205
|
-
if (ref)
|
|
1206
|
-
moduleRefNames.add(ref);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
const moduleSchemas = ir.schemas.filter((s) => moduleRefNames.has(s.name));
|
|
1210
|
-
files.push(emitModuleTypesFile(mod, moduleSchemas));
|
|
1211
|
-
}
|
|
1212
|
-
if (sharedSchemas.length > 0) {
|
|
1213
|
-
files.push(emitSharedTypesFile(sharedSchemas));
|
|
1214
|
-
}
|
|
1215
|
-
files.push(emitRouteMapType(ir));
|
|
1216
|
-
for (const mod of ir.modules) {
|
|
1217
|
-
files.push(emitModuleFile(mod));
|
|
1218
|
-
}
|
|
1219
|
-
files.push(emitClientFile(ir));
|
|
1220
|
-
if (ir.schemas.length > 0) {
|
|
1221
|
-
files.push(emitSchemaReExports(ir.schemas));
|
|
1222
|
-
}
|
|
1223
|
-
files.push(emitBarrelIndex(ir));
|
|
1224
|
-
if (config.typescript?.publishable) {
|
|
1225
|
-
files.push(emitPackageJson(ir, {
|
|
1226
|
-
packageName: config.typescript.publishable.name,
|
|
1227
|
-
packageVersion: config.typescript.publishable.version
|
|
1228
|
-
}));
|
|
1229
|
-
}
|
|
1230
|
-
return files;
|
|
1231
|
-
}
|
|
1232
|
-
function runCLIGenerator(ir) {
|
|
785
|
+
function runTypescriptGenerator(ir, _config) {
|
|
1233
786
|
const files = [];
|
|
1234
|
-
|
|
787
|
+
const generatorConfig = { outputDir: _config.outputDir, options: {} };
|
|
788
|
+
const entityTypesGen = new EntityTypesGenerator;
|
|
789
|
+
files.push(...entityTypesGen.generate(ir, generatorConfig));
|
|
790
|
+
const entitySchemaGen = new EntitySchemaGenerator;
|
|
791
|
+
files.push(...entitySchemaGen.generate(ir, generatorConfig));
|
|
792
|
+
const entitySdkGen = new EntitySdkGenerator;
|
|
793
|
+
files.push(...entitySdkGen.generate(ir, generatorConfig));
|
|
794
|
+
const clientGen = new ClientGenerator;
|
|
795
|
+
files.push(...clientGen.generate(ir, generatorConfig));
|
|
1235
796
|
return files;
|
|
1236
797
|
}
|
|
1237
798
|
function generateSync(ir, config) {
|
|
@@ -1241,9 +802,6 @@ function generateSync(ir, config) {
|
|
|
1241
802
|
if (gen === "typescript") {
|
|
1242
803
|
generators.push("typescript");
|
|
1243
804
|
files.push(...runTypescriptGenerator(ir, config));
|
|
1244
|
-
} else if (gen === "cli") {
|
|
1245
|
-
generators.push("cli");
|
|
1246
|
-
files.push(...runCLIGenerator(ir));
|
|
1247
805
|
}
|
|
1248
806
|
}
|
|
1249
807
|
return {
|
|
@@ -1253,6 +811,42 @@ function generateSync(ir, config) {
|
|
|
1253
811
|
generators
|
|
1254
812
|
};
|
|
1255
813
|
}
|
|
814
|
+
async function mergeImportsToPackageJson(files, outputDir) {
|
|
815
|
+
const generatedPkg = files.find((f) => f.path === "package.json");
|
|
816
|
+
if (!generatedPkg)
|
|
817
|
+
return false;
|
|
818
|
+
const generated = JSON.parse(generatedPkg.content);
|
|
819
|
+
const imports = generated.imports;
|
|
820
|
+
if (!imports || Object.keys(imports).length === 0)
|
|
821
|
+
return false;
|
|
822
|
+
const projectRoot = await findProjectRoot(resolve3(outputDir));
|
|
823
|
+
if (!projectRoot)
|
|
824
|
+
return false;
|
|
825
|
+
const pkgPath = join3(projectRoot, "package.json");
|
|
826
|
+
const raw = await readFile3(pkgPath, "utf-8");
|
|
827
|
+
const pkg = JSON.parse(raw);
|
|
828
|
+
const existing = pkg.imports;
|
|
829
|
+
if (existing && JSON.stringify(existing) === JSON.stringify(imports)) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
pkg.imports = imports;
|
|
833
|
+
await writeFile3(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
834
|
+
`, "utf-8");
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
async function findProjectRoot(startDir) {
|
|
838
|
+
let dir = startDir;
|
|
839
|
+
const root = dirname3(dir);
|
|
840
|
+
while (dir !== root) {
|
|
841
|
+
try {
|
|
842
|
+
await readFile3(join3(dir, "package.json"), "utf-8");
|
|
843
|
+
return dir;
|
|
844
|
+
} catch {
|
|
845
|
+
dir = dirname3(dir);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
1256
850
|
async function generate(appIR, config) {
|
|
1257
851
|
const ir = adaptIR(appIR);
|
|
1258
852
|
const result = generateSync(ir, config);
|
|
@@ -1269,11 +863,16 @@ async function generate(appIR, config) {
|
|
|
1269
863
|
await mkdir3(config.outputDir, { recursive: true });
|
|
1270
864
|
for (const file of files) {
|
|
1271
865
|
const filePath = join3(config.outputDir, file.path);
|
|
866
|
+
const resolvedPath = resolve3(filePath);
|
|
867
|
+
if (!resolvedPath.startsWith(resolve3(config.outputDir))) {
|
|
868
|
+
throw new Error(`Generated file path "${file.path}" escapes output directory`);
|
|
869
|
+
}
|
|
1272
870
|
const dir = dirname3(filePath);
|
|
1273
871
|
await mkdir3(dir, { recursive: true });
|
|
1274
872
|
await writeFile3(filePath, file.content, "utf-8");
|
|
1275
873
|
}
|
|
1276
874
|
}
|
|
875
|
+
await mergeImportsToPackageJson(files, config.outputDir);
|
|
1277
876
|
return {
|
|
1278
877
|
files,
|
|
1279
878
|
ir,
|
|
@@ -1282,6 +881,106 @@ async function generate(appIR, config) {
|
|
|
1282
881
|
incremental: incrementalResult
|
|
1283
882
|
};
|
|
1284
883
|
}
|
|
884
|
+
// src/json-schema-converter.ts
|
|
885
|
+
var PRIMITIVE_MAP = {
|
|
886
|
+
string: "string",
|
|
887
|
+
number: "number",
|
|
888
|
+
integer: "number",
|
|
889
|
+
boolean: "boolean",
|
|
890
|
+
null: "null"
|
|
891
|
+
};
|
|
892
|
+
function jsonSchemaToTS(schema, ctx) {
|
|
893
|
+
const context = ctx ?? {
|
|
894
|
+
namedTypes: new Map,
|
|
895
|
+
resolving: new Set
|
|
896
|
+
};
|
|
897
|
+
const type = convert(schema, context);
|
|
898
|
+
return { type, extractedTypes: context.namedTypes };
|
|
899
|
+
}
|
|
900
|
+
function convert(schema, _ctx) {
|
|
901
|
+
if (schema.$defs && typeof schema.$defs === "object") {
|
|
902
|
+
const defs = schema.$defs;
|
|
903
|
+
for (const [name, defSchema] of Object.entries(defs)) {
|
|
904
|
+
if (!_ctx.namedTypes.has(name)) {
|
|
905
|
+
_ctx.resolving.add(name);
|
|
906
|
+
const typeStr = convert(defSchema, _ctx);
|
|
907
|
+
_ctx.resolving.delete(name);
|
|
908
|
+
_ctx.namedTypes.set(name, typeStr);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (typeof schema.$ref === "string") {
|
|
913
|
+
if (!schema.$ref.startsWith("#")) {
|
|
914
|
+
throw new Error(`External $ref is not supported: ${schema.$ref}`);
|
|
915
|
+
}
|
|
916
|
+
return refToName(schema.$ref);
|
|
917
|
+
}
|
|
918
|
+
if (schema.const !== undefined) {
|
|
919
|
+
return toLiteral(schema.const);
|
|
920
|
+
}
|
|
921
|
+
if (Array.isArray(schema.enum)) {
|
|
922
|
+
return schema.enum.map((v) => toLiteral(v)).join(" | ");
|
|
923
|
+
}
|
|
924
|
+
if (Array.isArray(schema.oneOf)) {
|
|
925
|
+
return schema.oneOf.map((s) => convert(s, _ctx)).join(" | ");
|
|
926
|
+
}
|
|
927
|
+
if (Array.isArray(schema.anyOf)) {
|
|
928
|
+
return schema.anyOf.map((s) => convert(s, _ctx)).join(" | ");
|
|
929
|
+
}
|
|
930
|
+
if (Array.isArray(schema.allOf)) {
|
|
931
|
+
return schema.allOf.map((s) => convert(s, _ctx)).join(" & ");
|
|
932
|
+
}
|
|
933
|
+
if (Array.isArray(schema.type)) {
|
|
934
|
+
return schema.type.map((t) => PRIMITIVE_MAP[t] ?? t).join(" | ");
|
|
935
|
+
}
|
|
936
|
+
if (typeof schema.type === "string") {
|
|
937
|
+
const type = schema.type;
|
|
938
|
+
if (type === "array") {
|
|
939
|
+
if (Array.isArray(schema.prefixItems)) {
|
|
940
|
+
const items = schema.prefixItems.map((s) => convert(s, _ctx));
|
|
941
|
+
return `[${items.join(", ")}]`;
|
|
942
|
+
}
|
|
943
|
+
if (schema.items && typeof schema.items === "object") {
|
|
944
|
+
const itemType = convert(schema.items, _ctx);
|
|
945
|
+
return itemType.includes(" | ") ? `(${itemType})[]` : `${itemType}[]`;
|
|
946
|
+
}
|
|
947
|
+
return "unknown[]";
|
|
948
|
+
}
|
|
949
|
+
if (type === "object") {
|
|
950
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && !schema.properties) {
|
|
951
|
+
const valueType = convert(schema.additionalProperties, _ctx);
|
|
952
|
+
return `Record<string, ${valueType}>`;
|
|
953
|
+
}
|
|
954
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
955
|
+
const props = schema.properties;
|
|
956
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
957
|
+
const parts = [];
|
|
958
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
959
|
+
const propType = convert(propSchema, _ctx);
|
|
960
|
+
const optional = required.has(key) ? "" : "?";
|
|
961
|
+
parts.push(`${key}${optional}: ${propType}`);
|
|
962
|
+
}
|
|
963
|
+
return `{ ${parts.join("; ")} }`;
|
|
964
|
+
}
|
|
965
|
+
return "Record<string, unknown>";
|
|
966
|
+
}
|
|
967
|
+
return PRIMITIVE_MAP[type] ?? "unknown";
|
|
968
|
+
}
|
|
969
|
+
return "unknown";
|
|
970
|
+
}
|
|
971
|
+
function refToName(ref) {
|
|
972
|
+
const segments = ref.split("/");
|
|
973
|
+
return segments[segments.length - 1] ?? "unknown";
|
|
974
|
+
}
|
|
975
|
+
function toLiteral(value) {
|
|
976
|
+
if (typeof value === "string")
|
|
977
|
+
return `'${value}'`;
|
|
978
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
979
|
+
return String(value);
|
|
980
|
+
if (value === null)
|
|
981
|
+
return "null";
|
|
982
|
+
return "unknown";
|
|
983
|
+
}
|
|
1285
984
|
// src/pipeline.ts
|
|
1286
985
|
function createCodegenPipeline() {
|
|
1287
986
|
return {
|
|
@@ -1301,6 +1000,49 @@ function createCodegenPipeline() {
|
|
|
1301
1000
|
}
|
|
1302
1001
|
};
|
|
1303
1002
|
}
|
|
1003
|
+
// src/utils/imports.ts
|
|
1004
|
+
function mergeImports(imports) {
|
|
1005
|
+
const seen = new Map;
|
|
1006
|
+
for (const imp of imports) {
|
|
1007
|
+
const key = `${imp.from}::${imp.name}::${imp.isType}::${imp.alias ?? ""}`;
|
|
1008
|
+
if (!seen.has(key)) {
|
|
1009
|
+
seen.set(key, imp);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return [...seen.values()].sort((a, b) => {
|
|
1013
|
+
const fromCmp = a.from.localeCompare(b.from);
|
|
1014
|
+
if (fromCmp !== 0)
|
|
1015
|
+
return fromCmp;
|
|
1016
|
+
return a.name.localeCompare(b.name);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function renderImports(imports) {
|
|
1020
|
+
const grouped = new Map;
|
|
1021
|
+
for (const imp of imports) {
|
|
1022
|
+
let group = grouped.get(imp.from);
|
|
1023
|
+
if (!group) {
|
|
1024
|
+
group = { types: [], values: [] };
|
|
1025
|
+
grouped.set(imp.from, group);
|
|
1026
|
+
}
|
|
1027
|
+
const nameStr = imp.alias ? `${imp.name} as ${imp.alias}` : imp.name;
|
|
1028
|
+
if (imp.isType) {
|
|
1029
|
+
group.types.push(nameStr);
|
|
1030
|
+
} else {
|
|
1031
|
+
group.values.push(nameStr);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const lines = [];
|
|
1035
|
+
for (const [from, group] of grouped) {
|
|
1036
|
+
if (group.types.length > 0) {
|
|
1037
|
+
lines.push(`import type { ${group.types.join(", ")} } from '${from}';`);
|
|
1038
|
+
}
|
|
1039
|
+
if (group.values.length > 0) {
|
|
1040
|
+
lines.push(`import { ${group.values.join(", ")} } from '${from}';`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return lines.join(`
|
|
1044
|
+
`);
|
|
1045
|
+
}
|
|
1304
1046
|
export {
|
|
1305
1047
|
writeIncremental,
|
|
1306
1048
|
validateCodegenConfig,
|
|
@@ -1308,36 +1050,19 @@ export {
|
|
|
1308
1050
|
toPascalCase,
|
|
1309
1051
|
toKebabCase,
|
|
1310
1052
|
toCamelCase,
|
|
1311
|
-
scaffoldCLIRootIndex,
|
|
1312
|
-
scaffoldCLIPackageJson,
|
|
1313
1053
|
resolveCodegenConfig,
|
|
1314
1054
|
renderImports,
|
|
1055
|
+
mergeImportsToPackageJson,
|
|
1315
1056
|
mergeImports,
|
|
1316
1057
|
jsonSchemaToTS,
|
|
1317
1058
|
hashContent,
|
|
1318
1059
|
generate,
|
|
1319
1060
|
formatWithBiome,
|
|
1320
|
-
emitStreamingMethod,
|
|
1321
|
-
emitStreamingEventType,
|
|
1322
|
-
emitSharedTypesFile,
|
|
1323
|
-
emitSchemaReExports,
|
|
1324
|
-
emitSDKConfig,
|
|
1325
|
-
emitRouteMapType,
|
|
1326
|
-
emitPackageJson,
|
|
1327
|
-
emitOperationResponseType,
|
|
1328
|
-
emitOperationMethod,
|
|
1329
|
-
emitOperationInputType,
|
|
1330
|
-
emitModuleTypesFile,
|
|
1331
|
-
emitModuleFile,
|
|
1332
|
-
emitModuleCommands,
|
|
1333
|
-
emitManifestFile,
|
|
1334
|
-
emitInterfaceFromSchema,
|
|
1335
|
-
emitCommandDefinition,
|
|
1336
|
-
emitClientFile,
|
|
1337
|
-
emitBinEntryPoint,
|
|
1338
|
-
emitBarrelIndex,
|
|
1339
|
-
emitAuthStrategyBuilder,
|
|
1340
1061
|
defineCodegenConfig,
|
|
1341
1062
|
createCodegenPipeline,
|
|
1342
|
-
adaptIR
|
|
1063
|
+
adaptIR,
|
|
1064
|
+
EntityTypesGenerator,
|
|
1065
|
+
EntitySdkGenerator,
|
|
1066
|
+
EntitySchemaGenerator,
|
|
1067
|
+
ClientGenerator
|
|
1343
1068
|
};
|