@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.
- package/README.md +1 -1
- package/dist/client-C-0-puI2.d.ts +554 -0
- package/dist/index.cjs +1377 -784
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -78
- package/dist/index.d.ts +117 -78
- package/dist/index.js +1375 -786
- package/dist/index.js.map +1 -1
- package/dist/templates/client.d.ts +3 -2
- package/dist/templates/core.d.ts +3 -3
- package/dist/templates/mediaTypes/formData.d.ts +5 -0
- package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
- package/dist/templates/mediaTypes.d.ts +2 -0
- package/dist/templates/serializers/cookies.d.ts +5 -0
- package/dist/templates/serializers/headers.d.ts +5 -0
- package/dist/templates/serializers/path.d.ts +5 -0
- package/dist/templates/serializers/pathStyles.d.ts +5 -0
- package/dist/templates/serializers/query.d.ts +5 -0
- package/dist/templates/serializers/queryStyles.d.ts +14 -0
- package/dist/templates/serializers.d.ts +118 -2
- package/dist/templates/sse.d.ts +20 -113
- package/dist/templates/standardSchema.d.ts +81 -2
- package/docs/guides/authentication.md +12 -2
- package/docs/guides/calling-operations.md +8 -6
- package/docs/guides/migrate-to-v6.md +139 -0
- package/docs/guides/validate-requests-and-responses.md +13 -8
- package/docs/guides/write-your-client.md +118 -12
- package/docs/options.md +5 -3
- package/docs/overview.md +3 -1
- package/kubb-extension.json +21 -3
- package/package.json +6 -5
- package/src/builders/callConfig.ts +80 -0
- package/src/builders/contentType.ts +35 -0
- package/src/builders/generics.ts +29 -0
- package/src/builders/operationBody.ts +43 -0
- package/src/builders/operationData.ts +64 -0
- package/src/builders/returnStatement.ts +103 -0
- package/src/builders/security.ts +125 -0
- package/src/builders/signature.ts +71 -0
- package/src/builders/styles.ts +94 -0
- package/src/builders/validator.ts +84 -0
- package/src/builders/validatorOptions.ts +53 -0
- package/src/clientTypes.ts +195 -0
- package/src/components/ClientRuntime.tsx +97 -0
- package/src/components/Operation.tsx +72 -0
- package/src/components/SdkClient.tsx +100 -0
- package/src/components/SdkFacade.tsx +35 -0
- package/src/generators/clientGenerator.tsx +91 -6
- package/src/generators/sdkGenerator.tsx +208 -0
- package/src/index.ts +3 -1
- package/src/macros.ts +9 -0
- package/src/plugin.ts +7 -8
- package/src/resolver.ts +28 -0
- package/src/setup.ts +90 -0
- package/src/types.ts +4 -2
- package/templates/client.ts +2 -1
- package/templates/core.ts +735 -286
- package/templates/mediaTypes/formData.ts +59 -0
- package/templates/mediaTypes/urlencoded.ts +33 -0
- package/templates/mediaTypes.ts +147 -0
- package/templates/serializers/cookies.ts +58 -0
- package/templates/serializers/headers.ts +35 -0
- package/templates/serializers/path.ts +19 -0
- package/templates/serializers/pathStyles.ts +27 -0
- package/templates/serializers/query.ts +21 -0
- package/templates/serializers/queryStyles.ts +107 -0
- package/templates/serializers.ts +48 -364
- package/templates/sse.ts +87 -178
- package/templates/standardSchema.ts +16 -37
- package/dist/client-BxO6Dygu.d.ts +0 -355
- package/dist/serializers-Ds8zZwOO.d.ts +0 -190
- package/dist/standardSchema-BShJWz2i.d.ts +0 -104
package/dist/index.js
CHANGED
|
@@ -1,287 +1,17 @@
|
|
|
1
|
-
import { Diagnostics, Resolver,
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
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/
|
|
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
|
-
*
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
*
|
|
522
|
-
*
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
//
|
|
775
|
+
// src/builders/operationData.ts
|
|
530
776
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
|
|
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 }) //
|
|
833
|
+
* `buildRequestResultGenerics({ node, types }) // "LoginUserResponses, ThrowOnError, { '200': LoginUserStatus200Headers }"`
|
|
537
834
|
*/
|
|
538
835
|
function buildRequestResultGenerics({ node, types }) {
|
|
539
|
-
return
|
|
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
|
|
856
|
+
const result = buildResultTypeRef(returnType === "data" ? "UnwrappedResult" : "RequestResult", types.response.responses(node), buildResponseHeadersType({
|
|
553
857
|
node,
|
|
554
858
|
types
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
*
|
|
563
|
-
* `returnType: '
|
|
564
|
-
*
|
|
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(
|
|
871
|
+
* `return withUnwrap(apiClient<AddPetResponses, ThrowOnError>({ method: 'POST', url: '/pet', ...config }))`
|
|
578
872
|
* @example
|
|
579
|
-
* `return unwrapResult(
|
|
873
|
+
* `return unwrapResult(apiClient<AddPetResponses, ThrowOnError>({ ... }), resolveThrowOnError(config.throwOnError, true))`
|
|
580
874
|
* @example
|
|
581
|
-
* `return
|
|
875
|
+
* `return apiClient<AddPetResponses, ThrowOnError>({ method: 'POST', url: '/pet', ...config })`
|
|
582
876
|
*/
|
|
583
877
|
function buildReturnStatement({ node, types, callConfig, returnType, throwOnErrorDefault }) {
|
|
584
|
-
const
|
|
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
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
711
|
-
*
|
|
712
|
-
*
|
|
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
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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((
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
*
|
|
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
|
|
747
|
-
const
|
|
748
|
-
|
|
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
|
-
//
|
|
951
|
+
// src/builders/contentType.ts
|
|
751
952
|
/**
|
|
752
|
-
* Builds the `contentType` entry baked into the call config
|
|
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
|
-
*
|
|
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(
|
|
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:
|
|
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: {
|
|
973
|
+
literal: merge ? `contentType: { ${defaults}, ...(typeof contentType === 'string' ? { request: contentType } : contentType) }` : `contentType: { ${defaults} }`
|
|
768
974
|
};
|
|
769
975
|
}
|
|
770
|
-
//
|
|
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
|
|
783
|
-
*
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
*
|
|
797
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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 =
|
|
840
|
-
|
|
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
|
-
//
|
|
1060
|
+
// src/builders/callConfig.ts
|
|
843
1061
|
/**
|
|
844
|
-
* `
|
|
845
|
-
*
|
|
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
|
|
848
|
-
|
|
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 `
|
|
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
|
|
860
|
-
*
|
|
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
|
|
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
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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:
|
|
884
|
-
`url:
|
|
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
|
-
|
|
887
|
-
validatorLiteral,
|
|
1107
|
+
ownRuntime ? null : styles,
|
|
888
1108
|
contentTypeLiteral,
|
|
889
|
-
|
|
1109
|
+
buildParseAsLiteral(content),
|
|
1110
|
+
stream === "event" || stream === "data" ? `eventShape: '${stream}'` : null,
|
|
1111
|
+
rawData ? `rawData: ${toLiteral(rawData)}` : null,
|
|
890
1112
|
"...config",
|
|
891
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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.
|
|
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:
|
|
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
|
-
//
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1043
|
-
*
|
|
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
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
-
//
|
|
1352
|
+
// src/generators/clientGenerator.tsx
|
|
1110
1353
|
/**
|
|
1111
|
-
* Builds the
|
|
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, {
|
|
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(
|
|
1361
|
+
return /* @__PURE__ */ jsx(ClientRuntime, {
|
|
1127
1362
|
nodes,
|
|
1128
1363
|
root: ctx.root,
|
|
1129
|
-
|
|
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.
|
|
1371
|
+
if (!ast.http.isOperation(node)) return null;
|
|
1135
1372
|
const { config, driver, resolver, root } = ctx;
|
|
1136
|
-
const { output,
|
|
1137
|
-
const
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1384
|
+
ctx,
|
|
1385
|
+
types
|
|
1145
1386
|
});
|
|
1146
|
-
const
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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 ?
|
|
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
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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:
|
|
1205
|
-
path:
|
|
1206
|
-
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:
|
|
1212
|
-
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:
|
|
1220
|
-
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
|
-
|
|
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:
|
|
1252
|
-
path:
|
|
1463
|
+
root: file.path,
|
|
1464
|
+
path: data.typeFile.path,
|
|
1253
1465
|
isTypeOnly: true
|
|
1254
1466
|
}),
|
|
1255
|
-
|
|
1256
|
-
name: importedZodNames,
|
|
1257
|
-
root:
|
|
1258
|
-
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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
1514
|
+
return ensureValidVarName(casing.pascal(name));
|
|
1302
1515
|
},
|
|
1303
1516
|
groupName(name) {
|
|
1304
|
-
return ensureValidVarName(
|
|
1517
|
+
return ensureValidVarName(casing.pascal(`${name} Client`));
|
|
1305
1518
|
},
|
|
1306
1519
|
propertyName(name) {
|
|
1307
|
-
return ensureValidVarName(
|
|
1520
|
+
return ensureValidVarName(casing.camel(name));
|
|
1308
1521
|
}
|
|
1309
1522
|
});
|
|
1310
|
-
// src/
|
|
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
|
-
*
|
|
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
|
-
|
|
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:
|
|
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(
|
|
1940
|
+
addGenerator(createClientGenerator("client"));
|
|
1352
1941
|
return {
|
|
1353
1942
|
output,
|
|
1354
1943
|
exclude,
|
|
1355
1944
|
include,
|
|
1356
1945
|
override,
|
|
1357
|
-
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
|