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/dist/cli.mjs CHANGED
@@ -214,11 +214,13 @@ function generate(spec, options = {}) {
214
214
  const output = [];
215
215
  const generatedTypes = /* @__PURE__ */ new Set();
216
216
  const { strictDates = false, strictNumeric = false } = options;
217
+ const typesConfig = normalizeTypesConfig(options.types);
217
218
  const schemaOptions = {
218
219
  strictDates,
219
220
  strictNumeric
220
221
  };
221
222
  output.push('import { z } from "zod";');
223
+ appendHelperTypesImport(output, typesConfig);
222
224
  output.push("");
223
225
  if (spec.components?.schemas) {
224
226
  output.push("// Generated Zod Schemas");
@@ -238,16 +240,79 @@ function generate(spec, options = {}) {
238
240
  output.push("// Path Functions");
239
241
  output.push("export const paths = {");
240
242
  for (const op of operations) {
241
- if (op.pathParams.length === 0) {
243
+ const pathParamNames = op.pathParams.map((p) => p.name);
244
+ const hasPathParams = pathParamNames.length > 0;
245
+ const hasQueryParams = op.queryParams.length > 0;
246
+ if (!hasPathParams && !hasQueryParams) {
242
247
  output.push(` ${op.operationId}: () => "${op.path}",`);
243
- } else {
244
- const paramNames = op.pathParams.map((p) => p.name).join(", ");
245
- const paramTypes = op.pathParams.map((p) => `${p.name}: string`).join(", ");
246
- const pathWithParams = op.path.replace(/{([^}]+)}/g, "${$1}");
248
+ continue;
249
+ }
250
+ const allParamNames = [
251
+ ...pathParamNames,
252
+ ...op.queryParams.map((p) => p.name)
253
+ ];
254
+ const signaturePieces = [];
255
+ for (const param of op.pathParams) {
256
+ signaturePieces.push(`${param.name}: string`);
257
+ }
258
+ for (const param of op.queryParams) {
259
+ signaturePieces.push(
260
+ `${param.name}${param.required ? "" : "?"}: ${mapQueryType(param)}`
261
+ );
262
+ }
263
+ const signatureParams = signaturePieces.join(", ");
264
+ const needsDefaultObject = !hasPathParams && hasQueryParams && op.queryParams.every((param) => !param.required);
265
+ const signatureArgs = allParamNames.length ? `{ ${allParamNames.join(", ")} }` : "{}";
266
+ const signature = `${signatureArgs}: { ${signatureParams} }${needsDefaultObject ? " = {}" : ""}`;
267
+ const pathWithParams = op.path.replace(/{([^}]+)}/g, "${$1}");
268
+ if (!hasQueryParams) {
247
269
  output.push(
248
- ` ${op.operationId}: ({ ${paramNames} }: { ${paramTypes} }) => \`${pathWithParams}\`,`
270
+ ` ${op.operationId}: (${signature}) => \`${pathWithParams}\`,`
249
271
  );
272
+ continue;
273
+ }
274
+ output.push(` ${op.operationId}: (${signature}) => {`);
275
+ output.push(" const params = new URLSearchParams()");
276
+ for (const param of op.queryParams) {
277
+ const propertyKey = formatPropertyName(param.name);
278
+ const accessor = isValidJSIdentifier(param.name) ? param.name : propertyKey;
279
+ const schema = param.schema ?? {};
280
+ if (schema?.type === "array") {
281
+ const itemValueExpression = convertQueryParamValue(
282
+ schema.items ?? {},
283
+ "value"
284
+ );
285
+ if (param.required) {
286
+ output.push(` for (const value of ${accessor}) {`);
287
+ output.push(
288
+ ` params.append("${param.name}", ${itemValueExpression})`
289
+ );
290
+ output.push(" }");
291
+ } else {
292
+ output.push(` if (${accessor} !== undefined) {`);
293
+ output.push(` for (const value of ${accessor}) {`);
294
+ output.push(
295
+ ` params.append("${param.name}", ${itemValueExpression})`
296
+ );
297
+ output.push(" }");
298
+ output.push(" }");
299
+ }
300
+ continue;
301
+ }
302
+ const valueExpression = convertQueryParamValue(schema, accessor);
303
+ if (param.required) {
304
+ output.push(` params.set("${param.name}", ${valueExpression})`);
305
+ } else {
306
+ output.push(` if (${accessor} !== undefined) {`);
307
+ output.push(` params.set("${param.name}", ${valueExpression})`);
308
+ output.push(" }");
309
+ }
250
310
  }
311
+ output.push(" const _searchParams = params.toString()");
312
+ output.push(
313
+ ` return \`${pathWithParams}\${_searchParams ? \`?\${_searchParams}\` : ""}\``
314
+ );
315
+ output.push(" },");
251
316
  }
252
317
  output.push("} as const;");
253
318
  output.push("");
@@ -339,6 +404,7 @@ function generate(spec, options = {}) {
339
404
  output.push("} as const;");
340
405
  output.push("");
341
406
  }
407
+ generateOperationTypes(output, operations, typesConfig);
342
408
  return output.join("\n");
343
409
  }
344
410
  function appendOperationField(buffer, key, value) {
@@ -374,11 +440,13 @@ function parseOperations(spec) {
374
440
  );
375
441
  const resolvedParameters = collectParameters(pathItem, operation, spec);
376
442
  const requestHeaders = getRequestHeaders(resolvedParameters);
443
+ const queryParams = getQueryParams(resolvedParameters);
377
444
  operations.push({
378
445
  operationId: operation.operationId,
379
446
  path: path2,
380
447
  method: method.toLowerCase(),
381
448
  pathParams,
449
+ queryParams,
382
450
  requestType,
383
451
  responseType: successResponse,
384
452
  requestHeaders,
@@ -388,6 +456,129 @@ function parseOperations(spec) {
388
456
  }
389
457
  return operations;
390
458
  }
459
+ function normalizeTypesConfig(config) {
460
+ return {
461
+ emit: config?.emit ?? true,
462
+ helpers: config?.helpers ?? "package",
463
+ helpersOutput: config?.helpersOutput ?? "./zenko-types"
464
+ };
465
+ }
466
+ function appendHelperTypesImport(buffer, config) {
467
+ if (!config.emit) return;
468
+ switch (config.helpers) {
469
+ case "package":
470
+ buffer.push(
471
+ 'import type { PathFn, HeaderFn, OperationDefinition, OperationErrors } from "zenko";'
472
+ );
473
+ return;
474
+ case "file":
475
+ buffer.push(
476
+ `import type { PathFn, HeaderFn, OperationDefinition, OperationErrors } from "${config.helpersOutput}";`
477
+ );
478
+ return;
479
+ case "inline":
480
+ buffer.push(
481
+ "type PathFn<TArgs extends unknown[] = []> = (...args: TArgs) => string;"
482
+ );
483
+ buffer.push(
484
+ "type HeaderFn<TArgs extends unknown[] = [], TResult = Record<string, unknown> | Record<string, never>> = (...args: TArgs) => TResult;"
485
+ );
486
+ buffer.push(
487
+ "type OperationErrors<TClient = unknown, TServer = unknown, TDefault = unknown, TOther = unknown> = {"
488
+ );
489
+ buffer.push(" clientErrors?: Record<string, TClient>;");
490
+ buffer.push(" serverErrors?: Record<string, TServer>;");
491
+ buffer.push(" defaultErrors?: Record<string, TDefault>;");
492
+ buffer.push(" otherErrors?: Record<string, TOther>;");
493
+ buffer.push("};");
494
+ buffer.push(
495
+ "type OperationDefinition<TPath extends (...args: any[]) => string, TRequest = undefined, TResponse = undefined, THeaders extends HeaderFn | undefined = undefined, TErrors extends OperationErrors | undefined = undefined> = {"
496
+ );
497
+ buffer.push(" path: TPath;");
498
+ buffer.push(" request?: TRequest;");
499
+ buffer.push(" response?: TResponse;");
500
+ buffer.push(" headers?: THeaders;");
501
+ buffer.push(" errors?: TErrors;");
502
+ buffer.push("};");
503
+ return;
504
+ }
505
+ }
506
+ function generateOperationTypes(buffer, operations, config) {
507
+ if (!config.emit) return;
508
+ buffer.push("// Operation Types");
509
+ for (const op of operations) {
510
+ const headerType = op.requestHeaders?.length ? `typeof headers.${op.operationId}` : "undefined";
511
+ const requestType = wrapTypeReference(op.requestType);
512
+ const responseType = wrapTypeReference(op.responseType);
513
+ const errorsType = buildOperationErrorsType(op.errors);
514
+ buffer.push(
515
+ `export type ${capitalize(op.operationId)}Operation = OperationDefinition<`
516
+ );
517
+ buffer.push(` typeof paths.${op.operationId},`);
518
+ buffer.push(` ${requestType},`);
519
+ buffer.push(` ${responseType},`);
520
+ buffer.push(` ${headerType},`);
521
+ buffer.push(` ${errorsType}`);
522
+ buffer.push(`>;`);
523
+ buffer.push("");
524
+ }
525
+ }
526
+ function buildOperationErrorsType(errors) {
527
+ if (!errors || !hasAnyErrors(errors)) {
528
+ return "OperationErrors";
529
+ }
530
+ const client = buildErrorBucket(errors.clientErrors);
531
+ const server = buildErrorBucket(errors.serverErrors);
532
+ const fallback = buildErrorBucket(errors.defaultErrors);
533
+ const other = buildErrorBucket(errors.otherErrors);
534
+ return `OperationErrors<${client}, ${server}, ${fallback}, ${other}>`;
535
+ }
536
+ function buildErrorBucket(bucket) {
537
+ if (!bucket || Object.keys(bucket).length === 0) {
538
+ return "unknown";
539
+ }
540
+ const entries = Object.entries(bucket);
541
+ const accessibleEntries = entries.map(([name, type]) => {
542
+ const propertyKey = formatPropertyName(name);
543
+ const valueType = wrapErrorValueType(type);
544
+ return `${propertyKey}: ${valueType}`;
545
+ });
546
+ return `{ ${accessibleEntries.join("; ")} }`;
547
+ }
548
+ var TYPE_KEYWORDS = /* @__PURE__ */ new Set([
549
+ "any",
550
+ "unknown",
551
+ "never",
552
+ "void",
553
+ "null",
554
+ "undefined",
555
+ "string",
556
+ "number",
557
+ "boolean",
558
+ "bigint",
559
+ "symbol"
560
+ ]);
561
+ function wrapTypeReference(typeName) {
562
+ if (!typeName) return "undefined";
563
+ if (typeName === "undefined") return "undefined";
564
+ if (TYPE_KEYWORDS.has(typeName)) return typeName;
565
+ if (typeName.startsWith("typeof ")) return typeName;
566
+ const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
567
+ if (identifierPattern.test(typeName)) {
568
+ return `typeof ${typeName}`;
569
+ }
570
+ return typeName;
571
+ }
572
+ function wrapErrorValueType(typeName) {
573
+ if (!typeName) return "unknown";
574
+ if (TYPE_KEYWORDS.has(typeName)) return typeName;
575
+ if (typeName.startsWith("typeof ")) return typeName;
576
+ const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
577
+ if (identifierPattern.test(typeName)) {
578
+ return `typeof ${typeName}`;
579
+ }
580
+ return typeName;
581
+ }
391
582
  function collectParameters(pathItem, operation, spec) {
392
583
  const parametersMap = /* @__PURE__ */ new Map();
393
584
  const addParameters = (params) => {
@@ -530,6 +721,20 @@ function getRequestHeaders(parameters) {
530
721
  }
531
722
  return headers;
532
723
  }
724
+ function getQueryParams(parameters) {
725
+ const queryParams = [];
726
+ for (const param of parameters ?? []) {
727
+ if (param.in === "query") {
728
+ queryParams.push({
729
+ name: param.name,
730
+ description: param.description,
731
+ schema: param.schema,
732
+ required: param.required
733
+ });
734
+ }
735
+ }
736
+ return queryParams;
737
+ }
533
738
  function mapHeaderType(header) {
534
739
  const schemaType = header.schema?.type;
535
740
  switch (schemaType) {
@@ -542,6 +747,39 @@ function mapHeaderType(header) {
542
747
  return "string";
543
748
  }
544
749
  }
750
+ function mapQueryType(param) {
751
+ return mapQuerySchemaType(param.schema);
752
+ }
753
+ function mapQuerySchemaType(schema) {
754
+ if (!schema) return "string";
755
+ if (schema.type === "array") {
756
+ const itemType = mapQuerySchemaType(schema.items);
757
+ return `Array<${itemType}>`;
758
+ }
759
+ switch (schema.type) {
760
+ case "integer":
761
+ case "number":
762
+ return "number";
763
+ case "boolean":
764
+ return "boolean";
765
+ default:
766
+ return "string";
767
+ }
768
+ }
769
+ function convertQueryParamValue(schema, accessor) {
770
+ if (!schema) {
771
+ return `String(${accessor})`;
772
+ }
773
+ switch (schema.type) {
774
+ case "integer":
775
+ case "number":
776
+ return `String(${accessor})`;
777
+ case "boolean":
778
+ return `${accessor} ? "true" : "false"`;
779
+ default:
780
+ return `String(${accessor})`;
781
+ }
782
+ }
545
783
  function generateZodSchema(name, schema, generatedTypes, options) {
546
784
  if (generatedTypes.has(name)) return "";
547
785
  generatedTypes.add(name);
@@ -550,18 +788,7 @@ function generateZodSchema(name, schema, generatedTypes, options) {
550
788
  return `export const ${name} = z.enum([${enumValues}]);`;
551
789
  }
552
790
  if (schema.type === "object" || schema.properties) {
553
- const properties = [];
554
- for (const [propName, propSchema] of Object.entries(
555
- schema.properties || {}
556
- )) {
557
- const isRequired = schema.required?.includes(propName) ?? false;
558
- const zodType = getZodTypeFromSchema(propSchema, options);
559
- const finalType = isRequired ? zodType : `${zodType}.optional()`;
560
- properties.push(` ${formatPropertyName(propName)}: ${finalType},`);
561
- }
562
- return `export const ${name} = z.object({
563
- ${properties.join("\n")}
564
- });`;
791
+ return `export const ${name} = ${buildZodObject(schema, options)};`;
565
792
  }
566
793
  if (schema.type === "array") {
567
794
  const itemSchema = schema.items ?? { type: "unknown" };
@@ -584,6 +811,9 @@ function getZodTypeFromSchema(schema, options) {
584
811
  const enumValues = schema.enum.map((v) => `"${v}"`).join(", ");
585
812
  return `z.enum([${enumValues}])`;
586
813
  }
814
+ if (schema.type === "object" || schema.properties) {
815
+ return buildZodObject(schema, options);
816
+ }
587
817
  switch (schema.type) {
588
818
  case "string":
589
819
  return buildString(schema, options);
@@ -604,6 +834,23 @@ function getZodTypeFromSchema(schema, options) {
604
834
  return "z.unknown()";
605
835
  }
606
836
  }
837
+ function buildZodObject(schema, options) {
838
+ const properties = [];
839
+ for (const [propName, propSchema] of Object.entries(
840
+ schema.properties || {}
841
+ )) {
842
+ const isRequired = schema.required?.includes(propName) ?? false;
843
+ const zodType = getZodTypeFromSchema(propSchema, options);
844
+ const finalType = isRequired ? zodType : `${zodType}.optional()`;
845
+ properties.push(` ${formatPropertyName(propName)}: ${finalType},`);
846
+ }
847
+ if (properties.length === 0) {
848
+ return "z.object({})";
849
+ }
850
+ return `z.object({
851
+ ${properties.join("\n")}
852
+ })`;
853
+ }
607
854
  function buildString(schema, options) {
608
855
  if (options.strictDates) {
609
856
  switch (schema.format) {
@@ -794,7 +1041,7 @@ function printHelp() {
794
1041
  console.log("");
795
1042
  console.log("Config file format:");
796
1043
  console.log(
797
- ' {"schemas": [{ input, output, strictDates?, strictNumeric? }] }'
1044
+ ' {"types"?: { emit?, helpers?, helpersOutput? }, "schemas": [{ input, output, strictDates?, strictNumeric?, types? }] }'
798
1045
  );
799
1046
  }
800
1047
  async function runFromConfig(parsed) {
@@ -803,14 +1050,17 @@ async function runFromConfig(parsed) {
803
1050
  const config = await loadConfig(resolvedConfigPath);
804
1051
  validateConfig(config);
805
1052
  const baseDir = path.dirname(resolvedConfigPath);
1053
+ const baseTypesConfig = config.types;
806
1054
  for (const entry of config.schemas) {
807
1055
  const inputFile = resolvePath(entry.input, baseDir);
808
1056
  const outputFile = resolvePath(entry.output, baseDir);
1057
+ const typesConfig = resolveTypesConfig(baseTypesConfig, entry.types);
809
1058
  await generateSingle({
810
1059
  inputFile,
811
1060
  outputFile,
812
1061
  strictDates: entry.strictDates ?? parsed.strictDates,
813
- strictNumeric: entry.strictNumeric ?? parsed.strictNumeric
1062
+ strictNumeric: entry.strictNumeric ?? parsed.strictNumeric,
1063
+ typesConfig
814
1064
  });
815
1065
  }
816
1066
  }
@@ -847,12 +1097,23 @@ function validateConfig(config) {
847
1097
  function resolvePath(filePath, baseDir) {
848
1098
  return path.isAbsolute(filePath) ? filePath : path.join(baseDir, filePath);
849
1099
  }
1100
+ function resolveTypesConfig(baseConfig, entryConfig) {
1101
+ if (!baseConfig && !entryConfig) return void 0;
1102
+ return {
1103
+ ...baseConfig,
1104
+ ...entryConfig
1105
+ };
1106
+ }
850
1107
  async function generateSingle(options) {
851
- const { inputFile, outputFile, strictDates, strictNumeric } = options;
1108
+ const { inputFile, outputFile, strictDates, strictNumeric, typesConfig } = options;
852
1109
  const resolvedInput = path.resolve(inputFile);
853
1110
  const resolvedOutput = path.resolve(outputFile);
854
1111
  const spec = readSpec(resolvedInput);
855
- const output = generate(spec, { strictDates, strictNumeric });
1112
+ const output = generate(spec, {
1113
+ strictDates,
1114
+ strictNumeric,
1115
+ types: typesConfig
1116
+ });
856
1117
  fs.mkdirSync(path.dirname(resolvedOutput), { recursive: true });
857
1118
  fs.writeFileSync(resolvedOutput, output);
858
1119
  console.log(`\u2705 Generated TypeScript types in ${resolvedOutput}`);