@donkeylabs/cli 2.1.0 → 2.3.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
|
@@ -18,7 +18,8 @@ export async function generateClientCommand(
|
|
|
18
18
|
// Extract routes from server
|
|
19
19
|
const entryPath = config.entry || "./src/index.ts";
|
|
20
20
|
console.log(pc.dim(`Extracting routes from ${entryPath}...`));
|
|
21
|
-
const
|
|
21
|
+
const serverOutput = await extractRoutesFromServer(entryPath);
|
|
22
|
+
const routes = serverOutput.routes;
|
|
22
23
|
|
|
23
24
|
if (routes.length === 0) {
|
|
24
25
|
console.warn(pc.yellow("No routes found - generating empty client"));
|
|
@@ -36,15 +36,27 @@ export interface RouteInfo {
|
|
|
36
36
|
eventsSource?: Record<string, string>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface ProcessInfo {
|
|
40
|
+
name: string;
|
|
41
|
+
events?: Record<string, string>;
|
|
42
|
+
commands?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ServerOutput {
|
|
46
|
+
routes: RouteInfo[];
|
|
47
|
+
processes: ProcessInfo[];
|
|
48
|
+
coreEvents?: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
/**
|
|
40
52
|
* Run the server entry file with DONKEYLABS_GENERATE=1 to get typed route metadata
|
|
41
53
|
*/
|
|
42
|
-
export async function extractRoutesFromServer(entryPath: string): Promise<
|
|
54
|
+
export async function extractRoutesFromServer(entryPath: string): Promise<ServerOutput> {
|
|
43
55
|
const fullPath = join(process.cwd(), entryPath);
|
|
44
56
|
|
|
45
57
|
if (!existsSync(fullPath)) {
|
|
46
58
|
console.warn(pc.yellow(`Entry file not found: ${entryPath}`));
|
|
47
|
-
return [];
|
|
59
|
+
return { routes: [], processes: [] };
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
const TIMEOUT_MS = 10000; // 10 second timeout
|
|
@@ -66,7 +78,7 @@ export async function extractRoutesFromServer(entryPath: string): Promise<RouteI
|
|
|
66
78
|
child.kill("SIGTERM");
|
|
67
79
|
console.warn(pc.yellow(`Route extraction timed out after ${TIMEOUT_MS / 1000}s`));
|
|
68
80
|
console.warn(pc.dim("Make sure routes are registered with server.use() before any blocking operations"));
|
|
69
|
-
resolve([]);
|
|
81
|
+
resolve({ routes: [], processes: [] });
|
|
70
82
|
}, TIMEOUT_MS);
|
|
71
83
|
|
|
72
84
|
child.stdout?.on("data", (data) => {
|
|
@@ -84,7 +96,7 @@ export async function extractRoutesFromServer(entryPath: string): Promise<RouteI
|
|
|
84
96
|
if (code !== 0) {
|
|
85
97
|
console.warn(pc.yellow(`Failed to extract routes from server (exit code ${code})`));
|
|
86
98
|
if (stderr) console.warn(pc.dim(stderr));
|
|
87
|
-
resolve([]);
|
|
99
|
+
resolve({ routes: [], processes: [] });
|
|
88
100
|
return;
|
|
89
101
|
}
|
|
90
102
|
|
|
@@ -105,17 +117,25 @@ export async function extractRoutesFromServer(entryPath: string): Promise<RouteI
|
|
|
105
117
|
eventsSource: r.eventsType,
|
|
106
118
|
};
|
|
107
119
|
});
|
|
108
|
-
|
|
120
|
+
|
|
121
|
+
// Parse process definitions
|
|
122
|
+
const processes: ProcessInfo[] = (result.processes || []).map((p: any) => ({
|
|
123
|
+
name: p.name,
|
|
124
|
+
events: p.events,
|
|
125
|
+
commands: p.commands,
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
resolve({ routes, processes, coreEvents: result.coreEvents });
|
|
109
129
|
} catch (e) {
|
|
110
130
|
console.warn(pc.yellow("Failed to parse route data from server"));
|
|
111
|
-
resolve([]);
|
|
131
|
+
resolve({ routes: [], processes: [] });
|
|
112
132
|
}
|
|
113
133
|
});
|
|
114
134
|
|
|
115
135
|
child.on("error", (err) => {
|
|
116
136
|
clearTimeout(timeout);
|
|
117
137
|
console.warn(pc.yellow(`Failed to run entry file: ${err.message}`));
|
|
118
|
-
resolve([]);
|
|
138
|
+
resolve({ routes: [], processes: [] });
|
|
119
139
|
});
|
|
120
140
|
});
|
|
121
141
|
}
|
package/src/commands/generate.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
|
7
7
|
import { Database } from "bun:sqlite";
|
|
8
8
|
import { generate, KyselyBunSqliteDialect } from "kysely-codegen";
|
|
9
9
|
import * as ts from "typescript";
|
|
10
|
-
import { loadConfig, extractRoutesFromServer, type DonkeylabsConfig, type RouteInfo } from "./generate-utils";
|
|
10
|
+
import { loadConfig, extractRoutesFromServer, type DonkeylabsConfig, type RouteInfo, type ProcessInfo } from "./generate-utils";
|
|
11
11
|
|
|
12
12
|
async function getPluginExportName(pluginPath: string): Promise<string | null> {
|
|
13
13
|
try {
|
|
@@ -918,9 +918,11 @@ export async function generateCommand(_args: string[]): Promise<void> {
|
|
|
918
918
|
);
|
|
919
919
|
}
|
|
920
920
|
|
|
921
|
-
// Extract routes by running the server with DONKEYLABS_GENERATE=1
|
|
921
|
+
// Extract routes and processes by running the server with DONKEYLABS_GENERATE=1
|
|
922
922
|
const entryPath = config.entry || "./src/index.ts";
|
|
923
|
-
const
|
|
923
|
+
const serverOutput = await extractRoutesFromServer(entryPath);
|
|
924
|
+
const serverRoutes = serverOutput.routes;
|
|
925
|
+
const serverProcesses = serverOutput.processes;
|
|
924
926
|
|
|
925
927
|
// Find custom service definitions
|
|
926
928
|
const services = await findServiceDefinitions(entryPath);
|
|
@@ -940,13 +942,22 @@ export async function generateCommand(_args: string[]): Promise<void> {
|
|
|
940
942
|
);
|
|
941
943
|
}
|
|
942
944
|
|
|
945
|
+
// Log process definitions found
|
|
946
|
+
if (serverProcesses.length > 0) {
|
|
947
|
+
console.log(
|
|
948
|
+
pc.green("Found processes:"),
|
|
949
|
+
serverProcesses.map((p) => pc.dim(p.name)).join(", ")
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
943
953
|
// Generate all files
|
|
944
954
|
await generateRegistry(plugins, outPath);
|
|
945
955
|
await generateContext(plugins, services, outPath);
|
|
946
956
|
await generateRouteTypes(fileRoutes, outPath);
|
|
947
|
-
await generateEventTypes(serverEvents, outPath);
|
|
957
|
+
await generateEventTypes(serverEvents, outPath, serverOutput.coreEvents);
|
|
958
|
+
await generateProcessTypes(serverProcesses, outPath);
|
|
948
959
|
|
|
949
|
-
const generated = ["registry", "context", "routes", "events"];
|
|
960
|
+
const generated = ["registry", "context", "routes", "events", "processes"];
|
|
950
961
|
|
|
951
962
|
// Determine client output path
|
|
952
963
|
const clientOutput = config.client?.output || join(outPath, "client.ts");
|
|
@@ -1362,9 +1373,17 @@ function parseZodObjectFields(objectContent: string): string {
|
|
|
1362
1373
|
|
|
1363
1374
|
/**
|
|
1364
1375
|
* Generate events.ts with namespace-nested types and EventMap.
|
|
1376
|
+
* Includes both core framework events and user-defined events.
|
|
1365
1377
|
*/
|
|
1366
|
-
async function generateEventTypes(
|
|
1367
|
-
|
|
1378
|
+
async function generateEventTypes(
|
|
1379
|
+
events: EventDefinitionInfo[],
|
|
1380
|
+
outPath: string,
|
|
1381
|
+
coreEvents?: Record<string, string>
|
|
1382
|
+
): Promise<void> {
|
|
1383
|
+
const hasCoreEvents = coreEvents && Object.keys(coreEvents).length > 0;
|
|
1384
|
+
const hasUserEvents = events.length > 0;
|
|
1385
|
+
|
|
1386
|
+
if (!hasCoreEvents && !hasUserEvents) {
|
|
1368
1387
|
// Still generate an empty file for consistency
|
|
1369
1388
|
const content = `// Auto-generated by donkeylabs generate
|
|
1370
1389
|
// Events - import as: import { type EventMap, type EventName } from ".@donkeylabs/server/events";
|
|
@@ -1387,10 +1406,32 @@ declare module "@donkeylabs/server" {
|
|
|
1387
1406
|
return;
|
|
1388
1407
|
}
|
|
1389
1408
|
|
|
1390
|
-
// Group events by namespace (first part of event name)
|
|
1409
|
+
// Group ALL events by namespace (first part of event name)
|
|
1391
1410
|
// e.g., "order.created" -> namespace "Order", event "Created"
|
|
1392
|
-
|
|
1411
|
+
// e.g., "workflow.step.started" -> namespace "Workflow", event "StepStarted"
|
|
1412
|
+
const byNamespace = new Map<string, { eventName: string; pascalName: string; tsType: string; fullName: string; isCore: boolean }[]>();
|
|
1413
|
+
|
|
1414
|
+
// Add core events first (using pre-computed TS type strings)
|
|
1415
|
+
if (hasCoreEvents) {
|
|
1416
|
+
for (const [eventFullName, tsTypeString] of Object.entries(coreEvents!)) {
|
|
1417
|
+
const parts = eventFullName.split(".");
|
|
1418
|
+
const namespace = toPascalCase(parts[0] || "App");
|
|
1419
|
+
const eventName = parts.slice(1).map(p => toPascalCase(p)).join("") || toPascalCase(parts[0] || "Event");
|
|
1420
|
+
|
|
1421
|
+
if (!byNamespace.has(namespace)) {
|
|
1422
|
+
byNamespace.set(namespace, []);
|
|
1423
|
+
}
|
|
1424
|
+
byNamespace.get(namespace)!.push({
|
|
1425
|
+
eventName,
|
|
1426
|
+
pascalName: eventName,
|
|
1427
|
+
tsType: tsTypeString,
|
|
1428
|
+
fullName: eventFullName,
|
|
1429
|
+
isCore: true,
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1393
1433
|
|
|
1434
|
+
// Add user-defined events (from Zod schemas)
|
|
1394
1435
|
for (const event of events) {
|
|
1395
1436
|
const parts = event.name.split(".");
|
|
1396
1437
|
const namespace = toPascalCase(parts[0] || "App");
|
|
@@ -1402,25 +1443,41 @@ declare module "@donkeylabs/server" {
|
|
|
1402
1443
|
byNamespace.get(namespace)!.push({
|
|
1403
1444
|
eventName,
|
|
1404
1445
|
pascalName: eventName,
|
|
1405
|
-
|
|
1446
|
+
tsType: zodSchemaToTypeScript(event.schemaSource),
|
|
1406
1447
|
fullName: event.name,
|
|
1448
|
+
isCore: false,
|
|
1407
1449
|
});
|
|
1408
1450
|
}
|
|
1409
1451
|
|
|
1410
1452
|
// Generate namespace blocks
|
|
1411
1453
|
const namespaceBlocks: string[] = [];
|
|
1454
|
+
const coreNamespaceBlocks: string[] = [];
|
|
1455
|
+
const userNamespaceBlocks: string[] = [];
|
|
1412
1456
|
const eventMapEntries: string[] = [];
|
|
1413
1457
|
const eventRegistryEntries: string[] = [];
|
|
1414
1458
|
const eventNames: string[] = [];
|
|
1415
1459
|
|
|
1460
|
+
// Track which namespaces are core-only, user-only, or mixed
|
|
1416
1461
|
for (const [namespace, nsEvents] of byNamespace) {
|
|
1462
|
+
const hasCoreInNs = nsEvents.some(e => e.isCore);
|
|
1463
|
+
const hasUserInNs = nsEvents.some(e => !e.isCore);
|
|
1464
|
+
|
|
1417
1465
|
const eventTypeDecls = nsEvents.map(e => {
|
|
1418
|
-
const tsType = zodSchemaToTypeScript(e.schemaSource);
|
|
1419
1466
|
return ` /** Event data for "${e.fullName}" */
|
|
1420
|
-
export type ${e.pascalName} = ${tsType};`;
|
|
1467
|
+
export type ${e.pascalName} = ${e.tsType};`;
|
|
1421
1468
|
}).join("\n\n");
|
|
1422
1469
|
|
|
1423
|
-
|
|
1470
|
+
const block = `export namespace ${namespace} {\n${eventTypeDecls}\n}`;
|
|
1471
|
+
|
|
1472
|
+
if (hasCoreInNs && !hasUserInNs) {
|
|
1473
|
+
coreNamespaceBlocks.push(block);
|
|
1474
|
+
} else if (!hasCoreInNs && hasUserInNs) {
|
|
1475
|
+
userNamespaceBlocks.push(block);
|
|
1476
|
+
} else {
|
|
1477
|
+
// Mixed - put in core section
|
|
1478
|
+
coreNamespaceBlocks.push(block);
|
|
1479
|
+
}
|
|
1480
|
+
namespaceBlocks.push(block);
|
|
1424
1481
|
|
|
1425
1482
|
// Add to EventMap and EventRegistry
|
|
1426
1483
|
for (const e of nsEvents) {
|
|
@@ -1430,11 +1487,20 @@ declare module "@donkeylabs/server" {
|
|
|
1430
1487
|
}
|
|
1431
1488
|
}
|
|
1432
1489
|
|
|
1490
|
+
// Build the output with clear sections
|
|
1491
|
+
const sections: string[] = [];
|
|
1492
|
+
|
|
1493
|
+
if (coreNamespaceBlocks.length > 0) {
|
|
1494
|
+
sections.push(`// Core framework events\n${coreNamespaceBlocks.join("\n\n")}`);
|
|
1495
|
+
}
|
|
1496
|
+
if (userNamespaceBlocks.length > 0) {
|
|
1497
|
+
sections.push(`// User-defined events\n${userNamespaceBlocks.join("\n\n")}`);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1433
1500
|
const content = `// Auto-generated by donkeylabs generate
|
|
1434
1501
|
// Events - import as: import { type EventMap, type EventName, Order, User } from ".@donkeylabs/server/events";
|
|
1435
1502
|
|
|
1436
|
-
|
|
1437
|
-
${namespaceBlocks.join("\n\n")}
|
|
1503
|
+
${sections.join("\n\n")}
|
|
1438
1504
|
|
|
1439
1505
|
/** Map of all event names to their data types */
|
|
1440
1506
|
export interface EventMap {
|
|
@@ -1454,3 +1520,99 @@ ${eventRegistryEntries.join("\n")}
|
|
|
1454
1520
|
|
|
1455
1521
|
await writeFile(join(outPath, "events.ts"), content);
|
|
1456
1522
|
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Generate processes.ts with typed events, commands, and ProcessRegistry augmentation.
|
|
1526
|
+
*/
|
|
1527
|
+
async function generateProcessTypes(processes: ProcessInfo[], outPath: string): Promise<void> {
|
|
1528
|
+
if (processes.length === 0) {
|
|
1529
|
+
const content = `// Auto-generated by donkeylabs generate
|
|
1530
|
+
// Processes - import as: import { type ProcessEventMap, type ProcessCommandMap } from ".@donkeylabs/server/processes";
|
|
1531
|
+
|
|
1532
|
+
/** Map of all process event names to their data types */
|
|
1533
|
+
export interface ProcessEventMap {}
|
|
1534
|
+
|
|
1535
|
+
/** Map of all process command names to their data types */
|
|
1536
|
+
export interface ProcessCommandMap {}
|
|
1537
|
+
|
|
1538
|
+
// Augment ProcessRegistry for typed send() (empty when no processes defined)
|
|
1539
|
+
declare module "@donkeylabs/server" {
|
|
1540
|
+
interface ProcessRegistry {}
|
|
1541
|
+
}
|
|
1542
|
+
`;
|
|
1543
|
+
await writeFile(join(outPath, "processes.ts"), content);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const namespaceBlocks: string[] = [];
|
|
1548
|
+
const eventMapEntries: string[] = [];
|
|
1549
|
+
const commandMapEntries: string[] = [];
|
|
1550
|
+
const registryEntries: string[] = [];
|
|
1551
|
+
|
|
1552
|
+
for (const proc of processes) {
|
|
1553
|
+
const pascalName = toPascalCase(proc.name);
|
|
1554
|
+
|
|
1555
|
+
const eventDecls: string[] = [];
|
|
1556
|
+
const commandDecls: string[] = [];
|
|
1557
|
+
|
|
1558
|
+
// Generate event types
|
|
1559
|
+
if (proc.events) {
|
|
1560
|
+
for (const [eventName, tsType] of Object.entries(proc.events)) {
|
|
1561
|
+
const pascalEvent = toPascalCase(eventName);
|
|
1562
|
+
eventDecls.push(` export type ${pascalEvent} = ${tsType};`);
|
|
1563
|
+
eventMapEntries.push(` "${proc.name}.${eventName}": ${pascalName}.Events.${pascalEvent};`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// Generate command types
|
|
1568
|
+
if (proc.commands) {
|
|
1569
|
+
for (const [cmdName, tsType] of Object.entries(proc.commands)) {
|
|
1570
|
+
const pascalCmd = toPascalCase(cmdName);
|
|
1571
|
+
commandDecls.push(` export type ${pascalCmd} = ${tsType};`);
|
|
1572
|
+
commandMapEntries.push(` "${proc.name}.${cmdName}": ${pascalName}.Commands.${pascalCmd};`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Build namespace
|
|
1577
|
+
const eventsNs = eventDecls.length > 0
|
|
1578
|
+
? ` export namespace Events {\n${eventDecls.join("\n")}\n }`
|
|
1579
|
+
: ` export namespace Events {}`;
|
|
1580
|
+
const commandsNs = commandDecls.length > 0
|
|
1581
|
+
? ` export namespace Commands {\n${commandDecls.join("\n")}\n }`
|
|
1582
|
+
: ` export namespace Commands {}`;
|
|
1583
|
+
|
|
1584
|
+
namespaceBlocks.push(`export namespace ${pascalName} {\n${eventsNs}\n${commandsNs}\n}`);
|
|
1585
|
+
|
|
1586
|
+
// Registry entry
|
|
1587
|
+
registryEntries.push(` "${proc.name}": {
|
|
1588
|
+
events: ${pascalName}.Events;
|
|
1589
|
+
commands: ${pascalName}.Commands;
|
|
1590
|
+
};`);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const content = `// Auto-generated by donkeylabs generate
|
|
1594
|
+
// Processes - import as: import { type ProcessEventMap, type ProcessCommandMap } from ".@donkeylabs/server/processes";
|
|
1595
|
+
|
|
1596
|
+
// Process type namespaces
|
|
1597
|
+
${namespaceBlocks.join("\n\n")}
|
|
1598
|
+
|
|
1599
|
+
/** Map of all process event names to their data types */
|
|
1600
|
+
export interface ProcessEventMap {
|
|
1601
|
+
${eventMapEntries.join("\n")}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/** Map of all process command names to their data types */
|
|
1605
|
+
export interface ProcessCommandMap {
|
|
1606
|
+
${commandMapEntries.join("\n")}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Augment ProcessRegistry for typed send()
|
|
1610
|
+
declare module "@donkeylabs/server" {
|
|
1611
|
+
interface ProcessRegistry {
|
|
1612
|
+
${registryEntries.join("\n")}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
`;
|
|
1616
|
+
|
|
1617
|
+
await writeFile(join(outPath, "processes.ts"), content);
|
|
1618
|
+
}
|