zenko 0.1.0 → 0.1.2
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 +60 -12
- package/dist/cli.cjs +283 -22
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +283 -22
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +265 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.mjs +265 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -8
package/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[](https://github.com/RawToast/zenko/actions/workflows/ci.yml)
|
4
4
|
|
5
|
-
A TypeScript generator for OpenAPI specifications that creates **Zod schemas**, **type-safe path functions**, and **operation objects**.
|
5
|
+
A work in progress TypeScript generator for OpenAPI specifications that creates **Zod schemas**, **type-safe path functions**, and **operation objects**.
|
6
6
|
|
7
7
|
Unlike most OpenAPI generators, Zenko does not create a client. Instead you are free to use your own fetch wrapper or library of choice.
|
8
8
|
|
@@ -11,6 +11,7 @@ Unlike most OpenAPI generators, Zenko does not create a client. Instead you are
|
|
11
11
|
- 🔧 **Zod Schema Generation** - Generates runtime-validated Zod schemas from OpenAPI schemas
|
12
12
|
- 🛣️ **Type-safe Path Functions** - Creates functions to build API paths with proper TypeScript types
|
13
13
|
- 📋 **Operation Objects** - Generates objects containing path functions, request validation, and response types
|
14
|
+
- 🧰 **Operation Type Helpers** - Import `PathFn`, `HeaderFn`, `OperationDefinition`, and `OperationErrors` to power reusable clients
|
14
15
|
- 🔄 **Dependency Resolution** - Automatically resolves schema dependencies with topological sorting
|
15
16
|
- ⚡ **CLI & Programmatic API** - Use via command line or import as a library
|
16
17
|
|
@@ -66,10 +67,14 @@ bunx zenko input.yaml output.ts
|
|
66
67
|
|
67
68
|
### Config File
|
68
69
|
|
69
|
-
The config file
|
70
|
+
The config file controls generation for multiple specs and can also configure type helper emission.
|
70
71
|
|
71
72
|
```json
|
72
73
|
{
|
74
|
+
"types": {
|
75
|
+
"emit": true,
|
76
|
+
"helpers": "package"
|
77
|
+
},
|
73
78
|
"schemas": [
|
74
79
|
{
|
75
80
|
"input": "my-api.yaml",
|
@@ -79,12 +84,22 @@ The config file is a JSON file that contains an array of schema objects. Each sc
|
|
79
84
|
"input": "my-strict-api.yaml",
|
80
85
|
"output": "my-strict-api.gen.ts",
|
81
86
|
"strictDates": true,
|
82
|
-
"strictNumeric": true
|
87
|
+
"strictNumeric": true,
|
88
|
+
"types": {
|
89
|
+
"helpers": "inline"
|
90
|
+
}
|
83
91
|
}
|
84
92
|
]
|
85
93
|
}
|
86
94
|
```
|
87
95
|
|
96
|
+
#### Type Helper Modes
|
97
|
+
|
98
|
+
- `helpers: "package"` (default) imports helpers from `zenko`
|
99
|
+
- `helpers: "inline"` writes the helper definitions into each generated file
|
100
|
+
- `helpers: "file"` imports from a custom module (`helpersOutput` path)
|
101
|
+
- `emit: false` disables per-operation type aliases entirely
|
102
|
+
|
88
103
|
### Programmatic Usage
|
89
104
|
|
90
105
|
```typescript
|
@@ -157,7 +172,7 @@ export const paths = {
|
|
157
172
|
} as const
|
158
173
|
```
|
159
174
|
|
160
|
-
### 3. Operation Objects
|
175
|
+
### 3. Operation Objects & Types
|
161
176
|
|
162
177
|
```typescript
|
163
178
|
// Operation Objects
|
@@ -171,12 +186,28 @@ export const getUserById = {
|
|
171
186
|
path: paths.getUserById,
|
172
187
|
response: UserResponse,
|
173
188
|
} as const
|
189
|
+
|
190
|
+
// Operation Types
|
191
|
+
import type { OperationDefinition, OperationErrors } from "zenko"
|
192
|
+
|
193
|
+
export type AuthenticateUserOperation = OperationDefinition<
|
194
|
+
typeof paths.authenticateUser,
|
195
|
+
typeof AuthenticateRequest,
|
196
|
+
typeof AuthenticateResponse,
|
197
|
+
undefined,
|
198
|
+
OperationErrors
|
199
|
+
>
|
174
200
|
```
|
175
201
|
|
176
202
|
## Example Usage in Your App
|
177
203
|
|
178
204
|
```typescript
|
179
|
-
import {
|
205
|
+
import {
|
206
|
+
paths,
|
207
|
+
authenticateUser,
|
208
|
+
type AuthenticateUserOperation,
|
209
|
+
AuthenticateRequest,
|
210
|
+
} from "./generated-types"
|
180
211
|
|
181
212
|
// Type-safe path building
|
182
213
|
const userPath = paths.getUserById({ userId: "123" })
|
@@ -201,6 +232,30 @@ if (validation.success) {
|
|
201
232
|
}
|
202
233
|
```
|
203
234
|
|
235
|
+
### Building a Generic Client
|
236
|
+
|
237
|
+
```typescript
|
238
|
+
import type { OperationDefinition } from "zenko"
|
239
|
+
|
240
|
+
async function runOperation<
|
241
|
+
T extends OperationDefinition<PathFn<any[]>, any, any>,
|
242
|
+
>(
|
243
|
+
operation: T,
|
244
|
+
config: { baseUrl: string; init?: RequestInit }
|
245
|
+
): Promise<
|
246
|
+
T["response"] extends undefined
|
247
|
+
? void
|
248
|
+
: T["response"] extends (...args: any[]) => infer U
|
249
|
+
? U
|
250
|
+
: T["response"]
|
251
|
+
> {
|
252
|
+
const url = `${config.baseUrl}${operation.path()}`
|
253
|
+
const res = await fetch(url, config.init)
|
254
|
+
if (!res.ok) throw new Error(`Request failed: ${res.status}`)
|
255
|
+
return (await res.json()) as any
|
256
|
+
}
|
257
|
+
```
|
258
|
+
|
204
259
|
## Key Improvements
|
205
260
|
|
206
261
|
### Dependency Resolution
|
@@ -234,10 +289,3 @@ zenko src/resources/petstore.yaml output.ts
|
|
234
289
|
# Format code
|
235
290
|
bun run format
|
236
291
|
```
|
237
|
-
|
238
|
-
## Architecture
|
239
|
-
|
240
|
-
- **`src/zenko.ts`** - Main OpenAPI → TypeScript generator
|
241
|
-
- **`src/cli.ts`** - Command-line interface
|
242
|
-
- **`dist/`** - Bundled outputs (CJS + ESM)
|
243
|
-
- **Topological Sort** - Ensures proper dependency ordering for schema generation
|
package/dist/cli.cjs
CHANGED
@@ -237,11 +237,13 @@ function generate(spec, options = {}) {
|
|
237
237
|
const output = [];
|
238
238
|
const generatedTypes = /* @__PURE__ */ new Set();
|
239
239
|
const { strictDates = false, strictNumeric = false } = options;
|
240
|
+
const typesConfig = normalizeTypesConfig(options.types);
|
240
241
|
const schemaOptions = {
|
241
242
|
strictDates,
|
242
243
|
strictNumeric
|
243
244
|
};
|
244
245
|
output.push('import { z } from "zod";');
|
246
|
+
appendHelperTypesImport(output, typesConfig);
|
245
247
|
output.push("");
|
246
248
|
if (spec.components?.schemas) {
|
247
249
|
output.push("// Generated Zod Schemas");
|
@@ -261,16 +263,79 @@ function generate(spec, options = {}) {
|
|
261
263
|
output.push("// Path Functions");
|
262
264
|
output.push("export const paths = {");
|
263
265
|
for (const op of operations) {
|
264
|
-
|
266
|
+
const pathParamNames = op.pathParams.map((p) => p.name);
|
267
|
+
const hasPathParams = pathParamNames.length > 0;
|
268
|
+
const hasQueryParams = op.queryParams.length > 0;
|
269
|
+
if (!hasPathParams && !hasQueryParams) {
|
265
270
|
output.push(` ${op.operationId}: () => "${op.path}",`);
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
271
|
+
continue;
|
272
|
+
}
|
273
|
+
const allParamNames = [
|
274
|
+
...pathParamNames,
|
275
|
+
...op.queryParams.map((p) => p.name)
|
276
|
+
];
|
277
|
+
const signaturePieces = [];
|
278
|
+
for (const param of op.pathParams) {
|
279
|
+
signaturePieces.push(`${param.name}: string`);
|
280
|
+
}
|
281
|
+
for (const param of op.queryParams) {
|
282
|
+
signaturePieces.push(
|
283
|
+
`${param.name}${param.required ? "" : "?"}: ${mapQueryType(param)}`
|
284
|
+
);
|
285
|
+
}
|
286
|
+
const signatureParams = signaturePieces.join(", ");
|
287
|
+
const needsDefaultObject = !hasPathParams && hasQueryParams && op.queryParams.every((param) => !param.required);
|
288
|
+
const signatureArgs = allParamNames.length ? `{ ${allParamNames.join(", ")} }` : "{}";
|
289
|
+
const signature = `${signatureArgs}: { ${signatureParams} }${needsDefaultObject ? " = {}" : ""}`;
|
290
|
+
const pathWithParams = op.path.replace(/{([^}]+)}/g, "${$1}");
|
291
|
+
if (!hasQueryParams) {
|
270
292
|
output.push(
|
271
|
-
` ${op.operationId}: (
|
293
|
+
` ${op.operationId}: (${signature}) => \`${pathWithParams}\`,`
|
272
294
|
);
|
295
|
+
continue;
|
296
|
+
}
|
297
|
+
output.push(` ${op.operationId}: (${signature}) => {`);
|
298
|
+
output.push(" const params = new URLSearchParams()");
|
299
|
+
for (const param of op.queryParams) {
|
300
|
+
const propertyKey = formatPropertyName(param.name);
|
301
|
+
const accessor = isValidJSIdentifier(param.name) ? param.name : propertyKey;
|
302
|
+
const schema = param.schema ?? {};
|
303
|
+
if (schema?.type === "array") {
|
304
|
+
const itemValueExpression = convertQueryParamValue(
|
305
|
+
schema.items ?? {},
|
306
|
+
"value"
|
307
|
+
);
|
308
|
+
if (param.required) {
|
309
|
+
output.push(` for (const value of ${accessor}) {`);
|
310
|
+
output.push(
|
311
|
+
` params.append("${param.name}", ${itemValueExpression})`
|
312
|
+
);
|
313
|
+
output.push(" }");
|
314
|
+
} else {
|
315
|
+
output.push(` if (${accessor} !== undefined) {`);
|
316
|
+
output.push(` for (const value of ${accessor}) {`);
|
317
|
+
output.push(
|
318
|
+
` params.append("${param.name}", ${itemValueExpression})`
|
319
|
+
);
|
320
|
+
output.push(" }");
|
321
|
+
output.push(" }");
|
322
|
+
}
|
323
|
+
continue;
|
324
|
+
}
|
325
|
+
const valueExpression = convertQueryParamValue(schema, accessor);
|
326
|
+
if (param.required) {
|
327
|
+
output.push(` params.set("${param.name}", ${valueExpression})`);
|
328
|
+
} else {
|
329
|
+
output.push(` if (${accessor} !== undefined) {`);
|
330
|
+
output.push(` params.set("${param.name}", ${valueExpression})`);
|
331
|
+
output.push(" }");
|
332
|
+
}
|
273
333
|
}
|
334
|
+
output.push(" const _searchParams = params.toString()");
|
335
|
+
output.push(
|
336
|
+
` return \`${pathWithParams}\${_searchParams ? \`?\${_searchParams}\` : ""}\``
|
337
|
+
);
|
338
|
+
output.push(" },");
|
274
339
|
}
|
275
340
|
output.push("} as const;");
|
276
341
|
output.push("");
|
@@ -362,6 +427,7 @@ function generate(spec, options = {}) {
|
|
362
427
|
output.push("} as const;");
|
363
428
|
output.push("");
|
364
429
|
}
|
430
|
+
generateOperationTypes(output, operations, typesConfig);
|
365
431
|
return output.join("\n");
|
366
432
|
}
|
367
433
|
function appendOperationField(buffer, key, value) {
|
@@ -397,11 +463,13 @@ function parseOperations(spec) {
|
|
397
463
|
);
|
398
464
|
const resolvedParameters = collectParameters(pathItem, operation, spec);
|
399
465
|
const requestHeaders = getRequestHeaders(resolvedParameters);
|
466
|
+
const queryParams = getQueryParams(resolvedParameters);
|
400
467
|
operations.push({
|
401
468
|
operationId: operation.operationId,
|
402
469
|
path: path2,
|
403
470
|
method: method.toLowerCase(),
|
404
471
|
pathParams,
|
472
|
+
queryParams,
|
405
473
|
requestType,
|
406
474
|
responseType: successResponse,
|
407
475
|
requestHeaders,
|
@@ -411,6 +479,129 @@ function parseOperations(spec) {
|
|
411
479
|
}
|
412
480
|
return operations;
|
413
481
|
}
|
482
|
+
function normalizeTypesConfig(config) {
|
483
|
+
return {
|
484
|
+
emit: config?.emit ?? true,
|
485
|
+
helpers: config?.helpers ?? "package",
|
486
|
+
helpersOutput: config?.helpersOutput ?? "./zenko-types"
|
487
|
+
};
|
488
|
+
}
|
489
|
+
function appendHelperTypesImport(buffer, config) {
|
490
|
+
if (!config.emit) return;
|
491
|
+
switch (config.helpers) {
|
492
|
+
case "package":
|
493
|
+
buffer.push(
|
494
|
+
'import type { PathFn, HeaderFn, OperationDefinition, OperationErrors } from "zenko";'
|
495
|
+
);
|
496
|
+
return;
|
497
|
+
case "file":
|
498
|
+
buffer.push(
|
499
|
+
`import type { PathFn, HeaderFn, OperationDefinition, OperationErrors } from "${config.helpersOutput}";`
|
500
|
+
);
|
501
|
+
return;
|
502
|
+
case "inline":
|
503
|
+
buffer.push(
|
504
|
+
"type PathFn<TArgs extends unknown[] = []> = (...args: TArgs) => string;"
|
505
|
+
);
|
506
|
+
buffer.push(
|
507
|
+
"type HeaderFn<TArgs extends unknown[] = [], TResult = Record<string, unknown> | Record<string, never>> = (...args: TArgs) => TResult;"
|
508
|
+
);
|
509
|
+
buffer.push(
|
510
|
+
"type OperationErrors<TClient = unknown, TServer = unknown, TDefault = unknown, TOther = unknown> = {"
|
511
|
+
);
|
512
|
+
buffer.push(" clientErrors?: Record<string, TClient>;");
|
513
|
+
buffer.push(" serverErrors?: Record<string, TServer>;");
|
514
|
+
buffer.push(" defaultErrors?: Record<string, TDefault>;");
|
515
|
+
buffer.push(" otherErrors?: Record<string, TOther>;");
|
516
|
+
buffer.push("};");
|
517
|
+
buffer.push(
|
518
|
+
"type OperationDefinition<TPath extends (...args: any[]) => string, TRequest = undefined, TResponse = undefined, THeaders extends HeaderFn | undefined = undefined, TErrors extends OperationErrors | undefined = undefined> = {"
|
519
|
+
);
|
520
|
+
buffer.push(" path: TPath;");
|
521
|
+
buffer.push(" request?: TRequest;");
|
522
|
+
buffer.push(" response?: TResponse;");
|
523
|
+
buffer.push(" headers?: THeaders;");
|
524
|
+
buffer.push(" errors?: TErrors;");
|
525
|
+
buffer.push("};");
|
526
|
+
return;
|
527
|
+
}
|
528
|
+
}
|
529
|
+
function generateOperationTypes(buffer, operations, config) {
|
530
|
+
if (!config.emit) return;
|
531
|
+
buffer.push("// Operation Types");
|
532
|
+
for (const op of operations) {
|
533
|
+
const headerType = op.requestHeaders?.length ? `typeof headers.${op.operationId}` : "undefined";
|
534
|
+
const requestType = wrapTypeReference(op.requestType);
|
535
|
+
const responseType = wrapTypeReference(op.responseType);
|
536
|
+
const errorsType = buildOperationErrorsType(op.errors);
|
537
|
+
buffer.push(
|
538
|
+
`export type ${capitalize(op.operationId)}Operation = OperationDefinition<`
|
539
|
+
);
|
540
|
+
buffer.push(` typeof paths.${op.operationId},`);
|
541
|
+
buffer.push(` ${requestType},`);
|
542
|
+
buffer.push(` ${responseType},`);
|
543
|
+
buffer.push(` ${headerType},`);
|
544
|
+
buffer.push(` ${errorsType}`);
|
545
|
+
buffer.push(`>;`);
|
546
|
+
buffer.push("");
|
547
|
+
}
|
548
|
+
}
|
549
|
+
function buildOperationErrorsType(errors) {
|
550
|
+
if (!errors || !hasAnyErrors(errors)) {
|
551
|
+
return "OperationErrors";
|
552
|
+
}
|
553
|
+
const client = buildErrorBucket(errors.clientErrors);
|
554
|
+
const server = buildErrorBucket(errors.serverErrors);
|
555
|
+
const fallback = buildErrorBucket(errors.defaultErrors);
|
556
|
+
const other = buildErrorBucket(errors.otherErrors);
|
557
|
+
return `OperationErrors<${client}, ${server}, ${fallback}, ${other}>`;
|
558
|
+
}
|
559
|
+
function buildErrorBucket(bucket) {
|
560
|
+
if (!bucket || Object.keys(bucket).length === 0) {
|
561
|
+
return "unknown";
|
562
|
+
}
|
563
|
+
const entries = Object.entries(bucket);
|
564
|
+
const accessibleEntries = entries.map(([name, type]) => {
|
565
|
+
const propertyKey = formatPropertyName(name);
|
566
|
+
const valueType = wrapErrorValueType(type);
|
567
|
+
return `${propertyKey}: ${valueType}`;
|
568
|
+
});
|
569
|
+
return `{ ${accessibleEntries.join("; ")} }`;
|
570
|
+
}
|
571
|
+
var TYPE_KEYWORDS = /* @__PURE__ */ new Set([
|
572
|
+
"any",
|
573
|
+
"unknown",
|
574
|
+
"never",
|
575
|
+
"void",
|
576
|
+
"null",
|
577
|
+
"undefined",
|
578
|
+
"string",
|
579
|
+
"number",
|
580
|
+
"boolean",
|
581
|
+
"bigint",
|
582
|
+
"symbol"
|
583
|
+
]);
|
584
|
+
function wrapTypeReference(typeName) {
|
585
|
+
if (!typeName) return "undefined";
|
586
|
+
if (typeName === "undefined") return "undefined";
|
587
|
+
if (TYPE_KEYWORDS.has(typeName)) return typeName;
|
588
|
+
if (typeName.startsWith("typeof ")) return typeName;
|
589
|
+
const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
590
|
+
if (identifierPattern.test(typeName)) {
|
591
|
+
return `typeof ${typeName}`;
|
592
|
+
}
|
593
|
+
return typeName;
|
594
|
+
}
|
595
|
+
function wrapErrorValueType(typeName) {
|
596
|
+
if (!typeName) return "unknown";
|
597
|
+
if (TYPE_KEYWORDS.has(typeName)) return typeName;
|
598
|
+
if (typeName.startsWith("typeof ")) return typeName;
|
599
|
+
const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
600
|
+
if (identifierPattern.test(typeName)) {
|
601
|
+
return `typeof ${typeName}`;
|
602
|
+
}
|
603
|
+
return typeName;
|
604
|
+
}
|
414
605
|
function collectParameters(pathItem, operation, spec) {
|
415
606
|
const parametersMap = /* @__PURE__ */ new Map();
|
416
607
|
const addParameters = (params) => {
|
@@ -553,6 +744,20 @@ function getRequestHeaders(parameters) {
|
|
553
744
|
}
|
554
745
|
return headers;
|
555
746
|
}
|
747
|
+
function getQueryParams(parameters) {
|
748
|
+
const queryParams = [];
|
749
|
+
for (const param of parameters ?? []) {
|
750
|
+
if (param.in === "query") {
|
751
|
+
queryParams.push({
|
752
|
+
name: param.name,
|
753
|
+
description: param.description,
|
754
|
+
schema: param.schema,
|
755
|
+
required: param.required
|
756
|
+
});
|
757
|
+
}
|
758
|
+
}
|
759
|
+
return queryParams;
|
760
|
+
}
|
556
761
|
function mapHeaderType(header) {
|
557
762
|
const schemaType = header.schema?.type;
|
558
763
|
switch (schemaType) {
|
@@ -565,6 +770,39 @@ function mapHeaderType(header) {
|
|
565
770
|
return "string";
|
566
771
|
}
|
567
772
|
}
|
773
|
+
function mapQueryType(param) {
|
774
|
+
return mapQuerySchemaType(param.schema);
|
775
|
+
}
|
776
|
+
function mapQuerySchemaType(schema) {
|
777
|
+
if (!schema) return "string";
|
778
|
+
if (schema.type === "array") {
|
779
|
+
const itemType = mapQuerySchemaType(schema.items);
|
780
|
+
return `Array<${itemType}>`;
|
781
|
+
}
|
782
|
+
switch (schema.type) {
|
783
|
+
case "integer":
|
784
|
+
case "number":
|
785
|
+
return "number";
|
786
|
+
case "boolean":
|
787
|
+
return "boolean";
|
788
|
+
default:
|
789
|
+
return "string";
|
790
|
+
}
|
791
|
+
}
|
792
|
+
function convertQueryParamValue(schema, accessor) {
|
793
|
+
if (!schema) {
|
794
|
+
return `String(${accessor})`;
|
795
|
+
}
|
796
|
+
switch (schema.type) {
|
797
|
+
case "integer":
|
798
|
+
case "number":
|
799
|
+
return `String(${accessor})`;
|
800
|
+
case "boolean":
|
801
|
+
return `${accessor} ? "true" : "false"`;
|
802
|
+
default:
|
803
|
+
return `String(${accessor})`;
|
804
|
+
}
|
805
|
+
}
|
568
806
|
function generateZodSchema(name, schema, generatedTypes, options) {
|
569
807
|
if (generatedTypes.has(name)) return "";
|
570
808
|
generatedTypes.add(name);
|
@@ -573,18 +811,7 @@ function generateZodSchema(name, schema, generatedTypes, options) {
|
|
573
811
|
return `export const ${name} = z.enum([${enumValues}]);`;
|
574
812
|
}
|
575
813
|
if (schema.type === "object" || schema.properties) {
|
576
|
-
const
|
577
|
-
for (const [propName, propSchema] of Object.entries(
|
578
|
-
schema.properties || {}
|
579
|
-
)) {
|
580
|
-
const isRequired = schema.required?.includes(propName) ?? false;
|
581
|
-
const zodType = getZodTypeFromSchema(propSchema, options);
|
582
|
-
const finalType = isRequired ? zodType : `${zodType}.optional()`;
|
583
|
-
properties.push(` ${formatPropertyName(propName)}: ${finalType},`);
|
584
|
-
}
|
585
|
-
return `export const ${name} = z.object({
|
586
|
-
${properties.join("\n")}
|
587
|
-
});`;
|
814
|
+
return `export const ${name} = ${buildZodObject(schema, options)};`;
|
588
815
|
}
|
589
816
|
if (schema.type === "array") {
|
590
817
|
const itemSchema = schema.items ?? { type: "unknown" };
|
@@ -607,6 +834,9 @@ function getZodTypeFromSchema(schema, options) {
|
|
607
834
|
const enumValues = schema.enum.map((v) => `"${v}"`).join(", ");
|
608
835
|
return `z.enum([${enumValues}])`;
|
609
836
|
}
|
837
|
+
if (schema.type === "object" || schema.properties) {
|
838
|
+
return buildZodObject(schema, options);
|
839
|
+
}
|
610
840
|
switch (schema.type) {
|
611
841
|
case "string":
|
612
842
|
return buildString(schema, options);
|
@@ -627,6 +857,23 @@ function getZodTypeFromSchema(schema, options) {
|
|
627
857
|
return "z.unknown()";
|
628
858
|
}
|
629
859
|
}
|
860
|
+
function buildZodObject(schema, options) {
|
861
|
+
const properties = [];
|
862
|
+
for (const [propName, propSchema] of Object.entries(
|
863
|
+
schema.properties || {}
|
864
|
+
)) {
|
865
|
+
const isRequired = schema.required?.includes(propName) ?? false;
|
866
|
+
const zodType = getZodTypeFromSchema(propSchema, options);
|
867
|
+
const finalType = isRequired ? zodType : `${zodType}.optional()`;
|
868
|
+
properties.push(` ${formatPropertyName(propName)}: ${finalType},`);
|
869
|
+
}
|
870
|
+
if (properties.length === 0) {
|
871
|
+
return "z.object({})";
|
872
|
+
}
|
873
|
+
return `z.object({
|
874
|
+
${properties.join("\n")}
|
875
|
+
})`;
|
876
|
+
}
|
630
877
|
function buildString(schema, options) {
|
631
878
|
if (options.strictDates) {
|
632
879
|
switch (schema.format) {
|
@@ -817,7 +1064,7 @@ function printHelp() {
|
|
817
1064
|
console.log("");
|
818
1065
|
console.log("Config file format:");
|
819
1066
|
console.log(
|
820
|
-
' {"schemas": [{ input, output, strictDates?, strictNumeric? }] }'
|
1067
|
+
' {"types"?: { emit?, helpers?, helpersOutput? }, "schemas": [{ input, output, strictDates?, strictNumeric?, types? }] }'
|
821
1068
|
);
|
822
1069
|
}
|
823
1070
|
async function runFromConfig(parsed) {
|
@@ -826,14 +1073,17 @@ async function runFromConfig(parsed) {
|
|
826
1073
|
const config = await loadConfig(resolvedConfigPath);
|
827
1074
|
validateConfig(config);
|
828
1075
|
const baseDir = path.dirname(resolvedConfigPath);
|
1076
|
+
const baseTypesConfig = config.types;
|
829
1077
|
for (const entry of config.schemas) {
|
830
1078
|
const inputFile = resolvePath(entry.input, baseDir);
|
831
1079
|
const outputFile = resolvePath(entry.output, baseDir);
|
1080
|
+
const typesConfig = resolveTypesConfig(baseTypesConfig, entry.types);
|
832
1081
|
await generateSingle({
|
833
1082
|
inputFile,
|
834
1083
|
outputFile,
|
835
1084
|
strictDates: entry.strictDates ?? parsed.strictDates,
|
836
|
-
strictNumeric: entry.strictNumeric ?? parsed.strictNumeric
|
1085
|
+
strictNumeric: entry.strictNumeric ?? parsed.strictNumeric,
|
1086
|
+
typesConfig
|
837
1087
|
});
|
838
1088
|
}
|
839
1089
|
}
|
@@ -870,12 +1120,23 @@ function validateConfig(config) {
|
|
870
1120
|
function resolvePath(filePath, baseDir) {
|
871
1121
|
return path.isAbsolute(filePath) ? filePath : path.join(baseDir, filePath);
|
872
1122
|
}
|
1123
|
+
function resolveTypesConfig(baseConfig, entryConfig) {
|
1124
|
+
if (!baseConfig && !entryConfig) return void 0;
|
1125
|
+
return {
|
1126
|
+
...baseConfig,
|
1127
|
+
...entryConfig
|
1128
|
+
};
|
1129
|
+
}
|
873
1130
|
async function generateSingle(options) {
|
874
|
-
const { inputFile, outputFile, strictDates, strictNumeric } = options;
|
1131
|
+
const { inputFile, outputFile, strictDates, strictNumeric, typesConfig } = options;
|
875
1132
|
const resolvedInput = path.resolve(inputFile);
|
876
1133
|
const resolvedOutput = path.resolve(outputFile);
|
877
1134
|
const spec = readSpec(resolvedInput);
|
878
|
-
const output = generate(spec, {
|
1135
|
+
const output = generate(spec, {
|
1136
|
+
strictDates,
|
1137
|
+
strictNumeric,
|
1138
|
+
types: typesConfig
|
1139
|
+
});
|
879
1140
|
fs.mkdirSync(path.dirname(resolvedOutput), { recursive: true });
|
880
1141
|
fs.writeFileSync(resolvedOutput, output);
|
881
1142
|
console.log(`\u2705 Generated TypeScript types in ${resolvedOutput}`);
|