@scalar/oas-utils
Version:
Open API spec and Yaml handling utilities
262 lines • 11.6 kB
TypeScript
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