@kubb/plugin-fetch 5.0.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/index.cjs +1381 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +1352 -0
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-C0LytTxp.js +8 -0
- package/package.json +76 -0
- package/src/generators/clientGenerator.tsx +9 -0
- package/src/index.ts +3 -0
- package/src/plugin.ts +108 -0
- package/src/templates.ts +13 -0
- package/src/types.ts +19 -0
- package/templates/fetch.ts +818 -0
- package/templates/serializers.ts +428 -0
- package/templates/standardSchema.ts +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
import "./rolldown-runtime-C0LytTxp.js";
|
|
2
|
+
import { Resolver, Url, ast, createResolver, defineGenerator, definePlugin, macroSimplifyUnion } from "kubb/kit";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createFunctionParameter, createFunctionParameters, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
|
|
5
|
+
import { File, Function, jsxRenderer } from "kubb/jsx";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
|
|
7
|
+
import { pluginZodName } from "@kubb/plugin-zod";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
//#region ../../internals/shared/src/params.ts
|
|
10
|
+
/**
|
|
11
|
+
* Drops parameters that share the same name, keeping the first.
|
|
12
|
+
*
|
|
13
|
+
* A malformed spec can declare the same parameter name twice within one `in` location. Both would
|
|
14
|
+
* resolve to the same output property, so emitting both would yield an object type with a duplicate
|
|
15
|
+
* member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
|
|
16
|
+
* parameter names flow through unchanged, so no two distinct names ever collide here anymore.
|
|
17
|
+
*/
|
|
18
|
+
function dedupeParams(params) {
|
|
19
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20
|
+
return params.filter((param) => {
|
|
21
|
+
if (seen.has(param.name)) return false;
|
|
22
|
+
seen.add(param.name);
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region ../../internals/shared/src/operation.ts
|
|
28
|
+
/**
|
|
29
|
+
* Builds the `ResolverFileParams` every operation generator passes to
|
|
30
|
+
* `resolver.file`: a file named `name`, tagged by the operation's first
|
|
31
|
+
* tag (or `'default'`), at the operation's path. Centralizes the entry object
|
|
32
|
+
* that was repeated at dozens of call sites across the client and query plugins.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function operationFileEntry(node, name, extname = ".ts") {
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
extname,
|
|
43
|
+
tag: node.tags[0] ?? "default",
|
|
44
|
+
path: node.path
|
|
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
|
+
* Whether an operation streams its primary success response as Server-Sent Events
|
|
88
|
+
* (`text/event-stream`). The client generator uses this to return a typed event stream instead of a
|
|
89
|
+
* one-shot `RequestResult`.
|
|
90
|
+
*/
|
|
91
|
+
function isEventStream(node) {
|
|
92
|
+
return getPrimarySuccessContentType(node) === "text/event-stream";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Derives the default `responseType` for an operation from its primary success response.
|
|
96
|
+
*
|
|
97
|
+
* Returns a value only when that response declares a single non-JSON content type. `text/event-stream`
|
|
98
|
+
* and other binary types (`application/octet-stream`, `application/pdf`, `image/*`, `audio/*`,
|
|
99
|
+
* `video/*`) map to a stream or `'blob'`, and other `text/*` maps to `'text'`. Otherwise `undefined`,
|
|
100
|
+
* leaving the runtime client's `Content-Type` auto-detection in charge.
|
|
101
|
+
*/
|
|
102
|
+
function getResponseType(node) {
|
|
103
|
+
const baseType = getPrimarySuccessContentType(node);
|
|
104
|
+
if (!baseType) return void 0;
|
|
105
|
+
if (baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json") return void 0;
|
|
106
|
+
if (baseType === "text/event-stream") return "stream";
|
|
107
|
+
if (baseType.startsWith("text/")) return "text";
|
|
108
|
+
if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Which of the grouped request options an operation carries.
|
|
112
|
+
*/
|
|
113
|
+
function getRequestGroups(node) {
|
|
114
|
+
const { path, query, header } = getOperationParameters(node);
|
|
115
|
+
return {
|
|
116
|
+
path: path.length > 0,
|
|
117
|
+
query: query.length > 0,
|
|
118
|
+
body: Boolean(node.requestBody?.content?.[0]?.schema),
|
|
119
|
+
headers: header.length > 0
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolves which grouped request options an operation carries together with whether each group
|
|
124
|
+
* holds a required member. The grouped parameter stays optional only when nothing inside it is
|
|
125
|
+
* required, matching the generated `RequestConfig` type.
|
|
126
|
+
*/
|
|
127
|
+
function getRequestGroupOptionality(node) {
|
|
128
|
+
const groups = getRequestGroups(node);
|
|
129
|
+
const { path, query, header } = getOperationParameters(node);
|
|
130
|
+
const hasRequiredPath = path.some((param) => param.required);
|
|
131
|
+
const hasRequiredQuery = query.some((param) => param.required);
|
|
132
|
+
const hasRequiredHeader = header.some((param) => param.required);
|
|
133
|
+
return {
|
|
134
|
+
groups,
|
|
135
|
+
hasRequiredPath,
|
|
136
|
+
hasRequiredQuery,
|
|
137
|
+
hasRequiredHeader,
|
|
138
|
+
isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function buildOperationComments(node, options = {}) {
|
|
142
|
+
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
143
|
+
const linkComment = getOperationLink(node, link);
|
|
144
|
+
const filteredComments = (linkPosition === "beforeDeprecated" ? [
|
|
145
|
+
node.description && `@description ${node.description}`,
|
|
146
|
+
node.summary && `@summary ${node.summary}`,
|
|
147
|
+
linkComment,
|
|
148
|
+
node.deprecated && "@deprecated"
|
|
149
|
+
] : [
|
|
150
|
+
node.description && `@description ${node.description}`,
|
|
151
|
+
node.summary && `@summary ${node.summary}`,
|
|
152
|
+
node.deprecated && "@deprecated",
|
|
153
|
+
linkComment
|
|
154
|
+
]).filter((comment) => Boolean(comment));
|
|
155
|
+
if (!splitLines) return filteredComments;
|
|
156
|
+
return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
|
|
157
|
+
}
|
|
158
|
+
function getOperationParameters(node) {
|
|
159
|
+
return {
|
|
160
|
+
path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
|
|
161
|
+
query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
|
|
162
|
+
header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
|
|
163
|
+
cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function getStatusCodeNumber(statusCode) {
|
|
167
|
+
const code = Number(statusCode);
|
|
168
|
+
return Number.isNaN(code) ? null : code;
|
|
169
|
+
}
|
|
170
|
+
function isSuccessStatusCode(statusCode) {
|
|
171
|
+
const code = getStatusCodeNumber(statusCode);
|
|
172
|
+
return code !== null && code >= 200 && code < 300;
|
|
173
|
+
}
|
|
174
|
+
function getSuccessResponses(responses) {
|
|
175
|
+
return responses.filter((response) => isSuccessStatusCode(response.statusCode));
|
|
176
|
+
}
|
|
177
|
+
function getOperationSuccessResponses(node) {
|
|
178
|
+
return getSuccessResponses(node.responses);
|
|
179
|
+
}
|
|
180
|
+
function getPrimarySuccessResponse(node) {
|
|
181
|
+
return getOperationSuccessResponses(node)[0] ?? null;
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region ../../internals/utils/src/casing.ts
|
|
185
|
+
/**
|
|
186
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
187
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
188
|
+
* and capitalizes each word according to `pascal`.
|
|
189
|
+
*
|
|
190
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
191
|
+
*/
|
|
192
|
+
function toCamelOrPascal(text, pascal) {
|
|
193
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
194
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
195
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
196
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Converts `text` to camelCase.
|
|
200
|
+
*
|
|
201
|
+
* @example Word boundaries
|
|
202
|
+
* `camelCase('hello-world') // 'helloWorld'`
|
|
203
|
+
*
|
|
204
|
+
* @example With a prefix
|
|
205
|
+
* `camelCase('tag', { prefix: 'create' }) // 'createTag'`
|
|
206
|
+
*/
|
|
207
|
+
function camelCase(text, { prefix = "", suffix = "" } = {}) {
|
|
208
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Converts `text` to PascalCase.
|
|
212
|
+
*
|
|
213
|
+
* @example Word boundaries
|
|
214
|
+
* `pascalCase('hello-world') // 'HelloWorld'`
|
|
215
|
+
*
|
|
216
|
+
* @example With a suffix
|
|
217
|
+
* `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
|
|
218
|
+
*/
|
|
219
|
+
function pascalCase(text, { prefix = "", suffix = "" } = {}) {
|
|
220
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region ../../internals/utils/src/reserved.ts
|
|
224
|
+
/**
|
|
225
|
+
* JavaScript and Java reserved words.
|
|
226
|
+
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
227
|
+
*/
|
|
228
|
+
const reservedWords = /* @__PURE__ */ new Set([
|
|
229
|
+
"abstract",
|
|
230
|
+
"arguments",
|
|
231
|
+
"boolean",
|
|
232
|
+
"break",
|
|
233
|
+
"byte",
|
|
234
|
+
"case",
|
|
235
|
+
"catch",
|
|
236
|
+
"char",
|
|
237
|
+
"class",
|
|
238
|
+
"const",
|
|
239
|
+
"continue",
|
|
240
|
+
"debugger",
|
|
241
|
+
"default",
|
|
242
|
+
"delete",
|
|
243
|
+
"do",
|
|
244
|
+
"double",
|
|
245
|
+
"else",
|
|
246
|
+
"enum",
|
|
247
|
+
"eval",
|
|
248
|
+
"export",
|
|
249
|
+
"extends",
|
|
250
|
+
"false",
|
|
251
|
+
"final",
|
|
252
|
+
"finally",
|
|
253
|
+
"float",
|
|
254
|
+
"for",
|
|
255
|
+
"function",
|
|
256
|
+
"goto",
|
|
257
|
+
"if",
|
|
258
|
+
"implements",
|
|
259
|
+
"import",
|
|
260
|
+
"in",
|
|
261
|
+
"instanceof",
|
|
262
|
+
"int",
|
|
263
|
+
"interface",
|
|
264
|
+
"let",
|
|
265
|
+
"long",
|
|
266
|
+
"native",
|
|
267
|
+
"new",
|
|
268
|
+
"null",
|
|
269
|
+
"package",
|
|
270
|
+
"private",
|
|
271
|
+
"protected",
|
|
272
|
+
"public",
|
|
273
|
+
"return",
|
|
274
|
+
"short",
|
|
275
|
+
"static",
|
|
276
|
+
"super",
|
|
277
|
+
"switch",
|
|
278
|
+
"synchronized",
|
|
279
|
+
"this",
|
|
280
|
+
"throw",
|
|
281
|
+
"throws",
|
|
282
|
+
"transient",
|
|
283
|
+
"true",
|
|
284
|
+
"try",
|
|
285
|
+
"typeof",
|
|
286
|
+
"var",
|
|
287
|
+
"void",
|
|
288
|
+
"volatile",
|
|
289
|
+
"while",
|
|
290
|
+
"with",
|
|
291
|
+
"yield",
|
|
292
|
+
"Array",
|
|
293
|
+
"Date",
|
|
294
|
+
"hasOwnProperty",
|
|
295
|
+
"Infinity",
|
|
296
|
+
"isFinite",
|
|
297
|
+
"isNaN",
|
|
298
|
+
"isPrototypeOf",
|
|
299
|
+
"length",
|
|
300
|
+
"Math",
|
|
301
|
+
"name",
|
|
302
|
+
"NaN",
|
|
303
|
+
"Number",
|
|
304
|
+
"Object",
|
|
305
|
+
"prototype",
|
|
306
|
+
"String",
|
|
307
|
+
"toString",
|
|
308
|
+
"undefined",
|
|
309
|
+
"valueOf"
|
|
310
|
+
]);
|
|
311
|
+
/**
|
|
312
|
+
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* isValidVarName('status') // true
|
|
317
|
+
* isValidVarName('class') // false (reserved word)
|
|
318
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
function isValidVarName(name) {
|
|
322
|
+
if (!name || reservedWords.has(name)) return false;
|
|
323
|
+
return isIdentifier(name);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Returns `name` when it's a syntactically valid JavaScript variable name,
|
|
327
|
+
* otherwise prefixes it with `_` so the result is a valid identifier.
|
|
328
|
+
*
|
|
329
|
+
* Useful for sanitizing OpenAPI schema names or operation IDs that start with
|
|
330
|
+
* a digit (e.g. `409`, `504AccountCancel`) before using them as exported
|
|
331
|
+
* variable, type, or function names.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* ensureValidVarName('409') // '_409'
|
|
336
|
+
* ensureValidVarName('504AccountCancel') // '_504AccountCancel'
|
|
337
|
+
* ensureValidVarName('Pet') // 'Pet'
|
|
338
|
+
* ensureValidVarName('class') // '_class'
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
function ensureValidVarName(name) {
|
|
342
|
+
if (!name || isValidVarName(name)) return name;
|
|
343
|
+
return `_${name}`;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
|
|
347
|
+
*
|
|
348
|
+
* Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
|
|
349
|
+
* even though they are not valid variable names, so use this (not {@link isValidVarName}) when
|
|
350
|
+
* deciding whether an object key needs quoting.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* isIdentifier('name') // true
|
|
355
|
+
* isIdentifier('x-total')// false
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
function isIdentifier(name) {
|
|
359
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region ../../internals/utils/src/codegen.ts
|
|
363
|
+
/**
|
|
364
|
+
* Builds a JSDoc comment block from an array of lines. Returns `fallback` when there are no
|
|
365
|
+
* comments.
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```ts
|
|
369
|
+
* buildJSDoc(['@type string', '@example hello'])
|
|
370
|
+
* // '/**\n * @type string\n * @example hello\n *\/\n '
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
function buildJSDoc(comments, options = {}) {
|
|
374
|
+
const { indent = " * ", suffix = "\n ", fallback = " " } = options;
|
|
375
|
+
if (comments.length === 0) return fallback;
|
|
376
|
+
return `/**\n${comments.map((c) => `${indent}${c}`).join("\n")}\n */${suffix}`;
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region ../../internals/shared/src/group.ts
|
|
380
|
+
/**
|
|
381
|
+
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
|
|
382
|
+
* shared default naming so every plugin groups output consistently:
|
|
383
|
+
*
|
|
384
|
+
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
385
|
+
* - other groups use the camelCased group (`pet store` → `petStore`).
|
|
386
|
+
*
|
|
387
|
+
* A user-provided `group.name` always wins over the default namer, so callers stay in
|
|
388
|
+
* control of their output folders. Returns `null` when grouping is disabled, matching the
|
|
389
|
+
* per-plugin convention.
|
|
390
|
+
*
|
|
391
|
+
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```ts
|
|
395
|
+
* createGroupConfig(group) // shared across every plugin
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
function createGroupConfig(group) {
|
|
399
|
+
if (!group) return null;
|
|
400
|
+
const defaultName = (ctx) => {
|
|
401
|
+
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
402
|
+
return camelCase(ctx.group);
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
...group,
|
|
406
|
+
name: group.name ? group.name : defaultName
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region ../../internals/client/src/builders/generics.ts
|
|
411
|
+
/**
|
|
412
|
+
* Builds the `RequestResult` generic arguments for one operation: the plugin-ts per-status responses
|
|
413
|
+
* record plus the per-call `ThrowOnError` flag. `SuccessOf` / `ErrorOf` split the record inside the
|
|
414
|
+
* runtime, so this only names the record and threads `ThrowOnError`.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* `buildRequestResultGenerics({ node, tsResolver }) // 'AddPetResponses, ThrowOnError'`
|
|
418
|
+
*/
|
|
419
|
+
function buildRequestResultGenerics({ node, tsResolver }) {
|
|
420
|
+
return `${tsResolver.response.responses(node)}, ThrowOnError`;
|
|
421
|
+
}
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region ../../internals/client/src/builders/returnStatement.ts
|
|
424
|
+
/**
|
|
425
|
+
* Builds the return statement of a generated operation function. The runtime call already resolves
|
|
426
|
+
* to `{ data, error, request, response }`; the generated code forwards that result and casts it to
|
|
427
|
+
* the operation's `RequestResult`, which carries the `throwOnError` discrimination.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* `return request({ method: 'POST', url: '/pet', ...config }) as Promise<RequestResult<AddPetResponses, ThrowOnError>>`
|
|
431
|
+
*/
|
|
432
|
+
function buildReturnStatement({ node, tsResolver, callConfig }) {
|
|
433
|
+
return `return request(${callConfig}) as Promise<RequestResult<${buildRequestResultGenerics({
|
|
434
|
+
node,
|
|
435
|
+
tsResolver
|
|
436
|
+
})}>>`;
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region ../../internals/client/src/builders/security.ts
|
|
440
|
+
function serializeAuth(auth) {
|
|
441
|
+
const parts = [`type: '${auth.type}'`];
|
|
442
|
+
if (auth.scheme) parts.push(`scheme: '${auth.scheme}'`);
|
|
443
|
+
if (auth.name) parts.push(`name: '${auth.name}'`);
|
|
444
|
+
if (auth.in) parts.push(`in: '${auth.in}'`);
|
|
445
|
+
return `{ ${parts.join(", ")} }`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Maps an OpenAPI security scheme to the inline `Auth` object, or `null` when the runtime cannot
|
|
449
|
+
* place it (an unresolved `$ref`, or an `apiKey` without a name or outside `header` / `query` /
|
|
450
|
+
* `cookie`). `http` schemes other than `basic` are treated as bearer.
|
|
451
|
+
*/
|
|
452
|
+
function resolveSecurityScheme(scheme) {
|
|
453
|
+
if (!scheme || "$ref" in scheme) return null;
|
|
454
|
+
if (scheme.type === "apiKey") {
|
|
455
|
+
if (!scheme.name || scheme.in !== "header" && scheme.in !== "query" && scheme.in !== "cookie") return null;
|
|
456
|
+
return {
|
|
457
|
+
type: "apiKey",
|
|
458
|
+
name: scheme.name,
|
|
459
|
+
in: scheme.in
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (scheme.type === "http") return {
|
|
463
|
+
type: "http",
|
|
464
|
+
scheme: scheme.scheme?.toLowerCase() === "basic" ? "basic" : "bearer"
|
|
465
|
+
};
|
|
466
|
+
if (scheme.type === "oauth2") return { type: "oauth2" };
|
|
467
|
+
if (scheme.type === "openIdConnect") return { type: "openIdConnect" };
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Derives the per-operation security metadata from the OpenAPI document. The operation's own
|
|
472
|
+
* `security` overrides the global `security` (an explicit empty array disables auth), and every
|
|
473
|
+
* referenced scheme is resolved from `components.securitySchemes` into a flat, de-duplicated list of
|
|
474
|
+
* `Auth` objects the runtime walks in order.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* `getOperationSecurity({ document, method: 'POST', path: '/pet' })`
|
|
478
|
+
* `// [{ type: 'http', scheme: 'bearer' }]`
|
|
479
|
+
*/
|
|
480
|
+
function getOperationSecurity({ document, method, path }) {
|
|
481
|
+
if (!document) return void 0;
|
|
482
|
+
const requirements = (document.paths?.[path]?.[method.toLowerCase()])?.security ?? document.security;
|
|
483
|
+
if (!requirements?.length) return void 0;
|
|
484
|
+
const definitions = document.components?.securitySchemes ?? {};
|
|
485
|
+
const security = [];
|
|
486
|
+
const seen = /* @__PURE__ */ new Set();
|
|
487
|
+
for (const requirement of requirements) for (const schemeName of Object.keys(requirement)) {
|
|
488
|
+
if (seen.has(schemeName)) continue;
|
|
489
|
+
seen.add(schemeName);
|
|
490
|
+
const auth = resolveSecurityScheme(definitions[schemeName]);
|
|
491
|
+
if (auth) security.push(auth);
|
|
492
|
+
}
|
|
493
|
+
return security.length ? security : void 0;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Serializes the per-operation security into the literal emitted on each generated call's `security`
|
|
497
|
+
* field. The runtime `resolveAuth` helper walks it, calling the configured `auth` resolver per entry.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* `buildSecurityMetadata({ security: [{ type: 'http', scheme: 'bearer' }] }) // "[{ type: 'http', scheme: 'bearer' }]"`
|
|
501
|
+
*/
|
|
502
|
+
function buildSecurityMetadata({ security }) {
|
|
503
|
+
if (!security?.length) return null;
|
|
504
|
+
return `[${security.map(serializeAuth).join(", ")}]`;
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region ../../internals/client/src/builders/signature.ts
|
|
508
|
+
const declarationPrinter = functionPrinter({ mode: "declaration" });
|
|
509
|
+
/**
|
|
510
|
+
* Builds the grouped-options signature for one operation: a single `options` object whose `TData`
|
|
511
|
+
* is the plugin-ts `<Name>Options` (carrying a literal `url`), and a `RequestResult` return type
|
|
512
|
+
* keyed to the plugin-ts per-status responses record. There are no positional arguments.
|
|
513
|
+
*
|
|
514
|
+
* The generated file imports `<Name>Options` and `<Name>Responses` and uses them directly, so no
|
|
515
|
+
* per-operation input type has to be emitted.
|
|
516
|
+
*/
|
|
517
|
+
function buildGroupedOptionsSignature({ node, tsResolver }) {
|
|
518
|
+
const optionsName = tsResolver.response.options(node);
|
|
519
|
+
const responsesName = tsResolver.response.responses(node);
|
|
520
|
+
const resultGenerics = buildRequestResultGenerics({
|
|
521
|
+
node,
|
|
522
|
+
tsResolver
|
|
523
|
+
});
|
|
524
|
+
const { isOptional } = getRequestGroupOptionality(node);
|
|
525
|
+
return {
|
|
526
|
+
dataTypeName: optionsName,
|
|
527
|
+
paramsSignature: declarationPrinter.print(createFunctionParameters({ params: [createFunctionParameter({
|
|
528
|
+
name: "options",
|
|
529
|
+
type: `Options<${optionsName}, ThrowOnError>`,
|
|
530
|
+
...isOptional ? { default: "{}" } : {}
|
|
531
|
+
})] })) ?? "",
|
|
532
|
+
returnType: `Promise<RequestResult<${resultGenerics}>>`,
|
|
533
|
+
generics: ["ThrowOnError extends boolean = true"],
|
|
534
|
+
importedTypeNames: [optionsName, responsesName]
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region ../../internals/client/src/builders/validatorOptions.ts
|
|
539
|
+
/**
|
|
540
|
+
* Returns `true` when any direction of the validator uses zod (used for dependency checks).
|
|
541
|
+
*/
|
|
542
|
+
function isValidatorEnabled(validator) {
|
|
543
|
+
if (!validator) return false;
|
|
544
|
+
if (validator === "zod") return true;
|
|
545
|
+
return Boolean(validator.request || validator.response);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Returns `'zod'` when request body parsing is enabled, `null` otherwise. The string shorthand
|
|
549
|
+
* `'zod'` validates the response only, so it does not enable request parsing.
|
|
550
|
+
*/
|
|
551
|
+
function resolveRequestValidator(validator) {
|
|
552
|
+
if (!validator || validator === "zod") return null;
|
|
553
|
+
return validator.request ?? null;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Returns `'zod'` when query-parameters parsing is enabled, `null` otherwise. Only the object form
|
|
557
|
+
* `{ request: 'zod' }` enables it.
|
|
558
|
+
*/
|
|
559
|
+
function resolveQueryParamsValidator(validator) {
|
|
560
|
+
if (!validator || validator === "zod") return null;
|
|
561
|
+
return validator.request ?? null;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Returns `'zod'` when response parsing is enabled, `null` otherwise. The string shorthand `'zod'`
|
|
565
|
+
* maps to response parsing.
|
|
566
|
+
*/
|
|
567
|
+
function resolveResponseValidator(validator) {
|
|
568
|
+
if (!validator) return null;
|
|
569
|
+
if (validator === "zod") return "zod";
|
|
570
|
+
return validator.response ?? null;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Resolves the zod expression a generated client validates a success response with. Only success
|
|
574
|
+
* (2xx) bodies reach the parse under the throw-on-error contract, so the success-only
|
|
575
|
+
* `<operation>ResponseSchema` is used; error bodies are never zod-parsed.
|
|
576
|
+
*/
|
|
577
|
+
function buildZodResponseParse(node, zodResolver) {
|
|
578
|
+
const name = zodResolver.response.response(node);
|
|
579
|
+
return name ? {
|
|
580
|
+
expression: name,
|
|
581
|
+
importNames: [name]
|
|
582
|
+
} : null;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Resolves the zod expression a generated client validates an error body with on the non-throw path.
|
|
586
|
+
* Uses the error-only `<operation>ErrorSchema` (the union of non-2xx statuses); returns `null` when the
|
|
587
|
+
* operation documents no error responses with a schema.
|
|
588
|
+
*/
|
|
589
|
+
function buildZodErrorParse(node, zodResolver) {
|
|
590
|
+
if (!node.responses.some((res) => !isSuccessStatusCode(res.statusCode) && res.content?.some((entry) => entry.schema))) return null;
|
|
591
|
+
const name = zodResolver.response.error?.(node);
|
|
592
|
+
return name ? {
|
|
593
|
+
expression: name,
|
|
594
|
+
importNames: [name]
|
|
595
|
+
} : null;
|
|
596
|
+
}
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region ../../internals/client/src/builders/validator.ts
|
|
599
|
+
/**
|
|
600
|
+
* Builds the validator-hook references for one operation. Request validation runs before the send;
|
|
601
|
+
* response validation runs on the success body only. Returns `null` references when the matching
|
|
602
|
+
* direction is disabled or the schema is absent.
|
|
603
|
+
*/
|
|
604
|
+
function buildValidatorHooks({ node, validator, zodResolver }) {
|
|
605
|
+
const importedZodNames = [];
|
|
606
|
+
const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
607
|
+
const zodRequestName = zodResolver && resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.response.body(node) : null;
|
|
608
|
+
const request = zodRequestName ?? null;
|
|
609
|
+
if (zodRequestName) importedZodNames.push(zodRequestName);
|
|
610
|
+
const responseParse = zodResolver && resolveResponseValidator(validator) === "zod" ? buildZodResponseParse(node, zodResolver) : null;
|
|
611
|
+
const response = responseParse ? responseParse.expression : null;
|
|
612
|
+
if (responseParse) importedZodNames.push(...responseParse.importNames);
|
|
613
|
+
const errorParse = zodResolver && resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver) : null;
|
|
614
|
+
const error = errorParse ? errorParse.expression : null;
|
|
615
|
+
if (errorParse) importedZodNames.push(...errorParse.importNames);
|
|
616
|
+
return {
|
|
617
|
+
request,
|
|
618
|
+
response,
|
|
619
|
+
error,
|
|
620
|
+
importedZodNames
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region ../../internals/client/src/builders/sdkMethod.ts
|
|
625
|
+
/**
|
|
626
|
+
* Builds the call config literal forwarded to the contract client, mirroring the shared `Operation`
|
|
627
|
+
* component: `{ method, url, security?, validator?, ...config }`. The `...config` spread carries every
|
|
628
|
+
* per-call field (including `throwOnError`), so the method stays a thin wrapper over the contract.
|
|
629
|
+
*/
|
|
630
|
+
function buildCallConfig({ node, validator, zodResolver, security }) {
|
|
631
|
+
const validators = buildValidatorHooks({
|
|
632
|
+
node,
|
|
633
|
+
validator,
|
|
634
|
+
zodResolver
|
|
635
|
+
});
|
|
636
|
+
const validatorEntries = [validators.request ? `request: ${validators.request}` : null, validators.response ? `response: ${validators.response}` : null].filter(Boolean);
|
|
637
|
+
const validatorLiteral = validatorEntries.length ? `validator: { ${validatorEntries.join(", ")} }` : null;
|
|
638
|
+
const securityLiteral = buildSecurityMetadata({ security });
|
|
639
|
+
return `{ ${[
|
|
640
|
+
`method: '${node.method.toUpperCase()}'`,
|
|
641
|
+
`url: '${node.path}'`,
|
|
642
|
+
securityLiteral ? `security: ${securityLiteral}` : null,
|
|
643
|
+
validatorLiteral,
|
|
644
|
+
"...config"
|
|
645
|
+
].filter(Boolean).join(", ")} }`;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Builds a single instance method for a generated SDK class. The body forwards the single grouped
|
|
649
|
+
* `options` object to the instance's own client (`this.client`, built once in the constructor) and
|
|
650
|
+
* returns the `RequestResult`. A per-call `options.client` still overrides the instance client, so
|
|
651
|
+
* one operation can be routed to a different environment without a new instance.
|
|
652
|
+
*/
|
|
653
|
+
function buildSdkMethod({ node, name, tsResolver, zodResolver, validator, security }) {
|
|
654
|
+
if (!ast.isHttpOperationNode(node)) return "";
|
|
655
|
+
const signature = buildGroupedOptionsSignature({
|
|
656
|
+
node,
|
|
657
|
+
tsResolver
|
|
658
|
+
});
|
|
659
|
+
const returnStatement = buildReturnStatement({
|
|
660
|
+
node,
|
|
661
|
+
tsResolver,
|
|
662
|
+
callConfig: buildCallConfig({
|
|
663
|
+
node,
|
|
664
|
+
validator,
|
|
665
|
+
zodResolver,
|
|
666
|
+
security
|
|
667
|
+
})
|
|
668
|
+
});
|
|
669
|
+
const generics = signature.generics.length ? `<${signature.generics.join(", ")}>` : "";
|
|
670
|
+
const jsdoc = buildJSDoc(buildOperationComments(node, {
|
|
671
|
+
link: "urlPath",
|
|
672
|
+
linkPosition: "beforeDeprecated",
|
|
673
|
+
splitLines: true
|
|
674
|
+
}));
|
|
675
|
+
const methodBody = [
|
|
676
|
+
"const { client: request = this.client, ...config } = options",
|
|
677
|
+
"",
|
|
678
|
+
returnStatement
|
|
679
|
+
].map((line) => line ? ` ${line}` : "").join("\n");
|
|
680
|
+
return `${jsdoc} public ${name}${generics}(${signature.paramsSignature}): ${signature.returnType} {\n${methodBody}\n }`;
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region ../../internals/client/src/builders/styles.ts
|
|
684
|
+
/**
|
|
685
|
+
* Renders a parameter name as an object-literal key, quoted when it is not a bare identifier.
|
|
686
|
+
* Path keys are camelCased to match the URL template placeholders. Query, header, and cookie keys
|
|
687
|
+
* keep the spec name, matching the remapped keys the runtime serializes.
|
|
688
|
+
*/
|
|
689
|
+
function toKey(name, location) {
|
|
690
|
+
const key = location === "path" ? camelCase(name) : name;
|
|
691
|
+
return isValidVarName(key) ? key : JSON.stringify(key);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Serializes one parameter's metadata into a `{ style, explode }` literal, or `null` when the
|
|
695
|
+
* parameter carries neither. Path and query carry the serialization `style`; header and cookie use a
|
|
696
|
+
* fixed style (`simple` and `form`), so only `explode` is emitted for them.
|
|
697
|
+
*/
|
|
698
|
+
function serializeParameter(parameter) {
|
|
699
|
+
const parts = [];
|
|
700
|
+
if ((parameter.in === "path" || parameter.in === "query") && parameter.style) parts.push(`style: '${parameter.style}'`);
|
|
701
|
+
if (parameter.explode !== void 0) parts.push(`explode: ${parameter.explode}`);
|
|
702
|
+
return parts.length > 0 ? `{ ${parts.join(", ")} }` : null;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Builds the per-operation `styles` literal from the operation's parameters, grouped by location.
|
|
706
|
+
* Path entries are keyed by the camelCased name to match the URL template placeholders; query,
|
|
707
|
+
* header, and cookie entries keep the spec name to match the keys the runtime serializes.
|
|
708
|
+
* Only parameters whose source defines `style` or `explode` are emitted, so calls without
|
|
709
|
+
* serialization metadata keep the runtime defaults and existing output is unchanged. Returns `null`
|
|
710
|
+
* when no parameter carries metadata.
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* ```ts
|
|
714
|
+
* // a path param with { style: 'matrix', explode: true } and a query param with { explode: false }
|
|
715
|
+
* buildStyles({ node }) // "{ path: { id: { style: 'matrix', explode: true } }, query: { tags: { explode: false } } }"
|
|
716
|
+
* ```
|
|
717
|
+
*/
|
|
718
|
+
function buildStyles({ node }) {
|
|
719
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
720
|
+
const groups = {
|
|
721
|
+
path: [],
|
|
722
|
+
query: [],
|
|
723
|
+
header: [],
|
|
724
|
+
cookie: []
|
|
725
|
+
};
|
|
726
|
+
for (const parameter of node.parameters) {
|
|
727
|
+
const literal = serializeParameter(parameter);
|
|
728
|
+
if (!literal) continue;
|
|
729
|
+
groups[parameter.in].push(`${toKey(parameter.name, parameter.in)}: ${literal}`);
|
|
730
|
+
}
|
|
731
|
+
const locations = Object.keys(groups).filter((location) => groups[location].length > 0);
|
|
732
|
+
if (locations.length === 0) return null;
|
|
733
|
+
return `{ ${locations.map((location) => `${location}: { ${groups[location].join(", ")} }`).join(", ")} }`;
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region ../../internals/client/src/components/Operation.tsx
|
|
737
|
+
/**
|
|
738
|
+
* Renders one client operation: the grouped `<Name>Request` type and the function that forwards a
|
|
739
|
+
* single `options` object to the resolved client and returns the `RequestResult`. The type, signature,
|
|
740
|
+
* and call config are built with the AST factory, and only the jsx-renderer emits the source.
|
|
741
|
+
*/
|
|
742
|
+
function Operation({ name, node, tsResolver, zodResolver, validator, security, isExportable = true, isIndexable = true }) {
|
|
743
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
744
|
+
const signature = buildGroupedOptionsSignature({
|
|
745
|
+
node,
|
|
746
|
+
tsResolver
|
|
747
|
+
});
|
|
748
|
+
const validators = buildValidatorHooks({
|
|
749
|
+
node,
|
|
750
|
+
validator,
|
|
751
|
+
zodResolver
|
|
752
|
+
});
|
|
753
|
+
const securityLiteral = buildSecurityMetadata({ security });
|
|
754
|
+
const stylesLiteral = buildStyles({ node });
|
|
755
|
+
const { defaultContentType } = getContentTypeInfo(node);
|
|
756
|
+
const bakedRequestContentType = Boolean(node.requestBody?.content?.[0]?.schema) && defaultContentType !== "application/json" ? defaultContentType : null;
|
|
757
|
+
const mergeContentType = Boolean(bakedRequestContentType) && getResponseContentTypeInfo(node).isMultipleContentTypes;
|
|
758
|
+
const contentTypeLiteral = !bakedRequestContentType ? null : mergeContentType ? `contentType: { request: '${bakedRequestContentType}', ...(typeof contentType === 'string' ? { request: contentType } : contentType) }` : `contentType: { request: '${bakedRequestContentType}' }`;
|
|
759
|
+
const eventStream = isEventStream(node);
|
|
760
|
+
const responseType = getResponseType(node);
|
|
761
|
+
const responseTypeLiteral = responseType ? `responseType: '${responseType}'` : null;
|
|
762
|
+
const validatorEntries = [
|
|
763
|
+
validators.request ? `request: ${validators.request}` : null,
|
|
764
|
+
validators.response ? `response: ${validators.response}` : null,
|
|
765
|
+
validators.error ? `error: ${validators.error}` : null
|
|
766
|
+
].filter(Boolean);
|
|
767
|
+
const validatorLiteral = validatorEntries.length ? `validator: { ${validatorEntries.join(", ")} }` : null;
|
|
768
|
+
const callConfig = `{ ${[
|
|
769
|
+
`method: '${node.method.toUpperCase()}'`,
|
|
770
|
+
`url: '${node.path}'`,
|
|
771
|
+
securityLiteral ? `security: ${securityLiteral}` : null,
|
|
772
|
+
stylesLiteral ? `styles: ${stylesLiteral}` : null,
|
|
773
|
+
validatorLiteral,
|
|
774
|
+
contentTypeLiteral,
|
|
775
|
+
responseTypeLiteral,
|
|
776
|
+
"...config"
|
|
777
|
+
].filter(Boolean).join(", ")} }`;
|
|
778
|
+
const eventType = `SuccessOf<${tsResolver.response.responses(node)}>`;
|
|
779
|
+
const returnType = eventStream ? `Promise<EventStreamResult<${eventType}>>` : signature.returnType;
|
|
780
|
+
const returnStatement = eventStream ? `return toEventStream<${eventType}>(request(${callConfig}))` : buildReturnStatement({
|
|
781
|
+
node,
|
|
782
|
+
tsResolver,
|
|
783
|
+
callConfig
|
|
784
|
+
});
|
|
785
|
+
return /* @__PURE__ */ jsx(File.Source, {
|
|
786
|
+
name,
|
|
787
|
+
isExportable,
|
|
788
|
+
isIndexable,
|
|
789
|
+
children: /* @__PURE__ */ jsxs(Function, {
|
|
790
|
+
name,
|
|
791
|
+
export: isExportable,
|
|
792
|
+
generics: signature.generics,
|
|
793
|
+
params: signature.paramsSignature,
|
|
794
|
+
returnType,
|
|
795
|
+
JSDoc: { comments: buildOperationComments(node, {
|
|
796
|
+
link: "urlPath",
|
|
797
|
+
linkPosition: "beforeDeprecated",
|
|
798
|
+
splitLines: true
|
|
799
|
+
}) },
|
|
800
|
+
children: [
|
|
801
|
+
mergeContentType ? "const { client: request = client, contentType, ...config } = options" : "const { client: request = client, ...config } = options",
|
|
802
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
803
|
+
returnStatement
|
|
804
|
+
]
|
|
805
|
+
})
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region ../../internals/client/src/components/SdkClient.tsx
|
|
810
|
+
/**
|
|
811
|
+
* Renders one instance class per tag with one method per operation. The constructor takes a client
|
|
812
|
+
* config object and builds its own client through `createClient`, so each environment is a separate
|
|
813
|
+
* instance: `const api = new PetClient({ baseURL }); api.getPetById(...)`. A per-call `client` option
|
|
814
|
+
* still overrides the instance client for a one-off call.
|
|
815
|
+
*/
|
|
816
|
+
function SdkClient({ name, isExportable = true, isIndexable = true, operations, validator, children }) {
|
|
817
|
+
const methods = operations.map(({ node, name: methodName, tsResolver, zodResolver, security }) => buildSdkMethod({
|
|
818
|
+
node,
|
|
819
|
+
name: methodName,
|
|
820
|
+
tsResolver,
|
|
821
|
+
zodResolver,
|
|
822
|
+
validator,
|
|
823
|
+
security
|
|
824
|
+
}));
|
|
825
|
+
const classCode = `export class ${name} {\n${[
|
|
826
|
+
" private readonly client: ClientInstance",
|
|
827
|
+
"",
|
|
828
|
+
" constructor(config: ClientConfig = {}) {",
|
|
829
|
+
" this.client = createClient(config)",
|
|
830
|
+
" }"
|
|
831
|
+
].join("\n")}\n\n${methods.join("\n\n")}\n}`;
|
|
832
|
+
return /* @__PURE__ */ jsxs(File.Source, {
|
|
833
|
+
name,
|
|
834
|
+
isExportable,
|
|
835
|
+
isIndexable,
|
|
836
|
+
children: [classCode, children]
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region ../../internals/client/src/generators/clientGenerator.tsx
|
|
841
|
+
/**
|
|
842
|
+
* Builds the built-in per-operation generator shared by the client plugins (`@kubb/plugin-fetch`,
|
|
843
|
+
* `@kubb/plugin-axios`). Emits one async function per OpenAPI operation using the shared
|
|
844
|
+
* `Operation` component: a grouped `<Name>Request` type and a function that forwards a single
|
|
845
|
+
* `options` object to the bundled `client` and returns the `RequestResult`. Only the generator
|
|
846
|
+
* `name` differs between plugins; every other resolution, import, and rendering step is identical.
|
|
847
|
+
*/
|
|
848
|
+
function createClientGenerator(name) {
|
|
849
|
+
return defineGenerator({
|
|
850
|
+
name,
|
|
851
|
+
renderer: jsxRenderer,
|
|
852
|
+
operation(node, ctx) {
|
|
853
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
854
|
+
const { config, driver, resolver, root } = ctx;
|
|
855
|
+
const { output, validator, group } = ctx.options;
|
|
856
|
+
const pluginTs = driver.getPlugin(pluginTsName);
|
|
857
|
+
if (!pluginTs) return null;
|
|
858
|
+
const tsResolver = driver.getResolver(pluginTsName);
|
|
859
|
+
const pluginZod = resolveResponseValidator(validator) === "zod" || resolveRequestValidator(validator) === "zod" ? driver.getPlugin(pluginZodName) : null;
|
|
860
|
+
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
|
|
861
|
+
const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
862
|
+
const importedTypeNames = [tsResolver.response.options(node), tsResolver.response.responses(node)];
|
|
863
|
+
const importedZodNames = zodResolver ? [
|
|
864
|
+
resolveResponseValidator(validator) === "zod" ? zodResolver.response.response?.(node) : null,
|
|
865
|
+
resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
|
|
866
|
+
resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.response.body?.(node) : null
|
|
867
|
+
].filter((name) => Boolean(name)) : [];
|
|
868
|
+
const meta = {
|
|
869
|
+
name: resolver.name(node.operationId),
|
|
870
|
+
file: resolver.file({
|
|
871
|
+
...operationFileEntry(node, node.operationId),
|
|
872
|
+
root,
|
|
873
|
+
output,
|
|
874
|
+
group: group ?? void 0
|
|
875
|
+
}),
|
|
876
|
+
fileTs: tsResolver.file({
|
|
877
|
+
...operationFileEntry(node, node.operationId),
|
|
878
|
+
root,
|
|
879
|
+
output: pluginTs.options?.output ?? output,
|
|
880
|
+
group: pluginTs.options?.group ?? void 0
|
|
881
|
+
}),
|
|
882
|
+
fileZod: zodResolver && pluginZod?.options ? zodResolver.file({
|
|
883
|
+
...operationFileEntry(node, node.operationId),
|
|
884
|
+
root,
|
|
885
|
+
output: pluginZod.options.output ?? output,
|
|
886
|
+
group: pluginZod.options?.group ?? void 0
|
|
887
|
+
}) : null
|
|
888
|
+
};
|
|
889
|
+
const security = getOperationSecurity({
|
|
890
|
+
document: ctx.adapter.document,
|
|
891
|
+
method: node.method,
|
|
892
|
+
path: node.path
|
|
893
|
+
});
|
|
894
|
+
const clientPath = path.resolve(root, ".kubb/client.ts");
|
|
895
|
+
const eventStream = isEventStream(node);
|
|
896
|
+
return /* @__PURE__ */ jsxs(File, {
|
|
897
|
+
baseName: meta.file.baseName,
|
|
898
|
+
path: meta.file.path,
|
|
899
|
+
meta: meta.file.meta,
|
|
900
|
+
banner: resolver.default.banner(ctx.meta, {
|
|
901
|
+
output,
|
|
902
|
+
config,
|
|
903
|
+
file: {
|
|
904
|
+
path: meta.file.path,
|
|
905
|
+
baseName: meta.file.baseName
|
|
906
|
+
}
|
|
907
|
+
}),
|
|
908
|
+
footer: resolver.default.footer(ctx.meta, {
|
|
909
|
+
output,
|
|
910
|
+
config,
|
|
911
|
+
file: {
|
|
912
|
+
path: meta.file.path,
|
|
913
|
+
baseName: meta.file.baseName
|
|
914
|
+
}
|
|
915
|
+
}),
|
|
916
|
+
children: [
|
|
917
|
+
/* @__PURE__ */ jsx(File.Import, {
|
|
918
|
+
name: eventStream ? ["client", "toEventStream"] : ["client"],
|
|
919
|
+
root: meta.file.path,
|
|
920
|
+
path: clientPath
|
|
921
|
+
}),
|
|
922
|
+
/* @__PURE__ */ jsx(File.Import, {
|
|
923
|
+
name: eventStream ? [
|
|
924
|
+
"Options",
|
|
925
|
+
"EventStreamResult",
|
|
926
|
+
"SuccessOf"
|
|
927
|
+
] : ["Options", "RequestResult"],
|
|
928
|
+
root: meta.file.path,
|
|
929
|
+
path: clientPath,
|
|
930
|
+
isTypeOnly: true
|
|
931
|
+
}),
|
|
932
|
+
meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
|
|
933
|
+
name: Array.from(new Set(importedTypeNames)),
|
|
934
|
+
root: meta.file.path,
|
|
935
|
+
path: meta.fileTs.path,
|
|
936
|
+
isTypeOnly: true
|
|
937
|
+
}),
|
|
938
|
+
meta.fileZod && importedZodNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
|
|
939
|
+
name: importedZodNames,
|
|
940
|
+
root: meta.file.path,
|
|
941
|
+
path: meta.fileZod.path
|
|
942
|
+
}),
|
|
943
|
+
/* @__PURE__ */ jsx(Operation, {
|
|
944
|
+
name: meta.name,
|
|
945
|
+
node,
|
|
946
|
+
tsResolver,
|
|
947
|
+
zodResolver,
|
|
948
|
+
validator,
|
|
949
|
+
security
|
|
950
|
+
})
|
|
951
|
+
]
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
//#endregion
|
|
957
|
+
//#region ../../internals/client/src/components/SdkFacade.tsx
|
|
958
|
+
/**
|
|
959
|
+
* Renders a composed root SDK class that instantiates every tag client from one shared config, so
|
|
960
|
+
* `new PetStore({ baseURL }).petClient.getPetById(...)` reaches an operation through a single entry
|
|
961
|
+
* point bound to one environment. The per-tag clients are read-only fields built in the constructor.
|
|
962
|
+
*/
|
|
963
|
+
function SdkFacade({ name, isExportable = true, isIndexable = true, members, children }) {
|
|
964
|
+
const fields = members.map((member) => ` readonly ${member.propName}: ${member.className}`);
|
|
965
|
+
const assignments = members.map((member) => ` this.${member.propName} = new ${member.className}(config)`);
|
|
966
|
+
const classCode = `export class ${name} {\n${[
|
|
967
|
+
...fields,
|
|
968
|
+
"",
|
|
969
|
+
" constructor(config: ClientConfig = {}) {",
|
|
970
|
+
...assignments,
|
|
971
|
+
" }"
|
|
972
|
+
].join("\n")}\n}`;
|
|
973
|
+
return /* @__PURE__ */ jsxs(File.Source, {
|
|
974
|
+
name,
|
|
975
|
+
isExportable,
|
|
976
|
+
isIndexable,
|
|
977
|
+
children: [classCode, children]
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
//#endregion
|
|
981
|
+
//#region ../../internals/client/src/generators/sdkGenerator.tsx
|
|
982
|
+
function resolveTypeImportNames(node, tsResolver) {
|
|
983
|
+
return [tsResolver.response.options(node), tsResolver.response.responses(node)];
|
|
984
|
+
}
|
|
985
|
+
function resolveZodImportNames(node, zodResolver, validator) {
|
|
986
|
+
const { query: queryParams } = getOperationParameters(node);
|
|
987
|
+
return [
|
|
988
|
+
resolveResponseValidator(validator) === "zod" ? zodResolver.response.response(node) : null,
|
|
989
|
+
resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
|
|
990
|
+
resolveRequestValidator(validator) === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.response.body(node) : null,
|
|
991
|
+
resolveQueryParamsValidator(validator) === "zod" && queryParams.length > 0 ? zodResolver.param.query(node, queryParams[0]) : null
|
|
992
|
+
].filter((n) => Boolean(n));
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Groups operations into one controller per tag. Operations without a tag fall back to a single
|
|
996
|
+
* `Client`/`ApiClient` controller, matching the resolver's default naming.
|
|
997
|
+
*/
|
|
998
|
+
function buildControllers(nodes, ctx) {
|
|
999
|
+
const { driver, resolver, root } = ctx;
|
|
1000
|
+
const { output, group, validator } = ctx.options;
|
|
1001
|
+
const pluginTs = driver.getPlugin(pluginTsName);
|
|
1002
|
+
const tsResolver = driver.getResolver(pluginTsName);
|
|
1003
|
+
const tsPluginOptions = pluginTs.options;
|
|
1004
|
+
const pluginZod = isValidatorEnabled(validator) ? driver.getPlugin(pluginZodName) : null;
|
|
1005
|
+
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
|
|
1006
|
+
const document = ctx.adapter.document;
|
|
1007
|
+
function buildOperationData(node) {
|
|
1008
|
+
const typeFile = tsResolver.file({
|
|
1009
|
+
...operationFileEntry(node, node.operationId),
|
|
1010
|
+
root,
|
|
1011
|
+
output: tsPluginOptions?.output ?? output,
|
|
1012
|
+
group: tsPluginOptions?.group
|
|
1013
|
+
});
|
|
1014
|
+
const zodFile = zodResolver && pluginZod?.options ? zodResolver.file({
|
|
1015
|
+
...operationFileEntry(node, node.operationId),
|
|
1016
|
+
root,
|
|
1017
|
+
output: pluginZod.options?.output ?? output,
|
|
1018
|
+
group: pluginZod.options?.group ?? void 0
|
|
1019
|
+
}) : null;
|
|
1020
|
+
const security = ast.isHttpOperationNode(node) ? getOperationSecurity({
|
|
1021
|
+
document,
|
|
1022
|
+
method: node.method,
|
|
1023
|
+
path: node.path
|
|
1024
|
+
}) : void 0;
|
|
1025
|
+
return {
|
|
1026
|
+
node,
|
|
1027
|
+
name: resolver.name(node.operationId),
|
|
1028
|
+
tsResolver,
|
|
1029
|
+
zodResolver,
|
|
1030
|
+
typeFile,
|
|
1031
|
+
zodFile,
|
|
1032
|
+
security
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
return nodes.reduce((acc, operationNode) => {
|
|
1036
|
+
if (!ast.isHttpOperationNode(operationNode)) return acc;
|
|
1037
|
+
const tag = operationNode.tags[0];
|
|
1038
|
+
const name = tag ? group?.name?.({ group: camelCase(tag) }) ?? resolver.groupName(tag) : resolver.className("ApiClient");
|
|
1039
|
+
const file = resolver.file({
|
|
1040
|
+
name,
|
|
1041
|
+
extname: ".ts",
|
|
1042
|
+
tag,
|
|
1043
|
+
root,
|
|
1044
|
+
output,
|
|
1045
|
+
group: group ?? void 0
|
|
1046
|
+
});
|
|
1047
|
+
const operationData = buildOperationData(operationNode);
|
|
1048
|
+
const previous = acc.find((item) => item.file.path === file.path);
|
|
1049
|
+
if (previous) previous.operations.push(operationData);
|
|
1050
|
+
else acc.push({
|
|
1051
|
+
name,
|
|
1052
|
+
tag,
|
|
1053
|
+
file,
|
|
1054
|
+
operations: [operationData]
|
|
1055
|
+
});
|
|
1056
|
+
return acc;
|
|
1057
|
+
}, []);
|
|
1058
|
+
}
|
|
1059
|
+
function collectImportsByFile(ops, pick) {
|
|
1060
|
+
const namesByPath = /* @__PURE__ */ new Map();
|
|
1061
|
+
const filesByPath = /* @__PURE__ */ new Map();
|
|
1062
|
+
ops.forEach((op) => {
|
|
1063
|
+
const { file, names } = pick(op);
|
|
1064
|
+
if (!file || names.length === 0) return;
|
|
1065
|
+
if (!namesByPath.has(file.path)) namesByPath.set(file.path, /* @__PURE__ */ new Set());
|
|
1066
|
+
const set = namesByPath.get(file.path);
|
|
1067
|
+
names.forEach((n) => set.add(n));
|
|
1068
|
+
filesByPath.set(file.path, file);
|
|
1069
|
+
});
|
|
1070
|
+
return {
|
|
1071
|
+
namesByPath,
|
|
1072
|
+
filesByPath
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Builds the class-based SDK generator for a client plugin (`@kubb/plugin-fetch`,
|
|
1077
|
+
* `@kubb/plugin-axios`). Only registered when `sdk` is set; otherwise the plugin keeps its
|
|
1078
|
+
* standalone per-operation functions.
|
|
1079
|
+
*
|
|
1080
|
+
* Every tag client is an instance class whose constructor takes a client config and builds its own
|
|
1081
|
+
* client, so each environment is a separate instance. With `sdk.mode: 'tag'` (the default) it
|
|
1082
|
+
* emits one class per tag and, when `sdk.name` is set, a composed root that instantiates every tag
|
|
1083
|
+
* client. With `sdk.mode: 'flat'` it emits one class named by `sdk.name`, with every operation as a
|
|
1084
|
+
* direct method.
|
|
1085
|
+
*/
|
|
1086
|
+
function createSdkGenerator() {
|
|
1087
|
+
return defineGenerator({
|
|
1088
|
+
name: "sdk",
|
|
1089
|
+
renderer: jsxRenderer,
|
|
1090
|
+
operations(nodes, ctx) {
|
|
1091
|
+
const { config, resolver, root } = ctx;
|
|
1092
|
+
const { output, group, validator, sdk } = ctx.options;
|
|
1093
|
+
if (!ctx.driver.getPlugin(pluginTsName) || !sdk) return null;
|
|
1094
|
+
const controllers = buildControllers(nodes, ctx);
|
|
1095
|
+
const clientPath = path.resolve(root, ".kubb/client.ts");
|
|
1096
|
+
const banner = (file) => resolver.default.banner(ctx.meta, {
|
|
1097
|
+
output,
|
|
1098
|
+
config,
|
|
1099
|
+
file: {
|
|
1100
|
+
path: file.path,
|
|
1101
|
+
baseName: file.baseName
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
const footer = (file) => resolver.default.footer(ctx.meta, {
|
|
1105
|
+
output,
|
|
1106
|
+
config,
|
|
1107
|
+
file: {
|
|
1108
|
+
path: file.path,
|
|
1109
|
+
baseName: file.baseName
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
const renderClassFile = (className, file, ops) => {
|
|
1113
|
+
const { namesByPath: typeNamesByPath, filesByPath: typeFilesByPath } = collectImportsByFile(ops, (op) => ({
|
|
1114
|
+
file: op.typeFile,
|
|
1115
|
+
names: resolveTypeImportNames(op.node, op.tsResolver)
|
|
1116
|
+
}));
|
|
1117
|
+
const { namesByPath: zodNamesByPath, filesByPath: zodFilesByPath } = isValidatorEnabled(validator) ? collectImportsByFile(ops, (op) => ({
|
|
1118
|
+
file: op.zodFile,
|
|
1119
|
+
names: op.zodResolver ? resolveZodImportNames(op.node, op.zodResolver, validator) : []
|
|
1120
|
+
})) : {
|
|
1121
|
+
namesByPath: /* @__PURE__ */ new Map(),
|
|
1122
|
+
filesByPath: /* @__PURE__ */ new Map()
|
|
1123
|
+
};
|
|
1124
|
+
return /* @__PURE__ */ jsxs(File, {
|
|
1125
|
+
baseName: file.baseName,
|
|
1126
|
+
path: file.path,
|
|
1127
|
+
meta: file.meta,
|
|
1128
|
+
banner: banner(file),
|
|
1129
|
+
footer: footer(file),
|
|
1130
|
+
children: [
|
|
1131
|
+
/* @__PURE__ */ jsx(File.Import, {
|
|
1132
|
+
name: ["createClient"],
|
|
1133
|
+
root: file.path,
|
|
1134
|
+
path: clientPath
|
|
1135
|
+
}),
|
|
1136
|
+
/* @__PURE__ */ jsx(File.Import, {
|
|
1137
|
+
name: [
|
|
1138
|
+
"ClientConfig",
|
|
1139
|
+
"ClientInstance",
|
|
1140
|
+
"Options",
|
|
1141
|
+
"RequestResult"
|
|
1142
|
+
],
|
|
1143
|
+
root: file.path,
|
|
1144
|
+
path: clientPath,
|
|
1145
|
+
isTypeOnly: true
|
|
1146
|
+
}),
|
|
1147
|
+
validator === "zod" && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && /* @__PURE__ */ jsx(File.Import, {
|
|
1148
|
+
name: ["z"],
|
|
1149
|
+
path: "zod",
|
|
1150
|
+
isTypeOnly: true
|
|
1151
|
+
}),
|
|
1152
|
+
Array.from(typeNamesByPath.entries()).map(([filePath, set]) => /* @__PURE__ */ jsx(File.Import, {
|
|
1153
|
+
name: Array.from(set),
|
|
1154
|
+
root: file.path,
|
|
1155
|
+
path: typeFilesByPath.get(filePath).path,
|
|
1156
|
+
isTypeOnly: true
|
|
1157
|
+
}, filePath)),
|
|
1158
|
+
isValidatorEnabled(validator) && Array.from(zodNamesByPath.entries()).map(([filePath, set]) => /* @__PURE__ */ jsx(File.Import, {
|
|
1159
|
+
name: Array.from(set),
|
|
1160
|
+
root: file.path,
|
|
1161
|
+
path: zodFilesByPath.get(filePath).path
|
|
1162
|
+
}, filePath)),
|
|
1163
|
+
/* @__PURE__ */ jsx(SdkClient, {
|
|
1164
|
+
name: className,
|
|
1165
|
+
operations: ops,
|
|
1166
|
+
validator
|
|
1167
|
+
})
|
|
1168
|
+
]
|
|
1169
|
+
}, file.path);
|
|
1170
|
+
};
|
|
1171
|
+
if (sdk.mode === "flat") return renderClassFile(resolver.className(sdk.name ?? "sdk"), resolver.file({
|
|
1172
|
+
name: sdk.name ?? "sdk",
|
|
1173
|
+
extname: ".ts",
|
|
1174
|
+
root,
|
|
1175
|
+
output,
|
|
1176
|
+
group: group ?? void 0
|
|
1177
|
+
}), controllers.flatMap((controller) => controller.operations));
|
|
1178
|
+
const classFiles = controllers.map(({ name, file, operations: ops }) => renderClassFile(name, file, ops));
|
|
1179
|
+
if (!sdk.name) return /* @__PURE__ */ jsx(Fragment, { children: classFiles });
|
|
1180
|
+
const sdkFile = resolver.file({
|
|
1181
|
+
name: sdk.name,
|
|
1182
|
+
extname: ".ts",
|
|
1183
|
+
root,
|
|
1184
|
+
output,
|
|
1185
|
+
group: group ?? void 0
|
|
1186
|
+
});
|
|
1187
|
+
const facadeName = resolver.className(sdk.name);
|
|
1188
|
+
const members = controllers.map(({ name, tag }) => ({
|
|
1189
|
+
className: name,
|
|
1190
|
+
propName: resolver.propertyName(tag ?? name)
|
|
1191
|
+
}));
|
|
1192
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [classFiles, /* @__PURE__ */ jsxs(File, {
|
|
1193
|
+
baseName: sdkFile.baseName,
|
|
1194
|
+
path: sdkFile.path,
|
|
1195
|
+
meta: sdkFile.meta,
|
|
1196
|
+
banner: banner(sdkFile),
|
|
1197
|
+
footer: footer(sdkFile),
|
|
1198
|
+
children: [
|
|
1199
|
+
/* @__PURE__ */ jsx(File.Import, {
|
|
1200
|
+
name: ["ClientConfig"],
|
|
1201
|
+
root: sdkFile.path,
|
|
1202
|
+
path: clientPath,
|
|
1203
|
+
isTypeOnly: true
|
|
1204
|
+
}),
|
|
1205
|
+
controllers.map(({ name, file }) => /* @__PURE__ */ jsx(File.Import, {
|
|
1206
|
+
name: [name],
|
|
1207
|
+
root: sdkFile.path,
|
|
1208
|
+
path: file.path
|
|
1209
|
+
}, name)),
|
|
1210
|
+
/* @__PURE__ */ jsx(SdkFacade, {
|
|
1211
|
+
name: facadeName,
|
|
1212
|
+
members
|
|
1213
|
+
})
|
|
1214
|
+
]
|
|
1215
|
+
}, sdkFile.path)] });
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
//#endregion
|
|
1220
|
+
//#region ../../internals/client/src/macros.ts
|
|
1221
|
+
/**
|
|
1222
|
+
* Macros the client plugins apply by default, ahead of any user macros. `macroSimplifyUnion`
|
|
1223
|
+
* drops union members a broader scalar already covers, keeping the generated response and error
|
|
1224
|
+
* unions tidy. A plugin wires them with `ctx.setMacros([...defaultMacros, ...userMacros])`.
|
|
1225
|
+
*/
|
|
1226
|
+
const defaultMacros = [macroSimplifyUnion];
|
|
1227
|
+
//#endregion
|
|
1228
|
+
//#region ../../internals/client/src/resolver.ts
|
|
1229
|
+
/**
|
|
1230
|
+
* Default resolver shared by the client plugins. Functions and files inherit the built-in camelCase
|
|
1231
|
+
* `name` and `file`; classes and tag groups use PascalCase.
|
|
1232
|
+
*
|
|
1233
|
+
* @example
|
|
1234
|
+
* ```ts
|
|
1235
|
+
* resolverClient.name('show pet by id') // 'showPetById'
|
|
1236
|
+
* resolverClient.groupName('pet') // 'PetClient'
|
|
1237
|
+
* ```
|
|
1238
|
+
*/
|
|
1239
|
+
const resolverClient = createResolver({
|
|
1240
|
+
pluginName: "plugin-contract-client",
|
|
1241
|
+
className(name) {
|
|
1242
|
+
return ensureValidVarName(pascalCase(name));
|
|
1243
|
+
},
|
|
1244
|
+
groupName(name) {
|
|
1245
|
+
return ensureValidVarName(pascalCase(`${name} Client`));
|
|
1246
|
+
},
|
|
1247
|
+
propertyName(name) {
|
|
1248
|
+
return ensureValidVarName(camelCase(name));
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/generators/clientGenerator.tsx
|
|
1253
|
+
/**
|
|
1254
|
+
* Built-in operation generator for `@kubb/plugin-fetch`. Emits one async function per OpenAPI
|
|
1255
|
+
* operation using the shared `Operation` component: a grouped `<Name>Request` type and a function
|
|
1256
|
+
* that forwards a single `options` object to the bundled `client` and returns the `RequestResult`.
|
|
1257
|
+
*/
|
|
1258
|
+
const clientGenerator = createClientGenerator("fetch");
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/templates.ts
|
|
1261
|
+
/** Absolute path to the fetch client template, copied into `.kubb/client.ts`. */
|
|
1262
|
+
const fetchClientTemplatePath = fileURLToPath(new URL("../templates/fetch.ts", import.meta.url));
|
|
1263
|
+
/** Absolute path to the fetch serializers template, copied into `.kubb/serializers.ts`. */
|
|
1264
|
+
const fetchSerializersTemplatePath = fileURLToPath(new URL("../templates/serializers.ts", import.meta.url));
|
|
1265
|
+
/**
|
|
1266
|
+
* Absolute path to the Standard Schema runtime template. Pass it to a file node's `copy` field to
|
|
1267
|
+
* emit the helper into the generated `.kubb/standardSchema.ts` verbatim.
|
|
1268
|
+
*/
|
|
1269
|
+
const standardSchemaTemplatePath = fileURLToPath(new URL("../templates/standardSchema.ts", import.meta.url));
|
|
1270
|
+
//#endregion
|
|
1271
|
+
//#region src/plugin.ts
|
|
1272
|
+
/**
|
|
1273
|
+
* Canonical plugin name for `@kubb/plugin-fetch`. Used for driver lookups and cross-plugin
|
|
1274
|
+
* dependency references.
|
|
1275
|
+
*/
|
|
1276
|
+
const pluginFetchName = "plugin-fetch";
|
|
1277
|
+
/**
|
|
1278
|
+
* Generates a type-safe HTTP client pinned to the Fetch API. Each operation becomes one async
|
|
1279
|
+
* function that takes a single grouped `options` object and returns the shared `RequestResult`
|
|
1280
|
+
* contract. The runtime is always bundled into `.kubb/client.ts`, so generated code never imports
|
|
1281
|
+
* from `@kubb/plugin-fetch` and the only runtime dependency is the global `fetch`.
|
|
1282
|
+
*
|
|
1283
|
+
* @example
|
|
1284
|
+
* ```ts
|
|
1285
|
+
* import { defineConfig } from 'kubb/config'
|
|
1286
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1287
|
+
* import { pluginFetch } from '@kubb/plugin-fetch'
|
|
1288
|
+
*
|
|
1289
|
+
* export default defineConfig({
|
|
1290
|
+
* input: './petStore.yaml',
|
|
1291
|
+
* output: { path: './src/gen' },
|
|
1292
|
+
* plugins: [
|
|
1293
|
+
* pluginTs(),
|
|
1294
|
+
* pluginFetch({ output: { path: './clients' } }),
|
|
1295
|
+
* ],
|
|
1296
|
+
* })
|
|
1297
|
+
* ```
|
|
1298
|
+
*/
|
|
1299
|
+
const pluginFetch = definePlugin((options) => {
|
|
1300
|
+
const { output = {
|
|
1301
|
+
path: "clients",
|
|
1302
|
+
barrel: { type: "named" }
|
|
1303
|
+
}, exclude = [], include, override = [], baseURL, validator = false, group, sdk, resolver: userResolver } = options;
|
|
1304
|
+
const resolved = {
|
|
1305
|
+
output,
|
|
1306
|
+
exclude,
|
|
1307
|
+
include,
|
|
1308
|
+
override,
|
|
1309
|
+
group: createGroupConfig(group),
|
|
1310
|
+
baseURL,
|
|
1311
|
+
validator,
|
|
1312
|
+
sdk: sdk ? {
|
|
1313
|
+
mode: sdk.mode ?? "tag",
|
|
1314
|
+
name: sdk.name
|
|
1315
|
+
} : void 0,
|
|
1316
|
+
resolver: userResolver ? Resolver.merge(resolverClient, userResolver) : resolverClient
|
|
1317
|
+
};
|
|
1318
|
+
const selectedGenerators = resolved.sdk ? [createSdkGenerator()] : [clientGenerator];
|
|
1319
|
+
return {
|
|
1320
|
+
name: pluginFetchName,
|
|
1321
|
+
options,
|
|
1322
|
+
dependencies: [pluginTsName, isValidatorEnabled(resolved.validator) ? pluginZodName : null].filter((dependency) => Boolean(dependency)),
|
|
1323
|
+
hooks: { "kubb:plugin:setup"(ctx) {
|
|
1324
|
+
ctx.setOptions(resolved);
|
|
1325
|
+
ctx.setResolver(resolved.resolver);
|
|
1326
|
+
ctx.setMacros([...defaultMacros, ...options.macros ?? []]);
|
|
1327
|
+
ctx.addGenerator(...selectedGenerators);
|
|
1328
|
+
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
1329
|
+
const baseURLExpression = baseURL ? baseURL.includes("${") ? `\`${baseURL.replaceAll("`", "\\`")}\`` : JSON.stringify(baseURL) : void 0;
|
|
1330
|
+
ctx.injectFile({
|
|
1331
|
+
baseName: "serializers.ts",
|
|
1332
|
+
path: path.resolve(root, ".kubb/serializers.ts"),
|
|
1333
|
+
copy: fetchSerializersTemplatePath
|
|
1334
|
+
});
|
|
1335
|
+
ctx.injectFile({
|
|
1336
|
+
baseName: "client.ts",
|
|
1337
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
1338
|
+
copy: fetchClientTemplatePath,
|
|
1339
|
+
footer: baseURLExpression ? `client.setConfig({ baseURL: ${baseURLExpression} })` : void 0
|
|
1340
|
+
});
|
|
1341
|
+
ctx.injectFile({
|
|
1342
|
+
baseName: "standardSchema.ts",
|
|
1343
|
+
path: path.resolve(root, ".kubb/standardSchema.ts"),
|
|
1344
|
+
copy: standardSchemaTemplatePath
|
|
1345
|
+
});
|
|
1346
|
+
} }
|
|
1347
|
+
};
|
|
1348
|
+
});
|
|
1349
|
+
//#endregion
|
|
1350
|
+
export { clientGenerator, pluginFetch as default, pluginFetch, pluginFetchName };
|
|
1351
|
+
|
|
1352
|
+
//# sourceMappingURL=index.js.map
|