@kubb/plugin-ts 5.5.0-canary.20261111T115959 → 5.5.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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { Diagnostics, Resolver, addGenerator, ast, camelCase, createResolver, defineExtension, defineGenerator, isStringType, pascalCase, setMacros, setResolver, syncSchemaRef } from "@kubb/kit";
2
+ import { Diagnostics, Resolver, addGenerator, ast, casing, createPrinter, createResolver, defineExtension, defineGenerator, setMacros, setResolver, text } from "@kubb/kit";
3
3
  import { File, jsxRenderer } from "@kubb/renderer-jsx";
4
4
  import { generate } from "yuku-codegen";
5
5
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
@@ -18,40 +18,6 @@ var __exportAll = (all, no_symbols) => {
18
18
  if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
19
19
  return target;
20
20
  };
21
- // ../../internals/utils/src/casing.ts
22
- const wordStart = /(?<=[a-z\d])(?=[A-Z])|(?<=[A-Z]{2})(?=[A-Z][a-z]{2})/;
23
- /**
24
- * Splits `text` into words on separators (whitespace, hyphens, underscores, dots, slashes, colons)
25
- * and on {@link wordStart}. A segment without lowercase letters, such as `COOL` or `2D`, is one word,
26
- * so snake case leaves a name that is already in it unchanged.
27
- */
28
- function words(text) {
29
- return text.split(/[\s\-_./\\:]+/).filter(Boolean).flatMap((segment) => segment === segment.toUpperCase() ? segment : segment.split(wordStart));
30
- }
31
- /**
32
- * Converts `text` to snake_case.
33
- *
34
- * @example From camelCase
35
- * `snakeCase('helloWorld') // 'hello_world'`
36
- *
37
- * @example From mixed separators
38
- * `snakeCase('Hello-World') // 'hello_world'`
39
- */
40
- function snakeCase(text, { prefix = "", suffix = "" } = {}) {
41
- return words(`${prefix} ${text} ${suffix}`).map((word) => word.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()).filter(Boolean).join("_");
42
- }
43
- /**
44
- * Converts `text` to SCREAMING_SNAKE_CASE.
45
- *
46
- * @example From camelCase
47
- * `screamingSnakeCase('helloWorld') // 'HELLO_WORLD'`
48
- */
49
- function screamingSnakeCase(text, { prefix = "", suffix = "" } = {}) {
50
- return snakeCase(text, {
51
- prefix,
52
- suffix
53
- }).toUpperCase();
54
- }
55
21
  // ../../internals/utils/src/reserved.ts
56
22
  /**
57
23
  * JavaScript and Java reserved words.
@@ -191,53 +157,6 @@ function ensureValidVarName(name) {
191
157
  function isIdentifier(name) {
192
158
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
193
159
  }
194
- // ../../internals/utils/src/strings.ts
195
- /**
196
- * Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
197
- * Returns the string unchanged when no balanced quote pair is found.
198
- *
199
- * @example
200
- * ```ts
201
- * trimQuotes('"hello"') // 'hello'
202
- * trimQuotes('hello') // 'hello'
203
- * ```
204
- */
205
- function trimQuotes(text) {
206
- if (text.length >= 2) {
207
- const first = text[0];
208
- const last = text[text.length - 1];
209
- if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`") return text.slice(1, -1);
210
- }
211
- return text;
212
- }
213
- /**
214
- * Serializes a primitive to a single-quoted string literal, stripping any surrounding quotes first.
215
- *
216
- * Escaping runs through `JSON.stringify`, then the result switches to single quotes so the generated
217
- * code matches the repo style without a formatter.
218
- *
219
- * @example
220
- * ```ts
221
- * stringify('hello') // "'hello'"
222
- * stringify('"hello"') // "'hello'"
223
- * ```
224
- */
225
- function stringify(value) {
226
- if (value === void 0 || value === null) return "''";
227
- return `'${JSON.stringify(trimQuotes(value.toString())).slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
228
- }
229
- /**
230
- * Escapes an `@` at the start of a line, so a multi-line description can't start a new JSDoc tag.
231
- * Editors render `\@` as `@` in hovers.
232
- *
233
- * @example
234
- * ```ts
235
- * jsDocEscape('intro\n@example value') // 'intro\n\\@example value'
236
- * ```
237
- */
238
- function jsDocEscape(input) {
239
- return `${input}`.replace(/^(\s*)@/gm, "$1\\@");
240
- }
241
160
  // ../../internals/utils/src/fs.ts
242
161
  /**
243
162
  * Builds a nested file path from a dotted name. Splits on dots that precede a letter
@@ -258,9 +177,22 @@ function jsDocEscape(input) {
258
177
  * @example Suffix applied to the final segment only
259
178
  * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
260
179
  */
261
- function toFilePath(name, caseLast = camelCase) {
180
+ function toFilePath(name, caseLast = casing.camel) {
262
181
  const parts = name.split(/\.(?=[a-zA-Z])/);
263
- return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
182
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : casing.camel(part)).filter(Boolean).join("/");
183
+ }
184
+ // ../../internals/utils/src/strings.ts
185
+ /**
186
+ * Escapes an `@` at the start of a line, so a multi-line description can't start a new JSDoc tag.
187
+ * Editors render `\@` as `@` in hovers.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * jsDocEscape('intro\n@example value') // 'intro\n\\@example value'
192
+ * ```
193
+ */
194
+ function jsDocEscape(input) {
195
+ return `${input}`.replace(/^(\s*)@/gm, "$1\\@");
264
196
  }
265
197
  // src/factory.ts
266
198
  var factory_exports = /* @__PURE__ */ __exportAll({
@@ -346,12 +278,6 @@ function propertyName(name) {
346
278
  return isValidIdentifier(name) ? identifier(name) : literal(name);
347
279
  }
348
280
  /**
349
- * Indents every non-empty line of `text` by two spaces.
350
- */
351
- function indent(text) {
352
- return text.split("\n").map((line) => line ? ` ${line}` : line).join("\n");
353
- }
354
- /**
355
281
  * Creates a TypeScript intersection type.
356
282
  * Returns the single type if only one is provided. `yuku-codegen` adds the parentheses a union member needs.
357
283
  */
@@ -512,12 +438,12 @@ function valueToExpression(value, asNumber = isNumber(value)) {
512
438
  /**
513
439
  * Apply casing transformation to enum keys
514
440
  */
515
- function applyEnumKeyCasing(key, casing = "none") {
516
- if (casing === "none") return key;
517
- if (casing === "screamingSnakeCase") return screamingSnakeCase(key);
518
- if (casing === "snakeCase") return snakeCase(key);
519
- if (casing === "pascalCase") return pascalCase(key);
520
- if (casing === "camelCase") return camelCase(key);
441
+ function applyEnumKeyCasing(key, keyCasing = "none") {
442
+ if (keyCasing === "none") return key;
443
+ if (keyCasing === "screamingSnakeCase") return casing.screamingSnake(key);
444
+ if (keyCasing === "snakeCase") return casing.snake(key);
445
+ if (keyCasing === "pascalCase") return casing.pascal(key);
446
+ if (keyCasing === "camelCase") return casing.camel(key);
521
447
  return key;
522
448
  }
523
449
  /**
@@ -596,7 +522,7 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
596
522
  exprName: identifier(identifierName),
597
523
  typeArguments: null
598
524
  };
599
- return [`export const ${identifierName} = {\n${properties.map(indent).join(",\n")},\n} as const;`, typeAlias({
525
+ return [`export const ${identifierName} = {\n${properties.map((property) => text.indent({ text: property })).join(",\n")},\n} as const;`, typeAlias({
600
526
  type: "TSIndexedAccessType",
601
527
  objectType: {
602
528
  type: "TSParenthesizedType",
@@ -720,7 +646,7 @@ function wrapNullableOptional(type, nullable, addsUndefined) {
720
646
  */
721
647
  function buildPropertyType(schema, baseType, optionalType, optional) {
722
648
  const addsUndefined = optionalType === "undefined" || optionalType === "questionTokenAndUndefined";
723
- const meta = syncSchemaRef(schema);
649
+ const meta = ast.ref.mergeSchema(schema);
724
650
  return wrapNullableOptional(baseType, !!meta.nullable, addsUndefined && !!(optional || meta.nullish || meta.optional));
725
651
  }
726
652
  /**
@@ -774,7 +700,7 @@ function buildIndexSignatures(node, propertyCount, printNode) {
774
700
  function getEnumNames$1({ node, enum: enumOptions, resolver }) {
775
701
  const resolved = resolver.name(node.name);
776
702
  return {
777
- enumName: enumOptions.constCasing === "pascalCase" ? resolved : ensureValidVarName(camelCase(node.name)),
703
+ enumName: enumOptions.constCasing === "pascalCase" ? resolved : ensureValidVarName(casing.camel(node.name)),
778
704
  typeName: enumOptions.type === "asConst" ? resolver.enum.keyName(node, enumOptions.typeSuffix) : resolved
779
705
  };
780
706
  }
@@ -800,10 +726,10 @@ function Enum({ node, enum: enumOptions, resolver }) {
800
726
  name: enumName,
801
727
  typeName,
802
728
  enums: node.namedEnumValues?.map((v) => [
803
- trimQuotes(v.name.toString()),
729
+ text.unquote(v.name.toString()),
804
730
  v.value,
805
731
  v.description
806
- ]) ?? node.enumValues?.filter((v) => v !== null && v !== void 0).map((v) => [trimQuotes(v.toString()), v]) ?? [],
732
+ ]) ?? node.enumValues?.filter((v) => v !== null && v !== void 0).map((v) => [text.unquote(v.toString()), v]) ?? [],
807
733
  type: enumOptions.type,
808
734
  enumKeyCasing: enumOptions.keyCasing
809
735
  });
@@ -828,228 +754,582 @@ function Enum({ node, enum: enumOptions, resolver }) {
828
754
  children: typeNode
829
755
  })] });
830
756
  }
831
- // ../../internals/shared/src/operation.ts
757
+ // src/utils.ts
832
758
  /**
833
- * Builds the `ResolverFileParams` every operation generator passes to
834
- * `resolver.file`: a file named `name`, tagged by the operation's first
835
- * tag (or `'default'`), at the operation's path. Centralizes the entry object
836
- * that was repeated at dozens of call sites across the client and query plugins.
837
- *
838
- * @example
839
- * ```ts
840
- * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
841
- * ```
759
+ * Renames inline enum declarations and their uses together.
842
760
  */
843
- function operationFileEntry(node, name, extname = ".ts") {
844
- return {
845
- name,
846
- extname,
847
- tag: node.tags[0] ?? "default",
848
- path: node.path
849
- };
761
+ function resolveInlineEnumNames({ node, owner, resolver, enumOptions }) {
762
+ return ast.node.transform(node, { schema(child) {
763
+ if (child === node || child.type !== "enum" || !child.name) return;
764
+ const name = resolver.resolveName({
765
+ key: [
766
+ "enum",
767
+ owner,
768
+ child.pointer ?? child.name
769
+ ],
770
+ name: child.name,
771
+ normalize: (value) => {
772
+ const { enumName, typeName } = getEnumNames$1({
773
+ node: {
774
+ ...child,
775
+ name: value
776
+ },
777
+ enum: enumOptions,
778
+ resolver
779
+ });
780
+ return enumOptions.type === "literal" ? [typeName] : [enumName, typeName];
781
+ }
782
+ });
783
+ return name === child.name ? void 0 : {
784
+ ...child,
785
+ name
786
+ };
787
+ } });
788
+ }
789
+ function enumTypeName({ resolver, name, enumOptions, isEnum }) {
790
+ return isEnum && enumOptions?.type === "asConst" && enumOptions.typeSuffix ? resolver.enum.keyName({ name }, enumOptions.typeSuffix) : resolver.name(name);
850
791
  }
851
792
  /**
852
- * Resolves the file a schema-emitting plugin (`plugin-ts`, `plugin-zod`, `plugin-faker`) writes
853
- * `node` to, cached in `ctx.memo` under the resolver's plugin name and the
854
- * operation. The plugin itself and every plugin that imports from it call this, so they all get
855
- * the same path.
856
- *
857
- * When that path matches the file of a component schema in `ctx.driver.inputNode`, ignoring case, the file name
858
- * gets an `Operation` suffix: operation `point` goes to `PointOperation.ts` next to schema `Point.ts`.
859
- * Otherwise the two merge into one file, or overwrite each other on a case-insensitive file system.
860
- * Only the file name changes. The declarations keep their names.
793
+ * Tells whether a `const` (single-value enum) should render as a bare literal type (`'active'`)
794
+ * rather than a named enum reference or a runtime enum declaration.
861
795
  *
862
- * @example Resolve `plugin-ts`'s file for the current operation
863
- * ```ts
864
- * const fileTs = resolveOperationFile({ ctx, node, resolver: tsResolver, root, output })
865
- * ```
796
+ * The parser folds `const` into a single-value enum node. The adapter decides which schemas are
797
+ * named enums and lists them in `enumSchemaNames`, and references to those names get suffixed (for
798
+ * example `StatusKey`). A const renders as a literal only when the adapter has not registered it as
799
+ * a named enum, which keeps the declaration and its references in sync across adapter versions.
866
800
  */
867
- function resolveOperationFile(options) {
868
- const { ctx, node, resolver, root, output } = options;
869
- const { memo } = ctx;
870
- const location = {
871
- root,
872
- output,
873
- group: options.group ?? void 0
874
- };
875
- return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => {
876
- const schemaPaths = memo(`${resolver.pluginName}:schemaFilePaths`, () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.name ? [resolver.file({
877
- name: schema.name,
878
- extname: ".ts",
879
- ...location
880
- }).path.toLowerCase()] : [])));
881
- let file = resolver.file({
882
- ...operationFileEntry(node, node.operationId),
883
- ...location
884
- });
885
- for (let name = `${node.operationId}Operation`; schemaPaths.has(file.path.toLowerCase()); name += "Operation") {
886
- const next = resolver.file({
887
- ...operationFileEntry(node, name),
888
- ...location
889
- });
890
- if (next.path === file.path) break;
891
- file = next;
892
- }
893
- return file;
894
- });
801
+ function isInlineConstEnum(node, enumSchemaNames) {
802
+ return (node.namedEnumValues ?? node.enumValues ?? []).length === 1 && !(node.name && enumSchemaNames?.has(node.name));
803
+ }
804
+ function isSchemaOptional(schema) {
805
+ return Boolean("optional" in schema && schema.optional || "nullish" in schema && schema.nullish);
895
806
  }
896
- function baseContentType(contentType) {
897
- return contentType.split(";")[0].trim().toLowerCase();
807
+ function formatExample(value) {
808
+ if (value === null || typeof value !== "object") return String(value);
809
+ return (JSON.stringify(value) ?? String(value)).replaceAll("*/", "*\\/");
898
810
  }
899
811
  /**
900
- * Whether a content entry is a sequence of JSON values the adapter marks as a stream, such as `application/jsonl`,
901
- * `application/x-ndjson`, `application/json-seq`, or any media type with an OpenAPI 3.2 `itemSchema`.
812
+ * Whether a schema is binary, or an array of binary items, so its TypeScript type is `Blob` or `Blob[]`.
902
813
  */
903
- function isJsonLines(entry) {
904
- const baseType = baseContentType(entry.contentType);
905
- return Boolean(entry.stream) && baseType !== "text/event-stream" && !baseType.startsWith("multipart/");
906
- }
907
- function isJsonContentType(baseType) {
908
- return baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json";
814
+ function isBinarySchema(schema) {
815
+ const meta = ast.ref.mergeSchema(schema);
816
+ const isBlob = (node) => ast.ref.mergeSchema(node)?.type === "blob";
817
+ return !!meta && (isBlob(meta) || meta.type === "array" && "items" in meta && (meta.items ?? []).some(isBlob));
909
818
  }
910
819
  /**
911
- * Parser for one response content entry.
820
+ * Collects JSDoc annotation strings for a schema node.
912
821
  *
913
- * JSON stays unset so the runtime detects it. `text/event-stream` and JSON Lines are streams. A binary schema
914
- * (`format: binary` / `contentMediaType`) is a blob even when the media type is not a well-known
915
- * binary type. Other `text/*` is text, and `application/octet-stream`, `application/pdf`,
916
- * `image/*`, `audio/*`, and `video/*` are blobs.
822
+ * Only uses official JSDoc tags from https://jsdoc.app/: `@description`, `@deprecated`, `@minimum`, `@maximum`, `@minLength`,
823
+ * `@maxLength`, `@pattern`, `@default`, `@example`, `@see` and `@type`.
917
824
  */
918
- function responseTypeForContent(entry) {
919
- const baseType = baseContentType(entry.contentType);
920
- if (!baseType) return void 0;
921
- if (baseType === "text/event-stream" || isJsonLines(entry)) return "stream";
922
- if (isJsonContentType(baseType)) return void 0;
923
- if (entry.schema?.type === "blob") return "blob";
924
- if (baseType.startsWith("text/")) return "text";
925
- if (isBinaryContentType(baseType)) return "blob";
926
- }
927
- function isBinaryContentType(baseType) {
928
- return baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType);
825
+ function buildPropertyJSDocComments(schema, optional) {
826
+ const meta = ast.ref.mergeSchema(schema);
827
+ const isArray = meta?.primitive === "array";
828
+ const isObject = meta?.type === "object";
829
+ const isNumeric = meta?.primitive === "number" || meta?.primitive === "integer" || meta?.primitive === "bigint";
830
+ const hasDescription = meta && "description" in meta && meta.description;
831
+ const formatComment = meta && "format" in meta && meta.format ? hasDescription ? [" ", `Format: \`${meta.format}\``] : ["@description", `Format: \`${meta.format}\``] : [];
832
+ const exampleValues = meta?.examples ?? [];
833
+ const comments = [
834
+ hasDescription ? `@description ${jsDocEscape(meta.description)}` : null,
835
+ ...formatComment,
836
+ meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : null,
837
+ !isArray && !isObject && meta && "min" in meta && meta.min !== void 0 ? `@${isNumeric ? "minimum" : "minLength"} ${meta.min}` : null,
838
+ !isArray && !isObject && meta && "max" in meta && meta.max !== void 0 ? `@${isNumeric ? "maximum" : "maxLength"} ${meta.max}` : null,
839
+ meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
840
+ meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" && typeof meta.default === "string" ? text.quote(meta.default) : formatExample(meta.default)}` : null,
841
+ ...exampleValues.map((example) => `@example ${formatExample(example)}`),
842
+ meta?.externalDocs ? `@see ${jsDocEscape([meta.externalDocs.url, meta.externalDocs.description].filter(Boolean).join(" "))}` : null
843
+ ].filter(Boolean);
844
+ const typeTag = comments.length && !isBinarySchema(schema) && meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, optional ?? isSchemaOptional(schema) ? " | undefined" : null].filter(Boolean).join("") : null;
845
+ return [...comments, typeTag].filter(Boolean);
929
846
  }
930
847
  /**
931
- * Maps a content type to the PascalCase suffix used to name per-content-type variants
932
- * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
848
+ * The example values. With `serialized`, each `serializedValue` is added as a `<caption>Serialized</caption>` example.
933
849
  */
934
- function getContentTypeSuffix(contentType) {
935
- const baseType = contentType.split(";")[0].trim();
936
- if (baseType === "application/json") return "Json";
937
- if (baseType === "multipart/form-data") return "FormData";
938
- if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
939
- const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
940
- if (parts.length === 0) return "Unknown";
941
- return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
850
+ function getExampleValues(examples, serialized = false) {
851
+ const values = (examples ?? []).flatMap((example) => [...example.value !== void 0 ? [example.value] : [], ...serialized && example.serializedValue !== void 0 ? [`<caption>Serialized</caption> ${jsDocEscape(example.serializedValue)}`] : []]);
852
+ return values.length > 0 ? values : void 0;
942
853
  }
943
854
  /**
944
- * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
945
- * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
855
+ * The JSDoc lines of a response type: `@summary`, and one `@see` per link, such as `@see getPet: getPet({ petId: $response.body#/id })`.
946
856
  */
947
- function getPerContentTypeName(baseName, suffix) {
948
- if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
949
- return baseName + suffix;
857
+ function buildResponseComments(response) {
858
+ const links = Object.entries(response.links ?? {}).map(([name, link]) => {
859
+ const values = {
860
+ ...link.parameters,
861
+ ...link.requestBody !== void 0 ? { body: link.requestBody } : {}
862
+ };
863
+ const args = Object.entries(values).map(([key, value]) => `${key}: ${formatExample(value)}`);
864
+ return `@see ${jsDocEscape(`${name}: ${link.operationId ?? link.operationRef ?? ""}(${args.length ? `{ ${args.join(", ")} }` : ""})`)}`;
865
+ });
866
+ return [...response.summary ? [`@summary ${jsDocEscape(response.summary)}`] : [], ...links];
950
867
  }
951
868
  /**
952
- * Returns the entry with the schema of the value the client returns: `string` for a body read as text, such as `Pet` on
953
- * `application/xml`, and `blob` for `image/*`, `audio/*`, `video/*`, PDF, and octet-stream. A binary schema elsewhere is a `Blob`
954
- * only through `parseAs: 'blob'`, which needs every entry in `entries` binary. JSON, vendor (`application/vnd.*`), stream,
955
- * schemaless, and wildcard entries keep their schema. `entries` is the response's full list of content types.
869
+ * Builds the object schema for one parameter group or a response's headers. Each entry embeds its own schema and JSDoc,
870
+ * so the group is the only type these entries are exported as.
956
871
  */
957
- function toResponseContent(entry, entries = [entry]) {
958
- const { contentType, schema } = entry;
959
- if (!schema || entry.stream) return entry;
960
- const baseType = baseContentType(contentType);
961
- const resolved = ast.resolveRef(schema);
962
- if (isBinaryContentType(baseType)) return resolved.type === "blob" ? entry : {
963
- ...entry,
964
- schema: ast.factory.createSchema({ type: "blob" }),
965
- keysToOmit: null
966
- };
967
- const isPlainMediaType = Boolean(baseType) && !isJsonContentType(baseType) && !baseType.includes("*");
968
- if (resolved.type === "blob") return isPlainMediaType && !entries.every((item) => item.schema?.type === "blob") ? {
969
- ...entry,
970
- schema: ast.factory.createSchema({ type: "string" }),
971
- keysToOmit: null
972
- } : entry;
973
- if (resolved.primitive === "string") return entry;
974
- const responseType = responseTypeForContent(entry);
975
- if (responseType !== "text" && (resolved.type === "unknown" || resolved.type === "any" || resolved.type === "void")) return entry;
976
- const isVendorJson = /^[^/]+\/vnd\./.test(baseType) && !/(xml|yaml|yml|csv)$/.test(baseType);
977
- return responseType === "text" || !responseType && isPlainMediaType && !isVendorJson ? {
978
- ...entry,
979
- schema: ast.factory.createSchema({ type: "string" }),
980
- keysToOmit: null
981
- } : entry;
872
+ function buildParams({ params, serializedExamples }) {
873
+ const pointer = params[0]?.pointer;
874
+ return ast.factory.createSchema({
875
+ type: "object",
876
+ pointer: pointer?.slice(0, pointer.lastIndexOf("/")),
877
+ properties: params.map((param) => ast.factory.createProperty({
878
+ name: param.name,
879
+ required: param.required,
880
+ schema: ast.factory.createSchema({
881
+ ...param.schema,
882
+ optional: !param.required,
883
+ deprecated: param.deprecated || param.schema.deprecated,
884
+ examples: getExampleValues(param.examples, serializedExamples) ?? param.schema.examples
885
+ })
886
+ }))
887
+ });
982
888
  }
983
- /**
984
- * Resolves per-content-type variant names for a set of content entries, deduplicating suffix
985
- * collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
986
- * the final (possibly counter-augmented) value, so callers can derive parallel names in another
987
- * namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
988
- */
989
- function resolveContentTypeVariants(entries, baseName) {
990
- const usedNames = /* @__PURE__ */ new Set();
991
- return entries.filter((entry) => entry.schema).map((entry) => {
992
- const baseSuffix = getContentTypeSuffix(entry.contentType);
993
- let suffix = baseSuffix;
994
- let name = getPerContentTypeName(baseName, suffix);
995
- let counter = 2;
996
- while (usedNames.has(name)) {
997
- suffix = `${baseSuffix}${counter++}`;
998
- name = getPerContentTypeName(baseName, suffix);
889
+ function buildResponseUnion(node, { resolver }) {
890
+ const allWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
891
+ const successWithSchema = allWithSchema.filter((res) => ast.http.status.isSuccess(res));
892
+ const responsesWithSchema = successWithSchema.length > 0 ? successWithSchema : allWithSchema;
893
+ if (responsesWithSchema.length === 0) return null;
894
+ return ast.factory.createSchema({
895
+ type: "union",
896
+ members: responsesWithSchema.map((res) => ast.factory.createSchema({
897
+ type: "ref",
898
+ name: resolver.response.status(node, res.statusCode)
899
+ }))
900
+ });
901
+ }
902
+ // src/printers/printerTs.ts
903
+ const { isNonNullable } = factory_exports;
904
+ const requireModule = createRequire(import.meta.url);
905
+ /**
906
+ * Loads `@kubb/parser-ts` on first use. It is an optional peer dependency: only a deprecated `printer.nodes`
907
+ * handler that returns a TypeScript compiler node needs it. Loads synchronously because node handlers are synchronous.
908
+ */
909
+ function loadParserTs(load = () => requireModule("@kubb/parser-ts")) {
910
+ try {
911
+ return load();
912
+ } catch (error) {
913
+ if (error.code !== "MODULE_NOT_FOUND" || !error.message.split("\n")[0].includes("@kubb/parser-ts")) throw error;
914
+ throw new Diagnostics.Error({
915
+ code: Diagnostics.code.missingDependency,
916
+ severity: "error",
917
+ message: "Missing dependency: @kubb/parser-ts. A `printer.nodes` handler returned a TypeScript compiler node, and printing it needs @kubb/parser-ts.",
918
+ help: "Run `npm i -D @kubb/parser-ts` in the project, or return a string from the handler.",
919
+ plugin: "plugin-ts"
920
+ });
921
+ }
922
+ }
923
+ function printNullable(node, print) {
924
+ const type = print(node);
925
+ return type && node.nullable ? createUnionDeclaration({ nodes: [type, keywordTypeNodes.null] }) : type;
926
+ }
927
+ /**
928
+ * Adapts user overrides, which work in strings, to the node-based printer: `this.transform` hands them printed
929
+ * strings and their result is wrapped back into a node. A deprecated compiler node prints through `@kubb/parser-ts`.
930
+ */
931
+ function toNodeHandlers(nodes) {
932
+ return Object.fromEntries(Object.entries(nodes ?? {}).map(([type, handler]) => [type, function(node) {
933
+ const transform = (child) => {
934
+ const output = this.transform(child);
935
+ return output ? print(output) : null;
936
+ };
937
+ const result = handler.call({
938
+ ...this,
939
+ transform
940
+ }, node);
941
+ if (result === null || result === void 0) return result;
942
+ return createRaw(typeof result === "string" ? result : loadParserTs().parserTs().print(result).trimEnd());
943
+ }]));
944
+ }
945
+ /**
946
+ * TypeScript type printer built with `definePrinter`.
947
+ *
948
+ * Converts a `SchemaNode` AST node into TypeScript source:
949
+ * - **`printer.print(node)`** — when `options.name` is set, returns a full
950
+ * `type Name = …` or `interface Name { … }` declaration.
951
+ * Without `name`, returns the type for the schema, such as `string | null`.
952
+ *
953
+ * Dispatches on `node.type` to the appropriate handler in `nodes`. Options are closed
954
+ * over per printer instance, so each call to `printerTs(options)` produces an independent printer.
955
+ *
956
+ * @example Raw type node (no `typeName`)
957
+ * ```ts
958
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' } })
959
+ * const type = printer.print(schemaNode) // 'string | null'
960
+ * ```
961
+ *
962
+ * @example Full declaration (with `typeName`)
963
+ * ```ts
964
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' }, typeName: 'MyType' })
965
+ * const declaration = printer.print(schemaNode) // 'export type MyType = …'
966
+ * ```
967
+ */
968
+ const printerTs = createPrinter((options) => {
969
+ const addsUndefined = options.optionalType === "undefined" || options.optionalType === "questionTokenAndUndefined";
970
+ return {
971
+ name: "typescript",
972
+ options,
973
+ nodes: {
974
+ any: () => keywordTypeNodes.any,
975
+ unknown: () => keywordTypeNodes.unknown,
976
+ void: () => keywordTypeNodes.void,
977
+ never: () => keywordTypeNodes.never,
978
+ boolean: () => keywordTypeNodes.boolean,
979
+ null: () => keywordTypeNodes.null,
980
+ blob: () => createTypeReferenceNode("Blob"),
981
+ string: () => keywordTypeNodes.string,
982
+ uuid: () => keywordTypeNodes.string,
983
+ email: () => keywordTypeNodes.string,
984
+ url: (node) => {
985
+ if (node.path) return createUrlTemplateType(node.path);
986
+ return keywordTypeNodes.string;
987
+ },
988
+ ipv4: () => keywordTypeNodes.string,
989
+ ipv6: () => keywordTypeNodes.string,
990
+ datetime: () => keywordTypeNodes.string,
991
+ number: () => keywordTypeNodes.number,
992
+ integer: () => keywordTypeNodes.number,
993
+ bigint: () => keywordTypeNodes.bigint,
994
+ date: dateOrStringNode,
995
+ time: dateOrStringNode,
996
+ ref(node) {
997
+ if (!node.name) return null;
998
+ const refName = ast.ref.getName(node);
999
+ if (!refName) return null;
1000
+ return createTypeReferenceNode(node.ref ? enumTypeName({
1001
+ resolver: this.options.resolver,
1002
+ name: refName,
1003
+ enumOptions: this.options.enum,
1004
+ isEnum: !!this.options.enumSchemaNames?.has(refName)
1005
+ }) : refName);
1006
+ },
1007
+ enum(node) {
1008
+ const values = ast.schema.getLiterals(node);
1009
+ if (values.some((value) => value !== null && typeof value === "object")) return createUnionDeclaration({ nodes: values.map((value) => constToTypeNode(value)) });
1010
+ if (this.options.enum.type === "inlineLiteral" || !node.name || isInlineConstEnum(node, this.options.enumSchemaNames)) return createUnionDeclaration({ nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(isNonNullable) });
1011
+ return createTypeReferenceNode(enumTypeName({
1012
+ resolver: this.options.resolver,
1013
+ name: node.name,
1014
+ enumOptions: this.options.enum,
1015
+ isEnum: true
1016
+ }));
1017
+ },
1018
+ union(node) {
1019
+ const members = node.members ?? [];
1020
+ const hasStringLiteral = members.some((m) => {
1021
+ return ast.schema.narrow(m, ast.schema.types.enum)?.primitive === "string";
1022
+ });
1023
+ const hasPlainString = members.some((m) => ast.schema.isString(m));
1024
+ if (hasStringLiteral && hasPlainString) return createUnionDeclaration({ nodes: members.map((m) => {
1025
+ const print = (node) => ast.schema.isString(node) ? createIntersectionDeclaration({ nodes: [keywordTypeNodes.string, createTypeLiteralNode([])] }) : this.transform(node);
1026
+ return printNullable(m, print);
1027
+ }).filter(isNonNullable) });
1028
+ return createUnionDeclaration({ nodes: buildMemberNodes(members, (member) => printNullable(member, this.transform)) });
1029
+ },
1030
+ intersection(node) {
1031
+ return createIntersectionDeclaration({ nodes: buildMemberNodes(node.members, this.transform) });
1032
+ },
1033
+ array(node) {
1034
+ return createArrayDeclaration({
1035
+ nodes: (node.items ?? []).map((item) => printNullable(item, this.transform)).filter(isNonNullable),
1036
+ arrayType: this.options.arrayType
1037
+ });
1038
+ },
1039
+ tuple(node) {
1040
+ return buildTupleNode(node, (item) => printNullable(item, this.transform));
1041
+ },
1042
+ object(node) {
1043
+ const { transform, options } = this;
1044
+ const keySchema = node.propertyNames && ast.ref.mergeSchema(node.propertyNames);
1045
+ if (!node.properties.length && !Object.keys(node.patternProperties ?? {}).length && node.propertyNames && keySchema?.type === "enum" && node.additionalProperties !== false) {
1046
+ const keyType = transform(node.propertyNames) ?? keywordTypeNodes.string;
1047
+ const valueSchema = typeof node.additionalProperties === "object" ? node.additionalProperties : ast.factory.createSchema({ type: "unknown" });
1048
+ return createTypeReferenceNode("Partial", [createTypeReferenceNode("Record", [keyType, buildPropertyType(valueSchema, transform(valueSchema) ?? keywordTypeNodes.unknown, options.optionalType, false)])]);
1049
+ }
1050
+ const addsQuestionToken = options.optionalType === "questionToken" || options.optionalType === "questionTokenAndUndefined";
1051
+ const propertyNodes = node.properties.map((property) => {
1052
+ const { name } = property;
1053
+ const baseType = transform(property.schema) ?? keywordTypeNodes.unknown;
1054
+ const optional = !property.required || !!property.schema.optional || !!property.schema.nullish;
1055
+ const type = buildPropertyType(property.schema, baseType, options.optionalType, optional);
1056
+ const propMeta = ast.ref.mergeSchema(property.schema);
1057
+ const propertyNode = createPropertySignature({
1058
+ questionToken: optional ? addsQuestionToken : false,
1059
+ name,
1060
+ type,
1061
+ readOnly: propMeta?.readOnly
1062
+ });
1063
+ const binaryRemark = isBinarySchema(property.schema) ? "@remarks Pass a `File` to send its filename. A plain `Blob` is sent with the filename `blob`." : void 0;
1064
+ return appendJSDocToNode({
1065
+ node: propertyNode,
1066
+ comments: [...buildPropertyJSDocComments(property.schema, optional), binaryRemark]
1067
+ });
1068
+ });
1069
+ const allElements = [...propertyNodes, ...buildIndexSignatures(node, propertyNodes.length, transform)];
1070
+ if (!allElements.length) return keywordTypeNodes.object;
1071
+ return createTypeLiteralNode(allElements);
1072
+ },
1073
+ ...toNodeHandlers(options.nodes)
1074
+ },
1075
+ print(node) {
1076
+ const { name, syntaxType = "type", description, examples, comments = [], keysToOmit } = this.options;
1077
+ const transformed = this.transform(node);
1078
+ if (!transformed) return null;
1079
+ const meta = ast.ref.mergeSchema(node);
1080
+ if (!name) return print(wrapNullableOptional(keysToOmit?.length ? createOmitDeclaration({
1081
+ keys: keysToOmit,
1082
+ type: transformed,
1083
+ nonNullable: true
1084
+ }) : transformed, !!meta.nullable, !!(meta.nullish || meta.optional) && addsUndefined));
1085
+ return createTypeDeclaration({
1086
+ name,
1087
+ isExportable: true,
1088
+ type: (() => {
1089
+ return wrapNullableOptional(keysToOmit?.length ? createOmitDeclaration({
1090
+ keys: keysToOmit,
1091
+ type: transformed,
1092
+ nonNullable: true
1093
+ }) : transformed, !!meta.nullable, !!(meta.nullish || meta.optional));
1094
+ })(),
1095
+ syntax: syntaxType === "type" || !!keysToOmit?.length ? "type" : "interface",
1096
+ comments: [...comments, ...buildPropertyJSDocComments({
1097
+ ...meta,
1098
+ description,
1099
+ examples: examples?.length ? examples : meta.examples
1100
+ })]
1101
+ });
999
1102
  }
1000
- usedNames.add(name);
1001
- return {
1103
+ };
1104
+ });
1105
+ // src/components/Type.tsx
1106
+ function Type({ name, node, printer, enum: enumOptions, resolver, inline }) {
1107
+ if (inline) {
1108
+ const withoutEnums = ast.node.transform(node, { schema: (n) => n.type === ast.schema.types.enum && n.name ? {
1109
+ ...n,
1110
+ name: void 0
1111
+ } : void 0 });
1112
+ const expression = printerTs({
1113
+ ...printer.options,
1114
+ name: void 0
1115
+ }).print(withoutEnums)?.replace(/\/\*\*[\s\S]*?\*\/\s*/g, "");
1116
+ if (!expression) return;
1117
+ return /* @__PURE__ */ jsx(File.Source, {
1002
1118
  name,
1003
- suffix,
1004
- schema: entry.schema,
1005
- keysToOmit: entry.keysToOmit,
1006
- contentType: entry.contentType
1119
+ isTypeOnly: true,
1120
+ isExportable: true,
1121
+ isIndexable: true,
1122
+ inline: { expression },
1123
+ children: expression
1124
+ });
1125
+ }
1126
+ const enumSchemaNodes = ast.node.collect(node, { schema(n) {
1127
+ const enumNode = ast.schema.narrow(n, ast.schema.types.enum);
1128
+ if (enumNode?.name && !isInlineConstEnum(enumNode, printer.options.enumSchemaNames)) return enumNode;
1129
+ } });
1130
+ const output = printer.print(node);
1131
+ const printerImports = printer.drainImports();
1132
+ if (!output) return;
1133
+ const enums = [...new Map(enumSchemaNodes.map((n) => [n.name, n])).values()].map((node) => {
1134
+ return {
1135
+ node,
1136
+ ...getEnumNames$1({
1137
+ node,
1138
+ enum: enumOptions,
1139
+ resolver
1140
+ })
1007
1141
  };
1008
1142
  });
1143
+ const shouldExportEnums = enumOptions.type !== "inlineLiteral";
1144
+ const shouldExportType = enumOptions.type === "inlineLiteral" || enums.every((item) => item.typeName !== name);
1145
+ return /* @__PURE__ */ jsxs(Fragment, { children: [shouldExportEnums && enums.map(({ node }) => /* @__PURE__ */ jsx(Enum, {
1146
+ node,
1147
+ enum: enumOptions,
1148
+ resolver
1149
+ }, node.name)), shouldExportType && /* @__PURE__ */ jsxs(Fragment, { children: [printerImports.map((imp) => /* @__PURE__ */ jsx(File.Import, { ...imp }, [
1150
+ name,
1151
+ imp.path,
1152
+ imp.name
1153
+ ].join("-"))), /* @__PURE__ */ jsx(File.Source, {
1154
+ name,
1155
+ pointer: node.pointer,
1156
+ isTypeOnly: true,
1157
+ isExportable: true,
1158
+ isIndexable: true,
1159
+ children: output
1160
+ })] })] });
1009
1161
  }
1010
- const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
1011
- /**
1012
- * An OpenAPI 3.2 `querystring` parameter is the whole query string, so a form-encoded object becomes one query parameter
1013
- * per property, typed and serialized like any other.
1014
- * Another content type, such as JSON, stays one parameter named after it, and the runtime sends its value as the whole query string.
1015
- */
1016
- function spreadQuerystring(param) {
1017
- const schema = param.schema.type === "ref" ? param.schema.schema : param.schema;
1018
- const formEncoded = !param.contentType || param.contentType.split(";")[0].trim().toLowerCase() === "application/x-www-form-urlencoded";
1019
- if (schema?.type !== "object" || !formEncoded) return [param];
1020
- return schema.properties.map((property) => ast.factory.createParameter({
1021
- name: property.name,
1022
- in: "query",
1023
- schema: property.schema,
1024
- required: param.required && property.required
1025
- }));
1162
+ // ../../internals/shared/src/operation.ts
1163
+ /**
1164
+ * Builds the `ResolverFileParams` every operation generator passes to
1165
+ * `resolver.file`: a file named `name`, tagged by the operation's first
1166
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
1167
+ * that was repeated at dozens of call sites across the client and query plugins.
1168
+ *
1169
+ * @example
1170
+ * ```ts
1171
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
1172
+ * ```
1173
+ */
1174
+ function operationFileEntry(node, name, extname = ".ts") {
1175
+ return {
1176
+ name,
1177
+ extname,
1178
+ tag: node.tags[0] ?? "default",
1179
+ path: node.path
1180
+ };
1026
1181
  }
1027
1182
  /**
1028
- * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
1029
- * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
1030
- * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
1031
- * same parameters once per plugin.
1032
- */
1033
- function getOperationParameters(node) {
1034
- const cached = operationParameterGroupsByNode.get(node);
1035
- if (cached) return cached;
1036
- const dedupe = (params) => {
1037
- const seen = /* @__PURE__ */ new Set();
1038
- return params.filter((param) => {
1039
- if (seen.has(param.name)) return false;
1040
- seen.add(param.name);
1041
- return true;
1042
- });
1183
+ * Resolves the file a schema-emitting plugin (`plugin-ts`, `plugin-zod`, `plugin-faker`) writes
1184
+ * `node` to, cached in `ctx.memo` under the resolver's plugin name and the
1185
+ * operation. The plugin itself and every plugin that imports from it call this, so they all get
1186
+ * the same path.
1187
+ *
1188
+ * When that path matches the file of a component schema in `ctx.driver.inputNode`, ignoring case, the file name
1189
+ * gets an `Operation` suffix: operation `point` goes to `PointOperation.ts` next to schema `Point.ts`.
1190
+ * Otherwise the two merge into one file, or overwrite each other on a case-insensitive file system.
1191
+ * Only the file name changes. The declarations keep their names.
1192
+ *
1193
+ * @example Resolve `plugin-ts`'s file for the current operation
1194
+ * ```ts
1195
+ * const fileTs = resolveOperationFile({ ctx, node, resolver: tsResolver, root, output })
1196
+ * ```
1197
+ */
1198
+ function resolveOperationFile(options) {
1199
+ const { ctx, node, resolver, root, output } = options;
1200
+ const { memo } = ctx;
1201
+ const location = {
1202
+ root,
1203
+ output,
1204
+ group: options.group ?? void 0
1043
1205
  };
1044
- const group = (location) => dedupe(node.parameters.filter((param) => param.in === location));
1045
- const groups = {
1046
- path: group("path"),
1047
- query: dedupe([...group("query"), ...group("querystring").flatMap(spreadQuerystring)]),
1048
- header: group("header"),
1049
- cookie: group("cookie")
1206
+ return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => {
1207
+ const schemaPaths = memo(`${resolver.pluginName}:schemaFilePaths`, () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.name ? [resolver.file({
1208
+ name: schema.name,
1209
+ extname: ".ts",
1210
+ ...location
1211
+ }).path.toLowerCase()] : [])));
1212
+ let file = resolver.file({
1213
+ ...operationFileEntry(node, node.operationId),
1214
+ ...location
1215
+ });
1216
+ for (let name = `${node.operationId}Operation`; schemaPaths.has(file.path.toLowerCase()); name += "Operation") {
1217
+ const next = resolver.file({
1218
+ ...operationFileEntry(node, name),
1219
+ ...location
1220
+ });
1221
+ if (next.path === file.path) break;
1222
+ file = next;
1223
+ }
1224
+ return file;
1225
+ });
1226
+ }
1227
+ /**
1228
+ * How a content entry is framed as a stream: `'events'` for Server-Sent Events, `'lines'` for JSON Lines.
1229
+ */
1230
+ function getStreamFraming(entry) {
1231
+ if (ast.mediaType.getBase(entry) === "text/event-stream") return "events";
1232
+ return entry.stream && ast.mediaType.isStream(entry) ? "lines" : void 0;
1233
+ }
1234
+ /**
1235
+ * Parser for one response content entry.
1236
+ *
1237
+ * JSON stays unset so the runtime detects it. `text/event-stream` and JSON Lines are streams. A binary schema
1238
+ * (`format: binary` / `contentMediaType`) is a blob even when the media type is not a well-known
1239
+ * binary type. Other `text/*` is text, and `application/octet-stream`, `application/pdf`,
1240
+ * `image/*`, `audio/*`, and `video/*` are blobs.
1241
+ */
1242
+ function responseTypeForContent(entry) {
1243
+ const baseType = ast.mediaType.getBase(entry);
1244
+ if (!baseType) return void 0;
1245
+ if (getStreamFraming(entry)) return "stream";
1246
+ if (ast.mediaType.isJson(baseType)) return void 0;
1247
+ if (entry.schema?.type === "blob") return "blob";
1248
+ if (baseType.startsWith("text/")) return "text";
1249
+ if (isBinaryContentType(baseType)) return "blob";
1250
+ }
1251
+ function isBinaryContentType(baseType) {
1252
+ return baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType);
1253
+ }
1254
+ /**
1255
+ * Maps a content type to the PascalCase suffix used to name per-content-type variants
1256
+ * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
1257
+ */
1258
+ function getContentTypeSuffix(contentType) {
1259
+ const baseType = contentType.split(";")[0].trim();
1260
+ if (baseType === "application/json") return "Json";
1261
+ if (baseType === "multipart/form-data") return "FormData";
1262
+ if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
1263
+ const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
1264
+ if (parts.length === 0) return "Unknown";
1265
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1266
+ }
1267
+ /**
1268
+ * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
1269
+ * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
1270
+ */
1271
+ function getPerContentTypeName(baseName, suffix) {
1272
+ if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
1273
+ return baseName + suffix;
1274
+ }
1275
+ /**
1276
+ * Returns the entry with the schema of the value the client returns: `string` for a body read as text, such as `Pet` on
1277
+ * `application/xml`, and `blob` for `image/*`, `audio/*`, `video/*`, PDF, and octet-stream. A binary schema elsewhere is a `Blob`
1278
+ * only through `parseAs: 'blob'`, which needs every entry in `entries` binary. JSON, vendor (`application/vnd.*`), stream,
1279
+ * schemaless, and wildcard entries keep their schema. `entries` is the response's full list of content types.
1280
+ */
1281
+ function toResponseContent(entry, entries = [entry]) {
1282
+ const { contentType, schema } = entry;
1283
+ if (!schema || entry.stream) return entry;
1284
+ const baseType = ast.mediaType.getBase(contentType);
1285
+ const resolved = ast.ref.getSchema(schema);
1286
+ if (isBinaryContentType(baseType)) return resolved.type === "blob" ? entry : {
1287
+ ...entry,
1288
+ schema: ast.factory.createSchema({ type: "blob" }),
1289
+ keysToOmit: null
1050
1290
  };
1051
- operationParameterGroupsByNode.set(node, groups);
1052
- return groups;
1291
+ const isPlainMediaType = Boolean(baseType) && !ast.mediaType.isJson(baseType) && !baseType.includes("*");
1292
+ if (resolved.type === "blob") return isPlainMediaType && !entries.every((item) => item.schema?.type === "blob") ? {
1293
+ ...entry,
1294
+ schema: ast.factory.createSchema({ type: "string" }),
1295
+ keysToOmit: null
1296
+ } : entry;
1297
+ if (resolved.primitive === "string") return entry;
1298
+ const responseType = responseTypeForContent(entry);
1299
+ if (responseType !== "text" && (resolved.type === "unknown" || resolved.type === "any" || resolved.type === "void")) return entry;
1300
+ const isVendorJson = /^[^/]+\/vnd\./.test(baseType) && !/(xml|yaml|yml|csv)$/.test(baseType);
1301
+ return responseType === "text" || !responseType && isPlainMediaType && !isVendorJson ? {
1302
+ ...entry,
1303
+ schema: ast.factory.createSchema({ type: "string" }),
1304
+ keysToOmit: null
1305
+ } : entry;
1306
+ }
1307
+ /**
1308
+ * Resolves per-content-type variant names for a set of content entries, deduplicating suffix
1309
+ * collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
1310
+ * the final (possibly counter-augmented) value, so callers can derive parallel names in another
1311
+ * namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
1312
+ */
1313
+ function resolveContentTypeVariants(entries, baseName) {
1314
+ const usedNames = /* @__PURE__ */ new Set();
1315
+ return entries.filter((entry) => entry.schema).map((entry) => {
1316
+ const baseSuffix = getContentTypeSuffix(entry.contentType);
1317
+ let suffix = baseSuffix;
1318
+ let name = getPerContentTypeName(baseName, suffix);
1319
+ let counter = 2;
1320
+ while (usedNames.has(name)) {
1321
+ suffix = `${baseSuffix}${counter++}`;
1322
+ name = getPerContentTypeName(baseName, suffix);
1323
+ }
1324
+ usedNames.add(name);
1325
+ return {
1326
+ name,
1327
+ suffix,
1328
+ schema: entry.schema,
1329
+ keysToOmit: entry.keysToOmit,
1330
+ contentType: entry.contentType
1331
+ };
1332
+ });
1053
1333
  }
1054
1334
  /**
1055
1335
  * Whether the operation declares more than one request media type with a schema, so a caller must pick one on `contentType`.
@@ -1066,8 +1346,8 @@ function hasRequestContentTypeChoice(node) {
1066
1346
  * than a record.
1067
1347
  */
1068
1348
  function buildOptionsSchema(node, resolver, { pairContentTypes = false } = {}) {
1069
- const { path, query, header, cookie } = getOperationParameters(node);
1070
- const hasBody = Boolean(node.requestBody?.content?.[0]?.schema);
1349
+ const { path, query, header, cookie } = ast.http.groupParameters(node);
1350
+ const hasBody = Boolean(ast.http.requestBody.getSchema(node));
1071
1351
  const createNever = () => ast.factory.createSchema({
1072
1352
  type: "never",
1073
1353
  primitive: void 0,
@@ -1158,9 +1438,10 @@ function buildOptionsSchema(node, resolver, { pairContentTypes = false } = {}) {
1158
1438
  * The schema a status occupies in the `<Name>Responses` record. A status that documents several
1159
1439
  * content types becomes a `{ contentType; data }` union so the runtime can surface the negotiated type
1160
1440
  * on `result.parsed`, while the standalone `<Name>StatusNNN` alias stays the plain body union that the
1161
- * query hooks and `result.data` use.
1441
+ * query hooks and `result.data` use. With `streamData`, the stream variant of a 2xx status holds that schema, since the
1442
+ * client returns its body unread.
1162
1443
  */
1163
- function buildResponseRecordEntry(node, res, resolver) {
1444
+ function buildResponseRecordEntry(node, res, resolver, streamData) {
1164
1445
  const statusName = resolver.response.status(node, res.statusCode);
1165
1446
  const variants = (res.content ?? []).filter((entry) => entry.schema);
1166
1447
  if (variants.length <= 1) return ast.factory.createSchema({
@@ -1177,12 +1458,12 @@ function buildResponseRecordEntry(node, res, resolver) {
1177
1458
  required: true,
1178
1459
  schema: ast.factory.createSchema({
1179
1460
  type: "enum",
1180
- enumValues: [baseContentType(variant.contentType)]
1461
+ enumValues: [ast.mediaType.getBase(variant)]
1181
1462
  })
1182
1463
  }), ast.factory.createProperty({
1183
1464
  name: "data",
1184
1465
  required: true,
1185
- schema: ast.factory.createSchema({
1466
+ schema: streamData && ast.http.status.isSuccess(res) && ast.mediaType.isStream(variant) ? streamData : ast.factory.createSchema({
1186
1467
  type: "ref",
1187
1468
  name: variant.name
1188
1469
  })
@@ -1198,32 +1479,29 @@ function buildResponseRecordEntry(node, res, resolver) {
1198
1479
  * Always emits the keyed record, even when an operation declares no responses. An operation with no
1199
1480
  * responses renders as an empty object, which keeps every consumer's import (for example the axios
1200
1481
  * SDK's `RequestResult<XResponses>`) resolvable instead of pointing at a missing export.
1482
+ *
1483
+ * `streamData` is the type of a stream variant's `data`, such as a `ReadableStream<Uint8Array>` reference.
1201
1484
  */
1202
- function buildResponses(node, resolver) {
1485
+ function buildResponses(node, resolver, { streamData } = {}) {
1203
1486
  return ast.factory.createSchema({
1204
1487
  type: "object",
1205
1488
  primitive: "object",
1206
1489
  properties: node.responses.map((res) => ast.factory.createProperty({
1207
1490
  name: String(res.statusCode),
1208
1491
  required: true,
1209
- schema: buildResponseRecordEntry(node, res, resolver)
1492
+ schema: buildResponseRecordEntry(node, res, resolver, streamData)
1210
1493
  }))
1211
1494
  });
1212
1495
  }
1213
- function getStatusCodeNumber(statusCode) {
1214
- const code = Number(statusCode);
1215
- return Number.isNaN(code) ? null : code;
1216
- }
1217
- function isSuccessStatusCode(statusCode) {
1218
- const code = getStatusCodeNumber(statusCode);
1219
- return code !== null && code >= 200 && code < 300;
1220
- }
1221
1496
  // ../../internals/shared/src/resolver.ts
1222
1497
  /**
1223
1498
  * Builds the allocation key for a declaration an operation owns. `scope` names which declaration
1224
1499
  * it is, so two of them never share an allocation even when a custom resolver gives both the same
1225
1500
  * name. Leave `scope` empty for the operation's own declaration.
1226
1501
  *
1502
+ * The key starts from the operation's input `pointer`, which macros keep, so a plugin whose macro
1503
+ * rewrites the method or path still resolves another plugin's names for the same operation.
1504
+ *
1227
1505
  * @example
1228
1506
  * ```ts
1229
1507
  * resolver.resolveName({ key: operationKey(node, 'response', 'status', 404), name })
@@ -1232,8 +1510,7 @@ function isSuccessStatusCode(statusCode) {
1232
1510
  function operationKey(node, ...scope) {
1233
1511
  return [
1234
1512
  "operation",
1235
- node.method,
1236
- node.path,
1513
+ ...node.pointer ? [node.pointer] : [node.method, node.path],
1237
1514
  ...scope.map(String)
1238
1515
  ];
1239
1516
  }
@@ -1269,516 +1546,90 @@ function createOperationParamResolver() {
1269
1546
  name
1270
1547
  });
1271
1548
  },
1272
- headers(node) {
1273
- const name = this.name(`${node.operationId} Headers`);
1274
- return this.resolveName({
1275
- key: operationKey(node, "param", "headers"),
1276
- name
1277
- });
1278
- },
1279
- cookies(node) {
1280
- const name = this.name(`${node.operationId} Cookies`);
1281
- return this.resolveName({
1282
- key: operationKey(node, "param", "cookies"),
1283
- name
1284
- });
1285
- }
1286
- };
1287
- }
1288
- /**
1289
- * Builds the shared `response` namespace. Spread the result into
1290
- * `createResolver` and add plugin-specific methods (`options`, `error`) next
1291
- * to it.
1292
- *
1293
- * @example
1294
- * ```ts
1295
- * createResolver<PluginTs>({ response: { ...createOperationResponseResolver(), options(node) {...} }, ... })
1296
- * ```
1297
- */
1298
- function createOperationResponseResolver() {
1299
- return {
1300
- status(node, statusCode) {
1301
- const name = this.name(`${node.operationId} Status ${statusCode}`);
1302
- return this.resolveName({
1303
- key: operationKey(node, "response", "status", statusCode),
1304
- name
1305
- });
1306
- },
1307
- body(node) {
1308
- const name = this.name(`${node.operationId} Body`);
1309
- return this.resolveName({
1310
- key: operationKey(node, "response", "body"),
1311
- name
1312
- });
1313
- },
1314
- responses(node) {
1315
- const name = this.name(`${node.operationId} Responses`);
1316
- return this.resolveName({
1317
- key: operationKey(node, "response", "responses"),
1318
- name
1319
- });
1320
- },
1321
- response(node) {
1322
- const name = this.name(`${node.operationId} Response`);
1323
- return this.resolveName({
1324
- key: operationKey(node, "response", "response"),
1325
- name
1326
- });
1327
- }
1328
- };
1329
- }
1330
- /**
1331
- * Builds a resolver `file` override whose base name runs every path segment
1332
- * through `toFilePath`, casing the final segment with `caseLast`.
1333
- *
1334
- * @example
1335
- * ```ts
1336
- * createResolver<PluginTs>({ file: createCasedFile(pascalCase), ... })
1337
- * ```
1338
- */
1339
- function createCasedFile(caseLast) {
1340
- return { baseName({ name, extname }) {
1341
- return this.memo(`file:${name}\0${extname}`, () => `${toFilePath(name, caseLast)}${extname}`);
1342
- } };
1343
- }
1344
- /**
1345
- * Names of the schemas whose type is `enum`, computed once per build.
1346
- */
1347
- function getEnumNames(ctx) {
1348
- return ctx.memo("enumNames:set", () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.type === "enum" && schema.name ? [schema.name] : [])));
1349
- }
1350
- // ../../internals/shared/src/group.ts
1351
- /**
1352
- * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
1353
- * shared default naming so every plugin groups output consistently:
1354
- *
1355
- * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
1356
- * - other groups use the camelCased group (`pet store` → `petStore`).
1357
- *
1358
- * A user-provided `group.name` always wins over the default namer, so callers stay in
1359
- * control of their output folders. Returns `null` when grouping is disabled, matching the
1360
- * per-plugin convention.
1361
- *
1362
- * @param group - The user-supplied group option, or `undefined` to disable grouping.
1363
- *
1364
- * @example
1365
- * ```ts
1366
- * createGroupConfig(group) // shared across every plugin
1367
- * ```
1368
- */
1369
- function createGroupConfig(group) {
1370
- if (!group) return null;
1371
- const defaultName = (ctx) => {
1372
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1373
- return camelCase(ctx.group);
1374
- };
1375
- return {
1376
- ...group,
1377
- name: group.name ? group.name : defaultName
1378
- };
1379
- }
1380
- // ../../internals/shared/src/schemaTraversal.ts
1381
- /**
1382
- * Maps each property of an object schema to its transformed output. Pairs every result with the
1383
- * original property so the printer keeps full control over modifiers, getters, and key syntax.
1384
- *
1385
- * @example
1386
- * ```ts
1387
- * const entries = mapSchemaProperties(node, (schema) => this.transform(schema))
1388
- * // entries: [{ name: 'id', property, output: 'z.number()' }, ...]
1389
- * ```
1390
- */
1391
- function mapSchemaProperties(node, transform) {
1392
- return node.properties.map((property) => ({
1393
- name: property.name,
1394
- property,
1395
- output: transform(property.schema)
1396
- }));
1397
- }
1398
- /**
1399
- * Maps each item of an array or tuple schema to its transformed output, pairing every result with
1400
- * the original item.
1401
- */
1402
- function mapSchemaItems(node, transform) {
1403
- return (node.items ?? []).map((schema) => ({
1404
- schema,
1405
- output: transform(schema)
1406
- }));
1407
- }
1408
- // src/utils.ts
1409
- /**
1410
- * Renames inline enum declarations and their uses together.
1411
- */
1412
- function resolveInlineEnumNames({ node, owner, resolver, enumOptions }) {
1413
- return ast.transform(node, { schema(child) {
1414
- if (child === node || child.type !== "enum" || !child.name) return;
1415
- const name = resolver.resolveName({
1416
- key: [
1417
- "enum",
1418
- owner,
1419
- child.pointer ?? child.name
1420
- ],
1421
- name: child.name,
1422
- normalize: (value) => {
1423
- const { enumName, typeName } = getEnumNames$1({
1424
- node: {
1425
- ...child,
1426
- name: value
1427
- },
1428
- enum: enumOptions,
1429
- resolver
1430
- });
1431
- return enumOptions.type === "literal" ? [typeName] : [enumName, typeName];
1432
- }
1433
- });
1434
- return name === child.name ? void 0 : {
1435
- ...child,
1436
- name
1437
- };
1438
- } });
1439
- }
1440
- function enumTypeName({ resolver, name, enumOptions, isEnum }) {
1441
- return isEnum && enumOptions?.type === "asConst" && enumOptions.typeSuffix ? resolver.enum.keyName({ name }, enumOptions.typeSuffix) : resolver.name(name);
1442
- }
1443
- /**
1444
- * Tells whether a `const` (single-value enum) should render as a bare literal type (`'active'`)
1445
- * rather than a named enum reference or a runtime enum declaration.
1446
- *
1447
- * The parser folds `const` into a single-value enum node. The adapter decides which schemas are
1448
- * named enums and lists them in `enumSchemaNames`, and references to those names get suffixed (for
1449
- * example `StatusKey`). A const renders as a literal only when the adapter has not registered it as
1450
- * a named enum, which keeps the declaration and its references in sync across adapter versions.
1451
- */
1452
- function isInlineConstEnum(node, enumSchemaNames) {
1453
- return (node.namedEnumValues ?? node.enumValues ?? []).length === 1 && !(node.name && enumSchemaNames?.has(node.name));
1454
- }
1455
- /**
1456
- * Collects JSDoc annotation strings for a schema node.
1457
- *
1458
- * Only uses official JSDoc tags from https://jsdoc.app/: `@description`, `@deprecated`, `@default`, `@example`, `@type`.
1459
- * Constraint metadata (min/max length, pattern, multipleOf, min/maxProperties) is emitted as plain-text lines.
1460
-
1461
- */
1462
- function isSchemaOptional(schema) {
1463
- return Boolean("optional" in schema && schema.optional || "nullish" in schema && schema.nullish);
1464
- }
1465
- function formatExample(value) {
1466
- if (value === null || typeof value !== "object") return String(value);
1467
- return (JSON.stringify(value) ?? String(value)).replaceAll("*/", "*\\/");
1468
- }
1469
- /**
1470
- * Whether a schema is binary, or an array of binary items, so its TypeScript type is `Blob` or `Blob[]`.
1471
- */
1472
- function isBinarySchema(schema) {
1473
- const meta = syncSchemaRef(schema);
1474
- const isBlob = (node) => syncSchemaRef(node)?.type === "blob";
1475
- return !!meta && (isBlob(meta) || meta.type === "array" && "items" in meta && (meta.items ?? []).some(isBlob));
1476
- }
1477
- function buildPropertyJSDocComments(schema, optional) {
1478
- const meta = syncSchemaRef(schema);
1479
- const isArray = meta?.primitive === "array";
1480
- const isNumeric = meta?.primitive === "number" || meta?.primitive === "integer" || meta?.primitive === "bigint";
1481
- const hasDescription = meta && "description" in meta && meta.description;
1482
- const formatComment = meta && "format" in meta && meta.format ? hasDescription ? [" ", `Format: \`${meta.format}\``] : ["@description", `Format: \`${meta.format}\``] : [];
1483
- const exampleValues = meta?.examples ?? [];
1484
- const comments = [
1485
- hasDescription ? `@description ${jsDocEscape(meta.description)}` : null,
1486
- ...formatComment,
1487
- meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : null,
1488
- !isArray && meta && "min" in meta && meta.min !== void 0 ? `@${isNumeric ? "minimum" : "minLength"} ${meta.min}` : null,
1489
- !isArray && meta && "max" in meta && meta.max !== void 0 ? `@${isNumeric ? "maximum" : "maxLength"} ${meta.max}` : null,
1490
- meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
1491
- meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" && typeof meta.default === "string" ? stringify(meta.default) : formatExample(meta.default)}` : null,
1492
- ...exampleValues.map((example) => `@example ${formatExample(example)}`)
1493
- ].filter(Boolean);
1494
- const typeTag = comments.length && !isBinarySchema(schema) && meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, optional ?? isSchemaOptional(schema) ? " | undefined" : null].filter(Boolean).join("") : null;
1495
- return [...comments, typeTag].filter(Boolean);
1496
- }
1497
- /**
1498
- * Builds the object schema for a group of parameters sharing one `in` location (path, query, or
1499
- * header), embedding each param's own schema (and JSDoc) directly rather than referencing a
1500
- * separate per-param type — the group itself is the only type these params get exported as.
1501
- */
1502
- function buildParams({ params }) {
1503
- return ast.factory.createSchema({
1504
- type: "object",
1505
- pointer: params[0]?.pointer?.replace(/\/\d+$/, ""),
1506
- properties: params.map((param) => ast.factory.createProperty({
1507
- name: param.name,
1508
- required: param.required,
1509
- schema: ast.factory.createSchema({
1510
- ...param.schema,
1511
- optional: !param.required
1512
- })
1513
- }))
1514
- });
1515
- }
1516
- function buildResponseUnion(node, { resolver }) {
1517
- const allWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
1518
- const successWithSchema = allWithSchema.filter((res) => isSuccessStatusCode(res.statusCode));
1519
- const responsesWithSchema = successWithSchema.length > 0 ? successWithSchema : allWithSchema;
1520
- if (responsesWithSchema.length === 0) return null;
1521
- return ast.factory.createSchema({
1522
- type: "union",
1523
- members: responsesWithSchema.map((res) => ast.factory.createSchema({
1524
- type: "ref",
1525
- name: resolver.response.status(node, res.statusCode)
1526
- }))
1527
- });
1528
- }
1529
- // src/printers/printerTs.ts
1530
- const { isNonNullable } = factory_exports;
1531
- const requireModule = createRequire(import.meta.url);
1532
- /**
1533
- * Loads `@kubb/parser-ts` on first use. It is an optional peer dependency: only a deprecated `printer.nodes`
1534
- * handler that returns a TypeScript compiler node needs it. Loads synchronously because node handlers are synchronous.
1535
- */
1536
- function loadParserTs(load = () => requireModule("@kubb/parser-ts")) {
1537
- try {
1538
- return load();
1539
- } catch (error) {
1540
- if (error.code !== "MODULE_NOT_FOUND" || !error.message.split("\n")[0].includes("@kubb/parser-ts")) throw error;
1541
- throw new Diagnostics.Error({
1542
- code: Diagnostics.code.missingDependency,
1543
- severity: "error",
1544
- message: "Missing dependency: @kubb/parser-ts. A `printer.nodes` handler returned a TypeScript compiler node, and printing it needs @kubb/parser-ts.",
1545
- help: "Run `npm i -D @kubb/parser-ts` in the project, or return a string from the handler.",
1546
- plugin: "plugin-ts"
1547
- });
1548
- }
1549
- }
1550
- /**
1551
- * Adapts user overrides, which work in strings, to the node-based printer: `this.transform` hands them printed
1552
- * strings and their result is wrapped back into a node. A deprecated compiler node prints through `@kubb/parser-ts`.
1553
- */
1554
- function toNodeHandlers(nodes) {
1555
- return Object.fromEntries(Object.entries(nodes ?? {}).map(([type, handler]) => [type, function(node) {
1556
- const transform = (child) => {
1557
- const output = this.transform(child);
1558
- return output ? print(output) : null;
1559
- };
1560
- const result = handler.call({
1561
- ...this,
1562
- transform
1563
- }, node);
1564
- if (result === null || result === void 0) return result;
1565
- return createRaw(typeof result === "string" ? result : loadParserTs().parserTs().print(result).trimEnd());
1566
- }]));
1549
+ headers(node) {
1550
+ const name = this.name(`${node.operationId} Headers`);
1551
+ return this.resolveName({
1552
+ key: operationKey(node, "param", "headers"),
1553
+ name
1554
+ });
1555
+ },
1556
+ cookies(node) {
1557
+ const name = this.name(`${node.operationId} Cookies`);
1558
+ return this.resolveName({
1559
+ key: operationKey(node, "param", "cookies"),
1560
+ name
1561
+ });
1562
+ }
1563
+ };
1567
1564
  }
1568
1565
  /**
1569
- * TypeScript type printer built with `definePrinter`.
1570
- *
1571
- * Converts a `SchemaNode` AST node into TypeScript source:
1572
- * - **`printer.print(node)`** — when `options.name` is set, returns a full
1573
- * `type Name = …` or `interface Name { … }` declaration.
1574
- * Without `name`, returns the type for the schema, such as `string | null`.
1575
- *
1576
- * Dispatches on `node.type` to the appropriate handler in `nodes`. Options are closed
1577
- * over per printer instance, so each call to `printerTs(options)` produces an independent printer.
1578
- *
1579
- * @example Raw type node (no `typeName`)
1580
- * ```ts
1581
- * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' } })
1582
- * const type = printer.print(schemaNode) // 'string | null'
1583
- * ```
1566
+ * Builds the shared `response` namespace. Spread the result into
1567
+ * `createResolver` and add plugin-specific methods (`options`, `error`) next
1568
+ * to it.
1584
1569
  *
1585
- * @example Full declaration (with `typeName`)
1570
+ * @example
1586
1571
  * ```ts
1587
- * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' }, typeName: 'MyType' })
1588
- * const declaration = printer.print(schemaNode) // 'export type MyType = …'
1572
+ * createResolver<PluginTs>({ response: { ...createOperationResponseResolver(), options(node) {...} }, ... })
1589
1573
  * ```
1590
1574
  */
1591
- const printerTs = ast.createPrinter((options) => {
1592
- const addsUndefined = options.optionalType === "undefined" || options.optionalType === "questionTokenAndUndefined";
1575
+ function createOperationResponseResolver() {
1593
1576
  return {
1594
- name: "typescript",
1595
- options,
1596
- nodes: {
1597
- any: () => keywordTypeNodes.any,
1598
- unknown: () => keywordTypeNodes.unknown,
1599
- void: () => keywordTypeNodes.void,
1600
- never: () => keywordTypeNodes.never,
1601
- boolean: () => keywordTypeNodes.boolean,
1602
- null: () => keywordTypeNodes.null,
1603
- blob: () => createTypeReferenceNode("Blob"),
1604
- string: () => keywordTypeNodes.string,
1605
- uuid: () => keywordTypeNodes.string,
1606
- email: () => keywordTypeNodes.string,
1607
- url: (node) => {
1608
- if (node.path) return createUrlTemplateType(node.path);
1609
- return keywordTypeNodes.string;
1610
- },
1611
- ipv4: () => keywordTypeNodes.string,
1612
- ipv6: () => keywordTypeNodes.string,
1613
- datetime: () => keywordTypeNodes.string,
1614
- number: () => keywordTypeNodes.number,
1615
- integer: () => keywordTypeNodes.number,
1616
- bigint: () => keywordTypeNodes.bigint,
1617
- date: dateOrStringNode,
1618
- time: dateOrStringNode,
1619
- ref(node) {
1620
- if (!node.name) return null;
1621
- const refName = ast.resolveRefName(node);
1622
- if (!refName) return null;
1623
- return createTypeReferenceNode(node.ref ? enumTypeName({
1624
- resolver: this.options.resolver,
1625
- name: refName,
1626
- enumOptions: this.options.enum,
1627
- isEnum: !!this.options.enumSchemaNames?.has(refName)
1628
- }) : refName);
1629
- },
1630
- enum(node) {
1631
- const values = ast.getSchemaLiteralValues(node);
1632
- if (values.some((value) => value !== null && typeof value === "object")) return createUnionDeclaration({ nodes: values.map((value) => constToTypeNode(value)) });
1633
- if (this.options.enum.type === "inlineLiteral" || !node.name || isInlineConstEnum(node, this.options.enumSchemaNames)) return createUnionDeclaration({ nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(isNonNullable) });
1634
- return createTypeReferenceNode(enumTypeName({
1635
- resolver: this.options.resolver,
1636
- name: node.name,
1637
- enumOptions: this.options.enum,
1638
- isEnum: true
1639
- }));
1640
- },
1641
- union(node) {
1642
- const members = node.members ?? [];
1643
- const hasStringLiteral = members.some((m) => {
1644
- return ast.narrowSchema(m, ast.schemaTypes.enum)?.primitive === "string";
1645
- });
1646
- const hasPlainString = members.some((m) => isStringType(m));
1647
- if (hasStringLiteral && hasPlainString) return createUnionDeclaration({ nodes: members.map((m) => {
1648
- if (isStringType(m)) return createIntersectionDeclaration({ nodes: [keywordTypeNodes.string, createTypeLiteralNode([])] });
1649
- return this.transform(m);
1650
- }).filter(isNonNullable) });
1651
- return createUnionDeclaration({ nodes: buildMemberNodes(members, this.transform) });
1652
- },
1653
- intersection(node) {
1654
- return createIntersectionDeclaration({ nodes: buildMemberNodes(node.members, this.transform) });
1655
- },
1656
- array(node) {
1657
- return createArrayDeclaration({
1658
- nodes: mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(isNonNullable),
1659
- arrayType: this.options.arrayType
1660
- });
1661
- },
1662
- tuple(node) {
1663
- return buildTupleNode(node, this.transform);
1664
- },
1665
- object(node) {
1666
- const { transform, options } = this;
1667
- const keySchema = node.propertyNames && syncSchemaRef(node.propertyNames);
1668
- if (!node.properties.length && !Object.keys(node.patternProperties ?? {}).length && node.propertyNames && keySchema?.type === "enum" && node.additionalProperties !== false) {
1669
- const keyType = transform(node.propertyNames) ?? keywordTypeNodes.string;
1670
- const valueSchema = typeof node.additionalProperties === "object" ? node.additionalProperties : ast.factory.createSchema({ type: "unknown" });
1671
- return createTypeReferenceNode("Partial", [createTypeReferenceNode("Record", [keyType, buildPropertyType(valueSchema, transform(valueSchema) ?? keywordTypeNodes.unknown, options.optionalType, false)])]);
1672
- }
1673
- const addsQuestionToken = options.optionalType === "questionToken" || options.optionalType === "questionTokenAndUndefined";
1674
- const propertyNodes = mapSchemaProperties(node, (schema) => transform(schema)).map(({ name, property, output }) => {
1675
- const baseType = output ?? keywordTypeNodes.unknown;
1676
- const optional = !property.required || !!property.schema.optional || !!property.schema.nullish;
1677
- const type = buildPropertyType(property.schema, baseType, options.optionalType, optional);
1678
- const propMeta = syncSchemaRef(property.schema);
1679
- const propertyNode = createPropertySignature({
1680
- questionToken: optional ? addsQuestionToken : false,
1681
- name,
1682
- type,
1683
- readOnly: propMeta?.readOnly
1684
- });
1685
- const binaryRemark = isBinarySchema(property.schema) ? "@remarks Pass a `File` to send its filename. A plain `Blob` is sent with the filename `blob`." : void 0;
1686
- return appendJSDocToNode({
1687
- node: propertyNode,
1688
- comments: [...buildPropertyJSDocComments(property.schema, optional), binaryRemark]
1689
- });
1690
- });
1691
- const allElements = [...propertyNodes, ...buildIndexSignatures(node, propertyNodes.length, transform)];
1692
- if (!allElements.length) return keywordTypeNodes.object;
1693
- return createTypeLiteralNode(allElements);
1694
- },
1695
- ...toNodeHandlers(options.nodes)
1577
+ status(node, statusCode) {
1578
+ const name = this.name(`${node.operationId} Status ${statusCode}`);
1579
+ return this.resolveName({
1580
+ key: operationKey(node, "response", "status", statusCode),
1581
+ name
1582
+ });
1696
1583
  },
1697
- print(node) {
1698
- const { name, syntaxType = "type", description, keysToOmit } = this.options;
1699
- const transformed = this.transform(node);
1700
- if (!transformed) return null;
1701
- const meta = syncSchemaRef(node);
1702
- if (!name) return print(wrapNullableOptional(keysToOmit?.length ? createOmitDeclaration({
1703
- keys: keysToOmit,
1704
- type: transformed,
1705
- nonNullable: true
1706
- }) : transformed, !!meta.nullable, !!(meta.nullish || meta.optional) && addsUndefined));
1707
- return createTypeDeclaration({
1708
- name,
1709
- isExportable: true,
1710
- type: (() => {
1711
- return wrapNullableOptional(keysToOmit?.length ? createOmitDeclaration({
1712
- keys: keysToOmit,
1713
- type: transformed,
1714
- nonNullable: true
1715
- }) : transformed, !!meta.nullable, !!(meta.nullish || meta.optional));
1716
- })(),
1717
- syntax: syntaxType === "type" || !!keysToOmit?.length ? "type" : "interface",
1718
- comments: buildPropertyJSDocComments({
1719
- ...meta,
1720
- description
1721
- })
1584
+ headers(node, statusCode) {
1585
+ const name = this.name(`${node.operationId} Status ${statusCode} Headers`);
1586
+ return this.resolveName({
1587
+ key: operationKey(node, "response", "headers", statusCode),
1588
+ name
1589
+ });
1590
+ },
1591
+ body(node) {
1592
+ const name = this.name(`${node.operationId} Body`);
1593
+ return this.resolveName({
1594
+ key: operationKey(node, "response", "body"),
1595
+ name
1596
+ });
1597
+ },
1598
+ responses(node) {
1599
+ const name = this.name(`${node.operationId} Responses`);
1600
+ return this.resolveName({
1601
+ key: operationKey(node, "response", "responses"),
1602
+ name
1603
+ });
1604
+ },
1605
+ response(node) {
1606
+ const name = this.name(`${node.operationId} Response`);
1607
+ return this.resolveName({
1608
+ key: operationKey(node, "response", "response"),
1609
+ name
1722
1610
  });
1723
1611
  }
1724
1612
  };
1725
- });
1726
- // src/components/Type.tsx
1727
- function Type({ name, node, printer, enum: enumOptions, resolver, inline }) {
1728
- if (inline) {
1729
- const withoutEnums = ast.transform(node, { schema: (n) => n.type === ast.schemaTypes.enum && n.name ? {
1730
- ...n,
1731
- name: void 0
1732
- } : void 0 });
1733
- const expression = printerTs({
1734
- ...printer.options,
1735
- name: void 0
1736
- }).print(withoutEnums)?.replace(/\/\*\*[\s\S]*?\*\/\s*/g, "");
1737
- if (!expression) return;
1738
- return /* @__PURE__ */ jsx(File.Source, {
1739
- name,
1740
- isTypeOnly: true,
1741
- isExportable: true,
1742
- isIndexable: true,
1743
- inline: { expression },
1744
- children: expression
1745
- });
1746
- }
1747
- const enumSchemaNodes = ast.collectSync(node, { schema(n) {
1748
- const enumNode = ast.narrowSchema(n, ast.schemaTypes.enum);
1749
- if (enumNode?.name && !isInlineConstEnum(enumNode, printer.options.enumSchemaNames)) return enumNode;
1750
- } });
1751
- const output = printer.print(node);
1752
- const printerImports = printer.drainImports();
1753
- if (!output) return;
1754
- const enums = [...new Map(enumSchemaNodes.map((n) => [n.name, n])).values()].map((node) => {
1755
- return {
1756
- node,
1757
- ...getEnumNames$1({
1758
- node,
1759
- enum: enumOptions,
1760
- resolver
1761
- })
1762
- };
1763
- });
1764
- const shouldExportEnums = enumOptions.type !== "inlineLiteral";
1765
- const shouldExportType = enumOptions.type === "inlineLiteral" || enums.every((item) => item.typeName !== name);
1766
- return /* @__PURE__ */ jsxs(Fragment, { children: [shouldExportEnums && enums.map(({ node }) => /* @__PURE__ */ jsx(Enum, {
1767
- node,
1768
- enum: enumOptions,
1769
- resolver
1770
- }, node.name)), shouldExportType && /* @__PURE__ */ jsxs(Fragment, { children: [printerImports.map((imp) => /* @__PURE__ */ jsx(File.Import, { ...imp }, [
1771
- name,
1772
- imp.path,
1773
- imp.name
1774
- ].join("-"))), /* @__PURE__ */ jsx(File.Source, {
1775
- name,
1776
- pointer: node.pointer,
1777
- isTypeOnly: true,
1778
- isExportable: true,
1779
- isIndexable: true,
1780
- children: output
1781
- })] })] });
1613
+ }
1614
+ /**
1615
+ * Builds a resolver `file` override whose base name runs every path segment
1616
+ * through `toFilePath`, casing the final segment with `caseLast`.
1617
+ *
1618
+ * @example
1619
+ * ```ts
1620
+ * createResolver<PluginTs>({ file: createCasedFile(pascalCase), ... })
1621
+ * ```
1622
+ */
1623
+ function createCasedFile(caseLast) {
1624
+ return { baseName({ name, extname }) {
1625
+ return this.memo(`file:${name}\0${extname}`, () => `${toFilePath(name, caseLast)}${extname}`);
1626
+ } };
1627
+ }
1628
+ /**
1629
+ * Names of the schemas whose type is `enum`, computed once per build.
1630
+ */
1631
+ function getEnumNames(ctx) {
1632
+ return ctx.memo("enumNames:set", () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.type === "enum" && schema.name ? [schema.name] : [])));
1782
1633
  }
1783
1634
  // src/generators/typeGenerator.tsx
1784
1635
  /**
@@ -1814,7 +1665,7 @@ const typeGenerator = defineGenerator({
1814
1665
  group: group ?? void 0,
1815
1666
  name: importName
1816
1667
  });
1817
- const enumNode = ast.narrowSchema(node, ast.schemaTypes.enum);
1668
+ const enumNode = ast.schema.narrow(node, ast.schema.types.enum);
1818
1669
  const isEnumSchema = !!enumNode && !isInlineConstEnum(enumNode, enumSchemaNames);
1819
1670
  const meta = {
1820
1671
  name: enumOptions.type === "asConst" && isEnumSchema ? resolver.enum.keyName(node, enumOptions.typeSuffix) : resolver.name(node.name),
@@ -1903,7 +1754,7 @@ const typeGenerator = defineGenerator({
1903
1754
  enumSchemaNames,
1904
1755
  nodes: printer?.nodes
1905
1756
  }) : null;
1906
- function renderSchemaType({ schema, name, keysToOmit }) {
1757
+ function renderSchemaType({ schema, name, keysToOmit, examples, comments }) {
1907
1758
  if (!schema) return null;
1908
1759
  if (!inline && enumOptions.type !== "inlineLiteral") schema = resolveInlineEnumNames({
1909
1760
  node: schema,
@@ -1925,6 +1776,8 @@ const typeGenerator = defineGenerator({
1925
1776
  name,
1926
1777
  syntaxType,
1927
1778
  description: schema.description,
1779
+ examples: getExampleValues(examples, true),
1780
+ comments,
1928
1781
  keysToOmit,
1929
1782
  resolver,
1930
1783
  enumSchemaNames,
@@ -1954,7 +1807,7 @@ const typeGenerator = defineGenerator({
1954
1807
  * variants by content type in {@link buildResponses}; the standalone alias stays a plain union so
1955
1808
  * query hooks and `result.data` keep the bare body.
1956
1809
  */
1957
- function buildContentTypeVariants(entries, baseName, pointer, decorate) {
1810
+ function buildContentTypeVariants(entries, baseName, pointer, { decorate, comments }) {
1958
1811
  const variants = resolveContentTypeVariants(entries, baseName);
1959
1812
  const keys = variants.map((variant) => inlinePrinter ? `${inlinePrinter.print(variant.schema)}:${variant.keysToOmit ?? ""}` : variant.name);
1960
1813
  const members = variants.filter((_, index) => keys.indexOf(keys[index]) === index);
@@ -1969,20 +1822,25 @@ const typeGenerator = defineGenerator({
1969
1822
  return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaType({
1970
1823
  schema: decorate ? decorate(variant.schema) : variant.schema,
1971
1824
  name: variant.name,
1972
- keysToOmit: variant.keysToOmit
1825
+ keysToOmit: variant.keysToOmit,
1826
+ examples: entries.find((entry) => entry.contentType === variant.contentType)?.examples
1973
1827
  })), renderSchemaType({
1974
1828
  schema: unionSchema,
1975
- name: baseName
1829
+ name: baseName,
1830
+ comments
1976
1831
  })] });
1977
1832
  }
1978
- const { path, query, header, cookie } = getOperationParameters(node);
1833
+ const { path, query, header, cookie } = ast.http.groupParameters(node);
1979
1834
  const paramGroupTypes = [
1980
1835
  [path, resolver.param.path],
1981
1836
  [query, resolver.param.query],
1982
1837
  [header, resolver.param.headers],
1983
1838
  [cookie, resolver.param.cookies]
1984
1839
  ].map(([params, resolveName]) => params.length > 0 && renderSchemaType({
1985
- schema: buildParams({ params }),
1840
+ schema: buildParams({
1841
+ params,
1842
+ serializedExamples: true
1843
+ }),
1986
1844
  name: resolveName.call(resolver.param, node, params[0])
1987
1845
  }));
1988
1846
  const requestBodyContent = node.requestBody?.content ?? [];
@@ -1998,19 +1856,20 @@ const typeGenerator = defineGenerator({
1998
1856
  description: node.requestBody.description ?? entry.schema.description
1999
1857
  },
2000
1858
  name: resolver.response.body(node),
2001
- keysToOmit: entry.keysToOmit
1859
+ keysToOmit: entry.keysToOmit,
1860
+ examples: entry.examples
2002
1861
  });
2003
1862
  }
2004
- return buildContentTypeVariants(requestBodyContent, resolver.response.body(node), node.requestBody.pointer, (schema) => ({
1863
+ return buildContentTypeVariants(requestBodyContent, resolver.response.body(node), node.requestBody.pointer, { decorate: (schema) => ({
2005
1864
  ...schema,
2006
1865
  description: node.requestBody.description ?? schema.description
2007
- }));
1866
+ }) });
2008
1867
  }
2009
1868
  const requestType = buildRequestType();
2010
1869
  const responseTypes = node.responses.map((res) => {
2011
1870
  const content = (res.content ?? []).map((entry, _, content) => toResponseContent(entry, content));
2012
1871
  const variants = content.filter((entry) => entry.schema);
2013
- if (variants.length > 1) return buildContentTypeVariants(variants, resolver.response.status(node, res.statusCode), res.pointer);
1872
+ if (variants.length > 1) return buildContentTypeVariants(variants, resolver.response.status(node, res.statusCode), res.pointer, { comments: buildResponseComments(res) });
2014
1873
  const primary = variants[0] ?? content[0];
2015
1874
  return renderSchemaType({
2016
1875
  schema: primary?.schema ? {
@@ -2018,22 +1877,34 @@ const typeGenerator = defineGenerator({
2018
1877
  pointer: res.pointer ?? primary.schema.pointer
2019
1878
  } : ast.factory.createSchema({ type: "unknown" }),
2020
1879
  name: resolver.response.status(node, res.statusCode),
2021
- keysToOmit: primary?.keysToOmit
1880
+ keysToOmit: primary?.keysToOmit,
1881
+ examples: primary?.examples,
1882
+ comments: buildResponseComments(res)
2022
1883
  });
2023
1884
  });
1885
+ const responseHeaderTypes = node.responses.map((res) => res.headers?.length ? renderSchemaType({
1886
+ schema: buildParams({
1887
+ params: res.headers,
1888
+ serializedExamples: true
1889
+ }),
1890
+ name: resolver.response.headers(node, res.statusCode)
1891
+ }) : null);
2024
1892
  const optionsType = renderSchemaType({
2025
1893
  schema: buildOptionsSchema(node, resolver, { pairContentTypes: true }),
2026
1894
  name: resolver.response.options(node)
2027
1895
  });
2028
1896
  const responsesType = renderSchemaType({
2029
- schema: buildResponses(node, resolver),
1897
+ schema: buildResponses(node, resolver, { streamData: ast.factory.createSchema({
1898
+ type: "ref",
1899
+ name: "ReadableStream<Uint8Array>"
1900
+ }) }),
2030
1901
  name: resolver.response.responses(node)
2031
1902
  });
2032
1903
  function buildResponseType() {
2033
1904
  const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
2034
1905
  const responseName = resolver.response.response(node);
2035
1906
  const responsesWithSchema = node.responses.filter(hasSchema);
2036
- if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? ast.collectImportedRefNames(entry.schema).map(importName) : []))).has(responseName)) return null;
1907
+ if (new Set(ast.http.responses.getImportedNames(responsesWithSchema).map(importName)).has(responseName)) return null;
2037
1908
  return renderSchemaType({
2038
1909
  schema: {
2039
1910
  ...buildResponseUnion(node, { resolver }) ?? ast.factory.createSchema({ type: "unknown" }),
@@ -2066,6 +1937,7 @@ const typeGenerator = defineGenerator({
2066
1937
  children: [
2067
1938
  paramGroupTypes,
2068
1939
  responseTypes,
1940
+ responseHeaderTypes,
2069
1941
  requestType,
2070
1942
  optionsType,
2071
1943
  responsesType,
@@ -2076,11 +1948,11 @@ const typeGenerator = defineGenerator({
2076
1948
  });
2077
1949
  // kubb-extension.json
2078
1950
  var compatibility = {
2079
- "kubb": ">=5.16.0",
1951
+ "kubb": ">=5.18.4",
2080
1952
  "node": ">=22.22"
2081
1953
  };
2082
1954
  // package.json
2083
- var version = "5.5.0-canary.20261111T115959";
1955
+ var version = "5.5.0";
2084
1956
  // src/resolvers/resolverTs.ts
2085
1957
  /**
2086
1958
  * Default resolver used by `@kubb/plugin-ts`. Decides the names and file paths
@@ -2103,9 +1975,9 @@ var version = "5.5.0-canary.20261111T115959";
2103
1975
  const resolverTs = createResolver({
2104
1976
  pluginName: "plugin-ts",
2105
1977
  name(name) {
2106
- return this.memo(`name:${name}`, () => ensureValidVarName(pascalCase(name)));
1978
+ return this.memo(`name:${name}`, () => ensureValidVarName(casing.pascal(name)));
2107
1979
  },
2108
- file: createCasedFile(pascalCase),
1980
+ file: createCasedFile(casing.pascal),
2109
1981
  param: createOperationParamResolver(),
2110
1982
  response: {
2111
1983
  ...createOperationResponseResolver(),
@@ -2178,7 +2050,7 @@ const pluginTs = defineExtension({
2178
2050
  include,
2179
2051
  override,
2180
2052
  optionalType,
2181
- group: createGroupConfig(group),
2053
+ group: group ?? null,
2182
2054
  arrayType,
2183
2055
  enum: resolvedEnum,
2184
2056
  syntaxType,