@donkeylabs/server 0.3.0 → 0.3.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.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/docs/api-client.md +7 -7
  3. package/docs/cache.md +1 -74
  4. package/docs/core-services.md +4 -116
  5. package/docs/cron.md +1 -1
  6. package/docs/errors.md +2 -2
  7. package/docs/events.md +3 -98
  8. package/docs/handlers.md +13 -48
  9. package/docs/logger.md +3 -58
  10. package/docs/middleware.md +2 -2
  11. package/docs/plugins.md +13 -64
  12. package/docs/project-structure.md +4 -142
  13. package/docs/rate-limiter.md +4 -136
  14. package/docs/router.md +6 -14
  15. package/docs/sse.md +1 -99
  16. package/docs/sveltekit-adapter.md +420 -0
  17. package/package.json +6 -6
  18. package/registry.d.ts +15 -14
  19. package/src/core/cache.ts +0 -75
  20. package/src/core/cron.ts +3 -96
  21. package/src/core/errors.ts +78 -11
  22. package/src/core/events.ts +1 -47
  23. package/src/core/index.ts +0 -4
  24. package/src/core/jobs.ts +0 -112
  25. package/src/core/logger.ts +12 -79
  26. package/src/core/rate-limiter.ts +29 -108
  27. package/src/core/sse.ts +1 -84
  28. package/src/core.ts +13 -104
  29. package/src/generator/index.ts +551 -0
  30. package/src/handlers.ts +14 -110
  31. package/src/index.ts +19 -23
  32. package/src/middleware.ts +2 -5
  33. package/src/registry.ts +4 -0
  34. package/src/server.ts +354 -337
  35. package/README.md +0 -254
  36. package/cli/commands/dev.ts +0 -134
  37. package/cli/commands/generate.ts +0 -605
  38. package/cli/commands/init.ts +0 -205
  39. package/cli/commands/interactive.ts +0 -417
  40. package/cli/commands/plugin.ts +0 -192
  41. package/cli/commands/route.ts +0 -195
  42. package/cli/donkeylabs +0 -2
  43. package/cli/index.ts +0 -114
  44. package/docs/svelte-frontend.md +0 -324
  45. package/docs/testing.md +0 -438
  46. package/mcp/donkeylabs-mcp +0 -3238
  47. package/mcp/server.ts +0 -3238
@@ -1,605 +0,0 @@
1
- import { readdir, writeFile, readFile, mkdir } from "node:fs/promises";
2
- import { join, relative, dirname, basename } from "node:path";
3
- import { existsSync } from "node:fs";
4
- import { spawn } from "node:child_process";
5
- import pc from "picocolors";
6
-
7
- interface DonkeylabsConfig {
8
- plugins: string[];
9
- outDir?: string;
10
- client?: {
11
- output: string;
12
- };
13
- routes?: string; // Route files pattern, default: "./src/routes/**/handler.ts"
14
- entry?: string; // Server entry file for extracting routes, default: "./src/index.ts"
15
- }
16
-
17
- async function loadConfig(): Promise<DonkeylabsConfig> {
18
- const configPath = join(process.cwd(), "donkeylabs.config.ts");
19
-
20
- if (!existsSync(configPath)) {
21
- throw new Error("donkeylabs.config.ts not found. Run 'donkeylabs init' first.");
22
- }
23
-
24
- const config = await import(configPath);
25
- return config.default;
26
- }
27
-
28
- async function getPluginExportName(pluginPath: string): Promise<string | null> {
29
- try {
30
- const content = await readFile(pluginPath, "utf-8");
31
- const match = content.match(/export\s+const\s+(\w+Plugin)\s*=/);
32
- return match?.[1] ?? null;
33
- } catch {
34
- return null;
35
- }
36
- }
37
-
38
- async function extractHandlerNames(pluginPath: string): Promise<string[]> {
39
- try {
40
- const content = await readFile(pluginPath, "utf-8");
41
- const handlersMatch = content.match(/handlers:\s*\{([^}]+)\}/);
42
- if (!handlersMatch?.[1]) return [];
43
-
44
- const handlersBlock = handlersMatch[1];
45
- return [...handlersBlock.matchAll(/(\w+)\s*:/g)]
46
- .map((m) => m[1])
47
- .filter((name): name is string => !!name);
48
- } catch {
49
- return [];
50
- }
51
- }
52
-
53
- async function extractMiddlewareNames(pluginPath: string): Promise<string[]> {
54
- try {
55
- const content = await readFile(pluginPath, "utf-8");
56
-
57
- // Look for middleware definitions: `name: createMiddleware(...)`
58
- // This works for both old `middleware: { timing: createMiddleware(...) }`
59
- // and new `middleware: (ctx) => ({ timing: createMiddleware(...) })`
60
- const middlewareNames = [...content.matchAll(/(\w+)\s*:\s*createMiddleware\s*\(/g)]
61
- .map((m) => m[1])
62
- .filter((name): name is string => !!name);
63
-
64
- return middlewareNames;
65
- } catch {
66
- return [];
67
- }
68
- }
69
-
70
- interface ExtractedRoute {
71
- name: string;
72
- handler: string;
73
- }
74
-
75
- async function extractRoutesFromServer(entryPath: string): Promise<ExtractedRoute[]> {
76
- const fullPath = join(process.cwd(), entryPath);
77
-
78
- if (!existsSync(fullPath)) {
79
- console.warn(pc.yellow(`Entry file not found: ${entryPath}, skipping route extraction`));
80
- return [];
81
- }
82
-
83
- return new Promise((resolve) => {
84
- const child = spawn("bun", [fullPath], {
85
- env: { ...process.env, DONKEYLABS_GENERATE: "1" },
86
- stdio: ["inherit", "pipe", "pipe"],
87
- cwd: process.cwd(),
88
- });
89
-
90
- let stdout = "";
91
- let stderr = "";
92
-
93
- child.stdout?.on("data", (data) => {
94
- stdout += data.toString();
95
- });
96
-
97
- child.stderr?.on("data", (data) => {
98
- stderr += data.toString();
99
- });
100
-
101
- child.on("close", (code) => {
102
- if (code !== 0) {
103
- console.warn(pc.yellow(`Failed to extract routes from server (exit code ${code})`));
104
- if (stderr) console.warn(pc.dim(stderr));
105
- resolve([]);
106
- return;
107
- }
108
-
109
- try {
110
- // Parse the JSON output from server
111
- const result = JSON.parse(stdout.trim());
112
- resolve(result.routes || []);
113
- } catch (e) {
114
- console.warn(pc.yellow("Failed to parse route data from server"));
115
- resolve([]);
116
- }
117
- });
118
-
119
- child.on("error", (err) => {
120
- console.warn(pc.yellow(`Failed to run entry file: ${err.message}`));
121
- resolve([]);
122
- });
123
- });
124
- }
125
-
126
-
127
- async function findPlugins(
128
- patterns: string[]
129
- ): Promise<{ name: string; path: string; exportName: string }[]> {
130
- const plugins: { name: string; path: string; exportName: string }[] = [];
131
-
132
- for (const pattern of patterns) {
133
- const baseDir = pattern.includes("**")
134
- ? pattern.split("**")[0] || "."
135
- : dirname(pattern);
136
-
137
- const targetDir = join(process.cwd(), baseDir);
138
- if (!existsSync(targetDir)) continue;
139
-
140
- async function scanDir(dir: string): Promise<void> {
141
- const entries = await readdir(dir, { withFileTypes: true });
142
-
143
- for (const entry of entries) {
144
- const fullPath = join(dir, entry.name);
145
-
146
- if (entry.isDirectory()) {
147
- await scanDir(fullPath);
148
- } else if (entry.name === "index.ts") {
149
- const exportName = await getPluginExportName(fullPath);
150
- if (exportName) {
151
- const pluginName = dirname(fullPath).split("/").pop()!;
152
- plugins.push({
153
- name: pluginName,
154
- path: relative(process.cwd(), fullPath),
155
- exportName,
156
- });
157
- }
158
- }
159
- }
160
- }
161
-
162
- await scanDir(targetDir);
163
- }
164
-
165
- return plugins;
166
- }
167
-
168
- export async function generateCommand(_args: string[]): Promise<void> {
169
- const config = await loadConfig();
170
- const outDir = config.outDir || ".@donkeylabs/server";
171
- const outPath = join(process.cwd(), outDir);
172
-
173
- await mkdir(outPath, { recursive: true });
174
-
175
- const plugins = await findPlugins(config.plugins);
176
- const fileRoutes = await findRoutes(config.routes || "./src/routes/**/schema.ts");
177
-
178
- // Extract routes from server entry file
179
- const entryPath = config.entry || "./src/index.ts";
180
- const serverRoutes = await extractRoutesFromServer(entryPath);
181
-
182
- // Generate all files
183
- await generateRegistry(plugins, outPath);
184
- await generateContext(plugins, outPath);
185
- await generateRouteTypes(fileRoutes, outPath);
186
-
187
- const generated = ["registry", "context", "routes"];
188
-
189
- // Always generate client to default location, or custom if specified
190
- const clientOutput = config.client?.output || join(outPath, "client.ts");
191
- await generateClientFromRoutes(serverRoutes, clientOutput);
192
- generated.push("client");
193
-
194
- console.log(pc.green("Generated:"), generated.map(g => pc.dim(g)).join(", "));
195
- }
196
-
197
- async function generateRegistry(
198
- plugins: { name: string; path: string; exportName: string }[],
199
- outPath: string
200
- ) {
201
- const importLines = plugins
202
- .map(
203
- (p) =>
204
- `import { ${p.exportName} } from "${join(process.cwd(), p.path).replace(/\.ts$/, "")}";`
205
- )
206
- .join("\n");
207
-
208
- const pluginRegistryEntries = plugins
209
- .map(
210
- (p) =>
211
- ` ${p.name}: Register<InferService<typeof ${p.exportName}>, InferSchema<typeof ${p.exportName}>, InferHandlers<typeof ${p.exportName}>, InferDependencies<typeof ${p.exportName}>, InferMiddleware<typeof ${p.exportName}>>;`
212
- )
213
- .join("\n");
214
-
215
- const handlerExtensions =
216
- plugins.map((p) => `InferHandlers<typeof ${p.exportName}>`).join(",\n ") ||
217
- "{}";
218
-
219
- const middlewareExtensions =
220
- plugins
221
- .map((p) => `InferMiddleware<typeof ${p.exportName}>`)
222
- .join(",\n ") || "{}";
223
-
224
- // Collect handlers and middleware from each plugin
225
- const allHandlers: { plugin: string; handler: string }[] = [];
226
- const allMiddleware: { plugin: string; middleware: string }[] = [];
227
-
228
- for (const p of plugins) {
229
- const handlers = await extractHandlerNames(join(process.cwd(), p.path));
230
- const middleware = await extractMiddlewareNames(join(process.cwd(), p.path));
231
-
232
- for (const h of handlers) {
233
- allHandlers.push({ plugin: p.exportName, handler: h });
234
- }
235
- for (const m of middleware) {
236
- allMiddleware.push({ plugin: p.exportName, middleware: m });
237
- }
238
- }
239
-
240
- const routeBuilderMethods = allHandlers
241
- .map(
242
- ({ plugin, handler }) => ` /** Custom handler from ${plugin} */
243
- ${handler}(config: {
244
- handle: InferHandlers<typeof ${plugin}>["${handler}"]["__signature"];
245
- }): TRouter;`
246
- )
247
- .join("\n");
248
-
249
- const handlerUnion =
250
- allHandlers.length > 0
251
- ? `"typed" | "raw" | ${allHandlers.map((h) => `"${h.handler}"`).join(" | ")}`
252
- : '"typed" | "raw"';
253
-
254
- const middlewareUnion =
255
- allMiddleware.length > 0
256
- ? allMiddleware.map((m) => `"${m.middleware}"`).join(" | ")
257
- : "never";
258
-
259
- // Router middleware methods (returns IRouter for chaining)
260
- const middlewareBuilderMethods = allMiddleware
261
- .map(
262
- ({ plugin, middleware }) => ` /** Middleware from ${plugin} */
263
- ${middleware}(config?: InferMiddleware<typeof ${plugin}>["${middleware}"]["__config"]): TRouter;`
264
- )
265
- .join("\n");
266
-
267
- const content = `// Auto-generated by donkeylabs generate
268
- import { type Register, type InferService, type InferSchema, type InferHandlers, type InferMiddleware, type InferDependencies } from "@donkeylabs/server";
269
- ${importLines}
270
-
271
- declare module "@donkeylabs/server" {
272
- export interface PluginRegistry {
273
- ${pluginRegistryEntries}
274
- }
275
-
276
- export interface PluginHandlerRegistry extends
277
- ${handlerExtensions}
278
- {}
279
-
280
- export interface PluginMiddlewareRegistry extends
281
- ${middlewareExtensions}
282
- {}
283
- }
284
-
285
- export type AvailableHandlers = ${handlerUnion};
286
- export type AvailableMiddleware = ${middlewareUnion};
287
-
288
- declare module "@donkeylabs/server" {
289
- export interface IRouteBuilder<TRouter> {
290
- ${routeBuilderMethods}
291
- }
292
-
293
- export interface IMiddlewareBuilder<TRouter> {
294
- ${middlewareBuilderMethods}
295
- }
296
- }
297
- `;
298
-
299
- await writeFile(join(outPath, "registry.d.ts"), content);
300
- }
301
-
302
- async function generateContext(
303
- plugins: { name: string; path: string; exportName: string }[],
304
- outPath: string
305
- ) {
306
- const schemaIntersection =
307
- plugins.map((p) => `PluginRegistry["${p.name}"]["schema"]`).join(" & ") ||
308
- "{}";
309
-
310
- const content = `// Auto-generated by donkeylabs generate
311
- // App context - import as: import type { AppContext } from ".@donkeylabs/server/context";
312
-
313
- /// <reference path="./registry.d.ts" />
314
- import type { PluginRegistry, CoreServices, Errors } from "@donkeylabs/server";
315
- import type { Kysely } from "kysely";
316
-
317
- /** Merged database schema from all plugins */
318
- export type DatabaseSchema = ${schemaIntersection};
319
-
320
- /**
321
- * Fully typed application context.
322
- * Use this instead of ServerContext for typed plugin access.
323
- */
324
- export interface AppContext {
325
- /** Database with merged schema from all plugins */
326
- db: Kysely<DatabaseSchema>;
327
- /** Typed plugin services */
328
- plugins: {
329
- [K in keyof PluginRegistry]: PluginRegistry[K]["service"];
330
- };
331
- /** Core services (logger, cache, events, etc.) */
332
- core: Omit<CoreServices, "db" | "config" | "errors">;
333
- /** Error factories (BadRequest, NotFound, etc.) */
334
- errors: Errors;
335
- /** Application config */
336
- config: Record<string, any>;
337
- /** Client IP address */
338
- ip: string;
339
- /** Unique request ID */
340
- requestId: string;
341
- /** Authenticated user (set by auth middleware) */
342
- user?: any;
343
- }
344
-
345
- // Re-export as GlobalContext for backwards compatibility
346
- export type GlobalContext = AppContext;
347
- `;
348
-
349
- await writeFile(join(outPath, "context.d.ts"), content);
350
- }
351
-
352
- // Route file structure: /src/routes/<namespace>/<route-name>/schema.ts
353
- interface RouteInfo {
354
- namespace: string;
355
- name: string;
356
- schemaPath: string;
357
- }
358
-
359
- async function findRoutes(_pattern: string): Promise<RouteInfo[]> {
360
- const routes: RouteInfo[] = [];
361
- const routesDir = join(process.cwd(), "src/routes");
362
-
363
- if (!existsSync(routesDir)) {
364
- return routes;
365
- }
366
-
367
- // Scan routes directory structure: /routes/<namespace>/<route>/schema.ts
368
- const namespaces = await readdir(routesDir, { withFileTypes: true });
369
-
370
- for (const ns of namespaces) {
371
- if (!ns.isDirectory() || ns.name.startsWith(".")) continue;
372
-
373
- const namespaceDir = join(routesDir, ns.name);
374
- const routeDirs = await readdir(namespaceDir, { withFileTypes: true });
375
-
376
- for (const routeDir of routeDirs) {
377
- if (!routeDir.isDirectory() || routeDir.name.startsWith(".")) continue;
378
-
379
- const schemaPath = join(namespaceDir, routeDir.name, "schema.ts");
380
-
381
- if (!existsSync(schemaPath)) continue;
382
-
383
- routes.push({
384
- namespace: ns.name,
385
- name: routeDir.name,
386
- schemaPath: relative(process.cwd(), schemaPath),
387
- });
388
- }
389
- }
390
-
391
- return routes;
392
- }
393
-
394
- function toPascalCase(str: string): string {
395
- return str
396
- .replace(/-([a-z])/g, (_, c) => c.toUpperCase())
397
- .replace(/^./, (c) => c.toUpperCase());
398
- }
399
-
400
- async function generateRouteTypes(routes: RouteInfo[], outPath: string): Promise<void> {
401
- // Group routes by namespace
402
- const byNamespace = new Map<string, RouteInfo[]>();
403
- for (const route of routes) {
404
- if (!byNamespace.has(route.namespace)) {
405
- byNamespace.set(route.namespace, []);
406
- }
407
- byNamespace.get(route.namespace)!.push(route);
408
- }
409
-
410
- // Generate imports for each route's schema (relative to outPath)
411
- const imports: string[] = [];
412
- for (const route of routes) {
413
- // Calculate relative path from .@donkeylabs/server/ to src/routes/.../schema
414
- const schemaAbsPath = join(process.cwd(), route.schemaPath).replace(/\.ts$/, "");
415
- const outAbsPath = outPath;
416
- const relativePath = relative(outAbsPath, schemaAbsPath);
417
- const alias = `${toPascalCase(route.namespace)}_${toPascalCase(route.name)}`;
418
- imports.push(`import { Input as ${alias}_Input, Output as ${alias}_Output } from "${relativePath}";`);
419
- }
420
-
421
- // Generate namespace declarations
422
- const namespaceBlocks: string[] = [];
423
- for (const [namespace, nsRoutes] of byNamespace) {
424
- const pascalNamespace = toPascalCase(namespace);
425
-
426
- const routeExports = nsRoutes.map((r) => {
427
- const pascalRoute = toPascalCase(r.name);
428
- const alias = `${pascalNamespace}_${pascalRoute}`;
429
- return ` export namespace ${pascalRoute} {
430
- /** Zod schema for input validation */
431
- export const Input = ${alias}_Input;
432
- /** Zod schema for output validation */
433
- export const Output = ${alias}_Output;
434
- /** TypeScript type for input data */
435
- export type Input = z.infer<typeof ${alias}_Input>;
436
- /** TypeScript type for output data */
437
- export type Output = z.infer<typeof ${alias}_Output>;
438
- }
439
- /** Route contract type - use with Route<${pascalNamespace}.${pascalRoute}> and Handler<${pascalNamespace}.${pascalRoute}> */
440
- export type ${pascalRoute} = { input: ${pascalRoute}.Input; output: ${pascalRoute}.Output };`;
441
- }).join("\n\n");
442
-
443
- namespaceBlocks.push(`export namespace ${pascalNamespace} {\n${routeExports}\n}`);
444
- }
445
-
446
- const content = `// Auto-generated by donkeylabs generate
447
- // Route Input/Output types - import as: import { Health } from ".@donkeylabs/server/routes";
448
-
449
- import { z } from "zod";
450
- import type { AppContext } from "./context";
451
- ${imports.join("\n")}
452
-
453
- ${namespaceBlocks.join("\n\n")}
454
- `;
455
-
456
- await writeFile(join(outPath, "routes.ts"), content);
457
- }
458
-
459
- async function generateClientFromRoutes(
460
- routes: ExtractedRoute[],
461
- outputPath: string
462
- ): Promise<void> {
463
- // Group routes by namespace (first part of route name)
464
- // e.g., "api.hello.test" -> namespace "api", sub "hello", method "test"
465
- const tree = new Map<string, Map<string, { method: string; fullName: string }[]>>();
466
-
467
- for (const route of routes) {
468
- const parts = route.name.split(".");
469
- if (parts.length < 2) {
470
- // Single-level route like "ping" -> namespace "", method "ping"
471
- const ns = "";
472
- if (!tree.has(ns)) tree.set(ns, new Map());
473
- const rootMethods = tree.get(ns)!;
474
- if (!rootMethods.has("")) rootMethods.set("", []);
475
- rootMethods.get("")!.push({ method: parts[0]!, fullName: route.name });
476
- } else if (parts.length === 2) {
477
- // Two-level route like "health.ping" -> namespace "health", method "ping"
478
- const [ns, method] = parts;
479
- if (!tree.has(ns!)) tree.set(ns!, new Map());
480
- const nsMethods = tree.get(ns!)!;
481
- if (!nsMethods.has("")) nsMethods.set("", []);
482
- nsMethods.get("")!.push({ method: method!, fullName: route.name });
483
- } else {
484
- // Multi-level route like "api.hello.test" -> namespace "api", sub "hello", method "test"
485
- const [ns, sub, ...rest] = parts;
486
- const method = rest.join(".");
487
- if (!tree.has(ns!)) tree.set(ns!, new Map());
488
- const nsMethods = tree.get(ns!)!;
489
- if (!nsMethods.has(sub!)) nsMethods.set(sub!, []);
490
- nsMethods.get(sub!)!.push({ method: method || sub!, fullName: route.name });
491
- }
492
- }
493
-
494
- // Generate method definitions
495
- const namespaceBlocks: string[] = [];
496
-
497
- for (const [namespace, subNamespaces] of tree) {
498
- if (namespace === "") {
499
- // Root-level methods
500
- const rootMethods = subNamespaces.get("");
501
- if (rootMethods && rootMethods.length > 0) {
502
- for (const { method, fullName } of rootMethods) {
503
- const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
504
- namespaceBlocks.push(` ${methodName} = (input: any) => this.request("${fullName}", input);`);
505
- }
506
- }
507
- continue;
508
- }
509
-
510
- const subBlocks: string[] = [];
511
- for (const [sub, methods] of subNamespaces) {
512
- if (sub === "") {
513
- // Direct methods on namespace
514
- for (const { method, fullName } of methods) {
515
- const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
516
- subBlocks.push(` ${methodName}: (input: any) => this.request("${fullName}", input)`);
517
- }
518
- } else {
519
- // Sub-namespace methods
520
- const subMethods = methods.map(({ method, fullName }) => {
521
- const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
522
- return ` ${methodName}: (input: any) => this.request("${fullName}", input)`;
523
- });
524
- subBlocks.push(` ${sub}: {\n${subMethods.join(",\n")}\n }`);
525
- }
526
- }
527
-
528
- namespaceBlocks.push(` ${namespace} = {\n${subBlocks.join(",\n")}\n };`);
529
- }
530
-
531
- const content = `// Auto-generated by donkeylabs generate
532
- // API Client
533
-
534
- import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";
535
-
536
- export class ApiClient extends ApiClientBase<{}> {
537
- constructor(baseUrl: string, options?: ApiClientOptions) {
538
- super(baseUrl, options);
539
- }
540
-
541
- ${namespaceBlocks.join("\n\n") || " // No routes defined"}
542
- }
543
-
544
- export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
545
- return new ApiClient(baseUrl, options);
546
- }
547
- `;
548
-
549
- const outputDir = dirname(outputPath);
550
- await mkdir(outputDir, { recursive: true });
551
- await writeFile(outputPath, content);
552
- }
553
-
554
- // Keep old function for backwards compatibility (unused now)
555
- async function generateClient(
556
- routes: RouteInfo[],
557
- plugins: { name: string; path: string; exportName: string }[],
558
- outputPath: string
559
- ): Promise<void> {
560
- // Group routes by namespace
561
- const byNamespace = new Map<string, RouteInfo[]>();
562
- for (const route of routes) {
563
- if (!byNamespace.has(route.namespace)) {
564
- byNamespace.set(route.namespace, []);
565
- }
566
- byNamespace.get(route.namespace)!.push(route);
567
- }
568
-
569
- // Generate method definitions
570
- const namespaceBlocks: string[] = [];
571
-
572
- for (const [namespace, nsRoutes] of byNamespace) {
573
- // All routes are treated as typed by default
574
- const methods = nsRoutes.map((r) => {
575
- const methodName = r.name.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
576
- return ` ${methodName}: (input: any) => this.request("${namespace}.${r.name}", input)`;
577
- });
578
-
579
- if (methods.length > 0) {
580
- namespaceBlocks.push(` ${namespace} = {\n${methods.join(",\n")}\n };`);
581
- }
582
- }
583
-
584
- const content = `// Auto-generated by donkeylabs generate
585
- // API Client
586
-
587
- import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";
588
-
589
- export class ApiClient extends ApiClientBase<{}> {
590
- constructor(baseUrl: string, options?: ApiClientOptions) {
591
- super(baseUrl, options);
592
- }
593
-
594
- ${namespaceBlocks.join("\n\n") || " // No routes defined"}
595
- }
596
-
597
- export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
598
- return new ApiClient(baseUrl, options);
599
- }
600
- `;
601
-
602
- const outputDir = dirname(outputPath);
603
- await mkdir(outputDir, { recursive: true });
604
- await writeFile(outputPath, content);
605
- }