@donkeylabs/server 0.3.1 → 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/package.json CHANGED
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
7
7
  "types": "./src/index.ts",
8
- "bin": {
9
- "donkeylabs-mcp": "./mcp/server.ts"
10
- },
11
8
  "exports": {
12
9
  ".": {
13
10
  "types": "./src/index.ts",
@@ -27,11 +24,11 @@
27
24
  },
28
25
  "./context": {
29
26
  "types": "./context.d.ts"
30
- }
27
+ },
28
+ "./docs/*": "./docs/*"
31
29
  },
32
30
  "files": [
33
31
  "src",
34
- "mcp",
35
32
  "docs",
36
33
  "context.d.ts",
37
34
  "registry.d.ts",
@@ -312,30 +312,38 @@ export function generateClientFromRoutes(
312
312
  ): string {
313
313
  const opts = { ...defaultGeneratorOptions, ...options };
314
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
+
315
319
  // Group routes by namespace
316
320
  const tree = new Map<string, Map<string, { method: string; fullName: string }[]>>();
317
321
 
318
- for (const route of routes) {
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
+
319
327
  const parts = route.name.split(".");
320
328
  if (parts.length < 2) {
321
329
  const ns = "";
322
330
  if (!tree.has(ns)) tree.set(ns, new Map());
323
331
  const rootMethods = tree.get(ns)!;
324
332
  if (!rootMethods.has("")) rootMethods.set("", []);
325
- rootMethods.get("")!.push({ method: parts[0]!, fullName: route.name });
333
+ rootMethods.get("")!.push({ method: parts[0]!, fullName });
326
334
  } else if (parts.length === 2) {
327
335
  const [ns, method] = parts;
328
336
  if (!tree.has(ns!)) tree.set(ns!, new Map());
329
337
  const nsMethods = tree.get(ns!)!;
330
338
  if (!nsMethods.has("")) nsMethods.set("", []);
331
- nsMethods.get("")!.push({ method: method!, fullName: route.name });
339
+ nsMethods.get("")!.push({ method: method!, fullName });
332
340
  } else {
333
341
  const [ns, sub, ...rest] = parts;
334
342
  const method = rest.join(".");
335
343
  if (!tree.has(ns!)) tree.set(ns!, new Map());
336
344
  const nsMethods = tree.get(ns!)!;
337
345
  if (!nsMethods.has(sub!)) nsMethods.set(sub!, []);
338
- nsMethods.get(sub!)!.push({ method: method || sub!, fullName: route.name });
346
+ nsMethods.get(sub!)!.push({ method: method || sub!, fullName });
339
347
  }
340
348
  }
341
349
 
@@ -435,8 +443,12 @@ export function generateClientCode(
435
443
  .map((r) => {
436
444
  const inputType = zodToTypeScript(r.inputSource);
437
445
  const outputType = zodToTypeScript(r.outputSource);
438
- return ` export type ${toPascalCase(r.routeName)}Input = ${inputType};
439
- export type ${toPascalCase(r.routeName)}Output = ${outputType};`;
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 };`;
440
452
  });
441
453
 
442
454
  if (typeEntries.length > 0) {
@@ -448,8 +460,8 @@ ${typeEntries.join("\n\n")}
448
460
  const methodEntries = prefixRoutes
449
461
  .filter((r) => r.handler === "typed")
450
462
  .map((r) => {
451
- const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}Input`;
452
- const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}Output`;
463
+ const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`;
464
+ const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Output`;
453
465
  return ` ${toCamelCase(r.routeName)}: (input: ${inputType}, options?: RequestOptions): Promise<${outputType}> =>
454
466
  this.request("${r.name}", input, options)`;
455
467
  });
@@ -549,3 +561,6 @@ export function createApiClient(config: ApiClientConfig): ApiClient {
549
561
  export { ApiError, ValidationError, type RequestOptions, type SSEOptions };
550
562
  `;
551
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
+ }
package/src/index.ts CHANGED
@@ -4,7 +4,17 @@
4
4
  export { AppServer, type ServerConfig } from "./server";
5
5
 
6
6
  // Router
7
- export { createRouter, type Router, type RouteBuilder, type ServerContext, type IRouter, type IRouteBuilder, type IMiddlewareBuilder } from "./router";
7
+ export {
8
+ createRouter,
9
+ defineRoute,
10
+ type Router,
11
+ type RouteBuilder,
12
+ type ServerContext,
13
+ type IRouter,
14
+ type IRouteBuilder,
15
+ type IMiddlewareBuilder,
16
+ type TypedRouteConfig,
17
+ } from "./router";
8
18
 
9
19
  // Handlers
10
20
  export {
package/src/router.ts CHANGED
@@ -36,10 +36,18 @@ export type RouteDefinition<
36
36
  : HandlerRegistry[T]["__signature"];
37
37
  };
38
38
 
39
+ export interface HandlerClass<I = any, O = any> {
40
+ new (ctx: ServerContext): { handle(input: I): Promise<O> | O };
41
+ }
42
+
43
+ function isHandlerClass(fn: any): fn is HandlerClass {
44
+ return typeof fn === 'function' && fn.prototype && typeof fn.prototype.handle === 'function';
45
+ }
46
+
39
47
  export interface TypedRouteConfig<I = any, O = any> {
40
48
  input?: z.ZodType<I>;
41
49
  output?: z.ZodType<O>;
42
- handle: (input: I, ctx: ServerContext) => Promise<O> | O;
50
+ handle: ((input: I, ctx: ServerContext) => Promise<O> | O) | HandlerClass<I, O>;
43
51
  }
44
52
 
45
53
  export interface RouteMetadata {
@@ -69,6 +77,10 @@ export class RouteBuilder<TRouter extends Router> implements IRouteBuilderBase<T
69
77
  ) {}
70
78
 
71
79
  typed<I, O>(config: TypedRouteConfig<I, O>): TRouter {
80
+ if (isHandlerClass(config.handle)) {
81
+ const HandlerClass = config.handle;
82
+ config.handle = (input, ctx) => new HandlerClass(ctx).handle(input);
83
+ }
72
84
  return this.router.addRoute(this.name, "typed", config, this._middleware);
73
85
  }
74
86
 
@@ -156,6 +168,23 @@ export class Router implements IRouter {
156
168
  }));
157
169
  }
158
170
 
171
+ /** Get route metadata with TypeScript type strings for code generation */
172
+ getTypedMetadata(): Array<{
173
+ name: string;
174
+ handler: string;
175
+ inputType?: string;
176
+ outputType?: string;
177
+ }> {
178
+ // Dynamic import to avoid circular deps
179
+ const { zodSchemaToTs } = require("./generator/zod-to-ts");
180
+ return this.getRoutes().map(route => ({
181
+ name: route.name,
182
+ handler: route.handler,
183
+ inputType: route.input ? zodSchemaToTs(route.input) : undefined,
184
+ outputType: route.output ? zodSchemaToTs(route.output) : undefined,
185
+ }));
186
+ }
187
+
159
188
  getPrefix(): string {
160
189
  return this.prefix;
161
190
  }
@@ -177,3 +206,20 @@ function createMiddlewareBuilderProxy<TRouter extends Router>(router: TRouter):
177
206
  }
178
207
 
179
208
  export const createRouter = (prefix?: string): IRouter => new Router(prefix);
209
+
210
+ /**
211
+ * Define a route with type inference for input/output schemas.
212
+ *
213
+ * @example
214
+ * export const myRoute = defineRoute({
215
+ * input: z.object({ name: z.string() }),
216
+ * output: z.object({ greeting: z.string() }),
217
+ * handle: async ({ name }, ctx) => {
218
+ * return { greeting: `Hello ${name}` };
219
+ * }
220
+ * });
221
+ */
222
+ export function defineRoute<I, O>(config: TypedRouteConfig<I, O>): TypedRouteConfig<I, O> {
223
+ return config;
224
+ }
225
+
package/src/server.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
2
4
  import { PluginManager, type CoreServices, type ConfiguredPlugin } from "./core";
3
5
  import { type IRouter, type RouteDefinition, type ServerContext } from "./router";
4
6
  import { Handlers } from "./handlers";
@@ -23,11 +25,29 @@ import {
23
25
  type RateLimiterConfig,
24
26
  type ErrorsConfig,
25
27
  } from "./core/index";
28
+ import { zodSchemaToTs } from "./generator/zod-to-ts";
29
+
30
+ export interface TypeGenerationConfig {
31
+ /** Output path for generated client types (e.g., "./src/lib/api.ts") */
32
+ output: string;
33
+ /** Custom base import for the client */
34
+ baseImport?: string;
35
+ /** Custom base class name */
36
+ baseClass?: string;
37
+ /** Constructor signature (e.g., "baseUrl: string, options?: ApiClientOptions") */
38
+ constructorSignature?: string;
39
+ /** Constructor body (e.g., "super(baseUrl, options);") */
40
+ constructorBody?: string;
41
+ /** Factory function code (optional, replaces default createApi) */
42
+ factoryFunction?: string;
43
+ }
26
44
 
27
45
  export interface ServerConfig {
28
46
  port?: number;
29
47
  db: CoreServices["db"];
30
48
  config?: Record<string, any>;
49
+ /** Auto-generate client types on startup in dev mode */
50
+ generateTypes?: TypeGenerationConfig;
31
51
  // Core service configurations
32
52
  logger?: LoggerConfig;
33
53
  cache?: CacheConfig;
@@ -45,6 +65,7 @@ export class AppServer {
45
65
  private routers: IRouter[] = [];
46
66
  private routeMap: Map<string, RouteDefinition> = new Map();
47
67
  private coreServices: CoreServices;
68
+ private typeGenConfig?: TypeGenerationConfig;
48
69
 
49
70
  constructor(options: ServerConfig) {
50
71
  this.port = options.port ?? 3000;
@@ -73,6 +94,7 @@ export class AppServer {
73
94
  };
74
95
 
75
96
  this.manager = new PluginManager(this.coreServices);
97
+ this.typeGenConfig = options.generateTypes;
76
98
  }
77
99
 
78
100
  /**
@@ -169,6 +191,247 @@ export class AppServer {
169
191
  return this.routeMap.has(routeName);
170
192
  }
171
193
 
194
+ /**
195
+ * Generate client types from registered routes.
196
+ * Called automatically on startup in dev mode if generateTypes config is provided.
197
+ */
198
+ private async generateTypes(): Promise<void> {
199
+ if (!this.typeGenConfig) return;
200
+
201
+ const { logger } = this.coreServices;
202
+ const isDev = process.env.NODE_ENV !== "production";
203
+
204
+ if (!isDev) {
205
+ logger.debug("Skipping type generation in production mode");
206
+ return;
207
+ }
208
+
209
+ // Collect all route metadata
210
+ const routes: Array<{
211
+ name: string;
212
+ prefix: string;
213
+ routeName: string;
214
+ handler: "typed" | "raw";
215
+ inputSource?: string;
216
+ outputSource?: string;
217
+ }> = [];
218
+
219
+ for (const router of this.routers) {
220
+ for (const route of router.getRoutes()) {
221
+ const parts = route.name.split(".");
222
+ const routeName = parts[parts.length - 1] || route.name;
223
+ const prefix = parts.slice(0, -1).join(".");
224
+
225
+ routes.push({
226
+ name: route.name,
227
+ prefix,
228
+ routeName,
229
+ handler: (route.handler || "typed") as "typed" | "raw",
230
+ inputSource: route.input ? zodSchemaToTs(route.input) : undefined,
231
+ outputSource: route.output ? zodSchemaToTs(route.output) : undefined,
232
+ });
233
+ }
234
+ }
235
+
236
+ // Generate the client code
237
+ const code = this.generateClientCode(routes);
238
+
239
+ // Write to output file
240
+ const outputDir = dirname(this.typeGenConfig.output);
241
+ await mkdir(outputDir, { recursive: true });
242
+ await writeFile(this.typeGenConfig.output, code);
243
+
244
+ logger.info(`Generated API client types`, { output: this.typeGenConfig.output, routes: routes.length });
245
+ }
246
+
247
+ /**
248
+ * Generate client code from route metadata.
249
+ */
250
+ private generateClientCode(
251
+ routes: Array<{
252
+ name: string;
253
+ prefix: string;
254
+ routeName: string;
255
+ handler: "typed" | "raw";
256
+ inputSource?: string;
257
+ outputSource?: string;
258
+ }>
259
+ ): string {
260
+ const baseImport =
261
+ this.typeGenConfig?.baseImport ??
262
+ 'import { UnifiedApiClientBase, type ClientOptions } from "@donkeylabs/adapter-sveltekit/client";';
263
+ const baseClass = this.typeGenConfig?.baseClass ?? "UnifiedApiClientBase";
264
+ const constructorSignature =
265
+ this.typeGenConfig?.constructorSignature ?? "options?: ClientOptions";
266
+ const constructorBody =
267
+ this.typeGenConfig?.constructorBody ?? "super(options);";
268
+ const defaultFactory = `/**
269
+ * Create an API client instance
270
+ */
271
+ export function createApi(options?: ClientOptions) {
272
+ return new ApiClient(options);
273
+ }`;
274
+ const factoryFunction = this.typeGenConfig?.factoryFunction ?? defaultFactory;
275
+
276
+ // Helper functions
277
+ const toPascalCase = (str: string): string =>
278
+ str
279
+ .split(/[._-]/)
280
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
281
+ .join("");
282
+
283
+ const toCamelCase = (str: string): string => {
284
+ const pascal = toPascalCase(str);
285
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
286
+ };
287
+
288
+ // Common prefix stripping is disabled to respect explicit router nesting (e.g. api.health)
289
+ const routesToProcess = routes;
290
+ const commonPrefix = "";
291
+
292
+ // Build recursive tree for nested routes
293
+ type RouteNode = {
294
+ children: Map<string, RouteNode>;
295
+ routes: typeof routes;
296
+ };
297
+ const rootNode: RouteNode = { children: new Map(), routes: [] };
298
+
299
+ for (const route of routesToProcess) {
300
+ const parts = route.name.split(".");
301
+ let currentNode = rootNode;
302
+ // Navigate/Build tree
303
+ for (let i = 0; i < parts.length - 1; i++) {
304
+ const part = parts[i]!;
305
+ if (!currentNode.children.has(part)) {
306
+ currentNode.children.set(part, { children: new Map(), routes: [] });
307
+ }
308
+ currentNode = currentNode.children.get(part)!;
309
+ }
310
+ // Add route to the leaf node (last part is the method name)
311
+ currentNode.routes.push({
312
+ ...route,
313
+ routeName: parts[parts.length - 1]! // precise method name
314
+ });
315
+ }
316
+
317
+ // Recursive function to generate Type definitions
318
+ function generateTypeBlock(node: RouteNode, indent: string): string {
319
+ const blocks: string[] = [];
320
+
321
+ // 1. Valid Input/Output types for routes at this level
322
+ if (node.routes.length > 0) {
323
+ const routeTypes = node.routes.map(r => {
324
+ if (r.handler !== "typed") return "";
325
+ const routeNs = toPascalCase(r.routeName);
326
+ const inputType = r.inputSource ?? "Record<string, never>";
327
+ const outputType = r.outputSource ?? "unknown";
328
+ return `${indent}export namespace ${routeNs} {
329
+ ${indent} export type Input = Expand<${inputType}>;
330
+ ${indent} export type Output = Expand<${outputType}>;
331
+ ${indent}}
332
+ ${indent}export type ${routeNs} = { Input: ${routeNs}.Input; Output: ${routeNs}.Output };`;
333
+ }).filter(Boolean);
334
+ if (routeTypes.length) blocks.push(routeTypes.join("\n\n"));
335
+ }
336
+
337
+ // 2. Nested namespaces
338
+ for (const [name, child] of node.children) {
339
+ const nsName = toPascalCase(name);
340
+ blocks.push(`${indent}export namespace ${nsName} {\n${generateTypeBlock(child, indent + " ")}\n${indent}}`);
341
+ }
342
+ return blocks.join("\n\n");
343
+ }
344
+
345
+ // Recursive function to generate Client Methods
346
+ function generateMethodBlock(node: RouteNode, indent: string, parentPath: string, isTopLevel: boolean): string {
347
+ const blocks: string[] = [];
348
+
349
+ // 1. Methods at this level
350
+ const methods = node.routes.map(r => {
351
+ const methodName = toCamelCase(r.routeName);
352
+ // r.name is the full path e.g. "api.v1.users.get"
353
+
354
+ if (r.handler === "typed") {
355
+ const pathParts = r.name.split(".");
356
+ const typePath = ["Routes", ...pathParts.slice(0, -1).map(toPascalCase), toPascalCase(r.routeName)];
357
+ const inputType = typePath.join(".") + ".Input";
358
+ const outputType = typePath.join(".") + ".Output";
359
+
360
+ return `${indent}${methodName}: (input: ${inputType}): Promise<${outputType}> => this.request("${r.name}", input)`;
361
+ } else {
362
+ return `${indent}${methodName}: (init?: RequestInit): Promise<Response> => this.rawRequest("${r.name}", init)`;
363
+ }
364
+ });
365
+ if (methods.length) blocks.push(methods.join(",\n"));
366
+
367
+ // 2. Nested Objects
368
+ for (const [name, child] of node.children) {
369
+ const camelName = toCamelCase(name);
370
+ const separator = isTopLevel ? " = " : ": ";
371
+ const terminator = isTopLevel ? ";" : "";
372
+ // For top level, we output `name = { ... };`
373
+ // For nested, we output `name: { ... }` (comma handled by join)
374
+
375
+ blocks.push(`${indent}${camelName}${separator}{\n${generateMethodBlock(child, indent + " ", "", false)}\n${indent}}${terminator}`);
376
+ }
377
+ // Top level blocks are separated by nothing (class members). Nested by comma.
378
+ // Wait, blocks.join needs care.
379
+ // If isTopLevel, join with "\n\n". If nested, join with ",\n".
380
+ return blocks.join(isTopLevel ? "\n\n" : ",\n");
381
+ }
382
+
383
+ const typeBlocks: string[] = [generateTypeBlock(rootNode, " ")];
384
+ // rootNode children are top-level namespaces (api, health) -> Top Level Class Properties
385
+ const methodBlocks: string[] = [generateMethodBlock(rootNode, " ", "", true)];
386
+
387
+ return `// Auto-generated by @donkeylabs/server
388
+ // DO NOT EDIT MANUALLY
389
+
390
+ ${baseImport}
391
+
392
+ // Utility type that forces TypeScript to expand types on hover
393
+ type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
394
+
395
+ /**
396
+ * Handler interface for implementing route handlers in model classes.
397
+ * @example
398
+ * class CounterModel implements Handler<Routes.Counter.get> {
399
+ * handle(input: Routes.Counter.get.Input): Routes.Counter.get.Output {
400
+ * return { count: 0 };
401
+ * }
402
+ * }
403
+ */
404
+ export interface Handler<T extends { Input: any; Output: any }> {
405
+ handle(input: T["Input"]): T["Output"] | Promise<T["Output"]>;
406
+ }
407
+
408
+ // Re-export server context for model classes
409
+ export { type ServerContext as AppContext } from "@donkeylabs/server";
410
+
411
+ // ============================================
412
+ // Route Types
413
+ // ============================================
414
+
415
+ export namespace Routes {
416
+ ${typeBlocks.join("\n\n") || " // No typed routes found"}
417
+ }
418
+
419
+ // ============================================
420
+ // API Client
421
+ // ============================================
422
+
423
+ export class ApiClient extends ${baseClass} {
424
+ constructor(${constructorSignature}) {
425
+ ${constructorBody}
426
+ }
427
+
428
+ ${methodBlocks.join("\n\n") || " // No routes defined"}
429
+ }
430
+
431
+ ${factoryFunction}
432
+ `;
433
+ }
434
+
172
435
  /**
173
436
  * Initialize server without starting HTTP server.
174
437
  * Used by adapters (e.g., SvelteKit) that manage their own HTTP server.
@@ -176,6 +439,9 @@ export class AppServer {
176
439
  async initialize(): Promise<void> {
177
440
  const { logger } = this.coreServices;
178
441
 
442
+ // Auto-generate types in dev mode if configured
443
+ await this.generateTypes();
444
+
179
445
  await this.manager.migrate();
180
446
  await this.manager.init();
181
447
 
@@ -387,6 +653,9 @@ export class AppServer {
387
653
  async start() {
388
654
  const { logger } = this.coreServices;
389
655
 
656
+ // Auto-generate types in dev mode if configured
657
+ await this.generateTypes();
658
+
390
659
  // 1. Run migrations
391
660
  await this.manager.migrate();
392
661