From 3b2d16cbb7c32217cee16e26c00bb6d846571571 Mon Sep 17 00:00:00 2001 From: Sebastiaan Wouters Date: Tue, 11 Nov 2025 10:49:40 +0100 Subject: [PATCH 1/5] 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. --- packages/openapi-ts/package.json | 1 + .../@hey-api/client-ky/bundle/client.ts | 328 ++++++++++++++++++ .../@hey-api/client-ky/bundle/index.ts | 24 ++ .../@hey-api/client-ky/bundle/types.ts | 271 +++++++++++++++ .../@hey-api/client-ky/bundle/utils.ts | 328 ++++++++++++++++++ .../src/plugins/@hey-api/client-ky/config.ts | 23 ++ .../src/plugins/@hey-api/client-ky/index.ts | 3 + .../src/plugins/@hey-api/client-ky/types.d.ts | 14 + packages/openapi-ts/src/plugins/config.ts | 4 + packages/openapi-ts/src/plugins/types.d.ts | 1 + pnpm-lock.yaml | 29 +- 11 files changed, 1016 insertions(+), 10 deletions(-) create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/index.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/utils.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/config.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/index.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/types.d.ts diff --git a/packages/openapi-ts/package.json b/packages/openapi-ts/package.json index aa84bf3b2..9e322c0b9 100644 --- a/packages/openapi-ts/package.json +++ b/packages/openapi-ts/package.json @@ -116,6 +116,7 @@ "axios": "1.8.2", "cross-spawn": "7.0.6", "eslint": "9.17.0", + "ky": "1.14.0", "nuxt": "3.14.1592", "ofetch": "1.4.1", "prettier": "3.4.2", diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts new file mode 100644 index 000000000..2a1171703 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts @@ -0,0 +1,328 @@ +import type { HTTPError, Options as KyOptions } from 'ky'; +import ky from 'ky'; + +import { createSseClient } from '../../client-core/bundle/serverSentEvents'; +import type { HttpMethod } from '../../client-core/bundle/types'; +import { getValidRequestBody } from '../../client-core/bundle/utils'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types'; +import type { Middleware } from './utils'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils'; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + headers: mergeHeaders(_config.headers, options.headers), + ky: options.ky ?? _config.ky ?? ky, + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const parseErrorResponse = async ( + response: Response, + request: Request, + opts: ResolvedRequestOptions, + interceptorsMiddleware: Middleware< + Request, + Response, + unknown, + ResolvedRequestOptions + >, + ) => { + const result = { + request, + response, + }; + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + jsonError = undefined; + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptorsMiddleware.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + + const kyInstance = opts.ky!; + + const validBody = getValidRequestBody(opts); + + const kyOptions: KyOptions = { + body: validBody as BodyInit, + cache: opts.cache, + credentials: opts.credentials, + headers: opts.headers, + integrity: opts.integrity, + keepalive: opts.keepalive, + method: opts.method as KyOptions['method'], + mode: opts.mode, + redirect: 'follow', + referrer: opts.referrer, + referrerPolicy: opts.referrerPolicy, + signal: opts.signal, + throwHttpErrors: opts.throwOnError ?? false, + timeout: opts.timeout, + ...(opts.kyOptions || {}), + }; + + if (opts.retry && typeof opts.retry === 'object') { + kyOptions.retry = { + limit: opts.retry.limit ?? 2, + methods: opts.retry.methods as KyOptions['retry']['methods'], + statusCodes: opts.retry.statusCodes, + }; + } + + let request = new Request(url, { + body: kyOptions.body as BodyInit, + headers: kyOptions.headers, + method: kyOptions.method, + }); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + let response: Response; + + try { + response = await kyInstance(request, kyOptions); + } catch (error) { + if (error && typeof error === 'object' && 'response' in error) { + const httpError = error as HTTPError; + response = httpError.response; + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + return parseErrorResponse(response, request, opts, interceptors); + } + + throw error; + } + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + return parseErrorResponse(response, request, opts, interceptors); + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + fetch: globalThis.fetch, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/index.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/index.ts new file mode 100644 index 000000000..00f096436 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/index.ts @@ -0,0 +1,24 @@ +export type { Auth } from '../../client-core/bundle/auth'; +export type { QuerySerializerOptions } from '../../client-core/bundle/bodySerializer'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../../client-core/bundle/bodySerializer'; +export { buildClientParams } from '../../client-core/bundle/params'; +export { serializeQueryKeyValue } from '../../client-core/bundle/queryKeySerializer'; +export { createClient } from './client'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + RetryOptions, + TDataShape, +} from './types'; +export { createConfig, mergeHeaders } from './utils'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts new file mode 100644 index 000000000..6aac2a257 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts @@ -0,0 +1,271 @@ +import type { Auth } from '../../client-core/bundle/auth'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../../client-core/bundle/serverSentEvents'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../../client-core/bundle/types'; +import type { Middleware } from './utils'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface RetryOptions { + /** + * Maximum number of retry attempts + * + * @default 2 + */ + limit?: number; + /** + * HTTP methods to retry + * + * @default ['get', 'put', 'head', 'delete', 'options', 'trace'] + */ + methods?: Array< + 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace' + >; + /** + * HTTP status codes to retry + * + * @default [408, 413, 429, 500, 502, 503, 504] + */ + statusCodes?: number[]; +} + +export interface Config + extends Omit< + import('ky').Options, + 'body' | 'headers' | 'method' | 'prefixUrl' | 'retry' | 'throwHttpErrors' + >, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Ky instance to use. You can use this option to provide a custom + * ky instance. + */ + ky?: typeof import('ky').default; + /** + * Additional ky-specific options that will be passed directly to ky. + * This allows you to use any ky option not explicitly exposed in the config. + */ + kyOptions?: Omit; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Retry configuration + */ + retry?: RetryOptions; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; + /** + * Request timeout in milliseconds + * + * @default 10000 + */ + timeout?: number; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: BodyInit | null; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/utils.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/utils.ts new file mode 100644 index 000000000..efeb558fe --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/utils.ts @@ -0,0 +1,328 @@ +import { getAuthToken } from '../../client-core/bundle/auth'; +import type { QuerySerializerOptions } from '../../client-core/bundle/bodySerializer'; +import { jsonBodySerializer } from '../../client-core/bundle/bodySerializer'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../../client-core/bundle/pathSerializer'; +import { getUrl } from '../../client-core/bundle/utils'; +import type { Client, ClientOptions, Config, RequestOptions } from './types'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + throwOnError: false, + timeout: 10000, + ...override, +}); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/config.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/config.ts new file mode 100644 index 000000000..37fa5deb5 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/config.ts @@ -0,0 +1,23 @@ +import { + clientDefaultConfig, + clientDefaultMeta, +} from '~/plugins/@hey-api/client-core/config'; +import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + +import type { HeyApiClientKyPlugin } from './types'; + +export const defaultConfig: HeyApiClientKyPlugin['Config'] = { + ...clientDefaultMeta, + config: { + ...clientDefaultConfig, + throwOnError: false, + }, + handler: clientPluginHandler, + name: '@hey-api/client-ky', +}; + +/** + * Type helper for `@hey-api/client-ky` plugin, returns {@link Plugin.Config} object + */ +export const defineConfig = definePluginConfig(defaultConfig); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/index.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/index.ts new file mode 100644 index 000000000..c66f46777 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/index.ts @@ -0,0 +1,3 @@ +export type { Client as KyClient } from './bundle/types'; +export { defaultConfig, defineConfig } from './config'; +export type { HeyApiClientKyPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/types.d.ts new file mode 100644 index 000000000..aa9e43a77 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/types.d.ts @@ -0,0 +1,14 @@ +import type { DefinePlugin, Plugin } from '~/plugins'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; + +export type UserConfig = Plugin.Name<'@hey-api/client-ky'> & + Client.Config & { + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: boolean; + }; + +export type HeyApiClientKyPlugin = DefinePlugin; diff --git a/packages/openapi-ts/src/plugins/config.ts b/packages/openapi-ts/src/plugins/config.ts index 590e88bfc..ee424efc8 100644 --- a/packages/openapi-ts/src/plugins/config.ts +++ b/packages/openapi-ts/src/plugins/config.ts @@ -7,6 +7,8 @@ import type { HeyApiClientAxiosPlugin } from '~/plugins/@hey-api/client-axios'; import { defaultConfig as heyApiClientAxios } from '~/plugins/@hey-api/client-axios'; import type { HeyApiClientFetchPlugin } from '~/plugins/@hey-api/client-fetch'; import { defaultConfig as heyApiClientFetch } from '~/plugins/@hey-api/client-fetch'; +import type { HeyApiClientKyPlugin } from '~/plugins/@hey-api/client-ky'; +import { defaultConfig as heyApiClientKy } from '~/plugins/@hey-api/client-ky'; import type { HeyApiClientNextPlugin } from '~/plugins/@hey-api/client-next'; import { defaultConfig as heyApiClientNext } from '~/plugins/@hey-api/client-next'; import type { HeyApiClientNuxtPlugin } from '~/plugins/@hey-api/client-nuxt'; @@ -50,6 +52,7 @@ export interface PluginConfigMap { '@hey-api/client-angular': HeyApiClientAngularPlugin['Types']; '@hey-api/client-axios': HeyApiClientAxiosPlugin['Types']; '@hey-api/client-fetch': HeyApiClientFetchPlugin['Types']; + '@hey-api/client-ky': HeyApiClientKyPlugin['Types']; '@hey-api/client-next': HeyApiClientNextPlugin['Types']; '@hey-api/client-nuxt': HeyApiClientNuxtPlugin['Types']; '@hey-api/client-ofetch': HeyApiClientOfetchPlugin['Types']; @@ -77,6 +80,7 @@ export const defaultPluginConfigs: { '@hey-api/client-angular': heyApiClientAngular, '@hey-api/client-axios': heyApiClientAxios, '@hey-api/client-fetch': heyApiClientFetch, + '@hey-api/client-ky': heyApiClientKy, '@hey-api/client-next': heyApiClientNext, '@hey-api/client-nuxt': heyApiClientNuxt, '@hey-api/client-ofetch': heyApiClientOfetch, diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index 04a7691ee..064647a2a 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -7,6 +7,7 @@ export type PluginClientNames = | '@hey-api/client-angular' | '@hey-api/client-axios' | '@hey-api/client-fetch' + | '@hey-api/client-ky' | '@hey-api/client-next' | '@hey-api/client-nuxt' | '@hey-api/client-ofetch'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0dec47e5..7030899b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1303,6 +1303,9 @@ importers: eslint: specifier: 9.17.0 version: 9.17.0(jiti@2.6.1) + ky: + specifier: 1.14.0 + version: 1.14.0 nuxt: specifier: 3.14.1592 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,6 +10033,10 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + ky@1.14.0: + resolution: {integrity: sha512-Rczb6FMM6JT0lvrOlP5WUOCB7s9XKxzwgErzhKlKde1bEV90FXplV1o87fpt4PU/asJFiqjYJxAJyzJhcrxOsQ==} + engines: {node: '>=18'} + lambda-local@2.2.0: resolution: {integrity: sha512-bPcgpIXbHnVGfI/omZIlgucDqlf4LrsunwoKue5JdZeGybt8L6KyJz2Zu19ffuZwIwLj2NAI2ZyaqNT6/cetcg==} engines: {node: '>=8'} @@ -14252,7 +14259,7 @@ snapshots: '@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)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14272,7 +14279,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14340,7 +14347,7 @@ snapshots: '@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)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14360,7 +14367,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14448,7 +14455,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -21753,7 +21760,7 @@ snapshots: schema-utils: 4.3.2 webpack: 5.98.0(esbuild@0.25.0) - babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)): + babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0): dependencies: '@babel/core': 7.26.9 find-cache-dir: 4.0.0 @@ -23183,7 +23190,7 @@ snapshots: eslint: 9.17.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 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-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)) + 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)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.17.0(jiti@2.6.1)) @@ -23221,7 +23228,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - 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)) + 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)) transitivePeerDependencies: - supports-color @@ -23236,7 +23243,7 @@ snapshots: transitivePeerDependencies: - supports-color - 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)): + 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)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -24914,6 +24921,8 @@ snapshots: kuler@2.0.0: {} + ky@1.14.0: {} + lambda-local@2.2.0: dependencies: commander: 10.0.1 @@ -26986,7 +26995,7 @@ snapshots: ts-node: 10.9.2(@types/node@22.10.5)(typescript@5.9.3) optional: true - postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)): + postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 From 9c683e39ac15fbd6d6f8274464e8efb86eda2d31 Mon Sep 17 00:00:00 2001 From: Sebastiaan Wouters Date: Tue, 11 Nov 2025 11:08:02 +0100 Subject: [PATCH 2/5] test(client-ky): add comprehensive test coverage Add test suite for ky client plugin with comprehensive coverage: - buildUrl functionality with various path/query scenarios - zero-length body handling for all content types - unserialized and serialized request body handling - request, response, and error interceptors - error handling with throwOnError option - retry configuration - responseStyle configuration Tests follow patterns from client-fetch and cover all core functionality. --- .../client-ky/__tests__/client.test.ts | 652 ++++++++++++++++++ .../client-ky/__tests__/utils.test.ts | 269 ++++++++ 2 files changed, 921 insertions(+) create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/utils.test.ts diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts new file mode 100644 index 000000000..cfc8b4c2f --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts @@ -0,0 +1,652 @@ +import { HTTPError } from 'ky'; +import { describe, expect, it, vi } from 'vitest'; + +import type { ResolvedRequestOptions } from '../bundle'; +import { createClient } from '../bundle/client'; + +type MockKy = ((...args: any[]) => any) & { + preconnect?: any; +}; + +describe('buildUrl', () => { + const client = createClient(); + + const scenarios: { + options: Parameters[0]; + url: string; + }[] = [ + { + options: { + url: '', + }, + url: '/', + }, + { + options: { + url: '/foo', + }, + url: '/foo', + }, + { + options: { + path: { + fooId: 1, + }, + url: '/foo/{fooId}', + }, + url: '/foo/1', + }, + { + options: { + path: { + fooId: 1, + }, + query: { + bar: 'baz', + }, + url: '/foo/{fooId}', + }, + url: '/foo/1?bar=baz', + }, + { + options: { + query: { + bar: [], + foo: [], + }, + url: '/', + }, + url: '/', + }, + { + options: { + query: { + bar: [], + foo: ['abc', 'def'], + }, + url: '/', + }, + url: '/?foo=abc&foo=def', + }, + ]; + + it.each(scenarios)('returns $url', ({ options, url }) => { + expect(client.buildUrl(options)).toBe(url); + }); +}); + +describe('zero-length body handling', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + it('returns empty Blob for zero-length application/octet-stream response', async () => { + const mockResponse = new Response(null, { + headers: { + 'Content-Length': '0', + 'Content-Type': 'application/octet-stream', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.request({ + ky: mockKy, + method: 'GET', + url: '/test', + }); + + expect(result.data).toBeInstanceOf(Blob); + expect((result.data as Blob).size).toBe(0); + }); + + it('returns empty ArrayBuffer for zero-length response with arrayBuffer parseAs', async () => { + const mockResponse = new Response(null, { + headers: { + 'Content-Length': '0', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.request({ + ky: mockKy, + method: 'GET', + parseAs: 'arrayBuffer', + url: '/test', + }); + + expect(result.data).toBeInstanceOf(ArrayBuffer); + expect((result.data as ArrayBuffer).byteLength).toBe(0); + }); + + it('returns empty string for zero-length text response', async () => { + const mockResponse = new Response(null, { + headers: { + 'Content-Length': '0', + 'Content-Type': 'text/plain', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.request({ + ky: mockKy, + method: 'GET', + url: '/test', + }); + + expect(result.data).toBe(''); + }); + + it('returns empty object for zero-length JSON response', async () => { + const mockResponse = new Response(null, { + headers: { + 'Content-Length': '0', + 'Content-Type': 'application/json', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.request({ + ky: mockKy, + method: 'GET', + url: '/test', + }); + + expect(result.data).toEqual({}); + }); + + it('returns empty FormData for zero-length multipart/form-data response', async () => { + const mockResponse = new Response(null, { + headers: { + 'Content-Length': '0', + 'Content-Type': 'multipart/form-data', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.request({ + ky: mockKy, + method: 'GET', + url: '/test', + }); + + expect(result.data).toBeInstanceOf(FormData); + expect([...(result.data as FormData).entries()]).toHaveLength(0); + }); + + it('returns stream body for zero-length stream response', async () => { + const mockBody = new ReadableStream(); + const mockResponse = new Response(mockBody, { + headers: { + 'Content-Length': '0', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.request({ + ky: mockKy, + method: 'GET', + parseAs: 'stream', + url: '/test', + }); + + expect(result.data).toBe(mockBody); + }); + + it('handles non-zero content correctly for comparison', async () => { + const blobContent = new Blob(['test data']); + const mockResponse = new Response(blobContent, { + headers: { + 'Content-Type': 'application/octet-stream', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.request({ + ky: mockKy, + method: 'GET', + url: '/test', + }); + + expect(result.data).toBeInstanceOf(Blob); + expect((result.data as Blob).size).toBeGreaterThan(0); + }); +}); + +describe('unserialized request body handling', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + const scenarios = [ + { body: 0, textValue: '0' }, + { body: false, textValue: 'false' }, + { body: 'test string', textValue: 'test string' }, + { body: '', textValue: '' }, + ]; + + it.each(scenarios)( + 'handles plain text body with $body value', + async ({ body, textValue }) => { + const mockResponse = new Response(JSON.stringify({ success: true }), { + headers: { + 'Content-Type': 'application/json', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValueOnce(mockResponse); + + const result = await client.post({ + body, + bodySerializer: null, + headers: { + 'Content-Type': 'text/plain', + }, + ky: mockKy, + url: '/test', + }); + + expect(mockKy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.any(ReadableStream), + }), + expect.any(Object), + ); + + await expect(result.request.text()).resolves.toEqual(textValue); + expect(result.request.headers.get('Content-Type')).toEqual('text/plain'); + }, + ); +}); + +describe('serialized request body handling', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + const scenarios = [ + { + body: '', + expectBodyValue: false, + expectContentHeader: false, + serializedBody: '', + textValue: '', + }, + { + body: 0, + expectBodyValue: true, + expectContentHeader: true, + serializedBody: 0, + textValue: '0', + }, + { + body: false, + expectBodyValue: true, + expectContentHeader: true, + serializedBody: false, + textValue: 'false', + }, + { + body: {}, + expectBodyValue: true, + expectContentHeader: true, + serializedBody: '{"key":"value"}', + textValue: '{"key":"value"}', + }, + ]; + + it.each(scenarios)( + 'handles $serializedBody serializedBody value', + async ({ + body, + expectBodyValue, + expectContentHeader, + serializedBody, + textValue, + }) => { + const mockResponse = new Response(JSON.stringify({ success: true }), { + headers: { + 'Content-Type': 'application/json', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValueOnce(mockResponse); + + const result = await client.post({ + body, + bodySerializer: () => serializedBody, + headers: { + 'Content-Type': 'application/json', + }, + ky: mockKy, + url: '/test', + }); + + expect(mockKy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expectBodyValue ? expect.any(ReadableStream) : null, + }), + expect.any(Object), + ); + + await expect(result.request.text()).resolves.toEqual(textValue); + expect(result.request.headers.get('Content-Type')).toEqual( + expectContentHeader ? 'application/json' : null, + ); + }, + ); +}); + +describe('request interceptor', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + const scenarios = [ + { + body: 'test string', + bodySerializer: null, + contentType: 'text/plain', + expectedSerializedValue: undefined, + expectedValue: async (request: Request) => await request.text(), + }, + { + body: { key: 'value' }, + bodySerializer: (body: object) => JSON.stringify(body), + contentType: 'application/json', + expectedSerializedValue: '{"key":"value"}', + expectedValue: async (request: Request) => await request.json(), + }, + ]; + + it.each(scenarios)( + 'exposes $contentType serialized and raw body values', + async ({ body, bodySerializer, contentType, expectedSerializedValue }) => { + const mockResponse = new Response(JSON.stringify({ success: true }), { + headers: { + 'Content-Type': 'application/json', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValueOnce(mockResponse); + + const mockRequestInterceptor = vi + .fn() + .mockImplementation( + (request: Request, options: ResolvedRequestOptions) => { + expect(options.serializedBody).toBe(expectedSerializedValue); + expect(options.body).toBe(body); + + return request; + }, + ); + + const interceptorId = client.interceptors.request.use( + mockRequestInterceptor, + ); + + await client.post({ + body, + bodySerializer, + headers: { + 'Content-Type': contentType, + }, + ky: mockKy, + url: '/test', + }); + + expect(mockRequestInterceptor).toHaveBeenCalledOnce(); + + client.interceptors.request.eject(interceptorId); + }, + ); +}); + +describe('response interceptor', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + it('allows response transformation', async () => { + const mockResponse = new Response(JSON.stringify({ success: true }), { + headers: { + 'Content-Type': 'application/json', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const mockResponseInterceptor = vi + .fn() + .mockImplementation((response: Response) => { + expect(response).toBe(mockResponse); + return response; + }); + + const interceptorId = client.interceptors.response.use( + mockResponseInterceptor, + ); + + await client.get({ + ky: mockKy, + url: '/test', + }); + + expect(mockResponseInterceptor).toHaveBeenCalledOnce(); + + client.interceptors.response.eject(interceptorId); + }); +}); + +describe('error handling', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + it('handles HTTP errors with throwOnError: false', async () => { + const errorResponse = new Response( + JSON.stringify({ message: 'Not found' }), + { + headers: { + 'Content-Type': 'application/json', + }, + status: 404, + }, + ); + + const mockKy: MockKy = vi.fn().mockRejectedValue( + new HTTPError(errorResponse, new Request('https://example.com/test'), { + method: 'GET', + } as any), + ); + + const result = await client.get({ + ky: mockKy, + throwOnError: false, + url: '/test', + }); + + expect(result.error).toEqual({ message: 'Not found' }); + expect(result.response.status).toBe(404); + }); + + it('throws HTTP errors with throwOnError: true', async () => { + const errorResponse = new Response( + JSON.stringify({ message: 'Not found' }), + { + headers: { + 'Content-Type': 'application/json', + }, + status: 404, + }, + ); + + const mockKy: MockKy = vi.fn().mockRejectedValue( + new HTTPError(errorResponse, new Request('https://example.com/test'), { + method: 'GET', + } as any), + ); + + await expect( + client.get({ + ky: mockKy, + throwOnError: true, + url: '/test', + }), + ).rejects.toEqual({ message: 'Not found' }); + }); + + it('handles text error responses', async () => { + const errorResponse = new Response('Internal Server Error', { + status: 500, + }); + + const mockKy: MockKy = vi.fn().mockRejectedValue( + new HTTPError(errorResponse, new Request('https://example.com/test'), { + method: 'GET', + } as any), + ); + + const result = await client.get({ + ky: mockKy, + throwOnError: false, + url: '/test', + }); + + expect(result.error).toBe('Internal Server Error'); + expect(result.response.status).toBe(500); + }); +}); + +describe('error interceptor', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + it('allows error transformation', async () => { + const errorResponse = new Response( + JSON.stringify({ message: 'Not found' }), + { + headers: { + 'Content-Type': 'application/json', + }, + status: 404, + }, + ); + + const mockKy: MockKy = vi.fn().mockRejectedValue( + new HTTPError(errorResponse, new Request('https://example.com/test'), { + method: 'GET', + } as any), + ); + + const mockErrorInterceptor = vi + .fn() + .mockImplementation((error: any) => ({ transformed: true, ...error })); + + const interceptorId = client.interceptors.error.use(mockErrorInterceptor); + + const result = await client.get({ + ky: mockKy, + throwOnError: false, + url: '/test', + }); + + expect(mockErrorInterceptor).toHaveBeenCalledOnce(); + expect(result.error).toEqual({ message: 'Not found', transformed: true }); + + client.interceptors.error.eject(interceptorId); + }); +}); + +describe('retry configuration', () => { + const client = createClient({ baseUrl: 'https://example.com' }); + + it('passes retry configuration to ky', async () => { + const mockResponse = new Response(JSON.stringify({ success: true }), { + headers: { + 'Content-Type': 'application/json', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + await client.get({ + ky: mockKy, + retry: { + limit: 3, + methods: ['get', 'post'], + statusCodes: [408, 429, 500], + }, + url: '/test', + }); + + expect(mockKy).toHaveBeenCalledWith( + expect.any(Request), + expect.objectContaining({ + retry: { + limit: 3, + methods: ['get', 'post'], + statusCodes: [408, 429, 500], + }, + }), + ); + }); +}); + +describe('responseStyle configuration', () => { + const client = createClient({ + baseUrl: 'https://example.com', + responseStyle: 'data', + }); + + it('returns only data when responseStyle is "data"', async () => { + const mockResponse = new Response(JSON.stringify({ result: 'success' }), { + headers: { + 'Content-Type': 'application/json', + }, + status: 200, + }); + + const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + + const result = await client.get({ + ky: mockKy, + url: '/test', + }); + + expect(result).toEqual({ result: 'success' }); + expect(result).not.toHaveProperty('response'); + expect(result).not.toHaveProperty('request'); + }); + + it('returns undefined for errors when responseStyle is "data"', async () => { + const errorResponse = new Response( + JSON.stringify({ message: 'Not found' }), + { + headers: { + 'Content-Type': 'application/json', + }, + status: 404, + }, + ); + + const mockKy: MockKy = vi.fn().mockRejectedValue( + new HTTPError(errorResponse, new Request('https://example.com/test'), { + method: 'GET', + } as any), + ); + + const result = await client.get({ + ky: mockKy, + throwOnError: false, + url: '/test', + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/utils.test.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/utils.test.ts new file mode 100644 index 000000000..4be082d47 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/utils.test.ts @@ -0,0 +1,269 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { Auth } from '../../client-core/bundle/auth'; +import type { Client } from '../bundle/types'; +import { buildUrl, getParseAs, setAuthParams } from '../bundle/utils'; + +describe('buildUrl', () => { + const scenarios: Array<{ + options: Parameters[0]; + url: string; + }> = [ + { + options: { + path: { + id: new Date('2025-01-01T00:00:00.000Z'), + }, + url: '/foo/{id}', + }, + url: '/foo/2025-01-01T00:00:00.000Z', + }, + ]; + + it.each(scenarios)('builds $url', async ({ options, url }) => { + expect(buildUrl(options)).toEqual(url); + }); +}); + +describe('getParseAs', () => { + const scenarios: Array<{ + content: Parameters[0]; + parseAs: ReturnType; + }> = [ + { + content: null, + parseAs: 'stream', + }, + { + content: 'application/json', + parseAs: 'json', + }, + { + content: 'application/ld+json', + parseAs: 'json', + }, + { + content: 'application/ld+json;charset=utf-8', + parseAs: 'json', + }, + { + content: 'application/ld+json; charset=utf-8', + parseAs: 'json', + }, + { + content: 'multipart/form-data', + parseAs: 'formData', + }, + { + content: 'application/*', + parseAs: 'blob', + }, + { + content: 'audio/*', + parseAs: 'blob', + }, + { + content: 'image/*', + parseAs: 'blob', + }, + { + content: 'video/*', + parseAs: 'blob', + }, + { + content: 'text/*', + parseAs: 'text', + }, + { + content: 'unsupported', + parseAs: undefined, + }, + ]; + + it.each(scenarios)( + 'detects $content as $parseAs', + async ({ content, parseAs }) => { + expect(getParseAs(content)).toEqual(parseAs); + }, + ); +}); + +describe('setAuthParams', () => { + it('sets bearer token in headers', async () => { + const auth = vi.fn().mockReturnValue('foo'); + const headers = new Headers(); + const query: Record = {}; + await setAuthParams({ + auth, + headers, + query, + security: [ + { + name: 'baz', + scheme: 'bearer', + type: 'http', + }, + ], + }); + expect(auth).toHaveBeenCalled(); + expect(headers.get('baz')).toBe('Bearer foo'); + expect(Object.keys(query).length).toBe(0); + }); + + it('sets access token in query', async () => { + const auth = vi.fn().mockReturnValue('foo'); + const headers = new Headers(); + const query: Record = {}; + await setAuthParams({ + auth, + headers, + query, + security: [ + { + in: 'query', + name: 'baz', + scheme: 'bearer', + type: 'http', + }, + ], + }); + expect(auth).toHaveBeenCalled(); + expect(headers.get('baz')).toBeNull(); + expect(query.baz).toBe('Bearer foo'); + }); + + it('sets Authorization header when `in` and `name` are undefined', async () => { + const auth = vi.fn().mockReturnValue('foo'); + const headers = new Headers(); + const query: Record = {}; + await setAuthParams({ + auth, + headers, + query, + security: [ + { + type: 'http', + }, + ], + }); + expect(auth).toHaveBeenCalled(); + expect(headers.get('Authorization')).toBe('foo'); + expect(query).toEqual({}); + }); + + it('sets first scheme only', async () => { + const auth = vi.fn().mockReturnValue('foo'); + const headers = new Headers(); + const query: Record = {}; + await setAuthParams({ + auth, + headers, + query, + security: [ + { + name: 'baz', + scheme: 'bearer', + type: 'http', + }, + { + in: 'query', + name: 'baz', + scheme: 'bearer', + type: 'http', + }, + ], + }); + expect(auth).toHaveBeenCalled(); + expect(headers.get('baz')).toBe('Bearer foo'); + expect(Object.keys(query).length).toBe(0); + }); + + it('sets first scheme with token', async () => { + const auth = vi.fn().mockImplementation((auth: Auth) => { + if (auth.type === 'apiKey') { + return; + } + return 'foo'; + }); + const headers = new Headers(); + const query: Record = {}; + await setAuthParams({ + auth, + headers, + query, + security: [ + { + name: 'baz', + type: 'apiKey', + }, + { + in: 'query', + name: 'baz', + scheme: 'bearer', + type: 'http', + }, + ], + }); + expect(auth).toHaveBeenCalled(); + expect(headers.get('baz')).toBeNull(); + expect(query.baz).toBe('Bearer foo'); + }); + + it('sets an API key in a cookie', async () => { + const auth = vi.fn().mockReturnValue('foo'); + const headers = new Headers(); + const query: Record = {}; + await setAuthParams({ + auth, + headers, + query, + security: [ + { + in: 'cookie', + name: 'baz', + type: 'apiKey', + }, + ], + }); + expect(auth).toHaveBeenCalled(); + expect(headers.get('Cookie')).toBe('baz=foo'); + expect(query).toEqual({}); + }); + + it('sets only one specific header', async () => { + const auth = vi.fn(({ name }: Auth) => { + if (name === 'baz') { + return 'foo'; + } + return 'buz'; + }); + const headers = new Headers(); + const query: Record = {}; + await setAuthParams({ + auth, + headers, + query, + security: [ + { + name: 'baz', + scheme: 'bearer', + type: 'http', + }, + { + name: 'fiz', + type: 'http', + }, + { + in: 'query', + name: 'baz', + scheme: 'bearer', + type: 'http', + }, + ], + }); + expect(auth).toHaveBeenCalled(); + expect(headers.get('baz')).toBe('Bearer foo'); + expect(headers.get('fiz')).toBe('buz'); + expect(Object.keys(query).length).toBe(0); + }); +}); From 484ceb15a90f50e1a872b5bdc8b865c818a4bc99 Mon Sep 17 00:00:00 2001 From: Sebastiaan Wouters Date: Tue, 11 Nov 2025 11:17:37 +0100 Subject: [PATCH 3/5] fix(client-ky): resolve TypeScript type errors Fix TypeScript compilation errors without using `as any`: - Use inline array type instead of accessing `KyOptions['retry']['methods']` to avoid type error when retry can be number | object - Add type assertion for headers as HeadersInit to satisfy Request constructor - Change body type from `BodyInit | null` to `unknown` for consistency with client-fetch and to allow testing with edge case values - Use `Partial as KyInstance` pattern in tests (same as client-axios) to properly type mock ky instances without `as any` --- .../client-ky/__tests__/client.test.ts | 77 +++++++++---------- .../@hey-api/client-ky/bundle/client.ts | 19 ++++- .../@hey-api/client-ky/bundle/types.ts | 2 +- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts index cfc8b4c2f..2da2979b3 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/__tests__/client.test.ts @@ -1,13 +1,10 @@ +import type { KyInstance } from 'ky'; import { HTTPError } from 'ky'; import { describe, expect, it, vi } from 'vitest'; import type { ResolvedRequestOptions } from '../bundle'; import { createClient } from '../bundle/client'; -type MockKy = ((...args: any[]) => any) & { - preconnect?: any; -}; - describe('buildUrl', () => { const client = createClient(); @@ -87,10 +84,10 @@ describe('zero-length body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.request({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, method: 'GET', url: '/test', }); @@ -107,10 +104,10 @@ describe('zero-length body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.request({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, method: 'GET', parseAs: 'arrayBuffer', url: '/test', @@ -129,10 +126,10 @@ describe('zero-length body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.request({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, method: 'GET', url: '/test', }); @@ -149,10 +146,10 @@ describe('zero-length body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.request({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, method: 'GET', url: '/test', }); @@ -169,10 +166,10 @@ describe('zero-length body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.request({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, method: 'GET', url: '/test', }); @@ -190,10 +187,10 @@ describe('zero-length body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.request({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, method: 'GET', parseAs: 'stream', url: '/test', @@ -211,10 +208,10 @@ describe('zero-length body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.request({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, method: 'GET', url: '/test', }); @@ -244,7 +241,7 @@ describe('unserialized request body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValueOnce(mockResponse); + const mockKy = vi.fn().mockResolvedValueOnce(mockResponse); const result = await client.post({ body, @@ -252,7 +249,7 @@ describe('unserialized request body handling', () => { headers: { 'Content-Type': 'text/plain', }, - ky: mockKy, + ky: mockKy as Partial as KyInstance, url: '/test', }); @@ -319,7 +316,7 @@ describe('serialized request body handling', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValueOnce(mockResponse); + const mockKy = vi.fn().mockResolvedValueOnce(mockResponse); const result = await client.post({ body, @@ -327,7 +324,7 @@ describe('serialized request body handling', () => { headers: { 'Content-Type': 'application/json', }, - ky: mockKy, + ky: mockKy as Partial as KyInstance, url: '/test', }); @@ -376,7 +373,7 @@ describe('request interceptor', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValueOnce(mockResponse); + const mockKy = vi.fn().mockResolvedValueOnce(mockResponse); const mockRequestInterceptor = vi .fn() @@ -399,7 +396,7 @@ describe('request interceptor', () => { headers: { 'Content-Type': contentType, }, - ky: mockKy, + ky: mockKy as Partial as KyInstance, url: '/test', }); @@ -421,7 +418,7 @@ describe('response interceptor', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const mockResponseInterceptor = vi .fn() @@ -435,7 +432,7 @@ describe('response interceptor', () => { ); await client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, url: '/test', }); @@ -459,14 +456,14 @@ describe('error handling', () => { }, ); - const mockKy: MockKy = vi.fn().mockRejectedValue( + const mockKy = vi.fn().mockRejectedValue( new HTTPError(errorResponse, new Request('https://example.com/test'), { method: 'GET', } as any), ); const result = await client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, throwOnError: false, url: '/test', }); @@ -486,7 +483,7 @@ describe('error handling', () => { }, ); - const mockKy: MockKy = vi.fn().mockRejectedValue( + const mockKy = vi.fn().mockRejectedValue( new HTTPError(errorResponse, new Request('https://example.com/test'), { method: 'GET', } as any), @@ -494,7 +491,7 @@ describe('error handling', () => { await expect( client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, throwOnError: true, url: '/test', }), @@ -506,14 +503,14 @@ describe('error handling', () => { status: 500, }); - const mockKy: MockKy = vi.fn().mockRejectedValue( + const mockKy = vi.fn().mockRejectedValue( new HTTPError(errorResponse, new Request('https://example.com/test'), { method: 'GET', } as any), ); const result = await client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, throwOnError: false, url: '/test', }); @@ -537,7 +534,7 @@ describe('error interceptor', () => { }, ); - const mockKy: MockKy = vi.fn().mockRejectedValue( + const mockKy = vi.fn().mockRejectedValue( new HTTPError(errorResponse, new Request('https://example.com/test'), { method: 'GET', } as any), @@ -550,7 +547,7 @@ describe('error interceptor', () => { const interceptorId = client.interceptors.error.use(mockErrorInterceptor); const result = await client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, throwOnError: false, url: '/test', }); @@ -573,10 +570,10 @@ describe('retry configuration', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); await client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, retry: { limit: 3, methods: ['get', 'post'], @@ -612,10 +609,10 @@ describe('responseStyle configuration', () => { status: 200, }); - const mockKy: MockKy = vi.fn().mockResolvedValue(mockResponse); + const mockKy = vi.fn().mockResolvedValue(mockResponse); const result = await client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, url: '/test', }); @@ -635,14 +632,14 @@ describe('responseStyle configuration', () => { }, ); - const mockKy: MockKy = vi.fn().mockRejectedValue( + const mockKy = vi.fn().mockRejectedValue( new HTTPError(errorResponse, new Request('https://example.com/test'), { method: 'GET', } as any), ); const result = await client.get({ - ky: mockKy, + ky: mockKy as Partial as KyInstance, throwOnError: false, url: '/test', }); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts index 2a1171703..3b19dccd8 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/client.ts @@ -9,6 +9,7 @@ import type { Config, RequestOptions, ResolvedRequestOptions, + RetryOptions, } from './types'; import type { Middleware } from './utils'; import { @@ -146,16 +147,26 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.retry && typeof opts.retry === 'object') { + const retryOpts = opts.retry as RetryOptions; kyOptions.retry = { - limit: opts.retry.limit ?? 2, - methods: opts.retry.methods as KyOptions['retry']['methods'], - statusCodes: opts.retry.statusCodes, + limit: retryOpts.limit ?? 2, + methods: retryOpts.methods as Array< + | 'get' + | 'post' + | 'put' + | 'patch' + | 'head' + | 'delete' + | 'options' + | 'trace' + >, + statusCodes: retryOpts.statusCodes, }; } let request = new Request(url, { body: kyOptions.body as BodyInit, - headers: kyOptions.headers, + headers: kyOptions.headers as HeadersInit, method: kyOptions.method, }); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts index 6aac2a257..baba033c9 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ky/bundle/types.ts @@ -116,7 +116,7 @@ export interface RequestOptions< * * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} */ - body?: BodyInit | null; + body?: unknown; path?: Record; query?: Record; /** From b249eedda4065556c41c4b9c606faa76aff05e01 Mon Sep 17 00:00:00 2001 From: Sebastiaan Wouters Date: Tue, 11 Nov 2025 11:27:25 +0100 Subject: [PATCH 4/5] feat(client-ky): add ky client example and build configuration Add example project demonstrating ky client usage: - Created @example/openapi-ts-ky package with React/Vite setup - Configured to use @hey-api/client-ky plugin - Includes generated client code from Swagger Petstore OpenAPI spec Updated build configuration: - Added 'client-ky' to tsdown.config.ts plugin list - Ensures ky bundle files are copied to dist/clients/ky during build - Enables runtime access to ky client implementation This example allows typecheck validation in CI and serves as documentation for ky client integration. --- examples/openapi-ts-ky/.gitignore | 24 + examples/openapi-ts-ky/CHANGELOG.md | 7 + examples/openapi-ts-ky/index.html | 13 + examples/openapi-ts-ky/openapi-ts.config.ts | 20 + examples/openapi-ts-ky/package.json | 40 + examples/openapi-ts-ky/postcss.config.js | 6 + examples/openapi-ts-ky/src/App.css | 3 + examples/openapi-ts-ky/src/App.tsx | 243 ++++++ .../openapi-ts-ky/src/client/client.gen.ts | 27 + .../src/client/client/client.gen.ts | 341 +++++++++ .../openapi-ts-ky/src/client/client/index.ts | 26 + .../src/client/client/types.gen.ts | 273 +++++++ .../src/client/client/utils.gen.ts | 335 +++++++++ .../openapi-ts-ky/src/client/core/auth.gen.ts | 42 ++ .../src/client/core/bodySerializer.gen.ts | 100 +++ .../src/client/core/params.gen.ts | 176 +++++ .../src/client/core/pathSerializer.gen.ts | 181 +++++ .../src/client/core/queryKeySerializer.gen.ts | 136 ++++ .../src/client/core/serverSentEvents.gen.ts | 264 +++++++ .../src/client/core/types.gen.ts | 118 +++ .../src/client/core/utils.gen.ts | 143 ++++ examples/openapi-ts-ky/src/client/index.ts | 4 + .../openapi-ts-ky/src/client/schemas.gen.ts | 188 +++++ examples/openapi-ts-ky/src/client/sdk.gen.ts | 486 ++++++++++++ .../openapi-ts-ky/src/client/types.gen.ts | 699 ++++++++++++++++++ examples/openapi-ts-ky/src/main.tsx | 26 + examples/openapi-ts-ky/src/vite-env.d.ts | 1 + examples/openapi-ts-ky/tailwind.config.js | 8 + examples/openapi-ts-ky/tsconfig.json | 25 + examples/openapi-ts-ky/tsconfig.node.json | 11 + examples/openapi-ts-ky/vite.config.ts | 7 + packages/openapi-ts/tsdown.config.ts | 1 + pnpm-lock.yaml | 449 +++++------ 33 files changed, 4203 insertions(+), 220 deletions(-) create mode 100644 examples/openapi-ts-ky/.gitignore create mode 100644 examples/openapi-ts-ky/CHANGELOG.md create mode 100644 examples/openapi-ts-ky/index.html create mode 100644 examples/openapi-ts-ky/openapi-ts.config.ts create mode 100644 examples/openapi-ts-ky/package.json create mode 100644 examples/openapi-ts-ky/postcss.config.js create mode 100644 examples/openapi-ts-ky/src/App.css create mode 100644 examples/openapi-ts-ky/src/App.tsx create mode 100644 examples/openapi-ts-ky/src/client/client.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/client/client.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/client/index.ts create mode 100644 examples/openapi-ts-ky/src/client/client/types.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/client/utils.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/auth.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/bodySerializer.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/params.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/pathSerializer.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/serverSentEvents.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/types.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/index.ts create mode 100644 examples/openapi-ts-ky/src/client/schemas.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/sdk.gen.ts create mode 100644 examples/openapi-ts-ky/src/client/types.gen.ts create mode 100644 examples/openapi-ts-ky/src/main.tsx create mode 100644 examples/openapi-ts-ky/src/vite-env.d.ts create mode 100644 examples/openapi-ts-ky/tailwind.config.js create mode 100644 examples/openapi-ts-ky/tsconfig.json create mode 100644 examples/openapi-ts-ky/tsconfig.node.json create mode 100644 examples/openapi-ts-ky/vite.config.ts diff --git a/examples/openapi-ts-ky/.gitignore b/examples/openapi-ts-ky/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/examples/openapi-ts-ky/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/openapi-ts-ky/CHANGELOG.md b/examples/openapi-ts-ky/CHANGELOG.md new file mode 100644 index 000000000..8de47d766 --- /dev/null +++ b/examples/openapi-ts-ky/CHANGELOG.md @@ -0,0 +1,7 @@ +# @example/openapi-ts-ky + +## 0.0.1 + +### Patch Changes + +- Initial release of ky client example diff --git a/examples/openapi-ts-ky/index.html b/examples/openapi-ts-ky/index.html new file mode 100644 index 000000000..d301a3a91 --- /dev/null +++ b/examples/openapi-ts-ky/index.html @@ -0,0 +1,13 @@ + + + + + + + Hey API + Fetch API Demo + + +
+ + + diff --git a/examples/openapi-ts-ky/openapi-ts.config.ts b/examples/openapi-ts-ky/openapi-ts.config.ts new file mode 100644 index 000000000..6e3b33f8b --- /dev/null +++ b/examples/openapi-ts-ky/openapi-ts.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@hey-api/openapi-ts'; + +export default defineConfig({ + input: + 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', + output: { + format: 'prettier', + lint: 'eslint', + path: './src/client', + }, + plugins: [ + '@hey-api/client-ky', + '@hey-api/schemas', + '@hey-api/sdk', + { + enums: 'javascript', + name: '@hey-api/typescript', + }, + ], +}); diff --git a/examples/openapi-ts-ky/package.json b/examples/openapi-ts-ky/package.json new file mode 100644 index 000000000..76fe42f59 --- /dev/null +++ b/examples/openapi-ts-ky/package.json @@ -0,0 +1,40 @@ +{ + "name": "@example/openapi-ts-ky", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "build": "tsc && vite build", + "dev": "vite", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "openapi-ts": "openapi-ts", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@radix-ui/react-form": "0.1.1", + "@radix-ui/react-icons": "1.3.2", + "@radix-ui/themes": "3.1.6", + "ky": "1.14.0", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@config/vite-base": "workspace:*", + "@hey-api/openapi-ts": "workspace:*", + "@types/react": "19.0.1", + "@types/react-dom": "19.0.1", + "@typescript-eslint/eslint-plugin": "8.29.1", + "@typescript-eslint/parser": "8.29.1", + "@vitejs/plugin-react": "4.4.0-beta.1", + "autoprefixer": "10.4.19", + "eslint": "9.17.0", + "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-refresh": "0.4.7", + "postcss": "8.4.41", + "prettier": "3.4.2", + "tailwindcss": "3.4.9", + "typescript": "5.8.3", + "vite": "7.1.2" + } +} diff --git a/examples/openapi-ts-ky/postcss.config.js b/examples/openapi-ts-ky/postcss.config.js new file mode 100644 index 000000000..9eef821c4 --- /dev/null +++ b/examples/openapi-ts-ky/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + autoprefixer: {}, + tailwindcss: {}, + }, +}; diff --git a/examples/openapi-ts-ky/src/App.css b/examples/openapi-ts-ky/src/App.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/examples/openapi-ts-ky/src/App.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/examples/openapi-ts-ky/src/App.tsx b/examples/openapi-ts-ky/src/App.tsx new file mode 100644 index 000000000..915ebb592 --- /dev/null +++ b/examples/openapi-ts-ky/src/App.tsx @@ -0,0 +1,243 @@ +import './App.css'; + +import * as Form from '@radix-ui/react-form'; +import { DownloadIcon, PlusIcon, ReloadIcon } from '@radix-ui/react-icons'; +import { + Avatar, + Box, + Button, + Card, + Container, + Flex, + Heading, + Section, + Text, + TextField, +} from '@radix-ui/themes'; +import { useState } from 'react'; + +import { createClient } from './client/client'; +import { PetSchema } from './client/schemas.gen'; +import { addPet, getPetById, updatePet } from './client/sdk.gen'; +import type { Pet } from './client/types.gen'; + +const localClient = createClient({ + // set default base url for requests made by this client + baseUrl: 'https://petstore3.swagger.io/api/v3', + /** + * Set default headers only for requests made by this client. This is to + * demonstrate local clients and their configuration taking precedence over + * internal service client. + */ + headers: { + Authorization: 'Bearer ', + }, +}); + +localClient.interceptors.request.use((request, options) => { + // Middleware is great for adding authorization tokens to requests made to + // protected paths. Headers are set randomly here to allow surfacing the + // default headers, too. + if ( + options.url === '/pet/{petId}' && + options.method === 'GET' && + Math.random() < 0.5 + ) { + request.headers.set('Authorization', 'Bearer '); + } + return request; +}); + +localClient.interceptors.error.use((error) => { + console.log(error); + return error; +}); + +function App() { + const [pet, setPet] = useState(); + const [isRequiredNameError, setIsRequiredNameError] = useState(false); + + const onAddPet = async (formData: FormData) => { + // simple form field validation to demonstrate using schemas + if (PetSchema.required.includes('name') && !formData.get('name')) { + setIsRequiredNameError(true); + return; + } + + const { data, error } = await addPet({ + body: { + category: { + id: 0, + name: formData.get('category') as string, + }, + id: 0, + name: formData.get('name') as string, + photoUrls: ['string'], + status: 'available', + tags: [ + { + id: 0, + name: 'string', + }, + ], + }, + }); + if (error) { + console.log(error); + return; + } + setPet(data!); + setIsRequiredNameError(false); + }; + + const onGetPetById = async () => { + const { data, error } = await getPetById({ + client: localClient, + path: { + // random id 1-10 + petId: Math.floor(Math.random() * (10 - 1 + 1) + 1), + }, + }); + if (error) { + console.log(error); + return; + } + setPet(data!); + }; + + const onUpdatePet = async () => { + const { data, error } = await updatePet({ + body: { + category: { + id: 0, + name: 'Cats', + }, + id: 2, + name: 'Updated Kitty', + photoUrls: ['string'], + status: 'available', + tags: [ + { + id: 0, + name: 'string', + }, + ], + }, + // setting headers per request + headers: { + Authorization: 'Bearer ', + }, + }); + if (error) { + console.log(error); + return; + } + setPet(data!); + }; + + return ( + + +
+ + + Hey API logo + + @hey-api/openapi-ts 🤝 Fetch API + +
+ + + + + + + + Name: {pet?.name ?? 'N/A'} + + + Category: {pet?.category?.name ?? 'N/A'} + + + + + + + +
+ + { + event.preventDefault(); + onAddPet(new FormData(event.currentTarget)); + }} + > + +
+ + Name + + {isRequiredNameError && ( + + Please enter a name + + )} +
+ + + +
+ +
+ + Category + + + Please enter a category + +
+ + + +
+ + + + + + +
+
+
+ + + ); +} + +export default App; diff --git a/examples/openapi-ts-ky/src/client/client.gen.ts b/examples/openapi-ts-ky/src/client/client.gen.ts new file mode 100644 index 000000000..069f4daba --- /dev/null +++ b/examples/openapi-ts-ky/src/client/client.gen.ts @@ -0,0 +1,27 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { + type ClientOptions, + type Config, + createClient, + createConfig, +} from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export const client = createClient( + createConfig({ + baseUrl: 'https://petstore3.swagger.io/api/v3', + }), +); diff --git a/examples/openapi-ts-ky/src/client/client/client.gen.ts b/examples/openapi-ts-ky/src/client/client/client.gen.ts new file mode 100644 index 000000000..ea60987ff --- /dev/null +++ b/examples/openapi-ts-ky/src/client/client/client.gen.ts @@ -0,0 +1,341 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { HTTPError, Options as KyOptions } from 'ky'; +import ky from 'ky'; + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, + RetryOptions, +} from './types.gen'; +import type { Middleware } from './utils.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + headers: mergeHeaders(_config.headers, options.headers), + ky: options.ky ?? _config.ky ?? ky, + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const parseErrorResponse = async ( + response: Response, + request: Request, + opts: ResolvedRequestOptions, + interceptorsMiddleware: Middleware< + Request, + Response, + unknown, + ResolvedRequestOptions + >, + ) => { + const result = { + request, + response, + }; + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + jsonError = undefined; + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptorsMiddleware.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + + const kyInstance = opts.ky!; + + const validBody = getValidRequestBody(opts); + + const kyOptions: KyOptions = { + body: validBody as BodyInit, + cache: opts.cache, + credentials: opts.credentials, + headers: opts.headers, + integrity: opts.integrity, + keepalive: opts.keepalive, + method: opts.method as KyOptions['method'], + mode: opts.mode, + redirect: 'follow', + referrer: opts.referrer, + referrerPolicy: opts.referrerPolicy, + signal: opts.signal, + throwHttpErrors: opts.throwOnError ?? false, + timeout: opts.timeout, + ...(opts.kyOptions || {}), + }; + + if (opts.retry && typeof opts.retry === 'object') { + const retryOpts = opts.retry as RetryOptions; + kyOptions.retry = { + limit: retryOpts.limit ?? 2, + methods: retryOpts.methods as Array< + | 'get' + | 'post' + | 'put' + | 'patch' + | 'head' + | 'delete' + | 'options' + | 'trace' + >, + statusCodes: retryOpts.statusCodes, + }; + } + + let request = new Request(url, { + body: kyOptions.body as BodyInit, + headers: kyOptions.headers as HeadersInit, + method: kyOptions.method, + }); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + let response: Response; + + try { + response = await kyInstance(request, kyOptions); + } catch (error) { + if (error && typeof error === 'object' && 'response' in error) { + const httpError = error as HTTPError; + response = httpError.response; + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + return parseErrorResponse(response, request, opts, interceptors); + } + + throw error; + } + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + return parseErrorResponse(response, request, opts, interceptors); + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + fetch: globalThis.fetch, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-ky/src/client/client/index.ts b/examples/openapi-ts-ky/src/client/client/index.ts new file mode 100644 index 000000000..c2814a610 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/client/index.ts @@ -0,0 +1,26 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + RetryOptions, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-ky/src/client/client/types.gen.ts b/examples/openapi-ts-ky/src/client/client/types.gen.ts new file mode 100644 index 000000000..62d18352c --- /dev/null +++ b/examples/openapi-ts-ky/src/client/client/types.gen.ts @@ -0,0 +1,273 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface RetryOptions { + /** + * Maximum number of retry attempts + * + * @default 2 + */ + limit?: number; + /** + * HTTP methods to retry + * + * @default ['get', 'put', 'head', 'delete', 'options', 'trace'] + */ + methods?: Array< + 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace' + >; + /** + * HTTP status codes to retry + * + * @default [408, 413, 429, 500, 502, 503, 504] + */ + statusCodes?: number[]; +} + +export interface Config + extends Omit< + import('ky').Options, + 'body' | 'headers' | 'method' | 'prefixUrl' | 'retry' | 'throwHttpErrors' + >, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Ky instance to use. You can use this option to provide a custom + * ky instance. + */ + ky?: typeof import('ky').default; + /** + * Additional ky-specific options that will be passed directly to ky. + * This allows you to use any ky option not explicitly exposed in the config. + */ + kyOptions?: Omit; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Retry configuration + */ + retry?: RetryOptions; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; + /** + * Request timeout in milliseconds + * + * @default 10000 + */ + timeout?: number; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/examples/openapi-ts-ky/src/client/client/utils.gen.ts b/examples/openapi-ts-ky/src/client/client/utils.gen.ts new file mode 100644 index 000000000..3582a06db --- /dev/null +++ b/examples/openapi-ts-ky/src/client/client/utils.gen.ts @@ -0,0 +1,335 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + throwOnError: false, + timeout: 10000, + ...override, +}); diff --git a/examples/openapi-ts-ky/src/client/core/auth.gen.ts b/examples/openapi-ts-ky/src/client/core/auth.gen.ts new file mode 100644 index 000000000..f8a73266f --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/examples/openapi-ts-ky/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-ky/src/client/core/bodySerializer.gen.ts new file mode 100644 index 000000000..552b50f7c --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,100 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/examples/openapi-ts-ky/src/client/core/params.gen.ts b/examples/openapi-ts-ky/src/client/core/params.gen.ts new file mode 100644 index 000000000..602715c46 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/params.gen.ts @@ -0,0 +1,176 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/examples/openapi-ts-ky/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-ky/src/client/core/pathSerializer.gen.ts new file mode 100644 index 000000000..8d9993104 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/examples/openapi-ts-ky/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-ky/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-ky/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-ky/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-ky/src/client/core/types.gen.ts b/examples/openapi-ts-ky/src/client/core/types.gen.ts new file mode 100644 index 000000000..643c070c9 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-ky/src/client/core/utils.gen.ts b/examples/openapi-ts-ky/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-ky/src/client/index.ts b/examples/openapi-ts-ky/src/client/index.ts new file mode 100644 index 000000000..57ed02bf5 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export * from './sdk.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-ky/src/client/schemas.gen.ts b/examples/openapi-ts-ky/src/client/schemas.gen.ts new file mode 100644 index 000000000..646632e83 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/schemas.gen.ts @@ -0,0 +1,188 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export const OrderSchema = { + properties: { + complete: { + type: 'boolean', + }, + id: { + example: 10, + format: 'int64', + type: 'integer', + }, + petId: { + example: 198772, + format: 'int64', + type: 'integer', + }, + quantity: { + example: 7, + format: 'int32', + type: 'integer', + }, + shipDate: { + format: 'date-time', + type: 'string', + }, + status: { + description: 'Order Status', + enum: ['placed', 'approved', 'delivered'], + example: 'approved', + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Order', + xml: { + name: 'order', + }, +} as const; + +export const CategorySchema = { + properties: { + id: { + example: 1, + format: 'int64', + type: 'integer', + }, + name: { + example: 'Dogs', + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Category', + xml: { + name: 'category', + }, +} as const; + +export const UserSchema = { + properties: { + email: { + example: 'john@email.com', + type: 'string', + }, + firstName: { + example: 'John', + type: 'string', + }, + id: { + example: 10, + format: 'int64', + type: 'integer', + }, + lastName: { + example: 'James', + type: 'string', + }, + password: { + example: '12345', + type: 'string', + }, + phone: { + example: '12345', + type: 'string', + }, + userStatus: { + description: 'User Status', + example: 1, + format: 'int32', + type: 'integer', + }, + username: { + example: 'theUser', + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.User', + xml: { + name: 'user', + }, +} as const; + +export const TagSchema = { + properties: { + id: { + format: 'int64', + type: 'integer', + }, + name: { + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Tag', + xml: { + name: 'tag', + }, +} as const; + +export const PetSchema = { + properties: { + category: { + $ref: '#/components/schemas/Category', + }, + id: { + example: 10, + format: 'int64', + type: 'integer', + }, + name: { + example: 'doggie', + type: 'string', + }, + photoUrls: { + items: { + type: 'string', + xml: { + name: 'photoUrl', + }, + }, + type: 'array', + xml: { + wrapped: true, + }, + }, + status: { + description: 'pet status in the store', + enum: ['available', 'pending', 'sold'], + type: 'string', + }, + tags: { + items: { + $ref: '#/components/schemas/Tag', + }, + type: 'array', + xml: { + wrapped: true, + }, + }, + }, + required: ['name', 'photoUrls'], + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Pet', + xml: { + name: 'pet', + }, +} as const; + +export const ApiResponseSchema = { + properties: { + code: { + format: 'int32', + type: 'integer', + }, + message: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + type: 'object', + xml: { + name: '##default', + }, +} as const; diff --git a/examples/openapi-ts-ky/src/client/sdk.gen.ts b/examples/openapi-ts-ky/src/client/sdk.gen.ts new file mode 100644 index 000000000..f424fe675 --- /dev/null +++ b/examples/openapi-ts-ky/src/client/sdk.gen.ts @@ -0,0 +1,486 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { + AddPetData, + AddPetErrors, + AddPetResponses, + CreateUserData, + CreateUserErrors, + CreateUserResponses, + CreateUsersWithListInputData, + CreateUsersWithListInputErrors, + CreateUsersWithListInputResponses, + DeleteOrderData, + DeleteOrderErrors, + DeleteOrderResponses, + DeletePetData, + DeletePetErrors, + DeletePetResponses, + DeleteUserData, + DeleteUserErrors, + DeleteUserResponses, + FindPetsByStatusData, + FindPetsByStatusErrors, + FindPetsByStatusResponses, + FindPetsByTagsData, + FindPetsByTagsErrors, + FindPetsByTagsResponses, + GetInventoryData, + GetInventoryErrors, + GetInventoryResponses, + GetOrderByIdData, + GetOrderByIdErrors, + GetOrderByIdResponses, + GetPetByIdData, + GetPetByIdErrors, + GetPetByIdResponses, + GetUserByNameData, + GetUserByNameErrors, + GetUserByNameResponses, + LoginUserData, + LoginUserErrors, + LoginUserResponses, + LogoutUserData, + LogoutUserErrors, + LogoutUserResponses, + PlaceOrderData, + PlaceOrderErrors, + PlaceOrderResponses, + UpdatePetData, + UpdatePetErrors, + UpdatePetResponses, + UpdatePetWithFormData, + UpdatePetWithFormErrors, + UpdatePetWithFormResponses, + UpdateUserData, + UpdateUserErrors, + UpdateUserResponses, + UploadFileData, + UploadFileErrors, + UploadFileResponses, +} from './types.gen'; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, +> = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Add a new pet to the store. + * + * Add a new pet to the store. + */ +export const addPet = ( + options: Options, +) => + (options.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * Update an existing pet. + * + * Update an existing pet by Id. + */ +export const updatePet = ( + options: Options, +) => + (options.client ?? client).put< + UpdatePetResponses, + UpdatePetErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * Finds Pets by status. + * + * Multiple status values can be provided with comma separated strings. + */ +export const findPetsByStatus = ( + options: Options, +) => + (options.client ?? client).get< + FindPetsByStatusResponses, + FindPetsByStatusErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/findByStatus', + ...options, + }); + +/** + * Finds Pets by tags. + * + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + */ +export const findPetsByTags = ( + options: Options, +) => + (options.client ?? client).get< + FindPetsByTagsResponses, + FindPetsByTagsErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/findByTags', + ...options, + }); + +/** + * Deletes a pet. + * + * Delete a pet. + */ +export const deletePet = ( + options: Options, +) => + (options.client ?? client).delete< + DeletePetResponses, + DeletePetErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}', + ...options, + }); + +/** + * Find pet by ID. + * + * Returns a single pet. + */ +export const getPetById = ( + options: Options, +) => + (options.client ?? client).get< + GetPetByIdResponses, + GetPetByIdErrors, + ThrowOnError + >({ + security: [ + { + name: 'api_key', + type: 'apiKey', + }, + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}', + ...options, + }); + +/** + * Updates a pet in the store with form data. + * + * Updates a pet resource based on the form data. + */ +export const updatePetWithForm = ( + options: Options, +) => + (options.client ?? client).post< + UpdatePetWithFormResponses, + UpdatePetWithFormErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}', + ...options, + }); + +/** + * Uploads an image. + * + * Upload image of the pet. + */ +export const uploadFile = ( + options: Options, +) => + (options.client ?? client).post< + UploadFileResponses, + UploadFileErrors, + ThrowOnError + >({ + bodySerializer: null, + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}/uploadImage', + ...options, + headers: { + 'Content-Type': 'application/octet-stream', + ...options.headers, + }, + }); + +/** + * Returns pet inventories by status. + * + * Returns a map of status codes to quantities. + */ +export const getInventory = ( + options?: Options, +) => + (options?.client ?? client).get< + GetInventoryResponses, + GetInventoryErrors, + ThrowOnError + >({ + security: [ + { + name: 'api_key', + type: 'apiKey', + }, + ], + url: '/store/inventory', + ...options, + }); + +/** + * Place an order for a pet. + * + * Place a new order in the store. + */ +export const placeOrder = ( + options?: Options, +) => + (options?.client ?? client).post< + PlaceOrderResponses, + PlaceOrderErrors, + ThrowOnError + >({ + url: '/store/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + +/** + * Delete purchase order by identifier. + * + * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. + */ +export const deleteOrder = ( + options: Options, +) => + (options.client ?? client).delete< + DeleteOrderResponses, + DeleteOrderErrors, + ThrowOnError + >({ + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Find purchase order by ID. + * + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + */ +export const getOrderById = ( + options: Options, +) => + (options.client ?? client).get< + GetOrderByIdResponses, + GetOrderByIdErrors, + ThrowOnError + >({ + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Create user. + * + * This can only be done by the logged in user. + */ +export const createUser = ( + options?: Options, +) => + (options?.client ?? client).post< + CreateUserResponses, + CreateUserErrors, + ThrowOnError + >({ + url: '/user', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + +/** + * Creates list of users with given input array. + * + * Creates list of users with given input array. + */ +export const createUsersWithListInput = ( + options?: Options, +) => + (options?.client ?? client).post< + CreateUsersWithListInputResponses, + CreateUsersWithListInputErrors, + ThrowOnError + >({ + url: '/user/createWithList', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + +/** + * Logs user into the system. + * + * Log into the system. + */ +export const loginUser = ( + options?: Options, +) => + (options?.client ?? client).get< + LoginUserResponses, + LoginUserErrors, + ThrowOnError + >({ + url: '/user/login', + ...options, + }); + +/** + * Logs out current logged in user session. + * + * Log user out of the system. + */ +export const logoutUser = ( + options?: Options, +) => + (options?.client ?? client).get< + LogoutUserResponses, + LogoutUserErrors, + ThrowOnError + >({ + url: '/user/logout', + ...options, + }); + +/** + * Delete user resource. + * + * This can only be done by the logged in user. + */ +export const deleteUser = ( + options: Options, +) => + (options.client ?? client).delete< + DeleteUserResponses, + DeleteUserErrors, + ThrowOnError + >({ + url: '/user/{username}', + ...options, + }); + +/** + * Get user by user name. + * + * Get user detail based on username. + */ +export const getUserByName = ( + options: Options, +) => + (options.client ?? client).get< + GetUserByNameResponses, + GetUserByNameErrors, + ThrowOnError + >({ + url: '/user/{username}', + ...options, + }); + +/** + * Update user resource. + * + * This can only be done by the logged in user. + */ +export const updateUser = ( + options: Options, +) => + (options.client ?? client).put< + UpdateUserResponses, + UpdateUserErrors, + ThrowOnError + >({ + url: '/user/{username}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); diff --git a/examples/openapi-ts-ky/src/client/types.gen.ts b/examples/openapi-ts-ky/src/client/types.gen.ts new file mode 100644 index 000000000..a2e6be0fa --- /dev/null +++ b/examples/openapi-ts-ky/src/client/types.gen.ts @@ -0,0 +1,699 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + +export type Order = { + complete?: boolean; + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + /** + * Order Status + */ + status?: 'placed' | 'approved' | 'delivered'; +}; + +export type Category = { + id?: number; + name?: string; +}; + +export type User = { + email?: string; + firstName?: string; + id?: number; + lastName?: string; + password?: string; + phone?: string; + /** + * User Status + */ + userStatus?: number; + username?: string; +}; + +export type Tag = { + id?: number; + name?: string; +}; + +export type Pet = { + category?: Category; + id?: number; + name: string; + photoUrls: Array; + /** + * pet status in the store + */ + status?: 'available' | 'pending' | 'sold'; + tags?: Array; +}; + +export type ApiResponse = { + code?: number; + message?: string; + type?: string; +}; + +export type Pet2 = Pet; + +/** + * List of user object + */ +export type UserArray = Array; + +export type AddPetData = { + /** + * Create a new pet in the store + */ + body: Pet; + path?: never; + query?: never; + url: '/pet'; +}; + +export type AddPetErrors = { + /** + * Invalid input + */ + 400: unknown; + /** + * Validation exception + */ + 422: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type AddPetResponses = { + /** + * Successful operation + */ + 200: Pet; +}; + +export type AddPetResponse = AddPetResponses[keyof AddPetResponses]; + +export type UpdatePetData = { + /** + * Update an existent pet in the store + */ + body: Pet; + path?: never; + query?: never; + url: '/pet'; +}; + +export type UpdatePetErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Pet not found + */ + 404: unknown; + /** + * Validation exception + */ + 422: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UpdatePetResponses = { + /** + * Successful operation + */ + 200: Pet; +}; + +export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; + +export type FindPetsByStatusData = { + body?: never; + path?: never; + query: { + /** + * Status values that need to be considered for filter + */ + status: 'available' | 'pending' | 'sold'; + }; + url: '/pet/findByStatus'; +}; + +export type FindPetsByStatusErrors = { + /** + * Invalid status value + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type FindPetsByStatusResponses = { + /** + * successful operation + */ + 200: Array; +}; + +export type FindPetsByStatusResponse = + FindPetsByStatusResponses[keyof FindPetsByStatusResponses]; + +export type FindPetsByTagsData = { + body?: never; + path?: never; + query: { + /** + * Tags to filter by + */ + tags: Array; + }; + url: '/pet/findByTags'; +}; + +export type FindPetsByTagsErrors = { + /** + * Invalid tag value + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type FindPetsByTagsResponses = { + /** + * successful operation + */ + 200: Array; +}; + +export type FindPetsByTagsResponse = + FindPetsByTagsResponses[keyof FindPetsByTagsResponses]; + +export type DeletePetData = { + body?: never; + headers?: { + api_key?: string; + }; + path: { + /** + * Pet id to delete + */ + petId: number; + }; + query?: never; + url: '/pet/{petId}'; +}; + +export type DeletePetErrors = { + /** + * Invalid pet value + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type DeletePetResponses = { + /** + * Pet deleted + */ + 200: unknown; +}; + +export type GetPetByIdData = { + body?: never; + path: { + /** + * ID of pet to return + */ + petId: number; + }; + query?: never; + url: '/pet/{petId}'; +}; + +export type GetPetByIdErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Pet not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetPetByIdResponses = { + /** + * successful operation + */ + 200: Pet; +}; + +export type GetPetByIdResponse = GetPetByIdResponses[keyof GetPetByIdResponses]; + +export type UpdatePetWithFormData = { + body?: never; + path: { + /** + * ID of pet that needs to be updated + */ + petId: number; + }; + query?: { + /** + * Name of pet that needs to be updated + */ + name?: string; + /** + * Status of pet that needs to be updated + */ + status?: string; + }; + url: '/pet/{petId}'; +}; + +export type UpdatePetWithFormErrors = { + /** + * Invalid input + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UpdatePetWithFormResponses = { + /** + * successful operation + */ + 200: Pet; +}; + +export type UpdatePetWithFormResponse = + UpdatePetWithFormResponses[keyof UpdatePetWithFormResponses]; + +export type UploadFileData = { + body?: Blob | File; + path: { + /** + * ID of pet to update + */ + petId: number; + }; + query?: { + /** + * Additional Metadata + */ + additionalMetadata?: string; + }; + url: '/pet/{petId}/uploadImage'; +}; + +export type UploadFileErrors = { + /** + * No file uploaded + */ + 400: unknown; + /** + * Pet not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UploadFileResponses = { + /** + * successful operation + */ + 200: ApiResponse; +}; + +export type UploadFileResponse = UploadFileResponses[keyof UploadFileResponses]; + +export type GetInventoryData = { + body?: never; + path?: never; + query?: never; + url: '/store/inventory'; +}; + +export type GetInventoryErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetInventoryResponses = { + /** + * successful operation + */ + 200: { + [key: string]: number; + }; +}; + +export type GetInventoryResponse = + GetInventoryResponses[keyof GetInventoryResponses]; + +export type PlaceOrderData = { + body?: Order; + path?: never; + query?: never; + url: '/store/order'; +}; + +export type PlaceOrderErrors = { + /** + * Invalid input + */ + 400: unknown; + /** + * Validation exception + */ + 422: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type PlaceOrderResponses = { + /** + * successful operation + */ + 200: Order; +}; + +export type PlaceOrderResponse = PlaceOrderResponses[keyof PlaceOrderResponses]; + +export type DeleteOrderData = { + body?: never; + path: { + /** + * ID of the order that needs to be deleted + */ + orderId: number; + }; + query?: never; + url: '/store/order/{orderId}'; +}; + +export type DeleteOrderErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Order not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type DeleteOrderResponses = { + /** + * order deleted + */ + 200: unknown; +}; + +export type GetOrderByIdData = { + body?: never; + path: { + /** + * ID of order that needs to be fetched + */ + orderId: number; + }; + query?: never; + url: '/store/order/{orderId}'; +}; + +export type GetOrderByIdErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Order not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetOrderByIdResponses = { + /** + * successful operation + */ + 200: Order; +}; + +export type GetOrderByIdResponse = + GetOrderByIdResponses[keyof GetOrderByIdResponses]; + +export type CreateUserData = { + /** + * Created user object + */ + body?: User; + path?: never; + query?: never; + url: '/user'; +}; + +export type CreateUserErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type CreateUserResponses = { + /** + * successful operation + */ + 200: User; +}; + +export type CreateUserResponse = CreateUserResponses[keyof CreateUserResponses]; + +export type CreateUsersWithListInputData = { + body?: Array; + path?: never; + query?: never; + url: '/user/createWithList'; +}; + +export type CreateUsersWithListInputErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type CreateUsersWithListInputResponses = { + /** + * Successful operation + */ + 200: User; +}; + +export type CreateUsersWithListInputResponse = + CreateUsersWithListInputResponses[keyof CreateUsersWithListInputResponses]; + +export type LoginUserData = { + body?: never; + path?: never; + query?: { + /** + * The password for login in clear text + */ + password?: string; + /** + * The user name for login + */ + username?: string; + }; + url: '/user/login'; +}; + +export type LoginUserErrors = { + /** + * Invalid username/password supplied + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type LoginUserResponses = { + /** + * successful operation + */ + 200: string; +}; + +export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; + +export type LogoutUserData = { + body?: never; + path?: never; + query?: never; + url: '/user/logout'; +}; + +export type LogoutUserErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type LogoutUserResponses = { + /** + * successful operation + */ + 200: unknown; +}; + +export type DeleteUserData = { + body?: never; + path: { + /** + * The name that needs to be deleted + */ + username: string; + }; + query?: never; + url: '/user/{username}'; +}; + +export type DeleteUserErrors = { + /** + * Invalid username supplied + */ + 400: unknown; + /** + * User not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type DeleteUserResponses = { + /** + * User deleted + */ + 200: unknown; +}; + +export type GetUserByNameData = { + body?: never; + path: { + /** + * The name that needs to be fetched. Use user1 for testing + */ + username: string; + }; + query?: never; + url: '/user/{username}'; +}; + +export type GetUserByNameErrors = { + /** + * Invalid username supplied + */ + 400: unknown; + /** + * User not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetUserByNameResponses = { + /** + * successful operation + */ + 200: User; +}; + +export type GetUserByNameResponse = + GetUserByNameResponses[keyof GetUserByNameResponses]; + +export type UpdateUserData = { + /** + * Update an existent user in the store + */ + body?: User; + path: { + /** + * name that need to be deleted + */ + username: string; + }; + query?: never; + url: '/user/{username}'; +}; + +export type UpdateUserErrors = { + /** + * bad request + */ + 400: unknown; + /** + * user not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UpdateUserResponses = { + /** + * successful operation + */ + 200: unknown; +}; diff --git a/examples/openapi-ts-ky/src/main.tsx b/examples/openapi-ts-ky/src/main.tsx new file mode 100644 index 000000000..201f55e26 --- /dev/null +++ b/examples/openapi-ts-ky/src/main.tsx @@ -0,0 +1,26 @@ +import '@radix-ui/themes/styles.css'; + +import { Theme } from '@radix-ui/themes'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import App from './App.tsx'; +import { client } from './client/client.gen'; + +// configure internal service client +client.setConfig({ + // set default base url for requests + baseUrl: 'https://petstore3.swagger.io/api/v3', + // set default headers for requests + headers: { + Authorization: 'Bearer ', + }, +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/examples/openapi-ts-ky/src/vite-env.d.ts b/examples/openapi-ts-ky/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/examples/openapi-ts-ky/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/openapi-ts-ky/tailwind.config.js b/examples/openapi-ts-ky/tailwind.config.js new file mode 100644 index 000000000..0284c5614 --- /dev/null +++ b/examples/openapi-ts-ky/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{html,js,ts,jsx,tsx}'], + plugins: [], + theme: { + extend: {}, + }, +}; diff --git a/examples/openapi-ts-ky/tsconfig.json b/examples/openapi-ts-ky/tsconfig.json new file mode 100644 index 000000000..04664de39 --- /dev/null +++ b/examples/openapi-ts-ky/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/openapi-ts-ky/tsconfig.node.json b/examples/openapi-ts-ky/tsconfig.node.json new file mode 100644 index 000000000..97ede7ee6 --- /dev/null +++ b/examples/openapi-ts-ky/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/openapi-ts-ky/vite.config.ts b/examples/openapi-ts-ky/vite.config.ts new file mode 100644 index 000000000..8cbf22f1b --- /dev/null +++ b/examples/openapi-ts-ky/vite.config.ts @@ -0,0 +1,7 @@ +import { createViteConfig } from '@config/vite-base'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default createViteConfig({ + plugins: [react()], +}); diff --git a/packages/openapi-ts/tsdown.config.ts b/packages/openapi-ts/tsdown.config.ts index 7fe486c52..e769c9dad 100644 --- a/packages/openapi-ts/tsdown.config.ts +++ b/packages/openapi-ts/tsdown.config.ts @@ -39,6 +39,7 @@ export default defineConfig((options) => ({ 'client-axios', 'client-core', 'client-fetch', + 'client-ky', 'client-next', 'client-nuxt', 'client-ofetch', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7030899b9..3f68d8aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -530,6 +530,76 @@ importers: specifier: 7.1.2 version: 7.1.2(@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) + examples/openapi-ts-ky: + dependencies: + '@radix-ui/react-form': + specifier: 0.1.1 + version: 0.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-icons': + specifier: 1.3.2 + version: 1.3.2(react@19.0.0) + '@radix-ui/themes': + specifier: 3.1.6 + version: 3.1.6(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + ky: + specifier: 1.14.0 + version: 1.14.0 + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) + devDependencies: + '@config/vite-base': + specifier: workspace:* + version: link:../../packages/config-vite-base + '@hey-api/openapi-ts': + specifier: workspace:* + version: link:../../packages/openapi-ts + '@types/react': + specifier: 19.0.1 + version: 19.0.1 + '@types/react-dom': + specifier: 19.0.1 + version: 19.0.1 + '@typescript-eslint/eslint-plugin': + specifier: 8.29.1 + version: 8.29.1(@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))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: 8.29.1 + version: 8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3) + '@vitejs/plugin-react': + specifier: 4.4.0-beta.1 + version: 4.4.0-beta.1(vite@7.1.2(@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)) + autoprefixer: + specifier: 10.4.19 + version: 10.4.19(postcss@8.4.41) + eslint: + specifier: 9.17.0 + version: 9.17.0(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: 5.2.0 + version: 5.2.0(eslint@9.17.0(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: 0.4.7 + version: 0.4.7(eslint@9.17.0(jiti@2.6.1)) + postcss: + specifier: 8.4.41 + version: 8.4.41 + prettier: + specifier: 3.4.2 + version: 3.4.2 + tailwindcss: + specifier: 3.4.9 + version: 3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)) + typescript: + specifier: 5.8.3 + version: 5.8.3 + vite: + specifier: 7.1.2 + version: 7.1.2(@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) + examples/openapi-ts-next: dependencies: next: @@ -1308,7 +1378,7 @@ importers: version: 1.14.0 nuxt: specifier: 3.14.1592 - 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)) + 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@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) ofetch: specifier: 1.4.1 version: 1.4.1 @@ -2145,10 +2215,6 @@ packages: resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} @@ -12732,10 +12798,6 @@ packages: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -13538,46 +13600,6 @@ packages: yaml: optional: true - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14241,7 +14263,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0)) '@angular-devkit/core': 19.2.0(chokidar@4.0.3) '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/platform-server@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.0(@angular/animations@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))))(@angular/ssr@19.2.15(5c03da8199d2fcdf9ff93b70f9349edd))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.1) '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3) @@ -14259,7 +14281,7 @@ snapshots: '@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)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14279,7 +14301,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14292,8 +14314,8 @@ snapshots: tslib: 2.8.1 typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0) + webpack-dev-server: 5.2.0(webpack@5.98.0) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) optionalDependencies: @@ -14329,7 +14351,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0)) '@angular-devkit/core': 19.2.0(chokidar@4.0.3) '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/platform-server@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.0(@angular/animations@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))))(@angular/ssr@19.2.15(5c03da8199d2fcdf9ff93b70f9349edd))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.1) '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3) @@ -14347,7 +14369,7 @@ snapshots: '@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)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14367,7 +14389,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14380,8 +14402,8 @@ snapshots: tslib: 2.8.1 typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0) + webpack-dev-server: 5.2.0(webpack@5.98.0) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) optionalDependencies: @@ -14455,7 +14477,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14468,7 +14490,7 @@ snapshots: tslib: 2.8.1 typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.4) - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0) webpack-dev-server: 5.2.2(webpack@5.98.0) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) @@ -14556,7 +14578,7 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 webpack: 5.98.0(esbuild@0.25.4) - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0) webpack-dev-server: 5.2.2(webpack@5.98.0) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) @@ -14588,12 +14610,12 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0))': + '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0))': dependencies: '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) rxjs: 7.8.1 webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-server: 5.2.0(webpack@5.98.0) transitivePeerDependencies: - chokidar @@ -15230,7 +15252,7 @@ snapshots: '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -15250,7 +15272,7 @@ snapshots: '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -15261,16 +15283,16 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) '@babel/helpers': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -15293,14 +15315,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.2 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 - jsesc: 3.1.0 - '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 @@ -15315,7 +15329,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -15412,7 +15426,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -15420,7 +15434,7 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -15429,7 +15443,7 @@ snapshots: dependencies: '@babel/core': 7.26.9 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -15438,7 +15452,7 @@ snapshots: dependencies: '@babel/core': 7.28.3 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -15497,7 +15511,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -15524,11 +15538,11 @@ snapshots: '@babel/helpers@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/parser@7.28.3': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/parser@7.28.5': dependencies: @@ -16537,14 +16551,14 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 esutils: 2.0.3 '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 esutils: 2.0.3 '@babel/runtime@7.26.10': @@ -16563,16 +16577,16 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.5 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/traverse@7.28.3': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -16580,7 +16594,7 @@ snapshots: '@babel/types@7.28.0': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/types@7.28.2': dependencies: @@ -17328,7 +17342,7 @@ snapshots: '@eslint/config-array@0.19.2': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -17344,7 +17358,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -18110,32 +18124,32 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.2(@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))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.2.0 - vite: 7.1.2(@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) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) transitivePeerDependencies: - magicast - supports-color - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(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))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.2(@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))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.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) + vite: 7.1.2(@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) transitivePeerDependencies: - magicast - supports-color - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(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))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(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))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.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) + 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) transitivePeerDependencies: - magicast - supports-color @@ -18200,13 +18214,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.23(typescript@5.9.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.2(@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)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3)) + '@vue/devtools-core': 7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.23(typescript@5.9.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18235,9 +18249,9 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 unimport: 3.14.6(rollup@4.50.0) - vite: 7.1.2(@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) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@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)) - vite-plugin-vue-inspector: 5.3.2(vite@7.1.2(@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)) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) + vite-plugin-vue-inspector: 5.3.2(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18247,13 +18261,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.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))(vue@3.5.23(typescript@5.9.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(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)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.2(@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)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(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))(vue@3.5.23(typescript@5.9.3)) + '@vue/devtools-core': 7.6.8(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18282,9 +18296,9 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 unimport: 3.14.6(rollup@4.50.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) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.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)) - vite-plugin-vue-inspector: 5.3.2(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)) + vite: 7.1.2(@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) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@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)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.2(@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)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18294,13 +18308,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.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))(vue@3.5.23(typescript@5.9.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.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))(vue@3.5.23(typescript@5.9.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(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)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(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)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(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))(vue@3.5.23(typescript@5.9.3)) + '@vue/devtools-core': 7.6.8(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))(vue@3.5.23(typescript@5.9.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18329,9 +18343,9 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 unimport: 3.14.6(rollup@4.50.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) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.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)) - vite-plugin-vue-inspector: 5.3.2(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)) + 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) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.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)) + vite-plugin-vue-inspector: 5.3.2(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)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18430,7 +18444,7 @@ snapshots: errx: 0.1.0 exsolve: 1.0.7 ignore: 7.0.5 - jiti: 2.5.1 + jiti: 2.6.1 klona: 2.0.6 knitwork: 1.2.0 mlly: 1.8.0 @@ -18440,7 +18454,7 @@ snapshots: scule: 1.3.0 semver: 7.7.2 std-env: 3.9.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 ufo: 1.6.1 unctx: 2.4.1 unimport: 5.2.0 @@ -18557,7 +18571,7 @@ snapshots: tinyexec: 0.3.2 ufo: 1.6.1 unplugin: 2.3.10 - vite: 6.3.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) + vite: 6.4.1(@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) vitest-environment-nuxt: 1.0.1(@types/node@22.10.5)(@vue/test-utils@2.4.6)(jiti@2.6.1)(jsdom@23.0.0)(less@4.2.2)(magicast@0.3.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.5)(jiti@2.6.1)(jsdom@23.0.0)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.1))(yaml@2.8.1) vue: 3.5.23(typescript@5.9.3) optionalDependencies: @@ -20062,7 +20076,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.19.9)(vite@7.1.2(@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)))(svelte@5.19.9)(vite@7.1.2(@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))': dependencies: '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.19.9)(vite@7.1.2(@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)) - debug: 4.4.1 + debug: 4.4.3 svelte: 5.19.9 vite: 7.1.2(@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) transitivePeerDependencies: @@ -20176,24 +20190,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.28.5 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/body-parser@1.19.6': dependencies: @@ -20448,7 +20462,7 @@ snapshots: '@typescript-eslint/types': 8.29.1 '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.29.1 - debug: 4.4.1 + debug: 4.4.3 eslint: 9.17.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: @@ -20460,7 +20474,7 @@ snapshots: '@typescript-eslint/types': 8.29.1 '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.29.1 - debug: 4.4.1 + debug: 4.4.3 eslint: 9.17.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -20493,7 +20507,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.8.3) '@typescript-eslint/utils': 8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.17.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -20504,7 +20518,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.9.3) '@typescript-eslint/utils': 8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.17.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 @@ -20535,7 +20549,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.29.1 '@typescript-eslint/visitor-keys': 8.29.1 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -20549,7 +20563,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.29.1 '@typescript-eslint/visitor-keys': 8.29.1 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -20577,7 +20591,7 @@ snapshots: '@typescript-eslint/utils@5.62.0(eslint@9.17.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.17.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.17.0(jiti@2.6.1)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 5.62.0 @@ -20592,7 +20606,7 @@ snapshots: '@typescript-eslint/utils@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.17.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.17.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.8.3) @@ -20603,7 +20617,7 @@ snapshots: '@typescript-eslint/utils@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.17.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.17.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.9.3) @@ -20851,13 +20865,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.1(vite@6.3.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))': + '@vitest/mocker@3.1.1(vite@6.4.1(@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))': dependencies: '@vitest/spy': 3.1.1 estree-walker: 3.0.3 magic-string: 0.30.18 optionalDependencies: - vite: 6.3.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) + vite: 6.4.1(@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) '@vitest/mocker@3.2.4(vite@7.1.2(@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))': dependencies: @@ -21079,38 +21093,38 @@ snapshots: dependencies: '@vue/devtools-kit': 8.0.3 - '@vue/devtools-core@7.6.8(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3))': + '@vue/devtools-core@7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.23(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(vite@7.1.2(@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)) + vite-hot-client: 0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) vue: 3.5.23(typescript@5.9.3) transitivePeerDependencies: - vite - '@vue/devtools-core@7.6.8(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))(vue@3.5.23(typescript@5.9.3))': + '@vue/devtools-core@7.6.8(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(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)) + vite-hot-client: 0.2.4(vite@7.1.2(@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)) vue: 3.5.23(typescript@5.9.3) transitivePeerDependencies: - vite - '@vue/devtools-core@7.6.8(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))(vue@3.5.23(typescript@5.9.3))': + '@vue/devtools-core@7.6.8(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))(vue@3.5.23(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(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)) + vite-hot-client: 0.2.4(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)) vue: 3.5.23(typescript@5.9.3) transitivePeerDependencies: - vite @@ -21743,7 +21757,7 @@ snapshots: axios@1.8.2: dependencies: - follow-redirects: 1.15.11(debug@4.4.1) + follow-redirects: 1.15.11(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -21760,7 +21774,7 @@ snapshots: schema-utils: 4.3.2 webpack: 5.98.0(esbuild@0.25.0) - babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0): + babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)): dependencies: '@babel/core': 7.26.9 find-cache-dir: 4.0.0 @@ -21968,7 +21982,7 @@ snapshots: defu: 6.1.4 dotenv: 16.6.1 giget: 1.2.5 - jiti: 2.5.1 + jiti: 2.6.1 mlly: 1.8.0 ohash: 1.1.6 pathe: 1.1.2 @@ -21986,7 +22000,7 @@ snapshots: dotenv: 17.2.3 exsolve: 1.0.7 giget: 2.0.0 - jiti: 2.5.1 + jiti: 2.6.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 1.0.0 @@ -23189,8 +23203,8 @@ snapshots: '@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-import-resolver-node: 0.3.9 - 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-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)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.17.0(jiti@2.6.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-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.17.0(jiti@2.6.1)) @@ -23217,33 +23231,33 @@ snapshots: transitivePeerDependencies: - supports-color - 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-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.17.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 + debug: 4.4.3 eslint: 9.17.0(jiti@2.6.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - 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)) + 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)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(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)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@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-import-resolver-node: 0.3.9 - 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-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.17.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - 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)): + 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)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -23254,7 +23268,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.17.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(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)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -23431,7 +23445,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -23847,9 +23861,9 @@ snapshots: dependencies: tabbable: 6.2.0 - follow-redirects@1.15.11(debug@4.4.1): + follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.1 + debug: 4.4.3 for-each@0.3.5: dependencies: @@ -24243,14 +24257,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color http-proxy-middleware@2.0.9(@types/express@4.17.21): dependencies: '@types/http-proxy': 1.17.16 - http-proxy: 1.18.1(debug@4.4.1) + http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -24262,8 +24276,8 @@ snapshots: http-proxy-middleware@3.0.3: dependencies: '@types/http-proxy': 1.17.16 - debug: 4.4.1 - http-proxy: 1.18.1(debug@4.4.1) + debug: 4.4.3 + http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 micromatch: 4.0.8 @@ -24273,18 +24287,18 @@ snapshots: http-proxy-middleware@3.0.5: dependencies: '@types/http-proxy': 1.17.16 - debug: 4.4.1 - http-proxy: 1.18.1(debug@4.4.1) + debug: 4.4.3 + http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 micromatch: 4.0.8 transitivePeerDependencies: - supports-color - http-proxy@1.18.1(debug@4.4.1): + http-proxy@1.18.1(debug@4.4.3): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.11(debug@4.4.1) + follow-redirects: 1.15.11(debug@4.4.3) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -24294,7 +24308,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -24675,7 +24689,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -24684,7 +24698,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.30 - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -24880,7 +24894,7 @@ snapshots: dom-serialize: 2.2.1 glob: 7.2.3 graceful-fs: 4.2.11 - http-proxy: 1.18.1(debug@4.4.1) + http-proxy: 1.18.1(debug@4.4.3) isbinaryfile: 4.0.10 lodash: 4.17.21 log4js: 6.9.1 @@ -25125,7 +25139,7 @@ snapshots: log4js@6.9.1: dependencies: date-format: 4.0.14 - debug: 4.4.1 + debug: 4.4.3 flatted: 3.3.3 rfdc: 1.4.1 streamroller: 3.1.5 @@ -26238,10 +26252,10 @@ snapshots: - vue-tsc - xml2js - nuxt@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.2(@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)): + nuxt@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@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3)) + '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.23(typescript@5.9.3)) '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/telemetry': 2.6.6(magicast@0.3.5) @@ -26297,7 +26311,7 @@ snapshots: unhead: 1.11.20 unimport: 3.14.6(rollup@4.50.0) unplugin: 1.16.1 - unplugin-vue-router: 0.10.9(rollup@4.50.0)(vue-router@4.5.0(vue@3.5.13(typescript@5.9.3)))(vue@3.5.23(typescript@5.9.3)) + unplugin-vue-router: 0.10.9(rollup@4.50.0)(vue-router@4.5.0(vue@3.5.23(typescript@5.9.3)))(vue@3.5.23(typescript@5.9.3)) unstorage: 1.17.0(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.7.0) untyped: 1.5.2 vue: 3.5.23(typescript@5.9.3) @@ -26359,10 +26373,10 @@ snapshots: - vue-tsc - xml2js - nuxt@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)): + nuxt@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.2(@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)): dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.7.0(rollup@4.50.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))(vue@3.5.23(typescript@5.9.3)) + '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@7.1.2(@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))(vue@3.5.23(typescript@5.9.3)) '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/telemetry': 2.6.6(magicast@0.3.5) @@ -26418,7 +26432,7 @@ snapshots: unhead: 1.11.20 unimport: 3.14.6(rollup@4.50.0) unplugin: 1.16.1 - unplugin-vue-router: 0.10.9(rollup@4.50.0)(vue-router@4.5.0(vue@3.5.23(typescript@5.9.3)))(vue@3.5.23(typescript@5.9.3)) + unplugin-vue-router: 0.10.9(rollup@4.50.0)(vue-router@4.5.0(vue@3.5.13(typescript@5.9.3)))(vue@3.5.23(typescript@5.9.3)) unstorage: 1.17.0(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.7.0) untyped: 1.5.2 vue: 3.5.23(typescript@5.9.3) @@ -26995,7 +27009,7 @@ snapshots: ts-node: 10.9.2(@types/node@22.10.5)(typescript@5.9.3) optional: true - postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0): + postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 @@ -28721,11 +28735,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinyglobby@0.2.14: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -29159,7 +29168,7 @@ snapshots: pkg-types: 2.3.0 scule: 1.3.0 strip-literal: 3.0.0 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 unplugin: 2.3.10 unplugin-utils: 0.2.5 @@ -29374,7 +29383,7 @@ snapshots: '@babel/types': 7.28.2 citty: 0.1.6 defu: 6.1.4 - jiti: 2.5.1 + jiti: 2.6.1 knitwork: 1.2.0 scule: 1.3.0 transitivePeerDependencies: @@ -29384,7 +29393,7 @@ snapshots: dependencies: citty: 0.1.6 defu: 6.1.4 - jiti: 2.5.1 + jiti: 2.6.1 knitwork: 1.2.0 scule: 1.3.0 @@ -29478,6 +29487,10 @@ snapshots: vite: 7.1.2(@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) vite-hot-client: 2.1.0(vite@7.1.2(@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)) + vite-hot-client@0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): + dependencies: + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) + vite-hot-client@0.2.4(vite@7.1.2(@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)): dependencies: vite: 7.1.2(@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) @@ -29486,10 +29499,6 @@ snapshots: dependencies: 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) - vite-hot-client@0.2.4(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)): - dependencies: - 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) - vite-hot-client@2.1.0(vite@7.1.2(@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)): dependencies: vite: 7.1.2(@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) @@ -29533,10 +29542,10 @@ snapshots: vite-node@3.1.1(@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): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.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) + vite: 6.4.1(@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) transitivePeerDependencies: - '@types/node' - jiti @@ -29554,7 +29563,7 @@ snapshots: vite-node@3.2.4(@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): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.1.2(@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) @@ -29634,7 +29643,7 @@ snapshots: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@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)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29645,14 +29654,14 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.2 - vite: 7.1.2(@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) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.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)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@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)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29663,14 +29672,14 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.2 - 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) + vite: 7.1.2(@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) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.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)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.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)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29681,7 +29690,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.2 - 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) + 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) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: @@ -29691,7 +29700,7 @@ snapshots: vite-plugin-inspect@11.3.3(vite@7.1.2(@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)): dependencies: ansis: 4.1.0 - debug: 4.4.1 + debug: 4.4.3 error-stack-parser-es: 1.0.5 ohash: 2.0.11 open: 10.2.0 @@ -29718,7 +29727,7 @@ snapshots: - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@7.1.2(@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)): + vite-plugin-vue-inspector@5.3.2(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29729,11 +29738,11 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - vite: 7.1.2(@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) + vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) transitivePeerDependencies: - supports-color - vite-plugin-vue-inspector@5.3.2(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)): + vite-plugin-vue-inspector@5.3.2(vite@7.1.2(@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)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29744,11 +29753,11 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - 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) + vite: 7.1.2(@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) transitivePeerDependencies: - supports-color - vite-plugin-vue-inspector@5.3.2(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)): + vite-plugin-vue-inspector@5.3.2(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)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29759,7 +29768,7 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - 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) + 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) transitivePeerDependencies: - supports-color @@ -29815,7 +29824,7 @@ snapshots: terser: 5.39.0 yaml: 2.8.1 - vite@6.3.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): + vite@6.4.1(@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): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -29829,10 +29838,10 @@ snapshots: jiti: 2.6.1 less: 4.2.2 sass: 1.85.0 - terser: 5.43.1 + terser: 5.39.0 yaml: 2.8.1 - vite@6.4.1(@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): + vite@6.4.1(@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): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -29846,7 +29855,7 @@ snapshots: jiti: 2.6.1 less: 4.2.2 sass: 1.85.0 - terser: 5.39.0 + terser: 5.43.1 yaml: 2.8.1 vite@7.1.2(@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): @@ -30001,7 +30010,7 @@ snapshots: vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.5)(jiti@2.6.1)(jsdom@23.0.0)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.1): dependencies: '@vitest/expect': 3.1.1 - '@vitest/mocker': 3.1.1(vite@6.3.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)) + '@vitest/mocker': 3.1.1(vite@6.4.1(@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)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.1.1 '@vitest/snapshot': 3.1.1 @@ -30017,7 +30026,7 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.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) + vite: 6.4.1(@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) vite-node: 3.1.1(@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) why-is-node-running: 2.3.0 optionalDependencies: @@ -30124,7 +30133,7 @@ snapshots: vue-eslint-parser@9.4.3(eslint@9.17.0(jiti@2.6.1)): dependencies: - debug: 4.4.1 + debug: 4.4.3 eslint: 9.17.0(jiti@2.6.1) eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -30223,7 +30232,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-dev-middleware@7.4.2(webpack@5.98.0(esbuild@0.25.0)): + webpack-dev-middleware@7.4.2(webpack@5.98.0): dependencies: colorette: 2.0.20 memfs: 4.38.2 @@ -30234,7 +30243,7 @@ snapshots: optionalDependencies: webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)): + webpack-dev-server@5.2.0(webpack@5.98.0): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -30261,7 +30270,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0) ws: 8.18.3 optionalDependencies: webpack: 5.98.0(esbuild@0.25.0) @@ -30299,7 +30308,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0) ws: 8.18.3 optionalDependencies: webpack: 5.98.0(esbuild@0.25.0) From 212fcaf5570ce05370dac6ace6e2751eecddb2d4 Mon Sep 17 00:00:00 2001 From: Lubos Date: Tue, 11 Nov 2025 21:40:01 +0800 Subject: [PATCH 5/5] Update client support in chilled-tools-complain --- .changeset/chilled-tools-complain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilled-tools-complain.md diff --git a/.changeset/chilled-tools-complain.md b/.changeset/chilled-tools-complain.md new file mode 100644 index 000000000..869140301 --- /dev/null +++ b/.changeset/chilled-tools-complain.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +**clients**: add support for Ky client