@donkeylabs/server 2.0.15 → 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 +1 -1
- package/src/generator/index.ts +135 -48
- package/src/server.ts +10 -6
package/package.json
CHANGED
package/src/generator/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
633
|
+
const namespaceTree = buildNamespaceTree(routeGroups, generateMethodForRoute);
|
|
554
634
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
package/src/server.ts
CHANGED
|
@@ -210,6 +210,7 @@ export class AppServer {
|
|
|
210
210
|
// Custom services registry
|
|
211
211
|
private serviceFactories = new Map<string, ServiceFactory<any>>();
|
|
212
212
|
private serviceRegistry: Record<string, any> = {};
|
|
213
|
+
private generateModeTimer?: ReturnType<typeof setTimeout>;
|
|
213
214
|
|
|
214
215
|
constructor(options: ServerConfig) {
|
|
215
216
|
// Port priority: explicit config > PORT env var > default 3000
|
|
@@ -500,15 +501,18 @@ export class AppServer {
|
|
|
500
501
|
use(router: IRouter): this {
|
|
501
502
|
this.routers.push(router);
|
|
502
503
|
|
|
503
|
-
//
|
|
504
|
+
// Handle CLI type generation mode with debounced timer
|
|
504
505
|
// This handles SvelteKit-style entry files that don't call start()
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
//
|
|
508
|
-
|
|
506
|
+
if (process.env.DONKEYLABS_GENERATE === "1") {
|
|
507
|
+
// Clear any existing timer and set a new one
|
|
508
|
+
// This ensures we wait for all use() calls to complete
|
|
509
|
+
if (this.generateModeTimer) {
|
|
510
|
+
clearTimeout(this.generateModeTimer);
|
|
511
|
+
}
|
|
512
|
+
this.generateModeTimer = setTimeout(() => {
|
|
509
513
|
this.outputRoutesForGeneration();
|
|
510
514
|
process.exit(0);
|
|
511
|
-
});
|
|
515
|
+
}, 100); // 100ms debounce - waits for all route registrations
|
|
512
516
|
}
|
|
513
517
|
|
|
514
518
|
return this;
|