@kubb/plugin-zod 5.0.0-beta.74 → 5.0.0-beta.76

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.d.ts CHANGED
@@ -282,6 +282,14 @@ type ResolverZod = Resolver & OperationParamsResolver & {
282
282
  * `resolver.resolveResponseName(node) // → 'listPetsResponseSchema'`
283
283
  */
284
284
  resolveResponseName(this: ResolverZod, node: ast.OperationNode): string;
285
+ /**
286
+ * Resolves the name for the union of an operation's error (non-2xx) responses. Generated clients
287
+ * validate the error body against this on the non-throw path.
288
+ *
289
+ * @example Error union names
290
+ * `resolver.resolveErrorName(node) // → 'listPetsErrorSchema'`
291
+ */
292
+ resolveErrorName(this: ResolverZod, node: ast.OperationNode): string;
285
293
  /**
286
294
  * Resolves the name for an operation's grouped path parameters type.
287
295
  *
@@ -355,11 +363,6 @@ type Options = OutputOptions & {
355
363
  strings?: boolean;
356
364
  numbers?: boolean;
357
365
  };
358
- /**
359
- * Emit an `operations.ts` file with request body, query/path params, and per-status
360
- * response schemas grouped by operation.
361
- */
362
- operations?: boolean;
363
366
  /**
364
367
  * Validator for `format: uuid` properties.
365
368
  * - `'uuid'` — `z.uuid()`. Standard RFC 4122.
@@ -420,7 +423,6 @@ type ResolvedOptions = {
420
423
  inferred: NonNullable<Options['inferred']>;
421
424
  importPath: NonNullable<Options['importPath']>;
422
425
  coercion: NonNullable<Options['coercion']>;
423
- operations: NonNullable<Options['operations']>;
424
426
  guidType: NonNullable<Options['guidType']>;
425
427
  regexType: NonNullable<Options['regexType']>;
426
428
  mini: NonNullable<Options['mini']>;
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { t as __name } from "./rolldown-runtime-C0LytTxp.js";
2
2
  import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
3
3
  import { Const, File, Type, jsxRenderer } from "@kubb/renderer-jsx";
4
- import { buildList, buildObject, containsCircularRef, extractRefName, lazyGetter, mapSchemaItems, mapSchemaMembers, mapSchemaProperties, objectKey, stringify, stringifyObject, syncSchemaRef, toRegExpString } from "@kubb/ast/utils";
5
4
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
5
+ import { buildList, buildObject, containsCircularRef, extractRefName, lazyGetter, mapSchemaItems, mapSchemaMembers, mapSchemaProperties, objectKey, stringify, syncSchemaRef, toRegExpString } from "@kubb/ast/utils";
6
6
  //#region ../../internals/utils/src/casing.ts
7
7
  /**
8
8
  * Shared implementation for camelCase and PascalCase conversion.
@@ -303,80 +303,6 @@ function createGroupConfig(group) {
303
303
  };
304
304
  }
305
305
  //#endregion
306
- //#region src/components/Operations.tsx
307
- function Operations({ name, operations }) {
308
- const operationsJSON = operations.reduce((prev, acc) => {
309
- prev[`"${acc.node.operationId}"`] = acc.data;
310
- return prev;
311
- }, {});
312
- const pathsJSON = operations.reduce((prev, acc) => {
313
- if (!ast.isHttpOperationNode(acc.node)) return prev;
314
- prev[`"${acc.node.path}"`] = {
315
- ...prev[`"${acc.node.path}"`] ?? {},
316
- [acc.node.method]: `operations["${acc.node.operationId}"]`
317
- };
318
- return prev;
319
- }, {});
320
- return /* @__PURE__ */ jsxs(Fragment, { children: [
321
- /* @__PURE__ */ jsx(File.Source, {
322
- name: "OperationSchema",
323
- isExportable: true,
324
- isIndexable: true,
325
- children: /* @__PURE__ */ jsx(Type, {
326
- name: "OperationSchema",
327
- export: true,
328
- children: `{
329
- readonly request: z.ZodTypeAny | undefined;
330
- readonly parameters: {
331
- readonly path: z.ZodTypeAny | undefined;
332
- readonly query: z.ZodTypeAny | undefined;
333
- readonly header: z.ZodTypeAny | undefined;
334
- };
335
- readonly responses: {
336
- readonly [status: number]: z.ZodTypeAny;
337
- readonly default: z.ZodTypeAny;
338
- };
339
- readonly errors: {
340
- readonly [status: number]: z.ZodTypeAny;
341
- };
342
- }`
343
- })
344
- }),
345
- /* @__PURE__ */ jsx(File.Source, {
346
- name: "OperationsMap",
347
- isExportable: true,
348
- isIndexable: true,
349
- children: /* @__PURE__ */ jsx(Type, {
350
- name: "OperationsMap",
351
- export: true,
352
- children: "Record<string, OperationSchema>"
353
- })
354
- }),
355
- /* @__PURE__ */ jsx(File.Source, {
356
- name,
357
- isExportable: true,
358
- isIndexable: true,
359
- children: /* @__PURE__ */ jsx(Const, {
360
- export: true,
361
- name,
362
- asConst: true,
363
- children: `{${stringifyObject(operationsJSON)}}`
364
- })
365
- }),
366
- /* @__PURE__ */ jsx(File.Source, {
367
- name: "paths",
368
- isExportable: true,
369
- isIndexable: true,
370
- children: /* @__PURE__ */ jsx(Const, {
371
- export: true,
372
- name: "paths",
373
- asConst: true,
374
- children: `{${stringifyObject(pathsJSON)}}`
375
- })
376
- })
377
- ] });
378
- }
379
- //#endregion
380
306
  //#region src/components/Zod.tsx
381
307
  function Zod({ name, node, printer, inferTypeName }) {
382
308
  const output = printer.print(node);
@@ -482,36 +408,6 @@ function collectCodecRefNames(node) {
482
408
  return ast.collect(node, { schema: (n) => n.type === "ref" && n.ref && containsCodec(n) ? extractRefName(n.ref) ?? void 0 : void 0 });
483
409
  }
484
410
  /**
485
- * Collects all resolved schema names for an operation's parameters and responses
486
- * into a single lookup object, useful for building imports and type references.
487
- */
488
- function buildSchemaNames(node, { params, resolver }) {
489
- const pathParam = params.find((p) => p.in === "path");
490
- const queryParam = params.find((p) => p.in === "query");
491
- const headerParam = params.find((p) => p.in === "header");
492
- const responses = {};
493
- const errors = {};
494
- for (const res of node.responses) {
495
- const name = resolver.resolveResponseStatusName(node, res.statusCode);
496
- const statusNum = Number(res.statusCode);
497
- if (!Number.isNaN(statusNum)) {
498
- responses[statusNum] = name;
499
- if (statusNum >= 400) errors[statusNum] = name;
500
- }
501
- }
502
- responses["default"] = resolver.resolveResponseName(node);
503
- return {
504
- request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null,
505
- parameters: {
506
- path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : null,
507
- query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : null,
508
- header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : null
509
- },
510
- responses,
511
- errors
512
- };
513
- }
514
- /**
515
411
  * Format a default value as a code-level literal.
516
412
  * Objects become `{}`, primitives become their string representation, strings are quoted.
517
413
  */
@@ -536,6 +432,28 @@ function regexFunc(regexType) {
536
432
  return regexType === "constructor" ? "RegExp" : null;
537
433
  }
538
434
  /**
435
+ * Builds a Zod record key schema that enforces the `patternProperties` regexes a plain
436
+ * `.catchall()` would drop. Several patterns combine into one alternation (`(^a)|(^b)`).
437
+ */
438
+ function patternKeySchema({ patterns, regexType }) {
439
+ return `z.string().regex(${patternKeySource({
440
+ patterns,
441
+ regexType
442
+ })})`;
443
+ }
444
+ /**
445
+ * `zod/mini` variant of {@link patternKeySchema}, wrapping the regex in `.check(z.regex(...))`.
446
+ */
447
+ function patternKeySchemaMini({ patterns, regexType }) {
448
+ return `z.string().check(z.regex(${patternKeySource({
449
+ patterns,
450
+ regexType
451
+ })}))`;
452
+ }
453
+ function patternKeySource({ patterns, regexType }) {
454
+ return toRegExpString(patterns.length === 1 ? patterns[0] : patterns.map((pattern) => `(${pattern})`).join("|"), regexFunc(regexType));
455
+ }
456
+ /**
539
457
  * Build `.min()` / `.max()` / `.gt()` / `.lt()` constraint chains for numbers
540
458
  * using the standard chainable Zod v4 API.
541
459
  */
@@ -725,7 +643,7 @@ const printerZod = ast.createPrinter((options) => {
725
643
  },
726
644
  object(node) {
727
645
  const isCyclic = (schema) => this.options.cyclicSchemas != null && containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas });
728
- const objectBase = `z.object(${buildObject(mapSchemaProperties(node, (schema) => {
646
+ const entries = mapSchemaProperties(node, (schema) => {
729
647
  const hasSelfRef = isCyclic(schema);
730
648
  const savedCyclicSchemas = this.options.cyclicSchemas;
731
649
  if (hasSelfRef) this.options.cyclicSchemas = void 0;
@@ -753,14 +671,29 @@ const printerZod = ast.createPrinter((options) => {
753
671
  name: propName,
754
672
  body: value
755
673
  }) : `${objectKey(propName)}: ${value}`;
756
- }))})`;
674
+ });
675
+ const objectBase = `z.object(${buildObject(entries)})`;
757
676
  return (() => {
677
+ const patterns = node.patternProperties ? Object.entries(node.patternProperties) : [];
758
678
  if (node.additionalProperties && node.additionalProperties !== true) {
759
679
  const catchallType = this.transform(node.additionalProperties);
760
680
  return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase;
761
681
  }
762
682
  if (node.additionalProperties === true) return `${objectBase}.catchall(${this.transform(ast.factory.createSchema({ type: "unknown" }))})`;
763
- if (node.additionalProperties === false) return `${objectBase}.strict()`;
683
+ if (node.additionalProperties === false && patterns.length === 0) return `${objectBase}.strict()`;
684
+ if (patterns.length > 0) {
685
+ const values = patterns.map(([, valueSchema]) => {
686
+ const valueType = this.transform(valueSchema) ?? this.transform(ast.factory.createSchema({ type: "unknown" }));
687
+ return valueSchema.nullable ? `${valueType}.nullable()` : valueType;
688
+ });
689
+ const distinct = [...new Set(values)];
690
+ const value = distinct.length === 1 ? distinct[0] : `z.union([${distinct.join(", ")}])`;
691
+ if (entries.length > 0) return `${objectBase}.catchall(${value})`;
692
+ return `z.record(${patternKeySchema({
693
+ patterns: patterns.map(([pattern]) => pattern),
694
+ regexType: this.options.regexType
695
+ })}, ${value})`;
696
+ }
764
697
  return objectBase;
765
698
  })();
766
699
  },
@@ -928,7 +861,7 @@ const printerZodMini = ast.createPrinter((options) => {
928
861
  },
929
862
  object(node) {
930
863
  const isCyclic = (schema) => this.options.cyclicSchemas != null && containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas });
931
- return `z.object(${buildObject(mapSchemaProperties(node, (schema) => {
864
+ const entries = mapSchemaProperties(node, (schema) => {
932
865
  const hasSelfRef = isCyclic(schema);
933
866
  const savedCyclicSchemas = this.options.cyclicSchemas;
934
867
  if (hasSelfRef) this.options.cyclicSchemas = void 0;
@@ -952,7 +885,29 @@ const printerZodMini = ast.createPrinter((options) => {
952
885
  name: propName,
953
886
  body: value
954
887
  }) : `${objectKey(propName)}: ${value}`;
955
- }))})`;
888
+ });
889
+ const objectBase = `z.object(${buildObject(entries)})`;
890
+ const patterns = node.patternProperties ? Object.entries(node.patternProperties) : [];
891
+ if (node.additionalProperties && node.additionalProperties !== true) {
892
+ const catchallType = this.transform(node.additionalProperties);
893
+ return catchallType ? `z.catchall(${objectBase}, ${catchallType})` : objectBase;
894
+ }
895
+ if (node.additionalProperties === true) return `z.catchall(${objectBase}, ${this.transform(ast.factory.createSchema({ type: "unknown" }))})`;
896
+ if (node.additionalProperties === false && patterns.length === 0) return objectBase.replace(/^z\.object\(/, "z.strictObject(");
897
+ if (patterns.length > 0) {
898
+ const values = patterns.map(([, valueSchema]) => {
899
+ const valueType = this.transform(valueSchema) ?? this.transform(ast.factory.createSchema({ type: "unknown" }));
900
+ return valueSchema.nullable ? `z.nullable(${valueType})` : valueType;
901
+ });
902
+ const distinct = [...new Set(values)];
903
+ const value = distinct.length === 1 ? distinct[0] : `z.union([${distinct.join(", ")}])`;
904
+ if (entries.length > 0) return `z.catchall(${objectBase}, ${value})`;
905
+ return `z.record(${patternKeySchemaMini({
906
+ patterns: patterns.map(([pattern]) => pattern),
907
+ regexType: this.options.regexType
908
+ })}, ${value})`;
909
+ }
910
+ return objectBase;
956
911
  },
957
912
  array(node) {
958
913
  const base = `z.array(${mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: "unknown" }))})${lengthChecksMini({
@@ -1331,6 +1286,25 @@ const zodGenerator = defineGenerator({
1331
1286
  name: responseUnionName
1332
1287
  });
1333
1288
  })() : null;
1289
+ const errorResponsesWithSchema = responsesWithSchema.filter((res) => !isSuccessStatusCode(res.statusCode));
1290
+ const errorUnionSchema = errorResponsesWithSchema.length > 0 ? (() => {
1291
+ const errorUnionName = resolver.resolveErrorName(node);
1292
+ if (new Set(errorResponsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? adapter.getImports(entry.schema, (schemaName) => ({
1293
+ name: resolver.resolveSchemaName(schemaName),
1294
+ path: ""
1295
+ })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : []))).has(errorUnionName)) return null;
1296
+ const members = errorResponsesWithSchema.map((res) => ast.factory.createSchema({
1297
+ type: "ref",
1298
+ name: resolver.resolveResponseStatusName(node, res.statusCode)
1299
+ }));
1300
+ return renderSchemaEntry({
1301
+ schema: members.length === 1 ? members[0] : ast.factory.createSchema({
1302
+ type: "union",
1303
+ members
1304
+ }),
1305
+ name: errorUnionName
1306
+ });
1307
+ })() : null;
1334
1308
  const requestBodyContent = node.requestBody?.content ?? [];
1335
1309
  const requestSchema = (() => {
1336
1310
  if (requestBodyContent.length === 0) return null;
@@ -1381,88 +1355,10 @@ const zodGenerator = defineGenerator({
1381
1355
  paramSchemas,
1382
1356
  responseSchemas,
1383
1357
  responseUnionSchema,
1358
+ errorUnionSchema,
1384
1359
  requestSchema
1385
1360
  ]
1386
1361
  });
1387
- },
1388
- operations(nodes, ctx) {
1389
- const { config, resolver, root } = ctx;
1390
- const { output, importPath, group, operations } = ctx.options;
1391
- if (!operations) return;
1392
- const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
1393
- const meta = { file: resolver.resolveFile({
1394
- name: "operations",
1395
- extname: ".ts"
1396
- }, {
1397
- root,
1398
- output,
1399
- group: group ?? void 0
1400
- }) };
1401
- const transformedOperations = nodes.filter(ast.isHttpOperationNode).map((node) => {
1402
- return {
1403
- node,
1404
- data: buildSchemaNames(node, {
1405
- params: caseParams(node.parameters, "camelcase"),
1406
- resolver
1407
- })
1408
- };
1409
- });
1410
- const imports = transformedOperations.flatMap(({ node, data }) => {
1411
- const names = [
1412
- data.request,
1413
- ...Object.values(data.responses),
1414
- ...Object.values(data.parameters)
1415
- ].filter(Boolean);
1416
- const opFile = resolver.resolveFile({
1417
- name: node.operationId,
1418
- extname: ".ts",
1419
- tag: node.tags[0] ?? "default",
1420
- path: node.path
1421
- }, {
1422
- root,
1423
- output,
1424
- group: group ?? void 0
1425
- });
1426
- return names.map((name) => /* @__PURE__ */ jsx(File.Import, {
1427
- name: [name],
1428
- root: meta.file.path,
1429
- path: opFile.path
1430
- }, [name, opFile.path].join("-")));
1431
- });
1432
- return /* @__PURE__ */ jsxs(File, {
1433
- baseName: meta.file.baseName,
1434
- path: meta.file.path,
1435
- meta: meta.file.meta,
1436
- banner: resolver.resolveBanner(ctx.meta, {
1437
- output,
1438
- config,
1439
- file: {
1440
- path: meta.file.path,
1441
- baseName: meta.file.baseName
1442
- }
1443
- }),
1444
- footer: resolver.resolveFooter(ctx.meta, {
1445
- output,
1446
- config,
1447
- file: {
1448
- path: meta.file.path,
1449
- baseName: meta.file.baseName
1450
- }
1451
- }),
1452
- children: [
1453
- /* @__PURE__ */ jsx(File.Import, {
1454
- isTypeOnly: true,
1455
- name: isZodImport ? "z" : ["z"],
1456
- path: importPath,
1457
- isNameSpace: isZodImport
1458
- }),
1459
- imports,
1460
- /* @__PURE__ */ jsx(Operations, {
1461
- name: "operations",
1462
- operations: transformedOperations
1463
- })
1464
- ]
1465
- });
1466
1362
  }
1467
1363
  });
1468
1364
  //#endregion
@@ -1523,6 +1419,9 @@ const resolverZod = defineResolver(() => {
1523
1419
  resolveResponseName(node) {
1524
1420
  return this.resolveSchemaName(`${node.operationId} Response`);
1525
1421
  },
1422
+ resolveErrorName(node) {
1423
+ return this.resolveSchemaName(`${node.operationId} Error`);
1424
+ },
1526
1425
  resolvePathParamsName(node, param) {
1527
1426
  return this.resolveParamName(node, param);
1528
1427
  },
@@ -1571,7 +1470,7 @@ const pluginZod = definePlugin((options) => {
1571
1470
  const { output = {
1572
1471
  path: "zod",
1573
1472
  barrel: { type: "named" }
1574
- }, group, exclude = [], include, override = [], typed = false, operations = false, mini = false, guidType = "uuid", regexType = "literal", importPath = mini ? "zod/mini" : "zod", coercion = false, inferred = false, wrapOutput = void 0, printer, resolver: userResolver, macros: userMacros } = options;
1473
+ }, group, exclude = [], include, override = [], typed = false, mini = false, guidType = "uuid", regexType = "literal", importPath = mini ? "zod/mini" : "zod", coercion = false, inferred = false, wrapOutput = void 0, printer, resolver: userResolver, macros: userMacros } = options;
1575
1474
  const groupConfig = createGroupConfig(group);
1576
1475
  return {
1577
1476
  name: pluginZodName,
@@ -1586,7 +1485,6 @@ const pluginZod = definePlugin((options) => {
1586
1485
  typed,
1587
1486
  importPath,
1588
1487
  coercion,
1589
- operations,
1590
1488
  inferred,
1591
1489
  guidType,
1592
1490
  regexType,