@donkeylabs/server 2.0.16 → 2.0.17

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -300,6 +300,92 @@ export function groupRoutesByPrefix(routes: RouteInfo[]): Map<string, RouteInfo[
300
300
  return groups;
301
301
  }
302
302
 
303
+ /**
304
+ * Represents a node in the namespace tree
305
+ */
306
+ interface NamespaceNode {
307
+ methods: string[]; // Method definitions at this level
308
+ children: Map<string, NamespaceNode>;
309
+ }
310
+
311
+ /**
312
+ * Build a tree structure from route groups to handle multi-level prefixes
313
+ * e.g., "api.cache" and "api.counter" become:
314
+ * api -> { cache -> methods, counter -> methods }
315
+ */
316
+ export function buildNamespaceTree(
317
+ routeGroups: Map<string, RouteInfo[]>,
318
+ generateMethod: (route: RouteInfo) => string | null
319
+ ): Map<string, NamespaceNode> {
320
+ const tree = new Map<string, NamespaceNode>();
321
+
322
+ for (const [prefix, routes] of routeGroups) {
323
+ const methods = routes
324
+ .map(generateMethod)
325
+ .filter((m): m is string => m !== null);
326
+
327
+ if (methods.length === 0) continue;
328
+
329
+ if (prefix === "_root") {
330
+ // Root level methods go directly
331
+ if (!tree.has("_root")) {
332
+ tree.set("_root", { methods: [], children: new Map() });
333
+ }
334
+ tree.get("_root")!.methods.push(...methods);
335
+ continue;
336
+ }
337
+
338
+ // Split prefix into parts for nested namespaces
339
+ const parts = prefix.split(".");
340
+ let current = tree;
341
+
342
+ for (let i = 0; i < parts.length; i++) {
343
+ const part = parts[i]!;
344
+ if (!current.has(part)) {
345
+ current.set(part, { methods: [], children: new Map() });
346
+ }
347
+
348
+ if (i === parts.length - 1) {
349
+ // Last part - add methods here
350
+ current.get(part)!.methods.push(...methods);
351
+ } else {
352
+ // Intermediate part - continue traversing
353
+ current = current.get(part)!.children;
354
+ }
355
+ }
356
+ }
357
+
358
+ return tree;
359
+ }
360
+
361
+ /**
362
+ * Generate nested namespace code from a tree node
363
+ */
364
+ export function generateNestedNamespaceCode(
365
+ node: NamespaceNode,
366
+ indent: string = " "
367
+ ): string {
368
+ const parts: string[] = [];
369
+
370
+ // Add methods at this level with proper indentation
371
+ for (const method of node.methods) {
372
+ // Indent each line of the method
373
+ const indentedMethod = method
374
+ .split("\n")
375
+ .map((line, i) => (i === 0 ? `${indent}${line}` : `${indent} ${line}`))
376
+ .join("\n");
377
+ parts.push(indentedMethod);
378
+ }
379
+
380
+ // Add nested namespaces
381
+ for (const [childName, childNode] of node.children) {
382
+ const childContent = generateNestedNamespaceCode(childNode, indent + " ");
383
+ parts.push(`${indent}${childName}: {\n${childContent}\n${indent}}`);
384
+ }
385
+
386
+ return parts.join(",\n\n");
387
+ }
388
+
303
389
  // ==========================================
304
390
  // Client Code Generation
305
391
  // ==========================================
@@ -504,59 +590,60 @@ ${typeEntries.join("\n\n")}
504
590
  }`);
505
591
  }
506
592
 
507
- const methodEntries = prefixRoutes
508
- .filter((r) => r.handler === "typed")
509
- .map((r) => {
510
- const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`;
511
- const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Output`;
512
- return ` ${toCamelCase(r.routeName)}: (input: ${inputType}, options?: RequestOptions): Promise<${outputType}> =>
513
- this.request("${r.name}", input, options)`;
514
- });
515
-
516
- const rawMethodEntries = prefixRoutes
517
- .filter((r) => r.handler === "raw")
518
- .map((r) => {
519
- return ` ${toCamelCase(r.routeName)}: (init?: RequestInit): Promise<Response> =>
520
- this.rawRequest("${r.name}", init)`;
521
- });
593
+ }
522
594
 
523
- const sseMethodEntries = prefixRoutes
524
- .filter((r) => r.handler === "sse")
525
- .map((r) => {
526
- const inputType = r.inputSource
527
- ? `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`
528
- : "Record<string, any>";
529
- const eventsType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Events`;
530
- return ` ${toCamelCase(r.routeName)}: (input: ${inputType}, options?: Omit<SSEOptions, "endpoint" | "channels">): SSESubscription<${eventsType}> =>
531
- this.connectToSSERoute("${r.name}", input, options)`;
532
- });
595
+ // Build namespace tree for proper nesting (handles multi-level prefixes like "api.cache")
596
+ const generateMethodForRoute = (r: RouteInfo): string | null => {
597
+ const namespaceName = r.prefix === "_root" ? "Root" : toPascalCase(r.prefix);
533
598
 
534
- const streamMethodEntries = prefixRoutes
535
- .filter((r) => r.handler === "stream" || r.handler === "html")
536
- .map((r) => {
537
- const inputType = r.inputSource
538
- ? `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`
539
- : "Record<string, any>";
540
- return ` ${toCamelCase(r.routeName)}: (input: ${inputType}): Promise<Response> =>
541
- this.streamRequest("${r.name}", input)`;
542
- });
543
-
544
- const formDataMethodEntries = prefixRoutes
545
- .filter((r) => r.handler === "formData")
546
- .map((r) => {
547
- const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`;
548
- const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Output`;
549
- return ` ${toCamelCase(r.routeName)}: (fields: ${inputType}, files?: File[]): Promise<${outputType}> =>
550
- this.uploadFormData("${r.name}", fields, files)`;
551
- });
599
+ if (r.handler === "typed") {
600
+ const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`;
601
+ const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Output`;
602
+ return `${toCamelCase(r.routeName)}: (input: ${inputType}, options?: RequestOptions): Promise<${outputType}> =>
603
+ this.request("${r.name}", input, options)`;
604
+ }
605
+ if (r.handler === "raw") {
606
+ return `${toCamelCase(r.routeName)}: (init?: RequestInit): Promise<Response> =>
607
+ this.rawRequest("${r.name}", init)`;
608
+ }
609
+ if (r.handler === "sse") {
610
+ const inputType = r.inputSource
611
+ ? `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`
612
+ : "Record<string, any>";
613
+ const eventsType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Events`;
614
+ return `${toCamelCase(r.routeName)}: (input: ${inputType}, options?: Omit<SSEOptions, "endpoint" | "channels">): SSESubscription<${eventsType}> =>
615
+ this.connectToSSERoute("${r.name}", input, options)`;
616
+ }
617
+ if (r.handler === "stream" || r.handler === "html") {
618
+ const inputType = r.inputSource
619
+ ? `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`
620
+ : "Record<string, any>";
621
+ return `${toCamelCase(r.routeName)}: (input: ${inputType}): Promise<Response> =>
622
+ this.streamRequest("${r.name}", input)`;
623
+ }
624
+ if (r.handler === "formData") {
625
+ const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`;
626
+ const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Output`;
627
+ return `${toCamelCase(r.routeName)}: (fields: ${inputType}, files?: File[]): Promise<${outputType}> =>
628
+ this.uploadFormData("${r.name}", fields, files)`;
629
+ }
630
+ return null;
631
+ };
552
632
 
553
- const allMethods = [...methodEntries, ...rawMethodEntries, ...sseMethodEntries, ...streamMethodEntries, ...formDataMethodEntries];
633
+ const namespaceTree = buildNamespaceTree(routeGroups, generateMethodForRoute);
554
634
 
555
- if (allMethods.length > 0) {
556
- routeNamespaceBlocks.push(` ${methodName} = {
557
- ${allMethods.join(",\n\n")}
558
- };`);
635
+ // Generate namespace blocks from tree
636
+ for (const [topLevel, node] of namespaceTree) {
637
+ if (topLevel === "_root") {
638
+ // Root level methods become direct class properties
639
+ for (const method of node.methods) {
640
+ routeNamespaceBlocks.push(` ${method.trim().replace(/^\s+/gm, " ")};`);
641
+ }
642
+ continue;
559
643
  }
644
+
645
+ const content = generateNestedNamespaceCode(node, " ");
646
+ routeNamespaceBlocks.push(` ${topLevel} = {\n${content}\n };`);
560
647
  }
561
648
 
562
649
  // Generate event types