UNPKG

@scalar/oas-utils

Version:

Open API spec and Yaml handling utilities

262 lines 11.6 kB
import type { AnyEventListener, ApiReferenceEvents, WorkspaceEventBus } from '@scalar/workspace-store/events'; import type { RequestFactory, VariablesStore } from '@scalar/workspace-store/request-example'; import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.1/strict/openapi-document'; import type { OperationObject } from '@scalar/workspace-store/schemas/v3.1/strict/operation'; import type { Component, DefineComponent } from 'vue'; /** Shared fields present on every response body handler variant */ type ResponseBodyHandlerBase = { /** MIME type patterns this handler matches (exact or glob like "application/vnd.*+json") */ mimeTypes: string[]; /** Custom decoder: transform raw bytes into displayable data */ decode?: (buffer: ArrayBuffer, contentType: string) => string | Blob | Promise<string | Blob>; /** Custom component for the preview view */ previewComponent?: Component; }; /** * Describes how a plugin handles a specific content type in the response body. * * The raw view is configured with either: * - `rawComponent`: A custom Vue component (receives `content` and `contentType` props). * - `language`: A CodeMirror language hint for the built-in raw renderer. * * These two options are mutually exclusive — providing `rawComponent` means the * built-in renderer is not used, so `language` would have no effect. */ export type ResponseBodyHandler = ResponseBodyHandlerBase & ({ rawComponent: Component; language?: never; } | { rawComponent?: never; language?: string; }); /** A type representing the hooks that a client plugin can define */ type ClientPluginHooks = { /** * Runs when an operation view mounts, before any request is sent. Useful for warming up * resources that would otherwise add latency to the first request (for example, lazily * created sandboxes). Receives the current document and operation so plugins can decide * whether the work is needed at all. */ onRequestMount: (payload: { document: OpenApiDocument; operation: OperationObject; }) => void | Promise<void>; /** * Runs before a request is sent. Receives the current document and operation so plugins can * modify the request before it is sent (for example, adding headers or modifying the body). * * Mutations here happen on the request builder, before the fetch `Request` exists. Use the * `requestBuilt` hook instead when you need the exact outgoing request (for example, to hash a * multipart body for request signing). */ beforeRequest: (payload: { /** Workspace-store request spec; mutable by pre-request scripts (headers, method). */ requestBuilder: RequestFactory; document: OpenApiDocument; operation: OperationObject; variablesStore?: VariablesStore; }) => void | Promise<void>; /** * Runs after the fetch `Request` has been built, right before it is sent. The request passed * here is the exact object handed to fetch, so header mutations apply to the outgoing request * and the body bytes match what goes over the wire (important for request signing, where a * rebuilt multipart body would get a different boundary). Mutations to the request builder * have no effect at this stage; use the `beforeRequest` hook for those. */ requestBuilt: (payload: { /** The exact fetch Request that will be sent. Mutate its headers to modify the outgoing request. */ request: Request; /** Request builder the request was built from. Mutating it has no effect at this stage. */ requestBuilder: RequestFactory; document: OpenApiDocument; operation: OperationObject; variablesStore?: VariablesStore; }) => void | Promise<void>; /** * Runs after a response is received. Receives the current document and operation so plugins can * modify the response after it is received (for example, adding headers or modifying the body). */ responseReceived: (payload: { response: Response; /** Request builder object that was used to build the request. Mutating this object will not affect the request object. */ requestBuilder: RequestFactory; /** Request object that was sent to the server. */ request: Request; document: OpenApiDocument; operation: OperationObject; variablesStore?: VariablesStore; }) => void | Promise<void>; }; /** Direction of a WebSocket message frame */ export type WebSocketFrameDirection = 'incoming' | 'outgoing'; /** Opcode classification for a WebSocket frame */ export type WebSocketFrameType = 'text' | 'binary' | 'close'; /** A single WebSocket message frame (sent or received) */ export type WebSocketPluginFrame = { direction: WebSocketFrameDirection; timestamp: number; data: string | ArrayBuffer; opcode: WebSocketFrameType; }; /** Close event metadata for WebSocket plugin hooks */ export type WebSocketPluginCloseInfo = { code: number; reason: string; wasClean: boolean; }; /** * WebSocket-specific plugin hooks for AsyncAPI channel operations. * * These are intentionally separate from the HTTP `ClientPluginHooks` because * the WebSocket lifecycle (long-lived connection, bidirectional frames) does * not map onto request/response semantics. */ export type ClientPluginWebSocketHooks = { /** Runs before the WebSocket handshake. Return a modified URL to override. */ beforeConnect: (payload: { url: string; }) => string | void | Promise<string | void>; /** Runs for every incoming or outgoing frame on an open connection. */ onWebSocketMessage: (payload: { frame: WebSocketPluginFrame; }) => void | Promise<void>; /** Runs when the connection closes (cleanly or due to error). */ onWebSocketClose: (payload: { info: WebSocketPluginCloseInfo; }) => void | Promise<void>; }; /** A vue component which accepts the specified props */ type ClientPluginComponent<Props extends Record<string, unknown>, Emits extends Record<string, (...args: any[]) => void> = {}> = { component: DefineComponent<Props, {}, {}, {}, {}, {}, {}, Emits>; additionalProps?: Record<string, unknown>; }; type ClientPluginComponents = { request: ClientPluginComponent<{ operation?: OperationObject; }, { 'operation:update:extension': (payload: ApiReferenceEvents['operation:update:extension']['payload']) => void; }>; response: ClientPluginComponent<{ operation?: OperationObject; }>; }; /** * ClientPlugin is used to extend the API Client with custom hooks and UI components. * * Example usage: * * const myPlugin: ClientPlugin = { * hooks: { * beforeRequest: ({ request }) => { * request.headers.set('X-Custom-Header', 'foo'); * return { request }; * }, * responseReceived: async (response, operation) => { * // Handle post-response logic * const data = await response.json(); * console.log('Received:', data, 'for operation:', operation.operationId); * } * }, * components: { * request: MyRequestComponent, // Custom Vue component for rendering the request section * response: MyResponseComponent // Custom Vue component for rendering the response section * }, * responseBody: [{ * mimeTypes: ['application/msgpack', 'application/x-msgpack'], * decode: (buffer) => { * const decoded = msgpack.decode(new Uint8Array(buffer)); * return JSON.stringify(decoded, null, 2); * }, * language: 'json', * }] * } */ /** Lifecycle hooks for app-level plugin concerns (analytics, logging, etc.) */ type ClientPluginLifecycle = { /** Called when the API client is initialized */ onInit?: (context?: { config: Record<string, unknown>; }) => void; /** Called when the API client configuration changes */ onConfigChange?: (context: { config: Record<string, unknown>; }) => void; /** Called when the API client is destroyed */ onDestroy?: () => void; }; export type ClientPlugin = { hooks?: Partial<ClientPluginHooks>; /** WebSocket-specific hooks for AsyncAPI channel operations */ webSocketHooks?: Partial<ClientPluginWebSocketHooks>; components?: Partial<ClientPluginComponents>; /** Lifecycle hooks for app-level concerns */ lifecycle?: ClientPluginLifecycle; /** * Subscribe to every event on the bus. The framework wires this up to * `bus.onAny` and handles subscribe/unsubscribe automatically. * * The listener receives a single tagged-union argument `{ event, payload }` * where `event` is the discriminant. Narrowing on `event` automatically * narrows `payload` to the exact type for that event — no casts, no `as` * assertions, and no manual runtime type checks just to satisfy the * compiler. Destructuring in the parameter list works too. * * @example * on: ({ event, payload }) => { * if (event === 'log:user-login') { * // payload is narrowed to { uid: string; email?: string; teamUid: string } * posthog.identify(payload.uid, { email: payload.email }) * } * * if (event === 'operation:create:operation') { * // payload is narrowed to the operation-create payload * analytics.track('operation_created', payload) * } * } */ on?: AnyEventListener; /** Custom response body handlers for specific content types */ responseBody?: ResponseBodyHandler[]; }; /** * Subscribes a single plugin's `on` listener to the given event bus via `onAny`. * * The plugin's `on` is passed straight through to `bus.onAny`, so it will * receive every event emitted on the bus as a `{ event, payload }` object. * Plugins without an `on` listener get a no-op unsubscribe. * * Returns an unsubscribe function. Call it when the plugin is torn down or * the bus is destroyed to remove the wildcard listener. * * @example * const unsubscribe = subscribePluginEvents(eventBus, plugin) * // later... * unsubscribe() */ export declare const subscribePluginEvents: (eventBus: WorkspaceEventBus, plugin: ClientPlugin) => (() => void); /** * Maps hook names to their expected payload types. * This ensures type safety when executing hooks with their corresponding payloads. * Derived from the ClientPlugin hooks definition. */ type HookPayloadMap = { [K in keyof ClientPluginHooks]: Parameters<ClientPluginHooks[K]>[0]; }; /** * Execute any hook with type-safe payload handling. * The payload type is inferred from the hook name to ensure correct usage. */ export declare const executeHook: <K extends keyof HookPayloadMap>(payload: HookPayloadMap[K], hookName: K, plugins: ClientPlugin[]) => Promise<HookPayloadMap[K]>; type WebSocketHookPayloadMap = { [K in keyof ClientPluginWebSocketHooks]: Parameters<ClientPluginWebSocketHooks[K]>[0]; }; /** * Execute a WebSocket plugin hook across all plugins. * * For `beforeConnect`, the returned URL string (if any) is threaded through * sequentially so each plugin can transform the URL. For fire-and-forget hooks * (`onWebSocketMessage`, `onWebSocketClose`) the return value is ignored. */ export declare const executeWebSocketHook: <K extends keyof WebSocketHookPayloadMap>(payload: WebSocketHookPayloadMap[K], hookName: K, plugins: ClientPlugin[]) => Promise<WebSocketHookPayloadMap[K]>; export {}; //# sourceMappingURL=client-plugins.d.ts.map