@routar/core 1.4.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +14 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -4
- package/dist/index.d.ts +55 -4
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -156,7 +156,12 @@ function buildChain(execute, middlewares) {
|
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
function createExecutor(execute, options = {}) {
|
|
159
|
-
const
|
|
159
|
+
const plugins = [...options.plugins ?? []];
|
|
160
|
+
if (options.unwrap) {
|
|
161
|
+
const unwrapFn = options.unwrap;
|
|
162
|
+
plugins.push({ onResponse: (raw) => unwrapFn(raw) });
|
|
163
|
+
}
|
|
164
|
+
const middlewares = plugins.map(pluginToMiddleware);
|
|
160
165
|
return { execute: buildChain(execute, middlewares) };
|
|
161
166
|
}
|
|
162
167
|
function dispatchExecutor(resolver) {
|
|
@@ -295,7 +300,8 @@ function createFetchExecutor(baseURL, options) {
|
|
|
295
300
|
headers,
|
|
296
301
|
signal
|
|
297
302
|
}) => {
|
|
298
|
-
const
|
|
303
|
+
const resolvedBase = typeof baseURL === "function" ? await baseURL() : baseURL;
|
|
304
|
+
const fullURL = new URL(resolvedBase.replace(/\/$/, "") + url);
|
|
299
305
|
if (params) {
|
|
300
306
|
serializeParams(params).forEach((v, k) => {
|
|
301
307
|
fullURL.searchParams.set(k, v);
|
|
@@ -314,7 +320,7 @@ function createFetchExecutor(baseURL, options) {
|
|
|
314
320
|
});
|
|
315
321
|
if (!res.ok) {
|
|
316
322
|
const errorBody = await res.json().catch(() => null);
|
|
317
|
-
throw new HttpError(res.status, res.statusText, errorBody);
|
|
323
|
+
throw new HttpError(res.status, res.statusText, errorBody, { url: fullURL.toString(), method });
|
|
318
324
|
}
|
|
319
325
|
if (res.status === 204 || res.status === 205 || res.status === 304) {
|
|
320
326
|
return null;
|
|
@@ -322,16 +328,18 @@ function createFetchExecutor(baseURL, options) {
|
|
|
322
328
|
const text = await res.text();
|
|
323
329
|
return text === "" ? null : JSON.parse(text);
|
|
324
330
|
};
|
|
325
|
-
const executor = createExecutor(transport, { plugins: options?.plugins });
|
|
331
|
+
const executor = createExecutor(transport, { plugins: options?.plugins, unwrap: options?.unwrap });
|
|
326
332
|
return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };
|
|
327
333
|
}
|
|
328
334
|
var HttpError = class extends Error {
|
|
329
|
-
constructor(status, statusText, body = null) {
|
|
330
|
-
super(`HTTP ${status}: ${statusText}
|
|
335
|
+
constructor(status, statusText, body = null, options) {
|
|
336
|
+
super(`HTTP ${status}: ${statusText}`, { cause: options?.cause });
|
|
331
337
|
this.status = status;
|
|
332
338
|
this.statusText = statusText;
|
|
333
339
|
this.body = body;
|
|
334
340
|
this.name = "HttpError";
|
|
341
|
+
this.url = options?.url;
|
|
342
|
+
this.method = options?.method;
|
|
335
343
|
}
|
|
336
344
|
};
|
|
337
345
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/define-router.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/middleware.ts","../src/utils/params.ts","../src/create-fetch-executor.ts","../src/define-endpoint.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { RouterDef, RouterEndpoints } from \"./types.js\";\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\n/** Type guard — distinguishes a {@link RouterDef} from a leaf {@link EndpointSpec} at runtime. */\nexport function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints> {\n return \"prefix\" in entry && \"endpoints\" in entry;\n}\n\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","/**\n * Joins URL path segments, normalising repeated slashes and trailing slashes.\n *\n * **Note:** Intended for relative API paths only. Absolute URLs containing\n * `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs\n * directly to the executor instead of through this helper.\n */\nexport function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter((s) => s !== \"\")\n .join(\"/\")\n .replace(/\\/+/g, \"/\");\n return joined.endsWith(\"/\") && joined.length > 1\n ? joined.slice(0, -1)\n : joined || \"/\";\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null || value === \"\") throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n readonly cause?: unknown;\n\n constructor(message: string, cause?: unknown) {\n super(message);\n this.name = \"ValidationError\";\n if (cause !== undefined) {\n Object.defineProperty(this, \"cause\", {\n value: cause,\n writable: false,\n enumerable: false,\n configurable: true,\n });\n }\n }\n}\n","import { isRouterDef } from \"./define-router.js\";\nimport type {\n CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\n ValidatorOutput,\n} from \"./types.js\";\nimport { joinPaths, resolvePath } from \"./utils/path.js\";\nimport { ValidationError } from \"./utils/validate.js\";\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> =\n TSpec[\"request\"] extends { parse: (data: unknown) => infer R }\n ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>>\n : (\n params?: RequestShape,\n signal?: AbortSignal,\n ) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\nexport type ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<\n infer TNestedEndpoints\n >\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * An {@link ApiClient} that also carries its source {@link RouterDef} on the\n * `$router` property. This is the actual return type of {@link createApi};\n * downstream tools (e.g. `@routar/react-query`) recover the router (prefix +\n * endpoint methods) from it without it being re-passed. `$router` is\n * non-enumerable and excluded from {@link ApiTypes}; the `$` prefix keeps it\n * from colliding with endpoint names.\n */\nexport type ApiClientWithRouter<TEndpoints extends RouterEndpoints> =\n ApiClient<TEndpoints> & { readonly $router: RouterDef<TEndpoints> };\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).\n *\n * @example Basic usage\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n * const next = await todoApi.create({ body: { title: 'buy milk' } });\n * ```\n *\n * @example Nested router — access via dot notation\n * ```ts\n * const api = createApi(executor, apiRouter); // apiRouter has users → todos nesting\n * await api.users.getList({});\n * await api.users.todos.getList({});\n * ```\n *\n * @example Cancel in-flight requests with AbortSignal\n * ```ts\n * const controller = new AbortController();\n * const todos = await todoApi.getList({}, controller.signal);\n * controller.abort();\n * ```\n *\n * @example Extract types from the client — no duplication\n * ```ts\n * import type { ApiTypes } from '@routar/core';\n * type TodoApiTypes = ApiTypes<typeof todoApi>;\n * type Todo = TodoApiTypes['getDetail']['response'];\n * type CreateRequest = TodoApiTypes['create']['request'];\n * ```\n *\n * @example Skip response validation in production\n * ```ts\n * const prodApi = createApi(executor, todoRouter, {\n * validate: { request: true, response: process.env.NODE_ENV !== 'production' },\n * });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints:\n | RouterDef<RouterEndpoints>\n | RouterEndpoints\n | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n const { prefix, endpoints, options } = resolveArgs(\n routerOrPrefixOrEndpoints,\n endpointsArgOrOptions,\n optionsArg,\n );\n const client = buildClient(executor, prefix, endpoints, options);\n // Stash the source router so downstream tools (e.g. @routar/react-query)\n // recover prefix + endpoint methods without it being re-passed.\n Object.defineProperty(client, \"$router\", {\n value: { prefix, endpoints } satisfies RouterDef<RouterEndpoints>,\n enumerable: false,\n });\n return client;\n}\n\nfunction resolveArgs(\n second: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n third: RouterEndpoints | CreateApiOptions | undefined,\n fourth: CreateApiOptions | undefined,\n): {\n prefix: string;\n endpoints: RouterEndpoints;\n options: CreateApiOptions | undefined;\n} {\n if (typeof second === \"string\") {\n if (!third)\n throw new Error(\"endpoints is required when prefix is provided\");\n return {\n prefix: second,\n endpoints: third as RouterEndpoints,\n options: fourth,\n };\n }\n if (isRouterDef(second)) {\n return {\n prefix: second.prefix,\n endpoints: second.endpoints,\n options: third as CreateApiOptions | undefined,\n };\n }\n return {\n prefix: \"\",\n endpoints: second as RouterEndpoints,\n options: third as CreateApiOptions | undefined,\n };\n}\n\nfunction shouldValidate(\n options: CreateApiOptions | undefined,\n kind: \"request\" | \"response\",\n): boolean {\n const v = options?.validate;\n if (v === undefined || v === true) return true;\n if (v === false) return false;\n return v[kind] ?? true;\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n options?: CreateApiOptions,\n): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n client[key] = isRouterDef(entry)\n ? buildClient(\n executor,\n joinPaths(prefix, entry.prefix),\n entry.endpoints,\n options,\n )\n : buildEndpointFn(\n executor,\n prefix,\n entry as EndpointSpec<any, any, any>,\n options,\n );\n }\n\n return client;\n}\n\nfunction buildEndpointFn(\n executor: Executor,\n prefix: string,\n spec: EndpointSpec<any, any, any>,\n options: CreateApiOptions | undefined,\n) {\n return async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request && shouldValidate(options, \"request\")) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError(\"Request validation failed\", err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let result: ValidatorOutput<typeof spec.response>;\n if (shouldValidate(options, \"response\")) {\n try {\n result = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError(\"Response validation failed\", err);\n }\n } else {\n result = raw;\n }\n\n return spec.adapter ? spec.adapter(result) : result;\n };\n}\n","import type {\n CreateExecutorOptions,\n ExecuteOptions,\n Executor,\n ExecutorMiddleware,\n ExecutorPlugin,\n} from \"./types.js\";\n\nfunction pluginToMiddleware(plugin: ExecutorPlugin): ExecutorMiddleware {\n return async (opts, next) => {\n const resolvedOpts = plugin.onRequest ? await plugin.onRequest(opts) : opts;\n try {\n const response = await next(resolvedOpts);\n return plugin.onResponse\n ? await plugin.onResponse(response, resolvedOpts)\n : response;\n } catch (err) {\n if (plugin.onError) {\n await plugin.onError(err, resolvedOpts);\n throw err;\n }\n throw err;\n }\n };\n}\n\nexport function buildChain(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[],\n): (options: ExecuteOptions) => Promise<unknown> {\n return middlewares.reduceRight<(options: ExecuteOptions) => Promise<unknown>>(\n (next, mw) => (opts) => mw(opts, next),\n execute,\n );\n}\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with plugins.\n *\n * Plugins run in declaration order (first plugin is outermost).\n *\n * For `retry` and `timeout`, use the options on {@link createFetchExecutor}\n * or configure them via the underlying HTTP client (axios, ky).\n *\n * @example\n * ```ts\n * const executor = createExecutor(transport, {\n * plugins: [authPlugin, logger()],\n * });\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n options: CreateExecutorOptions = {},\n): Executor {\n const middlewares = (options.plugins ?? []).map(pluginToMiddleware);\n return { execute: buildChain(execute, middlewares) };\n}\n\n/**\n * Creates an {@link Executor} that selects the underlying transport at\n * request time based on the result of `resolver`.\n *\n * @example\n * ```ts\n * const apiExecutor = dispatchExecutor(() =>\n * typeof window === 'undefined' ? serverExecutor : clientExecutor,\n * );\n * ```\n */\nexport function dispatchExecutor(\n resolver: (opts: ExecuteOptions) => Executor,\n): Executor {\n return {\n execute: (opts) => resolver(opts).execute(opts),\n };\n}\n","import type { ExecuteOptions, ExecutorMiddleware, ExecutorPlugin } from \"./types.js\";\n\n/**\n * Thrown by the built-in `timeout` option when a request exceeds the\n * configured duration. Distinguishable from a user-initiated\n * {@link AbortSignal} cancellation.\n */\nexport class TimeoutError extends Error {\n constructor(public readonly ms: number) {\n super(`Request timed out after ${ms}ms`);\n this.name = \"TimeoutError\";\n }\n}\n\n/**\n * Identity helper that returns the plugin as-is, providing full type inference.\n *\n * @example\n * ```ts\n * const authPlugin = definePlugin({\n * name: 'auth',\n * onRequest: async (opts) => ({\n * ...opts,\n * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },\n * }),\n * });\n * ```\n */\nexport function definePlugin(plugin: ExecutorPlugin): ExecutorPlugin {\n return plugin;\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * createExecutor(transport, {\n * plugins: [logger({ log: (msg, data) => myLogger.debug(msg, data) })],\n * })\n * ```\n */\nexport function logger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorPlugin {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n const timings = new WeakMap<ExecuteOptions, number>();\n return definePlugin({\n name: \"logger\",\n onRequest: (opts) => {\n timings.set(opts, Date.now());\n log(`[routar] ${opts.method} ${opts.url}`, {\n params: opts.params,\n body: opts.body,\n });\n return opts;\n },\n onResponse: (res, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n );\n timings.delete(opts);\n return res;\n },\n onError: (err, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n err,\n );\n timings.delete(opts);\n throw err as never;\n },\n });\n}\n\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n };\n}\n\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);\n\n const { signal, cleanup } = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : { signal: controller.signal, cleanup: () => {} };\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n cleanup();\n }\n };\n}\n\nfunction anySignal(signals: AbortSignal[]): {\n signal: AbortSignal;\n cleanup: () => void;\n} {\n const controller = new AbortController();\n const onAbort = () => controller.abort();\n const attached: AbortSignal[] = [];\n for (const s of signals) {\n if (s.aborted) {\n controller.abort();\n break;\n }\n s.addEventListener(\"abort\", onAbort, { once: true });\n attached.push(s);\n }\n return {\n signal: controller.signal,\n cleanup: () =>\n attached.forEach((s) => {\n s.removeEventListener(\"abort\", onAbort);\n }),\n };\n}\n","export function serializeParams(\n params: Record<string, unknown>,\n): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else if (typeof value === \"object\") {\n throw new TypeError(\n `serializeParams: value for key \"${key}\" is a plain object. Serialize it to a string before passing as a query parameter.`,\n );\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n","import type { CreateExecutorOptions, ExecuteOptions, Executor, ExecutorMiddleware } from \"./types.js\";\nimport { buildChain, createExecutor } from \"./create-executor.js\";\nimport { withRetry, withTimeout } from \"./middleware.js\";\nimport { serializeParams } from \"./utils/params.js\";\n\nexport type FetchRetryOption =\n | number\n | { count: number; shouldRetry?: (error: unknown, attempt: number) => boolean };\n\nexport interface FetchExecutorOptions extends CreateExecutorOptions {\n defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n retry?: FetchRetryOption;\n timeout?: number;\n}\n\nfunction buildFetchChain(\n transport: (opts: ExecuteOptions) => Promise<unknown>,\n retry?: FetchRetryOption,\n timeout?: number,\n): (opts: ExecuteOptions) => Promise<unknown> {\n const middlewares: ExecutorMiddleware[] = [\n ...(retry != null\n ? [\n typeof retry === \"number\"\n ? withRetry(retry)\n : withRetry(retry.count, { shouldRetry: retry.shouldRetry }),\n ]\n : []),\n ...(timeout != null ? [withTimeout(timeout)] : []),\n ];\n if (middlewares.length === 0) return transport;\n return buildChain(transport, middlewares);\n}\n\n/**\n * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.\n *\n * Suited for SSR environments where you need per-request dynamic headers\n * (e.g. forwarding auth cookies) without sharing state across requests.\n *\n * - Query params are serialized and appended to the URL.\n * - A `Content-Type: application/json` header is added automatically when\n * a request body is present.\n * - Responses with status 204 or `Content-Length: 0` resolve to `null`.\n * - Non-2xx responses throw an {@link HttpError}.\n *\n * @param baseURL - Absolute base URL prepended to every endpoint path.\n * @param options.defaultHeaders - Async factory called on every request to\n * produce headers (e.g. reading cookies in a Next.js server component).\n * @param options.plugins - Plugins applied around the fetch call. Each retry\n * attempt re-runs `onRequest` hooks (so headers are refreshed per attempt)\n * and `onError` hooks. Token-refresh-on-401 patterns work by updating an\n * external token store in `onError` and letting `onRequest` pick it up on\n * the next attempt.\n * @param options.retry - Number of retries, or `{ count, shouldRetry? }`.\n * @param options.timeout - Per-attempt timeout in milliseconds.\n *\n * @example Minimal — no options needed\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com');\n * ```\n *\n * @example SSR with bearer token\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * defaultHeaders: async () => {\n * const token = await getServerToken();\n * return token ? { Authorization: `Bearer ${token}` } : {};\n * },\n * });\n * ```\n *\n * @example With retry and timeout\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * retry: 2,\n * timeout: 8_000,\n * });\n * ```\n */\nexport function createFetchExecutor(\n baseURL: string,\n options?: FetchExecutorOptions,\n): Executor {\n const transport = async ({\n method,\n url,\n params,\n body,\n headers,\n signal,\n }: ExecuteOptions) => {\n const fullURL = new URL(baseURL.replace(/\\/$/, \"\") + url);\n if (params) {\n serializeParams(params).forEach((v, k) => {\n fullURL.searchParams.set(k, v);\n });\n }\n\n const defaultHeaders = (await options?.defaultHeaders?.()) ?? {};\n\n const res = await fetch(fullURL.toString(), {\n method,\n headers: {\n ...defaultHeaders,\n ...headers,\n ...(body != null ? { \"Content-Type\": \"application/json\" } : {}),\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal,\n });\n\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new HttpError(res.status, res.statusText, errorBody);\n }\n if (res.status === 204 || res.status === 205 || res.status === 304) {\n return null;\n }\n const text = await res.text();\n return text === \"\" ? null : JSON.parse(text);\n };\n\n const executor = createExecutor(transport, { plugins: options?.plugins });\n return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };\n}\n\n/**\n * Thrown by {@link createFetchExecutor} when the server returns a non-2xx\n * status code.\n *\n * @example\n * ```ts\n * try {\n * await api.getDetail({ path: { id: 999 } });\n * } catch (err) {\n * if (err instanceof HttpError && err.status === 404) {\n * // handle not-found\n * }\n * }\n * ```\n */\nexport class HttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly statusText: string,\n public readonly body: unknown = null,\n ) {\n super(`HTTP ${status}: ${statusText}`);\n this.name = \"HttpError\";\n }\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from \"./types.js\";\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * The literal HTTP method (`'GET'`, `'POST'`, …) is preserved on the return\n * type — `endpoint({ method: 'GET', ... }).method` is typed `'GET'`, not the\n * `HttpMethod` union.\n *\n * @example Basic GET with no params\n * ```ts\n * const getList = endpoint({ method: 'GET', path: '/', response: z.array(TodoSchema) });\n * ```\n *\n * @example GET with query params\n * ```ts\n * const search = endpoint({\n * method: 'GET',\n * path: '/search',\n * request: z.object({ query: z.object({ q: z.string(), limit: z.number().optional() }) }),\n * response: z.array(TodoSchema),\n * });\n * ```\n *\n * @example POST with body\n * ```ts\n * const create = endpoint({\n * method: 'POST',\n * path: '/',\n * request: z.object({ body: z.object({ title: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n *\n * @example Adapter — raw is inferred from the response schema, no cast needed\n * ```ts\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoRawSchema,\n * adapter: (raw) => ({ ...raw, label: `#${raw.id} ${raw.title}` }),\n * });\n * ```\n *\n * @example Path param enforcement\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/define-router.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/middleware.ts","../src/utils/params.ts","../src/create-fetch-executor.ts","../src/define-endpoint.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { RouterDef, RouterEndpoints } from \"./types.js\";\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\n/** Type guard — distinguishes a {@link RouterDef} from a leaf {@link EndpointSpec} at runtime. */\nexport function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints> {\n return \"prefix\" in entry && \"endpoints\" in entry;\n}\n\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","/**\n * Joins URL path segments, normalising repeated slashes and trailing slashes.\n *\n * **Note:** Intended for relative API paths only. Absolute URLs containing\n * `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs\n * directly to the executor instead of through this helper.\n */\nexport function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter((s) => s !== \"\")\n .join(\"/\")\n .replace(/\\/+/g, \"/\");\n return joined.endsWith(\"/\") && joined.length > 1\n ? joined.slice(0, -1)\n : joined || \"/\";\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null || value === \"\") throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n readonly cause?: unknown;\n\n constructor(message: string, cause?: unknown) {\n super(message);\n this.name = \"ValidationError\";\n if (cause !== undefined) {\n Object.defineProperty(this, \"cause\", {\n value: cause,\n writable: false,\n enumerable: false,\n configurable: true,\n });\n }\n }\n}\n","import { isRouterDef } from \"./define-router.js\";\nimport type {\n CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\n ValidatorOutput,\n} from \"./types.js\";\nimport { joinPaths, resolvePath } from \"./utils/path.js\";\nimport { ValidationError } from \"./utils/validate.js\";\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> =\n TSpec[\"request\"] extends { parse: (data: unknown) => infer R }\n ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>>\n : (\n params?: RequestShape,\n signal?: AbortSignal,\n ) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\nexport type ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<\n infer TNestedEndpoints\n >\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * An {@link ApiClient} that also carries its source {@link RouterDef} on the\n * `$router` property. This is the actual return type of {@link createApi};\n * downstream tools (e.g. `@routar/react-query`) recover the router (prefix +\n * endpoint methods) from it without it being re-passed. `$router` is\n * non-enumerable and excluded from {@link ApiTypes}; the `$` prefix keeps it\n * from colliding with endpoint names.\n */\nexport type ApiClientWithRouter<TEndpoints extends RouterEndpoints> =\n ApiClient<TEndpoints> & { readonly $router: RouterDef<TEndpoints> };\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).\n *\n * @example Basic usage\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n * const next = await todoApi.create({ body: { title: 'buy milk' } });\n * ```\n *\n * @example Nested router — access via dot notation\n * ```ts\n * const api = createApi(executor, apiRouter); // apiRouter has users → todos nesting\n * await api.users.getList({});\n * await api.users.todos.getList({});\n * ```\n *\n * @example Cancel in-flight requests with AbortSignal\n * ```ts\n * const controller = new AbortController();\n * const todos = await todoApi.getList({}, controller.signal);\n * controller.abort();\n * ```\n *\n * @example Extract types from the client — no duplication\n * ```ts\n * import type { ApiTypes } from '@routar/core';\n * type TodoApiTypes = ApiTypes<typeof todoApi>;\n * type Todo = TodoApiTypes['getDetail']['response'];\n * type CreateRequest = TodoApiTypes['create']['request'];\n * ```\n *\n * @example Skip response validation in production\n * ```ts\n * const prodApi = createApi(executor, todoRouter, {\n * validate: { request: true, response: process.env.NODE_ENV !== 'production' },\n * });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints:\n | RouterDef<RouterEndpoints>\n | RouterEndpoints\n | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n const { prefix, endpoints, options } = resolveArgs(\n routerOrPrefixOrEndpoints,\n endpointsArgOrOptions,\n optionsArg,\n );\n const client = buildClient(executor, prefix, endpoints, options);\n // Stash the source router so downstream tools (e.g. @routar/react-query)\n // recover prefix + endpoint methods without it being re-passed.\n Object.defineProperty(client, \"$router\", {\n value: { prefix, endpoints } satisfies RouterDef<RouterEndpoints>,\n enumerable: false,\n });\n return client;\n}\n\nfunction resolveArgs(\n second: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n third: RouterEndpoints | CreateApiOptions | undefined,\n fourth: CreateApiOptions | undefined,\n): {\n prefix: string;\n endpoints: RouterEndpoints;\n options: CreateApiOptions | undefined;\n} {\n if (typeof second === \"string\") {\n if (!third)\n throw new Error(\"endpoints is required when prefix is provided\");\n return {\n prefix: second,\n endpoints: third as RouterEndpoints,\n options: fourth,\n };\n }\n if (isRouterDef(second)) {\n return {\n prefix: second.prefix,\n endpoints: second.endpoints,\n options: third as CreateApiOptions | undefined,\n };\n }\n return {\n prefix: \"\",\n endpoints: second as RouterEndpoints,\n options: third as CreateApiOptions | undefined,\n };\n}\n\nfunction shouldValidate(\n options: CreateApiOptions | undefined,\n kind: \"request\" | \"response\",\n): boolean {\n const v = options?.validate;\n if (v === undefined || v === true) return true;\n if (v === false) return false;\n return v[kind] ?? true;\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n options?: CreateApiOptions,\n): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n client[key] = isRouterDef(entry)\n ? buildClient(\n executor,\n joinPaths(prefix, entry.prefix),\n entry.endpoints,\n options,\n )\n : buildEndpointFn(\n executor,\n prefix,\n entry as EndpointSpec<any, any, any>,\n options,\n );\n }\n\n return client;\n}\n\nfunction buildEndpointFn(\n executor: Executor,\n prefix: string,\n spec: EndpointSpec<any, any, any>,\n options: CreateApiOptions | undefined,\n) {\n return async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request && shouldValidate(options, \"request\")) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError(\"Request validation failed\", err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let result: ValidatorOutput<typeof spec.response>;\n if (shouldValidate(options, \"response\")) {\n try {\n result = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError(\"Response validation failed\", err);\n }\n } else {\n result = raw;\n }\n\n return spec.adapter ? spec.adapter(result) : result;\n };\n}\n","import type {\n CreateExecutorOptions,\n ExecuteOptions,\n Executor,\n ExecutorMiddleware,\n ExecutorPlugin,\n} from \"./types.js\";\n\nfunction pluginToMiddleware(plugin: ExecutorPlugin): ExecutorMiddleware {\n return async (opts, next) => {\n const resolvedOpts = plugin.onRequest ? await plugin.onRequest(opts) : opts;\n try {\n const response = await next(resolvedOpts);\n return plugin.onResponse\n ? await plugin.onResponse(response, resolvedOpts)\n : response;\n } catch (err) {\n if (plugin.onError) {\n await plugin.onError(err, resolvedOpts);\n throw err;\n }\n throw err;\n }\n };\n}\n\nexport function buildChain(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[],\n): (options: ExecuteOptions) => Promise<unknown> {\n return middlewares.reduceRight<(options: ExecuteOptions) => Promise<unknown>>(\n (next, mw) => (opts) => mw(opts, next),\n execute,\n );\n}\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with plugins.\n *\n * Plugins run in declaration order (first plugin is outermost).\n *\n * For `retry` and `timeout`, use the options on {@link createFetchExecutor}\n * or configure them via the underlying HTTP client (axios, ky).\n *\n * @example\n * ```ts\n * const executor = createExecutor(transport, {\n * plugins: [authPlugin, logger()],\n * });\n * ```\n *\n * @example Unwrap an envelope response\n * ```ts\n * const executor = createExecutor(transport, {\n * unwrap: (raw) => (raw as { data: unknown })?.data ?? raw,\n * });\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n options: CreateExecutorOptions = {},\n): Executor {\n const plugins = [...(options.plugins ?? [])];\n // `unwrap` becomes the innermost `onResponse` hook: appended last so it runs\n // first on the response (immediately after the transport, before user plugins).\n if (options.unwrap) {\n const unwrapFn = options.unwrap;\n plugins.push({ onResponse: (raw) => unwrapFn(raw) });\n }\n const middlewares = plugins.map(pluginToMiddleware);\n return { execute: buildChain(execute, middlewares) };\n}\n\n/**\n * Creates an {@link Executor} that selects the underlying transport at\n * request time based on the result of `resolver`.\n *\n * @example\n * ```ts\n * const apiExecutor = dispatchExecutor(() =>\n * typeof window === 'undefined' ? serverExecutor : clientExecutor,\n * );\n * ```\n */\nexport function dispatchExecutor(\n resolver: (opts: ExecuteOptions) => Executor,\n): Executor {\n return {\n execute: (opts) => resolver(opts).execute(opts),\n };\n}\n","import type { ExecuteOptions, ExecutorMiddleware, ExecutorPlugin } from \"./types.js\";\n\n/**\n * Thrown by the built-in `timeout` option when a request exceeds the\n * configured duration. Distinguishable from a user-initiated\n * {@link AbortSignal} cancellation.\n */\nexport class TimeoutError extends Error {\n constructor(public readonly ms: number) {\n super(`Request timed out after ${ms}ms`);\n this.name = \"TimeoutError\";\n }\n}\n\n/**\n * Identity helper that returns the plugin as-is, providing full type inference.\n *\n * @example\n * ```ts\n * const authPlugin = definePlugin({\n * name: 'auth',\n * onRequest: async (opts) => ({\n * ...opts,\n * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },\n * }),\n * });\n * ```\n */\nexport function definePlugin(plugin: ExecutorPlugin): ExecutorPlugin {\n return plugin;\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * createExecutor(transport, {\n * plugins: [logger({ log: (msg, data) => myLogger.debug(msg, data) })],\n * })\n * ```\n */\nexport function logger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorPlugin {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n const timings = new WeakMap<ExecuteOptions, number>();\n return definePlugin({\n name: \"logger\",\n onRequest: (opts) => {\n timings.set(opts, Date.now());\n log(`[routar] ${opts.method} ${opts.url}`, {\n params: opts.params,\n body: opts.body,\n });\n return opts;\n },\n onResponse: (res, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n );\n timings.delete(opts);\n return res;\n },\n onError: (err, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n err,\n );\n timings.delete(opts);\n throw err as never;\n },\n });\n}\n\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n };\n}\n\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);\n\n const { signal, cleanup } = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : { signal: controller.signal, cleanup: () => {} };\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n cleanup();\n }\n };\n}\n\nfunction anySignal(signals: AbortSignal[]): {\n signal: AbortSignal;\n cleanup: () => void;\n} {\n const controller = new AbortController();\n const onAbort = () => controller.abort();\n const attached: AbortSignal[] = [];\n for (const s of signals) {\n if (s.aborted) {\n controller.abort();\n break;\n }\n s.addEventListener(\"abort\", onAbort, { once: true });\n attached.push(s);\n }\n return {\n signal: controller.signal,\n cleanup: () =>\n attached.forEach((s) => {\n s.removeEventListener(\"abort\", onAbort);\n }),\n };\n}\n","export function serializeParams(\n params: Record<string, unknown>,\n): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else if (typeof value === \"object\") {\n throw new TypeError(\n `serializeParams: value for key \"${key}\" is a plain object. Serialize it to a string before passing as a query parameter.`,\n );\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n","import type { CreateExecutorOptions, ExecuteOptions, Executor, ExecutorMiddleware } from \"./types.js\";\nimport { buildChain, createExecutor } from \"./create-executor.js\";\nimport { withRetry, withTimeout } from \"./middleware.js\";\nimport { serializeParams } from \"./utils/params.js\";\n\nexport type FetchRetryOption =\n | number\n | { count: number; shouldRetry?: (error: unknown, attempt: number) => boolean };\n\nexport interface FetchExecutorOptions extends CreateExecutorOptions {\n defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n retry?: FetchRetryOption;\n timeout?: number;\n}\n\nfunction buildFetchChain(\n transport: (opts: ExecuteOptions) => Promise<unknown>,\n retry?: FetchRetryOption,\n timeout?: number,\n): (opts: ExecuteOptions) => Promise<unknown> {\n const middlewares: ExecutorMiddleware[] = [\n ...(retry != null\n ? [\n typeof retry === \"number\"\n ? withRetry(retry)\n : withRetry(retry.count, { shouldRetry: retry.shouldRetry }),\n ]\n : []),\n ...(timeout != null ? [withTimeout(timeout)] : []),\n ];\n if (middlewares.length === 0) return transport;\n return buildChain(transport, middlewares);\n}\n\n/**\n * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.\n *\n * Suited for SSR environments where you need per-request dynamic headers\n * (e.g. forwarding auth cookies) without sharing state across requests.\n *\n * - Query params are serialized and appended to the URL.\n * - A `Content-Type: application/json` header is added automatically when\n * a request body is present.\n * - Responses with status 204 or `Content-Length: 0` resolve to `null`.\n * - Non-2xx responses throw an {@link HttpError}.\n *\n * @param baseURL - Absolute base URL prepended to every endpoint path.\n * Accepts a static string or a sync/async factory called on every request —\n * useful when the origin depends on runtime environment (e.g. SSR vs CSR).\n * @param options.defaultHeaders - Async factory called on every request to\n * produce headers (e.g. reading cookies in a Next.js server component).\n * @param options.plugins - Plugins applied around the fetch call. Each retry\n * attempt re-runs `onRequest` hooks (so headers are refreshed per attempt)\n * and `onError` hooks. Token-refresh-on-401 patterns work by updating an\n * external token store in `onError` and letting `onRequest` pick it up on\n * the next attempt.\n * @param options.retry - Number of retries, or `{ count, shouldRetry? }`.\n * @param options.timeout - Per-attempt timeout in milliseconds.\n *\n * @example Minimal — no options needed\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com');\n * ```\n *\n * @example Dynamic base URL for SSR/CSR\n * ```ts\n * const executor = createFetchExecutor(\n * () => typeof window === 'undefined' ? 'http://localhost:3000/api' : '/api',\n * );\n * ```\n *\n * @example SSR with bearer token\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * defaultHeaders: async () => {\n * const token = await getServerToken();\n * return token ? { Authorization: `Bearer ${token}` } : {};\n * },\n * });\n * ```\n *\n * @example With retry and timeout\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * retry: 2,\n * timeout: 8_000,\n * });\n * ```\n */\nexport function createFetchExecutor(\n baseURL: string | (() => string | Promise<string>),\n options?: FetchExecutorOptions,\n): Executor {\n const transport = async ({\n method,\n url,\n params,\n body,\n headers,\n signal,\n }: ExecuteOptions) => {\n const resolvedBase = typeof baseURL === \"function\" ? await baseURL() : baseURL;\n const fullURL = new URL(resolvedBase.replace(/\\/$/, \"\") + url);\n if (params) {\n serializeParams(params).forEach((v, k) => {\n fullURL.searchParams.set(k, v);\n });\n }\n\n const defaultHeaders = (await options?.defaultHeaders?.()) ?? {};\n\n const res = await fetch(fullURL.toString(), {\n method,\n headers: {\n ...defaultHeaders,\n ...headers,\n ...(body != null ? { \"Content-Type\": \"application/json\" } : {}),\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal,\n });\n\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new HttpError(res.status, res.statusText, errorBody, { url: fullURL.toString(), method });\n }\n if (res.status === 204 || res.status === 205 || res.status === 304) {\n return null;\n }\n const text = await res.text();\n return text === \"\" ? null : JSON.parse(text);\n };\n\n const executor = createExecutor(transport, { plugins: options?.plugins, unwrap: options?.unwrap });\n return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };\n}\n\n/**\n * Thrown by {@link createFetchExecutor} when the server returns a non-2xx\n * status code. The Axios and ky executors also normalize their transport\n * errors to `HttpError`, so `onError` plugins and callers can branch on a\n * single error type regardless of the underlying transport. The original\n * transport error (e.g. an `AxiosError` or ky `HTTPError`) is preserved on\n * the `cause` property.\n *\n * @example\n * ```ts\n * try {\n * await api.getDetail({ path: { id: 999 } });\n * } catch (err) {\n * if (err instanceof HttpError && err.status === 404) {\n * // handle not-found\n * }\n * }\n * ```\n */\nexport class HttpError extends Error {\n public readonly url?: string;\n public readonly method?: string;\n\n constructor(\n public readonly status: number,\n public readonly statusText: string,\n public readonly body: unknown = null,\n options?: { url?: string; method?: string; cause?: unknown },\n ) {\n super(`HTTP ${status}: ${statusText}`, { cause: options?.cause });\n this.name = \"HttpError\";\n this.url = options?.url;\n this.method = options?.method;\n }\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from \"./types.js\";\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * The literal HTTP method (`'GET'`, `'POST'`, …) is preserved on the return\n * type — `endpoint({ method: 'GET', ... }).method` is typed `'GET'`, not the\n * `HttpMethod` union.\n *\n * @example Basic GET with no params\n * ```ts\n * const getList = endpoint({ method: 'GET', path: '/', response: z.array(TodoSchema) });\n * ```\n *\n * @example GET with query params\n * ```ts\n * const search = endpoint({\n * method: 'GET',\n * path: '/search',\n * request: z.object({ query: z.object({ q: z.string(), limit: z.number().optional() }) }),\n * response: z.array(TodoSchema),\n * });\n * ```\n *\n * @example POST with body\n * ```ts\n * const create = endpoint({\n * method: 'POST',\n * path: '/',\n * request: z.object({ body: z.object({ title: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n *\n * @example Adapter — raw is inferred from the response schema, no cast needed\n * ```ts\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoRawSchema,\n * adapter: (raw) => ({ ...raw, label: `#${raw.id} ${raw.title}` }),\n * });\n * ```\n *\n * @example Path param enforcement\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -50,7 +50,17 @@ interface ExecutorPlugin {
|
|
|
50
50
|
onRequest?: (opts: ExecuteOptions) => ExecuteOptions | Promise<ExecuteOptions>;
|
|
51
51
|
/** Runs after a successful response. Return a modified value to transform the response. */
|
|
52
52
|
onResponse?: (response: unknown, opts: ExecuteOptions) => unknown | Promise<unknown>;
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Runs when the request throws.
|
|
55
|
+
*
|
|
56
|
+
* The return value is ignored — this hook MUST always throw. To transform
|
|
57
|
+
* the error, throw a new error from within the hook (the original error is
|
|
58
|
+
* not automatically re-thrown for you).
|
|
59
|
+
*
|
|
60
|
+
* Transport errors reaching this hook are normalized to `HttpError` across
|
|
61
|
+
* all executors (fetch, Axios, ky), so you can branch on `instanceof
|
|
62
|
+
* HttpError` without depending on the underlying client.
|
|
63
|
+
*/
|
|
54
64
|
onError?: (error: unknown, opts: ExecuteOptions) => never | Promise<never>;
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
@@ -66,6 +76,21 @@ interface ExecutorPlugin {
|
|
|
66
76
|
interface CreateExecutorOptions {
|
|
67
77
|
/** Plugins applied in declaration order (first plugin is outermost). */
|
|
68
78
|
plugins?: ExecutorPlugin[];
|
|
79
|
+
/**
|
|
80
|
+
* Transforms the raw response immediately after the transport returns,
|
|
81
|
+
* before any plugin `onResponse` hooks and before schema validation in
|
|
82
|
+
* `createApi`. Use to unwrap envelope shapes like `{ data: T }`.
|
|
83
|
+
*
|
|
84
|
+
* Equivalent to an innermost `onResponse` plugin, but declarative.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const executor = createExecutor(transport, {
|
|
89
|
+
* unwrap: (raw) => (raw as { data: unknown })?.data ?? raw,
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
unwrap?: (raw: unknown) => unknown;
|
|
69
94
|
}
|
|
70
95
|
/**
|
|
71
96
|
* Any object with a `parse` method — compatible with Zod, Valibot, Yup, etc.
|
|
@@ -283,6 +308,13 @@ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executo
|
|
|
283
308
|
* plugins: [authPlugin, logger()],
|
|
284
309
|
* });
|
|
285
310
|
* ```
|
|
311
|
+
*
|
|
312
|
+
* @example Unwrap an envelope response
|
|
313
|
+
* ```ts
|
|
314
|
+
* const executor = createExecutor(transport, {
|
|
315
|
+
* unwrap: (raw) => (raw as { data: unknown })?.data ?? raw,
|
|
316
|
+
* });
|
|
317
|
+
* ```
|
|
286
318
|
*/
|
|
287
319
|
declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, options?: CreateExecutorOptions): Executor;
|
|
288
320
|
/**
|
|
@@ -320,6 +352,8 @@ interface FetchExecutorOptions extends CreateExecutorOptions {
|
|
|
320
352
|
* - Non-2xx responses throw an {@link HttpError}.
|
|
321
353
|
*
|
|
322
354
|
* @param baseURL - Absolute base URL prepended to every endpoint path.
|
|
355
|
+
* Accepts a static string or a sync/async factory called on every request —
|
|
356
|
+
* useful when the origin depends on runtime environment (e.g. SSR vs CSR).
|
|
323
357
|
* @param options.defaultHeaders - Async factory called on every request to
|
|
324
358
|
* produce headers (e.g. reading cookies in a Next.js server component).
|
|
325
359
|
* @param options.plugins - Plugins applied around the fetch call. Each retry
|
|
@@ -335,6 +369,13 @@ interface FetchExecutorOptions extends CreateExecutorOptions {
|
|
|
335
369
|
* const executor = createFetchExecutor('https://api.example.com');
|
|
336
370
|
* ```
|
|
337
371
|
*
|
|
372
|
+
* @example Dynamic base URL for SSR/CSR
|
|
373
|
+
* ```ts
|
|
374
|
+
* const executor = createFetchExecutor(
|
|
375
|
+
* () => typeof window === 'undefined' ? 'http://localhost:3000/api' : '/api',
|
|
376
|
+
* );
|
|
377
|
+
* ```
|
|
378
|
+
*
|
|
338
379
|
* @example SSR with bearer token
|
|
339
380
|
* ```ts
|
|
340
381
|
* const executor = createFetchExecutor('https://api.example.com', {
|
|
@@ -353,10 +394,14 @@ interface FetchExecutorOptions extends CreateExecutorOptions {
|
|
|
353
394
|
* });
|
|
354
395
|
* ```
|
|
355
396
|
*/
|
|
356
|
-
declare function createFetchExecutor(baseURL: string, options?: FetchExecutorOptions): Executor;
|
|
397
|
+
declare function createFetchExecutor(baseURL: string | (() => string | Promise<string>), options?: FetchExecutorOptions): Executor;
|
|
357
398
|
/**
|
|
358
399
|
* Thrown by {@link createFetchExecutor} when the server returns a non-2xx
|
|
359
|
-
* status code.
|
|
400
|
+
* status code. The Axios and ky executors also normalize their transport
|
|
401
|
+
* errors to `HttpError`, so `onError` plugins and callers can branch on a
|
|
402
|
+
* single error type regardless of the underlying transport. The original
|
|
403
|
+
* transport error (e.g. an `AxiosError` or ky `HTTPError`) is preserved on
|
|
404
|
+
* the `cause` property.
|
|
360
405
|
*
|
|
361
406
|
* @example
|
|
362
407
|
* ```ts
|
|
@@ -373,7 +418,13 @@ declare class HttpError extends Error {
|
|
|
373
418
|
readonly status: number;
|
|
374
419
|
readonly statusText: string;
|
|
375
420
|
readonly body: unknown;
|
|
376
|
-
|
|
421
|
+
readonly url?: string;
|
|
422
|
+
readonly method?: string;
|
|
423
|
+
constructor(status: number, statusText: string, body?: unknown, options?: {
|
|
424
|
+
url?: string;
|
|
425
|
+
method?: string;
|
|
426
|
+
cause?: unknown;
|
|
427
|
+
});
|
|
377
428
|
}
|
|
378
429
|
|
|
379
430
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -50,7 +50,17 @@ interface ExecutorPlugin {
|
|
|
50
50
|
onRequest?: (opts: ExecuteOptions) => ExecuteOptions | Promise<ExecuteOptions>;
|
|
51
51
|
/** Runs after a successful response. Return a modified value to transform the response. */
|
|
52
52
|
onResponse?: (response: unknown, opts: ExecuteOptions) => unknown | Promise<unknown>;
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Runs when the request throws.
|
|
55
|
+
*
|
|
56
|
+
* The return value is ignored — this hook MUST always throw. To transform
|
|
57
|
+
* the error, throw a new error from within the hook (the original error is
|
|
58
|
+
* not automatically re-thrown for you).
|
|
59
|
+
*
|
|
60
|
+
* Transport errors reaching this hook are normalized to `HttpError` across
|
|
61
|
+
* all executors (fetch, Axios, ky), so you can branch on `instanceof
|
|
62
|
+
* HttpError` without depending on the underlying client.
|
|
63
|
+
*/
|
|
54
64
|
onError?: (error: unknown, opts: ExecuteOptions) => never | Promise<never>;
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
@@ -66,6 +76,21 @@ interface ExecutorPlugin {
|
|
|
66
76
|
interface CreateExecutorOptions {
|
|
67
77
|
/** Plugins applied in declaration order (first plugin is outermost). */
|
|
68
78
|
plugins?: ExecutorPlugin[];
|
|
79
|
+
/**
|
|
80
|
+
* Transforms the raw response immediately after the transport returns,
|
|
81
|
+
* before any plugin `onResponse` hooks and before schema validation in
|
|
82
|
+
* `createApi`. Use to unwrap envelope shapes like `{ data: T }`.
|
|
83
|
+
*
|
|
84
|
+
* Equivalent to an innermost `onResponse` plugin, but declarative.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const executor = createExecutor(transport, {
|
|
89
|
+
* unwrap: (raw) => (raw as { data: unknown })?.data ?? raw,
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
unwrap?: (raw: unknown) => unknown;
|
|
69
94
|
}
|
|
70
95
|
/**
|
|
71
96
|
* Any object with a `parse` method — compatible with Zod, Valibot, Yup, etc.
|
|
@@ -283,6 +308,13 @@ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executo
|
|
|
283
308
|
* plugins: [authPlugin, logger()],
|
|
284
309
|
* });
|
|
285
310
|
* ```
|
|
311
|
+
*
|
|
312
|
+
* @example Unwrap an envelope response
|
|
313
|
+
* ```ts
|
|
314
|
+
* const executor = createExecutor(transport, {
|
|
315
|
+
* unwrap: (raw) => (raw as { data: unknown })?.data ?? raw,
|
|
316
|
+
* });
|
|
317
|
+
* ```
|
|
286
318
|
*/
|
|
287
319
|
declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, options?: CreateExecutorOptions): Executor;
|
|
288
320
|
/**
|
|
@@ -320,6 +352,8 @@ interface FetchExecutorOptions extends CreateExecutorOptions {
|
|
|
320
352
|
* - Non-2xx responses throw an {@link HttpError}.
|
|
321
353
|
*
|
|
322
354
|
* @param baseURL - Absolute base URL prepended to every endpoint path.
|
|
355
|
+
* Accepts a static string or a sync/async factory called on every request —
|
|
356
|
+
* useful when the origin depends on runtime environment (e.g. SSR vs CSR).
|
|
323
357
|
* @param options.defaultHeaders - Async factory called on every request to
|
|
324
358
|
* produce headers (e.g. reading cookies in a Next.js server component).
|
|
325
359
|
* @param options.plugins - Plugins applied around the fetch call. Each retry
|
|
@@ -335,6 +369,13 @@ interface FetchExecutorOptions extends CreateExecutorOptions {
|
|
|
335
369
|
* const executor = createFetchExecutor('https://api.example.com');
|
|
336
370
|
* ```
|
|
337
371
|
*
|
|
372
|
+
* @example Dynamic base URL for SSR/CSR
|
|
373
|
+
* ```ts
|
|
374
|
+
* const executor = createFetchExecutor(
|
|
375
|
+
* () => typeof window === 'undefined' ? 'http://localhost:3000/api' : '/api',
|
|
376
|
+
* );
|
|
377
|
+
* ```
|
|
378
|
+
*
|
|
338
379
|
* @example SSR with bearer token
|
|
339
380
|
* ```ts
|
|
340
381
|
* const executor = createFetchExecutor('https://api.example.com', {
|
|
@@ -353,10 +394,14 @@ interface FetchExecutorOptions extends CreateExecutorOptions {
|
|
|
353
394
|
* });
|
|
354
395
|
* ```
|
|
355
396
|
*/
|
|
356
|
-
declare function createFetchExecutor(baseURL: string, options?: FetchExecutorOptions): Executor;
|
|
397
|
+
declare function createFetchExecutor(baseURL: string | (() => string | Promise<string>), options?: FetchExecutorOptions): Executor;
|
|
357
398
|
/**
|
|
358
399
|
* Thrown by {@link createFetchExecutor} when the server returns a non-2xx
|
|
359
|
-
* status code.
|
|
400
|
+
* status code. The Axios and ky executors also normalize their transport
|
|
401
|
+
* errors to `HttpError`, so `onError` plugins and callers can branch on a
|
|
402
|
+
* single error type regardless of the underlying transport. The original
|
|
403
|
+
* transport error (e.g. an `AxiosError` or ky `HTTPError`) is preserved on
|
|
404
|
+
* the `cause` property.
|
|
360
405
|
*
|
|
361
406
|
* @example
|
|
362
407
|
* ```ts
|
|
@@ -373,7 +418,13 @@ declare class HttpError extends Error {
|
|
|
373
418
|
readonly status: number;
|
|
374
419
|
readonly statusText: string;
|
|
375
420
|
readonly body: unknown;
|
|
376
|
-
|
|
421
|
+
readonly url?: string;
|
|
422
|
+
readonly method?: string;
|
|
423
|
+
constructor(status: number, statusText: string, body?: unknown, options?: {
|
|
424
|
+
url?: string;
|
|
425
|
+
method?: string;
|
|
426
|
+
cause?: unknown;
|
|
427
|
+
});
|
|
377
428
|
}
|
|
378
429
|
|
|
379
430
|
/**
|
package/dist/index.js
CHANGED
|
@@ -154,7 +154,12 @@ function buildChain(execute, middlewares) {
|
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
156
|
function createExecutor(execute, options = {}) {
|
|
157
|
-
const
|
|
157
|
+
const plugins = [...options.plugins ?? []];
|
|
158
|
+
if (options.unwrap) {
|
|
159
|
+
const unwrapFn = options.unwrap;
|
|
160
|
+
plugins.push({ onResponse: (raw) => unwrapFn(raw) });
|
|
161
|
+
}
|
|
162
|
+
const middlewares = plugins.map(pluginToMiddleware);
|
|
158
163
|
return { execute: buildChain(execute, middlewares) };
|
|
159
164
|
}
|
|
160
165
|
function dispatchExecutor(resolver) {
|
|
@@ -293,7 +298,8 @@ function createFetchExecutor(baseURL, options) {
|
|
|
293
298
|
headers,
|
|
294
299
|
signal
|
|
295
300
|
}) => {
|
|
296
|
-
const
|
|
301
|
+
const resolvedBase = typeof baseURL === "function" ? await baseURL() : baseURL;
|
|
302
|
+
const fullURL = new URL(resolvedBase.replace(/\/$/, "") + url);
|
|
297
303
|
if (params) {
|
|
298
304
|
serializeParams(params).forEach((v, k) => {
|
|
299
305
|
fullURL.searchParams.set(k, v);
|
|
@@ -312,7 +318,7 @@ function createFetchExecutor(baseURL, options) {
|
|
|
312
318
|
});
|
|
313
319
|
if (!res.ok) {
|
|
314
320
|
const errorBody = await res.json().catch(() => null);
|
|
315
|
-
throw new HttpError(res.status, res.statusText, errorBody);
|
|
321
|
+
throw new HttpError(res.status, res.statusText, errorBody, { url: fullURL.toString(), method });
|
|
316
322
|
}
|
|
317
323
|
if (res.status === 204 || res.status === 205 || res.status === 304) {
|
|
318
324
|
return null;
|
|
@@ -320,16 +326,18 @@ function createFetchExecutor(baseURL, options) {
|
|
|
320
326
|
const text = await res.text();
|
|
321
327
|
return text === "" ? null : JSON.parse(text);
|
|
322
328
|
};
|
|
323
|
-
const executor = createExecutor(transport, { plugins: options?.plugins });
|
|
329
|
+
const executor = createExecutor(transport, { plugins: options?.plugins, unwrap: options?.unwrap });
|
|
324
330
|
return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };
|
|
325
331
|
}
|
|
326
332
|
var HttpError = class extends Error {
|
|
327
|
-
constructor(status, statusText, body = null) {
|
|
328
|
-
super(`HTTP ${status}: ${statusText}
|
|
333
|
+
constructor(status, statusText, body = null, options) {
|
|
334
|
+
super(`HTTP ${status}: ${statusText}`, { cause: options?.cause });
|
|
329
335
|
this.status = status;
|
|
330
336
|
this.statusText = statusText;
|
|
331
337
|
this.body = body;
|
|
332
338
|
this.name = "HttpError";
|
|
339
|
+
this.url = options?.url;
|
|
340
|
+
this.method = options?.method;
|
|
333
341
|
}
|
|
334
342
|
};
|
|
335
343
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/define-router.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/middleware.ts","../src/utils/params.ts","../src/create-fetch-executor.ts","../src/define-endpoint.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { RouterDef, RouterEndpoints } from \"./types.js\";\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\n/** Type guard — distinguishes a {@link RouterDef} from a leaf {@link EndpointSpec} at runtime. */\nexport function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints> {\n return \"prefix\" in entry && \"endpoints\" in entry;\n}\n\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","/**\n * Joins URL path segments, normalising repeated slashes and trailing slashes.\n *\n * **Note:** Intended for relative API paths only. Absolute URLs containing\n * `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs\n * directly to the executor instead of through this helper.\n */\nexport function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter((s) => s !== \"\")\n .join(\"/\")\n .replace(/\\/+/g, \"/\");\n return joined.endsWith(\"/\") && joined.length > 1\n ? joined.slice(0, -1)\n : joined || \"/\";\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null || value === \"\") throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n readonly cause?: unknown;\n\n constructor(message: string, cause?: unknown) {\n super(message);\n this.name = \"ValidationError\";\n if (cause !== undefined) {\n Object.defineProperty(this, \"cause\", {\n value: cause,\n writable: false,\n enumerable: false,\n configurable: true,\n });\n }\n }\n}\n","import { isRouterDef } from \"./define-router.js\";\nimport type {\n CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\n ValidatorOutput,\n} from \"./types.js\";\nimport { joinPaths, resolvePath } from \"./utils/path.js\";\nimport { ValidationError } from \"./utils/validate.js\";\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> =\n TSpec[\"request\"] extends { parse: (data: unknown) => infer R }\n ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>>\n : (\n params?: RequestShape,\n signal?: AbortSignal,\n ) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\nexport type ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<\n infer TNestedEndpoints\n >\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * An {@link ApiClient} that also carries its source {@link RouterDef} on the\n * `$router` property. This is the actual return type of {@link createApi};\n * downstream tools (e.g. `@routar/react-query`) recover the router (prefix +\n * endpoint methods) from it without it being re-passed. `$router` is\n * non-enumerable and excluded from {@link ApiTypes}; the `$` prefix keeps it\n * from colliding with endpoint names.\n */\nexport type ApiClientWithRouter<TEndpoints extends RouterEndpoints> =\n ApiClient<TEndpoints> & { readonly $router: RouterDef<TEndpoints> };\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).\n *\n * @example Basic usage\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n * const next = await todoApi.create({ body: { title: 'buy milk' } });\n * ```\n *\n * @example Nested router — access via dot notation\n * ```ts\n * const api = createApi(executor, apiRouter); // apiRouter has users → todos nesting\n * await api.users.getList({});\n * await api.users.todos.getList({});\n * ```\n *\n * @example Cancel in-flight requests with AbortSignal\n * ```ts\n * const controller = new AbortController();\n * const todos = await todoApi.getList({}, controller.signal);\n * controller.abort();\n * ```\n *\n * @example Extract types from the client — no duplication\n * ```ts\n * import type { ApiTypes } from '@routar/core';\n * type TodoApiTypes = ApiTypes<typeof todoApi>;\n * type Todo = TodoApiTypes['getDetail']['response'];\n * type CreateRequest = TodoApiTypes['create']['request'];\n * ```\n *\n * @example Skip response validation in production\n * ```ts\n * const prodApi = createApi(executor, todoRouter, {\n * validate: { request: true, response: process.env.NODE_ENV !== 'production' },\n * });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints:\n | RouterDef<RouterEndpoints>\n | RouterEndpoints\n | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n const { prefix, endpoints, options } = resolveArgs(\n routerOrPrefixOrEndpoints,\n endpointsArgOrOptions,\n optionsArg,\n );\n const client = buildClient(executor, prefix, endpoints, options);\n // Stash the source router so downstream tools (e.g. @routar/react-query)\n // recover prefix + endpoint methods without it being re-passed.\n Object.defineProperty(client, \"$router\", {\n value: { prefix, endpoints } satisfies RouterDef<RouterEndpoints>,\n enumerable: false,\n });\n return client;\n}\n\nfunction resolveArgs(\n second: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n third: RouterEndpoints | CreateApiOptions | undefined,\n fourth: CreateApiOptions | undefined,\n): {\n prefix: string;\n endpoints: RouterEndpoints;\n options: CreateApiOptions | undefined;\n} {\n if (typeof second === \"string\") {\n if (!third)\n throw new Error(\"endpoints is required when prefix is provided\");\n return {\n prefix: second,\n endpoints: third as RouterEndpoints,\n options: fourth,\n };\n }\n if (isRouterDef(second)) {\n return {\n prefix: second.prefix,\n endpoints: second.endpoints,\n options: third as CreateApiOptions | undefined,\n };\n }\n return {\n prefix: \"\",\n endpoints: second as RouterEndpoints,\n options: third as CreateApiOptions | undefined,\n };\n}\n\nfunction shouldValidate(\n options: CreateApiOptions | undefined,\n kind: \"request\" | \"response\",\n): boolean {\n const v = options?.validate;\n if (v === undefined || v === true) return true;\n if (v === false) return false;\n return v[kind] ?? true;\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n options?: CreateApiOptions,\n): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n client[key] = isRouterDef(entry)\n ? buildClient(\n executor,\n joinPaths(prefix, entry.prefix),\n entry.endpoints,\n options,\n )\n : buildEndpointFn(\n executor,\n prefix,\n entry as EndpointSpec<any, any, any>,\n options,\n );\n }\n\n return client;\n}\n\nfunction buildEndpointFn(\n executor: Executor,\n prefix: string,\n spec: EndpointSpec<any, any, any>,\n options: CreateApiOptions | undefined,\n) {\n return async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request && shouldValidate(options, \"request\")) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError(\"Request validation failed\", err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let result: ValidatorOutput<typeof spec.response>;\n if (shouldValidate(options, \"response\")) {\n try {\n result = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError(\"Response validation failed\", err);\n }\n } else {\n result = raw;\n }\n\n return spec.adapter ? spec.adapter(result) : result;\n };\n}\n","import type {\n CreateExecutorOptions,\n ExecuteOptions,\n Executor,\n ExecutorMiddleware,\n ExecutorPlugin,\n} from \"./types.js\";\n\nfunction pluginToMiddleware(plugin: ExecutorPlugin): ExecutorMiddleware {\n return async (opts, next) => {\n const resolvedOpts = plugin.onRequest ? await plugin.onRequest(opts) : opts;\n try {\n const response = await next(resolvedOpts);\n return plugin.onResponse\n ? await plugin.onResponse(response, resolvedOpts)\n : response;\n } catch (err) {\n if (plugin.onError) {\n await plugin.onError(err, resolvedOpts);\n throw err;\n }\n throw err;\n }\n };\n}\n\nexport function buildChain(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[],\n): (options: ExecuteOptions) => Promise<unknown> {\n return middlewares.reduceRight<(options: ExecuteOptions) => Promise<unknown>>(\n (next, mw) => (opts) => mw(opts, next),\n execute,\n );\n}\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with plugins.\n *\n * Plugins run in declaration order (first plugin is outermost).\n *\n * For `retry` and `timeout`, use the options on {@link createFetchExecutor}\n * or configure them via the underlying HTTP client (axios, ky).\n *\n * @example\n * ```ts\n * const executor = createExecutor(transport, {\n * plugins: [authPlugin, logger()],\n * });\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n options: CreateExecutorOptions = {},\n): Executor {\n const middlewares = (options.plugins ?? []).map(pluginToMiddleware);\n return { execute: buildChain(execute, middlewares) };\n}\n\n/**\n * Creates an {@link Executor} that selects the underlying transport at\n * request time based on the result of `resolver`.\n *\n * @example\n * ```ts\n * const apiExecutor = dispatchExecutor(() =>\n * typeof window === 'undefined' ? serverExecutor : clientExecutor,\n * );\n * ```\n */\nexport function dispatchExecutor(\n resolver: (opts: ExecuteOptions) => Executor,\n): Executor {\n return {\n execute: (opts) => resolver(opts).execute(opts),\n };\n}\n","import type { ExecuteOptions, ExecutorMiddleware, ExecutorPlugin } from \"./types.js\";\n\n/**\n * Thrown by the built-in `timeout` option when a request exceeds the\n * configured duration. Distinguishable from a user-initiated\n * {@link AbortSignal} cancellation.\n */\nexport class TimeoutError extends Error {\n constructor(public readonly ms: number) {\n super(`Request timed out after ${ms}ms`);\n this.name = \"TimeoutError\";\n }\n}\n\n/**\n * Identity helper that returns the plugin as-is, providing full type inference.\n *\n * @example\n * ```ts\n * const authPlugin = definePlugin({\n * name: 'auth',\n * onRequest: async (opts) => ({\n * ...opts,\n * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },\n * }),\n * });\n * ```\n */\nexport function definePlugin(plugin: ExecutorPlugin): ExecutorPlugin {\n return plugin;\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * createExecutor(transport, {\n * plugins: [logger({ log: (msg, data) => myLogger.debug(msg, data) })],\n * })\n * ```\n */\nexport function logger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorPlugin {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n const timings = new WeakMap<ExecuteOptions, number>();\n return definePlugin({\n name: \"logger\",\n onRequest: (opts) => {\n timings.set(opts, Date.now());\n log(`[routar] ${opts.method} ${opts.url}`, {\n params: opts.params,\n body: opts.body,\n });\n return opts;\n },\n onResponse: (res, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n );\n timings.delete(opts);\n return res;\n },\n onError: (err, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n err,\n );\n timings.delete(opts);\n throw err as never;\n },\n });\n}\n\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n };\n}\n\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);\n\n const { signal, cleanup } = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : { signal: controller.signal, cleanup: () => {} };\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n cleanup();\n }\n };\n}\n\nfunction anySignal(signals: AbortSignal[]): {\n signal: AbortSignal;\n cleanup: () => void;\n} {\n const controller = new AbortController();\n const onAbort = () => controller.abort();\n const attached: AbortSignal[] = [];\n for (const s of signals) {\n if (s.aborted) {\n controller.abort();\n break;\n }\n s.addEventListener(\"abort\", onAbort, { once: true });\n attached.push(s);\n }\n return {\n signal: controller.signal,\n cleanup: () =>\n attached.forEach((s) => {\n s.removeEventListener(\"abort\", onAbort);\n }),\n };\n}\n","export function serializeParams(\n params: Record<string, unknown>,\n): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else if (typeof value === \"object\") {\n throw new TypeError(\n `serializeParams: value for key \"${key}\" is a plain object. Serialize it to a string before passing as a query parameter.`,\n );\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n","import type { CreateExecutorOptions, ExecuteOptions, Executor, ExecutorMiddleware } from \"./types.js\";\nimport { buildChain, createExecutor } from \"./create-executor.js\";\nimport { withRetry, withTimeout } from \"./middleware.js\";\nimport { serializeParams } from \"./utils/params.js\";\n\nexport type FetchRetryOption =\n | number\n | { count: number; shouldRetry?: (error: unknown, attempt: number) => boolean };\n\nexport interface FetchExecutorOptions extends CreateExecutorOptions {\n defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n retry?: FetchRetryOption;\n timeout?: number;\n}\n\nfunction buildFetchChain(\n transport: (opts: ExecuteOptions) => Promise<unknown>,\n retry?: FetchRetryOption,\n timeout?: number,\n): (opts: ExecuteOptions) => Promise<unknown> {\n const middlewares: ExecutorMiddleware[] = [\n ...(retry != null\n ? [\n typeof retry === \"number\"\n ? withRetry(retry)\n : withRetry(retry.count, { shouldRetry: retry.shouldRetry }),\n ]\n : []),\n ...(timeout != null ? [withTimeout(timeout)] : []),\n ];\n if (middlewares.length === 0) return transport;\n return buildChain(transport, middlewares);\n}\n\n/**\n * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.\n *\n * Suited for SSR environments where you need per-request dynamic headers\n * (e.g. forwarding auth cookies) without sharing state across requests.\n *\n * - Query params are serialized and appended to the URL.\n * - A `Content-Type: application/json` header is added automatically when\n * a request body is present.\n * - Responses with status 204 or `Content-Length: 0` resolve to `null`.\n * - Non-2xx responses throw an {@link HttpError}.\n *\n * @param baseURL - Absolute base URL prepended to every endpoint path.\n * @param options.defaultHeaders - Async factory called on every request to\n * produce headers (e.g. reading cookies in a Next.js server component).\n * @param options.plugins - Plugins applied around the fetch call. Each retry\n * attempt re-runs `onRequest` hooks (so headers are refreshed per attempt)\n * and `onError` hooks. Token-refresh-on-401 patterns work by updating an\n * external token store in `onError` and letting `onRequest` pick it up on\n * the next attempt.\n * @param options.retry - Number of retries, or `{ count, shouldRetry? }`.\n * @param options.timeout - Per-attempt timeout in milliseconds.\n *\n * @example Minimal — no options needed\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com');\n * ```\n *\n * @example SSR with bearer token\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * defaultHeaders: async () => {\n * const token = await getServerToken();\n * return token ? { Authorization: `Bearer ${token}` } : {};\n * },\n * });\n * ```\n *\n * @example With retry and timeout\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * retry: 2,\n * timeout: 8_000,\n * });\n * ```\n */\nexport function createFetchExecutor(\n baseURL: string,\n options?: FetchExecutorOptions,\n): Executor {\n const transport = async ({\n method,\n url,\n params,\n body,\n headers,\n signal,\n }: ExecuteOptions) => {\n const fullURL = new URL(baseURL.replace(/\\/$/, \"\") + url);\n if (params) {\n serializeParams(params).forEach((v, k) => {\n fullURL.searchParams.set(k, v);\n });\n }\n\n const defaultHeaders = (await options?.defaultHeaders?.()) ?? {};\n\n const res = await fetch(fullURL.toString(), {\n method,\n headers: {\n ...defaultHeaders,\n ...headers,\n ...(body != null ? { \"Content-Type\": \"application/json\" } : {}),\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal,\n });\n\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new HttpError(res.status, res.statusText, errorBody);\n }\n if (res.status === 204 || res.status === 205 || res.status === 304) {\n return null;\n }\n const text = await res.text();\n return text === \"\" ? null : JSON.parse(text);\n };\n\n const executor = createExecutor(transport, { plugins: options?.plugins });\n return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };\n}\n\n/**\n * Thrown by {@link createFetchExecutor} when the server returns a non-2xx\n * status code.\n *\n * @example\n * ```ts\n * try {\n * await api.getDetail({ path: { id: 999 } });\n * } catch (err) {\n * if (err instanceof HttpError && err.status === 404) {\n * // handle not-found\n * }\n * }\n * ```\n */\nexport class HttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly statusText: string,\n public readonly body: unknown = null,\n ) {\n super(`HTTP ${status}: ${statusText}`);\n this.name = \"HttpError\";\n }\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from \"./types.js\";\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * The literal HTTP method (`'GET'`, `'POST'`, …) is preserved on the return\n * type — `endpoint({ method: 'GET', ... }).method` is typed `'GET'`, not the\n * `HttpMethod` union.\n *\n * @example Basic GET with no params\n * ```ts\n * const getList = endpoint({ method: 'GET', path: '/', response: z.array(TodoSchema) });\n * ```\n *\n * @example GET with query params\n * ```ts\n * const search = endpoint({\n * method: 'GET',\n * path: '/search',\n * request: z.object({ query: z.object({ q: z.string(), limit: z.number().optional() }) }),\n * response: z.array(TodoSchema),\n * });\n * ```\n *\n * @example POST with body\n * ```ts\n * const create = endpoint({\n * method: 'POST',\n * path: '/',\n * request: z.object({ body: z.object({ title: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n *\n * @example Adapter — raw is inferred from the response schema, no cast needed\n * ```ts\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoRawSchema,\n * adapter: (raw) => ({ ...raw, label: `#${raw.id} ${raw.title}` }),\n * });\n * ```\n *\n * @example Path param enforcement\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/define-router.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/middleware.ts","../src/utils/params.ts","../src/create-fetch-executor.ts","../src/define-endpoint.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { RouterDef, RouterEndpoints } from \"./types.js\";\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\n/** Type guard — distinguishes a {@link RouterDef} from a leaf {@link EndpointSpec} at runtime. */\nexport function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints> {\n return \"prefix\" in entry && \"endpoints\" in entry;\n}\n\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","/**\n * Joins URL path segments, normalising repeated slashes and trailing slashes.\n *\n * **Note:** Intended for relative API paths only. Absolute URLs containing\n * `://` will be collapsed (`https://` → `https:/`). Pass absolute URLs\n * directly to the executor instead of through this helper.\n */\nexport function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter((s) => s !== \"\")\n .join(\"/\")\n .replace(/\\/+/g, \"/\");\n return joined.endsWith(\"/\") && joined.length > 1\n ? joined.slice(0, -1)\n : joined || \"/\";\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null || value === \"\") throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n readonly cause?: unknown;\n\n constructor(message: string, cause?: unknown) {\n super(message);\n this.name = \"ValidationError\";\n if (cause !== undefined) {\n Object.defineProperty(this, \"cause\", {\n value: cause,\n writable: false,\n enumerable: false,\n configurable: true,\n });\n }\n }\n}\n","import { isRouterDef } from \"./define-router.js\";\nimport type {\n CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\n ValidatorOutput,\n} from \"./types.js\";\nimport { joinPaths, resolvePath } from \"./utils/path.js\";\nimport { ValidationError } from \"./utils/validate.js\";\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> =\n TSpec[\"request\"] extends { parse: (data: unknown) => infer R }\n ? (params: R, signal?: AbortSignal) => Promise<InferResponse<TSpec>>\n : (\n params?: RequestShape,\n signal?: AbortSignal,\n ) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\nexport type ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<\n infer TNestedEndpoints\n >\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * An {@link ApiClient} that also carries its source {@link RouterDef} on the\n * `$router` property. This is the actual return type of {@link createApi};\n * downstream tools (e.g. `@routar/react-query`) recover the router (prefix +\n * endpoint methods) from it without it being re-passed. `$router` is\n * non-enumerable and excluded from {@link ApiTypes}; the `$` prefix keeps it\n * from colliding with endpoint names.\n */\nexport type ApiClientWithRouter<TEndpoints extends RouterEndpoints> =\n ApiClient<TEndpoints> & { readonly $router: RouterDef<TEndpoints> };\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).\n *\n * @example Basic usage\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n * const next = await todoApi.create({ body: { title: 'buy milk' } });\n * ```\n *\n * @example Nested router — access via dot notation\n * ```ts\n * const api = createApi(executor, apiRouter); // apiRouter has users → todos nesting\n * await api.users.getList({});\n * await api.users.todos.getList({});\n * ```\n *\n * @example Cancel in-flight requests with AbortSignal\n * ```ts\n * const controller = new AbortController();\n * const todos = await todoApi.getList({}, controller.signal);\n * controller.abort();\n * ```\n *\n * @example Extract types from the client — no duplication\n * ```ts\n * import type { ApiTypes } from '@routar/core';\n * type TodoApiTypes = ApiTypes<typeof todoApi>;\n * type Todo = TodoApiTypes['getDetail']['response'];\n * type CreateRequest = TodoApiTypes['create']['request'];\n * ```\n *\n * @example Skip response validation in production\n * ```ts\n * const prodApi = createApi(executor, todoRouter, {\n * validate: { request: true, response: process.env.NODE_ENV !== 'production' },\n * });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClientWithRouter<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints:\n | RouterDef<RouterEndpoints>\n | RouterEndpoints\n | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n const { prefix, endpoints, options } = resolveArgs(\n routerOrPrefixOrEndpoints,\n endpointsArgOrOptions,\n optionsArg,\n );\n const client = buildClient(executor, prefix, endpoints, options);\n // Stash the source router so downstream tools (e.g. @routar/react-query)\n // recover prefix + endpoint methods without it being re-passed.\n Object.defineProperty(client, \"$router\", {\n value: { prefix, endpoints } satisfies RouterDef<RouterEndpoints>,\n enumerable: false,\n });\n return client;\n}\n\nfunction resolveArgs(\n second: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n third: RouterEndpoints | CreateApiOptions | undefined,\n fourth: CreateApiOptions | undefined,\n): {\n prefix: string;\n endpoints: RouterEndpoints;\n options: CreateApiOptions | undefined;\n} {\n if (typeof second === \"string\") {\n if (!third)\n throw new Error(\"endpoints is required when prefix is provided\");\n return {\n prefix: second,\n endpoints: third as RouterEndpoints,\n options: fourth,\n };\n }\n if (isRouterDef(second)) {\n return {\n prefix: second.prefix,\n endpoints: second.endpoints,\n options: third as CreateApiOptions | undefined,\n };\n }\n return {\n prefix: \"\",\n endpoints: second as RouterEndpoints,\n options: third as CreateApiOptions | undefined,\n };\n}\n\nfunction shouldValidate(\n options: CreateApiOptions | undefined,\n kind: \"request\" | \"response\",\n): boolean {\n const v = options?.validate;\n if (v === undefined || v === true) return true;\n if (v === false) return false;\n return v[kind] ?? true;\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n options?: CreateApiOptions,\n): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n client[key] = isRouterDef(entry)\n ? buildClient(\n executor,\n joinPaths(prefix, entry.prefix),\n entry.endpoints,\n options,\n )\n : buildEndpointFn(\n executor,\n prefix,\n entry as EndpointSpec<any, any, any>,\n options,\n );\n }\n\n return client;\n}\n\nfunction buildEndpointFn(\n executor: Executor,\n prefix: string,\n spec: EndpointSpec<any, any, any>,\n options: CreateApiOptions | undefined,\n) {\n return async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request && shouldValidate(options, \"request\")) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError(\"Request validation failed\", err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let result: ValidatorOutput<typeof spec.response>;\n if (shouldValidate(options, \"response\")) {\n try {\n result = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError(\"Response validation failed\", err);\n }\n } else {\n result = raw;\n }\n\n return spec.adapter ? spec.adapter(result) : result;\n };\n}\n","import type {\n CreateExecutorOptions,\n ExecuteOptions,\n Executor,\n ExecutorMiddleware,\n ExecutorPlugin,\n} from \"./types.js\";\n\nfunction pluginToMiddleware(plugin: ExecutorPlugin): ExecutorMiddleware {\n return async (opts, next) => {\n const resolvedOpts = plugin.onRequest ? await plugin.onRequest(opts) : opts;\n try {\n const response = await next(resolvedOpts);\n return plugin.onResponse\n ? await plugin.onResponse(response, resolvedOpts)\n : response;\n } catch (err) {\n if (plugin.onError) {\n await plugin.onError(err, resolvedOpts);\n throw err;\n }\n throw err;\n }\n };\n}\n\nexport function buildChain(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[],\n): (options: ExecuteOptions) => Promise<unknown> {\n return middlewares.reduceRight<(options: ExecuteOptions) => Promise<unknown>>(\n (next, mw) => (opts) => mw(opts, next),\n execute,\n );\n}\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with plugins.\n *\n * Plugins run in declaration order (first plugin is outermost).\n *\n * For `retry` and `timeout`, use the options on {@link createFetchExecutor}\n * or configure them via the underlying HTTP client (axios, ky).\n *\n * @example\n * ```ts\n * const executor = createExecutor(transport, {\n * plugins: [authPlugin, logger()],\n * });\n * ```\n *\n * @example Unwrap an envelope response\n * ```ts\n * const executor = createExecutor(transport, {\n * unwrap: (raw) => (raw as { data: unknown })?.data ?? raw,\n * });\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n options: CreateExecutorOptions = {},\n): Executor {\n const plugins = [...(options.plugins ?? [])];\n // `unwrap` becomes the innermost `onResponse` hook: appended last so it runs\n // first on the response (immediately after the transport, before user plugins).\n if (options.unwrap) {\n const unwrapFn = options.unwrap;\n plugins.push({ onResponse: (raw) => unwrapFn(raw) });\n }\n const middlewares = plugins.map(pluginToMiddleware);\n return { execute: buildChain(execute, middlewares) };\n}\n\n/**\n * Creates an {@link Executor} that selects the underlying transport at\n * request time based on the result of `resolver`.\n *\n * @example\n * ```ts\n * const apiExecutor = dispatchExecutor(() =>\n * typeof window === 'undefined' ? serverExecutor : clientExecutor,\n * );\n * ```\n */\nexport function dispatchExecutor(\n resolver: (opts: ExecuteOptions) => Executor,\n): Executor {\n return {\n execute: (opts) => resolver(opts).execute(opts),\n };\n}\n","import type { ExecuteOptions, ExecutorMiddleware, ExecutorPlugin } from \"./types.js\";\n\n/**\n * Thrown by the built-in `timeout` option when a request exceeds the\n * configured duration. Distinguishable from a user-initiated\n * {@link AbortSignal} cancellation.\n */\nexport class TimeoutError extends Error {\n constructor(public readonly ms: number) {\n super(`Request timed out after ${ms}ms`);\n this.name = \"TimeoutError\";\n }\n}\n\n/**\n * Identity helper that returns the plugin as-is, providing full type inference.\n *\n * @example\n * ```ts\n * const authPlugin = definePlugin({\n * name: 'auth',\n * onRequest: async (opts) => ({\n * ...opts,\n * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },\n * }),\n * });\n * ```\n */\nexport function definePlugin(plugin: ExecutorPlugin): ExecutorPlugin {\n return plugin;\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * createExecutor(transport, {\n * plugins: [logger({ log: (msg, data) => myLogger.debug(msg, data) })],\n * })\n * ```\n */\nexport function logger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorPlugin {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n const timings = new WeakMap<ExecuteOptions, number>();\n return definePlugin({\n name: \"logger\",\n onRequest: (opts) => {\n timings.set(opts, Date.now());\n log(`[routar] ${opts.method} ${opts.url}`, {\n params: opts.params,\n body: opts.body,\n });\n return opts;\n },\n onResponse: (res, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n );\n timings.delete(opts);\n return res;\n },\n onError: (err, opts) => {\n log(\n `[routar] ${opts.method} ${opts.url} — error after ${Date.now() - (timings.get(opts) ?? Date.now())}ms`,\n err,\n );\n timings.delete(opts);\n throw err as never;\n },\n });\n}\n\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n };\n}\n\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms);\n\n const { signal, cleanup } = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : { signal: controller.signal, cleanup: () => {} };\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n cleanup();\n }\n };\n}\n\nfunction anySignal(signals: AbortSignal[]): {\n signal: AbortSignal;\n cleanup: () => void;\n} {\n const controller = new AbortController();\n const onAbort = () => controller.abort();\n const attached: AbortSignal[] = [];\n for (const s of signals) {\n if (s.aborted) {\n controller.abort();\n break;\n }\n s.addEventListener(\"abort\", onAbort, { once: true });\n attached.push(s);\n }\n return {\n signal: controller.signal,\n cleanup: () =>\n attached.forEach((s) => {\n s.removeEventListener(\"abort\", onAbort);\n }),\n };\n}\n","export function serializeParams(\n params: Record<string, unknown>,\n): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else if (typeof value === \"object\") {\n throw new TypeError(\n `serializeParams: value for key \"${key}\" is a plain object. Serialize it to a string before passing as a query parameter.`,\n );\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n","import type { CreateExecutorOptions, ExecuteOptions, Executor, ExecutorMiddleware } from \"./types.js\";\nimport { buildChain, createExecutor } from \"./create-executor.js\";\nimport { withRetry, withTimeout } from \"./middleware.js\";\nimport { serializeParams } from \"./utils/params.js\";\n\nexport type FetchRetryOption =\n | number\n | { count: number; shouldRetry?: (error: unknown, attempt: number) => boolean };\n\nexport interface FetchExecutorOptions extends CreateExecutorOptions {\n defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n retry?: FetchRetryOption;\n timeout?: number;\n}\n\nfunction buildFetchChain(\n transport: (opts: ExecuteOptions) => Promise<unknown>,\n retry?: FetchRetryOption,\n timeout?: number,\n): (opts: ExecuteOptions) => Promise<unknown> {\n const middlewares: ExecutorMiddleware[] = [\n ...(retry != null\n ? [\n typeof retry === \"number\"\n ? withRetry(retry)\n : withRetry(retry.count, { shouldRetry: retry.shouldRetry }),\n ]\n : []),\n ...(timeout != null ? [withTimeout(timeout)] : []),\n ];\n if (middlewares.length === 0) return transport;\n return buildChain(transport, middlewares);\n}\n\n/**\n * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.\n *\n * Suited for SSR environments where you need per-request dynamic headers\n * (e.g. forwarding auth cookies) without sharing state across requests.\n *\n * - Query params are serialized and appended to the URL.\n * - A `Content-Type: application/json` header is added automatically when\n * a request body is present.\n * - Responses with status 204 or `Content-Length: 0` resolve to `null`.\n * - Non-2xx responses throw an {@link HttpError}.\n *\n * @param baseURL - Absolute base URL prepended to every endpoint path.\n * Accepts a static string or a sync/async factory called on every request —\n * useful when the origin depends on runtime environment (e.g. SSR vs CSR).\n * @param options.defaultHeaders - Async factory called on every request to\n * produce headers (e.g. reading cookies in a Next.js server component).\n * @param options.plugins - Plugins applied around the fetch call. Each retry\n * attempt re-runs `onRequest` hooks (so headers are refreshed per attempt)\n * and `onError` hooks. Token-refresh-on-401 patterns work by updating an\n * external token store in `onError` and letting `onRequest` pick it up on\n * the next attempt.\n * @param options.retry - Number of retries, or `{ count, shouldRetry? }`.\n * @param options.timeout - Per-attempt timeout in milliseconds.\n *\n * @example Minimal — no options needed\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com');\n * ```\n *\n * @example Dynamic base URL for SSR/CSR\n * ```ts\n * const executor = createFetchExecutor(\n * () => typeof window === 'undefined' ? 'http://localhost:3000/api' : '/api',\n * );\n * ```\n *\n * @example SSR with bearer token\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * defaultHeaders: async () => {\n * const token = await getServerToken();\n * return token ? { Authorization: `Bearer ${token}` } : {};\n * },\n * });\n * ```\n *\n * @example With retry and timeout\n * ```ts\n * const executor = createFetchExecutor('https://api.example.com', {\n * retry: 2,\n * timeout: 8_000,\n * });\n * ```\n */\nexport function createFetchExecutor(\n baseURL: string | (() => string | Promise<string>),\n options?: FetchExecutorOptions,\n): Executor {\n const transport = async ({\n method,\n url,\n params,\n body,\n headers,\n signal,\n }: ExecuteOptions) => {\n const resolvedBase = typeof baseURL === \"function\" ? await baseURL() : baseURL;\n const fullURL = new URL(resolvedBase.replace(/\\/$/, \"\") + url);\n if (params) {\n serializeParams(params).forEach((v, k) => {\n fullURL.searchParams.set(k, v);\n });\n }\n\n const defaultHeaders = (await options?.defaultHeaders?.()) ?? {};\n\n const res = await fetch(fullURL.toString(), {\n method,\n headers: {\n ...defaultHeaders,\n ...headers,\n ...(body != null ? { \"Content-Type\": \"application/json\" } : {}),\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal,\n });\n\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new HttpError(res.status, res.statusText, errorBody, { url: fullURL.toString(), method });\n }\n if (res.status === 204 || res.status === 205 || res.status === 304) {\n return null;\n }\n const text = await res.text();\n return text === \"\" ? null : JSON.parse(text);\n };\n\n const executor = createExecutor(transport, { plugins: options?.plugins, unwrap: options?.unwrap });\n return { execute: buildFetchChain(executor.execute, options?.retry, options?.timeout) };\n}\n\n/**\n * Thrown by {@link createFetchExecutor} when the server returns a non-2xx\n * status code. The Axios and ky executors also normalize their transport\n * errors to `HttpError`, so `onError` plugins and callers can branch on a\n * single error type regardless of the underlying transport. The original\n * transport error (e.g. an `AxiosError` or ky `HTTPError`) is preserved on\n * the `cause` property.\n *\n * @example\n * ```ts\n * try {\n * await api.getDetail({ path: { id: 999 } });\n * } catch (err) {\n * if (err instanceof HttpError && err.status === 404) {\n * // handle not-found\n * }\n * }\n * ```\n */\nexport class HttpError extends Error {\n public readonly url?: string;\n public readonly method?: string;\n\n constructor(\n public readonly status: number,\n public readonly statusText: string,\n public readonly body: unknown = null,\n options?: { url?: string; method?: string; cause?: unknown },\n ) {\n super(`HTTP ${status}: ${statusText}`, { cause: options?.cause });\n this.name = \"HttpError\";\n this.url = options?.url;\n this.method = options?.method;\n }\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from \"./types.js\";\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * The literal HTTP method (`'GET'`, `'POST'`, …) is preserved on the return\n * type — `endpoint({ method: 'GET', ... }).method` is typed `'GET'`, not the\n * `HttpMethod` union.\n *\n * @example Basic GET with no params\n * ```ts\n * const getList = endpoint({ method: 'GET', path: '/', response: z.array(TodoSchema) });\n * ```\n *\n * @example GET with query params\n * ```ts\n * const search = endpoint({\n * method: 'GET',\n * path: '/search',\n * request: z.object({ query: z.object({ q: z.string(), limit: z.number().optional() }) }),\n * response: z.array(TodoSchema),\n * });\n * ```\n *\n * @example POST with body\n * ```ts\n * const create = endpoint({\n * method: 'POST',\n * path: '/',\n * request: z.object({ body: z.object({ title: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n *\n * @example Adapter — raw is inferred from the response schema, no cast needed\n * ```ts\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoRawSchema,\n * adapter: (raw) => ({ ...raw, label: `#${raw.id} ${raw.title}` }),\n * });\n * ```\n *\n * @example Path param enforcement\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<\n TMethod extends HttpMethod,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: TMethod;\n path: string;\n response: TResponse;\n}): {\n method: TMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n"]}
|