@kubb/plugin-ts 5.0.0-beta.98 → 5.0.0

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
@@ -845,10 +845,6 @@ type CreateOperationParamsOptions = {
845
845
  * - `'inlineSpread'` emits a single rest parameter `...pathParams: PathParams`
846
846
  */
847
847
  pathParamsType: 'object' | 'inline' | 'inlineSpread';
848
- /**
849
- * Converts parameter names to camelCase before output.
850
- */
851
- paramsCasing?: 'camelcase';
852
848
  /**
853
849
  * Resolver for parameter and request body type names.
854
850
  * Pass `ResolverTs` from `@kubb/plugin-ts` directly.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as __name, t as __exportAll } from "./rolldown-runtime-CNktS9qV.js";
2
2
  import { parserTs } from "@kubb/parser-ts";
3
3
  import { File, jsxRenderer } from "kubb/jsx";
4
- import { Resolver, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
4
+ import { Resolver, ast, createResolver, defineGenerator, definePlugin, isStringType, syncSchemaRef } from "kubb/kit";
5
5
  import ts from "typescript";
6
6
  import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
7
7
  //#region ../../internals/utils/src/casing.ts
@@ -817,7 +817,7 @@ function buildTupleNode(node, print) {
817
817
  */
818
818
  function buildPropertyType(schema, baseType, optionalType, optional) {
819
819
  const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(optionalType);
820
- const meta = ast.syncSchemaRef(schema);
820
+ const meta = syncSchemaRef(schema);
821
821
  let type = baseType;
822
822
  if (meta.nullable) type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.null] });
823
823
  if ((optional || meta.nullish || meta.optional) && addsUndefined) type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.undefined] });
@@ -912,39 +912,19 @@ function Enum({ node, enum: enumOptions, resolver }) {
912
912
  }
913
913
  //#endregion
914
914
  //#region ../../internals/shared/src/params.ts
915
- const caseParamsCache = /* @__PURE__ */ new WeakMap();
916
915
  /**
917
- * Applies camelCase to parameter names and returns a new array without mutating the input.
916
+ * Drops parameters that share the same name, keeping the first.
918
917
  *
919
- * Run it before handing parameters to schema builders so output property keys get the right casing
920
- * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
921
- * original array is returned unchanged. Results are cached per input array.
918
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
919
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
920
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
921
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
922
922
  */
923
- function caseParams(params, casing) {
924
- if (!casing) return params;
925
- const cached = caseParamsCache.get(params);
926
- if (cached) return cached;
927
- const result = params.map((param) => ({
928
- ...param,
929
- name: camelCase(param.name)
930
- }));
931
- caseParamsCache.set(params, result);
932
- return result;
933
- }
934
- /**
935
- * Drops parameters that collapse to the same property identity once camelCased, keeping the first.
936
- *
937
- * Some specs declare the same parameter twice under different casings (for example AWS S3 lists both
938
- * `max-uploads` and `MaxUploads`). Both resolve to one output property, so emitting both would yield
939
- * an object type with a duplicate member, which TypeScript rejects. De-duplicate by the camelCased
940
- * identity so the resulting group is collision-free regardless of the names each caller carries.
941
- */
942
- function dedupeByCasedName(params) {
923
+ function dedupeParams(params) {
943
924
  const seen = /* @__PURE__ */ new Set();
944
925
  return params.filter((param) => {
945
- const key = camelCase(param.name);
946
- if (seen.has(key)) return false;
947
- seen.add(key);
926
+ if (seen.has(param.name)) return false;
927
+ seen.add(param.name);
948
928
  return true;
949
929
  });
950
930
  }
@@ -998,14 +978,81 @@ function resolveContentTypeVariants(entries, baseName) {
998
978
  };
999
979
  });
1000
980
  }
1001
- function getOperationParameters(node, options = {}) {
1002
- const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
1003
- return {
1004
- path: dedupeByCasedName(params.filter((param) => param.in === "path")),
1005
- query: dedupeByCasedName(params.filter((param) => param.in === "query")),
1006
- header: dedupeByCasedName(params.filter((param) => param.in === "header")),
1007
- cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
981
+ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
982
+ /**
983
+ * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
984
+ * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
985
+ * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
986
+ * same parameters once per plugin.
987
+ */
988
+ function getOperationParameters(node) {
989
+ const cached = operationParameterGroupsByNode.get(node);
990
+ if (cached) return cached;
991
+ const groups = {
992
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
993
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
994
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
995
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
1008
996
  };
997
+ operationParameterGroupsByNode.set(node, groups);
998
+ return groups;
999
+ }
1000
+ /**
1001
+ * Builds the combined `{ body, path, query, headers }` options object schema for an operation,
1002
+ * referencing the already-resolved body and grouped param names. Shared by `@kubb/plugin-ts`'s
1003
+ * `Options` type and `@kubb/plugin-zod`'s inferred options schema, so both printers emit the same
1004
+ * shape from the same inputs. `primitive: 'object'` is a no-op for the TS printer and tells the Zod
1005
+ * printer to emit `z.object(…)` rather than a record.
1006
+ */
1007
+ function buildOptionsSchema(node, resolver) {
1008
+ const { path, query, header } = getOperationParameters(node);
1009
+ const hasBody = Boolean(node.requestBody?.content?.[0]?.schema);
1010
+ const createNever = () => ast.factory.createSchema({
1011
+ type: "never",
1012
+ primitive: void 0,
1013
+ optional: true
1014
+ });
1015
+ const groups = [
1016
+ {
1017
+ name: "path",
1018
+ params: path,
1019
+ resolve: resolver.param.path
1020
+ },
1021
+ {
1022
+ name: "query",
1023
+ params: query,
1024
+ resolve: resolver.param.query
1025
+ },
1026
+ {
1027
+ name: "headers",
1028
+ params: header,
1029
+ resolve: resolver.param.headers
1030
+ }
1031
+ ];
1032
+ return ast.factory.createSchema({
1033
+ type: "object",
1034
+ primitive: "object",
1035
+ deprecated: node.deprecated,
1036
+ properties: [ast.factory.createProperty({
1037
+ name: "body",
1038
+ required: hasBody,
1039
+ schema: hasBody ? ast.factory.createSchema({
1040
+ type: "ref",
1041
+ name: resolver.response.body(node)
1042
+ }) : createNever()
1043
+ }), ...groups.map(({ name, params, resolve }) => {
1044
+ const required = params.some((param) => param.required);
1045
+ return ast.factory.createProperty({
1046
+ name,
1047
+ required,
1048
+ schema: params.length > 0 ? ast.factory.createSchema({
1049
+ type: "ref",
1050
+ name: resolve.call(resolver.param, node, params[0]),
1051
+ optional: !required
1052
+ }) : createNever()
1053
+ });
1054
+ })]
1055
+ });
1009
1056
  }
1010
1057
  //#endregion
1011
1058
  //#region ../../internals/shared/src/resolver.ts
@@ -1090,7 +1137,7 @@ function createCasedFile(caseLast) {
1090
1137
  * `resolver.imports` would resolve file paths that are then discarded.
1091
1138
  */
1092
1139
  function collectRefNames(schema) {
1093
- return ast.collect(schema, { schema: (node) => {
1140
+ return ast.collectSync(schema, { schema: (node) => {
1094
1141
  const refNode = ast.narrowSchema(node, "ref");
1095
1142
  if (!refNode?.ref) return null;
1096
1143
  return ast.resolveRefName(refNode);
@@ -1128,6 +1175,35 @@ function createGroupConfig(group) {
1128
1175
  };
1129
1176
  }
1130
1177
  //#endregion
1178
+ //#region ../../internals/shared/src/schemaTraversal.ts
1179
+ /**
1180
+ * Maps each property of an object schema to its transformed output. Pairs every result with the
1181
+ * original property so the printer keeps full control over modifiers, getters, and key syntax.
1182
+ *
1183
+ * @example
1184
+ * ```ts
1185
+ * const entries = mapSchemaProperties(node, (schema) => this.transform(schema))
1186
+ * // entries: [{ name: 'id', property, output: 'z.number()' }, ...]
1187
+ * ```
1188
+ */
1189
+ function mapSchemaProperties(node, transform) {
1190
+ return node.properties.map((property) => ({
1191
+ name: property.name,
1192
+ property,
1193
+ output: transform(property.schema)
1194
+ }));
1195
+ }
1196
+ /**
1197
+ * Maps each item of an array or tuple schema to its transformed output, pairing every result with
1198
+ * the original item.
1199
+ */
1200
+ function mapSchemaItems(node, transform) {
1201
+ return (node.items ?? []).map((schema) => ({
1202
+ schema,
1203
+ output: transform(schema)
1204
+ }));
1205
+ }
1206
+ //#endregion
1131
1207
  //#region src/utils.ts
1132
1208
  /**
1133
1209
  * Tells whether a `const` (single-value enum) should render as a bare literal type (`'active'`)
@@ -1156,12 +1232,12 @@ function formatExample(value) {
1156
1232
  return (JSON.stringify(value) ?? String(value)).replaceAll("*/", "*\\/");
1157
1233
  }
1158
1234
  function buildPropertyJSDocComments(schema, optional) {
1159
- const meta = ast.syncSchemaRef(schema);
1235
+ const meta = syncSchemaRef(schema);
1160
1236
  const isArray = meta?.primitive === "array";
1161
1237
  const hasDescription = meta && "description" in meta && meta.description;
1162
1238
  const formatComment = meta && "format" in meta && meta.format ? hasDescription ? [" ", `Format: \`${meta.format}\``] : ["@description", `Format: \`${meta.format}\``] : [];
1163
1239
  const exampleValues = meta?.examples ?? [];
1164
- return [
1240
+ const comments = [
1165
1241
  hasDescription ? `@description ${jsStringEscape(meta.description)}` : null,
1166
1242
  ...formatComment,
1167
1243
  meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : null,
@@ -1169,9 +1245,10 @@ function buildPropertyJSDocComments(schema, optional) {
1169
1245
  !isArray && meta && "max" in meta && meta.max !== void 0 ? `@maxLength ${meta.max}` : null,
1170
1246
  meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
1171
1247
  meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" && typeof meta.default === "string" ? stringify(meta.default) : meta.default}` : null,
1172
- ...exampleValues.map((example) => `@example ${formatExample(example)}`),
1173
- meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, optional ?? isSchemaOptional(schema) ? " | undefined" : null].filter(Boolean).join("") : null
1248
+ ...exampleValues.map((example) => `@example ${formatExample(example)}`)
1174
1249
  ].filter(Boolean);
1250
+ const typeTag = comments.length && meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, optional ?? isSchemaOptional(schema) ? " | undefined" : null].filter(Boolean).join("") : null;
1251
+ return [...comments, typeTag].filter(Boolean);
1175
1252
  }
1176
1253
  /**
1177
1254
  * Builds the object schema for a group of parameters sharing one `in` location (path, query, or
@@ -1191,70 +1268,6 @@ function buildParams({ params }) {
1191
1268
  }))
1192
1269
  });
1193
1270
  }
1194
- function buildOptions(node, { resolver }) {
1195
- const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing: "original" });
1196
- const hasBody = Boolean(node.requestBody?.content?.[0]?.schema);
1197
- const hasRequiredPath = pathParams.some((param) => param.required);
1198
- const hasRequiredQuery = queryParams.some((param) => param.required);
1199
- const hasRequiredHeader = headerParams.some((param) => param.required);
1200
- return ast.factory.createSchema({
1201
- type: "object",
1202
- deprecated: node.deprecated,
1203
- properties: [
1204
- ast.factory.createProperty({
1205
- name: "body",
1206
- required: hasBody,
1207
- schema: hasBody ? ast.factory.createSchema({
1208
- type: "ref",
1209
- name: resolver.response.body(node)
1210
- }) : ast.factory.createSchema({
1211
- type: "never",
1212
- primitive: void 0,
1213
- optional: true
1214
- })
1215
- }),
1216
- ast.factory.createProperty({
1217
- name: "path",
1218
- required: hasRequiredPath,
1219
- schema: pathParams.length > 0 ? ast.factory.createSchema({
1220
- type: "ref",
1221
- name: resolver.param.path(node, pathParams[0]),
1222
- optional: !hasRequiredPath
1223
- }) : ast.factory.createSchema({
1224
- type: "never",
1225
- primitive: void 0,
1226
- optional: true
1227
- })
1228
- }),
1229
- ast.factory.createProperty({
1230
- name: "query",
1231
- required: hasRequiredQuery,
1232
- schema: queryParams.length > 0 ? ast.factory.createSchema({
1233
- type: "ref",
1234
- name: resolver.param.query(node, queryParams[0]),
1235
- optional: !hasRequiredQuery
1236
- }) : ast.factory.createSchema({
1237
- type: "never",
1238
- primitive: void 0,
1239
- optional: true
1240
- })
1241
- }),
1242
- ast.factory.createProperty({
1243
- name: "headers",
1244
- required: hasRequiredHeader,
1245
- schema: headerParams.length > 0 ? ast.factory.createSchema({
1246
- type: "ref",
1247
- name: resolver.param.headers(node, headerParams[0]),
1248
- optional: !hasRequiredHeader
1249
- }) : ast.factory.createSchema({
1250
- type: "never",
1251
- primitive: void 0,
1252
- optional: true
1253
- })
1254
- })
1255
- ]
1256
- });
1257
- }
1258
1271
  /**
1259
1272
  * The schema a status occupies in the `<Name>Responses` record. A status that documents several
1260
1273
  * content types becomes a `{ contentType; data }` union so the runtime can surface the negotiated type
@@ -1314,7 +1327,7 @@ function buildResponseUnion(node, { resolver }) {
1314
1327
  //#endregion
1315
1328
  //#region src/components/Type.tsx
1316
1329
  function Type({ name, node, printer, enum: enumOptions, resolver }) {
1317
- const enumSchemaNodes = ast.collect(node, { schema(n) {
1330
+ const enumSchemaNodes = ast.collectSync(node, { schema(n) {
1318
1331
  const enumNode = ast.narrowSchema(n, ast.schemaTypes.enum);
1319
1332
  if (enumNode?.name && !isInlineConstEnum(enumNode, printer.options.enumSchemaNames)) return enumNode;
1320
1333
  } });
@@ -1419,11 +1432,11 @@ const printerTs = ast.createPrinter((options) => {
1419
1432
  const hasStringLiteral = members.some((m) => {
1420
1433
  return ast.narrowSchema(m, ast.schemaTypes.enum)?.primitive === "string";
1421
1434
  });
1422
- const hasPlainString = members.some((m) => ast.isStringType(m));
1435
+ const hasPlainString = members.some((m) => isStringType(m));
1423
1436
  if (hasStringLiteral && hasPlainString) return createUnionDeclaration({
1424
1437
  withParentheses: true,
1425
1438
  nodes: members.map((m) => {
1426
- if (ast.isStringType(m)) return createIntersectionDeclaration({
1439
+ if (isStringType(m)) return createIntersectionDeclaration({
1427
1440
  nodes: [keywordTypeNodes.string, createTypeLiteralNode([])],
1428
1441
  withParentheses: true
1429
1442
  });
@@ -1443,7 +1456,7 @@ const printerTs = ast.createPrinter((options) => {
1443
1456
  },
1444
1457
  array(node) {
1445
1458
  return createArrayDeclaration({
1446
- nodes: ast.mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(isNonNullable),
1459
+ nodes: mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(isNonNullable),
1447
1460
  arrayType: this.options.arrayType
1448
1461
  }) ?? null;
1449
1462
  },
@@ -1453,11 +1466,11 @@ const printerTs = ast.createPrinter((options) => {
1453
1466
  object(node) {
1454
1467
  const { transform, options } = this;
1455
1468
  const addsQuestionToken = OPTIONAL_ADDS_QUESTION_TOKEN.has(options.optionalType);
1456
- const propertyNodes = ast.mapSchemaProperties(node, (schema) => transform(schema)).map(({ name, property, output }) => {
1469
+ const propertyNodes = mapSchemaProperties(node, (schema) => transform(schema)).map(({ name, property, output }) => {
1457
1470
  const baseType = output ?? keywordTypeNodes.unknown;
1458
1471
  const optional = !property.required || !!property.schema.optional || !!property.schema.nullish;
1459
1472
  const type = buildPropertyType(property.schema, baseType, options.optionalType, optional);
1460
- const propMeta = ast.syncSchemaRef(property.schema);
1473
+ const propMeta = syncSchemaRef(property.schema);
1461
1474
  return appendJSDocToNode({
1462
1475
  node: createPropertySignature({
1463
1476
  questionToken: optional ? addsQuestionToken : false,
@@ -1478,7 +1491,7 @@ const printerTs = ast.createPrinter((options) => {
1478
1491
  const { name, syntaxType = "type", description, keysToOmit } = this.options;
1479
1492
  const transformed = this.transform(node);
1480
1493
  if (!transformed) return null;
1481
- const meta = ast.syncSchemaRef(node);
1494
+ const meta = syncSchemaRef(node);
1482
1495
  if (!name) {
1483
1496
  const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [transformed, keywordTypeNodes.null] }) : transformed;
1484
1497
  const result = (meta.nullish || meta.optional) && addsUndefined ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
@@ -1609,7 +1622,6 @@ const typeGenerator = defineGenerator({
1609
1622
  operation(node, ctx) {
1610
1623
  const { enum: enumOptions, optionalType, arrayType, syntaxType, group, output, printer } = ctx.options;
1611
1624
  const { config, resolver, root } = ctx;
1612
- const params = caseParams(node.parameters, "camelcase");
1613
1625
  const meta = { file: resolver.file({
1614
1626
  name: node.operationId,
1615
1627
  extname: ".ts",
@@ -1688,10 +1700,7 @@ const typeGenerator = defineGenerator({
1688
1700
  name: baseName
1689
1701
  })] });
1690
1702
  }
1691
- const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters({
1692
- ...node,
1693
- parameters: params
1694
- }, { paramsCasing: "original" });
1703
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
1695
1704
  const paramGroupTypes = [
1696
1705
  pathParams.length > 0 && renderSchemaType({
1697
1706
  schema: buildParams({ params: pathParams }),
@@ -1738,10 +1747,7 @@ const typeGenerator = defineGenerator({
1738
1747
  });
1739
1748
  });
1740
1749
  const optionsType = renderSchemaType({
1741
- schema: buildOptions({
1742
- ...node,
1743
- parameters: params
1744
- }, { resolver }),
1750
+ schema: buildOptionsSchema(node, resolver),
1745
1751
  name: resolver.response.options(node)
1746
1752
  });
1747
1753
  const responsesType = renderSchemaType({
@@ -2191,17 +2197,14 @@ function resolveGroupType({ node, params, group, resolver }) {
2191
2197
  * name resolution and `extraParams` for plugin-specific trailing parameters such as an `options` object.
2192
2198
  */
2193
2199
  function createOperationParams(node, options) {
2194
- const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
2200
+ const { paramsType, pathParamsType, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
2195
2201
  const dataName = paramNames?.data ?? "data";
2196
2202
  const paramsName = paramNames?.params ?? "params";
2197
2203
  const headersName = paramNames?.headers ?? "headers";
2198
2204
  const pathName = paramNames?.path ?? "pathParams";
2199
2205
  const wrapType = (type) => typeWrapper ? typeWrapper(type) : type;
2200
2206
  const wrapTypeExpression = (type) => typeof type === "string" ? wrapType(type) : type;
2201
- const casedParams = caseParams(node.parameters, paramsCasing);
2202
- const pathParams = casedParams.filter((p) => p.in === "path");
2203
- const queryParams = casedParams.filter((p) => p.in === "query");
2204
- const headerParams = casedParams.filter((p) => p.in === "header");
2207
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
2205
2208
  const toProperty = (param) => ({
2206
2209
  name: param.name,
2207
2210
  type: wrapTypeExpression(resolveParamType({