@kizunajs/fetch 2.0.0-beta.2 → 2.0.0-beta.20

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/server.mjs DELETED
@@ -1,435 +0,0 @@
1
- import { ValidationErrorSchema } from "kizunajs/schemas";
2
- import { isBinarySchema, isFileSchema, isStreamResponse, readDef, readDeprecation, readDiscriminatedUnion, readMetaDescription, readMetaExamples, readMetaId, readObjectShape, resolveResponseBody, resolveResponseHeaders, streamContentType, toPascalCase, unwrapOptionalWrappers } from "kizunajs/generator";
3
- //#region src/zod-to-typescript.ts
4
- /**
5
- * Collects the named types a walk produces, so a model shared by many routes is
6
- * declared once.
7
- */
8
- var TypeCollector = class {
9
- named = /* @__PURE__ */ new Map();
10
- claimed = /* @__PURE__ */ new Set();
11
- /**
12
- * Claims a name before its body is walked, so a model that refers to itself
13
- * resolves to its own name instead of recursing. Answers whether the caller
14
- * is the one that has to emit it.
15
- */
16
- claim(name) {
17
- if (this.claimed.has(name)) return false;
18
- this.claimed.add(name);
19
- return true;
20
- }
21
- add(type) {
22
- this.named.set(type.name, type);
23
- }
24
- all() {
25
- return [...this.named.values()].sort((left, right) => left.name.localeCompare(right.name));
26
- }
27
- };
28
- const quote = (value) => JSON.stringify(value);
29
- const indent$1 = (text, depth = 1) => text.split("\n").map((line) => line.length > 0 ? " ".repeat(depth) + line : line).join("\n");
30
- /**
31
- * A property name, quoted only when it is not a plain identifier.
32
- */
33
- const propertyKey = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : quote(key);
34
- /**
35
- * A doc comment over the lines it is given, or nothing when there are none.
36
- * Always multi-line, the way the rest of this repository writes them.
37
- */
38
- const docComment = (lines) => {
39
- const body = lines.flatMap((line) => line.split("\n"));
40
- if (body.length === 0) return "";
41
- return `/**\n${body.map((line) => ` * ${line}`.trimEnd()).join("\n")}\n */\n`;
42
- };
43
- /**
44
- * Renders a declared example as the TypeScript literal a caller would write.
45
- */
46
- const exampleLiteral = (value) => {
47
- if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
48
- if (value === void 0) return "undefined";
49
- return JSON.stringify(value) ?? "undefined";
50
- };
51
- const STRING_SAMPLES = {
52
- email: "ada@example.com",
53
- url: "https://example.com",
54
- uuid: "3f1c2d9e-5b6a-4c7d-8e9f-0a1b2c3d4e5f",
55
- datetime: "2026-01-01T00:00:00Z",
56
- date: "2026-01-01",
57
- time: "00:00:00"
58
- };
59
- const stringSample = (schema, key) => {
60
- const def = readDef(schema);
61
- const formats = [def.format, ...(def.checks ?? []).map((check) => check.format ?? check.def?.format)];
62
- for (const format of formats) if (format !== void 0 && format in STRING_SAMPLES) return exampleLiteral(STRING_SAMPLES[format]);
63
- if (key !== void 0 && /id$/i.test(key)) return exampleLiteral("1");
64
- return exampleLiteral("string");
65
- };
66
- /**
67
- * A value a caller could pass for a schema, used in the `@example` on each
68
- * client method. A schema that declares an example is taken at its word;
69
- * anything else gets a value of the right shape.
70
- */
71
- const sampleValue = (schema, depth = 0, key) => {
72
- const [declared] = readMetaExamples(schema);
73
- if (declared !== void 0) return exampleLiteral(declared);
74
- if (isFileSchema(schema)) return "new File([], 'upload.txt')";
75
- if (isBinarySchema(schema)) return "new Uint8Array()";
76
- const def = readDef(schema);
77
- if (def.type === "default" && def.defaultValue !== void 0) return exampleLiteral(def.defaultValue);
78
- if (def.type === "pipe") {
79
- const side = def.out ?? def.in;
80
- return side ? sampleValue(side, depth, key) : "undefined";
81
- }
82
- if (def.innerType && def.type !== "object") return sampleValue(def.innerType, depth, key);
83
- switch (def.type) {
84
- case "number":
85
- case "int": return "1";
86
- case "bigint": return "1n";
87
- case "boolean": return "true";
88
- case "date": return exampleLiteral("2026-01-01");
89
- case "null": return "null";
90
- case "literal": return def.values?.length ? exampleLiteral(def.values[0]) : "undefined";
91
- case "enum": {
92
- const [first] = Object.values(def.entries ?? {});
93
- return first === void 0 ? "undefined" : exampleLiteral(first);
94
- }
95
- case "array":
96
- if (!def.element || depth >= SAMPLE_DEPTH) return "[]";
97
- return `[\n${indent$1(`${sampleValue(def.element, depth + 1, key)},`)}\n]`;
98
- case "object": return sampleObject(schema, depth);
99
- case "record": return "{}";
100
- case "union": {
101
- const options = readDiscriminatedUnion(schema)?.options ?? def.options ?? [];
102
- return options[0] ? sampleValue(options[0], depth, key) : "undefined";
103
- }
104
- case "string": return stringSample(schema, key);
105
- default: return "undefined";
106
- }
107
- };
108
- const SAMPLE_DEPTH = 2;
109
- /**
110
- * The required fields of an object schema, written as the literal a caller
111
- * would pass. Optional fields are left out, since an example shows the least
112
- * the route needs.
113
- */
114
- const sampleObject = (schema, depth = 0) => {
115
- const shape = readObjectShape(schema);
116
- if (!shape) return "{}";
117
- const required = Object.entries(shape).filter(([, field]) => !unwrapOptionalWrappers(field).optional);
118
- if (required.length === 0 || depth >= SAMPLE_DEPTH) return "{}";
119
- return `{\n${indent$1(required.map(([key, field]) => `${propertyKey(key)}: ${sampleValue(unwrapOptionalWrappers(field).inner, depth + 1, key)},`).join("\n"))}\n}`;
120
- };
121
- /**
122
- * What an editor shows above a field: its description, whether it is on its way
123
- * out, and the values the schema declares as examples.
124
- */
125
- const fieldDoc = (field) => {
126
- const lines = [];
127
- const description = readMetaDescription(field);
128
- if (description !== void 0) lines.push(description);
129
- const deprecation = readDeprecation(field);
130
- if (deprecation !== void 0) {
131
- if (lines.length > 0) lines.push("");
132
- lines.push(`@deprecated ${deprecation.message ?? ""}`.trimEnd());
133
- }
134
- for (const example of readMetaExamples(field)) {
135
- if (lines.length > 0) lines.push("");
136
- lines.push("@example", exampleLiteral(example));
137
- }
138
- return lines;
139
- };
140
- const objectBody = (schema, collector, hint) => {
141
- const shape = readObjectShape(schema);
142
- if (!shape) return "Record<string, unknown>";
143
- const entries = Object.entries(shape);
144
- if (entries.length === 0) return "Record<string, never>";
145
- return `{\n${indent$1(entries.map(([key, field]) => {
146
- const doc = docComment(fieldDoc(field));
147
- const mark = unwrapOptionalWrappers(field).optional ? "?" : "";
148
- return `${doc}${propertyKey(key)}${mark}: ${typeOf(field, collector, `${hint}${toPascalCase(key)}`)};`;
149
- }).join("\n"))}\n}`;
150
- };
151
- /**
152
- * The TypeScript type a schema describes. Named models are collected and
153
- * referenced; anonymous shapes are inlined.
154
- */
155
- const typeOf = (schema, collector, hint) => {
156
- if (isFileSchema(schema)) return "File | Blob";
157
- if (isBinarySchema(schema)) return "Uint8Array";
158
- const def = readDef(schema);
159
- if (def.type === "nullable" && def.innerType) return `${typeOf(def.innerType, collector, hint)} | null`;
160
- if (def.type === "nullish" && def.innerType) return `${typeOf(def.innerType, collector, hint)} | null`;
161
- if (def.innerType && def.type !== "object") {
162
- const { inner } = unwrapOptionalWrappers(schema);
163
- if (inner !== schema) return typeOf(inner, collector, hint);
164
- return typeOf(def.innerType, collector, hint);
165
- }
166
- if (def.type === "pipe") {
167
- const side = def.out ?? def.in;
168
- return side ? typeOf(side, collector, hint) : "unknown";
169
- }
170
- const modelName = readMetaId(schema);
171
- if (modelName !== void 0 && def.type === "object") {
172
- if (collector.claim(modelName)) collector.add({
173
- name: modelName,
174
- description: readMetaDescription(schema),
175
- body: objectBody(schema, collector, modelName)
176
- });
177
- return modelName;
178
- }
179
- switch (def.type) {
180
- case "string": return "string";
181
- case "number":
182
- case "int": return "number";
183
- case "bigint": return "string";
184
- case "boolean": return "boolean";
185
- case "date": return "string";
186
- case "null": return "null";
187
- case "any":
188
- case "unknown": return "unknown";
189
- case "never": return "never";
190
- case "void":
191
- case "undefined": return "undefined";
192
- case "literal": {
193
- const values = def.values ?? [];
194
- return values.length > 0 ? values.map((value) => quote(String(value))).join(" | ") : "never";
195
- }
196
- case "enum": {
197
- const values = Object.values(def.entries ?? {});
198
- return values.length > 0 ? values.map((value) => quote(String(value))).join(" | ") : "never";
199
- }
200
- case "array": return def.element ? `Array<${typeOf(def.element, collector, `${hint}Item`)}>` : "unknown[]";
201
- case "object": return objectBody(schema, collector, hint);
202
- case "record": return `Record<string, ${def.valueType ? typeOf(def.valueType, collector, `${hint}Value`) : "unknown"}>`;
203
- case "union": {
204
- const options = readDiscriminatedUnion(schema)?.options ?? def.options ?? [];
205
- if (options.length === 0) return "unknown";
206
- return options.map((option, index) => typeOf(option, collector, `${hint}${index}`)).join(" | ");
207
- }
208
- default: return "unknown";
209
- }
210
- };
211
- //#endregion
212
- //#region src/generator.ts
213
- const indent = (text, depth = 1) => text.split("\n").map((line) => line.length > 0 ? " ".repeat(depth) + line : line).join("\n");
214
- const pathParamNames = (path) => [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1] ?? "");
215
- /**
216
- * One server-sent event, typed from what the response declares. A record of
217
- * event name to schema becomes a union discriminated on `event`; a single
218
- * schema is the only shape a message takes.
219
- */
220
- const eventUnion = (stream, collector, namespace) => {
221
- const frame = "id?: string; retry?: number";
222
- if (stream && typeof stream === "object" && !("_zod" in stream) && !("_def" in stream)) {
223
- const arms = Object.entries(stream).map(([event, schema]) => `{ event: '${event}'; data: ${typeOf(schema, collector, `${namespace}${toPascalCase(event)}`)}; ${frame} }`);
224
- return arms.length > 0 ? arms.join(" | ") : `{ event?: string; data: string; ${frame} }`;
225
- }
226
- return `{ event?: string; data: ${typeOf(stream, collector, `${namespace}Message`)}; ${frame} }`;
227
- };
228
- const streamBody = (mode, stream, collector, namespace) => {
229
- if (mode === "binary") return "AsyncIterable<Uint8Array>";
230
- if (mode === "text") return "AsyncIterable<string>";
231
- return `AsyncIterable<${eventUnion(stream, collector, namespace)}>`;
232
- };
233
- /**
234
- * The call a route's `@example` shows: the arguments it cannot be called
235
- * without, filled with values of the shape each schema declares.
236
- */
237
- const exampleCall = (routeKey, route) => {
238
- const entries = [];
239
- const params = pathParamNames(route.path);
240
- if (params.length > 0) {
241
- const shape = route.pathParams ? readObjectShape(route.pathParams) : void 0;
242
- const fields = params.map((name) => {
243
- const declared = shape?.[name];
244
- return `${name}: ${declared ? sampleValue(declared, 0, name) : "'1'"},`;
245
- });
246
- entries.push(`params: {\n${indent(fields.join("\n"))}\n},`);
247
- }
248
- if (route.query && requiresArgument(route.query)) entries.push(`query: ${sampleObject(route.query)},`);
249
- if (route.body && requiresArgument(route.body)) entries.push(`body: ${sampleValue(route.body)},`);
250
- return `const result = await client.${routeKey}(${entries.length > 0 ? `{\n${indent(entries.join("\n"))}\n}` : ""});`;
251
- };
252
- /**
253
- * Whether a schema has anything a caller has to pass, so a route whose query is
254
- * every-field-optional keeps its example to the call itself.
255
- */
256
- const requiresArgument = (schema) => {
257
- const shape = readObjectShape(schema);
258
- if (!shape) return true;
259
- return Object.values(shape).some((field) => !unwrapOptionalWrappers(field).optional);
260
- };
261
- /**
262
- * What an editor shows above a client method: what the route is for, whether it
263
- * is on its way out, and how it is called.
264
- */
265
- const routeDoc = (routeKey, route) => {
266
- const lines = [];
267
- if (route.summary) lines.push(route.summary);
268
- if (route.description) {
269
- if (lines.length > 0) lines.push("");
270
- lines.push(route.description);
271
- }
272
- if (route.deprecated !== void 0 && route.deprecated !== false) {
273
- const message = typeof route.deprecated === "string" ? route.deprecated : typeof route.deprecated === "object" ? route.deprecated.message : void 0;
274
- if (lines.length > 0) lines.push("");
275
- lines.push(`@deprecated ${message ?? ""}`.trimEnd());
276
- }
277
- if (lines.length > 0) lines.push("");
278
- lines.push("@example", exampleCall(routeKey, route));
279
- return docComment(lines);
280
- };
281
- const emitRoute = (routeKey, key, route, collector) => {
282
- const namespace = routeKey.split(".").map(toPascalCase).join("");
283
- const members = [];
284
- const args = [];
285
- const params = pathParamNames(route.path);
286
- if (params.length > 0) {
287
- const body = route.pathParams ? typeOf(route.pathParams, collector, `${namespace}Params`) : `{\n${indent(params.map((name) => `${name}: string;`).join("\n"))}\n}`;
288
- members.push(`export type Params = ${body};`);
289
- args.push(`params: API.${namespace}.Params`);
290
- }
291
- if (route.query) {
292
- members.push(`export type Query = ${typeOf(route.query, collector, `${namespace}Query`)};`);
293
- args.push(`query?: API.${namespace}.Query`);
294
- }
295
- if (route.body) {
296
- members.push(`export type Body = ${typeOf(route.body, collector, `${namespace}Body`)};`);
297
- args.push(`body: API.${namespace}.Body`);
298
- }
299
- if (route.headers) {
300
- members.push(`export type Headers = ${typeOf(route.headers, collector, `${namespace}Headers`)};`);
301
- args.push(`headers?: API.${namespace}.Headers`);
302
- } else args.push("headers?: Record<string, string>");
303
- args.push("fetchOptions?: RequestInit");
304
- const validates = route.body !== void 0 || route.query !== void 0;
305
- const declaredBadRequest = Object.keys(route.responses).includes("400");
306
- const validationBody = validates ? typeOf(ValidationErrorSchema, collector, "ValidationError") : void 0;
307
- const results = Object.entries(route.responses).map(([status, response]) => {
308
- const headers = resolveResponseHeaders(response);
309
- const headerType = headers ? typeOf(headers, collector, `${namespace}${status}Headers`) : "Record<string, string>";
310
- if (isStreamResponse(response)) return `{ status: ${status}; body: ${streamBody(streamContentType(response).includes("event-stream") ? "events" : streamContentType(response).startsWith("text/") ? "text" : "binary", response.stream, collector, `${namespace}${status}`)}; headers: ${headerType} }`;
311
- const schema = resolveResponseBody(response);
312
- const body = schema ? typeOf(schema, collector, `${namespace}${status}`) : "undefined";
313
- return `{ status: ${status}; body: ${status === "400" && validationBody ? `${body} | ${validationBody}` : body}; headers: ${headerType} }`;
314
- });
315
- if (validationBody && !declaredBadRequest) results.push(`{ status: 400; body: ${validationBody}; headers: Record<string, string> }`);
316
- members.push(`export type Result =\n${indent(results.map((result) => `| ${result}`).join("\n"))};`);
317
- const argsType = `{\n${indent(args.map((arg) => `${arg};`).join("\n"))}\n}`;
318
- const tableEntries = [`method: '${route.method}'`, `path: '${route.path}'`];
319
- if (route.contentType) tableEntries.push(`contentType: '${route.contentType}'`);
320
- const responses = Object.entries(route.responses).map(([status, response]) => isStreamResponse(response) ? `${status}: { stream: { contentType: '${streamContentType(response)}' } }` : `${status}: {}`);
321
- tableEntries.push(`responses: {\n${indent(responses.map((entry) => `${entry},`).join("\n"))}\n}`);
322
- const streams = Object.values(route.responses).some((response) => isStreamResponse(response));
323
- return {
324
- namespace,
325
- signature: `${routeDoc(routeKey, route)}${key}: ClientMethod<'${route.method}', ${streams}, ${argsType}, API.${namespace}.Result>;`,
326
- declaration: `export namespace ${namespace} {\n${indent(members.join("\n\n"))}\n}`,
327
- table: `${key}: {\n${indent(tableEntries.map((entry) => `${entry},`).join("\n"))}\n},`
328
- };
329
- };
330
- const isRoute = (value) => !!value && typeof value === "object" && typeof value.method === "string";
331
- const emitTree = (routes, prefix, collector) => {
332
- const signatures = [];
333
- const tables = [];
334
- const declarations = [];
335
- for (const key of Object.keys(routes)) {
336
- const node = routes[key];
337
- const routeKey = prefix ? `${prefix}.${key}` : key;
338
- if (isRoute(node)) {
339
- const emitted = emitRoute(routeKey, key, node, collector);
340
- signatures.push(emitted.signature);
341
- tables.push(emitted.table);
342
- declarations.push(emitted.declaration);
343
- continue;
344
- }
345
- if (node && typeof node === "object") {
346
- const group = emitTree(node, routeKey, collector);
347
- signatures.push(`${key}: {\n${indent(group.signatures)}\n};`);
348
- tables.push(`${key}: {\n${indent(group.table)}\n},`);
349
- declarations.push(...group.declarations);
350
- }
351
- }
352
- return {
353
- signatures: signatures.join("\n"),
354
- table: tables.join("\n"),
355
- declarations
356
- };
357
- };
358
- /**
359
- * Renders a TypeScript client from an api: its named types, the route table
360
- * the runtime reads, and a `createClient` typed against both.
361
- */
362
- /**
363
- * The headers a request context declares, as one interface the caller fills
364
- * once. Absent when the API declares no request context that reads headers.
365
- */
366
- const emitRequestContext = (contract, collector) => {
367
- const declarations = Object.values(contract.requestContext ?? {});
368
- const fields = [];
369
- for (const declaration of declarations) {
370
- const headers = declaration.headers;
371
- if (!headers) continue;
372
- const inner = typeOf(headers, collector, "RequestContext").trim().replace(/^\{/, "").replace(/\}$/, "").split("\n").map((line) => line.trim()).filter((line) => line !== "").join("\n");
373
- if (inner !== "") fields.push(inner);
374
- }
375
- return fields.length > 0 ? `export interface RequestContext {\n${indent(fields.join("\n"))}\n}` : void 0;
376
- };
377
- const generateFetchClient = (contract, options = {}) => {
378
- const { runtimeModule = "@kizunajs/fetch", regenerateCommand = "kizuna generate", source } = options;
379
- const collector = new TypeCollector();
380
- const tree = emitTree(contract.routes, "", collector);
381
- const api = [...collector.all().map((model) => {
382
- return `${model.description ? `/**\n * ${model.description}\n */\n` : ""}export type ${model.name} = ${model.body};`;
383
- }), ...tree.declarations].join("\n\n");
384
- const requestContext = emitRequestContext(contract, collector);
385
- return `/* eslint-disable */
386
- /**
387
- ${[
388
- " * Generated by @kizunajs/fetch. Do not edit.",
389
- " *",
390
- ...source === void 0 ? [] : [` * Source: ${source}`],
391
- ` * Regenerate: ${regenerateCommand}`
392
- ].join("\n")}
393
- */
394
- import { createGeneratedClient, type ClientConfig, type ClientMethod, type GeneratedRoutes } from '${runtimeModule}';
395
-
396
- export namespace API {
397
- ${indent(api)}
398
- }
399
-
400
- export interface Client {
401
- ${indent(tree.signatures)}
402
- }
403
-
404
- const routes: GeneratedRoutes = {
405
- ${indent(tree.table)}
406
- };
407
-
408
- ${requestContext === void 0 ? `/**
409
- * A client for this API. Pass the base URL and anything else the runtime takes.
410
- */
411
- export const createClient = (config: ClientConfig): Client => createGeneratedClient(routes, config) as unknown as Client;` : `${requestContext}
412
-
413
- /**
414
- * A client for this API. Pass the base URL, the request context headers this
415
- * API declares, and anything else the runtime takes.
416
- */
417
- export const createClient = (config: ClientConfig & { requestContext?: RequestContext }): Client =>
418
- createGeneratedClient(routes, config) as unknown as Client;`}
419
- `;
420
- };
421
- //#endregion
422
- //#region src/client-target.ts
423
- /**
424
- * A TypeScript client target for `kizuna.config.ts`.
425
- *
426
- * @example
427
- * fetchClient({ output: './packages/api-client/src/generated.ts' });
428
- */
429
- const fetchClient = ({ output, ...options }) => ({
430
- kind: "fetch",
431
- output,
432
- generate: (contract) => generateFetchClient(contract, options)
433
- });
434
- //#endregion
435
- export { fetchClient, generateFetchClient };