@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/cli",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -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 servicesType = serviceEntries.length > 0
814
- ? `{\n${serviceEntries.join("\n")}\n }`
815
- : "Record<string, never>";
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
- export interface AppServices ${servicesType}
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.18",
28
- "@donkeylabs/adapter-sveltekit": "^1.1.18",
29
- "@donkeylabs/server": "^1.1.18",
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
+ });