@donkeylabs/server 0.3.0 → 0.4.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/LICENSE +1 -1
- package/docs/api-client.md +7 -7
- package/docs/cache.md +1 -74
- package/docs/core-services.md +4 -116
- package/docs/cron.md +1 -1
- package/docs/errors.md +2 -2
- package/docs/events.md +3 -98
- package/docs/handlers.md +13 -48
- package/docs/logger.md +3 -58
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +13 -64
- package/docs/project-structure.md +4 -142
- package/docs/rate-limiter.md +4 -136
- package/docs/router.md +6 -14
- package/docs/sse.md +1 -99
- package/docs/sveltekit-adapter.md +420 -0
- package/package.json +8 -11
- package/registry.d.ts +15 -14
- package/src/core/cache.ts +0 -75
- package/src/core/cron.ts +3 -96
- package/src/core/errors.ts +78 -11
- package/src/core/events.ts +1 -47
- package/src/core/index.ts +0 -4
- package/src/core/jobs.ts +0 -112
- package/src/core/logger.ts +12 -79
- package/src/core/rate-limiter.ts +29 -108
- package/src/core/sse.ts +1 -84
- package/src/core.ts +13 -104
- package/src/generator/index.ts +566 -0
- package/src/generator/zod-to-ts.ts +114 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +30 -24
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/router.ts +47 -1
- package/src/server.ts +618 -332
- package/README.md +0 -254
- package/cli/commands/dev.ts +0 -134
- package/cli/commands/generate.ts +0 -605
- package/cli/commands/init.ts +0 -205
- package/cli/commands/interactive.ts +0 -417
- package/cli/commands/plugin.ts +0 -192
- package/cli/commands/route.ts +0 -195
- package/cli/donkeylabs +0 -2
- package/cli/index.ts +0 -114
- package/docs/svelte-frontend.md +0 -324
- package/docs/testing.md +0 -438
- package/mcp/donkeylabs-mcp +0 -3238
- package/mcp/server.ts +0 -3238
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generator Building Blocks
|
|
3
|
+
*
|
|
4
|
+
* This module exports reusable functions for generating API clients.
|
|
5
|
+
* Adapters (like @donkeylabs/adapter-sveltekit) can import these functions
|
|
6
|
+
* and compose them with custom options to generate framework-specific clients.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ==========================================
|
|
10
|
+
// Types
|
|
11
|
+
// ==========================================
|
|
12
|
+
|
|
13
|
+
export interface RouteInfo {
|
|
14
|
+
name: string;
|
|
15
|
+
prefix: string;
|
|
16
|
+
routeName: string;
|
|
17
|
+
handler: "typed" | "raw" | string;
|
|
18
|
+
inputSource?: string;
|
|
19
|
+
outputSource?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EventInfo {
|
|
23
|
+
name: string;
|
|
24
|
+
plugin: string;
|
|
25
|
+
schemaSource: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ClientConfigInfo {
|
|
29
|
+
plugin: string;
|
|
30
|
+
credentials?: "include" | "same-origin" | "omit";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GeneratorConfig {
|
|
34
|
+
routes: RouteInfo[] | ExtractedRoute[];
|
|
35
|
+
events?: EventInfo[];
|
|
36
|
+
clientConfigs?: ClientConfigInfo[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ExtractedRoute {
|
|
40
|
+
name: string;
|
|
41
|
+
handler: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ClientGeneratorOptions {
|
|
45
|
+
/** Import statement for base class */
|
|
46
|
+
baseImport: string;
|
|
47
|
+
/** Base class name to extend */
|
|
48
|
+
baseClass: string;
|
|
49
|
+
/** Constructor parameters signature */
|
|
50
|
+
constructorSignature: string;
|
|
51
|
+
/** Constructor body implementation */
|
|
52
|
+
constructorBody: string;
|
|
53
|
+
/** Additional imports to include */
|
|
54
|
+
additionalImports?: string[];
|
|
55
|
+
/** Custom factory function (replaces default) */
|
|
56
|
+
factoryFunction?: string;
|
|
57
|
+
/** Additional class members to include */
|
|
58
|
+
additionalMembers?: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Default options for standard HTTP-only client */
|
|
62
|
+
export const defaultGeneratorOptions: ClientGeneratorOptions = {
|
|
63
|
+
baseImport: 'import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";',
|
|
64
|
+
baseClass: "ApiClientBase<{}>",
|
|
65
|
+
constructorSignature: "baseUrl: string, options?: ApiClientOptions",
|
|
66
|
+
constructorBody: "super(baseUrl, options);",
|
|
67
|
+
factoryFunction: `export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
|
|
68
|
+
return new ApiClient(baseUrl, options);
|
|
69
|
+
}`,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ==========================================
|
|
73
|
+
// Utility Functions
|
|
74
|
+
// ==========================================
|
|
75
|
+
|
|
76
|
+
export function toPascalCase(str: string): string {
|
|
77
|
+
return str
|
|
78
|
+
.split(/[-_.]/)
|
|
79
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
80
|
+
.join("");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function toCamelCase(str: string): string {
|
|
84
|
+
const pascal = toPascalCase(str);
|
|
85
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Split string by delimiter, respecting nested brackets
|
|
90
|
+
*/
|
|
91
|
+
export function splitTopLevel(source: string, delimiter: string): string[] {
|
|
92
|
+
const result: string[] = [];
|
|
93
|
+
let current = "";
|
|
94
|
+
let depth = 0;
|
|
95
|
+
|
|
96
|
+
for (const char of source) {
|
|
97
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
98
|
+
depth++;
|
|
99
|
+
current += char;
|
|
100
|
+
} else if (char === ")" || char === "]" || char === "}") {
|
|
101
|
+
depth--;
|
|
102
|
+
current += char;
|
|
103
|
+
} else if (char === delimiter && depth === 0) {
|
|
104
|
+
if (current.trim()) {
|
|
105
|
+
result.push(current.trim());
|
|
106
|
+
}
|
|
107
|
+
current = "";
|
|
108
|
+
} else {
|
|
109
|
+
current += char;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (current.trim()) {
|
|
114
|
+
result.push(current.trim());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract content between balanced parentheses starting at a given position
|
|
122
|
+
*/
|
|
123
|
+
export function extractParenContent(source: string, startPos: number): string {
|
|
124
|
+
let depth = 0;
|
|
125
|
+
let start = -1;
|
|
126
|
+
let end = -1;
|
|
127
|
+
|
|
128
|
+
for (let i = startPos; i < source.length; i++) {
|
|
129
|
+
if (source[i] === "(") {
|
|
130
|
+
if (depth === 0) start = i;
|
|
131
|
+
depth++;
|
|
132
|
+
} else if (source[i] === ")") {
|
|
133
|
+
depth--;
|
|
134
|
+
if (depth === 0) {
|
|
135
|
+
end = i;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (start !== -1 && end !== -1) {
|
|
142
|
+
return source.slice(start + 1, end);
|
|
143
|
+
}
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ==========================================
|
|
148
|
+
// Zod to TypeScript Conversion
|
|
149
|
+
// ==========================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse object property definitions from Zod source
|
|
153
|
+
*/
|
|
154
|
+
function parseObjectProps(
|
|
155
|
+
propsSource: string
|
|
156
|
+
): { name: string; schema: string; optional: boolean }[] {
|
|
157
|
+
const props: { name: string; schema: string; optional: boolean }[] = [];
|
|
158
|
+
const entries = splitTopLevel(propsSource, ",");
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const colonIndex = entry.indexOf(":");
|
|
162
|
+
if (colonIndex === -1) continue;
|
|
163
|
+
|
|
164
|
+
const name = entry.slice(0, colonIndex).trim();
|
|
165
|
+
let schema = entry.slice(colonIndex + 1).trim();
|
|
166
|
+
|
|
167
|
+
const optional = schema.endsWith(".optional()");
|
|
168
|
+
if (optional) {
|
|
169
|
+
schema = schema.slice(0, -".optional()".length);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
props.push({ name, schema, optional });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return props;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert Zod schema source to TypeScript type string
|
|
180
|
+
*/
|
|
181
|
+
export function zodToTypeScript(zodSource: string | undefined): string {
|
|
182
|
+
if (!zodSource) return "unknown";
|
|
183
|
+
|
|
184
|
+
const typeMap: Record<string, string> = {
|
|
185
|
+
"z.string()": "string",
|
|
186
|
+
"z.number()": "number",
|
|
187
|
+
"z.boolean()": "boolean",
|
|
188
|
+
"z.null()": "null",
|
|
189
|
+
"z.undefined()": "undefined",
|
|
190
|
+
"z.void()": "void",
|
|
191
|
+
"z.any()": "any",
|
|
192
|
+
"z.unknown()": "unknown",
|
|
193
|
+
"z.never()": "never",
|
|
194
|
+
"z.date()": "Date",
|
|
195
|
+
"z.bigint()": "bigint",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (typeMap[zodSource]) return typeMap[zodSource];
|
|
199
|
+
|
|
200
|
+
let source = zodSource;
|
|
201
|
+
let suffix = "";
|
|
202
|
+
|
|
203
|
+
if (source.endsWith(".optional()")) {
|
|
204
|
+
source = source.slice(0, -".optional()".length);
|
|
205
|
+
suffix = " | undefined";
|
|
206
|
+
} else if (source.endsWith(".nullable()")) {
|
|
207
|
+
source = source.slice(0, -".nullable()".length);
|
|
208
|
+
suffix = " | null";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// z.object({ ... })
|
|
212
|
+
if (source.startsWith("z.object(")) {
|
|
213
|
+
const innerContent = extractParenContent(source, 8);
|
|
214
|
+
const trimmed = innerContent.trim();
|
|
215
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
216
|
+
const propsSource = trimmed.slice(1, -1).trim();
|
|
217
|
+
const props = parseObjectProps(propsSource);
|
|
218
|
+
const typeProps = props
|
|
219
|
+
.map((p) => ` ${p.name}${p.optional ? "?" : ""}: ${zodToTypeScript(p.schema)};`)
|
|
220
|
+
.join("\n");
|
|
221
|
+
return `{\n${typeProps}\n}${suffix}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// z.array(...)
|
|
226
|
+
if (source.startsWith("z.array(")) {
|
|
227
|
+
const innerContent = extractParenContent(source, 7);
|
|
228
|
+
if (innerContent) {
|
|
229
|
+
return `${zodToTypeScript(innerContent.trim())}[]${suffix}`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// z.enum([...])
|
|
234
|
+
const enumMatch = source.match(/z\.enum\s*\(\s*\[([^\]]+)\]\s*\)/);
|
|
235
|
+
if (enumMatch?.[1]) {
|
|
236
|
+
const values = enumMatch[1]
|
|
237
|
+
.split(",")
|
|
238
|
+
.map((v) => v.trim())
|
|
239
|
+
.filter(Boolean);
|
|
240
|
+
return values.join(" | ") + suffix;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// z.literal(...)
|
|
244
|
+
const literalMatch = source.match(/z\.literal\s*\(\s*([^)]+)\s*\)/);
|
|
245
|
+
if (literalMatch?.[1]) {
|
|
246
|
+
return literalMatch[1].trim() + suffix;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// z.union([...])
|
|
250
|
+
const unionMatch = source.match(/z\.union\s*\(\s*\[([^\]]+)\]\s*\)/);
|
|
251
|
+
if (unionMatch?.[1]) {
|
|
252
|
+
const schemas = splitTopLevel(unionMatch[1], ",");
|
|
253
|
+
return schemas.map((s) => zodToTypeScript(s.trim())).join(" | ") + suffix;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// z.record(...)
|
|
257
|
+
const recordMatch = source.match(/z\.record\s*\(\s*([^)]+)\s*\)/);
|
|
258
|
+
if (recordMatch?.[1]) {
|
|
259
|
+
const parts = splitTopLevel(recordMatch[1], ",");
|
|
260
|
+
if (parts.length === 2) {
|
|
261
|
+
return `Record<${zodToTypeScript(parts[0]?.trim())}, ${zodToTypeScript(parts[1]?.trim())}>${suffix}`;
|
|
262
|
+
}
|
|
263
|
+
return `Record<string, ${zodToTypeScript(recordMatch[1].trim())}>${suffix}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// z.tuple([...])
|
|
267
|
+
const tupleMatch = source.match(/z\.tuple\s*\(\s*\[([^\]]+)\]\s*\)/);
|
|
268
|
+
if (tupleMatch?.[1]) {
|
|
269
|
+
const schemas = splitTopLevel(tupleMatch[1], ",");
|
|
270
|
+
return `[${schemas.map((s) => zodToTypeScript(s.trim())).join(", ")}]${suffix}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// z.string().min/max/email/etc
|
|
274
|
+
if (source.startsWith("z.string()")) return "string" + suffix;
|
|
275
|
+
if (source.startsWith("z.number()")) return "number" + suffix;
|
|
276
|
+
|
|
277
|
+
return "unknown" + suffix;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ==========================================
|
|
281
|
+
// Route Grouping
|
|
282
|
+
// ==========================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Group routes by prefix for namespace organization
|
|
286
|
+
*/
|
|
287
|
+
export function groupRoutesByPrefix(routes: RouteInfo[]): Map<string, RouteInfo[]> {
|
|
288
|
+
const groups = new Map<string, RouteInfo[]>();
|
|
289
|
+
|
|
290
|
+
for (const route of routes) {
|
|
291
|
+
const prefix = route.prefix || "_root";
|
|
292
|
+
if (!groups.has(prefix)) {
|
|
293
|
+
groups.set(prefix, []);
|
|
294
|
+
}
|
|
295
|
+
groups.get(prefix)!.push(route);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return groups;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ==========================================
|
|
302
|
+
// Client Code Generation
|
|
303
|
+
// ==========================================
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Generate client code from extracted routes (simple format)
|
|
307
|
+
* This is used by the CLI command which extracts routes by running the server
|
|
308
|
+
*/
|
|
309
|
+
export function generateClientFromRoutes(
|
|
310
|
+
routes: ExtractedRoute[],
|
|
311
|
+
options: Partial<ClientGeneratorOptions> = {}
|
|
312
|
+
): string {
|
|
313
|
+
const opts = { ...defaultGeneratorOptions, ...options };
|
|
314
|
+
|
|
315
|
+
// Check if all routes share a common prefix (e.g., "api.") - if so, skip it
|
|
316
|
+
// Common prefix stripping is disabled to respect explicit router nesting
|
|
317
|
+
const routesToProcess = routes;
|
|
318
|
+
|
|
319
|
+
// Group routes by namespace
|
|
320
|
+
const tree = new Map<string, Map<string, { method: string; fullName: string }[]>>();
|
|
321
|
+
|
|
322
|
+
for (const route of routesToProcess) {
|
|
323
|
+
// Find original route name for the actual request
|
|
324
|
+
const originalRoute = routes.find(r => r.name.endsWith(route.name));
|
|
325
|
+
const fullName = originalRoute?.name || route.name;
|
|
326
|
+
|
|
327
|
+
const parts = route.name.split(".");
|
|
328
|
+
if (parts.length < 2) {
|
|
329
|
+
const ns = "";
|
|
330
|
+
if (!tree.has(ns)) tree.set(ns, new Map());
|
|
331
|
+
const rootMethods = tree.get(ns)!;
|
|
332
|
+
if (!rootMethods.has("")) rootMethods.set("", []);
|
|
333
|
+
rootMethods.get("")!.push({ method: parts[0]!, fullName });
|
|
334
|
+
} else if (parts.length === 2) {
|
|
335
|
+
const [ns, method] = parts;
|
|
336
|
+
if (!tree.has(ns!)) tree.set(ns!, new Map());
|
|
337
|
+
const nsMethods = tree.get(ns!)!;
|
|
338
|
+
if (!nsMethods.has("")) nsMethods.set("", []);
|
|
339
|
+
nsMethods.get("")!.push({ method: method!, fullName });
|
|
340
|
+
} else {
|
|
341
|
+
const [ns, sub, ...rest] = parts;
|
|
342
|
+
const method = rest.join(".");
|
|
343
|
+
if (!tree.has(ns!)) tree.set(ns!, new Map());
|
|
344
|
+
const nsMethods = tree.get(ns!)!;
|
|
345
|
+
if (!nsMethods.has(sub!)) nsMethods.set(sub!, []);
|
|
346
|
+
nsMethods.get(sub!)!.push({ method: method || sub!, fullName });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Generate method definitions
|
|
351
|
+
const namespaceBlocks: string[] = [];
|
|
352
|
+
|
|
353
|
+
for (const [namespace, subNamespaces] of tree) {
|
|
354
|
+
if (namespace === "") {
|
|
355
|
+
const rootMethods = subNamespaces.get("");
|
|
356
|
+
if (rootMethods && rootMethods.length > 0) {
|
|
357
|
+
for (const { method, fullName } of rootMethods) {
|
|
358
|
+
const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
359
|
+
namespaceBlocks.push(` ${methodName} = (input: any) => this.request("${fullName}", input);`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const subBlocks: string[] = [];
|
|
366
|
+
for (const [sub, methods] of subNamespaces) {
|
|
367
|
+
if (sub === "") {
|
|
368
|
+
for (const { method, fullName } of methods) {
|
|
369
|
+
const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
370
|
+
subBlocks.push(` ${methodName}: (input: any) => this.request("${fullName}", input)`);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
const subMethods = methods.map(({ method, fullName }) => {
|
|
374
|
+
const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
375
|
+
return ` ${methodName}: (input: any) => this.request("${fullName}", input)`;
|
|
376
|
+
});
|
|
377
|
+
subBlocks.push(` ${sub}: {\n${subMethods.join(",\n")}\n }`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
namespaceBlocks.push(` ${namespace} = {\n${subBlocks.join(",\n")}\n };`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Build additional imports
|
|
385
|
+
const additionalImportsStr = opts.additionalImports?.length
|
|
386
|
+
? "\n" + opts.additionalImports.join("\n")
|
|
387
|
+
: "";
|
|
388
|
+
|
|
389
|
+
// Build additional members
|
|
390
|
+
const additionalMembersStr = opts.additionalMembers?.length
|
|
391
|
+
? "\n\n" + opts.additionalMembers.join("\n\n")
|
|
392
|
+
: "";
|
|
393
|
+
|
|
394
|
+
return `// Auto-generated by donkeylabs generate
|
|
395
|
+
// DO NOT EDIT MANUALLY
|
|
396
|
+
|
|
397
|
+
${opts.baseImport}${additionalImportsStr}
|
|
398
|
+
|
|
399
|
+
export class ApiClient extends ${opts.baseClass} {
|
|
400
|
+
constructor(${opts.constructorSignature}) {
|
|
401
|
+
${opts.constructorBody}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
${namespaceBlocks.join("\n\n") || " // No routes defined"}${additionalMembersStr}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
${opts.factoryFunction}
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Generate fully-typed client code with route types (advanced format)
|
|
413
|
+
* This is used by the standalone script which parses source files
|
|
414
|
+
*/
|
|
415
|
+
export function generateClientCode(
|
|
416
|
+
ctx: GeneratorConfig,
|
|
417
|
+
options: Partial<ClientGeneratorOptions> = {}
|
|
418
|
+
): string {
|
|
419
|
+
const { routes, events = [], clientConfigs = [] } = ctx;
|
|
420
|
+
const opts = { ...defaultGeneratorOptions, ...options };
|
|
421
|
+
|
|
422
|
+
// Check if routes are simple ExtractedRoute format
|
|
423
|
+
if (routes.length > 0 && !("prefix" in routes[0])) {
|
|
424
|
+
return generateClientFromRoutes(routes as ExtractedRoute[], options);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const routeInfos = routes as RouteInfo[];
|
|
428
|
+
const defaultCredentials =
|
|
429
|
+
clientConfigs.find((c) => c.credentials)?.credentials || "include";
|
|
430
|
+
|
|
431
|
+
const routeGroups = groupRoutesByPrefix(routeInfos);
|
|
432
|
+
|
|
433
|
+
// Generate route type definitions
|
|
434
|
+
const routeTypeBlocks: string[] = [];
|
|
435
|
+
const routeNamespaceBlocks: string[] = [];
|
|
436
|
+
|
|
437
|
+
for (const [prefix, prefixRoutes] of routeGroups) {
|
|
438
|
+
const namespaceName = prefix === "_root" ? "Root" : toPascalCase(prefix);
|
|
439
|
+
const methodName = prefix === "_root" ? "_root" : prefix;
|
|
440
|
+
|
|
441
|
+
const typeEntries = prefixRoutes
|
|
442
|
+
.filter((r) => r.handler === "typed")
|
|
443
|
+
.map((r) => {
|
|
444
|
+
const inputType = zodToTypeScript(r.inputSource);
|
|
445
|
+
const outputType = zodToTypeScript(r.outputSource);
|
|
446
|
+
const routeNs = toPascalCase(r.routeName);
|
|
447
|
+
return ` export namespace ${routeNs} {
|
|
448
|
+
export type Input = ${inputType};
|
|
449
|
+
export type Output = ${outputType};
|
|
450
|
+
}
|
|
451
|
+
export type ${routeNs} = { Input: ${routeNs}.Input; Output: ${routeNs}.Output };`;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (typeEntries.length > 0) {
|
|
455
|
+
routeTypeBlocks.push(` export namespace ${namespaceName} {
|
|
456
|
+
${typeEntries.join("\n\n")}
|
|
457
|
+
}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const methodEntries = prefixRoutes
|
|
461
|
+
.filter((r) => r.handler === "typed")
|
|
462
|
+
.map((r) => {
|
|
463
|
+
const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`;
|
|
464
|
+
const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Output`;
|
|
465
|
+
return ` ${toCamelCase(r.routeName)}: (input: ${inputType}, options?: RequestOptions): Promise<${outputType}> =>
|
|
466
|
+
this.request("${r.name}", input, options)`;
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const rawMethodEntries = prefixRoutes
|
|
470
|
+
.filter((r) => r.handler === "raw")
|
|
471
|
+
.map((r) => {
|
|
472
|
+
return ` ${toCamelCase(r.routeName)}: (init?: RequestInit): Promise<Response> =>
|
|
473
|
+
this.rawRequest("${r.name}", init)`;
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const allMethods = [...methodEntries, ...rawMethodEntries];
|
|
477
|
+
|
|
478
|
+
if (allMethods.length > 0) {
|
|
479
|
+
routeNamespaceBlocks.push(` ${methodName} = {
|
|
480
|
+
${allMethods.join(",\n\n")}
|
|
481
|
+
};`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Generate event types
|
|
486
|
+
const eventTypeEntries = events.map((e) => {
|
|
487
|
+
const type = zodToTypeScript(e.schemaSource);
|
|
488
|
+
return ` "${e.name}": ${type};`;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const eventTypesBlock =
|
|
492
|
+
eventTypeEntries.length > 0
|
|
493
|
+
? `export interface SSEEvents {
|
|
494
|
+
${eventTypeEntries.join("\n")}
|
|
495
|
+
}`
|
|
496
|
+
: `export interface SSEEvents {}`;
|
|
497
|
+
|
|
498
|
+
// Build additional imports
|
|
499
|
+
const additionalImportsStr = opts.additionalImports?.length
|
|
500
|
+
? "\n" + opts.additionalImports.join("\n")
|
|
501
|
+
: "";
|
|
502
|
+
|
|
503
|
+
return `// Auto-generated by scripts/generate-client.ts
|
|
504
|
+
// DO NOT EDIT MANUALLY
|
|
505
|
+
|
|
506
|
+
import {
|
|
507
|
+
ApiClientBase,
|
|
508
|
+
ApiError,
|
|
509
|
+
ValidationError,
|
|
510
|
+
type RequestOptions,
|
|
511
|
+
type ApiClientOptions,
|
|
512
|
+
type SSEOptions,
|
|
513
|
+
} from "./base";${additionalImportsStr}
|
|
514
|
+
|
|
515
|
+
// ============================================
|
|
516
|
+
// Route Types
|
|
517
|
+
// ============================================
|
|
518
|
+
|
|
519
|
+
export namespace Routes {
|
|
520
|
+
${routeTypeBlocks.join("\n\n") || " // No typed routes found"}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ============================================
|
|
524
|
+
// SSE Event Types
|
|
525
|
+
// ============================================
|
|
526
|
+
|
|
527
|
+
${eventTypesBlock}
|
|
528
|
+
|
|
529
|
+
// ============================================
|
|
530
|
+
// API Client
|
|
531
|
+
// ============================================
|
|
532
|
+
|
|
533
|
+
export interface ApiClientConfig extends ApiClientOptions {
|
|
534
|
+
baseUrl: string;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export class ApiClient extends ApiClientBase<SSEEvents> {
|
|
538
|
+
constructor(config: ApiClientConfig) {
|
|
539
|
+
super(config.baseUrl, {
|
|
540
|
+
credentials: "${defaultCredentials}",
|
|
541
|
+
...config,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ==========================================
|
|
546
|
+
// Route Namespaces
|
|
547
|
+
// ==========================================
|
|
548
|
+
|
|
549
|
+
${routeNamespaceBlocks.join("\n\n") || " // No routes defined"}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ============================================
|
|
553
|
+
// Factory Function
|
|
554
|
+
// ============================================
|
|
555
|
+
|
|
556
|
+
export function createApiClient(config: ApiClientConfig): ApiClient {
|
|
557
|
+
return new ApiClient(config);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Re-export base types for convenience
|
|
561
|
+
export { ApiError, ValidationError, type RequestOptions, type SSEOptions };
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Re-export runtime Zod to TypeScript converter
|
|
566
|
+
export { zodSchemaToTs } from "./zod-to-ts";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert a Zod schema to TypeScript type string at runtime
|
|
5
|
+
* Uses Zod's internal _def to introspect the schema
|
|
6
|
+
*/
|
|
7
|
+
export function zodSchemaToTs(schema: z.ZodType<any>): string {
|
|
8
|
+
return convertZodType(schema);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function convertZodType(schema: z.ZodType<any>): string {
|
|
12
|
+
const def = (schema as any)._def;
|
|
13
|
+
const typeName = def?.typeName;
|
|
14
|
+
|
|
15
|
+
switch (typeName) {
|
|
16
|
+
case "ZodString":
|
|
17
|
+
return "string";
|
|
18
|
+
|
|
19
|
+
case "ZodNumber":
|
|
20
|
+
return "number";
|
|
21
|
+
|
|
22
|
+
case "ZodBoolean":
|
|
23
|
+
return "boolean";
|
|
24
|
+
|
|
25
|
+
case "ZodDate":
|
|
26
|
+
return "Date";
|
|
27
|
+
|
|
28
|
+
case "ZodUndefined":
|
|
29
|
+
return "undefined";
|
|
30
|
+
|
|
31
|
+
case "ZodNull":
|
|
32
|
+
return "null";
|
|
33
|
+
|
|
34
|
+
case "ZodAny":
|
|
35
|
+
return "any";
|
|
36
|
+
|
|
37
|
+
case "ZodUnknown":
|
|
38
|
+
return "unknown";
|
|
39
|
+
|
|
40
|
+
case "ZodVoid":
|
|
41
|
+
return "void";
|
|
42
|
+
|
|
43
|
+
case "ZodNever":
|
|
44
|
+
return "never";
|
|
45
|
+
|
|
46
|
+
case "ZodLiteral":
|
|
47
|
+
const value = def.value;
|
|
48
|
+
return typeof value === "string" ? `"${value}"` : String(value);
|
|
49
|
+
|
|
50
|
+
case "ZodArray":
|
|
51
|
+
const itemType = convertZodType(def.type);
|
|
52
|
+
return `${itemType}[]`;
|
|
53
|
+
|
|
54
|
+
case "ZodObject":
|
|
55
|
+
const shape = def.shape();
|
|
56
|
+
const props = Object.entries(shape).map(([key, value]) => {
|
|
57
|
+
const propSchema = value as z.ZodType<any>;
|
|
58
|
+
const isOptional = (propSchema as any)._def?.typeName === "ZodOptional";
|
|
59
|
+
const innerType = isOptional
|
|
60
|
+
? convertZodType((propSchema as any)._def.innerType)
|
|
61
|
+
: convertZodType(propSchema);
|
|
62
|
+
return ` ${key}${isOptional ? "?" : ""}: ${innerType};`;
|
|
63
|
+
});
|
|
64
|
+
return `{\n${props.join("\n")}\n}`;
|
|
65
|
+
|
|
66
|
+
case "ZodOptional":
|
|
67
|
+
return convertZodType(def.innerType);
|
|
68
|
+
|
|
69
|
+
case "ZodNullable":
|
|
70
|
+
return `${convertZodType(def.innerType)} | null`;
|
|
71
|
+
|
|
72
|
+
case "ZodDefault":
|
|
73
|
+
return convertZodType(def.innerType);
|
|
74
|
+
|
|
75
|
+
case "ZodUnion":
|
|
76
|
+
const options = def.options.map((opt: z.ZodType<any>) => convertZodType(opt));
|
|
77
|
+
return options.join(" | ");
|
|
78
|
+
|
|
79
|
+
case "ZodEnum":
|
|
80
|
+
return def.values.map((v: string) => `"${v}"`).join(" | ");
|
|
81
|
+
|
|
82
|
+
case "ZodNativeEnum":
|
|
83
|
+
return "number | string"; // Simplified
|
|
84
|
+
|
|
85
|
+
case "ZodRecord":
|
|
86
|
+
const keyType = def.keyType ? convertZodType(def.keyType) : "string";
|
|
87
|
+
const valueType = convertZodType(def.valueType);
|
|
88
|
+
return `Record<${keyType}, ${valueType}>`;
|
|
89
|
+
|
|
90
|
+
case "ZodTuple":
|
|
91
|
+
const items = def.items.map((item: z.ZodType<any>) => convertZodType(item));
|
|
92
|
+
return `[${items.join(", ")}]`;
|
|
93
|
+
|
|
94
|
+
case "ZodPromise":
|
|
95
|
+
return `Promise<${convertZodType(def.type)}>`;
|
|
96
|
+
|
|
97
|
+
case "ZodEffects":
|
|
98
|
+
// .transform(), .refine(), etc - use the inner schema
|
|
99
|
+
return convertZodType(def.schema);
|
|
100
|
+
|
|
101
|
+
case "ZodLazy":
|
|
102
|
+
// Lazy schemas - try to resolve
|
|
103
|
+
return convertZodType(def.getter());
|
|
104
|
+
|
|
105
|
+
case "ZodIntersection":
|
|
106
|
+
const left = convertZodType(def.left);
|
|
107
|
+
const right = convertZodType(def.right);
|
|
108
|
+
return `${left} & ${right}`;
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
// Fallback for unknown types
|
|
112
|
+
return "unknown";
|
|
113
|
+
}
|
|
114
|
+
}
|