fork of hey-api/openapi-ts because I need some additional things

feat: add ky client plugin

Implements @hey-api/client-ky plugin with support for ky HTTP client.

Features:
- Ky integration with native retry, timeout, and hooks
- Standard interceptors pattern (request/response/error)
- Configurable retry options (limit, methods, statusCodes)
- Raw ky options via kyOptions escape hatch
- Full HTTP method support and SSE
- Follows established client plugin architecture

The implementation maintains consistency with other client plugins
while leveraging ky's powerful built-in features.

+1016 -10
+1
packages/openapi-ts/package.json
··· 116 116 "axios": "1.8.2", 117 117 "cross-spawn": "7.0.6", 118 118 "eslint": "9.17.0", 119 + "ky": "1.14.0", 119 120 "nuxt": "3.14.1592", 120 121 "ofetch": "1.4.1", 121 122 "prettier": "3.4.2",
+328
packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts
··· 1 + import type { HTTPError, Options as KyOptions } from 'ky'; 2 + import ky from 'ky'; 3 + 4 + import { createSseClient } from '../../client-core/bundle/serverSentEvents'; 5 + import type { HttpMethod } from '../../client-core/bundle/types'; 6 + import { getValidRequestBody } from '../../client-core/bundle/utils'; 7 + import type { 8 + Client, 9 + Config, 10 + RequestOptions, 11 + ResolvedRequestOptions, 12 + } from './types'; 13 + import type { Middleware } from './utils'; 14 + import { 15 + buildUrl, 16 + createConfig, 17 + createInterceptors, 18 + getParseAs, 19 + mergeConfigs, 20 + mergeHeaders, 21 + setAuthParams, 22 + } from './utils'; 23 + 24 + export const createClient = (config: Config = {}): Client => { 25 + let _config = mergeConfigs(createConfig(), config); 26 + 27 + const getConfig = (): Config => ({ ..._config }); 28 + 29 + const setConfig = (config: Config): Config => { 30 + _config = mergeConfigs(_config, config); 31 + return getConfig(); 32 + }; 33 + 34 + const interceptors = createInterceptors< 35 + Request, 36 + Response, 37 + unknown, 38 + ResolvedRequestOptions 39 + >(); 40 + 41 + const beforeRequest = async (options: RequestOptions) => { 42 + const opts = { 43 + ..._config, 44 + ...options, 45 + headers: mergeHeaders(_config.headers, options.headers), 46 + ky: options.ky ?? _config.ky ?? ky, 47 + serializedBody: undefined, 48 + }; 49 + 50 + if (opts.security) { 51 + await setAuthParams({ 52 + ...opts, 53 + security: opts.security, 54 + }); 55 + } 56 + 57 + if (opts.requestValidator) { 58 + await opts.requestValidator(opts); 59 + } 60 + 61 + if (opts.body !== undefined && opts.bodySerializer) { 62 + opts.serializedBody = opts.bodySerializer(opts.body); 63 + } 64 + 65 + if (opts.body === undefined || opts.serializedBody === '') { 66 + opts.headers.delete('Content-Type'); 67 + } 68 + 69 + const url = buildUrl(opts); 70 + 71 + return { opts, url }; 72 + }; 73 + 74 + const parseErrorResponse = async ( 75 + response: Response, 76 + request: Request, 77 + opts: ResolvedRequestOptions, 78 + interceptorsMiddleware: Middleware< 79 + Request, 80 + Response, 81 + unknown, 82 + ResolvedRequestOptions 83 + >, 84 + ) => { 85 + const result = { 86 + request, 87 + response, 88 + }; 89 + 90 + const textError = await response.text(); 91 + let jsonError: unknown; 92 + 93 + try { 94 + jsonError = JSON.parse(textError); 95 + } catch { 96 + jsonError = undefined; 97 + } 98 + 99 + const error = jsonError ?? textError; 100 + let finalError = error; 101 + 102 + for (const fn of interceptorsMiddleware.error.fns) { 103 + if (fn) { 104 + finalError = (await fn(error, response, request, opts)) as string; 105 + } 106 + } 107 + 108 + finalError = finalError || ({} as string); 109 + 110 + if (opts.throwOnError) { 111 + throw finalError; 112 + } 113 + 114 + return opts.responseStyle === 'data' 115 + ? undefined 116 + : { 117 + error: finalError, 118 + ...result, 119 + }; 120 + }; 121 + 122 + const request: Client['request'] = async (options) => { 123 + // @ts-expect-error 124 + const { opts, url } = await beforeRequest(options); 125 + 126 + const kyInstance = opts.ky!; 127 + 128 + const validBody = getValidRequestBody(opts); 129 + 130 + const kyOptions: KyOptions = { 131 + body: validBody as BodyInit, 132 + cache: opts.cache, 133 + credentials: opts.credentials, 134 + headers: opts.headers, 135 + integrity: opts.integrity, 136 + keepalive: opts.keepalive, 137 + method: opts.method as KyOptions['method'], 138 + mode: opts.mode, 139 + redirect: 'follow', 140 + referrer: opts.referrer, 141 + referrerPolicy: opts.referrerPolicy, 142 + signal: opts.signal, 143 + throwHttpErrors: opts.throwOnError ?? false, 144 + timeout: opts.timeout, 145 + ...(opts.kyOptions || {}), 146 + }; 147 + 148 + if (opts.retry && typeof opts.retry === 'object') { 149 + kyOptions.retry = { 150 + limit: opts.retry.limit ?? 2, 151 + methods: opts.retry.methods as KyOptions['retry']['methods'], 152 + statusCodes: opts.retry.statusCodes, 153 + }; 154 + } 155 + 156 + let request = new Request(url, { 157 + body: kyOptions.body as BodyInit, 158 + headers: kyOptions.headers, 159 + method: kyOptions.method, 160 + }); 161 + 162 + for (const fn of interceptors.request.fns) { 163 + if (fn) { 164 + request = await fn(request, opts); 165 + } 166 + } 167 + 168 + let response: Response; 169 + 170 + try { 171 + response = await kyInstance(request, kyOptions); 172 + } catch (error) { 173 + if (error && typeof error === 'object' && 'response' in error) { 174 + const httpError = error as HTTPError; 175 + response = httpError.response; 176 + 177 + for (const fn of interceptors.response.fns) { 178 + if (fn) { 179 + response = await fn(response, request, opts); 180 + } 181 + } 182 + 183 + return parseErrorResponse(response, request, opts, interceptors); 184 + } 185 + 186 + throw error; 187 + } 188 + 189 + for (const fn of interceptors.response.fns) { 190 + if (fn) { 191 + response = await fn(response, request, opts); 192 + } 193 + } 194 + 195 + const result = { 196 + request, 197 + response, 198 + }; 199 + 200 + if (response.ok) { 201 + const parseAs = 202 + (opts.parseAs === 'auto' 203 + ? getParseAs(response.headers.get('Content-Type')) 204 + : opts.parseAs) ?? 'json'; 205 + 206 + if ( 207 + response.status === 204 || 208 + response.headers.get('Content-Length') === '0' 209 + ) { 210 + let emptyData: any; 211 + switch (parseAs) { 212 + case 'arrayBuffer': 213 + case 'blob': 214 + case 'text': 215 + emptyData = await response[parseAs](); 216 + break; 217 + case 'formData': 218 + emptyData = new FormData(); 219 + break; 220 + case 'stream': 221 + emptyData = response.body; 222 + break; 223 + case 'json': 224 + default: 225 + emptyData = {}; 226 + break; 227 + } 228 + return opts.responseStyle === 'data' 229 + ? emptyData 230 + : { 231 + data: emptyData, 232 + ...result, 233 + }; 234 + } 235 + 236 + let data: any; 237 + switch (parseAs) { 238 + case 'arrayBuffer': 239 + case 'blob': 240 + case 'formData': 241 + case 'json': 242 + case 'text': 243 + data = await response[parseAs](); 244 + break; 245 + case 'stream': 246 + return opts.responseStyle === 'data' 247 + ? response.body 248 + : { 249 + data: response.body, 250 + ...result, 251 + }; 252 + } 253 + 254 + if (parseAs === 'json') { 255 + if (opts.responseValidator) { 256 + await opts.responseValidator(data); 257 + } 258 + 259 + if (opts.responseTransformer) { 260 + data = await opts.responseTransformer(data); 261 + } 262 + } 263 + 264 + return opts.responseStyle === 'data' 265 + ? data 266 + : { 267 + data, 268 + ...result, 269 + }; 270 + } 271 + 272 + return parseErrorResponse(response, request, opts, interceptors); 273 + }; 274 + 275 + const makeMethodFn = 276 + (method: Uppercase<HttpMethod>) => (options: RequestOptions) => 277 + request({ ...options, method }); 278 + 279 + const makeSseFn = 280 + (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { 281 + const { opts, url } = await beforeRequest(options); 282 + return createSseClient({ 283 + ...opts, 284 + body: opts.body as BodyInit | null | undefined, 285 + fetch: globalThis.fetch, 286 + headers: opts.headers as unknown as Record<string, string>, 287 + method, 288 + onRequest: async (url, init) => { 289 + let request = new Request(url, init); 290 + for (const fn of interceptors.request.fns) { 291 + if (fn) { 292 + request = await fn(request, opts); 293 + } 294 + } 295 + return request; 296 + }, 297 + url, 298 + }); 299 + }; 300 + 301 + return { 302 + buildUrl, 303 + connect: makeMethodFn('CONNECT'), 304 + delete: makeMethodFn('DELETE'), 305 + get: makeMethodFn('GET'), 306 + getConfig, 307 + head: makeMethodFn('HEAD'), 308 + interceptors, 309 + options: makeMethodFn('OPTIONS'), 310 + patch: makeMethodFn('PATCH'), 311 + post: makeMethodFn('POST'), 312 + put: makeMethodFn('PUT'), 313 + request, 314 + setConfig, 315 + sse: { 316 + connect: makeSseFn('CONNECT'), 317 + delete: makeSseFn('DELETE'), 318 + get: makeSseFn('GET'), 319 + head: makeSseFn('HEAD'), 320 + options: makeSseFn('OPTIONS'), 321 + patch: makeSseFn('PATCH'), 322 + post: makeSseFn('POST'), 323 + put: makeSseFn('PUT'), 324 + trace: makeSseFn('TRACE'), 325 + }, 326 + trace: makeMethodFn('TRACE'), 327 + } as Client; 328 + };
+24
packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/index.ts
··· 1 + export type { Auth } from '../../client-core/bundle/auth'; 2 + export type { QuerySerializerOptions } from '../../client-core/bundle/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../../client-core/bundle/bodySerializer'; 8 + export { buildClientParams } from '../../client-core/bundle/params'; 9 + export { serializeQueryKeyValue } from '../../client-core/bundle/queryKeySerializer'; 10 + export { createClient } from './client'; 11 + export type { 12 + Client, 13 + ClientOptions, 14 + Config, 15 + CreateClientConfig, 16 + Options, 17 + RequestOptions, 18 + RequestResult, 19 + ResolvedRequestOptions, 20 + ResponseStyle, 21 + RetryOptions, 22 + TDataShape, 23 + } from './types'; 24 + export { createConfig, mergeHeaders } from './utils';
+271
packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts
··· 1 + import type { Auth } from '../../client-core/bundle/auth'; 2 + import type { 3 + ServerSentEventsOptions, 4 + ServerSentEventsResult, 5 + } from '../../client-core/bundle/serverSentEvents'; 6 + import type { 7 + Client as CoreClient, 8 + Config as CoreConfig, 9 + } from '../../client-core/bundle/types'; 10 + import type { Middleware } from './utils'; 11 + 12 + export type ResponseStyle = 'data' | 'fields'; 13 + 14 + export interface RetryOptions { 15 + /** 16 + * Maximum number of retry attempts 17 + * 18 + * @default 2 19 + */ 20 + limit?: number; 21 + /** 22 + * HTTP methods to retry 23 + * 24 + * @default ['get', 'put', 'head', 'delete', 'options', 'trace'] 25 + */ 26 + methods?: Array< 27 + 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace' 28 + >; 29 + /** 30 + * HTTP status codes to retry 31 + * 32 + * @default [408, 413, 429, 500, 502, 503, 504] 33 + */ 34 + statusCodes?: number[]; 35 + } 36 + 37 + export interface Config<T extends ClientOptions = ClientOptions> 38 + extends Omit< 39 + import('ky').Options, 40 + 'body' | 'headers' | 'method' | 'prefixUrl' | 'retry' | 'throwHttpErrors' 41 + >, 42 + CoreConfig { 43 + /** 44 + * Base URL for all requests made by this client. 45 + */ 46 + baseUrl?: T['baseUrl']; 47 + /** 48 + * Ky instance to use. You can use this option to provide a custom 49 + * ky instance. 50 + */ 51 + ky?: typeof import('ky').default; 52 + /** 53 + * Additional ky-specific options that will be passed directly to ky. 54 + * This allows you to use any ky option not explicitly exposed in the config. 55 + */ 56 + kyOptions?: Omit<import('ky').Options, 'method' | 'prefixUrl'>; 57 + /** 58 + * Return the response data parsed in a specified format. By default, `auto` 59 + * will infer the appropriate method from the `Content-Type` response header. 60 + * You can override this behavior with any of the {@link Body} methods. 61 + * Select `stream` if you don't want to parse response data at all. 62 + * 63 + * @default 'auto' 64 + */ 65 + parseAs?: 66 + | 'arrayBuffer' 67 + | 'auto' 68 + | 'blob' 69 + | 'formData' 70 + | 'json' 71 + | 'stream' 72 + | 'text'; 73 + /** 74 + * Should we return only data or multiple fields (data, error, response, etc.)? 75 + * 76 + * @default 'fields' 77 + */ 78 + responseStyle?: ResponseStyle; 79 + /** 80 + * Retry configuration 81 + */ 82 + retry?: RetryOptions; 83 + /** 84 + * Throw an error instead of returning it in the response? 85 + * 86 + * @default false 87 + */ 88 + throwOnError?: T['throwOnError']; 89 + /** 90 + * Request timeout in milliseconds 91 + * 92 + * @default 10000 93 + */ 94 + timeout?: number; 95 + } 96 + 97 + export interface RequestOptions< 98 + TData = unknown, 99 + TResponseStyle extends ResponseStyle = 'fields', 100 + ThrowOnError extends boolean = boolean, 101 + Url extends string = string, 102 + > extends Config<{ 103 + responseStyle: TResponseStyle; 104 + throwOnError: ThrowOnError; 105 + }>, 106 + Pick< 107 + ServerSentEventsOptions<TData>, 108 + | 'onSseError' 109 + | 'onSseEvent' 110 + | 'sseDefaultRetryDelay' 111 + | 'sseMaxRetryAttempts' 112 + | 'sseMaxRetryDelay' 113 + > { 114 + /** 115 + * Any body that you want to add to your request. 116 + * 117 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 118 + */ 119 + body?: BodyInit | null; 120 + path?: Record<string, unknown>; 121 + query?: Record<string, unknown>; 122 + /** 123 + * Security mechanism(s) to use for the request. 124 + */ 125 + security?: ReadonlyArray<Auth>; 126 + url: Url; 127 + } 128 + 129 + export interface ResolvedRequestOptions< 130 + TResponseStyle extends ResponseStyle = 'fields', 131 + ThrowOnError extends boolean = boolean, 132 + Url extends string = string, 133 + > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { 134 + serializedBody?: string; 135 + } 136 + 137 + export type RequestResult< 138 + TData = unknown, 139 + TError = unknown, 140 + ThrowOnError extends boolean = boolean, 141 + TResponseStyle extends ResponseStyle = 'fields', 142 + > = ThrowOnError extends true 143 + ? Promise< 144 + TResponseStyle extends 'data' 145 + ? TData extends Record<string, unknown> 146 + ? TData[keyof TData] 147 + : TData 148 + : { 149 + data: TData extends Record<string, unknown> 150 + ? TData[keyof TData] 151 + : TData; 152 + request: Request; 153 + response: Response; 154 + } 155 + > 156 + : Promise< 157 + TResponseStyle extends 'data' 158 + ? 159 + | (TData extends Record<string, unknown> 160 + ? TData[keyof TData] 161 + : TData) 162 + | undefined 163 + : ( 164 + | { 165 + data: TData extends Record<string, unknown> 166 + ? TData[keyof TData] 167 + : TData; 168 + error: undefined; 169 + } 170 + | { 171 + data: undefined; 172 + error: TError extends Record<string, unknown> 173 + ? TError[keyof TError] 174 + : TError; 175 + } 176 + ) & { 177 + request: Request; 178 + response: Response; 179 + } 180 + >; 181 + 182 + export interface ClientOptions { 183 + baseUrl?: string; 184 + responseStyle?: ResponseStyle; 185 + throwOnError?: boolean; 186 + } 187 + 188 + type MethodFn = < 189 + TData = unknown, 190 + TError = unknown, 191 + ThrowOnError extends boolean = false, 192 + TResponseStyle extends ResponseStyle = 'fields', 193 + >( 194 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 195 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 196 + 197 + type SseFn = < 198 + TData = unknown, 199 + TError = unknown, 200 + ThrowOnError extends boolean = false, 201 + TResponseStyle extends ResponseStyle = 'fields', 202 + >( 203 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 204 + ) => Promise<ServerSentEventsResult<TData, TError>>; 205 + 206 + type RequestFn = < 207 + TData = unknown, 208 + TError = unknown, 209 + ThrowOnError extends boolean = false, 210 + TResponseStyle extends ResponseStyle = 'fields', 211 + >( 212 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> & 213 + Pick< 214 + Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 215 + 'method' 216 + >, 217 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 218 + 219 + type BuildUrlFn = < 220 + TData extends { 221 + body?: unknown; 222 + path?: Record<string, unknown>; 223 + query?: Record<string, unknown>; 224 + url: string; 225 + }, 226 + >( 227 + options: TData & Options<TData>, 228 + ) => string; 229 + 230 + export type Client = CoreClient< 231 + RequestFn, 232 + Config, 233 + MethodFn, 234 + BuildUrlFn, 235 + SseFn 236 + > & { 237 + interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>; 238 + }; 239 + 240 + /** 241 + * The `createClientConfig()` function will be called on client initialization 242 + * and the returned object will become the client's initial configuration. 243 + * 244 + * You may want to initialize your client this way instead of calling 245 + * `setConfig()`. This is useful for example if you're using Next.js 246 + * to ensure your client always has the correct values. 247 + */ 248 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 249 + override?: Config<ClientOptions & T>, 250 + ) => Config<Required<ClientOptions> & T>; 251 + 252 + export interface TDataShape { 253 + body?: unknown; 254 + headers?: unknown; 255 + path?: unknown; 256 + query?: unknown; 257 + url: string; 258 + } 259 + 260 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 261 + 262 + export type Options< 263 + TData extends TDataShape = TDataShape, 264 + ThrowOnError extends boolean = boolean, 265 + TResponse = unknown, 266 + TResponseStyle extends ResponseStyle = 'fields', 267 + > = OmitKeys< 268 + RequestOptions<TResponse, TResponseStyle, ThrowOnError>, 269 + 'body' | 'path' | 'query' | 'url' 270 + > & 271 + ([TData] extends [never] ? unknown : Omit<TData, 'url'>);
+328
packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/utils.ts
··· 1 + import { getAuthToken } from '../../client-core/bundle/auth'; 2 + import type { QuerySerializerOptions } from '../../client-core/bundle/bodySerializer'; 3 + import { jsonBodySerializer } from '../../client-core/bundle/bodySerializer'; 4 + import { 5 + serializeArrayParam, 6 + serializeObjectParam, 7 + serializePrimitiveParam, 8 + } from '../../client-core/bundle/pathSerializer'; 9 + import { getUrl } from '../../client-core/bundle/utils'; 10 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 11 + 12 + export const createQuerySerializer = <T = unknown>({ 13 + parameters = {}, 14 + ...args 15 + }: QuerySerializerOptions = {}) => { 16 + const querySerializer = (queryParams: T) => { 17 + const search: string[] = []; 18 + if (queryParams && typeof queryParams === 'object') { 19 + for (const name in queryParams) { 20 + const value = queryParams[name]; 21 + 22 + if (value === undefined || value === null) { 23 + continue; 24 + } 25 + 26 + const options = parameters[name] || args; 27 + 28 + if (Array.isArray(value)) { 29 + const serializedArray = serializeArrayParam({ 30 + allowReserved: options.allowReserved, 31 + explode: true, 32 + name, 33 + style: 'form', 34 + value, 35 + ...options.array, 36 + }); 37 + if (serializedArray) search.push(serializedArray); 38 + } else if (typeof value === 'object') { 39 + const serializedObject = serializeObjectParam({ 40 + allowReserved: options.allowReserved, 41 + explode: true, 42 + name, 43 + style: 'deepObject', 44 + value: value as Record<string, unknown>, 45 + ...options.object, 46 + }); 47 + if (serializedObject) search.push(serializedObject); 48 + } else { 49 + const serializedPrimitive = serializePrimitiveParam({ 50 + allowReserved: options.allowReserved, 51 + name, 52 + value: value as string, 53 + }); 54 + if (serializedPrimitive) search.push(serializedPrimitive); 55 + } 56 + } 57 + } 58 + return search.join('&'); 59 + }; 60 + return querySerializer; 61 + }; 62 + 63 + /** 64 + * Infers parseAs value from provided Content-Type header. 65 + */ 66 + export const getParseAs = ( 67 + contentType: string | null, 68 + ): Exclude<Config['parseAs'], 'auto'> => { 69 + if (!contentType) { 70 + return 'stream'; 71 + } 72 + 73 + const cleanContent = contentType.split(';')[0]?.trim(); 74 + 75 + if (!cleanContent) { 76 + return; 77 + } 78 + 79 + if ( 80 + cleanContent.startsWith('application/json') || 81 + cleanContent.endsWith('+json') 82 + ) { 83 + return 'json'; 84 + } 85 + 86 + if (cleanContent === 'multipart/form-data') { 87 + return 'formData'; 88 + } 89 + 90 + if ( 91 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 92 + cleanContent.startsWith(type), 93 + ) 94 + ) { 95 + return 'blob'; 96 + } 97 + 98 + if (cleanContent.startsWith('text/')) { 99 + return 'text'; 100 + } 101 + 102 + return; 103 + }; 104 + 105 + const checkForExistence = ( 106 + options: Pick<RequestOptions, 'auth' | 'query'> & { 107 + headers: Headers; 108 + }, 109 + name?: string, 110 + ): boolean => { 111 + if (!name) { 112 + return false; 113 + } 114 + if ( 115 + options.headers.has(name) || 116 + options.query?.[name] || 117 + options.headers.get('Cookie')?.includes(`${name}=`) 118 + ) { 119 + return true; 120 + } 121 + return false; 122 + }; 123 + 124 + export const setAuthParams = async ({ 125 + security, 126 + ...options 127 + }: Pick<Required<RequestOptions>, 'security'> & 128 + Pick<RequestOptions, 'auth' | 'query'> & { 129 + headers: Headers; 130 + }) => { 131 + for (const auth of security) { 132 + if (checkForExistence(options, auth.name)) { 133 + continue; 134 + } 135 + 136 + const token = await getAuthToken(auth, options.auth); 137 + 138 + if (!token) { 139 + continue; 140 + } 141 + 142 + const name = auth.name ?? 'Authorization'; 143 + 144 + switch (auth.in) { 145 + case 'query': 146 + if (!options.query) { 147 + options.query = {}; 148 + } 149 + options.query[name] = token; 150 + break; 151 + case 'cookie': 152 + options.headers.append('Cookie', `${name}=${token}`); 153 + break; 154 + case 'header': 155 + default: 156 + options.headers.set(name, token); 157 + break; 158 + } 159 + } 160 + }; 161 + 162 + export const buildUrl: Client['buildUrl'] = (options) => 163 + getUrl({ 164 + baseUrl: options.baseUrl as string, 165 + path: options.path, 166 + query: options.query, 167 + querySerializer: 168 + typeof options.querySerializer === 'function' 169 + ? options.querySerializer 170 + : createQuerySerializer(options.querySerializer), 171 + url: options.url, 172 + }); 173 + 174 + export const mergeConfigs = (a: Config, b: Config): Config => { 175 + const config = { ...a, ...b }; 176 + if (config.baseUrl?.endsWith('/')) { 177 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 178 + } 179 + config.headers = mergeHeaders(a.headers, b.headers); 180 + return config; 181 + }; 182 + 183 + const headersEntries = (headers: Headers): Array<[string, string]> => { 184 + const entries: Array<[string, string]> = []; 185 + headers.forEach((value, key) => { 186 + entries.push([key, value]); 187 + }); 188 + return entries; 189 + }; 190 + 191 + export const mergeHeaders = ( 192 + ...headers: Array<Required<Config>['headers'] | undefined> 193 + ): Headers => { 194 + const mergedHeaders = new Headers(); 195 + for (const header of headers) { 196 + if (!header) { 197 + continue; 198 + } 199 + 200 + const iterator = 201 + header instanceof Headers 202 + ? headersEntries(header) 203 + : Object.entries(header); 204 + 205 + for (const [key, value] of iterator) { 206 + if (value === null) { 207 + mergedHeaders.delete(key); 208 + } else if (Array.isArray(value)) { 209 + for (const v of value) { 210 + mergedHeaders.append(key, v as string); 211 + } 212 + } else if (value !== undefined) { 213 + mergedHeaders.set( 214 + key, 215 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 216 + ); 217 + } 218 + } 219 + } 220 + return mergedHeaders; 221 + }; 222 + 223 + type ErrInterceptor<Err, Res, Req, Options> = ( 224 + error: Err, 225 + response: Res, 226 + request: Req, 227 + options: Options, 228 + ) => Err | Promise<Err>; 229 + 230 + type ReqInterceptor<Req, Options> = ( 231 + request: Req, 232 + options: Options, 233 + ) => Req | Promise<Req>; 234 + 235 + type ResInterceptor<Res, Req, Options> = ( 236 + response: Res, 237 + request: Req, 238 + options: Options, 239 + ) => Res | Promise<Res>; 240 + 241 + class Interceptors<Interceptor> { 242 + fns: Array<Interceptor | null> = []; 243 + 244 + clear(): void { 245 + this.fns = []; 246 + } 247 + 248 + eject(id: number | Interceptor): void { 249 + const index = this.getInterceptorIndex(id); 250 + if (this.fns[index]) { 251 + this.fns[index] = null; 252 + } 253 + } 254 + 255 + exists(id: number | Interceptor): boolean { 256 + const index = this.getInterceptorIndex(id); 257 + return Boolean(this.fns[index]); 258 + } 259 + 260 + getInterceptorIndex(id: number | Interceptor): number { 261 + if (typeof id === 'number') { 262 + return this.fns[id] ? id : -1; 263 + } 264 + return this.fns.indexOf(id); 265 + } 266 + 267 + update( 268 + id: number | Interceptor, 269 + fn: Interceptor, 270 + ): number | Interceptor | false { 271 + const index = this.getInterceptorIndex(id); 272 + if (this.fns[index]) { 273 + this.fns[index] = fn; 274 + return id; 275 + } 276 + return false; 277 + } 278 + 279 + use(fn: Interceptor): number { 280 + this.fns.push(fn); 281 + return this.fns.length - 1; 282 + } 283 + } 284 + 285 + export interface Middleware<Req, Res, Err, Options> { 286 + error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>; 287 + request: Interceptors<ReqInterceptor<Req, Options>>; 288 + response: Interceptors<ResInterceptor<Res, Req, Options>>; 289 + } 290 + 291 + export const createInterceptors = <Req, Res, Err, Options>(): Middleware< 292 + Req, 293 + Res, 294 + Err, 295 + Options 296 + > => ({ 297 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 298 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 299 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 300 + }); 301 + 302 + const defaultQuerySerializer = createQuerySerializer({ 303 + allowReserved: false, 304 + array: { 305 + explode: true, 306 + style: 'form', 307 + }, 308 + object: { 309 + explode: true, 310 + style: 'deepObject', 311 + }, 312 + }); 313 + 314 + const defaultHeaders = { 315 + 'Content-Type': 'application/json', 316 + }; 317 + 318 + export const createConfig = <T extends ClientOptions = ClientOptions>( 319 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 320 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 321 + ...jsonBodySerializer, 322 + headers: defaultHeaders, 323 + parseAs: 'auto', 324 + querySerializer: defaultQuerySerializer, 325 + throwOnError: false, 326 + timeout: 10000, 327 + ...override, 328 + });
+23
packages/openapi-ts/src/plugins/@hey-api/client-ky/config.ts
··· 1 + import { 2 + clientDefaultConfig, 3 + clientDefaultMeta, 4 + } from '~/plugins/@hey-api/client-core/config'; 5 + import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; 6 + import { definePluginConfig } from '~/plugins/shared/utils/config'; 7 + 8 + import type { HeyApiClientKyPlugin } from './types'; 9 + 10 + export const defaultConfig: HeyApiClientKyPlugin['Config'] = { 11 + ...clientDefaultMeta, 12 + config: { 13 + ...clientDefaultConfig, 14 + throwOnError: false, 15 + }, 16 + handler: clientPluginHandler, 17 + name: '@hey-api/client-ky', 18 + }; 19 + 20 + /** 21 + * Type helper for `@hey-api/client-ky` plugin, returns {@link Plugin.Config} object 22 + */ 23 + export const defineConfig = definePluginConfig(defaultConfig);
+3
packages/openapi-ts/src/plugins/@hey-api/client-ky/index.ts
··· 1 + export type { Client as KyClient } from './bundle/types'; 2 + export { defaultConfig, defineConfig } from './config'; 3 + export type { HeyApiClientKyPlugin } from './types';
+14
packages/openapi-ts/src/plugins/@hey-api/client-ky/types.d.ts
··· 1 + import type { DefinePlugin, Plugin } from '~/plugins'; 2 + import type { Client } from '~/plugins/@hey-api/client-core/types'; 3 + 4 + export type UserConfig = Plugin.Name<'@hey-api/client-ky'> & 5 + Client.Config & { 6 + /** 7 + * Throw an error instead of returning it in the response? 8 + * 9 + * @default false 10 + */ 11 + throwOnError?: boolean; 12 + }; 13 + 14 + export type HeyApiClientKyPlugin = DefinePlugin<UserConfig, UserConfig>;
+4
packages/openapi-ts/src/plugins/config.ts
··· 7 7 import { defaultConfig as heyApiClientAxios } from '~/plugins/@hey-api/client-axios'; 8 8 import type { HeyApiClientFetchPlugin } from '~/plugins/@hey-api/client-fetch'; 9 9 import { defaultConfig as heyApiClientFetch } from '~/plugins/@hey-api/client-fetch'; 10 + import type { HeyApiClientKyPlugin } from '~/plugins/@hey-api/client-ky'; 11 + import { defaultConfig as heyApiClientKy } from '~/plugins/@hey-api/client-ky'; 10 12 import type { HeyApiClientNextPlugin } from '~/plugins/@hey-api/client-next'; 11 13 import { defaultConfig as heyApiClientNext } from '~/plugins/@hey-api/client-next'; 12 14 import type { HeyApiClientNuxtPlugin } from '~/plugins/@hey-api/client-nuxt'; ··· 50 52 '@hey-api/client-angular': HeyApiClientAngularPlugin['Types']; 51 53 '@hey-api/client-axios': HeyApiClientAxiosPlugin['Types']; 52 54 '@hey-api/client-fetch': HeyApiClientFetchPlugin['Types']; 55 + '@hey-api/client-ky': HeyApiClientKyPlugin['Types']; 53 56 '@hey-api/client-next': HeyApiClientNextPlugin['Types']; 54 57 '@hey-api/client-nuxt': HeyApiClientNuxtPlugin['Types']; 55 58 '@hey-api/client-ofetch': HeyApiClientOfetchPlugin['Types']; ··· 77 80 '@hey-api/client-angular': heyApiClientAngular, 78 81 '@hey-api/client-axios': heyApiClientAxios, 79 82 '@hey-api/client-fetch': heyApiClientFetch, 83 + '@hey-api/client-ky': heyApiClientKy, 80 84 '@hey-api/client-next': heyApiClientNext, 81 85 '@hey-api/client-nuxt': heyApiClientNuxt, 82 86 '@hey-api/client-ofetch': heyApiClientOfetch,
+1
packages/openapi-ts/src/plugins/types.d.ts
··· 7 7 | '@hey-api/client-angular' 8 8 | '@hey-api/client-axios' 9 9 | '@hey-api/client-fetch' 10 + | '@hey-api/client-ky' 10 11 | '@hey-api/client-next' 11 12 | '@hey-api/client-nuxt' 12 13 | '@hey-api/client-ofetch';
+19 -10
pnpm-lock.yaml
··· 1303 1303 eslint: 1304 1304 specifier: 9.17.0 1305 1305 version: 9.17.0(jiti@2.6.1) 1306 + ky: 1307 + specifier: 1.14.0 1308 + version: 1.14.0 1306 1309 nuxt: 1307 1310 specifier: 3.14.1592 1308 1311 version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.1)) ··· 10030 10033 kuler@2.0.0: 10031 10034 resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} 10032 10035 10036 + ky@1.14.0: 10037 + resolution: {integrity: sha512-Rczb6FMM6JT0lvrOlP5WUOCB7s9XKxzwgErzhKlKde1bEV90FXplV1o87fpt4PU/asJFiqjYJxAJyzJhcrxOsQ==} 10038 + engines: {node: '>=18'} 10039 + 10033 10040 lambda-local@2.2.0: 10034 10041 resolution: {integrity: sha512-bPcgpIXbHnVGfI/omZIlgucDqlf4LrsunwoKue5JdZeGybt8L6KyJz2Zu19ffuZwIwLj2NAI2ZyaqNT6/cetcg==} 10035 10042 engines: {node: '>=8'} ··· 14252 14259 '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.1)) 14253 14260 ansi-colors: 4.1.3 14254 14261 autoprefixer: 10.4.20(postcss@8.5.2) 14255 - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) 14262 + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) 14256 14263 browserslist: 4.25.4 14257 14264 copy-webpack-plugin: 12.0.2(webpack@5.98.0) 14258 14265 css-loader: 7.1.2(webpack@5.98.0) ··· 14272 14279 picomatch: 4.0.2 14273 14280 piscina: 4.8.0 14274 14281 postcss: 8.5.2 14275 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 14282 + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) 14276 14283 resolve-url-loader: 5.0.0 14277 14284 rxjs: 7.8.1 14278 14285 sass: 1.85.0 ··· 14340 14347 '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.1)) 14341 14348 ansi-colors: 4.1.3 14342 14349 autoprefixer: 10.4.20(postcss@8.5.2) 14343 - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) 14350 + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) 14344 14351 browserslist: 4.25.4 14345 14352 copy-webpack-plugin: 12.0.2(webpack@5.98.0) 14346 14353 css-loader: 7.1.2(webpack@5.98.0) ··· 14360 14367 picomatch: 4.0.2 14361 14368 piscina: 4.8.0 14362 14369 postcss: 8.5.2 14363 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 14370 + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) 14364 14371 resolve-url-loader: 5.0.0 14365 14372 rxjs: 7.8.1 14366 14373 sass: 1.85.0 ··· 14448 14455 picomatch: 4.0.2 14449 14456 piscina: 4.8.0 14450 14457 postcss: 8.5.2 14451 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 14458 + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) 14452 14459 resolve-url-loader: 5.0.0 14453 14460 rxjs: 7.8.1 14454 14461 sass: 1.85.0 ··· 21753 21760 schema-utils: 4.3.2 21754 21761 webpack: 5.98.0(esbuild@0.25.0) 21755 21762 21756 - babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)): 21763 + babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0): 21757 21764 dependencies: 21758 21765 '@babel/core': 7.26.9 21759 21766 find-cache-dir: 4.0.0 ··· 23183 23190 eslint: 9.17.0(jiti@2.6.1) 23184 23191 eslint-import-resolver-node: 0.3.9 23185 23192 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) 23186 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) 23193 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) 23187 23194 eslint-plugin-jsx-a11y: 6.10.2(eslint@9.17.0(jiti@2.6.1)) 23188 23195 eslint-plugin-react: 7.37.5(eslint@9.17.0(jiti@2.6.1)) 23189 23196 eslint-plugin-react-hooks: 5.2.0(eslint@9.17.0(jiti@2.6.1)) ··· 23221 23228 tinyglobby: 0.2.14 23222 23229 unrs-resolver: 1.11.1 23223 23230 optionalDependencies: 23224 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) 23231 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) 23225 23232 transitivePeerDependencies: 23226 23233 - supports-color 23227 23234 ··· 23236 23243 transitivePeerDependencies: 23237 23244 - supports-color 23238 23245 23239 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)): 23246 + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)): 23240 23247 dependencies: 23241 23248 '@rtsao/scc': 1.1.0 23242 23249 array-includes: 3.1.9 ··· 24913 24920 kolorist@1.8.0: {} 24914 24921 24915 24922 kuler@2.0.0: {} 24923 + 24924 + ky@1.14.0: {} 24916 24925 24917 24926 lambda-local@2.2.0: 24918 24927 dependencies: ··· 26986 26995 ts-node: 10.9.2(@types/node@22.10.5)(typescript@5.9.3) 26987 26996 optional: true 26988 26997 26989 - postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)): 26998 + postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0): 26990 26999 dependencies: 26991 27000 cosmiconfig: 9.0.0(typescript@5.8.3) 26992 27001 jiti: 1.21.7