@sdk-it/typescript 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { npmRunPathEnv } from "npm-run-path";
6
6
  import ts, { TypeFlags, symbolName } from "typescript";
7
7
  import debug from "debug";
8
8
  import ts2 from "typescript";
9
+ import "lodash-es";
9
10
  import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
10
11
  import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
11
12
  var deriveSymbol = Symbol.for("serialize");
@@ -45,7 +46,7 @@ async function getFolderExports(folder, extensions = ["ts"]) {
45
46
  const exports = [];
46
47
  for (const file of files) {
47
48
  if (file.isDirectory()) {
48
- exports.push(`export * from './${file.name}';`);
49
+ exports.push(`export * from './${file.name}/index.ts';`);
49
50
  } else if (file.name !== "index.ts" && extensions.includes(getExt(file.name))) {
50
51
  exports.push(`export * from './${file.name}';`);
51
52
  }
@@ -77,259 +78,8 @@ function toLitObject(obj, accessor = (value) => value) {
77
78
  import { get as get2, merge } from "lodash-es";
78
79
  import { camelcase as camelcase2, pascalcase as pascalcase2, spinalcase as spinalcase2 } from "stringcase";
79
80
 
80
- // packages/typescript/src/lib/json-zod.ts
81
+ // packages/typescript/src/lib/utils.ts
81
82
  import { get } from "lodash-es";
82
- function cleanRef(ref) {
83
- return ref.replace(/^#\//, "");
84
- }
85
- function parseRef(ref) {
86
- const parts = ref.split(ref);
87
- const [model] = parts.splice(-1);
88
- return { model, path: parts.join("/") };
89
- }
90
- function followRef(spec, ref) {
91
- const pathParts = cleanRef(ref).split("/");
92
- const entry = get(spec, pathParts);
93
- if (entry && "$ref" in entry) {
94
- return followRef(spec, entry.$ref);
95
- }
96
- return entry;
97
- }
98
- function jsonSchemaToZod(spec, schema, required = false, onRef, circularRefTracker = /* @__PURE__ */ new Set()) {
99
- if ("$ref" in schema) {
100
- const schemaName = cleanRef(schema.$ref).split("/").pop();
101
- if (circularRefTracker.has(schemaName)) {
102
- return schemaName;
103
- }
104
- circularRefTracker.add(schemaName);
105
- onRef(
106
- schemaName,
107
- jsonSchemaToZod(
108
- spec,
109
- followRef(spec, schema.$ref),
110
- required,
111
- onRef,
112
- circularRefTracker
113
- )
114
- );
115
- circularRefTracker.delete(schemaName);
116
- return schemaName;
117
- }
118
- if (schema.allOf && Array.isArray(schema.allOf)) {
119
- const allOfSchemas = schema.allOf.map(
120
- (sub) => jsonSchemaToZod(spec, sub, true, onRef, circularRefTracker)
121
- );
122
- return allOfSchemas.length ? `z.intersection(${allOfSchemas.join(", ")})` : allOfSchemas[0];
123
- }
124
- if (schema.anyOf && Array.isArray(schema.anyOf)) {
125
- const anyOfSchemas = schema.anyOf.map(
126
- (sub) => jsonSchemaToZod(spec, sub, false, onRef, circularRefTracker)
127
- );
128
- return anyOfSchemas.length > 1 ? `z.union([${anyOfSchemas.join(", ")}])${appendOptional(required)}` : (
129
- // Handle an invalid anyOf with one schema
130
- anyOfSchemas[0]
131
- );
132
- }
133
- if (schema.oneOf && Array.isArray(schema.oneOf)) {
134
- const oneOfSchemas = schema.oneOf.map((sub) => {
135
- if ("$ref" in sub) {
136
- const { model } = parseRef(sub.$ref);
137
- if (circularRefTracker.has(model)) {
138
- return model;
139
- }
140
- }
141
- return jsonSchemaToZod(spec, sub, false, onRef, circularRefTracker);
142
- });
143
- return oneOfSchemas.length > 1 ? `z.union([${oneOfSchemas.join(", ")}])${appendOptional(required)}` : (
144
- // Handle an invalid oneOf with one schema
145
- oneOfSchemas[0]
146
- );
147
- }
148
- if (schema.enum && Array.isArray(schema.enum)) {
149
- const enumVals = schema.enum.map((val) => JSON.stringify(val)).join(", ");
150
- return `z.enum([${enumVals}])${appendOptional(required)}`;
151
- }
152
- const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
153
- if (!types.length) {
154
- return `z.unknown()${appendOptional(required)}`;
155
- }
156
- if (types.length > 1) {
157
- const realTypes = types.filter((t) => t !== "null");
158
- if (realTypes.length === 1 && types.includes("null")) {
159
- const typeZod = basicTypeToZod(
160
- realTypes[0],
161
- schema,
162
- spec,
163
- false,
164
- onRef,
165
- circularRefTracker
166
- );
167
- return `${typeZod}.nullable()${appendOptional(required)}`;
168
- }
169
- const subSchemas = types.map(
170
- (t) => basicTypeToZod(t, schema, spec, false, onRef, circularRefTracker)
171
- );
172
- return `z.union([${subSchemas.join(", ")}])${appendOptional(required)}`;
173
- }
174
- return basicTypeToZod(
175
- types[0],
176
- schema,
177
- spec,
178
- required,
179
- onRef,
180
- circularRefTracker
181
- );
182
- }
183
- function basicTypeToZod(type, schema, spec, required = false, onRef, refProcessingStack) {
184
- switch (type) {
185
- case "string":
186
- return handleString(schema, required);
187
- case "number":
188
- case "integer":
189
- return handleNumber(schema, required);
190
- case "boolean":
191
- return `z.boolean()${appendDefault(schema.default)}${appendOptional(required)}`;
192
- case "object":
193
- return handleObject(schema, spec, required, onRef, refProcessingStack);
194
- case "array":
195
- return handleArray(schema, spec, required, onRef, refProcessingStack);
196
- case "null":
197
- return `z.null()${appendOptional(required)}`;
198
- default:
199
- return `z.unknown()${appendOptional(required)}`;
200
- }
201
- }
202
- function handleString(schema, required) {
203
- let base = "z.string()";
204
- switch (schema.format) {
205
- case "date-time":
206
- case "datetime":
207
- base = "z.coerce.date()";
208
- break;
209
- case "date":
210
- base = "z.coerce.date() /* or z.string() if you want raw date strings */";
211
- break;
212
- case "time":
213
- base = "z.string() /* optionally add .regex(...) for HH:MM:SS format */";
214
- break;
215
- case "email":
216
- base = "z.string().email()";
217
- break;
218
- case "uuid":
219
- base = "z.string().uuid()";
220
- break;
221
- case "url":
222
- case "uri":
223
- base = "z.string().url()";
224
- break;
225
- case "ipv4":
226
- base = 'z.string().ip({version: "v4"})';
227
- break;
228
- case "ipv6":
229
- base = 'z.string().ip({version: "v6"})';
230
- break;
231
- case "phone":
232
- base = "z.string() /* or add .regex(...) for phone formats */";
233
- break;
234
- case "byte":
235
- case "binary":
236
- base = "z.instanceof(Blob) /* consider base64 check if needed */";
237
- break;
238
- case "int64":
239
- base = "z.string() /* or z.bigint() if your app can handle it */";
240
- break;
241
- default:
242
- break;
243
- }
244
- return `${base}${appendDefault(schema.default)}${appendOptional(required)}`;
245
- }
246
- function handleNumber(schema, required) {
247
- let defaultValue = schema.default !== void 0 ? `.default(${schema.default})` : ``;
248
- let base = "z.number()";
249
- if (schema.format === "int64") {
250
- base = "z.bigint()";
251
- if (schema.default !== void 0) {
252
- defaultValue = `.default(BigInt(${schema.default}))`;
253
- }
254
- }
255
- if (schema.format === "int32") {
256
- base += ".int()";
257
- }
258
- if (typeof schema.exclusiveMinimum === "number") {
259
- base += `.gt(${schema.exclusiveMinimum})`;
260
- }
261
- if (typeof schema.exclusiveMaximum === "number") {
262
- base += `.lt(${schema.exclusiveMaximum})`;
263
- }
264
- if (typeof schema.minimum === "number") {
265
- base += schema.format === "int64" ? `.min(BigInt(${schema.minimum}))` : `.min(${schema.minimum})`;
266
- }
267
- if (typeof schema.maximum === "number") {
268
- base += schema.format === "int64" ? `.max(BigInt(${schema.maximum}))` : `.max(${schema.maximum})`;
269
- }
270
- if (typeof schema.multipleOf === "number") {
271
- base += `.refine((val) => Number.isInteger(val / ${schema.multipleOf}), "Must be a multiple of ${schema.multipleOf}")`;
272
- }
273
- return `${base}${defaultValue}${appendOptional(required)}`;
274
- }
275
- function handleObject(schema, spec, required = false, onRef, refProcessingStack) {
276
- const properties = schema.properties || {};
277
- const propEntries = Object.entries(properties).map(([key, propSchema]) => {
278
- const isRequired = (schema.required ?? []).includes(key);
279
- const zodPart = jsonSchemaToZod(
280
- spec,
281
- propSchema,
282
- isRequired,
283
- onRef,
284
- refProcessingStack
285
- );
286
- return `'${key}': ${zodPart}`;
287
- });
288
- let additionalProps = "";
289
- if (schema.additionalProperties) {
290
- if (typeof schema.additionalProperties === "object") {
291
- const addPropZod = jsonSchemaToZod(
292
- spec,
293
- schema.additionalProperties,
294
- true,
295
- onRef,
296
- refProcessingStack
297
- );
298
- additionalProps = `.catchall(${addPropZod})`;
299
- } else if (schema.additionalProperties === true) {
300
- additionalProps = `.catchall(z.unknown())`;
301
- }
302
- }
303
- const objectSchema = `z.object({${propEntries.join(", ")}})${additionalProps}`;
304
- return `${objectSchema}${appendOptional(required)}`;
305
- }
306
- function handleArray(schema, spec, required = false, onRef, refProcessingStack) {
307
- const { items } = schema;
308
- if (!items) {
309
- return `z.array(z.unknown())${appendOptional(required)}`;
310
- }
311
- if (Array.isArray(items)) {
312
- const tupleItems = items.map(
313
- (sub) => jsonSchemaToZod(spec, sub, true, onRef, refProcessingStack)
314
- );
315
- const base = `z.tuple([${tupleItems.join(", ")}])`;
316
- return `${base}${appendOptional(required)}`;
317
- }
318
- const itemsSchema = jsonSchemaToZod(
319
- spec,
320
- items,
321
- true,
322
- onRef,
323
- refProcessingStack
324
- );
325
- return `z.array(${itemsSchema})${appendOptional(required)}`;
326
- }
327
- function appendOptional(isRequired) {
328
- return isRequired ? "" : ".optional()";
329
- }
330
- function appendDefault(defaultValue) {
331
- return defaultValue !== void 0 ? `.default(${JSON.stringify(defaultValue)})` : "";
332
- }
333
83
 
334
84
  // packages/typescript/src/lib/sdk.ts
335
85
  import { camelcase, pascalcase, spinalcase } from "stringcase";
@@ -455,7 +205,6 @@ function generateClientSdk(spec) {
455
205
  const emitter = new Emitter();
456
206
  const streamEmitter = new StreamEmitter();
457
207
  const schemas = {};
458
- const schemasImports = [];
459
208
  const schemaEndpoint = new SchemaEndpoint();
460
209
  const errors = [];
461
210
  for (const [name, operations] of Object.entries(spec.operations)) {
@@ -474,9 +223,6 @@ function generateClientSdk(spec) {
474
223
  const schemaName = camelcase(`${operation.name} schema`);
475
224
  const schema = `export const ${schemaName} = ${Object.keys(operation.schemas).length === 1 ? Object.values(operation.schemas)[0] : toLitObject(operation.schemas)};`;
476
225
  schemas[featureSchemaFileName].push(schema);
477
- schemasImports.push(
478
- ...operation.imports.map((it) => (it.namedImports ?? []).map((it2) => it2.name)).flat()
479
- );
480
226
  const schemaRef = `${featureSchemaFileName}.${schemaName}`;
481
227
  const output = operation.formatOutput();
482
228
  const inputHeaders = [];
@@ -506,7 +252,7 @@ function generateClientSdk(spec) {
506
252
  const input = `z.infer<typeof ${schemaRef}>`;
507
253
  const endpoint = `${operation.trigger.method.toUpperCase()} ${operation.trigger.path}`;
508
254
  streamEmitter.addImport(
509
- `import type {${pascalcase(operation.name)}} from './outputs/${spinalcase(operation.name)}';`
255
+ `import type {${pascalcase(operation.name)}} from './outputs/${spinalcase(operation.name)}.ts';`
510
256
  );
511
257
  streamEmitter.addEndpoint(
512
258
  endpoint,
@@ -529,7 +275,7 @@ function generateClientSdk(spec) {
529
275
  );
530
276
  } else {
531
277
  emitter.addImport(
532
- `import type {${output.import}} from './outputs/${spinalcase(operation.name)}';`
278
+ `import type {${output.import}} from './outputs/${spinalcase(operation.name)}.ts';`
533
279
  );
534
280
  errors.push(...operation.errors ?? []);
535
281
  const addTypeParser = Object.keys(operation.schemas).length > 1;
@@ -571,7 +317,9 @@ function generateClientSdk(spec) {
571
317
  Object.entries(schemas).map(([key, value]) => [
572
318
  `inputs/${key}.ts`,
573
319
  [
574
- schemasImports.length ? `import {${removeDuplicates(schemasImports, (it) => it)}} from '../zod';` : "",
320
+ // schemasImports.length
321
+ // ? `import {${removeDuplicates(schemasImports, (it) => it)}} from '../zod';`
322
+ // : '',
575
323
  spec.commonZod ? 'import * as commonZod from "../zod.ts";' : "",
576
324
  ...value
577
325
  ].map((it) => it.trim()).filter(Boolean).join("\n") + "\n"
@@ -588,6 +336,22 @@ function generateClientSdk(spec) {
588
336
  function isRef(obj) {
589
337
  return "$ref" in obj;
590
338
  }
339
+ function cleanRef(ref) {
340
+ return ref.replace(/^#\//, "");
341
+ }
342
+ function parseRef(ref) {
343
+ const parts = ref.split("/");
344
+ const [model] = parts.splice(-1);
345
+ return { model, path: parts.join("/") };
346
+ }
347
+ function followRef(spec, ref) {
348
+ const pathParts = cleanRef(ref).split("/");
349
+ const entry = get(spec, pathParts);
350
+ if (entry && "$ref" in entry) {
351
+ return followRef(spec, entry.$ref);
352
+ }
353
+ return entry;
354
+ }
591
355
  function securityToOptions(security2, securitySchemas, staticIn) {
592
356
  securitySchemas ??= {};
593
357
  const options = {};
@@ -621,6 +385,447 @@ function securityToOptions(security2, securitySchemas, staticIn) {
621
385
  }
622
386
  return options;
623
387
  }
388
+ function mergeImports(imports) {
389
+ const merged = {};
390
+ for (const i of imports) {
391
+ merged[i.moduleSpecifier] = merged[i.moduleSpecifier] ?? {
392
+ moduleSpecifier: i.moduleSpecifier,
393
+ defaultImport: i.defaultImport,
394
+ namespaceImport: i.namespaceImport,
395
+ namedImports: []
396
+ };
397
+ if (i.namedImports) {
398
+ merged[i.moduleSpecifier].namedImports.push(...i.namedImports);
399
+ }
400
+ }
401
+ return Object.values(merged);
402
+ }
403
+ function importsToString(...imports) {
404
+ return imports.map((it) => {
405
+ if (it.defaultImport) {
406
+ return `import ${it.defaultImport} from '${it.moduleSpecifier}'`;
407
+ }
408
+ if (it.namespaceImport) {
409
+ return `import * as ${it.namespaceImport} from '${it.moduleSpecifier}'`;
410
+ }
411
+ if (it.namedImports) {
412
+ return `import {${removeDuplicates(it.namedImports, (it2) => it2.name).map((n) => `${n.isTypeOnly ? "type" : ""} ${n.name}`).join(", ")}} from '${it.moduleSpecifier}'`;
413
+ }
414
+ throw new Error(`Invalid import ${JSON.stringify(it)}`);
415
+ });
416
+ }
417
+
418
+ // packages/typescript/src/lib/emitters/interface.ts
419
+ var TypeScriptDeserialzer = class {
420
+ circularRefTracker = /* @__PURE__ */ new Set();
421
+ #spec;
422
+ #onRef;
423
+ constructor(spec, onRef) {
424
+ this.#spec = spec;
425
+ this.#onRef = onRef;
426
+ }
427
+ /**
428
+ * Handle objects (properties)
429
+ */
430
+ object(schema, required = false) {
431
+ const properties = schema.properties || {};
432
+ const propEntries = Object.entries(properties).map(([key, propSchema]) => {
433
+ const isRequired = (schema.required ?? []).includes(key);
434
+ const tsType = this.handle(propSchema, isRequired);
435
+ return `${key}: ${tsType}`;
436
+ });
437
+ if (schema.additionalProperties) {
438
+ if (typeof schema.additionalProperties === "object") {
439
+ const indexType = this.handle(schema.additionalProperties, true);
440
+ propEntries.push(`[key: string]: ${indexType}`);
441
+ } else if (schema.additionalProperties === true) {
442
+ propEntries.push("[key: string]: any");
443
+ }
444
+ }
445
+ return `{ ${propEntries.join("; ")} }`;
446
+ }
447
+ /**
448
+ * Handle arrays (items could be a single schema or a tuple)
449
+ */
450
+ array(schema, required = false) {
451
+ const { items } = schema;
452
+ if (!items) {
453
+ return "any[]";
454
+ }
455
+ if (Array.isArray(items)) {
456
+ const tupleItems = items.map((sub) => this.handle(sub, true));
457
+ return `[${tupleItems.join(", ")}]`;
458
+ }
459
+ const itemsType = this.handle(items, true);
460
+ return `${itemsType}[]`;
461
+ }
462
+ /**
463
+ * Convert a basic type (string | number | boolean | object | array, etc.) to TypeScript
464
+ */
465
+ normal(type, schema, required = false) {
466
+ switch (type) {
467
+ case "string":
468
+ return this.string(schema, required);
469
+ case "number":
470
+ case "integer":
471
+ return this.number(schema, required);
472
+ case "boolean":
473
+ return appendOptional("boolean", required);
474
+ case "object":
475
+ return this.object(schema, required);
476
+ case "array":
477
+ return this.array(schema, required);
478
+ case "null":
479
+ return "null";
480
+ default:
481
+ return appendOptional("any", required);
482
+ }
483
+ }
484
+ ref($ref, required) {
485
+ const schemaName = cleanRef($ref).split("/").pop();
486
+ if (this.circularRefTracker.has(schemaName)) {
487
+ return schemaName;
488
+ }
489
+ this.circularRefTracker.add(schemaName);
490
+ this.#onRef(schemaName, this.handle(followRef(this.#spec, $ref), true));
491
+ this.circularRefTracker.delete(schemaName);
492
+ return appendOptional(schemaName, required);
493
+ }
494
+ allOf(schemas) {
495
+ const allOfTypes = schemas.map((sub) => this.handle(sub, true));
496
+ return allOfTypes.length > 1 ? `${allOfTypes.join(" & ")}` : allOfTypes[0];
497
+ }
498
+ anyOf(schemas, required) {
499
+ const anyOfTypes = schemas.map((sub) => this.handle(sub, true));
500
+ return appendOptional(
501
+ anyOfTypes.length > 1 ? `${anyOfTypes.join(" | ")}` : anyOfTypes[0],
502
+ required
503
+ );
504
+ }
505
+ oneOf(schemas, required) {
506
+ const oneOfTypes = schemas.map((sub) => {
507
+ if (isRef(sub)) {
508
+ const { model } = parseRef(sub.$ref);
509
+ if (this.circularRefTracker.has(model)) {
510
+ return model;
511
+ }
512
+ }
513
+ return this.handle(sub, false);
514
+ });
515
+ return appendOptional(
516
+ oneOfTypes.length > 1 ? `${oneOfTypes.join(" | ")}` : oneOfTypes[0],
517
+ required
518
+ );
519
+ }
520
+ enum(values, required) {
521
+ const enumValues = values.map((val) => typeof val === "string" ? `'${val}'` : `${val}`).join(" | ");
522
+ return appendOptional(enumValues, required);
523
+ }
524
+ /**
525
+ * Handle string type with formats
526
+ */
527
+ string(schema, required) {
528
+ let type;
529
+ switch (schema.format) {
530
+ case "date-time":
531
+ case "datetime":
532
+ case "date":
533
+ type = "Date";
534
+ break;
535
+ case "binary":
536
+ case "byte":
537
+ type = "Blob";
538
+ break;
539
+ case "int64":
540
+ type = "bigint";
541
+ break;
542
+ default:
543
+ type = "string";
544
+ }
545
+ return appendOptional(type, required);
546
+ }
547
+ /**
548
+ * Handle number/integer types with formats
549
+ */
550
+ number(schema, required) {
551
+ const type = schema.format === "int64" ? "bigint" : "number";
552
+ return appendOptional(type, required);
553
+ }
554
+ handle(schema, required) {
555
+ if (isRef(schema)) {
556
+ return this.ref(schema.$ref, required);
557
+ }
558
+ if (schema.allOf && Array.isArray(schema.allOf)) {
559
+ return this.allOf(schema.allOf);
560
+ }
561
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
562
+ return this.anyOf(schema.anyOf, required);
563
+ }
564
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
565
+ return this.oneOf(schema.oneOf, required);
566
+ }
567
+ if (schema.enum && Array.isArray(schema.enum)) {
568
+ return this.enum(schema.enum, required);
569
+ }
570
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
571
+ if (!types.length) {
572
+ return appendOptional("any", required);
573
+ }
574
+ if (types.length > 1) {
575
+ const realTypes = types.filter((t) => t !== "null");
576
+ if (realTypes.length === 1 && types.includes("null")) {
577
+ const tsType = this.normal(realTypes[0], schema, false);
578
+ return appendOptional(`${tsType} | null`, required);
579
+ }
580
+ const typeResults = types.map((t) => this.normal(t, schema, false));
581
+ return appendOptional(typeResults.join(" | "), required);
582
+ }
583
+ return this.normal(types[0], schema, required);
584
+ }
585
+ /**
586
+ * Generate an interface declaration
587
+ */
588
+ generateInterface(name, schema) {
589
+ const content = this.handle(schema, true);
590
+ return `interface ${name} ${content}`;
591
+ }
592
+ };
593
+ function appendOptional(type, isRequired) {
594
+ return isRequired ? type : `${type} | undefined`;
595
+ }
596
+
597
+ // packages/typescript/src/lib/emitters/zod.ts
598
+ var ZodDeserialzer = class {
599
+ circularRefTracker = /* @__PURE__ */ new Set();
600
+ #spec;
601
+ #onRef;
602
+ constructor(spec, onRef) {
603
+ this.#spec = spec;
604
+ this.#onRef = onRef;
605
+ }
606
+ /**
607
+ * Handle objects (properties, additionalProperties).
608
+ */
609
+ object(schema, required = false) {
610
+ const properties = schema.properties || {};
611
+ const propEntries = Object.entries(properties).map(([key, propSchema]) => {
612
+ const isRequired = (schema.required ?? []).includes(key);
613
+ const zodPart = this.handle(propSchema, isRequired);
614
+ return `'${key}': ${zodPart}`;
615
+ });
616
+ let additionalProps = "";
617
+ if (schema.additionalProperties) {
618
+ if (typeof schema.additionalProperties === "object") {
619
+ const addPropZod = this.handle(schema.additionalProperties, true);
620
+ additionalProps = `.catchall(${addPropZod})`;
621
+ } else if (schema.additionalProperties === true) {
622
+ additionalProps = `.catchall(z.unknown())`;
623
+ }
624
+ }
625
+ const objectSchema = `z.object({${propEntries.join(", ")}})${additionalProps}`;
626
+ return `${objectSchema}${appendOptional2(required)}`;
627
+ }
628
+ /**
629
+ * Handle arrays (items could be a single schema or a tuple (array of schemas)).
630
+ * In JSON Schema 2020-12, `items` can be an array → tuple style.
631
+ */
632
+ array(schema, required = false) {
633
+ const { items } = schema;
634
+ if (!items) {
635
+ return `z.array(z.unknown())${appendOptional2(required)}`;
636
+ }
637
+ if (Array.isArray(items)) {
638
+ const tupleItems = items.map((sub) => this.handle(sub, true));
639
+ const base = `z.tuple([${tupleItems.join(", ")}])`;
640
+ return `${base}${appendOptional2(required)}`;
641
+ }
642
+ const itemsSchema = this.handle(items, true);
643
+ return `z.array(${itemsSchema})${appendOptional2(required)}`;
644
+ }
645
+ // oneOf() {}
646
+ // enum() {}
647
+ /**
648
+ * Convert a basic type (string | number | boolean | object | array, etc.) to Zod.
649
+ * We'll also handle .optional() if needed.
650
+ */
651
+ normal(type, schema, required = false) {
652
+ switch (type) {
653
+ case "string":
654
+ return this.string(schema, required);
655
+ case "number":
656
+ case "integer":
657
+ return this.number(schema, required);
658
+ case "boolean":
659
+ return `z.boolean()${appendDefault(schema.default)}${appendOptional2(required)}`;
660
+ case "object":
661
+ return this.object(schema, required);
662
+ case "array":
663
+ return this.array(schema, required);
664
+ case "null":
665
+ return `z.null()${appendOptional2(required)}`;
666
+ default:
667
+ return `z.unknown()${appendOptional2(required)}`;
668
+ }
669
+ }
670
+ ref($ref, required) {
671
+ const schemaName = cleanRef($ref).split("/").pop();
672
+ if (this.circularRefTracker.has(schemaName)) {
673
+ return schemaName;
674
+ }
675
+ this.circularRefTracker.add(schemaName);
676
+ this.#onRef?.(schemaName, this.handle(followRef(this.#spec, $ref), required));
677
+ this.circularRefTracker.delete(schemaName);
678
+ return schemaName;
679
+ }
680
+ allOf(schemas) {
681
+ const allOfSchemas = schemas.map((sub) => this.handle(sub, true));
682
+ return allOfSchemas.length ? `z.intersection(${allOfSchemas.join(", ")})` : allOfSchemas[0];
683
+ }
684
+ anyOf(schemas, required) {
685
+ const anyOfSchemas = schemas.map((sub) => this.handle(sub, false));
686
+ return anyOfSchemas.length > 1 ? `z.union([${anyOfSchemas.join(", ")}])${appendOptional2(required)}` : (
687
+ // Handle an invalid anyOf with one schema
688
+ anyOfSchemas[0]
689
+ );
690
+ }
691
+ oneOf(schemas, required) {
692
+ const oneOfSchemas = schemas.map((sub) => {
693
+ if ("$ref" in sub) {
694
+ const { model } = parseRef(sub.$ref);
695
+ if (this.circularRefTracker.has(model)) {
696
+ return model;
697
+ }
698
+ }
699
+ return this.handle(sub, false);
700
+ });
701
+ return oneOfSchemas.length > 1 ? `z.union([${oneOfSchemas.join(", ")}])${appendOptional2(required)}` : (
702
+ // Handle an invalid oneOf with one schema
703
+ oneOfSchemas[0]
704
+ );
705
+ }
706
+ enum(values, required) {
707
+ const enumVals = values.map((val) => JSON.stringify(val)).join(", ");
708
+ return `z.enum([${enumVals}])${appendOptional2(required)}`;
709
+ }
710
+ /**
711
+ * Handle a `string` schema with possible format keywords (JSON Schema).
712
+ */
713
+ string(schema, required) {
714
+ let base = "z.string()";
715
+ switch (schema.format) {
716
+ case "date-time":
717
+ case "datetime":
718
+ base = "z.coerce.date()";
719
+ break;
720
+ case "date":
721
+ base = "z.coerce.date() /* or z.string() if you want raw date strings */";
722
+ break;
723
+ case "time":
724
+ base = "z.string() /* optionally add .regex(...) for HH:MM:SS format */";
725
+ break;
726
+ case "email":
727
+ base = "z.string().email()";
728
+ break;
729
+ case "uuid":
730
+ base = "z.string().uuid()";
731
+ break;
732
+ case "url":
733
+ case "uri":
734
+ base = "z.string().url()";
735
+ break;
736
+ case "ipv4":
737
+ base = 'z.string().ip({version: "v4"})';
738
+ break;
739
+ case "ipv6":
740
+ base = 'z.string().ip({version: "v6"})';
741
+ break;
742
+ case "phone":
743
+ base = "z.string() /* or add .regex(...) for phone formats */";
744
+ break;
745
+ case "byte":
746
+ case "binary":
747
+ base = "z.instanceof(Blob) /* consider base64 check if needed */";
748
+ break;
749
+ case "int64":
750
+ base = "z.string() /* or z.bigint() if your app can handle it */";
751
+ break;
752
+ default:
753
+ break;
754
+ }
755
+ return `${base}${appendDefault(schema.default)}${appendOptional2(required)}`;
756
+ }
757
+ /**
758
+ * Handle number/integer constraints from OpenAPI/JSON Schema.
759
+ * In 3.1, exclusiveMinimum/Maximum hold the actual numeric threshold,
760
+ * rather than a boolean toggling `minimum`/`maximum`.
761
+ */
762
+ number(schema, required) {
763
+ let defaultValue = schema.default !== void 0 ? `.default(${schema.default})` : ``;
764
+ let base = "z.number()";
765
+ if (schema.format === "int64") {
766
+ base = "z.bigint()";
767
+ if (schema.default !== void 0) {
768
+ defaultValue = `.default(BigInt(${schema.default}))`;
769
+ }
770
+ }
771
+ if (schema.format === "int32") {
772
+ base += ".int()";
773
+ }
774
+ if (typeof schema.exclusiveMinimum === "number") {
775
+ base += `.gt(${schema.exclusiveMinimum})`;
776
+ }
777
+ if (typeof schema.exclusiveMaximum === "number") {
778
+ base += `.lt(${schema.exclusiveMaximum})`;
779
+ }
780
+ if (typeof schema.minimum === "number") {
781
+ base += schema.format === "int64" ? `.min(BigInt(${schema.minimum}))` : `.min(${schema.minimum})`;
782
+ }
783
+ if (typeof schema.maximum === "number") {
784
+ base += schema.format === "int64" ? `.max(BigInt(${schema.maximum}))` : `.max(${schema.maximum})`;
785
+ }
786
+ if (typeof schema.multipleOf === "number") {
787
+ base += `.refine((val) => Number.isInteger(val / ${schema.multipleOf}), "Must be a multiple of ${schema.multipleOf}")`;
788
+ }
789
+ return `${base}${defaultValue}${appendOptional2(required)}`;
790
+ }
791
+ handle(schema, required) {
792
+ if (isRef(schema)) {
793
+ return this.ref(schema.$ref, required);
794
+ }
795
+ if (schema.allOf && Array.isArray(schema.allOf)) {
796
+ return this.allOf(schema.allOf ?? []);
797
+ }
798
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
799
+ return this.anyOf(schema.anyOf ?? [], required);
800
+ }
801
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
802
+ return this.oneOf(schema.oneOf ?? [], required);
803
+ }
804
+ if (schema.enum && Array.isArray(schema.enum)) {
805
+ return this.enum(schema.enum, required);
806
+ }
807
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
808
+ if (!types.length) {
809
+ return `z.unknown()${appendOptional2(required)}`;
810
+ }
811
+ if (types.length > 1) {
812
+ const realTypes = types.filter((t) => t !== "null");
813
+ if (realTypes.length === 1 && types.includes("null")) {
814
+ const typeZod = this.normal(realTypes[0], schema, false);
815
+ return `${typeZod}.nullable()${appendOptional2(required)}`;
816
+ }
817
+ const subSchemas = types.map((t) => this.normal(t, schema, false));
818
+ return `z.union([${subSchemas.join(", ")}])${appendOptional2(required)}`;
819
+ }
820
+ return this.normal(types[0], schema, required);
821
+ }
822
+ };
823
+ function appendOptional2(isRequired) {
824
+ return isRequired ? "" : ".optional()";
825
+ }
826
+ function appendDefault(defaultValue) {
827
+ return defaultValue !== void 0 ? `.default(${JSON.stringify(defaultValue)})` : "";
828
+ }
624
829
 
625
830
  // packages/typescript/src/lib/generator.ts
626
831
  var responses = {
@@ -653,8 +858,10 @@ var defaults = {
653
858
  }
654
859
  };
655
860
  function generateCode(config) {
656
- const groups = {};
861
+ const imports = [];
657
862
  const commonSchemas = {};
863
+ const zodDeserialzer = new ZodDeserialzer(config.spec);
864
+ const groups = {};
658
865
  const outputs = {};
659
866
  for (const [path, methods2] of Object.entries(config.spec.paths ?? {})) {
660
867
  for (const [method, operation] of Object.entries(methods2)) {
@@ -664,7 +871,6 @@ function generateCode(config) {
664
871
  const groupName = (operation.tags ?? ["unknown"])[0];
665
872
  groups[groupName] ??= [];
666
873
  const inputs = {};
667
- const imports = [];
668
874
  const additionalProperties = [];
669
875
  for (const param of operation.parameters ?? []) {
670
876
  if (isRef(param)) {
@@ -707,31 +913,18 @@ function generateCode(config) {
707
913
  if (operation.requestBody && Object.keys(operation.requestBody).length) {
708
914
  const content = isRef(operation.requestBody) ? get2(followRef(config.spec, operation.requestBody.$ref), ["content"]) : operation.requestBody.content;
709
915
  for (const type in content) {
710
- const schema = isRef(content[type].schema) ? followRef(config.spec, content[type].schema.$ref) : content[type].schema;
711
- types[shortContenTypeMap[type]] = jsonSchemaToZod(
712
- config.spec,
713
- merge(schema, {
714
- required: additionalProperties.filter((p) => p.required).map((p) => p.name),
715
- properties: additionalProperties.reduce(
716
- (acc, p) => ({
717
- ...acc,
718
- [p.name]: p.schema
719
- }),
720
- {}
721
- )
722
- }),
723
- true,
724
- (schemaName, zod) => {
725
- commonSchemas[schemaName] = zod;
726
- imports.push({
727
- defaultImport: void 0,
728
- isTypeOnly: false,
729
- moduleSpecifier: "../zod",
730
- namedImports: [{ isTypeOnly: false, name: schemaName }],
731
- namespaceImport: void 0
732
- });
733
- }
734
- );
916
+ const ctSchema = isRef(content[type].schema) ? followRef(config.spec, content[type].schema.$ref) : content[type].schema;
917
+ const schema = merge(ctSchema, {
918
+ required: additionalProperties.filter((p) => p.required).map((p) => p.name),
919
+ properties: additionalProperties.reduce(
920
+ (acc, p) => ({
921
+ ...acc,
922
+ [p.name]: p.schema
923
+ }),
924
+ {}
925
+ )
926
+ });
927
+ types[shortContenTypeMap[type]] = zodDeserialzer.handle(schema, true);
735
928
  }
736
929
  if (content["application/json"]) {
737
930
  contentType = "json";
@@ -743,30 +936,20 @@ function generateCode(config) {
743
936
  contentType = "json";
744
937
  }
745
938
  } else {
746
- types[shortContenTypeMap["application/json"]] = jsonSchemaToZod(
747
- config.spec,
939
+ const properties = additionalProperties.reduce(
940
+ (acc, p) => ({
941
+ ...acc,
942
+ [p.name]: p.schema
943
+ }),
944
+ {}
945
+ );
946
+ types[shortContenTypeMap["application/json"]] = zodDeserialzer.handle(
748
947
  {
749
948
  type: "object",
750
949
  required: additionalProperties.filter((p) => p.required).map((p) => p.name),
751
- properties: additionalProperties.reduce(
752
- (acc, p) => ({
753
- ...acc,
754
- [p.name]: p.schema
755
- }),
756
- {}
757
- )
950
+ properties
758
951
  },
759
- true,
760
- (schemaName, zod) => {
761
- commonSchemas[schemaName] = zod;
762
- imports.push({
763
- defaultImport: void 0,
764
- isTypeOnly: false,
765
- moduleSpecifier: "./zod",
766
- namedImports: [{ isTypeOnly: false, name: schemaName }],
767
- namespaceImport: void 0
768
- });
769
- }
952
+ true
770
953
  );
771
954
  }
772
955
  const errors = [];
@@ -783,48 +966,57 @@ function generateCode(config) {
783
966
  foundResponse = true;
784
967
  const responseContent = get2(response, ["content"]);
785
968
  const isJson = responseContent && responseContent["application/json"];
786
- const responseSchema = isJson ? jsonSchemaToZod(
969
+ const imports2 = [];
970
+ const typeScriptDeserialzer = new TypeScriptDeserialzer(
787
971
  config.spec,
788
- responseContent["application/json"].schema,
789
- true,
790
972
  (schemaName, zod) => {
791
973
  commonSchemas[schemaName] = zod;
792
- imports.push({
974
+ imports2.push({
793
975
  defaultImport: void 0,
794
- isTypeOnly: false,
795
- moduleSpecifier: "../zod",
796
- namedImports: [{ isTypeOnly: false, name: schemaName }],
976
+ isTypeOnly: true,
977
+ moduleSpecifier: `../models/${schemaName}.ts`,
978
+ namedImports: [{ isTypeOnly: true, name: schemaName }],
797
979
  namespaceImport: void 0
798
980
  });
799
981
  }
800
- ) : "z.instanceof(ReadableStream)";
801
- output.push(
802
- importsToString(mergeImports(Object.values(imports).flat())).join(
803
- "\n"
804
- )
805
982
  );
983
+ const responseSchema = isJson ? typeScriptDeserialzer.handle(
984
+ responseContent["application/json"].schema,
985
+ true
986
+ ) : "ReadableStream";
987
+ for (const it of mergeImports(imports2)) {
988
+ const singleImport = it.defaultImport ?? it.namespaceImport;
989
+ if (singleImport && responseSchema.includes(singleImport)) {
990
+ output.push(importsToString(it).join("\n"));
991
+ } else if (it.namedImports.length) {
992
+ for (const namedImport of it.namedImports) {
993
+ if (responseSchema.includes(namedImport.name)) {
994
+ output.push(importsToString(it).join("\n"));
995
+ }
996
+ }
997
+ }
998
+ }
806
999
  output.push(
807
- `export const ${pascalcase2(operationName + " output")} = ${responseSchema}`
1000
+ `export type ${pascalcase2(operationName + " output")} = ${responseSchema}`
808
1001
  );
809
1002
  }
810
1003
  }
811
1004
  if (!foundResponse) {
812
1005
  output.push(
813
- `export const ${pascalcase2(operationName + " output")} = z.void()`
1006
+ `export type ${pascalcase2(operationName + " output")} = void`
814
1007
  );
815
1008
  }
816
1009
  outputs[`${spinalcase2(operationName)}.ts`] = output.join("\n");
817
1010
  groups[groupName].push({
818
1011
  name: operationName,
819
1012
  type: "http",
820
- imports: mergeImports(Object.values(imports).flat()),
821
1013
  inputs,
822
1014
  errors: errors.length ? errors : ["ServerError"],
823
1015
  contentType,
824
1016
  schemas: types,
825
1017
  formatOutput: () => ({
826
1018
  import: pascalcase2(operationName + " output"),
827
- use: `z.infer<typeof ${pascalcase2(operationName + " output")}>`
1019
+ use: pascalcase2(operationName + " output")
828
1020
  }),
829
1021
  trigger: {
830
1022
  path,
@@ -835,35 +1027,6 @@ function generateCode(config) {
835
1027
  }
836
1028
  return { groups, commonSchemas, outputs };
837
1029
  }
838
- function mergeImports(imports) {
839
- const merged = {};
840
- for (const i of imports) {
841
- merged[i.moduleSpecifier] = merged[i.moduleSpecifier] ?? {
842
- moduleSpecifier: i.moduleSpecifier,
843
- defaultImport: i.defaultImport,
844
- namespaceImport: i.namespaceImport,
845
- namedImports: []
846
- };
847
- if (i.namedImports) {
848
- merged[i.moduleSpecifier].namedImports.push(...i.namedImports);
849
- }
850
- }
851
- return Object.values(merged);
852
- }
853
- function importsToString(imports) {
854
- return imports.map((i) => {
855
- if (i.defaultImport) {
856
- return `import ${i.defaultImport} from '${i.moduleSpecifier}'`;
857
- }
858
- if (i.namespaceImport) {
859
- return `import * as ${i.namespaceImport} from '${i.moduleSpecifier}'`;
860
- }
861
- if (i.namedImports) {
862
- return `import {${removeDuplicates(i.namedImports, (it) => it.name).map((n) => n.name).join(", ")}} from '${i.moduleSpecifier}'`;
863
- }
864
- throw new Error(`Invalid import ${JSON.stringify(i)}`);
865
- });
866
- }
867
1030
 
868
1031
  // packages/typescript/src/lib/http/client.txt
869
1032
  var client_default2 = "import { parse } from 'fast-content-type-parse';\n\nexport async function handleError(response: Response) {\n try {\n if (response.status >= 400 && response.status < 500) {\n const body = (await response.json()) as Record<string, any>;\n return {\n status: response.status,\n body: body,\n };\n }\n return new Error(\n `An error occurred while fetching the data. Status: ${response.status}`,\n );\n } catch (error) {\n return error as any;\n }\n}\n\nasync function handleChunkedResponse(response: Response, contentType: string) {\n const { type } = parse(contentType);\n\n switch (type) {\n case 'application/json': {\n let buffer = '';\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return JSON.parse(buffer);\n }\n case 'text/html':\n case 'text/plain': {\n let buffer = '';\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return buffer;\n }\n default:\n return response.body;\n }\n}\n\nexport async function parseResponse(response: Response) {\n const contentType = response.headers.get('Content-Type');\n if (!contentType) {\n throw new Error('Content-Type header is missing');\n }\n\n if (response.status === 204) {\n return null;\n }\n const isChunked = response.headers.get('Transfer-Encoding') === 'chunked';\n if (isChunked) {\n return response.body!;\n // return handleChunkedResponse(response, contentType);\n }\n\n const { type } = parse(contentType);\n switch (type) {\n case 'application/json':\n return response.json();\n case 'text/plain':\n return response.text();\n case 'text/html':\n return response.text();\n case 'text/xml':\n case 'application/xml':\n return response.text();\n case 'application/x-www-form-urlencoded': {\n const text = await response.text();\n return Object.fromEntries(new URLSearchParams(text));\n }\n case 'multipart/form-data':\n return response.formData();\n default:\n throw new Error(`Unsupported content type: ${contentType}`);\n }\n}\n";
@@ -872,13 +1035,13 @@ var client_default2 = "import { parse } from 'fast-content-type-parse';\n\nexpor
872
1035
  var parser_default = "import { z } from 'zod';\n\nexport type ParseError<T extends z.ZodType<any, any, any>> = {\n kind: 'parse';\n} & z.inferFlattenedErrors<T>;\n\nexport function parse<T extends z.ZodType>(\n schema: T,\n input: unknown,\n) {\n const result = schema.safeParse(input);\n if (!result.success) {\n const errors = result.error.flatten((issue) => issue);\n return [null, errors];\n }\n return [result.data as z.infer<T>, null];\n}\n";
873
1036
 
874
1037
  // packages/typescript/src/lib/http/request.txt
875
- var request_default = "type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\ntype ContentType = 'xml' | 'json' | 'urlencoded' | 'multipart';\ntype Endpoint = `${ContentType} ${Method} ${string}` | `${Method} ${string}`;\n\nexport function createUrl(base: string, path: string, query: URLSearchParams) {\n const url = new URL(path, base);\n url.search = query.toString();\n return url;\n}\nfunction template(\n templateString: string,\n templateVariables: Record<string, any>,\n): string {\n const nargs = /{([0-9a-zA-Z_]+)}/g;\n return templateString.replace(nargs, (match, key: string, index: number) => {\n // Handle escaped double braces\n if (\n templateString[index - 1] === '{' &&\n templateString[index + match.length] === '}'\n ) {\n return key;\n }\n\n const result = key in templateVariables ? templateVariables[key] : null;\n return result === null || result === undefined ? '' : String(result);\n });\n}\n\ninterface ToRequest {\n <T extends Endpoint>(\n endpoint: T,\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n ): Request;\n urlencoded: <T extends Endpoint>(\n endpoint: T,\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n ) => Request;\n}\n\nfunction _json(\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n) {\n const headers = new Headers({});\n for (const header of props.inputHeaders) {\n headers.set(header, input[header]);\n }\n\n const body: Record<string, any> = {};\n for (const prop of props.inputBody) {\n body[prop] = input[prop];\n }\n\n const query = new URLSearchParams();\n for (const key of props.inputQuery) {\n const value = input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = props.inputParams.reduce<Record<string, any>>((acc, key) => {\n acc[key] = input[key];\n return acc;\n }, {});\n\n return {\n body: JSON.stringify(body),\n query,\n params,\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n };\n}\n\ntype Input = Record<string, any>;\ntype Props = {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n};\n\nabstract class Serializer {\n constructor(\n protected input: Input,\n protected props: Props,\n ) {}\n abstract getBody(): BodyInit | null;\n abstract getHeaders(): Partial<Record<string, string>>;\n serialize(): Serialized {\n const headers = new Headers({});\n for (const header of this.props.inputHeaders) {\n headers.set(header, this.input[header]);\n }\n\n const query = new URLSearchParams();\n for (const key of this.props.inputQuery) {\n const value = this.input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = this.props.inputParams.reduce<Record<string, any>>(\n (acc, key) => {\n acc[key] = this.input[key];\n return acc;\n },\n {},\n );\n\n return {\n body: this.getBody(),\n query,\n params,\n headers: this.getHeaders(),\n };\n }\n}\n\ninterface Serialized {\n body: BodyInit | null;\n query: URLSearchParams;\n params: Record<string, any>;\n headers: Partial<Record<string, string>>;\n}\n\nclass JsonSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body: Record<string, any> = {};\n for (const prop of this.props.inputBody) {\n body[prop] = this.input[prop];\n }\n return JSON.stringify(body);\n }\n getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n}\n\nclass UrlencodedSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new URLSearchParams();\n for (const prop of this.props.inputBody) {\n body.set(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass FormDataSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new FormData();\n for (const prop of this.props.inputBody) {\n body.append(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nexport function json(input: Input, props: Props) {\n return new JsonSerializer(input, props).serialize();\n}\nexport function urlencoded(input: Input, props: Props) {\n return new UrlencodedSerializer(input, props).serialize();\n}\nexport function formdata(input: Input, props: Props) {\n return new FormDataSerializer(input, props).serialize();\n}\n\nexport function _urlencoded(\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n) {\n const headers = new Headers({});\n for (const header of props.inputHeaders) {\n headers.set(header, input[header]);\n }\n\n const body = new URLSearchParams();\n for (const prop of props.inputBody) {\n body.set(prop, input[prop]);\n }\n\n const query = new URLSearchParams();\n for (const key of props.inputQuery) {\n const value = input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = props.inputParams.reduce<Record<string, any>>((acc, key) => {\n acc[key] = input[key];\n return acc;\n }, {});\n\n return {\n body,\n query,\n params,\n headers: {},\n };\n}\n\nexport function toRequest<T extends Endpoint>(\n endpoint: T,\n input: Serialized,\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n): Request {\n const [method, path] = endpoint.split(' ');\n\n const headers = new Headers(\n Object.entries({\n ...defaults?.headers,\n ...input.headers,\n }).filter(truthyEntry),\n );\n const pathVariable = template(path, input.params);\n\n const url = createUrl(defaults.baseUrl, pathVariable, input.query);\n return new Request(url, {\n method: method,\n headers: headers,\n body: method === 'GET' ? undefined : input.body,\n });\n}\n\nfunction truthyEntry(entry: [string, unknown]): entry is [string, string] {\n return entry[1] !== undefined;\n}\n";
1038
+ var request_default = "type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\ntype ContentType = 'xml' | 'json' | 'urlencoded' | 'multipart';\ntype Endpoint = `${ContentType} ${Method} ${string}` | `${Method} ${string}`;\nexport type BodyInit =\n | ArrayBuffer\n | AsyncIterable<Uint8Array>\n | Blob\n | FormData\n | Iterable<Uint8Array>\n | NodeJS.ArrayBufferView\n | URLSearchParams\n | null\n | string;\n\nexport function createUrl(base: string, path: string, query: URLSearchParams) {\n const url = new URL(path, base);\n url.search = query.toString();\n return url;\n}\nfunction template(\n templateString: string,\n templateVariables: Record<string, any>,\n): string {\n const nargs = /{([0-9a-zA-Z_]+)}/g;\n return templateString.replace(nargs, (match, key: string, index: number) => {\n // Handle escaped double braces\n if (\n templateString[index - 1] === '{' &&\n templateString[index + match.length] === '}'\n ) {\n return key;\n }\n\n const result = key in templateVariables ? templateVariables[key] : null;\n return result === null || result === undefined ? '' : String(result);\n });\n}\n\ninterface ToRequest {\n <T extends Endpoint>(\n endpoint: T,\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n ): Request;\n urlencoded: <T extends Endpoint>(\n endpoint: T,\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n ) => Request;\n}\n\nfunction _json(\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n) {\n const headers = new Headers({});\n for (const header of props.inputHeaders) {\n headers.set(header, input[header]);\n }\n\n const body: Record<string, any> = {};\n for (const prop of props.inputBody) {\n body[prop] = input[prop];\n }\n\n const query = new URLSearchParams();\n for (const key of props.inputQuery) {\n const value = input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = props.inputParams.reduce<Record<string, any>>((acc, key) => {\n acc[key] = input[key];\n return acc;\n }, {});\n\n return {\n body: JSON.stringify(body),\n query,\n params,\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n };\n}\n\ntype Input = Record<string, any>;\ntype Props = {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n};\n\nabstract class Serializer {\n constructor(\n protected input: Input,\n protected props: Props,\n ) {}\n abstract getBody(): BodyInit | null;\n abstract getHeaders(): Partial<Record<string, string>>;\n serialize(): Serialized {\n const headers = new Headers({});\n for (const header of this.props.inputHeaders) {\n headers.set(header, this.input[header]);\n }\n\n const query = new URLSearchParams();\n for (const key of this.props.inputQuery) {\n const value = this.input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = this.props.inputParams.reduce<Record<string, any>>(\n (acc, key) => {\n acc[key] = this.input[key];\n return acc;\n },\n {},\n );\n\n return {\n body: this.getBody(),\n query,\n params,\n headers: this.getHeaders(),\n };\n }\n}\n\ninterface Serialized {\n body: BodyInit | null;\n query: URLSearchParams;\n params: Record<string, any>;\n headers: Partial<Record<string, string>>;\n}\n\nclass JsonSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body: Record<string, any> = {};\n for (const prop of this.props.inputBody) {\n body[prop] = this.input[prop];\n }\n return JSON.stringify(body);\n }\n getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n}\n\nclass UrlencodedSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new URLSearchParams();\n for (const prop of this.props.inputBody) {\n body.set(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nclass FormDataSerializer extends Serializer {\n getBody(): BodyInit | null {\n const body = new FormData();\n for (const prop of this.props.inputBody) {\n body.append(prop, this.input[prop]);\n }\n return body;\n }\n getHeaders(): Record<string, string> {\n return {};\n }\n}\n\nexport function json(input: Input, props: Props) {\n return new JsonSerializer(input, props).serialize();\n}\nexport function urlencoded(input: Input, props: Props) {\n return new UrlencodedSerializer(input, props).serialize();\n}\nexport function formdata(input: Input, props: Props) {\n return new FormDataSerializer(input, props).serialize();\n}\n\nexport function _urlencoded(\n input: Record<string, any>,\n props: {\n inputHeaders: string[];\n inputQuery: string[];\n inputBody: string[];\n inputParams: string[];\n },\n) {\n const headers = new Headers({});\n for (const header of props.inputHeaders) {\n headers.set(header, input[header]);\n }\n\n const body = new URLSearchParams();\n for (const prop of props.inputBody) {\n body.set(prop, input[prop]);\n }\n\n const query = new URLSearchParams();\n for (const key of props.inputQuery) {\n const value = input[key];\n if (value !== undefined) {\n query.set(key, String(value));\n }\n }\n\n const params = props.inputParams.reduce<Record<string, any>>((acc, key) => {\n acc[key] = input[key];\n return acc;\n }, {});\n\n return {\n body,\n query,\n params,\n headers: {},\n };\n}\n\nexport function toRequest<T extends Endpoint>(\n endpoint: T,\n input: Serialized,\n defaults: {\n baseUrl: string;\n headers?: Partial<Record<string, string>>;\n },\n): Request {\n const [method, path] = endpoint.split(' ');\n\n const headers = new Headers(\n Object.entries({\n ...defaults?.headers,\n ...input.headers,\n }).filter(truthyEntry),\n );\n const pathVariable = template(path, input.params);\n\n const url = createUrl(defaults.baseUrl, pathVariable, input.query);\n return new Request(url, {\n method: method,\n headers: headers,\n body: method === 'GET' ? undefined : input.body,\n });\n}\n\nfunction truthyEntry(entry: [string, unknown]): entry is [string, string] {\n return entry[1] !== undefined;\n}\n";
876
1039
 
877
1040
  // packages/typescript/src/lib/http/response.txt
878
1041
  var response_default = "export interface ApiResponse<Status extends number, Body extends unknown> {\n kind: 'response';\n status: Status;\n body: Body;\n}\n\n// 4xx Client Errors\nexport type BadRequest = ApiResponse<400, { message: string }>;\nexport type Unauthorized = ApiResponse<401, { message: string }>;\nexport type PaymentRequired = ApiResponse<402, { message: string }>;\nexport type Forbidden = ApiResponse<403, { message: string }>;\nexport type NotFound = ApiResponse<404, { message: string }>;\nexport type MethodNotAllowed = ApiResponse<405, { message: string }>;\nexport type NotAcceptable = ApiResponse<406, { message: string }>;\nexport type Conflict = ApiResponse<409, { message: string }>;\nexport type Gone = ApiResponse<410, { message: string }>;\nexport type UnprocessableEntity = ApiResponse<422, { message: string; errors?: Record<string, string[]> }>;\nexport type TooManyRequests = ApiResponse<429, { message: string; retryAfter?: string }>;\nexport type PayloadTooLarge = ApiResponse<413, { message: string; }>;\nexport type UnsupportedMediaType = ApiResponse<415, { message: string; }>;\n\n// 5xx Server Errors\nexport type InternalServerError = ApiResponse<500, { message: string }>;\nexport type NotImplemented = ApiResponse<501, { message: string }>;\nexport type BadGateway = ApiResponse<502, { message: string }>;\nexport type ServiceUnavailable = ApiResponse<503, { message: string; retryAfter?: string }>;\nexport type GatewayTimeout = ApiResponse<504, { message: string }>;\n\nexport type ClientError =\n | BadRequest\n | Unauthorized\n | PaymentRequired\n | Forbidden\n | NotFound\n | MethodNotAllowed\n | NotAcceptable\n | Conflict\n | Gone\n | UnprocessableEntity\n | TooManyRequests;\n\nexport type ServerError =\n | InternalServerError\n | NotImplemented\n | BadGateway\n | ServiceUnavailable\n | GatewayTimeout;\n\nexport type ProblematicResponse = ClientError | ServerError;\n";
879
1042
 
880
1043
  // packages/typescript/src/lib/http/send-request.txt
881
- var send_request_default = "import z from 'zod';\n\nimport { handleError, parseResponse } from './parse-response.ts';\nimport { parse } from './parser.ts';\n\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (\n input: any,\n init: { baseUrl: string; headers?: Partial<Record<string, string>> },\n ) => any;\n}\n\nexport const fetchType = z\n .function()\n .args(z.instanceof(Request))\n .returns(z.promise(z.instanceof(Response)))\n .optional();\n\nexport async function sendRequest(\n input: any,\n route: RequestSchema,\n options: {\n baseUrl: string;\n fetch?: z.infer<typeof fetchType>;\n headers?: Partial<Record<string, string>>;\n },\n) {\n const [parsedInput, parseError] = parse(route.schema, input);\n if (parseError) {\n return [null as never, { ...parseError, kind: 'parse' } as never] as const;\n }\n const request = route.toRequest(parsedInput as never, {\n headers: options.headers,\n baseUrl: options.baseUrl,\n });\n const response = await (options.fetch ?? fetch)(request);\n if (response.ok) {\n const data = await parseResponse(response);\n return [data, null] as const;\n }\n const error = await handleError(response);\n return [null as never, { ...error, kind: 'response' }] as const;\n}\n";
1044
+ var send_request_default = "import z from 'zod';\n\nimport { handleError, parseResponse } from './parse-response.ts';\nimport { parse } from './parser.ts';\n\nexport interface RequestSchema {\n schema: z.ZodType;\n toRequest: (\n input: any,\n init: { baseUrl: string; headers?: Partial<Record<string, string>> },\n ) => any;\n}\n\nexport const fetchType = z\n .function()\n .args(z.instanceof(Request))\n .returns(z.promise(z.instanceof(Response)))\n .optional();\n\nexport async function sendRequest(\n input: any,\n route: RequestSchema,\n options: {\n baseUrl: string;\n fetch?: z.infer<typeof fetchType>;\n headers?: Partial<Record<string, string>>;\n },\n) {\n const [parsedInput, parseError] = parse(route.schema, input);\n if (parseError) {\n return [null as never, { ...parseError, kind: 'parse' } as never] as const;\n }\n const request = route.toRequest(parsedInput as never, {\n headers: options.headers,\n baseUrl: options.baseUrl,\n });\n const response = await (options.fetch ?? fetch)(request);\n if (response.ok) {\n const data = await parseResponse(response);\n return [data as never, null] as const;\n }\n const error = await handleError(response);\n return [null as never, { ...error, kind: 'response' }] as const;\n}\n";
882
1045
 
883
1046
  // packages/typescript/src/lib/generate.ts
884
1047
  function security(spec) {
@@ -928,22 +1091,35 @@ async function generate(spec, settings) {
928
1091
  "request.ts": request_default
929
1092
  });
930
1093
  await writeFiles(join(output, "outputs"), outputs);
1094
+ const imports = Object.entries(commonSchemas).map(([name]) => name);
931
1095
  await writeFiles(output, {
932
1096
  ...clientFiles,
933
- "zod.ts": `import z from 'zod';
934
- ${Object.entries(commonSchemas).map(([name, schema]) => `export const ${name} = ${schema};`).join("\n")}`
1097
+ ...Object.fromEntries(
1098
+ Object.entries(commonSchemas).map(([name, schema]) => [
1099
+ `models/${name}.ts`,
1100
+ [
1101
+ `import { z } from 'zod';`,
1102
+ ...exclude(imports, [name]).map(
1103
+ (it) => `import type { ${it} } from './${it}.ts';`
1104
+ ),
1105
+ `export type ${name} = ${schema};`
1106
+ ].join("\n")
1107
+ ])
1108
+ )
935
1109
  });
936
- const [index, outputIndex, inputsIndex, httpIndex] = await Promise.all([
1110
+ const [index, outputIndex, inputsIndex, httpIndex, modelsIndex] = await Promise.all([
937
1111
  getFolderExports(output),
938
1112
  getFolderExports(join(output, "outputs")),
939
1113
  getFolderExports(join(output, "inputs")),
940
- getFolderExports(join(output, "http"))
1114
+ getFolderExports(join(output, "http")),
1115
+ getFolderExports(join(output, "models"))
941
1116
  ]);
942
1117
  await writeFiles(output, {
943
1118
  "index.ts": index,
944
1119
  "outputs/index.ts": outputIndex,
945
1120
  "inputs/index.ts": inputsIndex,
946
- "http/index.ts": httpIndex
1121
+ "http/index.ts": httpIndex,
1122
+ "models/index.ts": modelsIndex
947
1123
  });
948
1124
  if (settings.mode === "full") {
949
1125
  await writeFiles(settings.output, {
@@ -958,6 +1134,9 @@ ${Object.entries(commonSchemas).map(([name, schema]) => `export const ${name} =
958
1134
  env: npmRunPathEnv()
959
1135
  });
960
1136
  }
1137
+ function exclude(list, exclude2) {
1138
+ return list.filter((it) => !exclude2.includes(it));
1139
+ }
961
1140
 
962
1141
  // packages/typescript/src/lib/watcher.ts
963
1142
  import { watch as nodeWatch } from "node:fs/promises";