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