silgi 0.52.2 → 0.53.1

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.
@@ -1,5 +1,5 @@
1
1
  import { routerCache } from "./router-utils.mjs";
2
- import { compileRouter, createContext, detachContext, releaseContext } from "../compile.mjs";
2
+ import { compileRouter } from "../compile.mjs";
3
3
  import { applyContext } from "./dispatch.mjs";
4
4
  import { detectResponseFormat, encodeResponse, makeErrorResponse } from "./codec.mjs";
5
5
  import { parseInput } from "./input.mjs";
@@ -7,58 +7,45 @@ import { iteratorToEventStream } from "./sse.mjs";
7
7
  import { parseUrlPath } from "./url.mjs";
8
8
  //#region src/core/handler.ts
9
9
  /**
10
- * Fetch API handler — single unified request handler.
10
+ * Fetch API handler
11
+ * -------------------
11
12
  *
12
- * Orchestrates: routing context input parsing pipeline response encoding.
13
- * Each concern lives in its own module (codec.ts, input.ts, sse.ts).
13
+ * Every adapter that speaks the Fetch API (Next.js App Router, SvelteKit,
14
+ * srvx, Bun, Cloudflare Workers, Deno) ends up calling the handler built
15
+ * here. It is the single place that turns a `Request` into a `Response`
16
+ * by running the compiled pipeline produced by `compileRouter`.
14
17
  *
15
- * Analytics / Scalar are NOT here — they wrap the handler externally
16
- * (see wrapWithAnalytics / wrapWithScalar in their respective modules).
18
+ * Responsibilities, in order:
19
+ *
20
+ * 1. URL parsing and `basePath` stripping.
21
+ * 2. Route lookup + HTTP method enforcement.
22
+ * 3. Context construction (factory + optional `AsyncLocalStorage` bridge).
23
+ * 4. `request:prepare` hook so plugins (analytics, etc.) can seed `ctx`.
24
+ * 5. Input parsing (body / query / URL params).
25
+ * 6. Pipeline execution — the compiled handler from `compileProcedure`.
26
+ * 7. Response encoding (JSON, msgpack, stream, SSE, raw `Response`).
27
+ *
28
+ * Analytics and the Scalar UI are layered on top via `wrapHandler` — they
29
+ * do not live inside the hot path.
17
30
  */
18
- /** Wrap a stream to release pooled context on completion or cancellation. */
19
- function wrapStreamWithRelease(source, ctx) {
20
- let released = false;
21
- const release = () => {
22
- if (!released) {
23
- released = true;
24
- releaseContext(ctx);
25
- }
26
- };
27
- const reader = source.getReader();
28
- return new ReadableStream({
29
- async pull(controller) {
30
- try {
31
- const { done, value } = await reader.read();
32
- if (done) {
33
- release();
34
- controller.close();
35
- } else controller.enqueue(value);
36
- } catch (err) {
37
- release();
38
- controller.error(err);
39
- }
40
- },
41
- cancel() {
42
- release();
43
- reader.cancel();
44
- }
45
- });
46
- }
47
31
  /**
48
- * Build the Response for a handler's output. The pooled `ctx` is released
49
- * by the caller's `using` scope; for streaming outputs we detach `ctx` first
50
- * so the stream becomes the sole owner (releases on stream end/cancel).
32
+ * Convert a pipeline output into an HTTP `Response`.
33
+ *
34
+ * We handle four shapes:
35
+ * - `Response` — user returned one directly; pass through.
36
+ * - `ReadableStream` — wrap in a binary response.
37
+ * - Async iterator — render as Server-Sent Events.
38
+ * - Plain value — encode as JSON (or msgpack when the client asked for it).
39
+ *
40
+ * The function is async so callers have a single `await` point and do not
41
+ * have to branch on sync-vs-async encoders underneath.
51
42
  */
52
- function makeResponse(output, route, format, ctx) {
43
+ async function makeResponse(output, route, format) {
53
44
  if (output instanceof Response) return output;
54
- if (output instanceof ReadableStream) {
55
- detachContext(ctx);
56
- return new Response(wrapStreamWithRelease(output, ctx), { headers: { "content-type": "application/octet-stream" } });
57
- }
45
+ if (output instanceof ReadableStream) return new Response(output, { headers: { "content-type": "application/octet-stream" } });
58
46
  if (output && typeof output === "object" && Symbol.asyncIterator in output) {
59
- detachContext(ctx);
60
47
  const stream = iteratorToEventStream(output);
61
- return new Response(wrapStreamWithRelease(stream, ctx), { headers: {
48
+ return new Response(stream, { headers: {
62
49
  "content-type": "text/event-stream",
63
50
  "cache-control": "no-cache"
64
51
  } });
@@ -71,29 +58,44 @@ function makeResponse(output, route, format, ctx) {
71
58
  } : { "content-type": "application/json" } });
72
59
  }
73
60
  /**
74
- * Lazily wrap a FetchHandler with analytics and/or scalar.
75
- * Returns a new handler that applies wrappers on first request (async import).
76
- * If no wrappers are needed, returns the original handler as-is.
61
+ * Wrap a `FetchHandler` with Scalar UI and/or analytics, if configured.
62
+ *
63
+ * Both wrappers have non-trivial imports (Scalar pulls in the API
64
+ * reference, analytics pulls in the dashboard). We defer those imports
65
+ * until the first request so that a handler you never hit does not pay
66
+ * the cost.
67
+ *
68
+ * If the lazy init fails (network blip, broken import) we fall back to
69
+ * the raw handler and log once — one failed init must not wedge every
70
+ * subsequent request.
77
71
  */
78
72
  function wrapHandler(handler, router, options, prefix) {
79
73
  if (!options?.scalar && !options?.analytics) return handler;
80
- let wrapped;
74
+ let wrapped = handler;
75
+ let initDone = false;
81
76
  let initPromise;
82
- async function init() {
83
- let h = handler;
84
- if (options.scalar) {
85
- const { wrapWithScalar } = await import("../scalar.mjs");
86
- const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
87
- h = wrapWithScalar(h, router, scalarOpts, prefix, options.schemaRegistry);
88
- }
89
- if (options.analytics) {
90
- const { wrapWithAnalytics } = await import("../plugins/analytics.mjs");
91
- h = wrapWithAnalytics(h, router, options.analytics, options.schemaRegistry, options.hooks);
77
+ const init = async () => {
78
+ try {
79
+ let next = handler;
80
+ if (options.scalar) {
81
+ const { wrapWithScalar } = await import("../scalar.mjs");
82
+ const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
83
+ next = wrapWithScalar(next, router, scalarOpts, prefix, options.schemaRegistry);
84
+ }
85
+ if (options.analytics) {
86
+ const { wrapWithAnalytics } = await import("../plugins/analytics.mjs");
87
+ next = wrapWithAnalytics(next, router, options.analytics, options.schemaRegistry, options.hooks);
88
+ }
89
+ wrapped = next;
90
+ } catch (err) {
91
+ console.error("[silgi] Failed to initialise scalar/analytics wrapper:", err);
92
+ wrapped = handler;
93
+ } finally {
94
+ initDone = true;
92
95
  }
93
- wrapped = h;
94
- }
96
+ };
95
97
  return (request) => {
96
- if (wrapped) return wrapped(request);
98
+ if (initDone) return wrapped(request);
97
99
  initPromise ??= init();
98
100
  return initPromise.then(() => wrapped(request));
99
101
  };
@@ -111,25 +113,39 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
111
113
  status: 404,
112
114
  message: "Procedure not found"
113
115
  });
114
- function callHook(name, event) {
116
+ /**
117
+ * Hook dispatch helpers.
118
+ *
119
+ * Hook errors never fail the request. But we do log them: silently
120
+ * swallowing a hook throw hides genuine user bugs (a typo'd field, a
121
+ * thrown assertion) and the dashboard / trace / logging pipeline just
122
+ * stops working with no visible signal.
123
+ */
124
+ const reportHookError = (name, err) => {
125
+ console.error(`[silgi] hook "${name}" threw:`, err);
126
+ };
127
+ const callHook = (name, event) => {
115
128
  if (!hooks) return;
116
129
  try {
117
130
  const result = hooks.callHook(name, event);
118
- if (result instanceof Promise) result.catch(() => {});
119
- } catch {}
120
- }
121
- function awaitHook(name, event) {
131
+ if (result instanceof Promise) result.catch((err) => reportHookError(name, err));
132
+ } catch (err) {
133
+ reportHookError(name, err);
134
+ }
135
+ };
136
+ const awaitHook = async (name, event) => {
122
137
  if (!hooks) return;
123
138
  try {
124
- const result = hooks.callHook(name, event);
125
- if (result instanceof Promise) return result.catch(() => {});
126
- } catch {}
127
- }
139
+ await hooks.callHook(name, event);
140
+ } catch (err) {
141
+ reportHookError(name, err);
142
+ }
143
+ };
128
144
  return async function handleRequest(request) {
129
145
  const url = request.url;
130
146
  let fullPath = parseUrlPath(url);
131
147
  if (prefix) {
132
- if (!fullPath.startsWith(prefix)) return new Response(notFoundBody, {
148
+ if (fullPath !== prefix && !fullPath.startsWith(prefix + "/")) return new Response(notFoundBody, {
133
149
  status: 404,
134
150
  headers: jsonHeaders
135
151
  });
@@ -158,17 +174,15 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
158
174
  });
159
175
  }
160
176
  const format = detectResponseFormat(request);
161
- using ctx = createContext();
177
+ const ctx = Object.create(null);
162
178
  let rawInput;
163
179
  try {
164
- const baseCtxResult = contextFactory(request);
165
- applyContext(ctx, baseCtxResult instanceof Promise ? await baseCtxResult : baseCtxResult);
180
+ applyContext(ctx, await contextFactory(request));
166
181
  if (match.params) ctx.params = match.params;
167
- const prepareResult = awaitHook("request:prepare", {
182
+ await awaitHook("request:prepare", {
168
183
  request,
169
184
  ctx
170
185
  });
171
- if (prepareResult) await prepareResult;
172
186
  if (!route.passthrough) rawInput = await parseInput(request, url, qMark);
173
187
  if (match.params) rawInput = rawInput != null && typeof rawInput === "object" ? {
174
188
  ...match.params,
@@ -178,8 +192,7 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
178
192
  path: pathname,
179
193
  input: rawInput
180
194
  });
181
- const pipelineResult = bridge ? bridge.run(ctx, () => route.handler(ctx, rawInput, request.signal)) : route.handler(ctx, rawInput, request.signal);
182
- const output = pipelineResult instanceof Promise ? await pipelineResult : pipelineResult;
195
+ const output = await (bridge ? bridge.run(ctx, () => route.handler(ctx, rawInput, request.signal)) : route.handler(ctx, rawInput, request.signal));
183
196
  callHook("response", {
184
197
  path: pathname,
185
198
  output,
@@ -190,15 +203,13 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
190
203
  ctx,
191
204
  output
192
205
  });
193
- const response = makeResponse(output, route, format, ctx);
194
- return response instanceof Promise ? await response : response;
206
+ return await makeResponse(output, route, format);
195
207
  } catch (error) {
196
208
  callHook("error", {
197
209
  path: pathname,
198
210
  error
199
211
  });
200
- const errorResponse = makeErrorResponse(error, format);
201
- return errorResponse instanceof Promise ? await errorResponse : errorResponse;
212
+ return await makeErrorResponse(error, format);
202
213
  }
203
214
  };
204
215
  }
@@ -1,49 +1,128 @@
1
1
  import { SilgiError } from "./error.mjs";
2
2
  //#region src/core/input.ts
3
3
  /**
4
- * Request input parsing — JSON, MessagePack, devalue, query string.
4
+ * Request input parsing
5
+ * -----------------------
6
+ *
7
+ * Pulls the RPC input payload out of an incoming `Request` and returns
8
+ * it in its decoded form. The shape of the input depends on how the
9
+ * client chose to send it:
10
+ *
11
+ * - `GET` / no-body — JSON-encoded, URL-escaped, on `?data=` query.
12
+ * - `content-type: application/msgpack` — binary, via the msgpack codec.
13
+ * - `content-type: application/x-devalue` — text, via devalue.
14
+ * - anything else (typically JSON) — JSON body.
15
+ *
16
+ * Empty bodies always resolve to `undefined` so that a procedure with
17
+ * no input, or one whose schema allows `undefined`, works without the
18
+ * client having to send a payload at all. Malformed non-empty bodies
19
+ * throw `BAD_REQUEST` with a message the client can show to a user.
5
20
  */
6
- let _msgpack;
7
- let _devalue;
8
- const isBun = typeof globalThis.Bun !== "undefined";
9
- /** Max allowed size for GET ?data= parameter (bytes). Prevents JSON bomb via URL. */
21
+ /**
22
+ * The msgpack and devalue codecs are pulled in on first use so that a
23
+ * handler that only ever sees JSON never pays the cost of loading
24
+ * them. `Promise`-cached at module scope: once the first request
25
+ * triggers the import, subsequent calls share the resolved module.
26
+ *
27
+ * Module-global is intentional and safe here — the cached value is a
28
+ * reference to an immutable ES module, not user data. Two silgi
29
+ * instances in the same process legitimately share it.
30
+ */
31
+ let msgpackModule;
32
+ let devalueModule;
33
+ /** Max bytes permitted in the `?data=` query param. Shields against JSON-bomb payloads in a URL. */
10
34
  const MAX_QUERY_DATA_LENGTH = 8192;
11
- /** Parse request input from body or query string */
12
- async function parseInput(request, url, qMark) {
13
- if (request.method === "GET" || !request.body) {
14
- if (qMark !== -1) {
15
- const searchStr = url.slice(qMark + 1);
16
- const dataIdx = searchStr.indexOf("data=");
17
- if (dataIdx !== -1) {
18
- const valueStart = dataIdx + 5;
19
- const valueEnd = searchStr.indexOf("&", valueStart);
20
- const encoded = valueEnd === -1 ? searchStr.slice(valueStart) : searchStr.slice(valueStart, valueEnd);
21
- if (encoded.length > MAX_QUERY_DATA_LENGTH) throw new SilgiError("BAD_REQUEST", { message: "Query data parameter too large" });
22
- return JSON.parse(decodeURIComponent(encoded));
23
- }
24
- }
25
- return;
26
- }
27
- const ct = request.headers.get("content-type");
28
- if (ct) {
29
- if (ct.includes("msgpack")) {
30
- _msgpack ??= await import("../codec/msgpack.mjs");
31
- const buf = new Uint8Array(await request.arrayBuffer());
32
- return buf.length > 0 ? _msgpack.decode(buf) : void 0;
33
- }
34
- if (ct.includes("x-devalue")) {
35
- _devalue ??= await import("../codec/devalue.mjs");
36
- const text = await request.text();
37
- return text ? _devalue.decode(text) : void 0;
35
+ /**
36
+ * Find the value of the `data=` query parameter by key, not by substring.
37
+ *
38
+ * A naive `searchStr.indexOf('data=')` matches `userdata=`, `mydata=`,
39
+ * or any other key that merely ends in `data`, and silently returns
40
+ * the wrong value. This scans for `data=` only at a parameter
41
+ * boundary i.e. at the start of the search string, or right after
42
+ * an `&`.
43
+ */
44
+ function findDataParam(searchStr) {
45
+ let i = 0;
46
+ while (i < searchStr.length) {
47
+ if (searchStr.startsWith("data=", i)) {
48
+ const valueStart = i + 5;
49
+ const valueEnd = searchStr.indexOf("&", valueStart);
50
+ return valueEnd === -1 ? searchStr.slice(valueStart) : searchStr.slice(valueStart, valueEnd);
38
51
  }
52
+ const nextAmp = searchStr.indexOf("&", i);
53
+ if (nextAmp === -1) return null;
54
+ i = nextAmp + 1;
39
55
  }
40
- if (isBun) try {
41
- return await request.json();
56
+ return null;
57
+ }
58
+ /**
59
+ * Decode the `?data=` query param as JSON. Returns `undefined` when
60
+ * the query is missing or has no `data=` field. Throws `BAD_REQUEST`
61
+ * when the payload is oversized or unparsable.
62
+ */
63
+ function decodeQueryInput(url, qMark) {
64
+ if (qMark === -1) return void 0;
65
+ const encoded = findDataParam(url.slice(qMark + 1));
66
+ if (encoded === null) return void 0;
67
+ if (encoded.length > MAX_QUERY_DATA_LENGTH) throw new SilgiError("BAD_REQUEST", { message: "Query data parameter too large" });
68
+ return JSON.parse(decodeURIComponent(encoded));
69
+ }
70
+ /**
71
+ * Decode a MessagePack-encoded request body. Empty bodies resolve to
72
+ * `undefined` (procedures with no input work without a payload).
73
+ */
74
+ async function decodeMsgpackBody(request) {
75
+ msgpackModule ??= await import("../codec/msgpack.mjs");
76
+ const buf = new Uint8Array(await request.arrayBuffer());
77
+ return buf.length > 0 ? msgpackModule.decode(buf) : void 0;
78
+ }
79
+ /** Decode a devalue-encoded request body. Empty bodies resolve to `undefined`. */
80
+ async function decodeDevalueBody(request) {
81
+ devalueModule ??= await import("../codec/devalue.mjs");
82
+ const text = await request.text();
83
+ return text ? devalueModule.decode(text) : void 0;
84
+ }
85
+ /**
86
+ * Decode a JSON-encoded request body.
87
+ *
88
+ * Empty bodies resolve to `undefined` so the input schema sees the
89
+ * same value whether or not the client sent a body at all. Malformed
90
+ * non-empty bodies throw `BAD_REQUEST`.
91
+ *
92
+ * Why we `text()` first and then `JSON.parse` — instead of
93
+ * `request.json()`: Bun's `request.json()` is a fast path, but it
94
+ * throws a generic `SyntaxError` for **both** empty and malformed
95
+ * bodies, so we cannot tell them apart. Reading text first keeps the
96
+ * two cases distinct (and Bun's `text()` is also fast).
97
+ */
98
+ async function decodeJsonBody(request) {
99
+ const text = await request.text();
100
+ if (!text) return void 0;
101
+ try {
102
+ return JSON.parse(text);
42
103
  } catch {
43
- return;
104
+ throw new SilgiError("BAD_REQUEST", { message: "Malformed JSON body" });
44
105
  }
45
- const text = await request.text();
46
- return text ? JSON.parse(text) : void 0;
106
+ }
107
+ /**
108
+ * Decode the input payload off a Fetch `Request`.
109
+ *
110
+ * @param request The incoming request.
111
+ * @param url The full request URL (reused by the caller — we avoid
112
+ * re-parsing it here).
113
+ * @param qMark Byte offset of the `?` in `url`, or `-1` when absent.
114
+ *
115
+ * @returns The decoded input value, or `undefined` when the request
116
+ * carries no payload.
117
+ */
118
+ async function parseInput(request, url, qMark) {
119
+ if (request.method === "GET" || !request.body) return decodeQueryInput(url, qMark);
120
+ const contentType = request.headers.get("content-type");
121
+ if (contentType) {
122
+ if (contentType.includes("msgpack")) return decodeMsgpackBody(request);
123
+ if (contentType.includes("x-devalue")) return decodeDevalueBody(request);
124
+ }
125
+ return decodeJsonBody(request);
47
126
  }
48
127
  //#endregion
49
128
  export { parseInput };
@@ -2,7 +2,9 @@ import { AnySchema } from "./schema.mjs";
2
2
 
3
3
  //#region src/core/schema-converter.d.ts
4
4
  /**
5
- * JSON Schema subset used for OpenAPI / analytics output.
5
+ * JSON Schema subset used across silgi's OpenAPI and analytics output.
6
+ * Intentionally broad (`[key: string]: unknown`) so library-specific
7
+ * fields (e.g. Zod's `x-native-type`) pass through untouched.
6
8
  *
7
9
  * @category Schema
8
10
  */
@@ -22,54 +24,68 @@ interface JSONSchema {
22
24
  default?: unknown;
23
25
  [key: string]: unknown;
24
26
  }
27
+ /**
28
+ * JSON Schema dialect passed through to the schema library. Matches the
29
+ * `target` field of the Standard JSON Schema spec. Unknown strings are
30
+ * allowed so new dialects can be threaded through without a silgi
31
+ * release; libraries that do not recognise the value should throw and
32
+ * the conversion falls back to an empty schema.
33
+ *
34
+ * @category Schema
35
+ */
36
+ type JSONSchemaTarget = 'draft-2020-12' | 'draft-07' | 'openapi-3.0' | (string & {});
25
37
  /**
26
38
  * Options passed to a converter's `toJsonSchema` method.
27
39
  *
28
40
  * @category Schema
29
41
  */
30
42
  interface ConvertOptions {
43
+ /** `'input'` for pre-transform types, `'output'` for post-transform. */
31
44
  strategy: 'input' | 'output';
45
+ /** JSON Schema dialect to target. Defaults to `'draft-2020-12'`. */
46
+ target?: JSONSchemaTarget;
47
+ /** Opaque options the converter may forward to its underlying library. */
48
+ libraryOptions?: Record<string, unknown>;
32
49
  }
33
50
  /**
34
- * A converter that translates a specific Standard Schema vendor's schemas
35
- * into JSON Schema. Pass instances via `silgi({ schemaConverters: [...] })`.
51
+ * Fallback converter for a schema library that has not adopted the
52
+ * Standard JSON Schema extension yet. Pass instances via
53
+ * `silgi({ schemaConverters: [...] })`.
36
54
  *
37
55
  * @remarks
38
- * Implement this interface to add OpenAPI / analytics support for a custom
39
- * schema library. The `vendor` string must match the `~standard.vendor`
40
- * property reported by the schema library's Standard Schema implementation.
56
+ * Libraries that *do* implement the extension (Zod v4.2+, ArkType
57
+ * v2.1.28+, Valibot v1.2+) are handled without a converter silgi
58
+ * calls their native `~standard.jsonSchema` directly. Write a converter
59
+ * only when you need to support a vendor that has not yet adopted the
60
+ * spec.
41
61
  *
42
62
  * @example
43
- * ```ts
44
- * import type { SchemaConverter } from 'silgi'
63
+ * import type { SchemaConverter } from 'silgi'
45
64
  *
46
- * const myConverter: SchemaConverter = {
47
- * vendor: 'my-lib',
48
- * toJsonSchema(schema, opts) {
49
- * return { type: 'string' }
50
- * },
51
- * }
52
- * ```
65
+ * const myConverter: SchemaConverter = {
66
+ * vendor: 'my-lib',
67
+ * toJsonSchema(schema, opts) {
68
+ * return { type: 'string' }
69
+ * },
70
+ * }
53
71
  *
54
72
  * @category Schema
55
73
  */
56
74
  interface SchemaConverter {
57
- /** The Standard Schema `~standard.vendor` string this converter handles (e.g. `"zod"`). */
75
+ /** Matches the `~standard.vendor` reported by the schema library. */
58
76
  vendor: string;
59
77
  /**
60
- * Convert a schema to a JSON Schema object.
61
- *
62
- * @param schema - The schema to convert.
63
- * @param opts - Conversion options including `strategy` (`'input'` | `'output'`).
64
- * @returns A JSON Schema object. Return `{}` for unsupported/unknown schemas.
78
+ * Convert a schema to a JSON Schema object. Return `{}` for schemas
79
+ * the converter does not understand.
65
80
  */
66
81
  toJsonSchema(schema: AnySchema, opts: ConvertOptions): JSONSchema;
67
82
  }
68
83
  /**
69
- * A per-instance registry mapping vendor strings to their converters.
70
- *
71
- * Built by {@link createSchemaRegistry} and threaded through the handler
72
- * pipeline to scalar and analytics wrappers.
84
+ * Per-instance mapping of vendor string fallback converter. Built by
85
+ * {@link createSchemaRegistry} and threaded through the handler pipeline
86
+ * to the scalar and analytics wrappers. Using `Map` gives O(1) lookup
87
+ * and keyed-by-vendor semantics that match the spec's own extension
88
+ * contract.
73
89
  *
74
90
  * @category Schema
75
91
  */
@@ -77,55 +93,44 @@ type SchemaRegistry = Map<string, SchemaConverter>;
77
93
  /**
78
94
  * Build a {@link SchemaRegistry} from an array of converters.
79
95
  *
80
- * @param converters - Array of {@link SchemaConverter} objects, each
81
- * declaring their own `vendor`.
82
- * @returns A `Map<string, SchemaConverter>` keyed by `converter.vendor`.
83
- *
84
96
  * @example
85
- * ```ts
86
- * import { zodConverter } from 'silgi/zod'
87
- * import { createSchemaRegistry } from 'silgi'
88
- *
89
- * const registry = createSchemaRegistry([zodConverter])
90
- * ```
97
+ * import { zodConverter } from 'silgi/zod'
98
+ * const registry = createSchemaRegistry([zodConverter])
91
99
  *
92
100
  * @category Schema
93
101
  */
94
102
  declare function createSchemaRegistry(converters?: SchemaConverter[]): SchemaRegistry;
95
103
  /**
96
- * Convert any Standard Schema to JSON Schema.
97
- *
98
- * @remarks
99
- * Resolution order:
100
- * 1. **Native fast path** `schema['~standard'].jsonSchema.input()`
101
- * (Valibot, ArkType, Zod v4, …). No registry needed.
102
- * 2. **Registry lookup** finds a converter by
103
- * `schema['~standard'].vendor`. Registry must be passed explicitly;
104
- * there is no global mutable state.
105
- * 3. **Empty schema `{}`** — emits a one-time `console.warn` per vendor
106
- * when a registry was provided but contained no matching converter.
107
- * No warn when no registry was passed (caller opted out).
108
- *
109
- * @param schema - Any Standard Schema compatible schema object.
110
- * @param strategy - `'input'` (default) for pre-transform types; `'output'`
111
- * for post-transform.
112
- * @param registry - Optional {@link SchemaRegistry} built from
113
- * {@link createSchemaRegistry}. When omitted the function still handles
114
- * schemas that expose the native `jsonSchema.input()` fast path.
115
- * @returns A JSON Schema object. Returns `{}` when conversion is not possible.
104
+ * Convert any Standard Schema to a JSON Schema object.
105
+ *
106
+ * @param schema The schema to convert.
107
+ * @param strategy `'input'` (default) for pre-transform types, `'output'`
108
+ * for post-transform. Matters for schemas that coerce
109
+ * (e.g. `z.coerce.number()` takes a string and yields a
110
+ * number input and output schemas differ).
111
+ * @param registry Optional fallback registry built by
112
+ * {@link createSchemaRegistry}. Omit to rely solely on
113
+ * the native Standard JSON Schema extension.
114
+ * @param options Extra knobs: `target` dialect (default
115
+ * `'draft-2020-12'`), opaque `libraryOptions`
116
+ * forwarded to the underlying library.
117
+ *
118
+ * @returns A JSON Schema object. `{}` when the schema cannot be
119
+ * converted (silent fallback — analytics / OpenAPI output
120
+ * still renders, just without schema detail for that field).
116
121
  *
117
122
  * @example
118
- * ```ts
119
- * import { zodConverter } from 'silgi/zod'
120
- * import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
121
- * import { z } from 'zod'
123
+ * import { zodConverter } from 'silgi/zod'
124
+ * import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
122
125
  *
123
- * const registry = createSchemaRegistry([zodConverter])
124
- * const json = schemaToJsonSchema(z.object({ name: z.string() }), 'input', registry)
125
- * ```
126
+ * const registry = createSchemaRegistry([zodConverter])
127
+ * const json = schemaToJsonSchema(MySchema, 'input', registry)
126
128
  *
127
129
  * @category Schema
128
130
  */
129
- declare function schemaToJsonSchema(schema: AnySchema, strategy?: 'input' | 'output', registry?: SchemaRegistry): JSONSchema;
131
+ declare function schemaToJsonSchema(schema: AnySchema, strategy?: 'input' | 'output', registry?: SchemaRegistry, options?: {
132
+ target?: JSONSchemaTarget;
133
+ libraryOptions?: Record<string, unknown>;
134
+ }): JSONSchema;
130
135
  //#endregion
131
136
  export { ConvertOptions, JSONSchema, SchemaConverter, SchemaRegistry, createSchemaRegistry, schemaToJsonSchema };