@kubb/plugin-client 5.12.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/client-C-0-puI2.d.ts +554 -0
  3. package/dist/index.cjs +1377 -784
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +117 -78
  6. package/dist/index.d.ts +117 -78
  7. package/dist/index.js +1375 -786
  8. package/dist/index.js.map +1 -1
  9. package/dist/templates/client.d.ts +3 -2
  10. package/dist/templates/core.d.ts +3 -3
  11. package/dist/templates/mediaTypes/formData.d.ts +5 -0
  12. package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
  13. package/dist/templates/mediaTypes.d.ts +2 -0
  14. package/dist/templates/serializers/cookies.d.ts +5 -0
  15. package/dist/templates/serializers/headers.d.ts +5 -0
  16. package/dist/templates/serializers/path.d.ts +5 -0
  17. package/dist/templates/serializers/pathStyles.d.ts +5 -0
  18. package/dist/templates/serializers/query.d.ts +5 -0
  19. package/dist/templates/serializers/queryStyles.d.ts +14 -0
  20. package/dist/templates/serializers.d.ts +118 -2
  21. package/dist/templates/sse.d.ts +20 -113
  22. package/dist/templates/standardSchema.d.ts +81 -2
  23. package/docs/guides/authentication.md +12 -2
  24. package/docs/guides/calling-operations.md +8 -6
  25. package/docs/guides/migrate-to-v6.md +139 -0
  26. package/docs/guides/validate-requests-and-responses.md +13 -8
  27. package/docs/guides/write-your-client.md +118 -12
  28. package/docs/options.md +5 -3
  29. package/docs/overview.md +3 -1
  30. package/kubb-extension.json +21 -3
  31. package/package.json +6 -5
  32. package/src/builders/callConfig.ts +80 -0
  33. package/src/builders/contentType.ts +35 -0
  34. package/src/builders/generics.ts +29 -0
  35. package/src/builders/operationBody.ts +43 -0
  36. package/src/builders/operationData.ts +64 -0
  37. package/src/builders/returnStatement.ts +103 -0
  38. package/src/builders/security.ts +125 -0
  39. package/src/builders/signature.ts +71 -0
  40. package/src/builders/styles.ts +94 -0
  41. package/src/builders/validator.ts +84 -0
  42. package/src/builders/validatorOptions.ts +53 -0
  43. package/src/clientTypes.ts +195 -0
  44. package/src/components/ClientRuntime.tsx +97 -0
  45. package/src/components/Operation.tsx +72 -0
  46. package/src/components/SdkClient.tsx +100 -0
  47. package/src/components/SdkFacade.tsx +35 -0
  48. package/src/generators/clientGenerator.tsx +91 -6
  49. package/src/generators/sdkGenerator.tsx +208 -0
  50. package/src/index.ts +3 -1
  51. package/src/macros.ts +9 -0
  52. package/src/plugin.ts +7 -8
  53. package/src/resolver.ts +28 -0
  54. package/src/setup.ts +90 -0
  55. package/src/types.ts +4 -2
  56. package/templates/client.ts +2 -1
  57. package/templates/core.ts +735 -286
  58. package/templates/mediaTypes/formData.ts +59 -0
  59. package/templates/mediaTypes/urlencoded.ts +33 -0
  60. package/templates/mediaTypes.ts +147 -0
  61. package/templates/serializers/cookies.ts +58 -0
  62. package/templates/serializers/headers.ts +35 -0
  63. package/templates/serializers/path.ts +19 -0
  64. package/templates/serializers/pathStyles.ts +27 -0
  65. package/templates/serializers/query.ts +21 -0
  66. package/templates/serializers/queryStyles.ts +107 -0
  67. package/templates/serializers.ts +48 -364
  68. package/templates/sse.ts +87 -178
  69. package/templates/standardSchema.ts +16 -37
  70. package/dist/client-BxO6Dygu.d.ts +0 -355
  71. package/dist/serializers-Ds8zZwOO.d.ts +0 -190
  72. package/dist/standardSchema-BShJWz2i.d.ts +0 -104
package/dist/index.js CHANGED
@@ -1,287 +1,17 @@
1
- import { Diagnostics, Resolver, Url, addGenerator, ast, camelCase, createResolver, defineExtension, defineGenerator, macroDedupeUnion, pascalCase, setMacros, setResolver } from "@kubb/kit";
1
+ import { Diagnostics, Resolver, addGenerator, ast, casing, createResolver, defineExtension, defineGenerator, injectFile, macroDedupeUnion, memo, setMacros, setResolver, text, useConfig } from "@kubb/kit";
2
2
  import path from "node:path";
3
3
  import { createFunctionParameter, createFunctionParameters, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
4
- import { pluginZodName } from "@kubb/plugin-zod";
5
4
  import { File, Function, jsxRenderer } from "@kubb/renderer-jsx";
5
+ import { pluginZodName as pluginZodName$1 } from "@kubb/plugin-zod";
6
6
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
7
- // ../../internals/shared/src/operation.ts
8
- /**
9
- * Builds the `ResolverFileParams` every operation generator passes to
10
- * `resolver.file`: a file named `name`, tagged by the operation's first
11
- * tag (or `'default'`), at the operation's path. Centralizes the entry object
12
- * that was repeated at dozens of call sites across the client and query plugins.
13
- *
14
- * @example
15
- * ```ts
16
- * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
17
- * ```
18
- */
19
- function operationFileEntry(node, name, extname = ".ts") {
20
- return {
21
- name,
22
- extname,
23
- tag: node.tags[0] ?? "default",
24
- path: node.path
25
- };
26
- }
27
- /**
28
- * Resolves a dependency plugin's generated file for `node.operationId`, cached in `memo` (the
29
- * build's `ctx.memo`) under the resolver's own plugin name and the operation. Several dependents reading the
30
- * same dependency for the same operation in one pass (a query plugin's several hook generators, the
31
- * MCP handler, ...) share one computed name and path instead of each calling `resolver.file` again.
32
- *
33
- * @example Cache `plugin-ts`'s file for the current operation
34
- * ```ts
35
- * const fileTs = resolveDependencyOperationFile({ memo: ctx.memo, node, resolver: tsResolver, root, output })
36
- * ```
37
- */
38
- function resolveDependencyOperationFile(options) {
39
- const { memo, node, resolver, root, output, group } = options;
40
- return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => resolver.file({
41
- ...operationFileEntry(node, node.operationId),
42
- root,
43
- output,
44
- group: group ?? void 0
45
- }));
46
- }
47
- function getOperationLink(node, link) {
48
- if (!link) return null;
49
- if (typeof link === "function") return link(node) ?? null;
50
- return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
51
- }
52
- /**
53
- * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
54
- * are present and the union, default, and form-data flags the client uses to pick one.
55
- */
56
- function buildContentTypeInfo(contentTypes) {
57
- const isMultipleContentTypes = contentTypes.length > 1;
58
- return {
59
- contentTypes,
60
- isMultipleContentTypes,
61
- contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
62
- defaultContentType: contentTypes[0] ?? "application/json",
63
- hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
64
- };
65
- }
66
- function getContentTypeInfo(node) {
67
- return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
68
- }
69
- /**
70
- * The request-body counterpart for the primary success response: the content types it documents and
71
- * whether several are present, so the client can let a caller pick which one to accept.
72
- */
73
- function getResponseContentTypeInfo(node) {
74
- return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
75
- }
76
- /**
77
- * Reads the single base content type of an operation's primary success response, lowercased and
78
- * stripped of any `; charset=...` suffix. Returns `undefined` when the response declares zero or
79
- * more than one content type, since neither case has a single type to act on.
80
- */
81
- function getPrimarySuccessContentType(node) {
82
- const contentTypes = getPrimarySuccessResponse(node)?.content?.map((entry) => entry.contentType) ?? [];
83
- if (contentTypes.length !== 1) return void 0;
84
- return contentTypes[0].split(";")[0].trim().toLowerCase();
85
- }
86
- /**
87
- * The `text/event-stream` content of the primary success response, when it is the only content type. A response that also
88
- * declares another type stays a normal call with its query hooks.
89
- */
90
- function getEventStreamContent(node) {
91
- return getPrimarySuccessContentType(node) === "text/event-stream" ? getPrimarySuccessResponse(node)?.content?.[0] : void 0;
92
- }
93
- /**
94
- * Whether an operation streams its primary success response as Server-Sent Events (`text/event-stream`). The client
95
- * generator uses this to return a typed event stream instead of a one-shot `RequestResult`.
96
- */
97
- function isEventStream(node) {
98
- return getEventStreamContent(node) !== void 0;
99
- }
100
- /**
101
- * How the document types a `text/event-stream` response, as the adapter read it from `itemSchema`: `event` when each item is
102
- * the parsed event with its own `event` and `data`, `data` when it is only the payload. Returns `null` without an event stream.
103
- */
104
- function getEventStream(node) {
105
- const content = getEventStreamContent(node);
106
- return content ? content.stream?.shape ?? "data" : null;
107
- }
108
- function baseContentType(contentType) {
109
- return contentType.split(";")[0].trim().toLowerCase();
110
- }
111
- function isJsonContentType(baseType) {
112
- return baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json";
113
- }
114
- /**
115
- * Parser for one response content entry.
116
- *
117
- * JSON stays unset so the runtime detects it. `text/event-stream` is a stream. A binary schema
118
- * (`format: binary` / `contentMediaType`) is a blob even when the media type is not a well-known
119
- * binary type. Other `text/*` is text, and `application/octet-stream`, `application/pdf`,
120
- * `image/*`, `audio/*`, and `video/*` are blobs.
121
- */
122
- function responseTypeForContent(contentType, schema) {
123
- const baseType = baseContentType(contentType);
124
- if (!baseType) return void 0;
125
- if (isJsonContentType(baseType)) return void 0;
126
- if (baseType === "text/event-stream") return "stream";
127
- if (schema?.type === "blob") return "blob";
128
- if (baseType.startsWith("text/")) return "text";
129
- if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
130
- }
131
- /**
132
- * Derives the default `responseType` string for an operation from its primary success response.
133
- *
134
- * One content type uses {@link responseTypeForContent}. Several content types that all use the same
135
- * parser collapse to that string, so a call can keep `responseType: 'blob'`. Mixed parsers return
136
- * `undefined`; {@link getResponseTypeMap} carries the per-type choice.
137
- */
138
- function getResponseType(node) {
139
- const entries = getPrimarySuccessResponse(node)?.content ?? [];
140
- if (entries.length === 0) return void 0;
141
- const responseTypes = entries.map((entry) => responseTypeForContent(entry.contentType, entry.schema));
142
- const first = responseTypes[0];
143
- if (!first) return void 0;
144
- if (responseTypes.every((responseType) => responseType === first)) return first;
145
- }
146
- /**
147
- * Maps each base content type of a primary success response to its parser when those parsers differ.
148
- *
149
- * Keys are lowercased and stripped of parameters. JSON entries are kept as `'json'` so a catch-all
150
- * entry cannot claim a JSON response. Returns `undefined` when {@link getResponseType} already
151
- * covers the response with one string, or when no content type needs a parser other than JSON.
152
- * The runtime matches the response `Content-Type` against this map instead of guessing from the media type.
153
- */
154
- function getResponseTypeMap(node) {
155
- if (getResponseType(node)) return void 0;
156
- const entries = getPrimarySuccessResponse(node)?.content ?? [];
157
- if (entries.length <= 1) return void 0;
158
- const map = {};
159
- for (const entry of entries) {
160
- const baseType = baseContentType(entry.contentType);
161
- const responseType = responseTypeForContent(entry.contentType, entry.schema) ?? (isJsonContentType(baseType) ? "json" : void 0);
162
- if (baseType && responseType) map[baseType] = responseType;
163
- }
164
- return Object.values(map).some((responseType) => responseType !== "json") ? map : void 0;
165
- }
166
- /**
167
- * Which of the grouped request options an operation carries.
168
- */
169
- function getRequestGroups(node) {
170
- const { path, query, header } = getOperationParameters(node);
171
- return {
172
- path: path.length > 0,
173
- query: query.length > 0,
174
- body: Boolean(node.requestBody?.content?.[0]?.schema),
175
- headers: header.length > 0
176
- };
177
- }
178
- /**
179
- * Resolves which grouped request options an operation carries together with whether each group
180
- * holds a required member. The grouped parameter stays optional only when nothing inside it is
181
- * required, matching the generated `RequestConfig` type.
182
- */
183
- function getRequestGroupOptionality(node) {
184
- const groups = getRequestGroups(node);
185
- const { path, query, header } = getOperationParameters(node);
186
- const hasRequiredPath = path.some((param) => param.required);
187
- const hasRequiredQuery = query.some((param) => param.required);
188
- const hasRequiredHeader = header.some((param) => param.required);
189
- return {
190
- groups,
191
- hasRequiredPath,
192
- hasRequiredQuery,
193
- hasRequiredHeader,
194
- isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !(groups.body && node.requestBody?.required === true)
195
- };
196
- }
197
- function buildOperationComments(node, options = {}) {
198
- const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
199
- const linkComment = getOperationLink(node, link);
200
- const filteredComments = (linkPosition === "beforeDeprecated" ? [
201
- node.description && `@description ${node.description}`,
202
- node.summary && `@summary ${node.summary}`,
203
- linkComment,
204
- node.deprecated && "@deprecated"
205
- ] : [
206
- node.description && `@description ${node.description}`,
207
- node.summary && `@summary ${node.summary}`,
208
- node.deprecated && "@deprecated",
209
- linkComment
210
- ]).filter((comment) => Boolean(comment));
211
- if (!splitLines) return filteredComments;
212
- return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
213
- }
214
- const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
215
- /**
216
- * An OpenAPI 3.2 `querystring` parameter is the whole query string, so a form-encoded object becomes one query parameter
217
- * per property, typed and serialized like any other.
218
- * Another content type, such as JSON, stays one parameter named after it, and the runtime sends its value as the whole query string.
219
- */
220
- function spreadQuerystring(param) {
221
- const schema = param.schema.type === "ref" ? param.schema.schema : param.schema;
222
- const formEncoded = !param.contentType || param.contentType.split(";")[0].trim().toLowerCase() === "application/x-www-form-urlencoded";
223
- if (schema?.type !== "object" || !formEncoded) return [param];
224
- return schema.properties.map((property) => ast.factory.createParameter({
225
- name: property.name,
226
- in: "query",
227
- schema: property.schema,
228
- required: param.required && property.required
229
- }));
230
- }
231
- /**
232
- * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
233
- * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
234
- * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
235
- * same parameters once per plugin.
236
- */
237
- function getOperationParameters(node) {
238
- const cached = operationParameterGroupsByNode.get(node);
239
- if (cached) return cached;
240
- const dedupe = (params) => {
241
- const seen = /* @__PURE__ */ new Set();
242
- return params.filter((param) => {
243
- if (seen.has(param.name)) return false;
244
- seen.add(param.name);
245
- return true;
246
- });
247
- };
248
- const group = (location) => dedupe(node.parameters.filter((param) => param.in === location));
249
- const groups = {
250
- path: group("path"),
251
- query: dedupe([...group("query"), ...group("querystring").flatMap(spreadQuerystring)]),
252
- header: group("header"),
253
- cookie: group("cookie")
254
- };
255
- operationParameterGroupsByNode.set(node, groups);
256
- return groups;
257
- }
258
- /**
259
- * Builds the combined `{ body, path, query, headers }` options object schema for an operation,
260
- * referencing the already-resolved body and grouped param names. Shared by `@kubb/plugin-ts`'s
261
- * `Options` type and `@kubb/plugin-zod`'s inferred options schema, so both printers emit the same
262
- * shape from the same inputs. `primitive: 'object'` is a no-op for the TS printer and tells the Zod
263
- * printer to emit `z.object(…)` rather than a record.
264
- */
265
- function hasRequestContentTypeChoice(node) {
266
- return (node.requestBody?.content ?? []).filter((entry) => entry.schema).length > 1;
267
- }
268
- function getStatusCodeNumber(statusCode) {
269
- const code = Number(statusCode);
270
- return Number.isNaN(code) ? null : code;
271
- }
272
- function isSuccessStatusCode(statusCode) {
273
- const code = getStatusCodeNumber(statusCode);
274
- return code !== null && code >= 200 && code < 300;
275
- }
276
- function getSuccessResponses(responses) {
277
- return responses.filter((response) => isSuccessStatusCode(response.statusCode));
278
- }
279
- function getOperationSuccessResponses(node) {
280
- return getSuccessResponses(node.responses);
281
- }
282
- function getPrimarySuccessResponse(node) {
283
- return getOperationSuccessResponses(node)[0] ?? null;
284
- }
7
+ import { fileURLToPath } from "node:url";
8
+ // kubb-extension.json
9
+ var compatibility = {
10
+ "kubb": ">=5.18.4",
11
+ "node": ">=22.22"
12
+ };
13
+ // package.json
14
+ var version = "6.0.1";
285
15
  // ../../internals/utils/src/reserved.ts
286
16
  /**
287
17
  * JavaScript and Java reserved words.
@@ -421,18 +151,302 @@ function ensureValidVarName(name) {
421
151
  function isIdentifier(name) {
422
152
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
423
153
  }
154
+ // ../../internals/utils/src/codegen.ts
155
+ /**
156
+ * Builds a JSDoc comment block from an array of lines. Returns `fallback` when there are no
157
+ * comments.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * buildJSDoc(['@type string', '@example hello'])
162
+ * // '/**\n * @type string\n * @example hello\n *\/\n '
163
+ * ```
164
+ */
165
+ function buildJSDoc(comments, options = {}) {
166
+ const { indent = " * ", suffix = "\n ", fallback = " " } = options;
167
+ if (comments.length === 0) return fallback;
168
+ return `/**\n${comments.map((c) => `${indent}${c}`).join("\n")}\n */${suffix}`;
169
+ }
170
+ /**
171
+ * An object key, quoted only when it is not identifier-shaped. Reserved words such as `class` stay bare.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * objectKey('x-total') // "'x-total'"
176
+ * ```
177
+ */
178
+ function objectKey(name) {
179
+ return isIdentifier(name) ? name : text.quote(name);
180
+ }
181
+ /**
182
+ * A JSON value as a JavaScript literal, with single-quoted strings and identifier keys left unquoted.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * toLiteral({ id: 1, 'x-tag': ['a'] }) // "{ id: 1, 'x-tag': ['a'] }"
187
+ * ```
188
+ */
189
+ function toLiteral(value) {
190
+ if (typeof value === "string") return text.quote(value);
191
+ if (Array.isArray(value)) return `[${value.map(toLiteral).join(", ")}]`;
192
+ if (value && typeof value === "object") return `{ ${Object.entries(value).map(([key, item]) => `${objectKey(key)}: ${toLiteral(item)}`).join(", ")} }`;
193
+ return String(value);
194
+ }
195
+ // ../../internals/utils/src/strings.ts
196
+ /**
197
+ * Escapes an `@` at the start of a line, so a multi-line description can't start a new JSDoc tag.
198
+ * Editors render `\@` as `@` in hovers.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * jsDocEscape('intro\n@example value') // 'intro\n\\@example value'
203
+ * ```
204
+ */
205
+ function jsDocEscape(input) {
206
+ return `${input}`.replace(/^(\s*)@/gm, "$1\\@");
207
+ }
208
+ // ../../internals/shared/src/operation.ts
209
+ /**
210
+ * Builds the `ResolverFileParams` every operation generator passes to
211
+ * `resolver.file`: a file named `name`, tagged by the operation's first
212
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
213
+ * that was repeated at dozens of call sites across the client and query plugins.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
218
+ * ```
219
+ */
220
+ function operationFileEntry(node, name, extname = ".ts") {
221
+ return {
222
+ name,
223
+ extname,
224
+ tag: node.tags[0] ?? "default",
225
+ path: node.path
226
+ };
227
+ }
228
+ /**
229
+ * Resolves the file a schema-emitting plugin (`plugin-ts`, `plugin-zod`, `plugin-faker`) writes
230
+ * `node` to, cached in `ctx.memo` under the resolver's plugin name and the
231
+ * operation. The plugin itself and every plugin that imports from it call this, so they all get
232
+ * the same path.
233
+ *
234
+ * When that path matches the file of a component schema in `ctx.driver.inputNode`, ignoring case, the file name
235
+ * gets an `Operation` suffix: operation `point` goes to `PointOperation.ts` next to schema `Point.ts`.
236
+ * Otherwise the two merge into one file, or overwrite each other on a case-insensitive file system.
237
+ * Only the file name changes. The declarations keep their names.
238
+ *
239
+ * @example Resolve `plugin-ts`'s file for the current operation
240
+ * ```ts
241
+ * const fileTs = resolveOperationFile({ ctx, node, resolver: tsResolver, root, output })
242
+ * ```
243
+ */
244
+ function resolveOperationFile(options) {
245
+ const { ctx, node, resolver, root, output } = options;
246
+ const { memo } = ctx;
247
+ const location = {
248
+ root,
249
+ output,
250
+ group: options.group ?? void 0
251
+ };
252
+ return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => {
253
+ const schemaPaths = memo(`${resolver.pluginName}:schemaFilePaths`, () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.name ? [resolver.file({
254
+ name: schema.name,
255
+ extname: ".ts",
256
+ ...location
257
+ }).path.toLowerCase()] : [])));
258
+ let file = resolver.file({
259
+ ...operationFileEntry(node, node.operationId),
260
+ ...location
261
+ });
262
+ for (let name = `${node.operationId}Operation`; schemaPaths.has(file.path.toLowerCase()); name += "Operation") {
263
+ const next = resolver.file({
264
+ ...operationFileEntry(node, name),
265
+ ...location
266
+ });
267
+ if (next.path === file.path) break;
268
+ file = next;
269
+ }
270
+ return file;
271
+ });
272
+ }
273
+ /**
274
+ * The operation's method and path as plain text, such as `` `GET /pet/{petId}` ``. A path is not a valid `{@link}` target.
275
+ */
276
+ function getOperationRequestLine(node) {
277
+ return ast.http.isOperation(node) && node.path ? `\`${node.method.toUpperCase()} ${node.path}\`` : null;
278
+ }
279
+ /**
280
+ * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
281
+ * are present and the union, default, and form-data flags the client uses to pick one.
282
+ */
283
+ function buildContentTypeInfo(contentTypes) {
284
+ const isMultipleContentTypes = contentTypes.length > 1;
285
+ return {
286
+ contentTypes,
287
+ isMultipleContentTypes,
288
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
289
+ defaultContentType: contentTypes[0] ?? "application/json",
290
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
291
+ };
292
+ }
293
+ function getContentTypeInfo(node) {
294
+ return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
295
+ }
296
+ /**
297
+ * The request-body counterpart for the primary success response: the content types it documents and
298
+ * whether several are present, so the client can let a caller pick which one to accept.
299
+ */
300
+ function getResponseContentTypeInfo(node) {
301
+ return buildContentTypeInfo(ast.http.responses.findPrimary(node)?.content?.map((e) => e.contentType) ?? []);
302
+ }
303
+ /**
304
+ * How an operation streams its primary success response item by item, when that response declares one content type:
305
+ * - `'event'` for `text/event-stream` whose `itemSchema` types the whole event with its own `event` and `data`
306
+ * - `'data'` for `text/event-stream` typed by its payload only
307
+ * - `'line'` for JSON Lines, one parsed JSON value per line
308
+ *
309
+ * Returns `null` for any other response, and for a response that also declares another content type, which stays a normal
310
+ * call with its query hooks.
311
+ */
312
+ function getEventStream(node) {
313
+ const content = ast.http.responses.findPrimary(node)?.content ?? [];
314
+ if (content.length !== 1) return null;
315
+ const entry = content[0];
316
+ const framing = getStreamFraming(entry);
317
+ if (framing === "events") return entry.stream?.shape ?? "data";
318
+ return framing === "lines" ? "line" : null;
319
+ }
320
+ /**
321
+ * How a content entry is framed as a stream: `'events'` for Server-Sent Events, `'lines'` for JSON Lines.
322
+ */
323
+ function getStreamFraming(entry) {
324
+ if (ast.mediaType.getBase(entry) === "text/event-stream") return "events";
325
+ return entry.stream && ast.mediaType.isStream(entry) ? "lines" : void 0;
326
+ }
327
+ /**
328
+ * Whether an operation streams its primary success response, as Server-Sent Events or JSON Lines. The client generator uses
329
+ * this to return a typed stream of items instead of a one-shot `RequestResult`.
330
+ */
331
+ function isEventStream(node) {
332
+ return getEventStream(node) !== null;
333
+ }
334
+ function isTextSchema(schema) {
335
+ if (!schema) return false;
336
+ const resolved = ast.ref.getSchema(schema);
337
+ return resolved.type !== "blob" && resolved.primitive === "string";
338
+ }
339
+ /**
340
+ * Whether an operation streams Server-Sent Events whose `data` the document types as a string, so the client keeps each
341
+ * `data` as text instead of JSON-parsing it. With an event-shaped `itemSchema` where only some events type `data` as a
342
+ * string, it returns the names of those events, or `false` when one of them has no fixed name.
343
+ */
344
+ function hasTextEventData(node) {
345
+ const shape = getEventStream(node);
346
+ const entry = ast.http.responses.findPrimary(node)?.content?.[0];
347
+ if (!entry || shape !== "data" && shape !== "event") return false;
348
+ if (shape === "data") return isTextSchema(entry.stream?.schema ?? entry.schema);
349
+ const item = entry.stream?.schema ? ast.ref.getSchema(entry.stream.schema) : void 0;
350
+ if (!item) return false;
351
+ const events = item.type === "union" ? ast.schema.narrow(item, "union")?.members ?? [] : [item];
352
+ const propertySchemas = (event, propertyName) => ast.schema.findProperties({
353
+ node: event,
354
+ propertyName
355
+ }).map(({ schema }) => schema);
356
+ const textEvents = events.filter((event) => {
357
+ const dataSchemas = propertySchemas(event, "data");
358
+ return dataSchemas.length > 0 && dataSchemas.every(isTextSchema);
359
+ });
360
+ if (textEvents.length === events.length) return events.length > 0;
361
+ const eventNames = textEvents.map((event) => propertySchemas(event, "event").flatMap((schema) => ast.schema.getLiterals(ast.ref.getSchema(schema))));
362
+ return eventNames.length > 0 && eventNames.every((values) => values.length > 0) && [...new Set(eventNames.flat().map(String))];
363
+ }
364
+ /**
365
+ * Whether the primary success response offers `text/event-stream`, alone or next to other content types. The client asks
366
+ * for the stream by default and ships the `sse.ts` runtime, so a mixed response can be read as events too.
367
+ */
368
+ function hasEventStreamContent(node) {
369
+ const entries = ast.http.responses.findPrimary(node)?.content ?? [];
370
+ return entries.some((entry) => getStreamFraming(entry) === "events" && (entries.length === 1 || Boolean(entry.schema)));
371
+ }
372
+ /**
373
+ * Whether a success response offers a stream media type next to other content types. The client returns that variant's
374
+ * body as a `ReadableStream`, so the stream runtime is registered and a hook's data type includes the stream.
375
+ */
376
+ function hasStreamVariant(node) {
377
+ return ast.http.responses.filterSuccess(node).some((response) => {
378
+ const variants = (response.content ?? []).filter((entry) => entry.schema);
379
+ return variants.length > 1 && variants.some((entry) => ast.mediaType.isStream(entry));
380
+ });
381
+ }
382
+ const formContentTypes = ["multipart/form-data", "application/x-www-form-urlencoded"];
383
+ /**
384
+ * The form media types the operation's request body offers, `multipart/form-data` and `application/x-www-form-urlencoded`,
385
+ * which the client needs a handler for.
386
+ */
387
+ function getFormContentTypes(node) {
388
+ const request = new Set((node.requestBody?.content ?? []).map((entry) => ast.mediaType.getBase(entry)));
389
+ return formContentTypes.filter((contentType) => request.has(contentType));
390
+ }
391
+ /**
392
+ * Which of the grouped request options an operation carries.
393
+ */
394
+ function getRequestGroups(node) {
395
+ const { path, query, header, cookie } = ast.http.groupParameters(node);
396
+ return {
397
+ path: path.length > 0,
398
+ query: query.length > 0,
399
+ body: Boolean(ast.http.requestBody.getSchema(node)),
400
+ headers: header.length > 0,
401
+ cookies: cookie.length > 0
402
+ };
403
+ }
404
+ /**
405
+ * Resolves which grouped request options an operation carries together with whether each group
406
+ * holds a required member. The grouped parameter stays optional only when nothing inside it is
407
+ * required, matching the generated `RequestConfig` type.
408
+ */
409
+ function getRequestGroupOptionality(node) {
410
+ const groups = getRequestGroups(node);
411
+ const { path, query, header } = ast.http.groupParameters(node);
412
+ const hasRequiredPath = path.some((param) => param.required);
413
+ const hasRequiredQuery = query.some((param) => param.required);
414
+ const hasRequiredHeader = header.some((param) => param.required);
415
+ return {
416
+ groups,
417
+ hasRequiredPath,
418
+ hasRequiredQuery,
419
+ hasRequiredHeader,
420
+ isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !(groups.body && node.requestBody?.required === true)
421
+ };
422
+ }
423
+ function buildOperationComments(node, options = {}) {
424
+ const { splitLines = false } = options;
425
+ const filteredComments = [
426
+ getOperationRequestLine(node),
427
+ node.description && `@description ${node.description}`,
428
+ node.summary && `@summary ${node.summary}`,
429
+ node.deprecated && "@deprecated",
430
+ node.externalDocs && `@see ${jsDocEscape([node.externalDocs.url, node.externalDocs.description].filter(Boolean).join(" "))}`
431
+ ].filter((comment) => Boolean(comment));
432
+ if (!splitLines) return filteredComments;
433
+ return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
434
+ }
424
435
  // ../../internals/shared/src/resolver.ts
425
436
  /**
426
437
  * The shared top-level `name` of the operation plugins, computed once per operation name and build.
427
438
  */
428
439
  function resolveOperationName(name) {
429
- return this.memo(`operation:${name}`, () => ensureValidVarName(camelCase(name)));
440
+ return this.memo(`operation:${name}`, () => ensureValidVarName(casing.camel(name)));
430
441
  }
431
442
  /**
432
443
  * Builds the allocation key for a declaration an operation owns. `scope` names which declaration
433
444
  * it is, so two of them never share an allocation even when a custom resolver gives both the same
434
445
  * name. Leave `scope` empty for the operation's own declaration.
435
446
  *
447
+ * The key starts from the operation's input `pointer`, which macros keep, so a plugin whose macro
448
+ * rewrites the method or path still resolves another plugin's names for the same operation.
449
+ *
436
450
  * @example
437
451
  * ```ts
438
452
  * resolver.resolveName({ key: operationKey(node, 'response', 'status', 404), name })
@@ -441,49 +455,228 @@ function resolveOperationName(name) {
441
455
  function operationKey(node, ...scope) {
442
456
  return [
443
457
  "operation",
444
- node.method,
445
- node.path,
458
+ ...node.pointer ? [node.pointer] : [node.method, node.path],
446
459
  ...scope.map(String)
447
460
  ];
448
461
  }
449
- // ../../internals/shared/src/group.ts
462
+ // ../../internals/client/src/resolveOperationTypes.ts
463
+ const pluginZodName = "plugin-zod";
464
+ function getResponseHeadersEntries({ node, types }) {
465
+ const resolve = types.response.headers;
466
+ if (!resolve) return [];
467
+ return node.responses.filter((response) => response.headers?.length).map((response) => [response.statusCode, resolve.call(types.response, node, response.statusCode)]);
468
+ }
450
469
  /**
451
- * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
452
- * shared default naming so every plugin groups output consistently:
453
- *
454
- * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
455
- * - other groups use the camelCased group (`pet store` → `petStore`).
456
- *
457
- * A user-provided `group.name` always wins over the default namer, so callers stay in
458
- * control of their output folders. Returns `null` when grouping is disabled, matching the
459
- * per-plugin convention.
470
+ * The headers type argument of `RequestResult`, such as `{ '200': LoginUserStatus200Headers }`, or `null` when no response declares headers.
471
+ */
472
+ function buildResponseHeadersType({ node, types }) {
473
+ const entries = getResponseHeadersEntries({
474
+ node,
475
+ types
476
+ }).map(([statusCode, name]) => `'${statusCode}': ${name}`);
477
+ return entries.length ? `{ ${entries.join("; ")} }` : null;
478
+ }
479
+ /**
480
+ * A runtime result type, such as `RequestResult`, in its `WithHeaders` form when `headers` is set.
481
+ */
482
+ function resultTypeName(name, headers) {
483
+ return headers ? `${name}WithHeaders` : name;
484
+ }
485
+ /**
486
+ * A runtime result type with its arguments, such as `RequestResultWithHeaders<Responses, Headers, ThrowOnError>`.
487
+ */
488
+ function buildResultTypeRef(name, responses, headers, rest = []) {
489
+ return `${resultTypeName(name, headers !== null)}<${[
490
+ responses,
491
+ headers,
492
+ ...rest
493
+ ].filter(Boolean).join(", ")}>`;
494
+ }
495
+ /**
496
+ * The names of the headers types `buildResponseHeadersType` references, for the file's imports.
497
+ */
498
+ function getResponseHeadersTypeNames({ node, types }) {
499
+ return getResponseHeadersEntries({
500
+ node,
501
+ types
502
+ }).map(([, name]) => name);
503
+ }
504
+ /**
505
+ * A `plugin-ts` shaped resolver whose operation type names point at the types `pluginZod({ inferred })`
506
+ * exports next to each schema, so code written against `plugin-ts` names picks up the zod types.
507
+ */
508
+ function toZodTypeResolver(resolver) {
509
+ const type = (name) => resolver.schema.type(name);
510
+ return {
511
+ ...resolver,
512
+ file: resolver.file.bind(resolver),
513
+ memo: (key, compute) => resolver.memo(`zodTypes:${key}`, compute),
514
+ param: {
515
+ name: (node, p) => type(resolver.param.name(node, p)),
516
+ path: (node, p) => type(resolver.param.path(node, p)),
517
+ query: (node, p) => type(resolver.param.query(node, p)),
518
+ headers: (node, p) => type(resolver.param.headers(node, p)),
519
+ cookies: (node, p) => type(resolver.param.cookies(node, p))
520
+ },
521
+ response: {
522
+ body: (node) => type(resolver.response.body(node)),
523
+ status: (node, statusCode) => type(resolver.response.status(node, statusCode)),
524
+ response: (node) => type(resolver.response.response(node)),
525
+ responses: (node) => type(resolver.response.responses(node)),
526
+ headers: (node, statusCode) => type(resolver.response.headers(node, statusCode)),
527
+ options: (node) => resolver.response.options(node)
528
+ }
529
+ };
530
+ }
531
+ /**
532
+ * Picks the plugin generated clients, query hooks, and MCP handlers take their operation types from.
460
533
  *
461
- * @param group - The user-supplied group option, or `undefined` to disable grouping.
534
+ * `pluginZod({ inferred: 'codec' })` wins, so a codec param takes its domain value. Otherwise `plugin-ts`
535
+ * wins when it is in the pipeline, and without it any `pluginZod({ inferred })`. Throws
536
+ * `KUBB_MISSING_DEPENDENCY` for `pluginName` when neither is available.
462
537
  *
463
538
  * @example
464
539
  * ```ts
465
- * createGroupConfig(group) // shared across every plugin
540
+ * const types = resolveOperationTypes(ctx.driver, ctx.plugin.name)
541
+ * types.response.options(node) // 'GetPetByIdOptions', or 'GetPetByIdOptionsSchemaType' from zod
466
542
  * ```
467
543
  */
468
- function createGroupConfig(group) {
469
- if (!group) return null;
470
- const defaultName = (ctx) => {
471
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
472
- return camelCase(ctx.group);
544
+ function resolveOperationTypes(driver, pluginName) {
545
+ return driver.memo(`operationTypes:${pluginName}`, () => {
546
+ const pluginTs = driver.getPlugin(pluginTsName);
547
+ const pluginZod = driver.getPlugin(pluginZodName);
548
+ if (pluginTs && pluginZod?.options?.inferred !== "codec") {
549
+ const resolver = driver.getResolver(pluginTsName);
550
+ return {
551
+ response: resolver.response,
552
+ resolver,
553
+ output: pluginTs.options?.output,
554
+ group: pluginTs.options?.group ?? void 0
555
+ };
556
+ }
557
+ if (!pluginZod?.options?.inferred) throw new Diagnostics.Error({
558
+ code: Diagnostics.code.missingDependency,
559
+ severity: "error",
560
+ message: `Plugin "${pluginName}" has no source for operation types.`,
561
+ help: `Add \`pluginTs()\` before "${pluginName}", or add \`pluginZod({ inferred: true })\`.`,
562
+ location: { kind: "config" },
563
+ plugin: pluginName
564
+ });
565
+ const resolver = toZodTypeResolver(driver.getResolver(pluginZodName));
566
+ return {
567
+ response: resolver.response,
568
+ resolver,
569
+ output: pluginZod.options.output,
570
+ group: pluginZod.options.group ?? void 0
571
+ };
572
+ });
573
+ }
574
+ // src/builders/security.ts
575
+ const inlineAuthKeys = [
576
+ "type",
577
+ "scheme",
578
+ "name",
579
+ "in",
580
+ "scopes",
581
+ "flows",
582
+ "metadataUrl",
583
+ "url"
584
+ ];
585
+ function serializeScheme({ ref, ...auth }) {
586
+ const scopes = auth.scopes?.length ? auth.scopes : void 0;
587
+ if (ref) {
588
+ const schemeRef = isValidVarName(ref) ? `securitySchemes.${ref}` : `securitySchemes[${text.quote(ref)}]`;
589
+ return scopes ? `{ ...${schemeRef}, scopes: ${toLiteral(scopes)} }` : schemeRef;
590
+ }
591
+ return `{ ${inlineAuthKeys.flatMap((key) => {
592
+ const entryValue = key === "scopes" ? scopes : auth[key];
593
+ return entryValue ? [`${key}: ${toLiteral(entryValue)}`] : [];
594
+ }).join(", ")} }`;
595
+ }
596
+ function serializeRequirement(requirement) {
597
+ return Array.isArray(requirement) ? `[${requirement.map(serializeScheme).join(", ")}]` : serializeScheme(requirement);
598
+ }
599
+ /**
600
+ * Maps an AST credential to the inline `Auth` object, or `null` for `mutualTLS`, which the runtime cannot send.
601
+ * `http` schemes other than `basic` are treated as bearer. OAuth2 and OpenID Connect keep their URLs for the `auth` resolver.
602
+ */
603
+ function resolveCredential(credential) {
604
+ if (credential.type === "apiKey") return {
605
+ type: "apiKey",
606
+ name: credential.name,
607
+ in: credential.in
473
608
  };
474
- return {
475
- ...group,
476
- name: group.name ? group.name : defaultName
609
+ if (credential.type === "http") return {
610
+ type: "http",
611
+ scheme: credential.scheme === "basic" ? "basic" : "bearer"
612
+ };
613
+ if (credential.type === "oauth2") {
614
+ const flows = Object.fromEntries(Object.entries(credential.flows).map(([name, { scopes: _scopes, ...urls }]) => [name, urls]));
615
+ return {
616
+ type: "oauth2",
617
+ ...Object.keys(flows).length ? { flows } : {},
618
+ ...credential.metadataUrl ? { metadataUrl: credential.metadataUrl } : {}
619
+ };
620
+ }
621
+ if (credential.type === "openIdConnect") return {
622
+ type: "openIdConnect",
623
+ ...credential.url ? { url: credential.url } : {}
477
624
  };
625
+ return null;
478
626
  }
479
- // kubb-extension.json
480
- var compatibility = {
481
- "kubb": ">=5.16.0",
482
- "node": ">=22.22"
483
- };
484
- // package.json
485
- var version = "5.12.0";
486
- // ../../internals/client/src/builders/validatorOptions.ts
627
+ /**
628
+ * Derives the per-operation security metadata from `OperationNode.auth` and the document's `meta.auth` schemes. Each
629
+ * requirement becomes one entry: its scheme, or an array of its schemes when it needs several together. A requirement
630
+ * with a scheme the runtime cannot place or that is not declared is left out, since it can never be met, and so is a
631
+ * repeated one. With `hoist`, each scheme carries its name as `ref`.
632
+ *
633
+ * @example
634
+ * `getOperationSecurity({ auth: [[{ scheme: 'bearerAuth' }]], schemes: meta.auth })`
635
+ * `// [{ type: 'http', scheme: 'bearer' }]`
636
+ * @example
637
+ * `// auth: [[{ scheme: 'api_key' }, { scheme: 'session' }]] gives [[{ type: 'apiKey', ... }, { type: 'apiKey', ... }]]`
638
+ */
639
+ function getOperationSecurity({ auth, schemes, hoist }) {
640
+ const security = ast.auth.getRequirements({
641
+ auth,
642
+ schemes
643
+ }).flatMap((requirement) => {
644
+ const resolved = requirement.map(({ name, credential, scopes }) => {
645
+ const entry = resolveCredential(credential);
646
+ return entry && {
647
+ ...entry,
648
+ ...scopes.length ? { scopes } : {},
649
+ ...hoist ? { ref: name } : {}
650
+ };
651
+ });
652
+ if (!resolved.length || !resolved.every((entry) => entry !== null)) return [];
653
+ return [resolved.length === 1 ? resolved[0] : resolved];
654
+ });
655
+ return security.length ? security : void 0;
656
+ }
657
+ /**
658
+ * Serializes the per-operation security into the literal emitted on each generated call's `security`
659
+ * field. The runtime `resolveAuth` helper walks it, calling the configured `auth` resolver per scheme.
660
+ *
661
+ * @example
662
+ * `buildSecurityMetadata({ security: [{ type: 'http', scheme: 'bearer' }] }) // "[{ type: 'http', scheme: 'bearer' }]"`
663
+ */
664
+ function buildSecurityMetadata({ security }) {
665
+ if (!security?.length) return null;
666
+ return `[${security.map(serializeRequirement).join(", ")}]`;
667
+ }
668
+ /**
669
+ * The `securitySchemes` const of `.kubb/client.ts`: every scheme the runtime can place, by name, or `null` without one.
670
+ */
671
+ function buildSecuritySchemes(schemes) {
672
+ const entries = (schemes ?? []).flatMap(({ name, credential }) => {
673
+ const auth = resolveCredential(credential);
674
+ return auth ? [`${objectKey(name)}: ${serializeScheme(auth)}`] : [];
675
+ });
676
+ if (!entries.length) return null;
677
+ return `/**\n * The security schemes from the spec by name. A generated call sends one of them to the \`auth\` resolver.\n */\nexport const securitySchemes = { ${entries.join(", ")} } as const`;
678
+ }
679
+ // src/builders/validatorOptions.ts
487
680
  /**
488
681
  * Returns `true` when any direction of the validator uses zod (used for dependency checks).
489
682
  */
@@ -518,25 +711,136 @@ function resolveResponseValidator(validator) {
518
711
  return validator.response ?? null;
519
712
  }
520
713
  /**
521
- * Resolves the zod schema name a generated client validates an error body with on the non-throw path.
522
- * Uses the error-only `<operation>ErrorSchema` (the union of non-2xx statuses); returns `null` when the
523
- * operation documents no error responses with a schema.
714
+ * Builds the `validator.error` map of a call, one plugin-zod `<operation>StatusNNNSchema` per non-2xx response with a
715
+ * schema, such as `{ 404: getPetStatus404Schema, '5xx': getPetStatus5xxSchema }`. Returns `null` without one.
524
716
  */
525
717
  function buildZodErrorParse(node, zodResolver) {
526
- if (!node.responses.some((res) => !isSuccessStatusCode(res.statusCode) && res.content?.some((entry) => entry.schema))) return null;
527
- return zodResolver.response.error?.(node) || null;
718
+ const entries = node.responses.filter((res) => !ast.http.status.isSuccess(res) && res.content?.some((entry) => entry.schema)).map((res) => ({
719
+ key: String(res.statusCode),
720
+ name: zodResolver.response.status(node, res.statusCode)
721
+ }));
722
+ if (!entries.length) return null;
723
+ return {
724
+ literal: `{ ${entries.map(({ key, name }) => `${/^\d+$/.test(key) ? key : text.quote(key)}: ${name}`).join(", ")} }`,
725
+ names: entries.map(({ name }) => name)
726
+ };
727
+ }
728
+ // src/builders/validator.ts
729
+ /**
730
+ * Builds the validator-hook references for one operation. Request body and params validation run before the send.
731
+ * Response validation runs on the success body only. Returns `null` references when the matching
732
+ * direction is disabled or the schema is absent.
733
+ */
734
+ function buildValidatorHooks({ node, validator, zodResolver }) {
735
+ const hasRequestBody = Boolean(ast.http.requestBody.getSchema(node));
736
+ const parsesResponse = zodResolver && resolveResponseValidator(validator) === "zod";
737
+ const hasResponseSchema = node.responses.some((res) => res.content?.some((entry) => entry.schema));
738
+ const response = parsesResponse && hasResponseSchema ? zodResolver.response.response(node) || null : null;
739
+ const errorParse = parsesResponse ? buildZodErrorParse(node, zodResolver) : null;
740
+ const request = zodResolver && resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.response.body(node) || null : null;
741
+ const paramGroups = ast.http.groupParameters(node);
742
+ const resolveParamsName = (kind, params) => zodResolver && resolveParamsValidator(validator) === "zod" && params.length > 0 ? zodResolver.param[kind](node, params[0]) : null;
743
+ const path = resolveParamsName("path", paramGroups.path);
744
+ const query = resolveParamsName("query", paramGroups.query);
745
+ const headers = resolveParamsName("headers", paramGroups.header);
746
+ const cookies = resolveParamsName("cookies", paramGroups.cookie);
747
+ const importedZodNames = [
748
+ response,
749
+ ...errorParse?.names ?? [],
750
+ request,
751
+ path,
752
+ query,
753
+ headers,
754
+ cookies
755
+ ].filter((name) => Boolean(name));
756
+ return {
757
+ request,
758
+ path,
759
+ query,
760
+ headers: headers ? `toHeadersStandardSchema(${headers}, ${headers}.shape)` : null,
761
+ cookies,
762
+ response,
763
+ error: errorParse?.literal ?? null,
764
+ importedZodNames
765
+ };
766
+ }
767
+ /**
768
+ * Prints the `validator: { … }` entry of a call config from the hooks named in `keys`, or `null` when none is set.
769
+ * The entry spreads `config.validator` last, so a per-call hook overrides the generated one for its key only.
770
+ */
771
+ function buildValidatorLiteral(hooks, keys) {
772
+ const entries = keys.flatMap((key) => hooks?.[key] ? [`${key}: ${hooks[key]}`] : []);
773
+ return entries.length ? `validator: { ${entries.join(", ")}, ...config.validator }` : null;
528
774
  }
529
- // ../../internals/client/src/builders/generics.ts
775
+ // src/builders/operationData.ts
530
776
  /**
531
- * Builds the `RequestResult` generic arguments for one operation: the per-status responses record
532
- * plus the per-call `ThrowOnError` flag. `SuccessOf` / `ErrorOf` split the record inside the
533
- * runtime, so this only names the record and threads `ThrowOnError`.
777
+ * Resolves what the function and SDK generators both print for one operation: its name, the files its types and zod
778
+ * schemas come from, and its validator hooks, security, base URL and response headers types.
779
+ */
780
+ function resolveOperationData({ node, ctx, types }) {
781
+ const { driver, resolver, root } = ctx;
782
+ const { output, validator, importPath } = ctx.options;
783
+ const pluginZod = isValidatorEnabled(validator) ? driver.getPlugin(pluginZodName$1) : null;
784
+ const zodResolver = pluginZod ? driver.getResolver(pluginZodName$1) : null;
785
+ return {
786
+ node,
787
+ name: resolver.resolveName({
788
+ key: operationKey(node),
789
+ name: resolver.name(node.operationId)
790
+ }),
791
+ types,
792
+ typeFile: resolveOperationFile({
793
+ ctx,
794
+ node,
795
+ resolver: types.resolver,
796
+ root,
797
+ output: types.output ?? output,
798
+ group: types.group
799
+ }),
800
+ zodFile: zodResolver && pluginZod?.options ? resolveOperationFile({
801
+ ctx,
802
+ node,
803
+ resolver: zodResolver,
804
+ root,
805
+ output: pluginZod.options.output ?? output,
806
+ group: pluginZod.options.group ?? void 0
807
+ }) : null,
808
+ validatorHooks: buildValidatorHooks({
809
+ node,
810
+ validator,
811
+ zodResolver
812
+ }),
813
+ headerTypeNames: getResponseHeadersTypeNames({
814
+ node,
815
+ types
816
+ }),
817
+ security: getOperationSecurity({
818
+ auth: node.auth,
819
+ schemes: ctx.meta.auth,
820
+ hoist: importPath === void 0
821
+ }),
822
+ baseURL: ast.http.getBaseURL(ast.server.get({
823
+ servers: node.servers,
824
+ index: ctx.adapter.options?.server?.index
825
+ }))
826
+ };
827
+ }
828
+ // src/builders/generics.ts
829
+ /**
830
+ * Builds the `RequestResult` type arguments of one operation: responses, `ThrowOnError` and the response headers, when a response declares any.
534
831
  *
535
832
  * @example
536
- * `buildRequestResultGenerics({ node, types }) // 'AddPetResponses, ThrowOnError'`
833
+ * `buildRequestResultGenerics({ node, types }) // "LoginUserResponses, ThrowOnError, { '200': LoginUserStatus200Headers }"`
537
834
  */
538
835
  function buildRequestResultGenerics({ node, types }) {
539
- return `${types.response.responses(node)}, ThrowOnError`;
836
+ return [
837
+ types.response.responses(node),
838
+ "ThrowOnError",
839
+ buildResponseHeadersType({
840
+ node,
841
+ types
842
+ })
843
+ ].filter(Boolean).join(", ");
540
844
  }
541
845
  /**
542
846
  * Builds the full return type an operation's function signature uses: `Unwrappable<RequestResult>`
@@ -549,225 +853,127 @@ function buildRequestResultGenerics({ node, types }) {
549
853
  * `buildResultType({ node, types, returnType: 'data' }) // 'Promise<UnwrappedResult<AddPetResponses, ThrowOnError>>'`
550
854
  */
551
855
  function buildResultType({ node, types, returnType }) {
552
- const generics = buildRequestResultGenerics({
856
+ const result = buildResultTypeRef(returnType === "data" ? "UnwrappedResult" : "RequestResult", types.response.responses(node), buildResponseHeadersType({
553
857
  node,
554
858
  types
555
- });
556
- if (returnType === "data") return `Promise<UnwrappedResult<${generics}>>`;
557
- if (returnType === "plain") return `Promise<RequestResult<${generics}>>`;
558
- return `Unwrappable<RequestResult<${generics}>>`;
559
- }
560
- // ../../internals/client/src/builders/returnStatement.ts
561
- /**
562
- * Builds the return statement of a generated operation function. With the default
563
- * `returnType: 'full'` the runtime call already resolves to `{ data, error, request, response }`.
564
- * The generated code casts that result to the operation's `RequestResult`, then wraps it with
565
- * `withUnwrap`, so the caller can `await` it directly or call `.unwrap()` for the bare success
566
- * body. With `returnType: 'data'` it instead routes the call through the runtime's `unwrapResult`,
567
- * which narrows the resolved value down to the bare success body the same way `RequestResult`
568
- * already does, using the plugin default when the call leaves `throwOnError` unset.
569
- * With `returnType: 'plain'` it returns the runtime call as is, cast to `RequestResult`, with no helper.
570
- *
571
- * Cast first, wrap second, for the `'full'` path. That order keeps `withUnwrap`'s generic inferred
572
- * as `RequestResult` instead of the runtime's own internal result type. Casting an `Unwrappable<A>`
573
- * straight to `Unwrappable<B>` does not work: the `.then` overload stays pinned to `A`, and `as`
574
- * rejects that two-generic swap even where `A` and `B` on their own would satisfy it.
859
+ }), ["ThrowOnError"]);
860
+ return returnType === "full" ? `Unwrappable<${result}>` : `Promise<${result}>`;
861
+ }
862
+ // src/builders/returnStatement.ts
863
+ /**
864
+ * Builds the return statement of a generated operation function. The client call takes the responses record and
865
+ * `ThrowOnError` as type arguments, so its result is already the operation's `RequestResult` and needs no cast. With the
866
+ * default `returnType: 'full'` it is wrapped in `withUnwrap`, so the caller can `await` it or call `.unwrap()`. With
867
+ * `returnType: 'data'` it goes through `unwrapResult`, which keeps the success body when the call throws on errors.
868
+ * With `returnType: 'plain'` it is returned as is.
575
869
  *
576
870
  * @example
577
- * `return withUnwrap(request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>)`
871
+ * `return withUnwrap(apiClient<AddPetResponses, ThrowOnError>({ method: 'POST', url: '/pet', ...config }))`
578
872
  * @example
579
- * `return unwrapResult(request({ method: 'POST', url: '/pet', ...config }), config.throwOnError ?? false) as Promise<UnwrappedResult<AddPetResponses, ThrowOnError>>`
873
+ * `return unwrapResult(apiClient<AddPetResponses, ThrowOnError>({ ... }), resolveThrowOnError(config.throwOnError, true))`
580
874
  * @example
581
- * `return request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>`
875
+ * `return apiClient<AddPetResponses, ThrowOnError>({ method: 'POST', url: '/pet', ...config })`
582
876
  */
583
877
  function buildReturnStatement({ node, types, callConfig, returnType, throwOnErrorDefault }) {
584
- const request = returnType !== "plain" && hasRequestContentTypeChoice(node) ? `request<${types.response.options(node)}['body']>` : "request";
585
- if (returnType === "data") return `return unwrapResult(${request}(${callConfig}), config.throwOnError ?? ${throwOnErrorDefault}) as ${buildResultType({
586
- node,
587
- types,
588
- returnType
589
- })}`;
590
- if (returnType === "plain") return `return ${request}(${callConfig}) as Promise<RequestResult<${buildRequestResultGenerics({
878
+ const call = `apiClient<${buildRequestResultGenerics({
591
879
  node,
592
880
  types
593
- })}>>`;
594
- return `return withUnwrap(${request}(${callConfig}) as Promise<RequestResult<${buildRequestResultGenerics({
595
- node,
596
- types
597
- })}>>)`;
598
- }
599
- /**
600
- * Builds the return type and statement of a `text/event-stream` operation. The item type is `TypedEvent<...>` when each item
601
- * declares its own `event` and `data`, and otherwise the payload type, which `EventStreamResult` wraps in `ServerSentEvent`. The call's
602
- * promise goes through `toEventStream` with the `client` for its defaults, the call's `config`, the shape and the per-event
603
- * validator. A `returnType: 'plain'` client receives only the promise,
604
- * `toEventStream(request(config))`, and keeps its response validator in the call config.
605
- */
606
- function buildEventStreamReturn({ node, types, callConfig, returnType, validatorHooks, throwOnErrorDefault }) {
607
- const shape = getEventStream(node) ?? "data";
608
- const responses = types.response.responses(node);
609
- const success = `SuccessOf<${responses}>`;
610
- const eventType = shape === "event" ? `TypedEvent<${success}>` : success;
611
- if (returnType === "plain") return {
612
- returnType: `Promise<EventStreamResult<${eventType}>>`,
613
- statement: `return toEventStream<${eventType}>(request(${callConfig}))`
614
- };
615
- const generics = `${responses}, ThrowOnError${shape === "event" ? `, ${eventType}` : ""}`;
616
- const call = `toEventStream<${eventType}>(request(${callConfig}), { client: request, config, ${[`shape: '${shape}'`, validatorHooks?.response ? `validator: ${validatorHooks.response}` : null].filter(Boolean).join(", ")} })`;
617
- if (returnType === "data") {
618
- const result = `Promise<UnwrappedStreamResult<${generics}>>`;
619
- return {
620
- returnType: result,
621
- statement: `return unwrapResult(${call}, config.throwOnError ?? ${throwOnErrorDefault}) as ${result}`
622
- };
623
- }
624
- return {
625
- returnType: `Unwrappable<StreamResult<${generics}>>`,
626
- statement: `return withUnwrap(${call} as Promise<StreamResult<${generics}>>)`
627
- };
628
- }
629
- /** The stream types an operation file imports: `StreamResult` and `StreamOptions`, `EventStreamResult` for a plain client, and `TypedEvent` for named events. */
630
- function buildEventStreamImports({ returnType, typed }) {
631
- return [...{
632
- plain: ["EventStreamResult"],
633
- full: ["StreamResult", "StreamOptions"],
634
- data: ["UnwrappedStreamResult", "StreamOptions"]
635
- }[returnType], ...typed ? ["TypedEvent"] : []];
636
- }
637
- // ../../internals/client/src/builders/security.ts
638
- function serializeAuth(auth) {
639
- const parts = [`type: '${auth.type}'`];
640
- if (auth.scheme) parts.push(`scheme: '${auth.scheme}'`);
641
- if (auth.name) parts.push(`name: '${auth.name}'`);
642
- if (auth.in) parts.push(`in: '${auth.in}'`);
643
- if (auth.scopes?.length) parts.push(`scopes: [${auth.scopes.map((scope) => `'${scope}'`).join(", ")}]`);
644
- return `{ ${parts.join(", ")} }`;
645
- }
646
- /**
647
- * Maps an OpenAPI security scheme to the inline `Auth` object, or `null` when the runtime cannot
648
- * place it (an unresolved `$ref`, or an `apiKey` without a name or outside `header` / `query` /
649
- * `cookie`). `http` schemes other than `basic` are treated as bearer.
650
- */
651
- function resolveSecurityScheme(scheme) {
652
- if (!scheme || "$ref" in scheme) return null;
653
- if (scheme.type === "apiKey") {
654
- if (!scheme.name || scheme.in !== "header" && scheme.in !== "query" && scheme.in !== "cookie") return null;
655
- return {
656
- type: "apiKey",
657
- name: scheme.name,
658
- in: scheme.in
659
- };
660
- }
661
- if (scheme.type === "http") return {
662
- type: "http",
663
- scheme: scheme.scheme?.toLowerCase() === "basic" ? "basic" : "bearer"
664
- };
665
- if (scheme.type === "oauth2") return { type: "oauth2" };
666
- if (scheme.type === "openIdConnect") return { type: "openIdConnect" };
667
- return null;
668
- }
669
- /**
670
- * Derives the per-operation security metadata from the OpenAPI document. The operation's own
671
- * `security` overrides the global `security` (an explicit empty array disables auth), and every
672
- * referenced scheme is resolved from `components.securitySchemes` into a flat, de-duplicated list of
673
- * `Auth` objects the runtime walks in order.
674
- *
675
- * @example
676
- * `getOperationSecurity({ document, method: 'POST', path: '/pet' })`
677
- * `// [{ type: 'http', scheme: 'bearer' }]`
678
- */
679
- function getOperationSecurity({ document, method, path }) {
680
- if (!document) return void 0;
681
- const requirements = (document.paths?.[path]?.[method.toLowerCase()])?.security ?? document.security;
682
- if (!requirements?.length) return void 0;
683
- const definitions = document.components?.securitySchemes ?? {};
684
- const security = [];
685
- const seen = /* @__PURE__ */ new Set();
686
- for (const requirement of requirements) for (const [schemeName, scopes] of Object.entries(requirement)) {
687
- if (seen.has(schemeName)) continue;
688
- seen.add(schemeName);
689
- const auth = resolveSecurityScheme(definitions[schemeName]);
690
- if (auth) security.push(scopes.length ? {
691
- ...auth,
692
- scopes
693
- } : auth);
694
- }
695
- return security.length ? security : void 0;
696
- }
697
- /**
698
- * Serializes the per-operation security into the literal emitted on each generated call's `security`
699
- * field. The runtime `resolveAuth` helper walks it, calling the configured `auth` resolver per entry.
700
- *
701
- * @example
702
- * `buildSecurityMetadata({ security: [{ type: 'http', scheme: 'bearer' }] }) // "[{ type: 'http', scheme: 'bearer' }]"`
703
- */
704
- function buildSecurityMetadata({ security }) {
705
- if (!security?.length) return null;
706
- return `[${security.map(serializeAuth).join(", ")}]`;
881
+ })}>(${callConfig})`;
882
+ if (returnType === "data") return `return unwrapResult(${call}, resolveThrowOnError(config.throwOnError, ${throwOnErrorDefault}))`;
883
+ if (returnType === "plain") return `return ${call}`;
884
+ return `return withUnwrap(${call})`;
707
885
  }
708
- // ../../internals/client/src/builders/validator.ts
709
886
  /**
710
- * Builds the validator-hook references for one operation. Request body and params validation run before the send.
711
- * Response validation runs on the success body only. Returns `null` references when the matching
712
- * direction is disabled or the schema is absent.
887
+ * Builds the return type and statement of a streamed operation, which calls `apiClient.stream`. The item type is
888
+ * `TypedEvent<...>` when each event declares its own `event` and `data`, the success type itself for JSON Lines, and
889
+ * otherwise the default, `ServerSentEvent` around the payload, which needs no type argument.
713
890
  */
714
- function buildValidatorHooks({ node, validator, zodResolver }) {
715
- const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
716
- const parsesResponse = zodResolver && resolveResponseValidator(validator) === "zod";
717
- const response = parsesResponse ? zodResolver.response.response(node) || null : null;
718
- const error = parsesResponse ? buildZodErrorParse(node, zodResolver) : null;
719
- const request = zodResolver && resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.response.body(node) || null : null;
720
- const paramGroups = getOperationParameters(node);
721
- const resolveParamsName = (kind, params) => zodResolver && resolveParamsValidator(validator) === "zod" && params.length > 0 ? zodResolver.param[kind](node, params[0]) : null;
722
- const path = resolveParamsName("path", paramGroups.path);
723
- const query = resolveParamsName("query", paramGroups.query);
724
- const headers = resolveParamsName("headers", paramGroups.header);
725
- const importedZodNames = [
726
- response,
727
- error,
728
- request,
729
- path,
730
- query,
891
+ function buildEventStreamReturn({ node, types, callConfig, returnType, throwOnErrorDefault }) {
892
+ const shape = getEventStream(node) ?? "data";
893
+ const responses = types.response.responses(node);
894
+ const headers = buildResponseHeadersType({
895
+ node,
896
+ types
897
+ });
898
+ const success = `SuccessOf<${responses}>`;
899
+ const item = {
900
+ event: `TypedEvent<${success}>`,
901
+ data: null,
902
+ line: success
903
+ }[shape];
904
+ const call = `apiClient.stream<${[
905
+ responses,
906
+ "ThrowOnError",
907
+ item ?? (headers ? `ServerSentEvent<${success}>` : null),
731
908
  headers
732
- ].filter((name) => Boolean(name));
909
+ ].filter(Boolean).join(", ")}>(${callConfig})`;
910
+ const result = (name) => buildResultTypeRef(name, responses, headers, ["ThrowOnError", item]);
911
+ if (returnType === "plain") return {
912
+ returnType: `Promise<${result("StreamResult")}>`,
913
+ statement: `return ${call}`
914
+ };
915
+ if (returnType === "data") return {
916
+ returnType: `Promise<${result("UnwrappedStreamResult")}>`,
917
+ statement: `return unwrapResult(${call}, resolveThrowOnError(config.throwOnError, ${throwOnErrorDefault}))`
918
+ };
733
919
  return {
734
- request,
735
- path,
736
- query,
737
- headers: headers ? `toHeadersStandardSchema(${headers}, ${headers}.shape)` : null,
738
- response,
739
- error,
740
- importedZodNames
920
+ returnType: `Unwrappable<${result("StreamResult")}>`,
921
+ statement: `return withUnwrap(${call})`
741
922
  };
742
923
  }
743
924
  /**
744
- * Prints the `validator: { … }` entry of a call config from the hooks named in `keys`, or `null` when none is set.
925
+ * The stream types an operation file imports from the client for one streamed operation: the result and options types
926
+ * for `returnType`, their `WithHeaders` form when the response declares headers, and `SuccessOf` with `TypedEvent` or
927
+ * `ServerSentEvent` for an item type the call spells out.
745
928
  */
746
- function buildValidatorLiteral(hooks, keys) {
747
- const entries = keys.flatMap((key) => hooks?.[key] ? [`${key}: ${hooks[key]}`] : []);
748
- return entries.length ? `validator: { ${entries.join(", ")} }` : null;
929
+ function buildEventStreamImports({ node, types, returnType }) {
930
+ const shape = getEventStream(node) ?? "data";
931
+ const headers = buildResponseHeadersType({
932
+ node,
933
+ types
934
+ }) !== null;
935
+ const result = {
936
+ plain: ["Options", "StreamResult"],
937
+ full: [
938
+ "StreamOptions",
939
+ "Unwrappable",
940
+ "StreamResult"
941
+ ],
942
+ data: ["StreamOptions", "UnwrappedStreamResult"]
943
+ }[returnType].map((name) => name.endsWith("Result") ? resultTypeName(name, headers) : name);
944
+ const item = {
945
+ event: ["SuccessOf", "TypedEvent"],
946
+ data: headers ? ["SuccessOf", "ServerSentEvent"] : [],
947
+ line: ["SuccessOf"]
948
+ }[shape];
949
+ return [...result, ...item];
749
950
  }
750
- // ../../internals/client/src/builders/contentType.ts
951
+ // src/builders/contentType.ts
751
952
  /**
752
- * Builds the `contentType` entry baked into the call config, or `null` without a request body.
953
+ * Builds the `contentType` entry baked into the call config. It holds the request default when the operation has a body
954
+ * and `response` set to the streamed media type when it streams items, or `text/event-stream` when it offers events next to
955
+ * another type, and is `null` with neither.
753
956
  *
754
- * `merge` is `true` when the generated function must destructure `contentType` from `options` and
755
- * merge it over the baked request default, because the caller can also pick a response content type.
957
+ * `merge` is `true` when the generated function must destructure `contentType` from `options` and merge it over the
958
+ * baked defaults, because the caller can also pick a response content type or a stream must keep its `Accept`.
756
959
  */
757
960
  function buildContentType({ node }) {
758
961
  const { defaultContentType } = getContentTypeInfo(node);
759
- const requestContentType = Boolean(node.requestBody?.content?.[0]?.schema) ? defaultContentType : null;
962
+ const requestContentType = Boolean(ast.http.requestBody.getSchema(node)) ? defaultContentType : null;
963
+ const streamType = isEventStream(node) ? ast.mediaType.getBase(getResponseContentTypeInfo(node).defaultContentType) : null;
964
+ const response = streamType || hasEventStreamContent(node) ? `response: ${text.quote(streamType ?? "text/event-stream")}` : null;
760
965
  if (!requestContentType) return {
761
- literal: null,
966
+ literal: response && `contentType: { ${response} }`,
762
967
  merge: false
763
968
  };
764
- const merge = getResponseContentTypeInfo(node).isMultipleContentTypes;
969
+ const merge = Boolean(response) || getResponseContentTypeInfo(node).isMultipleContentTypes;
970
+ const defaults = [`request: ${text.quote(requestContentType)}`, response].filter(Boolean).join(", ");
765
971
  return {
766
972
  merge,
767
- literal: merge ? `contentType: { request: '${requestContentType}', ...(typeof contentType === 'string' ? { request: contentType } : contentType) }` : `contentType: { request: '${requestContentType}' }`
973
+ literal: merge ? `contentType: { ${defaults}, ...(typeof contentType === 'string' ? { request: contentType } : contentType) }` : `contentType: { ${defaults} }`
768
974
  };
769
975
  }
770
- // ../../internals/client/src/builders/styles.ts
976
+ // src/builders/styles.ts
771
977
  /**
772
978
  * Renders a parameter name as an object-literal key, quoted when it is not a bare identifier. Every
773
979
  * location keeps the spec name: the runtime looks path styles up by the `{name}` placeholder in the
@@ -779,27 +985,35 @@ function toKey(name) {
779
985
  /**
780
986
  * Serializes one parameter's metadata into a `{ style, explode, allowReserved, contentType }`
781
987
  * literal, or `null` when the parameter carries none. Path and query carry the serialization
782
- * `style`; header and cookie use a fixed style (`simple` and `form`), so only `explode` is emitted
783
- * for them. `allowReserved` only applies to query parameters, and `contentType` to every location
784
- * except path.
988
+ * `style`. Header and cookie have a fixed style (`simple` and `form`), so they emit only `explode`,
989
+ * plus `style: 'cookie'` when a cookie parameter sets the OpenAPI 3.2 `cookie` style.
990
+ * `allowReserved` only applies to query parameters, and `contentType` to every location except path.
785
991
  */
786
992
  function serializeParameter(parameter) {
787
- const parts = [];
788
- if ((parameter.in === "path" || parameter.in === "query") && parameter.style) parts.push(`style: '${parameter.style}'`);
789
- if (parameter.explode !== void 0) parts.push(`explode: ${parameter.explode}`);
790
- if (parameter.in === "query" && parameter.allowReserved !== void 0) parts.push(`allowReserved: ${parameter.allowReserved}`);
791
- if (parameter.in !== "path" && parameter.contentType) parts.push(`contentType: '${parameter.contentType}'`);
792
- if (parameter.in === "querystring") parts.push("querystring: true");
993
+ const parts = [
994
+ (parameter.in === "path" || parameter.in === "query") && parameter.style && `style: '${parameter.style}'`,
995
+ parameter.in === "cookie" && parameter.style === "cookie" && `style: 'cookie'`,
996
+ parameter.explode !== void 0 && `explode: ${parameter.explode}`,
997
+ parameter.in === "query" && parameter.allowReserved !== void 0 && `allowReserved: ${parameter.allowReserved}`,
998
+ parameter.in !== "path" && parameter.contentType && `contentType: ${text.quote(parameter.contentType)}`,
999
+ parameter.in === "querystring" && "querystring: true"
1000
+ ].filter(Boolean);
793
1001
  return parts.length > 0 ? `{ ${parts.join(", ")} }` : null;
794
1002
  }
795
1003
  /**
796
- * Serializes the `encoding` of the request body's default content type into a `{ property: { ... } }`
797
- * literal, or `null` when it carries none. Only an operation with a single request content type
1004
+ * Whether a query parameter of the operation has a style entry, so the call needs the styled query serializer.
1005
+ */
1006
+ function hasQueryStyles(node) {
1007
+ return ast.http.isOperation(node) && ast.http.groupParameters(node).query.some((parameter) => serializeParameter(parameter) !== null);
1008
+ }
1009
+ /**
1010
+ * Serializes the `encoding` of the request body's default content type into `property: { ... }`
1011
+ * entries, empty when it carries none. Only an operation with a single request content type
798
1012
  * emits it, because the runtime applies `styles.body` to whichever type the caller selects.
799
1013
  */
800
1014
  function serializeBodyEncoding(node) {
801
1015
  const content = node.requestBody?.content;
802
- const entries = Object.entries(content?.length === 1 ? content[0]?.encoding ?? {} : {}).map(([property, { contentType, style, explode, allowReserved }]) => {
1016
+ return Object.entries(content?.length === 1 ? content[0]?.encoding ?? {} : {}).map(([property, { contentType, style, explode, allowReserved }]) => {
803
1017
  const parts = [
804
1018
  style && `style: ${JSON.stringify(style)}`,
805
1019
  explode !== void 0 && `explode: ${explode}`,
@@ -808,7 +1022,6 @@ function serializeBodyEncoding(node) {
808
1022
  ].filter(Boolean);
809
1023
  return `${isValidVarName(property) ? property : JSON.stringify(property)}: { ${parts.join(", ")} }`;
810
1024
  });
811
- return entries.length > 0 ? `body: { ${entries.join(", ")} }` : null;
812
1025
  }
813
1026
  /**
814
1027
  * Builds the per-operation `styles` literal from the operation's parameters, grouped by location.
@@ -818,83 +1031,94 @@ function serializeBodyEncoding(node) {
818
1031
  * is unchanged. A form request body adds a `body` group keyed by property name from its `encoding`.
819
1032
  * Returns `null` when nothing carries metadata.
820
1033
  *
1034
+ * With `merge`, the literal spreads `config.styles` per parameter, so a per-call entry overrides the generated one for
1035
+ * that parameter and leaves the others in place.
1036
+ *
821
1037
  * @example
822
1038
  * ```ts
823
1039
  * // a path param with { style: 'matrix', explode: true } and a query param with { explode: false }
824
1040
  * buildStyles({ node }) // "{ path: { id: { style: 'matrix', explode: true } }, query: { tags: { explode: false } } }"
825
1041
  * ```
826
1042
  */
827
- function buildStyles({ node }) {
828
- if (!ast.isHttpOperationNode(node)) return null;
1043
+ function buildStyles({ node, merge }) {
1044
+ if (!ast.http.isOperation(node)) return null;
829
1045
  const groups = {
830
1046
  path: [],
831
1047
  query: [],
832
1048
  header: [],
833
- cookie: []
1049
+ cookie: [],
1050
+ body: serializeBodyEncoding(node)
834
1051
  };
835
- for (const [location, parameters] of Object.entries(getOperationParameters(node))) for (const parameter of parameters) {
1052
+ for (const [location, parameters] of Object.entries(ast.http.groupParameters(node))) for (const parameter of parameters) {
836
1053
  const literal = serializeParameter(parameter);
837
1054
  if (literal) groups[location].push(`${toKey(parameter.name)}: ${literal}`);
838
1055
  }
839
- const entries = [...Object.keys(groups).filter((location) => groups[location].length > 0).map((location) => `${location}: { ${groups[location].join(", ")} }`), serializeBodyEncoding(node)].filter(Boolean);
840
- return entries.length > 0 ? `{ ${entries.join(", ")} }` : null;
1056
+ const entries = Object.entries(groups).filter(([, items]) => items.length > 0).map(([location, items]) => `${location}: { ${(merge ? [...items, `...config.styles?.${location}`] : items).join(", ")} }`);
1057
+ if (entries.length === 0) return null;
1058
+ return `{ ${(merge ? ["...config.styles", ...entries] : entries).join(", ")} }`;
841
1059
  }
842
- // ../../internals/client/src/builders/callConfig.ts
1060
+ // src/builders/callConfig.ts
843
1061
  /**
844
- * `responseType` baked into a call. One parser is a string. Several parsers become a map from content
845
- * type to parser so the runtime can choose from the response `Content-Type`.
1062
+ * `parseAs: 'blob'` when every success content type is binary, so a download is a `Blob` whatever its media type, and
1063
+ * also when the server sends no `Content-Type`. Any other body is decoded by its media type at runtime.
846
1064
  */
847
- function buildResponseTypeLiteral(node) {
848
- const responseType = getResponseType(node);
849
- if (responseType) return `responseType: '${responseType}'`;
850
- const responseTypes = getResponseTypeMap(node);
851
- if (!responseTypes) return null;
852
- return `responseType: { ${Object.entries(responseTypes).map(([contentType, type]) => `${JSON.stringify(contentType)}: '${type}'`).join(", ")} }`;
1065
+ function buildParseAsLiteral(content) {
1066
+ return content.length > 0 && content.every((entry) => entry.schema?.type === "blob") ? `parseAs: 'blob'` : null;
853
1067
  }
854
1068
  /**
855
1069
  * Builds the call config literal a generated operation forwards to the client, shared by the standalone
856
- * functions and the SDK methods. The `throwOnError` fallback is applied after the `...config` spread.
1070
+ * functions and the SDK methods. The operation's own `baseURL` comes before the `...config` spread, so a per-call one wins. The
1071
+ * `validator` and `throwOnError` entries come after it, so a per-call `validator` is merged per key with the generated one instead
1072
+ * of replacing it. `styles` is merged the same way, per parameter, unless `returnType` is `'plain'`.
857
1073
  *
858
1074
  * `mergeContentType` is `true` when the caller must destructure `contentType` from `options`, and
859
- * `eventStream` is `true` when the operation returns `text/event-stream`. Unless `returnType` is `'plain'`, a
860
- * stream's literal leaves the `response` validator to `toEventStream`, which runs it per event.
1075
+ * `eventStream` is `true` when the operation streams `text/event-stream` or JSON Lines. A Server-Sent Events call
1076
+ * carries its `eventShape`, which tells the runtime what the `response` validator checks on each event, and `rawData` when
1077
+ * the document types each event's `data` as a string, so the runtime keeps it as text.
861
1078
  */
862
- function buildCallConfig({ node, validatorHooks, security, throwOnErrorDefault, returnType }) {
863
- const eventStream = isEventStream(node);
1079
+ function buildCallConfig({ node, validatorHooks, security, baseURL, throwOnErrorDefault, returnType }) {
1080
+ const stream = getEventStream(node);
1081
+ const content = ast.http.responses.findPrimary(node)?.content ?? [];
864
1082
  const securityLiteral = buildSecurityMetadata({ security });
865
- const stylesLiteral = buildStyles({ node });
866
- const validatorLiteral = buildValidatorLiteral(validatorHooks, eventStream && returnType !== "plain" ? [
867
- "request",
868
- "path",
869
- "query",
870
- "headers",
871
- "error"
872
- ] : [
1083
+ const ownRuntime = returnType !== "plain";
1084
+ const stylesLiteral = buildStyles({
1085
+ node,
1086
+ merge: ownRuntime
1087
+ });
1088
+ const styles = stylesLiteral ? `styles: ${stylesLiteral}` : null;
1089
+ const validatorLiteral = buildValidatorLiteral(validatorHooks, [
873
1090
  "request",
874
1091
  "path",
875
1092
  "query",
876
1093
  "headers",
1094
+ "cookies",
877
1095
  "response",
878
1096
  "error"
879
1097
  ]);
880
1098
  const { literal: contentTypeLiteral, merge: mergeContentType } = buildContentType({ node });
1099
+ const throwOnError = ownRuntime ? `throwOnError: resolveThrowOnError(config.throwOnError, ${throwOnErrorDefault})` : `throwOnError: config.throwOnError ?? ${throwOnErrorDefault}`;
1100
+ const rawData = hasTextEventData(node);
881
1101
  return {
882
1102
  callConfig: `{ ${[
883
- `method: '${node.method.toUpperCase()}'`,
884
- `url: '${node.path}'`,
1103
+ `method: ${text.quote(node.method.toUpperCase())}`,
1104
+ `url: ${text.quote(node.path)}`,
1105
+ baseURL ? `baseURL: ${JSON.stringify(baseURL)}` : null,
885
1106
  securityLiteral ? `security: ${securityLiteral}` : null,
886
- stylesLiteral ? `styles: ${stylesLiteral}` : null,
887
- validatorLiteral,
1107
+ ownRuntime ? null : styles,
888
1108
  contentTypeLiteral,
889
- buildResponseTypeLiteral(node),
1109
+ buildParseAsLiteral(content),
1110
+ stream === "event" || stream === "data" ? `eventShape: '${stream}'` : null,
1111
+ rawData ? `rawData: ${toLiteral(rawData)}` : null,
890
1112
  "...config",
891
- `throwOnError: config.throwOnError ?? ${throwOnErrorDefault}`
1113
+ ownRuntime ? styles : null,
1114
+ validatorLiteral,
1115
+ throwOnError
892
1116
  ].filter(Boolean).join(", ")} }`,
893
1117
  mergeContentType,
894
- eventStream
1118
+ eventStream: stream !== null
895
1119
  };
896
1120
  }
897
- // ../../internals/client/src/builders/signature.ts
1121
+ // src/builders/signature.ts
898
1122
  const declarationPrinter = functionPrinter({ mode: "declaration" });
899
1123
  /**
900
1124
  * Builds the grouped-options signature for one operation: a single `options` object whose `TData`
@@ -923,17 +1147,18 @@ function buildGroupedOptionsSignature({ node, types, returnType, throwOnErrorDef
923
1147
  generics: [`ThrowOnError extends boolean = ${throwOnErrorDefault}`]
924
1148
  };
925
1149
  }
926
- // ../../internals/client/src/builders/operationBody.ts
1150
+ // src/builders/operationBody.ts
927
1151
  /**
928
1152
  * The pieces of a generated operation: its generics, `options` parameter, return type, return statement, and whether the
929
1153
  * body destructures `contentType` from `options`. Shared by the standalone functions and the SDK methods, so a
930
- * `text/event-stream` operation is wired to `toEventStream` in one place.
1154
+ * streamed operation is wired to `apiClient.stream` in one place.
931
1155
  */
932
- function buildOperationBody({ node, types, validatorHooks, security, returnType, throwOnErrorDefault }) {
1156
+ function buildOperationBody({ node, types, validatorHooks, security, baseURL, returnType, throwOnErrorDefault }) {
933
1157
  const { callConfig, mergeContentType, eventStream } = buildCallConfig({
934
1158
  node,
935
1159
  validatorHooks,
936
1160
  security,
1161
+ baseURL,
937
1162
  throwOnErrorDefault,
938
1163
  returnType
939
1164
  });
@@ -949,7 +1174,6 @@ function buildOperationBody({ node, types, validatorHooks, security, returnType,
949
1174
  types,
950
1175
  callConfig,
951
1176
  returnType,
952
- validatorHooks,
953
1177
  throwOnErrorDefault
954
1178
  }) : null;
955
1179
  return {
@@ -966,20 +1190,21 @@ function buildOperationBody({ node, types, validatorHooks, security, returnType,
966
1190
  mergeContentType
967
1191
  };
968
1192
  }
969
- // ../../internals/client/src/components/Operation.tsx
1193
+ // src/components/Operation.tsx
970
1194
  /**
971
1195
  * Renders one client operation: the grouped `<Name>Request` type and the function that forwards a
972
1196
  * single `options` object to the resolved client and returns the `Unwrappable<RequestResult>`. The
973
1197
  * type, signature, and call config are built with the AST factory, and only the jsx-renderer emits
974
1198
  * the source.
975
1199
  */
976
- function Operation({ name, node, types, validatorHooks, returnType, throwOnErrorDefault, security }) {
977
- if (!ast.isHttpOperationNode(node)) return null;
1200
+ function Operation({ name, node, types, validatorHooks, returnType, throwOnErrorDefault, security, baseURL }) {
1201
+ if (!ast.http.isOperation(node)) return null;
978
1202
  const body = buildOperationBody({
979
1203
  node,
980
1204
  types,
981
1205
  validatorHooks,
982
1206
  security,
1207
+ baseURL,
983
1208
  returnType,
984
1209
  throwOnErrorDefault
985
1210
  });
@@ -993,231 +1218,232 @@ function Operation({ name, node, types, validatorHooks, returnType, throwOnError
993
1218
  generics: body.generics,
994
1219
  params: body.paramsSignature,
995
1220
  returnType: body.returnType,
996
- JSDoc: { comments: buildOperationComments(node, {
997
- link: "urlPath",
998
- linkPosition: "beforeDeprecated",
999
- splitLines: true
1000
- }) },
1221
+ JSDoc: { comments: buildOperationComments(node, { splitLines: true }) },
1001
1222
  children: [
1002
- body.mergeContentType ? "const { client: request = client, contentType, ...config } = options" : "const { client: request = client, ...config } = options",
1223
+ body.mergeContentType ? "const { client: apiClient = client, contentType, ...config } = options" : "const { client: apiClient = client, ...config } = options",
1003
1224
  /* @__PURE__ */ jsx("br", {}),
1004
1225
  body.statement
1005
1226
  ]
1006
1227
  })
1007
1228
  });
1008
1229
  }
1009
- // ../../internals/client/src/components/StreamRuntime.tsx
1010
- /**
1011
- * The `footer` of `.kubb/client.ts`: the `baseURL` set at generation time, and the `sse.ts` re-export when the document
1012
- * streams events, so `ServerSentEvent` and `toEventStream` stay importable from the client as in earlier releases.
1013
- */
1014
- function buildClientFooter({ baseURL, sse }) {
1015
- const baseURLExpression = baseURL ? baseURL.includes("${") ? `\`${baseURL.replaceAll("`", "\\`")}\`` : JSON.stringify(baseURL) : void 0;
1016
- return [baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : null, sse ? `export * from './sse.ts'` : null].filter(Boolean).join("\n") || void 0;
1017
- }
1018
- /**
1019
- * Writes the footer of the `.kubb/client.ts` the plugin injected, the one place it is built, and copies `sse.ts` into
1020
- * `.kubb/` only for a document with a `text/event-stream` operation, so a document without streams ships no stream runtime.
1021
- * `sse` is set by the plugins that ship the runtime, so a custom client from `@kubb/plugin-client` gets neither file.
1022
- */
1023
- function StreamRuntime({ nodes, root, sse, baseURL }) {
1024
- if (!sse) return null;
1025
- const streams = nodes.some(isEventStream);
1026
- const footer = buildClientFooter({
1027
- baseURL,
1028
- sse: streams
1029
- });
1030
- return /* @__PURE__ */ jsxs(Fragment, { children: [streams && /* @__PURE__ */ jsx(File, {
1031
- baseName: "sse.ts",
1032
- path: path.resolve(root, ".kubb/sse.ts"),
1033
- copy: sse
1034
- }), footer && /* @__PURE__ */ jsx(File, {
1035
- baseName: "client.ts",
1036
- path: path.resolve(root, ".kubb/client.ts"),
1037
- footer
1038
- })] });
1230
+ // src/components/ClientRuntime.tsx
1231
+ function buildServersExport(servers) {
1232
+ const named = ast.server.groupByName(servers);
1233
+ if (!named.size) return null;
1234
+ return `/**\n * The servers from the spec by name, for \`baseURL\`.\n */\nexport const servers = { ${[...named].map(([name, { url }]) => `${objectKey(name)}: ${text.quote(url)}`).join(", ")} } as const`;
1039
1235
  }
1040
- // ../../internals/client/src/resolveOperationTypes.ts
1236
+ const formMediaTypes = {
1237
+ "multipart/form-data": {
1238
+ file: "mediaTypes/formData",
1239
+ name: "multipartFormData"
1240
+ },
1241
+ "application/x-www-form-urlencoded": {
1242
+ file: "mediaTypes/urlencoded",
1243
+ name: "urlencoded"
1244
+ }
1245
+ };
1041
1246
  /**
1042
- * A `plugin-ts` shaped resolver whose operation type names point at the types `pluginZod({ inferred })`
1043
- * exports next to each schema, so code written against `plugin-ts` names picks up the zod types.
1247
+ * Writes the runtime a document needs into `.kubb/`: the optional media type, serializer and `sse.ts` files, and a
1248
+ * `config.ts` that imports them, which `client.ts` passes to `setConfig`. The footer of `client.ts` gets the named
1249
+ * `servers` and `securitySchemes` and the `sse.ts` re-export. `templates` is the folder the runtime is copied from, set
1250
+ * by the plugins that ship it, so a custom client from `@kubb/plugin-client` gets none of this.
1044
1251
  */
1045
- function toZodTypeResolver(resolver) {
1046
- const type = (name) => resolver.schema.type(name);
1047
- return {
1048
- ...resolver,
1049
- file: resolver.file.bind(resolver),
1050
- memo: (key, compute) => resolver.memo(`zodTypes:${key}`, compute),
1051
- param: {
1052
- name: (node, p) => type(resolver.param.name(node, p)),
1053
- path: (node, p) => type(resolver.param.path(node, p)),
1054
- query: (node, p) => type(resolver.param.query(node, p)),
1055
- headers: (node, p) => type(resolver.param.headers(node, p))
1252
+ function ClientRuntime({ nodes, root, templates, baseURL, servers, auth }) {
1253
+ if (!templates) return null;
1254
+ const hasParameter = (match) => nodes.some((node) => node.parameters.some(match));
1255
+ const streams = nodes.some((node) => hasEventStreamContent(node) || isEventStream(node) || hasStreamVariant(node));
1256
+ const forms = [...new Set(nodes.flatMap(getFormContentTypes))].sort().map((contentType) => ({
1257
+ contentType,
1258
+ ...formMediaTypes[contentType]
1259
+ }));
1260
+ const serializers = [
1261
+ nodes.some(hasQueryStyles) ? {
1262
+ key: "query",
1263
+ file: "serializers/queryStyles",
1264
+ name: "serializeStyledQuery"
1265
+ } : {
1266
+ key: "query",
1267
+ file: "serializers/query",
1268
+ name: "serializeQuery"
1056
1269
  },
1057
- response: {
1058
- body: (node) => type(resolver.response.body(node)),
1059
- status: (node, statusCode) => type(resolver.response.status(node, statusCode)),
1060
- response: (node) => type(resolver.response.response(node)),
1061
- responses: (node) => type(resolver.response.responses(node)),
1062
- options: (node) => resolver.response.options(node)
1063
- }
1064
- };
1065
- }
1066
- /**
1067
- * Picks the plugin generated clients, query hooks, and MCP handlers take their operation types from.
1068
- *
1069
- * `pluginZod({ inferred: 'codec' })` wins, so a codec param takes its domain value. Otherwise `plugin-ts`
1070
- * wins when it is in the pipeline, and without it any `pluginZod({ inferred })`. Throws
1071
- * `KUBB_MISSING_DEPENDENCY` for `pluginName` when neither is available.
1072
- *
1073
- * @example
1074
- * ```ts
1075
- * const types = resolveOperationTypes(ctx.driver, ctx.plugin.name)
1076
- * types.response.options(node) // 'GetPetByIdOptions', or 'GetPetByIdOptionsSchemaType' from zod
1077
- * ```
1078
- */
1079
- function resolveOperationTypes(driver, pluginName) {
1080
- return driver.memo(`operationTypes:${pluginName}`, () => {
1081
- const pluginTs = driver.getPlugin(pluginTsName);
1082
- const pluginZod = driver.getPlugin(pluginZodName);
1083
- if (pluginTs && pluginZod?.options?.inferred !== "codec") {
1084
- const resolver = driver.getResolver(pluginTsName);
1085
- return {
1086
- response: resolver.response,
1087
- resolver,
1088
- output: pluginTs.options?.output,
1089
- group: pluginTs.options?.group ?? void 0
1090
- };
1091
- }
1092
- if (!pluginZod?.options?.inferred) throw new Diagnostics.Error({
1093
- code: Diagnostics.code.missingDependency,
1094
- severity: "error",
1095
- message: `Plugin "${pluginName}" has no source for operation types.`,
1096
- help: `Add \`pluginTs()\` before "${pluginName}", or add \`pluginZod({ inferred: true })\`.`,
1097
- location: { kind: "config" },
1098
- plugin: pluginName
1099
- });
1100
- const resolver = toZodTypeResolver(driver.getResolver(pluginZodName));
1101
- return {
1102
- response: resolver.response,
1103
- resolver,
1104
- output: pluginZod.options.output,
1105
- group: pluginZod.options.group ?? void 0
1106
- };
1107
- });
1270
+ hasParameter((parameter) => parameter.in === "path" && (parameter.style === "label" || parameter.style === "matrix")) ? {
1271
+ key: "path",
1272
+ file: "serializers/pathStyles",
1273
+ name: "serializeStyledPath"
1274
+ } : {
1275
+ key: "path",
1276
+ file: "serializers/path",
1277
+ name: "serializePath"
1278
+ },
1279
+ hasParameter((parameter) => parameter.in === "header") ? {
1280
+ key: "header",
1281
+ file: "serializers/headers",
1282
+ name: "serializeHeaders"
1283
+ } : null,
1284
+ hasParameter((parameter) => parameter.in === "cookie") ? {
1285
+ key: "cookie",
1286
+ file: "serializers/cookies",
1287
+ name: "serializeCookies"
1288
+ } : null
1289
+ ].filter((serializer) => serializer !== null);
1290
+ const imports = [
1291
+ ...forms,
1292
+ ...streams ? [{
1293
+ file: "sse",
1294
+ name: "streamMediaTypes"
1295
+ }] : [],
1296
+ ...serializers
1297
+ ];
1298
+ const files = /* @__PURE__ */ new Set([
1299
+ ...imports.map(({ file }) => file),
1300
+ "serializers/path",
1301
+ ...forms.some(({ name }) => name === "urlencoded") ? ["serializers/queryStyles"] : []
1302
+ ]);
1303
+ const baseURLExpression = baseURL ? baseURL.includes("${") ? `\`${baseURL.replaceAll("`", "\\`")}\`` : JSON.stringify(baseURL) : void 0;
1304
+ const mediaTypes = [...forms.map(({ contentType, name }) => `'${contentType}': ${name}`), streams ? "...streamMediaTypes" : null].filter(Boolean);
1305
+ const config = [
1306
+ baseURLExpression ? `baseURL: ${baseURLExpression}` : null,
1307
+ mediaTypes.length ? `mediaTypes: { ${mediaTypes.join(", ")} }` : null,
1308
+ `serializer: { ${serializers.map(({ key, name }) => `${key}: ${name}`).join(", ")} }`
1309
+ ].filter(Boolean);
1310
+ const footer = [
1311
+ buildServersExport(servers),
1312
+ buildSecuritySchemes(auth),
1313
+ streams ? `export * from './sse.ts'` : null
1314
+ ].filter(Boolean).join("\n");
1315
+ const configPath = path.resolve(root, ".kubb/config.ts");
1316
+ const runtimePath = (file) => path.resolve(root, ".kubb", `${file}.ts`);
1317
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1318
+ [...files].map((file) => /* @__PURE__ */ jsx(File, {
1319
+ baseName: `${path.basename(file)}.ts`,
1320
+ path: runtimePath(file),
1321
+ copy: path.join(templates, `${file}.ts`)
1322
+ }, file)),
1323
+ /* @__PURE__ */ jsxs(File, {
1324
+ baseName: "config.ts",
1325
+ path: configPath,
1326
+ children: [
1327
+ imports.map(({ file, name }) => /* @__PURE__ */ jsx(File.Import, {
1328
+ name: [name],
1329
+ root: configPath,
1330
+ path: runtimePath(file)
1331
+ }, name)),
1332
+ /* @__PURE__ */ jsx(File.Import, {
1333
+ name: ["ClientConfig"],
1334
+ root: configPath,
1335
+ path: runtimePath("core"),
1336
+ isTypeOnly: true
1337
+ }),
1338
+ /* @__PURE__ */ jsx(File.Source, {
1339
+ name: "defaultConfig",
1340
+ isExportable: true,
1341
+ children: `/**\n * The config \`client.ts\` starts with: what this spec needs from the runtime.\n */\nexport const defaultConfig: ClientConfig = { ${config.join(", ")} }`
1342
+ })
1343
+ ]
1344
+ }),
1345
+ footer && /* @__PURE__ */ jsx(File, {
1346
+ baseName: "client.ts",
1347
+ path: path.resolve(root, ".kubb/client.ts"),
1348
+ footer
1349
+ })
1350
+ ] });
1108
1351
  }
1109
- // ../../internals/client/src/generators/clientGenerator.tsx
1352
+ // src/generators/clientGenerator.tsx
1110
1353
  /**
1111
- * Builds the built-in per-operation generator shared by the client plugins (`@kubb/plugin-fetch`,
1112
- * `@kubb/plugin-axios`). Emits one async function per OpenAPI operation using the shared
1113
- * `Operation` component: a grouped `<Name>Request` type and a function that forwards a single
1114
- * `options` object to the bundled `client` and returns the `Unwrappable<RequestResult>`. Only the
1115
- * generator `name` and the runtime templates differ between plugins. Every other resolution, import,
1116
- * and rendering step is identical.
1117
- *
1118
- * `sse` is the Server-Sent Events runtime template, copied into `.kubb/sse.ts` only for a document with a
1119
- * `text/event-stream` operation.
1354
+ * Builds the per-operation client generator shared by the client plugins. `templates` is the folder the optional runtime, such as `sse.ts`, is copied from.
1120
1355
  */
1121
- function createClientGenerator(name, { sse } = {}) {
1356
+ function createClientGenerator(name, { templates } = {}) {
1122
1357
  return defineGenerator({
1123
1358
  name,
1124
1359
  renderer: jsxRenderer,
1125
1360
  operations(nodes, ctx) {
1126
- return /* @__PURE__ */ jsx(StreamRuntime, {
1361
+ return /* @__PURE__ */ jsx(ClientRuntime, {
1127
1362
  nodes,
1128
1363
  root: ctx.root,
1129
- sse,
1130
- baseURL: ctx.options.baseURL
1364
+ templates,
1365
+ baseURL: ctx.options.baseURL,
1366
+ servers: ctx.meta.servers,
1367
+ auth: ctx.meta.auth
1131
1368
  });
1132
1369
  },
1133
1370
  operation(node, ctx) {
1134
- if (!ast.isHttpOperationNode(node)) return null;
1371
+ if (!ast.http.isOperation(node)) return null;
1135
1372
  const { config, driver, resolver, root } = ctx;
1136
- const { output, validator, returnType, throwOnErrorDefault, group, importPath } = ctx.options;
1137
- const types = resolveOperationTypes(driver, ctx.plugin.name);
1138
- const pluginZod = isValidatorEnabled(validator) ? driver.getPlugin(pluginZodName) : null;
1139
- const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
1140
- const importedTypeNames = [types.response.options(node), types.response.responses(node)];
1141
- const validatorHooks = buildValidatorHooks({
1373
+ const { output, returnType, throwOnErrorDefault, group, importPath } = ctx.options;
1374
+ const resolvedTypes = resolveOperationTypes(driver, ctx.plugin.name);
1375
+ const types = importPath === void 0 ? resolvedTypes : {
1376
+ ...resolvedTypes,
1377
+ response: {
1378
+ ...resolvedTypes.response,
1379
+ headers: void 0
1380
+ }
1381
+ };
1382
+ const data = resolveOperationData({
1142
1383
  node,
1143
- validator,
1144
- zodResolver
1384
+ ctx,
1385
+ types
1145
1386
  });
1146
- const importedZodNames = validatorHooks.importedZodNames;
1147
- const operationName = resolver.name(node.operationId);
1148
- const meta = {
1149
- name: resolver.resolveName({
1150
- key: operationKey(node),
1151
- name: operationName
1152
- }),
1153
- file: resolver.file({
1154
- ...operationFileEntry(node, node.operationId),
1155
- root,
1156
- output,
1157
- group: group ?? void 0
1158
- }),
1159
- fileTypes: resolveDependencyOperationFile({
1160
- memo: ctx.memo,
1161
- node,
1162
- resolver: types.resolver,
1163
- root,
1164
- output: types.output ?? output,
1165
- group: types.group
1166
- }),
1167
- fileZod: zodResolver && pluginZod?.options ? zodResolver.file({
1168
- ...operationFileEntry(node, node.operationId),
1169
- root,
1170
- output: pluginZod.options.output ?? output,
1171
- group: pluginZod.options?.group ?? void 0
1172
- }) : null
1173
- };
1174
- const security = getOperationSecurity({
1175
- document: ctx.adapter.document,
1176
- method: node.method,
1177
- path: node.path
1387
+ const { validatorHooks, headerTypeNames } = data;
1388
+ const file = resolver.file({
1389
+ ...operationFileEntry(node, node.operationId),
1390
+ root,
1391
+ output,
1392
+ group: group ?? void 0
1178
1393
  });
1394
+ const importedTypeNames = [
1395
+ types.response.options(node),
1396
+ types.response.responses(node),
1397
+ ...headerTypeNames
1398
+ ];
1179
1399
  const clientPath = importPath ?? path.resolve(root, ".kubb/client.ts");
1180
- const clientRoot = importPath === void 0 ? meta.file.path : void 0;
1400
+ const clientRoot = importPath === void 0 ? file.path : void 0;
1181
1401
  const standardSchemaPath = importPath ?? path.resolve(root, ".kubb/standardSchema.ts");
1182
- const ssePath = importPath ?? path.resolve(root, ".kubb/sse.ts");
1183
1402
  const stream = getEventStream(node);
1184
- const clientValueImports = (() => {
1185
- if (returnType === "plain") return ["client"];
1186
- if (returnType === "data") return ["client", "unwrapResult"];
1187
- return ["client", "withUnwrap"];
1188
- })();
1189
- const clientTypeImports = (() => {
1190
- if (stream) return {
1191
- plain: ["Options", "SuccessOf"],
1192
- data: ["SuccessOf"],
1193
- full: ["SuccessOf", "Unwrappable"]
1194
- }[returnType];
1195
- if (returnType === "plain") return ["Options", "RequestResult"];
1196
- if (returnType === "data") return ["Options", "UnwrappedResult"];
1197
- return [
1403
+ const clientValueImports = [...{
1404
+ plain: ["client"],
1405
+ data: [
1406
+ "client",
1407
+ "unwrapResult",
1408
+ "resolveThrowOnError"
1409
+ ],
1410
+ full: [
1411
+ "client",
1412
+ "withUnwrap",
1413
+ "resolveThrowOnError"
1414
+ ]
1415
+ }[returnType], ...importPath === void 0 && data.security ? ["securitySchemes"] : []];
1416
+ const clientTypeImports = stream ? buildEventStreamImports({
1417
+ node,
1418
+ types,
1419
+ returnType
1420
+ }) : {
1421
+ plain: ["Options", "RequestResult"],
1422
+ data: ["Options", "UnwrappedResult"],
1423
+ full: [
1198
1424
  "Options",
1199
1425
  "Unwrappable",
1200
1426
  "RequestResult"
1201
- ];
1202
- })();
1427
+ ]
1428
+ }[returnType].map((name) => name.endsWith("Result") ? resultTypeName(name, headerTypeNames.length > 0) : name);
1203
1429
  return /* @__PURE__ */ jsxs(File, {
1204
- baseName: meta.file.baseName,
1205
- path: meta.file.path,
1206
- meta: meta.file.meta,
1430
+ baseName: file.baseName,
1431
+ path: file.path,
1432
+ meta: file.meta,
1207
1433
  banner: resolver.default.banner(ctx.meta, {
1208
1434
  output,
1209
1435
  config,
1210
1436
  file: {
1211
- path: meta.file.path,
1212
- baseName: meta.file.baseName
1437
+ path: file.path,
1438
+ baseName: file.baseName
1213
1439
  }
1214
1440
  }),
1215
1441
  footer: resolver.default.footer(ctx.meta, {
1216
1442
  output,
1217
1443
  config,
1218
1444
  file: {
1219
- path: meta.file.path,
1220
- baseName: meta.file.baseName
1445
+ path: file.path,
1446
+ baseName: file.baseName
1221
1447
  }
1222
1448
  }),
1223
1449
  children: [
@@ -1232,30 +1458,16 @@ function createClientGenerator(name, { sse } = {}) {
1232
1458
  path: clientPath,
1233
1459
  isTypeOnly: true
1234
1460
  }),
1235
- stream && /* @__PURE__ */ jsx(File.Import, {
1236
- name: ["toEventStream"],
1237
- root: clientRoot,
1238
- path: ssePath
1239
- }),
1240
- stream && /* @__PURE__ */ jsx(File.Import, {
1241
- name: buildEventStreamImports({
1242
- returnType,
1243
- typed: stream === "event"
1244
- }),
1245
- root: clientRoot,
1246
- path: ssePath,
1247
- isTypeOnly: true
1248
- }),
1249
- meta.fileTypes && importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
1461
+ /* @__PURE__ */ jsx(File.Import, {
1250
1462
  name: Array.from(new Set(importedTypeNames)),
1251
- root: meta.file.path,
1252
- path: meta.fileTypes.path,
1463
+ root: file.path,
1464
+ path: data.typeFile.path,
1253
1465
  isTypeOnly: true
1254
1466
  }),
1255
- meta.fileZod && importedZodNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
1256
- name: importedZodNames,
1257
- root: meta.file.path,
1258
- path: meta.fileZod.path
1467
+ data.zodFile && validatorHooks.importedZodNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
1468
+ name: validatorHooks.importedZodNames,
1469
+ root: file.path,
1470
+ path: data.zodFile.path
1259
1471
  }),
1260
1472
  validatorHooks.headers && /* @__PURE__ */ jsx(File.Import, {
1261
1473
  name: ["toHeadersStandardSchema"],
@@ -1263,27 +1475,28 @@ function createClientGenerator(name, { sse } = {}) {
1263
1475
  path: standardSchemaPath
1264
1476
  }),
1265
1477
  /* @__PURE__ */ jsx(Operation, {
1266
- name: meta.name,
1478
+ name: data.name,
1267
1479
  node,
1268
1480
  types,
1269
1481
  validatorHooks,
1270
1482
  returnType,
1271
1483
  throwOnErrorDefault,
1272
- security
1484
+ security: data.security,
1485
+ baseURL: data.baseURL
1273
1486
  })
1274
1487
  ]
1275
1488
  });
1276
1489
  }
1277
1490
  });
1278
1491
  }
1279
- // ../../internals/client/src/macros.ts
1492
+ // src/macros.ts
1280
1493
  /**
1281
1494
  * Macros the client plugins apply by default, ahead of any user macros. `macroDedupeUnion`
1282
1495
  * drops repeated union members, keeping the generated response and error
1283
1496
  * unions tidy. A plugin wires them with `ctx.setMacros([...defaultMacros, ...userMacros])`.
1284
1497
  */
1285
1498
  const defaultMacros = [macroDedupeUnion];
1286
- // ../../internals/client/src/resolver.ts
1499
+ // src/resolver.ts
1287
1500
  /**
1288
1501
  * Default resolver shared by the client plugins. Functions use safe camelCase identifiers and
1289
1502
  * files inherit the built-in casing; classes and tag groups use PascalCase.
@@ -1298,22 +1511,401 @@ const resolverClient = createResolver({
1298
1511
  pluginName: "plugin-contract-client",
1299
1512
  name: resolveOperationName,
1300
1513
  className(name) {
1301
- return ensureValidVarName(pascalCase(name));
1514
+ return ensureValidVarName(casing.pascal(name));
1302
1515
  },
1303
1516
  groupName(name) {
1304
- return ensureValidVarName(pascalCase(`${name} Client`));
1517
+ return ensureValidVarName(casing.pascal(`${name} Client`));
1305
1518
  },
1306
1519
  propertyName(name) {
1307
- return ensureValidVarName(camelCase(name));
1520
+ return ensureValidVarName(casing.camel(name));
1308
1521
  }
1309
1522
  });
1310
- // src/generators/clientGenerator.tsx
1523
+ // src/components/SdkClient.tsx
1524
+ /**
1525
+ * Builds one instance method of a generated SDK class; a per-call `options.client` overrides `this.client`.
1526
+ */
1527
+ function buildSdkMethod({ node, name, types, validatorHooks, security, baseURL, returnType, throwOnErrorDefault }) {
1528
+ if (!ast.http.isOperation(node)) return "";
1529
+ const body = buildOperationBody({
1530
+ node,
1531
+ types,
1532
+ validatorHooks,
1533
+ security,
1534
+ baseURL,
1535
+ returnType,
1536
+ throwOnErrorDefault
1537
+ });
1538
+ const generics = body.generics.length ? `<${body.generics.join(", ")}>` : "";
1539
+ const jsdoc = buildJSDoc(buildOperationComments(node, { splitLines: true }));
1540
+ const methodBody = [
1541
+ body.mergeContentType ? "const { client: apiClient = this.client, contentType, ...config } = options" : "const { client: apiClient = this.client, ...config } = options",
1542
+ "",
1543
+ body.statement
1544
+ ].map((line) => line ? ` ${line}` : "").join("\n");
1545
+ return `${jsdoc} public ${name}${generics}(${body.paramsSignature}): ${body.returnType} {\n${methodBody}\n }`;
1546
+ }
1547
+ /**
1548
+ * Renders one instance class per tag with one method per operation. The constructor takes a client
1549
+ * config object and builds its own client through `createClient`, so each environment is a separate
1550
+ * instance: `const api = new PetClient({ baseURL }); api.getPetById(...)`. It also takes an existing
1551
+ * client, which is how the root SDK shares one client across its tags. The client is public so callers
1552
+ * can add interceptors, and a per-call `client` option still overrides it for a one-off call.
1553
+ */
1554
+ function SdkClient({ name, operations, returnType, throwOnErrorDefault }) {
1555
+ const methods = operations.map(({ node, name: methodName, types, validatorHooks, security, baseURL }) => buildSdkMethod({
1556
+ node,
1557
+ name: methodName,
1558
+ types,
1559
+ validatorHooks,
1560
+ security,
1561
+ baseURL,
1562
+ returnType,
1563
+ throwOnErrorDefault
1564
+ }));
1565
+ const classCode = `export class ${name} {\n${[
1566
+ " readonly client: ClientInstance",
1567
+ "",
1568
+ " constructor(config: ClientConfig | ClientInstance = {}) {",
1569
+ " this.client = typeof config === 'function' ? config : createClient(config)",
1570
+ " }"
1571
+ ].join("\n")}\n\n${methods.join("\n\n")}\n}`;
1572
+ return /* @__PURE__ */ jsx(File.Source, {
1573
+ name,
1574
+ isExportable: true,
1575
+ isIndexable: true,
1576
+ children: classCode
1577
+ });
1578
+ }
1579
+ // src/components/SdkFacade.tsx
1580
+ /**
1581
+ * Renders a composed root SDK class that creates one client from the config and hands it to every tag
1582
+ * client, so `new PetStore({ baseURL }).pet.getPetById(...)` reaches an operation through a single entry
1583
+ * point bound to one environment. The shared client is the public `client` field, where interceptors
1584
+ * registered once apply to every tag. The per-tag clients are read-only fields built in the constructor.
1585
+ */
1586
+ function SdkFacade({ name, members }) {
1587
+ const fields = [" readonly client: ClientInstance", ...members.map((member) => ` readonly ${member.propName}: ${member.className}`)];
1588
+ const assignments = [" this.client = createClient(config)", ...members.map((member) => ` this.${member.propName} = new ${member.className}(this.client)`)];
1589
+ const classCode = `export class ${name} {\n${[
1590
+ ...fields,
1591
+ "",
1592
+ " constructor(config: ClientConfig = {}) {",
1593
+ ...assignments,
1594
+ " }"
1595
+ ].join("\n")}\n}`;
1596
+ return /* @__PURE__ */ jsx(File.Source, {
1597
+ name,
1598
+ isExportable: true,
1599
+ isIndexable: true,
1600
+ children: classCode
1601
+ });
1602
+ }
1603
+ // src/generators/sdkGenerator.tsx
1604
+ /**
1605
+ * Groups operations into one controller per tag. Operations without a tag fall back to a single
1606
+ * `Client`/`ApiClient` controller, matching the resolver's default naming.
1607
+ */
1608
+ function buildControllers(nodes, ctx, types) {
1609
+ const { resolver, root } = ctx;
1610
+ const { output, group } = ctx.options;
1611
+ return nodes.reduce((acc, operationNode) => {
1612
+ if (!ast.http.isOperation(operationNode)) return acc;
1613
+ const tag = operationNode.tags[0];
1614
+ const name = tag ? ensureValidVarName(resolver.groupName(tag)) : resolver.className("ApiClient");
1615
+ const fileName = tag && group ? ensureValidVarName(path.basename(resolver.groupDir(group, tag))) : name;
1616
+ const file = resolver.file({
1617
+ name: fileName,
1618
+ extname: ".ts",
1619
+ tag,
1620
+ root,
1621
+ output,
1622
+ group: group ?? void 0
1623
+ });
1624
+ const operationData = resolveOperationData({
1625
+ node: operationNode,
1626
+ ctx,
1627
+ types
1628
+ });
1629
+ const previous = acc.find((item) => item.file.path === file.path);
1630
+ if (previous) previous.operations.push(operationData);
1631
+ else acc.push({
1632
+ name,
1633
+ tag,
1634
+ file,
1635
+ operations: [operationData]
1636
+ });
1637
+ return acc;
1638
+ }, []);
1639
+ }
1640
+ function collectImportsByFile(ops, pick) {
1641
+ const namesByPath = /* @__PURE__ */ new Map();
1642
+ const filesByPath = /* @__PURE__ */ new Map();
1643
+ ops.forEach((op) => {
1644
+ const { file, names } = pick(op);
1645
+ if (!file || names.length === 0) return;
1646
+ const set = memo(namesByPath, file.path, () => /* @__PURE__ */ new Set());
1647
+ names.forEach((n) => set.add(n));
1648
+ filesByPath.set(file.path, file);
1649
+ });
1650
+ return {
1651
+ namesByPath,
1652
+ filesByPath
1653
+ };
1654
+ }
1655
+ /**
1656
+ * Builds the class-based SDK generator for a client plugin (`@kubb/plugin-fetch`,
1657
+ * `@kubb/plugin-axios`). Only registered when `sdk` is set; otherwise the plugin keeps its
1658
+ * standalone per-operation functions.
1659
+ *
1660
+ * Every tag client is an instance class whose constructor takes a client config and builds its own
1661
+ * client, or takes an existing client, so each environment is a separate instance. With `sdk.mode:
1662
+ * 'tag'` (the default) it emits one class per tag, named by `resolver.groupName`, and, when `sdk.name`
1663
+ * is set, a composed root that creates one client and shares it with every tag client. With
1664
+ * `sdk.mode: 'flat'` it emits one class named by `sdk.name`, with every operation as a direct method.
1665
+ *
1666
+ * `templates` is the folder the optional runtime is copied from: `sse.ts` only for a document that streams, and each
1667
+ * form media type only for a document with a body of that type.
1668
+ */
1669
+ function createSdkGenerator({ templates } = {}) {
1670
+ return defineGenerator({
1671
+ name: "sdk",
1672
+ renderer: jsxRenderer,
1673
+ operations(nodes, ctx) {
1674
+ const { config, resolver, root } = ctx;
1675
+ const { output, group, validator, returnType, throwOnErrorDefault, sdk } = ctx.options;
1676
+ if (!sdk) return null;
1677
+ if (sdk.mode === "tag" && output.mode === "file") throw new Diagnostics.Error({
1678
+ code: Diagnostics.code.invalidPluginOptions,
1679
+ severity: "error",
1680
+ message: `Plugin "${ctx.plugin.name}" resolves \`output.mode\` to 'file' but also configures \`sdk.mode: 'tag'\`.`,
1681
+ help: "A single-file output has nothing to split tag classes into. Set `sdk.mode: 'flat'` to emit one class into that file, or give `output.path` an extensionless directory name (or set `output.mode: 'directory'` explicitly) so each tag gets its own file.",
1682
+ location: { kind: "config" },
1683
+ plugin: ctx.plugin.name
1684
+ });
1685
+ const types = resolveOperationTypes(ctx.driver, ctx.plugin.name);
1686
+ const validatorEnabled = isValidatorEnabled(validator);
1687
+ const controllers = buildControllers(nodes, ctx, types);
1688
+ const clientPath = path.resolve(root, ".kubb/client.ts");
1689
+ const standardSchemaPath = path.resolve(root, ".kubb/standardSchema.ts");
1690
+ const banner = (file) => resolver.default.banner(ctx.meta, {
1691
+ output,
1692
+ config,
1693
+ file: {
1694
+ path: file.path,
1695
+ baseName: file.baseName
1696
+ }
1697
+ });
1698
+ const footer = (file) => resolver.default.footer(ctx.meta, {
1699
+ output,
1700
+ config,
1701
+ file: {
1702
+ path: file.path,
1703
+ baseName: file.baseName
1704
+ }
1705
+ });
1706
+ const renderClassFile = (className, file, ops) => {
1707
+ const { namesByPath: typeNamesByPath, filesByPath: typeFilesByPath } = collectImportsByFile(ops, (op) => ({
1708
+ file: op.typeFile,
1709
+ names: [
1710
+ op.types.response.options(op.node),
1711
+ op.types.response.responses(op.node),
1712
+ ...op.headerTypeNames
1713
+ ]
1714
+ }));
1715
+ const { namesByPath: zodNamesByPath, filesByPath: zodFilesByPath } = validatorEnabled ? collectImportsByFile(ops, (op) => ({
1716
+ file: op.zodFile,
1717
+ names: op.validatorHooks.importedZodNames
1718
+ })) : {
1719
+ namesByPath: /* @__PURE__ */ new Map(),
1720
+ filesByPath: /* @__PURE__ */ new Map()
1721
+ };
1722
+ const resultOps = ops.filter((op) => !isEventStream(op.node));
1723
+ const typedHeaders = (op) => op.headerTypeNames.length > 0;
1724
+ const resultName = returnType === "data" ? "UnwrappedResult" : "RequestResult";
1725
+ return /* @__PURE__ */ jsxs(File, {
1726
+ baseName: file.baseName,
1727
+ path: file.path,
1728
+ meta: file.meta,
1729
+ banner: banner(file),
1730
+ footer: footer(file),
1731
+ children: [
1732
+ /* @__PURE__ */ jsx(File.Import, {
1733
+ name: [
1734
+ "createClient",
1735
+ returnType === "data" ? "unwrapResult" : "withUnwrap",
1736
+ "resolveThrowOnError",
1737
+ ...ctx.options.importPath === void 0 && ops.some((op) => op.security) ? ["securitySchemes"] : []
1738
+ ],
1739
+ root: file.path,
1740
+ path: clientPath
1741
+ }),
1742
+ /* @__PURE__ */ jsx(File.Import, {
1743
+ name: Array.from(/* @__PURE__ */ new Set([
1744
+ "ClientConfig",
1745
+ "ClientInstance",
1746
+ "Options",
1747
+ ...returnType === "data" ? [] : ["Unwrappable"],
1748
+ ...resultOps.some((op) => !typedHeaders(op)) ? [resultName] : [],
1749
+ ...resultOps.some(typedHeaders) ? [resultTypeName(resultName, true)] : [],
1750
+ ...ops.filter((op) => isEventStream(op.node)).flatMap((op) => buildEventStreamImports({
1751
+ node: op.node,
1752
+ types: op.types,
1753
+ returnType
1754
+ }))
1755
+ ])),
1756
+ root: file.path,
1757
+ path: clientPath,
1758
+ isTypeOnly: true
1759
+ }),
1760
+ validator === "zod" && ops.some((op) => ast.http.requestBody.getSchema(op.node) != null) && /* @__PURE__ */ jsx(File.Import, {
1761
+ name: ["z"],
1762
+ path: "zod",
1763
+ isTypeOnly: true
1764
+ }),
1765
+ Array.from(typeNamesByPath.entries()).map(([filePath, set]) => /* @__PURE__ */ jsx(File.Import, {
1766
+ name: Array.from(set),
1767
+ root: file.path,
1768
+ path: typeFilesByPath.get(filePath).path,
1769
+ isTypeOnly: true
1770
+ }, filePath)),
1771
+ validatorEnabled && Array.from(zodNamesByPath.entries()).map(([filePath, set]) => /* @__PURE__ */ jsx(File.Import, {
1772
+ name: Array.from(set),
1773
+ root: file.path,
1774
+ path: zodFilesByPath.get(filePath).path
1775
+ }, filePath)),
1776
+ ops.some((op) => op.validatorHooks.headers) && /* @__PURE__ */ jsx(File.Import, {
1777
+ name: ["toHeadersStandardSchema"],
1778
+ root: file.path,
1779
+ path: standardSchemaPath
1780
+ }),
1781
+ /* @__PURE__ */ jsx(SdkClient, {
1782
+ name: className,
1783
+ operations: ops,
1784
+ returnType,
1785
+ throwOnErrorDefault
1786
+ })
1787
+ ]
1788
+ }, file.path);
1789
+ };
1790
+ const renderFacadeFile = (name) => {
1791
+ const sdkFile = resolver.file({
1792
+ name,
1793
+ extname: ".ts",
1794
+ root,
1795
+ output,
1796
+ group: group ?? void 0
1797
+ });
1798
+ const members = controllers.map(({ name, tag }) => ({
1799
+ className: name,
1800
+ propName: resolver.propertyName(tag ?? name)
1801
+ }));
1802
+ return /* @__PURE__ */ jsxs(File, {
1803
+ baseName: sdkFile.baseName,
1804
+ path: sdkFile.path,
1805
+ meta: sdkFile.meta,
1806
+ banner: banner(sdkFile),
1807
+ footer: footer(sdkFile),
1808
+ children: [
1809
+ /* @__PURE__ */ jsx(File.Import, {
1810
+ name: ["createClient"],
1811
+ root: sdkFile.path,
1812
+ path: clientPath
1813
+ }),
1814
+ /* @__PURE__ */ jsx(File.Import, {
1815
+ name: ["ClientConfig", "ClientInstance"],
1816
+ root: sdkFile.path,
1817
+ path: clientPath,
1818
+ isTypeOnly: true
1819
+ }),
1820
+ controllers.map(({ name, file }) => /* @__PURE__ */ jsx(File.Import, {
1821
+ name: [name],
1822
+ root: sdkFile.path,
1823
+ path: file.path
1824
+ }, name)),
1825
+ /* @__PURE__ */ jsx(SdkFacade, {
1826
+ name: resolver.className(name),
1827
+ members
1828
+ })
1829
+ ]
1830
+ }, sdkFile.path);
1831
+ };
1832
+ const files = sdk.mode === "flat" ? renderClassFile(resolver.className(sdk.name ?? "sdk"), resolver.file({
1833
+ name: sdk.name ?? "sdk",
1834
+ extname: ".ts",
1835
+ root,
1836
+ output,
1837
+ group: group ?? void 0
1838
+ }), controllers.flatMap((controller) => controller.operations)) : [...controllers.map(({ name, file, operations: ops }) => renderClassFile(name, file, ops)), sdk.name && renderFacadeFile(sdk.name)];
1839
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ClientRuntime, {
1840
+ nodes,
1841
+ root,
1842
+ templates,
1843
+ baseURL: ctx.options.baseURL,
1844
+ servers: ctx.meta.servers,
1845
+ auth: ctx.meta.auth
1846
+ }), files] });
1847
+ }
1848
+ });
1849
+ }
1850
+ // src/setup.ts
1851
+ const templatePath = (name) => fileURLToPath(new URL(`../templates/${name}`, import.meta.url));
1852
+ /**
1853
+ * Applies the client option defaults and merges a user `resolver` over the built-in one.
1854
+ */
1855
+ function resolveClientOptions(options) {
1856
+ const { output = {
1857
+ path: "clients",
1858
+ barrel: { type: "named" }
1859
+ }, exclude = [], include, override = [], baseURL, throwOnErrorDefault = true, validator = false, returnType = "full", group, sdk, resolver: userResolver } = options;
1860
+ return {
1861
+ output,
1862
+ exclude,
1863
+ include,
1864
+ override,
1865
+ group: group ?? null,
1866
+ baseURL,
1867
+ throwOnErrorDefault,
1868
+ validator,
1869
+ returnType,
1870
+ sdk: sdk ? {
1871
+ mode: sdk.mode ?? "tag",
1872
+ name: sdk.name
1873
+ } : void 0,
1874
+ resolver: userResolver ? Resolver.merge(resolverClient, userResolver) : resolverClient
1875
+ };
1876
+ }
1877
+ /**
1878
+ * Wires a client plugin in its `setup`: resolver, macros, client or SDK generator, and the `.kubb/` runtime with the plugin's own `client.ts`.
1879
+ */
1880
+ function setupClient({ name, resolved, macros = [], client }) {
1881
+ const config = useConfig();
1882
+ setResolver(resolved.resolver);
1883
+ setMacros([...defaultMacros, ...macros]);
1884
+ const templates = templatePath("");
1885
+ addGenerator(resolved.sdk ? createSdkGenerator({ templates }) : createClientGenerator(name, { templates }));
1886
+ const root = path.resolve(config.root, config.output.path);
1887
+ const files = [
1888
+ ["serializers.ts", templatePath("serializers.ts")],
1889
+ ["mediaTypes.ts", templatePath("mediaTypes.ts")],
1890
+ ["core.ts", templatePath("core.ts")],
1891
+ ["client.ts", client],
1892
+ ["standardSchema.ts", templatePath("standardSchema.ts")]
1893
+ ];
1894
+ for (const [baseName, copy] of files) injectFile({
1895
+ baseName,
1896
+ path: path.resolve(root, ".kubb", baseName),
1897
+ copy
1898
+ });
1899
+ }
1311
1900
  /**
1312
- * Built-in operation generator for `@kubb/plugin-client`. Emits one async function per OpenAPI
1313
- * operation using the shared `Operation` component: a grouped `<Name>Request` type and a function
1314
- * that forwards a single `options` object to the user-provided client module and returns the `RequestResult`.
1901
+ * Declares `plugin-ts` as optional and `plugin-zod` as required once `validator` enables a direction.
1315
1902
  */
1316
- const clientGenerator = createClientGenerator("client");
1903
+ function clientDependencies(options) {
1904
+ return {
1905
+ [pluginTsName]: { optional: true },
1906
+ [pluginZodName$1]: { optional: !isValidatorEnabled(options.validator) }
1907
+ };
1908
+ }
1317
1909
  // src/plugin.ts
1318
1910
  /**
1319
1911
  * Canonical plugin name for `@kubb/plugin-client`.
@@ -1336,10 +1928,7 @@ const pluginClient = defineExtension({
1336
1928
  version,
1337
1929
  compatibility
1338
1930
  },
1339
- dependencies: (options) => ({
1340
- [pluginTsName]: { optional: true },
1341
- [pluginZodName]: { optional: !isValidatorEnabled(options.validator) }
1342
- }),
1931
+ dependencies: clientDependencies,
1343
1932
  setup(options) {
1344
1933
  const { output = {
1345
1934
  path: "clients",
@@ -1348,13 +1937,13 @@ const pluginClient = defineExtension({
1348
1937
  const resolver = userResolver ? Resolver.merge(resolverClient, userResolver) : resolverClient;
1349
1938
  setResolver(resolver);
1350
1939
  setMacros([...defaultMacros, ...options.macros ?? []]);
1351
- addGenerator(clientGenerator);
1940
+ addGenerator(createClientGenerator("client"));
1352
1941
  return {
1353
1942
  output,
1354
1943
  exclude,
1355
1944
  include,
1356
1945
  override,
1357
- group: createGroupConfig(group),
1946
+ group: group ?? null,
1358
1947
  importPath,
1359
1948
  baseURL: void 0,
1360
1949
  throwOnErrorDefault,
@@ -1365,6 +1954,6 @@ const pluginClient = defineExtension({
1365
1954
  };
1366
1955
  }
1367
1956
  });
1368
- export { pluginClient as default, pluginClient, pluginClientName };
1957
+ export { clientDependencies, pluginClient as default, pluginClient, pluginClientName, resolveClientOptions, resolverClient, setupClient };
1369
1958
 
1370
1959
  //# sourceMappingURL=index.js.map