@kubb/plugin-client 5.12.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/client-C-0-puI2.d.ts +554 -0
  3. package/dist/index.cjs +1377 -784
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +117 -78
  6. package/dist/index.d.ts +117 -78
  7. package/dist/index.js +1375 -786
  8. package/dist/index.js.map +1 -1
  9. package/dist/templates/client.d.ts +3 -2
  10. package/dist/templates/core.d.ts +3 -3
  11. package/dist/templates/mediaTypes/formData.d.ts +5 -0
  12. package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
  13. package/dist/templates/mediaTypes.d.ts +2 -0
  14. package/dist/templates/serializers/cookies.d.ts +5 -0
  15. package/dist/templates/serializers/headers.d.ts +5 -0
  16. package/dist/templates/serializers/path.d.ts +5 -0
  17. package/dist/templates/serializers/pathStyles.d.ts +5 -0
  18. package/dist/templates/serializers/query.d.ts +5 -0
  19. package/dist/templates/serializers/queryStyles.d.ts +14 -0
  20. package/dist/templates/serializers.d.ts +118 -2
  21. package/dist/templates/sse.d.ts +20 -113
  22. package/dist/templates/standardSchema.d.ts +81 -2
  23. package/docs/guides/authentication.md +12 -2
  24. package/docs/guides/calling-operations.md +8 -6
  25. package/docs/guides/migrate-to-v6.md +139 -0
  26. package/docs/guides/validate-requests-and-responses.md +13 -8
  27. package/docs/guides/write-your-client.md +118 -12
  28. package/docs/options.md +5 -3
  29. package/docs/overview.md +3 -1
  30. package/kubb-extension.json +21 -3
  31. package/package.json +6 -5
  32. package/src/builders/callConfig.ts +80 -0
  33. package/src/builders/contentType.ts +35 -0
  34. package/src/builders/generics.ts +29 -0
  35. package/src/builders/operationBody.ts +43 -0
  36. package/src/builders/operationData.ts +64 -0
  37. package/src/builders/returnStatement.ts +103 -0
  38. package/src/builders/security.ts +125 -0
  39. package/src/builders/signature.ts +71 -0
  40. package/src/builders/styles.ts +94 -0
  41. package/src/builders/validator.ts +84 -0
  42. package/src/builders/validatorOptions.ts +53 -0
  43. package/src/clientTypes.ts +195 -0
  44. package/src/components/ClientRuntime.tsx +97 -0
  45. package/src/components/Operation.tsx +72 -0
  46. package/src/components/SdkClient.tsx +100 -0
  47. package/src/components/SdkFacade.tsx +35 -0
  48. package/src/generators/clientGenerator.tsx +91 -6
  49. package/src/generators/sdkGenerator.tsx +208 -0
  50. package/src/index.ts +3 -1
  51. package/src/macros.ts +9 -0
  52. package/src/plugin.ts +7 -8
  53. package/src/resolver.ts +28 -0
  54. package/src/setup.ts +90 -0
  55. package/src/types.ts +4 -2
  56. package/templates/client.ts +2 -1
  57. package/templates/core.ts +735 -286
  58. package/templates/mediaTypes/formData.ts +59 -0
  59. package/templates/mediaTypes/urlencoded.ts +33 -0
  60. package/templates/mediaTypes.ts +147 -0
  61. package/templates/serializers/cookies.ts +58 -0
  62. package/templates/serializers/headers.ts +35 -0
  63. package/templates/serializers/path.ts +19 -0
  64. package/templates/serializers/pathStyles.ts +27 -0
  65. package/templates/serializers/query.ts +21 -0
  66. package/templates/serializers/queryStyles.ts +107 -0
  67. package/templates/serializers.ts +48 -364
  68. package/templates/sse.ts +87 -178
  69. package/templates/standardSchema.ts +16 -37
  70. package/dist/client-BxO6Dygu.d.ts +0 -355
  71. package/dist/serializers-Ds8zZwOO.d.ts +0 -190
  72. package/dist/standardSchema-BShJWz2i.d.ts +0 -104
@@ -0,0 +1,53 @@
1
+ import { ast, text } from '@kubb/kit'
2
+ import type { ResolverZod } from '@kubb/plugin-zod'
3
+ import type { ValidatorOptions } from '../types.ts'
4
+
5
+ /**
6
+ * Returns `true` when any direction of the validator uses zod (used for dependency checks).
7
+ */
8
+ export function isValidatorEnabled(validator: ValidatorOptions | undefined): boolean {
9
+ if (!validator) return false
10
+ if (validator === 'zod') return true
11
+ return Boolean(validator.request || validator.params || validator.response)
12
+ }
13
+
14
+ /**
15
+ * Returns `'zod'` when request body parsing is enabled, `null` otherwise. The string shorthand
16
+ * `'zod'` validates the response only, so it does not enable request parsing.
17
+ */
18
+ export function resolveRequestValidator(validator: ValidatorOptions | undefined): 'zod' | null {
19
+ if (!validator || validator === 'zod') return null
20
+ return validator.request ?? null
21
+ }
22
+
23
+ /**
24
+ * Returns `'zod'` when path, query, and header params parsing is enabled, `null` otherwise. Only the
25
+ * object form `{ params: 'zod' }` enables it.
26
+ */
27
+ export function resolveParamsValidator(validator: ValidatorOptions | undefined): 'zod' | null {
28
+ if (!validator || validator === 'zod') return null
29
+ return validator.params ?? null
30
+ }
31
+
32
+ /**
33
+ * Returns `'zod'` when response parsing is enabled, `null` otherwise. The string shorthand `'zod'`
34
+ * maps to response parsing.
35
+ */
36
+ export function resolveResponseValidator(validator: ValidatorOptions | undefined): 'zod' | null {
37
+ if (!validator) return null
38
+ if (validator === 'zod') return 'zod'
39
+ return validator.response ?? null
40
+ }
41
+
42
+ /**
43
+ * Builds the `validator.error` map of a call, one plugin-zod `<operation>StatusNNNSchema` per non-2xx response with a
44
+ * schema, such as `{ 404: getPetStatus404Schema, '5xx': getPetStatus5xxSchema }`. Returns `null` without one.
45
+ */
46
+ export function buildZodErrorParse(node: ast.OperationNode, zodResolver: ResolverZod): { literal: string; names: Array<string> } | null {
47
+ const entries = node.responses
48
+ .filter((res) => !ast.http.status.isSuccess(res) && res.content?.some((entry) => entry.schema))
49
+ .map((res) => ({ key: String(res.statusCode), name: zodResolver.response.status(node, res.statusCode) }))
50
+ if (!entries.length) return null
51
+ const literal = entries.map(({ key, name }) => `${/^\d+$/.test(key) ? key : text.quote(key)}: ${name}`).join(', ')
52
+ return { literal: `{ ${literal} }`, names: entries.map(({ name }) => name) }
53
+ }
@@ -0,0 +1,195 @@
1
+ import type { ReturnTypeOption } from '@internals/client'
2
+ import type { ast, ResolverPatch, Exclude, Group, Include, Output, OutputOptions, Override, PluginType, Resolver } from '@kubb/kit'
3
+
4
+ /**
5
+ * Validator applied to the request and response using schemas from `@kubb/plugin-zod`.
6
+ * - `false`: no validation.
7
+ * - `'zod'`: validates the success (2xx) response body, and the error body of a non-2xx call, whether it
8
+ * throws or returns the error.
9
+ * - `{ request?: 'zod'; params?: 'zod'; response?: 'zod' }`: opt in per slot. `request` encodes the
10
+ * request body before the call; `params` validate the path, query, and header params;
11
+ * `response` validates the success response body and the error body.
12
+ */
13
+ export type ValidatorOptions = false | 'zod' | { request?: 'zod'; params?: 'zod'; response?: 'zod' }
14
+
15
+ /**
16
+ * How the class-based SDK groups operations.
17
+ * - `'tag'`: one class per tag, optionally composed into a root client.
18
+ * - `'flat'`: one class with every operation as a direct method.
19
+ */
20
+ export type Mode = 'tag' | 'flat'
21
+
22
+ /**
23
+ * The resolver shared by the client plugins. Inherits the built-in camelCase `name` and `file`;
24
+ * classes and tag groups use PascalCase (with a `Client` suffix for groups).
25
+ */
26
+ export type ResolverClient = Resolver & {
27
+ /**
28
+ * Resolves the generated class name for class-based clients.
29
+ */
30
+ className(this: ResolverClient, name: string): string
31
+ /**
32
+ * Resolves the generated class name for a tag-based client group. The default appends a
33
+ * `Client` suffix (tag `pet` becomes `PetClient`) so the class never collides with the schema
34
+ * model of the same name in the barrel.
35
+ *
36
+ * @example
37
+ * `resolver.groupName('pet') // -> 'PetClient'`
38
+ */
39
+ groupName(this: ResolverClient, name: string): string
40
+ /**
41
+ * Resolves the property name a tag client is exposed under on the composed root SDK
42
+ * (`new PetStore(config).pet`).
43
+ */
44
+ propertyName(this: ResolverClient, name: string): string
45
+ }
46
+
47
+ /**
48
+ * The shared options surface for the client plugins. Deliberately small: there is one
49
+ * response contract and one grouped options object. Each plugin extends this with its own
50
+ * `transport` field.
51
+ */
52
+ export type ClientOptions = OutputOptions & {
53
+ /**
54
+ * Skip operations matching at least one entry in the list.
55
+ */
56
+ exclude?: Array<Exclude>
57
+ /**
58
+ * Restrict generation to operations matching at least one entry in the list.
59
+ */
60
+ include?: Array<Include>
61
+ /**
62
+ * Apply a different options object to operations matching a pattern.
63
+ */
64
+ override?: Array<Override<Omit<ClientResolvedOptions, 'throwOnErrorDefault'>>>
65
+ /**
66
+ * Base URL prepended to every request. When omitted, the generated client sets no base URL, so set one at runtime with
67
+ * `client.setConfig({ baseURL })`. The top-level server URL is not used, but an operation with path-level or operation-level `servers` calls it.
68
+ * Values containing a `${...}` interpolation are emitted as template literals in the generated client config, which keeps
69
+ * runtime environment reads dynamic.
70
+ */
71
+ baseURL?: string
72
+ /**
73
+ * Default error behavior for generated operations and their client. Per-call `throwOnError`
74
+ * takes precedence.
75
+ *
76
+ * @default true
77
+ */
78
+ throwOnErrorDefault?: boolean
79
+ /**
80
+ * Validate the request body, the request params, and the response with schemas from `@kubb/plugin-zod`.
81
+ *
82
+ * @default false
83
+ */
84
+ validator?: ValidatorOptions
85
+ /**
86
+ * Shape of the value a generated operation function resolves to. Applies to the standalone
87
+ * functions and the class-based SDK. The query-hook plugins (`@kubb/plugin-react-query`,
88
+ * `@kubb/plugin-vue-query`, `@kubb/plugin-swr`) and `@kubb/plugin-mcp` read it to unwrap the
89
+ * call they make, and their own `returnType` sets what a hook resolves to.
90
+ *
91
+ * @default 'full'
92
+ * @example
93
+ * ```ts
94
+ * pluginAxios({ returnType: 'data' })
95
+ * // const pet = await getPetById({ path: { petId: 1 } }) // Pet, not { status, data, ... }
96
+ * ```
97
+ */
98
+ returnType?: ReturnTypeOption
99
+ /**
100
+ * Generates a class-based SDK instead of the standalone functions. Each tag client is an instance
101
+ * class whose constructor takes a client config and builds its own client, so every environment is
102
+ * a separate instance. Leave `sdk` unset to keep the standalone per-operation functions (the
103
+ * default), which is also what query plugins consume.
104
+ *
105
+ * @example Instance class per tag
106
+ * ```ts
107
+ * pluginFetch({ sdk: {} })
108
+ * // const api = new PetClient({ baseURL: 'https://api.example.com' })
109
+ * // await api.getPetById({ path: { petId: 1 } })
110
+ * ```
111
+ * @example Composed root that shares one client with every tag client
112
+ * ```ts
113
+ * pluginFetch({ sdk: { name: 'petStore' } })
114
+ * // class PetStore {
115
+ * // readonly client: ClientInstance
116
+ * // readonly pet: PetClient
117
+ * // readonly store: StoreClient
118
+ * // constructor(config = {}) { ... }
119
+ * // }
120
+ * // const api = new PetStore({ baseURL })
121
+ * // api.client.interceptors.request.use(...) // applies to every tag
122
+ * // await api.pet.getPetById({ path: { petId: 1 } })
123
+ * ```
124
+ * @example Flat class with every operation as a direct method
125
+ * ```ts
126
+ * pluginFetch({ sdk: { name: 'petStore', mode: 'flat' } })
127
+ * // const api = new PetStore({ baseURL })
128
+ * // await api.getPetById({ path: { petId: 1 } })
129
+ * ```
130
+ */
131
+ sdk?: {
132
+ /**
133
+ * How the SDK groups operations.
134
+ * - `'tag'`: one class per tag, such as `PetClient`. With `name`, a composed root shares one client with every tag client.
135
+ * - `'flat'`: one class named by `name`, with every operation as a direct method.
136
+ *
137
+ * @default 'tag'
138
+ */
139
+ mode?: Mode
140
+ /**
141
+ * Name of the generated entry point, also the file name. With `mode: 'tag'` it emits a
142
+ * composed root class that creates one client and shares it with every tag client. With
143
+ * `mode: 'flat'` it names the single class.
144
+ */
145
+ name?: string
146
+ }
147
+ /**
148
+ * Override how names and file paths are built. Methods you omit fall back to the default resolver.
149
+ */
150
+ resolver?: ResolverPatch<ResolverClient>
151
+ /**
152
+ * Macros applied to each operation node before code is printed.
153
+ */
154
+ macros?: Array<ast.Macro>
155
+ }
156
+
157
+ /**
158
+ * The resolved options after defaults are applied.
159
+ */
160
+ export type ClientResolvedOptions = {
161
+ output: Output
162
+ exclude: Array<Exclude>
163
+ include: Array<Include> | undefined
164
+ override: Array<Override<Omit<ClientResolvedOptions, 'throwOnErrorDefault'>>>
165
+ group: Group | null
166
+ baseURL: ClientOptions['baseURL']
167
+ /**
168
+ * Module the generated operations import the client runtime from, used verbatim. Unset, they import the bundled `.kubb/client.ts`.
169
+ */
170
+ importPath?: string
171
+ throwOnErrorDefault: boolean
172
+ validator: NonNullable<ClientOptions['validator']>
173
+ returnType: ReturnTypeOption
174
+ sdk:
175
+ | {
176
+ mode: Mode
177
+ name: string | undefined
178
+ }
179
+ | undefined
180
+ resolver: ResolverClient
181
+ }
182
+
183
+ /**
184
+ * The plugin type behind the shared resolver. Each concrete plugin (plugin-fetch, plugin-axios)
185
+ * declares its own name and registers itself; this base ties the shared builders, resolver, and
186
+ * components to one option/resolver shape.
187
+ */
188
+ export type PluginContractClient = PluginType<'plugin-contract-client', ClientOptions, ClientResolvedOptions, ResolverClient>
189
+
190
+ /**
191
+ * The shape any client plugin (plugin-fetch, plugin-axios) must satisfy to reuse the shared
192
+ * client and SDK generators. Pins the option, resolved-option, and resolver shapes while leaving
193
+ * the plugin name free.
194
+ */
195
+ export type ContractClientFactory = PluginType<string, ClientOptions, ClientResolvedOptions, ResolverClient>
@@ -0,0 +1,97 @@
1
+ import { objectKey } from '@internals/utils'
2
+ import path from 'node:path'
3
+ import { getFormContentTypes, hasEventStreamContent, hasStreamVariant, isEventStream } from '@internals/shared'
4
+
5
+ import { ast, text } from '@kubb/kit'
6
+ import { File } from '@kubb/renderer-jsx'
7
+ import { buildSecuritySchemes } from '../builders/security.ts'
8
+ import { hasQueryStyles } from '../builders/styles.ts'
9
+ import type { KubbReactNode } from '@kubb/renderer-jsx'
10
+
11
+ function buildServersExport(servers: Array<ast.InputServer> | undefined): string | null {
12
+ const named = ast.server.groupByName(servers)
13
+ if (!named.size) return null
14
+ const entries = [...named].map(([name, { url }]) => `${objectKey(name)}: ${text.quote(url)}`)
15
+ return `/**\n * The servers from the spec by name, for \`baseURL\`.\n */\nexport const servers = { ${entries.join(', ')} } as const`
16
+ }
17
+
18
+ // The form media types, each in its own file under `templates/mediaTypes/`, so a document ships only the ones it uses.
19
+ const formMediaTypes = {
20
+ 'multipart/form-data': { file: 'mediaTypes/formData', name: 'multipartFormData' },
21
+ 'application/x-www-form-urlencoded': { file: 'mediaTypes/urlencoded', name: 'urlencoded' },
22
+ } as const
23
+
24
+ /**
25
+ * Writes the runtime a document needs into `.kubb/`: the optional media type, serializer and `sse.ts` files, and a
26
+ * `config.ts` that imports them, which `client.ts` passes to `setConfig`. The footer of `client.ts` gets the named
27
+ * `servers` and `securitySchemes` and the `sse.ts` re-export. `templates` is the folder the runtime is copied from, set
28
+ * by the plugins that ship it, so a custom client from `@kubb/plugin-client` gets none of this.
29
+ */
30
+ export function ClientRuntime({
31
+ nodes,
32
+ root,
33
+ templates,
34
+ baseURL,
35
+ servers,
36
+ auth,
37
+ }: {
38
+ nodes: Array<ast.OperationNode>
39
+ root: string
40
+ templates?: string
41
+ baseURL?: string
42
+ servers?: Array<ast.InputServer>
43
+ auth?: Array<ast.AuthScheme>
44
+ }): KubbReactNode {
45
+ if (!templates) return null
46
+ const hasParameter = (match: (parameter: ast.ParameterNode) => boolean) => nodes.some((node) => node.parameters.some(match))
47
+ const streams = nodes.some((node) => hasEventStreamContent(node) || isEventStream(node) || hasStreamVariant(node))
48
+ const forms = [...new Set(nodes.flatMap(getFormContentTypes))].sort().map((contentType) => ({ contentType, ...formMediaTypes[contentType] }))
49
+ const queryStyles = nodes.some(hasQueryStyles)
50
+
51
+ const serializers = [
52
+ queryStyles
53
+ ? { key: 'query', file: 'serializers/queryStyles', name: 'serializeStyledQuery' }
54
+ : { key: 'query', file: 'serializers/query', name: 'serializeQuery' },
55
+ hasParameter((parameter) => parameter.in === 'path' && (parameter.style === 'label' || parameter.style === 'matrix'))
56
+ ? { key: 'path', file: 'serializers/pathStyles', name: 'serializeStyledPath' }
57
+ : { key: 'path', file: 'serializers/path', name: 'serializePath' },
58
+ hasParameter((parameter) => parameter.in === 'header') ? { key: 'header', file: 'serializers/headers', name: 'serializeHeaders' } : null,
59
+ hasParameter((parameter) => parameter.in === 'cookie') ? { key: 'cookie', file: 'serializers/cookies', name: 'serializeCookies' } : null,
60
+ ].filter((serializer) => serializer !== null)
61
+ const imports = [...forms, ...(streams ? [{ file: 'sse', name: 'streamMediaTypes' }] : []), ...serializers]
62
+ // `pathStyles.ts` builds on `path.ts`, and the urlencoded media type on `queryStyles.ts`.
63
+ const files = new Set([
64
+ ...imports.map(({ file }) => file),
65
+ 'serializers/path',
66
+ ...(forms.some(({ name }) => name === 'urlencoded') ? ['serializers/queryStyles'] : []),
67
+ ])
68
+
69
+ const baseURLExpression = baseURL ? (baseURL.includes('${') ? `\`${baseURL.replaceAll('`', '\\`')}\`` : JSON.stringify(baseURL)) : undefined
70
+ const mediaTypes = [...forms.map(({ contentType, name }) => `'${contentType}': ${name}`), streams ? '...streamMediaTypes' : null].filter(Boolean)
71
+ const config = [
72
+ baseURLExpression ? `baseURL: ${baseURLExpression}` : null,
73
+ mediaTypes.length ? `mediaTypes: { ${mediaTypes.join(', ')} }` : null,
74
+ `serializer: { ${serializers.map(({ key, name }) => `${key}: ${name}`).join(', ')} }`,
75
+ ].filter(Boolean)
76
+ const footer = [buildServersExport(servers), buildSecuritySchemes(auth), streams ? `export * from './sse.ts'` : null].filter(Boolean).join('\n')
77
+ const configPath = path.resolve(root, '.kubb/config.ts')
78
+ const runtimePath = (file: string) => path.resolve(root, '.kubb', `${file}.ts`)
79
+
80
+ return (
81
+ <>
82
+ {[...files].map((file) => (
83
+ <File key={file} baseName={`${path.basename(file)}.ts`} path={runtimePath(file)} copy={path.join(templates, `${file}.ts`)} />
84
+ ))}
85
+ <File baseName="config.ts" path={configPath}>
86
+ {imports.map(({ file, name }) => (
87
+ <File.Import key={name} name={[name]} root={configPath} path={runtimePath(file)} />
88
+ ))}
89
+ <File.Import name={['ClientConfig']} root={configPath} path={runtimePath('core')} isTypeOnly />
90
+ <File.Source name="defaultConfig" isExportable>
91
+ {`/**\n * The config \`client.ts\` starts with: what this spec needs from the runtime.\n */\nexport const defaultConfig: ClientConfig = { ${config.join(', ')} }`}
92
+ </File.Source>
93
+ </File>
94
+ {footer && <File baseName="client.ts" path={path.resolve(root, '.kubb/client.ts')} footer={footer} />}
95
+ </>
96
+ )
97
+ }
@@ -0,0 +1,72 @@
1
+ import type { OperationTypeNames } from '@internals/client'
2
+ import { buildOperationComments } from '@internals/shared'
3
+ import { ast } from '@kubb/kit'
4
+ import { File, Function } from '@kubb/renderer-jsx'
5
+ import type { KubbReactNode } from '@kubb/renderer-jsx'
6
+ import { buildOperationBody } from '../builders/operationBody.ts'
7
+ import type { SecurityRequirement } from '../builders/security.ts'
8
+ import type { ValidatorHooks } from '../builders/validator.ts'
9
+ import type { ReturnTypeOption } from '../types.ts'
10
+
11
+ type Props = {
12
+ /**
13
+ * The generated function name.
14
+ */
15
+ name: string
16
+ /**
17
+ * The operation being generated.
18
+ */
19
+ node: ast.OperationNode
20
+ /**
21
+ * The operation type names the signature references, from `plugin-ts` or `plugin-zod`'s inferred
22
+ * types.
23
+ */
24
+ types: OperationTypeNames
25
+ /**
26
+ * The operation's validator hooks, built once by the generator and shared with its zod imports.
27
+ */
28
+ validatorHooks: ValidatorHooks | null
29
+ /**
30
+ * Shape of the value the generated function resolves to.
31
+ */
32
+ returnType: ReturnTypeOption
33
+ throwOnErrorDefault: boolean
34
+ /**
35
+ * Per-operation security, resolved from the spec into inline security requirements and serialized onto the
36
+ * call config's `security` field for the runtime `auth` resolver to consume.
37
+ */
38
+ security?: Array<SecurityRequirement>
39
+ /** The operation's own server URL from a path-level or operation-level `servers` entry, emitted as the call's default `baseURL`. */
40
+ baseURL?: string
41
+ }
42
+
43
+ /**
44
+ * Renders one client operation: the grouped `<Name>Request` type and the function that forwards a
45
+ * single `options` object to the resolved client and returns the `Unwrappable<RequestResult>`. The
46
+ * type, signature, and call config are built with the AST factory, and only the jsx-renderer emits
47
+ * the source.
48
+ */
49
+ export function Operation({ name, node, types, validatorHooks, returnType, throwOnErrorDefault, security, baseURL }: Props): KubbReactNode {
50
+ if (!ast.http.isOperation(node)) return null
51
+
52
+ const body = buildOperationBody({ node, types, validatorHooks, security, baseURL, returnType, throwOnErrorDefault })
53
+
54
+ return (
55
+ <File.Source name={name} isExportable isIndexable>
56
+ <Function
57
+ name={name}
58
+ export
59
+ generics={body.generics}
60
+ params={body.paramsSignature}
61
+ returnType={body.returnType}
62
+ JSDoc={{ comments: buildOperationComments(node, { splitLines: true }) }}
63
+ >
64
+ {body.mergeContentType
65
+ ? 'const { client: apiClient = client, contentType, ...config } = options'
66
+ : 'const { client: apiClient = client, ...config } = options'}
67
+ <br />
68
+ {body.statement}
69
+ </Function>
70
+ </File.Source>
71
+ )
72
+ }
@@ -0,0 +1,100 @@
1
+ import type { OperationTypeNames } from '@internals/client'
2
+ import { buildOperationComments } from '@internals/shared'
3
+ import { buildJSDoc } from '@internals/utils'
4
+ import { ast } from '@kubb/kit'
5
+ import { File } from '@kubb/renderer-jsx'
6
+ import type { KubbReactNode } from '@kubb/renderer-jsx'
7
+ import { buildOperationBody } from '../builders/operationBody.ts'
8
+ import type { SecurityRequirement } from '../builders/security.ts'
9
+ import type { ValidatorHooks } from '../builders/validator.ts'
10
+ import type { ReturnTypeOption } from '../types.ts'
11
+
12
+ /**
13
+ * Builds one instance method of a generated SDK class; a per-call `options.client` overrides `this.client`.
14
+ */
15
+ export function buildSdkMethod({
16
+ node,
17
+ name,
18
+ types,
19
+ validatorHooks,
20
+ security,
21
+ baseURL,
22
+ returnType,
23
+ throwOnErrorDefault,
24
+ }: {
25
+ node: ast.OperationNode
26
+ name: string
27
+ types: OperationTypeNames
28
+ validatorHooks: ValidatorHooks | null
29
+ security?: Array<SecurityRequirement>
30
+ baseURL?: string
31
+ returnType: ReturnTypeOption
32
+ throwOnErrorDefault: boolean
33
+ }): string {
34
+ if (!ast.http.isOperation(node)) return ''
35
+
36
+ const body = buildOperationBody({ node, types, validatorHooks, security, baseURL, returnType, throwOnErrorDefault })
37
+ const generics = body.generics.length ? `<${body.generics.join(', ')}>` : ''
38
+ const jsdoc = buildJSDoc(buildOperationComments(node, { splitLines: true }))
39
+
40
+ const destructure = body.mergeContentType
41
+ ? 'const { client: apiClient = this.client, contentType, ...config } = options'
42
+ : 'const { client: apiClient = this.client, ...config } = options'
43
+ const methodBody = [destructure, '', body.statement].map((line) => (line ? ` ${line}` : '')).join('\n')
44
+
45
+ return `${jsdoc} public ${name}${generics}(${body.paramsSignature}): ${body.returnType} {\n${methodBody}\n }`
46
+ }
47
+
48
+ type OperationData = {
49
+ node: ast.OperationNode
50
+ name: string
51
+ types: OperationTypeNames
52
+ validatorHooks: ValidatorHooks | null
53
+ security?: Array<SecurityRequirement>
54
+ baseURL?: string
55
+ }
56
+
57
+ type Props = {
58
+ name: string
59
+ operations: Array<OperationData>
60
+ returnType: ReturnTypeOption
61
+ throwOnErrorDefault: boolean
62
+ }
63
+
64
+ /**
65
+ * Renders one instance class per tag with one method per operation. The constructor takes a client
66
+ * config object and builds its own client through `createClient`, so each environment is a separate
67
+ * instance: `const api = new PetClient({ baseURL }); api.getPetById(...)`. It also takes an existing
68
+ * client, which is how the root SDK shares one client across its tags. The client is public so callers
69
+ * can add interceptors, and a per-call `client` option still overrides it for a one-off call.
70
+ */
71
+ export function SdkClient({ name, operations, returnType, throwOnErrorDefault }: Props): KubbReactNode {
72
+ const methods = operations.map(({ node, name: methodName, types, validatorHooks, security, baseURL }) =>
73
+ buildSdkMethod({
74
+ node,
75
+ name: methodName,
76
+ types,
77
+ validatorHooks,
78
+ security,
79
+ baseURL,
80
+ returnType,
81
+ throwOnErrorDefault,
82
+ }),
83
+ )
84
+
85
+ const constructor = [
86
+ ' readonly client: ClientInstance',
87
+ '',
88
+ ' constructor(config: ClientConfig | ClientInstance = {}) {',
89
+ " this.client = typeof config === 'function' ? config : createClient(config)",
90
+ ' }',
91
+ ].join('\n')
92
+
93
+ const classCode = `export class ${name} {\n${constructor}\n\n${methods.join('\n\n')}\n}`
94
+
95
+ return (
96
+ <File.Source name={name} isExportable isIndexable>
97
+ {classCode}
98
+ </File.Source>
99
+ )
100
+ }
@@ -0,0 +1,35 @@
1
+ import { File } from '@kubb/renderer-jsx'
2
+ import type { KubbReactNode } from '@kubb/renderer-jsx'
3
+
4
+ type Member = {
5
+ className: string
6
+ propName: string
7
+ }
8
+
9
+ type Props = {
10
+ name: string
11
+ members: Array<Member>
12
+ }
13
+
14
+ /**
15
+ * Renders a composed root SDK class that creates one client from the config and hands it to every tag
16
+ * client, so `new PetStore({ baseURL }).pet.getPetById(...)` reaches an operation through a single entry
17
+ * point bound to one environment. The shared client is the public `client` field, where interceptors
18
+ * registered once apply to every tag. The per-tag clients are read-only fields built in the constructor.
19
+ */
20
+ export function SdkFacade({ name, members }: Props): KubbReactNode {
21
+ const fields = [' readonly client: ClientInstance', ...members.map((member) => ` readonly ${member.propName}: ${member.className}`)]
22
+ const assignments = [
23
+ ' this.client = createClient(config)',
24
+ ...members.map((member) => ` this.${member.propName} = new ${member.className}(this.client)`),
25
+ ]
26
+ const body = [...fields, '', ' constructor(config: ClientConfig = {}) {', ...assignments, ' }'].join('\n')
27
+
28
+ const classCode = `export class ${name} {\n${body}\n}`
29
+
30
+ return (
31
+ <File.Source name={name} isExportable isIndexable>
32
+ {classCode}
33
+ </File.Source>
34
+ )
35
+ }
@@ -1,9 +1,94 @@
1
- import { createClientGenerator } from '@internals/client'
2
- import type { PluginClient } from '../types.ts'
1
+ import path from 'node:path'
2
+ import { resolveOperationTypes, resultTypeName } from '@internals/client'
3
+ import { getEventStream, operationFileEntry } from '@internals/shared'
4
+ import { ast, defineGenerator } from '@kubb/kit'
5
+ import type { Generator } from '@kubb/kit'
6
+ import { File, jsxRenderer } from '@kubb/renderer-jsx'
7
+ import { resolveOperationData } from '../builders/operationData.ts'
8
+ import { buildEventStreamImports } from '../builders/returnStatement.ts'
9
+ import { Operation } from '../components/Operation.tsx'
10
+ import { ClientRuntime } from '../components/ClientRuntime.tsx'
11
+ import type { ContractClientFactory } from '../types.ts'
3
12
 
4
13
  /**
5
- * Built-in operation generator for `@kubb/plugin-client`. Emits one async function per OpenAPI
6
- * operation using the shared `Operation` component: a grouped `<Name>Request` type and a function
7
- * that forwards a single `options` object to the user-provided client module and returns the `RequestResult`.
14
+ * 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.
8
15
  */
9
- export const clientGenerator = createClientGenerator<PluginClient>('client')
16
+ export function createClientGenerator<TFactory extends ContractClientFactory>(name: string, { templates }: { templates?: string } = {}): Generator<TFactory> {
17
+ return defineGenerator<TFactory>({
18
+ name,
19
+ renderer: jsxRenderer,
20
+ operations(nodes, ctx) {
21
+ return <ClientRuntime nodes={nodes} root={ctx.root} templates={templates} baseURL={ctx.options.baseURL} servers={ctx.meta.servers} auth={ctx.meta.auth} />
22
+ },
23
+ operation(node, ctx) {
24
+ if (!ast.http.isOperation(node)) return null
25
+
26
+ const { config, driver, resolver, root } = ctx
27
+ const { output, returnType, throwOnErrorDefault, group, importPath } = ctx.options
28
+
29
+ const resolvedTypes = resolveOperationTypes(driver, ctx.plugin.name)
30
+ // A custom client module keeps the two-argument `RequestResult` it documents, so its results get no typed headers.
31
+ const types = importPath === undefined ? resolvedTypes : { ...resolvedTypes, response: { ...resolvedTypes.response, headers: undefined } }
32
+
33
+ const data = resolveOperationData({ node, ctx, types })
34
+ const { validatorHooks, headerTypeNames } = data
35
+ const file = resolver.file({ ...operationFileEntry(node, node.operationId), root, output, group: group ?? undefined })
36
+ const importedTypeNames = [types.response.options(node), types.response.responses(node), ...headerTypeNames]
37
+
38
+ // A custom client module is emitted verbatim, so `importPath` is used exactly as configured.
39
+ // The default `.kubb/client.ts` is a file path and is made relative to the operation file.
40
+ const clientPath = importPath ?? path.resolve(root, '.kubb/client.ts')
41
+ const clientRoot = importPath === undefined ? file.path : undefined
42
+ // The headers validator helper ships in `.kubb/standardSchema.ts`; a custom client module exports it
43
+ // alongside its other runtime helpers.
44
+ const standardSchemaPath = importPath ?? path.resolve(root, '.kubb/standardSchema.ts')
45
+ const stream = getEventStream(node)
46
+
47
+ const clientValueImports = [
48
+ ...{
49
+ plain: ['client'],
50
+ data: ['client', 'unwrapResult', 'resolveThrowOnError'],
51
+ full: ['client', 'withUnwrap', 'resolveThrowOnError'],
52
+ }[returnType],
53
+ ...(importPath === undefined && data.security ? ['securitySchemes'] : []),
54
+ ]
55
+
56
+ const clientTypeImports = stream
57
+ ? buildEventStreamImports({ node, types, returnType })
58
+ : { plain: ['Options', 'RequestResult'], data: ['Options', 'UnwrappedResult'], full: ['Options', 'Unwrappable', 'RequestResult'] }[returnType].map(
59
+ (name) => (name.endsWith('Result') ? resultTypeName(name, headerTypeNames.length > 0) : name),
60
+ )
61
+
62
+ return (
63
+ <File
64
+ baseName={file.baseName}
65
+ path={file.path}
66
+ meta={file.meta}
67
+ banner={resolver.default.banner(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
68
+ footer={resolver.default.footer(ctx.meta, { output, config, file: { path: file.path, baseName: file.baseName } })}
69
+ >
70
+ <File.Import name={clientValueImports} root={clientRoot} path={clientPath} />
71
+ <File.Import name={clientTypeImports} root={clientRoot} path={clientPath} isTypeOnly />
72
+
73
+ <File.Import name={Array.from(new Set(importedTypeNames))} root={file.path} path={data.typeFile.path} isTypeOnly />
74
+
75
+ {data.zodFile && validatorHooks.importedZodNames.length > 0 && (
76
+ <File.Import name={validatorHooks.importedZodNames} root={file.path} path={data.zodFile.path} />
77
+ )}
78
+ {validatorHooks.headers && <File.Import name={['toHeadersStandardSchema']} root={clientRoot} path={standardSchemaPath} />}
79
+
80
+ <Operation
81
+ name={data.name}
82
+ node={node}
83
+ types={types}
84
+ validatorHooks={validatorHooks}
85
+ returnType={returnType}
86
+ throwOnErrorDefault={throwOnErrorDefault}
87
+ security={data.security}
88
+ baseURL={data.baseURL}
89
+ />
90
+ </File>
91
+ )
92
+ },
93
+ })
94
+ }