@donkeylabs/cli 1.1.18 → 1.1.20
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
package/src/commands/generate.ts
CHANGED
|
@@ -90,6 +90,121 @@ interface ServiceDefinitionInfo {
|
|
|
90
90
|
returnType: string | null;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
interface EventDefinitionInfo {
|
|
94
|
+
name: string; // e.g., "order.created"
|
|
95
|
+
schemaSource: string; // e.g., "z.object({ orderId: z.string() })"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract events from a defineEvents() call in a file.
|
|
100
|
+
* Parses patterns like:
|
|
101
|
+
* export const events = defineEvents({
|
|
102
|
+
* "order.created": z.object({ orderId: z.string() }),
|
|
103
|
+
* })
|
|
104
|
+
*/
|
|
105
|
+
async function extractEventsFromFile(filePath: string): Promise<EventDefinitionInfo[]> {
|
|
106
|
+
try {
|
|
107
|
+
const content = await readFile(filePath, "utf-8");
|
|
108
|
+
const events: EventDefinitionInfo[] = [];
|
|
109
|
+
|
|
110
|
+
// Find defineEvents call
|
|
111
|
+
const defineEventsMatch = content.match(/defineEvents\s*\(\s*\{/);
|
|
112
|
+
if (!defineEventsMatch || defineEventsMatch.index === undefined) {
|
|
113
|
+
return events;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extract the object block
|
|
117
|
+
const blockStart = defineEventsMatch.index + defineEventsMatch[0].length - 1;
|
|
118
|
+
const block = extractBalancedBlock(content, blockStart, "{", "}");
|
|
119
|
+
if (!block) return events;
|
|
120
|
+
|
|
121
|
+
// Parse event entries: "event.name": z.object({...})
|
|
122
|
+
// Match quoted keys followed by Zod schemas
|
|
123
|
+
const eventPattern = /["']([^"']+)["']\s*:\s*(z\.[^,}]+(?:\([^)]*\))?)/g;
|
|
124
|
+
|
|
125
|
+
let match;
|
|
126
|
+
const innerBlock = block.slice(1, -1); // Remove outer braces
|
|
127
|
+
|
|
128
|
+
// More robust extraction - find each event key and its schema
|
|
129
|
+
const keyPattern = /["']([a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)*)["']\s*:/gi;
|
|
130
|
+
let keyMatch;
|
|
131
|
+
const keyPositions: { name: string; pos: number }[] = [];
|
|
132
|
+
|
|
133
|
+
while ((keyMatch = keyPattern.exec(innerBlock)) !== null) {
|
|
134
|
+
keyPositions.push({ name: keyMatch[1]!, pos: keyMatch.index + keyMatch[0].length });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// For each key, extract the Zod schema that follows
|
|
138
|
+
for (let i = 0; i < keyPositions.length; i++) {
|
|
139
|
+
const { name, pos } = keyPositions[i]!;
|
|
140
|
+
const nextPos = keyPositions[i + 1]?.pos ?? innerBlock.length;
|
|
141
|
+
|
|
142
|
+
// Get the slice between this key and the next
|
|
143
|
+
let schemaSlice = innerBlock.slice(pos, nextPos).trim();
|
|
144
|
+
|
|
145
|
+
// Find where the Zod expression ends
|
|
146
|
+
if (schemaSlice.startsWith("z.")) {
|
|
147
|
+
// Extract balanced parentheses for the schema
|
|
148
|
+
let depth = 0;
|
|
149
|
+
let endIdx = 0;
|
|
150
|
+
let foundParen = false;
|
|
151
|
+
|
|
152
|
+
for (let j = 0; j < schemaSlice.length; j++) {
|
|
153
|
+
if (schemaSlice[j] === "(") {
|
|
154
|
+
depth++;
|
|
155
|
+
foundParen = true;
|
|
156
|
+
} else if (schemaSlice[j] === ")") {
|
|
157
|
+
depth--;
|
|
158
|
+
if (depth === 0 && foundParen) {
|
|
159
|
+
endIdx = j + 1;
|
|
160
|
+
// Check for chained methods
|
|
161
|
+
const rest = schemaSlice.slice(endIdx);
|
|
162
|
+
const chainMatch = rest.match(/^(\s*\.\w+\([^)]*\))+/);
|
|
163
|
+
if (chainMatch) {
|
|
164
|
+
endIdx += chainMatch[0].length;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (endIdx > 0) {
|
|
172
|
+
const schema = schemaSlice.slice(0, endIdx).trim();
|
|
173
|
+
events.push({ name, schemaSource: schema });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return events;
|
|
179
|
+
} catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Find and extract server-level events from common locations.
|
|
186
|
+
*/
|
|
187
|
+
async function findServerEvents(): Promise<EventDefinitionInfo[]> {
|
|
188
|
+
const possiblePaths = [
|
|
189
|
+
"src/events.ts",
|
|
190
|
+
"src/server/events.ts",
|
|
191
|
+
"src/lib/events.ts",
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const path of possiblePaths) {
|
|
195
|
+
const fullPath = join(process.cwd(), path);
|
|
196
|
+
if (existsSync(fullPath)) {
|
|
197
|
+
const events = await extractEventsFromFile(fullPath);
|
|
198
|
+
if (events.length > 0) {
|
|
199
|
+
console.log(pc.dim(` Found events in ${path}`));
|
|
200
|
+
return events;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
93
208
|
/**
|
|
94
209
|
* Extract defineService calls from source files.
|
|
95
210
|
* Looks for patterns like: export const myService = defineService("name", ...)
|
|
@@ -632,12 +747,22 @@ export async function generateCommand(_args: string[]): Promise<void> {
|
|
|
632
747
|
);
|
|
633
748
|
}
|
|
634
749
|
|
|
750
|
+
// Find server-level events
|
|
751
|
+
const serverEvents = await findServerEvents();
|
|
752
|
+
if (serverEvents.length > 0) {
|
|
753
|
+
console.log(
|
|
754
|
+
pc.green("Found events:"),
|
|
755
|
+
serverEvents.map((e) => pc.dim(e.name)).join(", ")
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
635
759
|
// Generate all files
|
|
636
760
|
await generateRegistry(plugins, outPath);
|
|
637
761
|
await generateContext(plugins, services, outPath);
|
|
638
762
|
await generateRouteTypes(fileRoutes, outPath);
|
|
763
|
+
await generateEventTypes(serverEvents, outPath);
|
|
639
764
|
|
|
640
|
-
const generated = ["registry", "context", "routes"];
|
|
765
|
+
const generated = ["registry", "context", "routes", "events"];
|
|
641
766
|
|
|
642
767
|
// Determine client output path
|
|
643
768
|
const clientOutput = config.client?.output || join(outPath, "client.ts");
|
|
@@ -810,9 +935,10 @@ async function generateContext(
|
|
|
810
935
|
}
|
|
811
936
|
|
|
812
937
|
const serviceImportsBlock = serviceImports.length > 0 ? serviceImports.join("\n") + "\n" : "";
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
938
|
+
const hasServices = serviceEntries.length > 0;
|
|
939
|
+
const servicesTypeDecl = hasServices
|
|
940
|
+
? `export interface AppServices {\n${serviceEntries.join("\n")}\n }`
|
|
941
|
+
: "export type AppServices = Record<string, never>;";
|
|
816
942
|
|
|
817
943
|
const content = `// Auto-generated by donkeylabs generate
|
|
818
944
|
// App context - import as: import type { AppContext } from ".@donkeylabs/server/context";
|
|
@@ -825,7 +951,7 @@ ${serviceImportsBlock}
|
|
|
825
951
|
export type DatabaseSchema = ${schemaIntersection};
|
|
826
952
|
|
|
827
953
|
/** Custom services registered via defineService() */
|
|
828
|
-
|
|
954
|
+
${servicesTypeDecl}
|
|
829
955
|
|
|
830
956
|
// Augment the ServiceRegistry for type inference in ctx.services
|
|
831
957
|
declare module "@donkeylabs/server" {
|
|
@@ -973,6 +1099,173 @@ ${namespaceBlocks.join("\n\n")}
|
|
|
973
1099
|
await writeFile(join(outPath, "routes.ts"), content);
|
|
974
1100
|
}
|
|
975
1101
|
|
|
1102
|
+
/**
|
|
1103
|
+
* Convert a Zod schema source to a TypeScript type string.
|
|
1104
|
+
* Handles common patterns like z.object({ ... }), z.string(), etc.
|
|
1105
|
+
*/
|
|
1106
|
+
function zodSchemaToTypeScript(schemaSource: string): string {
|
|
1107
|
+
// Handle z.object({ ... })
|
|
1108
|
+
if (schemaSource.startsWith("z.object(")) {
|
|
1109
|
+
const inner = schemaSource.slice(9, -1); // Remove z.object( and )
|
|
1110
|
+
// Parse the object fields
|
|
1111
|
+
return parseZodObjectFields(inner);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Handle z.string(), z.number(), etc.
|
|
1115
|
+
if (schemaSource.startsWith("z.string")) return "string";
|
|
1116
|
+
if (schemaSource.startsWith("z.number")) return "number";
|
|
1117
|
+
if (schemaSource.startsWith("z.boolean")) return "boolean";
|
|
1118
|
+
if (schemaSource.startsWith("z.date")) return "Date";
|
|
1119
|
+
if (schemaSource.startsWith("z.undefined")) return "undefined";
|
|
1120
|
+
if (schemaSource.startsWith("z.null")) return "null";
|
|
1121
|
+
if (schemaSource.startsWith("z.any")) return "any";
|
|
1122
|
+
if (schemaSource.startsWith("z.unknown")) return "unknown";
|
|
1123
|
+
|
|
1124
|
+
// Handle z.array(...)
|
|
1125
|
+
if (schemaSource.startsWith("z.array(")) {
|
|
1126
|
+
const inner = schemaSource.slice(8, -1);
|
|
1127
|
+
return `${zodSchemaToTypeScript(inner)}[]`;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Handle z.enum([...])
|
|
1131
|
+
if (schemaSource.startsWith("z.enum(")) {
|
|
1132
|
+
const inner = schemaSource.slice(7, -1);
|
|
1133
|
+
// Extract values from ["a", "b", "c"]
|
|
1134
|
+
const match = inner.match(/\[(.*)\]/);
|
|
1135
|
+
if (match?.[1]) {
|
|
1136
|
+
return match[1].split(",").map(s => s.trim()).join(" | ");
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Fallback - return the source as a comment
|
|
1141
|
+
return "unknown /* " + schemaSource + " */";
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Parse z.object({ field: z.string(), ... }) inner content to TypeScript.
|
|
1146
|
+
*/
|
|
1147
|
+
function parseZodObjectFields(objectContent: string): string {
|
|
1148
|
+
// Remove outer braces if present
|
|
1149
|
+
let content = objectContent.trim();
|
|
1150
|
+
if (content.startsWith("{")) content = content.slice(1);
|
|
1151
|
+
if (content.endsWith("}")) content = content.slice(0, -1);
|
|
1152
|
+
|
|
1153
|
+
const fields: string[] = [];
|
|
1154
|
+
|
|
1155
|
+
// Match field patterns: fieldName: z.type()
|
|
1156
|
+
const fieldPattern = /(\w+)\s*:\s*(z\.[^,}]+(?:\([^)]*\))?)/g;
|
|
1157
|
+
let match;
|
|
1158
|
+
|
|
1159
|
+
while ((match = fieldPattern.exec(content)) !== null) {
|
|
1160
|
+
const fieldName = match[1]!;
|
|
1161
|
+
let fieldSchema = match[2]!;
|
|
1162
|
+
|
|
1163
|
+
// Check for .optional() modifier
|
|
1164
|
+
const isOptional = fieldSchema.includes(".optional()");
|
|
1165
|
+
fieldSchema = fieldSchema.replace(/\.optional\(\)/, "");
|
|
1166
|
+
|
|
1167
|
+
const tsType = zodSchemaToTypeScript(fieldSchema);
|
|
1168
|
+
fields.push(`${fieldName}${isOptional ? "?" : ""}: ${tsType}`);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return `{ ${fields.join("; ")} }`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Generate events.ts with namespace-nested types and EventMap.
|
|
1176
|
+
*/
|
|
1177
|
+
async function generateEventTypes(events: EventDefinitionInfo[], outPath: string): Promise<void> {
|
|
1178
|
+
if (events.length === 0) {
|
|
1179
|
+
// Still generate an empty file for consistency
|
|
1180
|
+
const content = `// Auto-generated by donkeylabs generate
|
|
1181
|
+
// Events - import as: import { type EventMap, type EventName } from ".@donkeylabs/server/events";
|
|
1182
|
+
|
|
1183
|
+
/** Map of all event names to their data types */
|
|
1184
|
+
export interface EventMap {}
|
|
1185
|
+
|
|
1186
|
+
/** Union of all available event names */
|
|
1187
|
+
export type EventName = never;
|
|
1188
|
+
|
|
1189
|
+
/** Namespace for event types - use as Order.Created */
|
|
1190
|
+
export namespace Events {}
|
|
1191
|
+
|
|
1192
|
+
// Augment EventRegistry for typed emit/on (empty when no events defined)
|
|
1193
|
+
declare module "@donkeylabs/server" {
|
|
1194
|
+
interface EventRegistry {}
|
|
1195
|
+
}
|
|
1196
|
+
`;
|
|
1197
|
+
await writeFile(join(outPath, "events.ts"), content);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Group events by namespace (first part of event name)
|
|
1202
|
+
// e.g., "order.created" -> namespace "Order", event "Created"
|
|
1203
|
+
const byNamespace = new Map<string, { eventName: string; pascalName: string; schemaSource: string; fullName: string }[]>();
|
|
1204
|
+
|
|
1205
|
+
for (const event of events) {
|
|
1206
|
+
const parts = event.name.split(".");
|
|
1207
|
+
const namespace = toPascalCase(parts[0] || "App");
|
|
1208
|
+
const eventName = parts.slice(1).map(p => toPascalCase(p)).join("") || toPascalCase(parts[0] || "Event");
|
|
1209
|
+
|
|
1210
|
+
if (!byNamespace.has(namespace)) {
|
|
1211
|
+
byNamespace.set(namespace, []);
|
|
1212
|
+
}
|
|
1213
|
+
byNamespace.get(namespace)!.push({
|
|
1214
|
+
eventName,
|
|
1215
|
+
pascalName: eventName,
|
|
1216
|
+
schemaSource: event.schemaSource,
|
|
1217
|
+
fullName: event.name,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Generate namespace blocks
|
|
1222
|
+
const namespaceBlocks: string[] = [];
|
|
1223
|
+
const eventMapEntries: string[] = [];
|
|
1224
|
+
const eventRegistryEntries: string[] = [];
|
|
1225
|
+
const eventNames: string[] = [];
|
|
1226
|
+
|
|
1227
|
+
for (const [namespace, nsEvents] of byNamespace) {
|
|
1228
|
+
const eventTypeDecls = nsEvents.map(e => {
|
|
1229
|
+
const tsType = zodSchemaToTypeScript(e.schemaSource);
|
|
1230
|
+
return ` /** Event data for "${e.fullName}" */
|
|
1231
|
+
export type ${e.pascalName} = ${tsType};`;
|
|
1232
|
+
}).join("\n\n");
|
|
1233
|
+
|
|
1234
|
+
namespaceBlocks.push(`export namespace ${namespace} {\n${eventTypeDecls}\n}`);
|
|
1235
|
+
|
|
1236
|
+
// Add to EventMap and EventRegistry
|
|
1237
|
+
for (const e of nsEvents) {
|
|
1238
|
+
eventMapEntries.push(` "${e.fullName}": ${namespace}.${e.pascalName};`);
|
|
1239
|
+
eventRegistryEntries.push(` "${e.fullName}": ${namespace}.${e.pascalName};`);
|
|
1240
|
+
eventNames.push(`"${e.fullName}"`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const content = `// Auto-generated by donkeylabs generate
|
|
1245
|
+
// Events - import as: import { type EventMap, type EventName, Order, User } from ".@donkeylabs/server/events";
|
|
1246
|
+
|
|
1247
|
+
// Event type namespaces - use as Order.Created, User.Signup, etc.
|
|
1248
|
+
${namespaceBlocks.join("\n\n")}
|
|
1249
|
+
|
|
1250
|
+
/** Map of all event names to their data types */
|
|
1251
|
+
export interface EventMap {
|
|
1252
|
+
${eventMapEntries.join("\n")}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/** Union of all available event names */
|
|
1256
|
+
export type EventName = ${eventNames.join(" | ") || "never"};
|
|
1257
|
+
|
|
1258
|
+
// Augment EventRegistry for typed emit/on
|
|
1259
|
+
declare module "@donkeylabs/server" {
|
|
1260
|
+
interface EventRegistry {
|
|
1261
|
+
${eventRegistryEntries.join("\n")}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
`;
|
|
1265
|
+
|
|
1266
|
+
await writeFile(join(outPath, "events.ts"), content);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
976
1269
|
async function generateClientFromRoutes(
|
|
977
1270
|
routes: ExtractedRoute[],
|
|
978
1271
|
outputPath: string
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"vite": "^7.2.6"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@donkeylabs/cli": "^1.1.
|
|
28
|
-
"@donkeylabs/adapter-sveltekit": "^1.1.
|
|
29
|
-
"@donkeylabs/server": "^1.1.
|
|
27
|
+
"@donkeylabs/cli": "^1.1.19",
|
|
28
|
+
"@donkeylabs/adapter-sveltekit": "^1.1.19",
|
|
29
|
+
"@donkeylabs/server": "^1.1.19",
|
|
30
30
|
"bits-ui": "^2.15.4",
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"kysely": "^0.27.6",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineEvents } from "@donkeylabs/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Server-level events.
|
|
6
|
+
* These events are typed and available across the app.
|
|
7
|
+
* Use ctx.core.events.emit("event.name", data) to emit.
|
|
8
|
+
* Use ctx.core.events.on("event.name", handler) to subscribe.
|
|
9
|
+
*/
|
|
10
|
+
export const events = defineEvents({
|
|
11
|
+
"order.created": z.object({
|
|
12
|
+
orderId: z.string(),
|
|
13
|
+
userId: z.string(),
|
|
14
|
+
total: z.number(),
|
|
15
|
+
}),
|
|
16
|
+
"order.shipped": z.object({
|
|
17
|
+
orderId: z.string(),
|
|
18
|
+
trackingNumber: z.string(),
|
|
19
|
+
shippedAt: z.string(),
|
|
20
|
+
}),
|
|
21
|
+
"user.signup": z.object({
|
|
22
|
+
userId: z.string(),
|
|
23
|
+
email: z.string(),
|
|
24
|
+
}),
|
|
25
|
+
"user.verified": z.object({
|
|
26
|
+
userId: z.string(),
|
|
27
|
+
}),
|
|
28
|
+
});
|