@donkeylabs/server 0.1.3 → 0.1.4

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.
Files changed (55) hide show
  1. package/examples/starter/node_modules/@donkeylabs/server/README.md +15 -0
  2. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/generate.ts +461 -0
  3. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/init.ts +476 -0
  4. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/interactive.ts +223 -0
  5. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/plugin.ts +192 -0
  6. package/examples/starter/node_modules/@donkeylabs/server/cli/donkeylabs +106 -0
  7. package/examples/starter/node_modules/@donkeylabs/server/cli/index.ts +100 -0
  8. package/examples/starter/node_modules/@donkeylabs/server/context.d.ts +17 -0
  9. package/examples/starter/node_modules/@donkeylabs/server/docs/api-client.md +520 -0
  10. package/examples/starter/node_modules/@donkeylabs/server/docs/cache.md +437 -0
  11. package/examples/starter/node_modules/@donkeylabs/server/docs/cli.md +353 -0
  12. package/examples/starter/node_modules/@donkeylabs/server/docs/core-services.md +338 -0
  13. package/examples/starter/node_modules/@donkeylabs/server/docs/cron.md +465 -0
  14. package/examples/starter/node_modules/@donkeylabs/server/docs/errors.md +303 -0
  15. package/examples/starter/node_modules/@donkeylabs/server/docs/events.md +460 -0
  16. package/examples/starter/node_modules/@donkeylabs/server/docs/handlers.md +549 -0
  17. package/examples/starter/node_modules/@donkeylabs/server/docs/jobs.md +556 -0
  18. package/examples/starter/node_modules/@donkeylabs/server/docs/logger.md +316 -0
  19. package/examples/starter/node_modules/@donkeylabs/server/docs/middleware.md +682 -0
  20. package/examples/starter/node_modules/@donkeylabs/server/docs/plugins.md +524 -0
  21. package/examples/starter/node_modules/@donkeylabs/server/docs/project-structure.md +493 -0
  22. package/examples/starter/node_modules/@donkeylabs/server/docs/rate-limiter.md +525 -0
  23. package/examples/starter/node_modules/@donkeylabs/server/docs/router.md +566 -0
  24. package/examples/starter/node_modules/@donkeylabs/server/docs/sse.md +542 -0
  25. package/examples/starter/node_modules/@donkeylabs/server/docs/svelte-frontend.md +324 -0
  26. package/examples/starter/node_modules/@donkeylabs/server/mcp/donkeylabs-mcp +3238 -0
  27. package/examples/starter/node_modules/@donkeylabs/server/mcp/server.ts +3238 -0
  28. package/examples/starter/node_modules/@donkeylabs/server/package.json +77 -0
  29. package/examples/starter/node_modules/@donkeylabs/server/registry.d.ts +11 -0
  30. package/examples/starter/node_modules/@donkeylabs/server/src/client/base.ts +481 -0
  31. package/examples/starter/node_modules/@donkeylabs/server/src/client/index.ts +150 -0
  32. package/examples/starter/node_modules/@donkeylabs/server/src/core/cache.ts +183 -0
  33. package/examples/starter/node_modules/@donkeylabs/server/src/core/cron.ts +255 -0
  34. package/examples/starter/node_modules/@donkeylabs/server/src/core/errors.ts +320 -0
  35. package/examples/starter/node_modules/@donkeylabs/server/src/core/events.ts +163 -0
  36. package/examples/starter/node_modules/@donkeylabs/server/src/core/index.ts +94 -0
  37. package/examples/starter/node_modules/@donkeylabs/server/src/core/jobs.ts +334 -0
  38. package/examples/starter/node_modules/@donkeylabs/server/src/core/logger.ts +131 -0
  39. package/examples/starter/node_modules/@donkeylabs/server/src/core/rate-limiter.ts +193 -0
  40. package/examples/starter/node_modules/@donkeylabs/server/src/core/sse.ts +210 -0
  41. package/examples/starter/node_modules/@donkeylabs/server/src/core.ts +428 -0
  42. package/examples/starter/node_modules/@donkeylabs/server/src/handlers.ts +87 -0
  43. package/examples/starter/node_modules/@donkeylabs/server/src/harness.ts +70 -0
  44. package/examples/starter/node_modules/@donkeylabs/server/src/index.ts +38 -0
  45. package/examples/starter/node_modules/@donkeylabs/server/src/middleware.ts +34 -0
  46. package/examples/starter/node_modules/@donkeylabs/server/src/registry.ts +13 -0
  47. package/examples/starter/node_modules/@donkeylabs/server/src/router.ts +155 -0
  48. package/examples/starter/node_modules/@donkeylabs/server/src/server.ts +234 -0
  49. package/examples/starter/node_modules/@donkeylabs/server/templates/init/donkeylabs.config.ts.template +14 -0
  50. package/examples/starter/node_modules/@donkeylabs/server/templates/init/index.ts.template +41 -0
  51. package/examples/starter/node_modules/@donkeylabs/server/templates/plugin/index.ts.template +25 -0
  52. package/examples/starter/src/routes/health/ping/models/model.ts +11 -7
  53. package/package.json +3 -3
  54. package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-native-jsx.d.ts +0 -32
  55. package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-shims-v4.d.ts +0 -290
@@ -0,0 +1,15 @@
1
+ # plugin
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
@@ -0,0 +1,461 @@
1
+ import { readdir, writeFile, readFile, mkdir } from "node:fs/promises";
2
+ import { join, relative, dirname, basename } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import pc from "picocolors";
5
+
6
+ interface DonkeylabsConfig {
7
+ plugins: string[];
8
+ outDir?: string;
9
+ client?: {
10
+ output: string;
11
+ };
12
+ routes?: string; // Route files pattern, default: "./src/routes/**/handler.ts"
13
+ }
14
+
15
+ async function loadConfig(): Promise<DonkeylabsConfig> {
16
+ const configPath = join(process.cwd(), "donkeylabs.config.ts");
17
+
18
+ if (!existsSync(configPath)) {
19
+ throw new Error("donkeylabs.config.ts not found. Run 'donkeylabs init' first.");
20
+ }
21
+
22
+ const config = await import(configPath);
23
+ return config.default;
24
+ }
25
+
26
+ async function getPluginExportName(pluginPath: string): Promise<string | null> {
27
+ try {
28
+ const content = await readFile(pluginPath, "utf-8");
29
+ const match = content.match(/export\s+const\s+(\w+Plugin)\s*=/);
30
+ return match?.[1] ?? null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ async function extractHandlerNames(pluginPath: string): Promise<string[]> {
37
+ try {
38
+ const content = await readFile(pluginPath, "utf-8");
39
+ const handlersMatch = content.match(/handlers:\s*\{([^}]+)\}/);
40
+ if (!handlersMatch?.[1]) return [];
41
+
42
+ const handlersBlock = handlersMatch[1];
43
+ return [...handlersBlock.matchAll(/(\w+)\s*:/g)]
44
+ .map((m) => m[1])
45
+ .filter((name): name is string => !!name);
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ async function extractMiddlewareNames(pluginPath: string): Promise<string[]> {
52
+ try {
53
+ const content = await readFile(pluginPath, "utf-8");
54
+ const middlewareMatch = content.match(/middleware:\s*\{([^}]+)\}/);
55
+ if (!middlewareMatch?.[1]) return [];
56
+
57
+ const middlewareBlock = middlewareMatch[1];
58
+ return [...middlewareBlock.matchAll(/(\w+)\s*:/g)]
59
+ .map((m) => m[1])
60
+ .filter((name): name is string => !!name);
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ async function findPlugins(
67
+ patterns: string[]
68
+ ): Promise<{ name: string; path: string; exportName: string }[]> {
69
+ const plugins: { name: string; path: string; exportName: string }[] = [];
70
+
71
+ for (const pattern of patterns) {
72
+ const baseDir = pattern.includes("**")
73
+ ? pattern.split("**")[0] || "."
74
+ : dirname(pattern);
75
+
76
+ const targetDir = join(process.cwd(), baseDir);
77
+ if (!existsSync(targetDir)) continue;
78
+
79
+ async function scanDir(dir: string): Promise<void> {
80
+ const entries = await readdir(dir, { withFileTypes: true });
81
+
82
+ for (const entry of entries) {
83
+ const fullPath = join(dir, entry.name);
84
+
85
+ if (entry.isDirectory()) {
86
+ await scanDir(fullPath);
87
+ } else if (entry.name === "index.ts") {
88
+ const exportName = await getPluginExportName(fullPath);
89
+ if (exportName) {
90
+ const pluginName = dirname(fullPath).split("/").pop()!;
91
+ plugins.push({
92
+ name: pluginName,
93
+ path: relative(process.cwd(), fullPath),
94
+ exportName,
95
+ });
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ await scanDir(targetDir);
102
+ }
103
+
104
+ return plugins;
105
+ }
106
+
107
+ export async function generateCommand(_args: string[]): Promise<void> {
108
+ console.log(pc.bold("\nGenerating types...\n"));
109
+
110
+ const config = await loadConfig();
111
+ const outDir = config.outDir || ".@donkeylabs/server";
112
+ const outPath = join(process.cwd(), outDir);
113
+
114
+ await mkdir(outPath, { recursive: true });
115
+
116
+ // 1. Find and process plugins
117
+ const plugins = await findPlugins(config.plugins);
118
+
119
+ if (plugins.length === 0) {
120
+ console.log(
121
+ pc.yellow("No plugins found matching patterns:"),
122
+ config.plugins.join(", ")
123
+ );
124
+ } else {
125
+ console.log(pc.dim(`Found ${plugins.length} plugin(s):`));
126
+ for (const p of plugins) {
127
+ console.log(pc.dim(` - ${p.name} (${p.exportName})`));
128
+ }
129
+ console.log();
130
+ }
131
+
132
+ // 2. Find and process routes
133
+ const routes = await findRoutes(config.routes || "./src/routes/**/handler.ts");
134
+ if (routes.length > 0) {
135
+ console.log(pc.dim(`Found ${routes.length} route(s):`));
136
+ for (const r of routes) {
137
+ console.log(pc.dim(` - ${r.namespace}.${r.name}`));
138
+ }
139
+ console.log();
140
+ }
141
+
142
+ // 3. Generate registry and context
143
+ await generateRegistry(plugins, outPath);
144
+ await generateContext(plugins, outPath);
145
+ await generateRouteTypes(routes, outPath);
146
+
147
+ // 4. Generate client if configured
148
+ if (config.client?.output) {
149
+ console.log(pc.dim("\nGenerating client..."));
150
+ await generateClient(routes, plugins, config.client.output);
151
+ }
152
+
153
+ console.log(pc.green("\nTypes generated successfully!"));
154
+ }
155
+
156
+ async function generateRegistry(
157
+ plugins: { name: string; path: string; exportName: string }[],
158
+ outPath: string
159
+ ) {
160
+ const importLines = plugins
161
+ .map(
162
+ (p) =>
163
+ `import { ${p.exportName} } from "${join(process.cwd(), p.path).replace(/\.ts$/, "")}";`
164
+ )
165
+ .join("\n");
166
+
167
+ const pluginRegistryEntries = plugins
168
+ .map(
169
+ (p) =>
170
+ ` ${p.name}: Register<InferService<typeof ${p.exportName}>, InferSchema<typeof ${p.exportName}>, InferHandlers<typeof ${p.exportName}>, InferDependencies<typeof ${p.exportName}>, InferMiddleware<typeof ${p.exportName}>>;`
171
+ )
172
+ .join("\n");
173
+
174
+ const handlerExtensions =
175
+ plugins.map((p) => `InferHandlers<typeof ${p.exportName}>`).join(",\n ") ||
176
+ "{}";
177
+
178
+ const middlewareExtensions =
179
+ plugins
180
+ .map((p) => `InferMiddleware<typeof ${p.exportName}>`)
181
+ .join(",\n ") || "{}";
182
+
183
+ // Collect handlers and middleware from each plugin
184
+ const allHandlers: { plugin: string; handler: string }[] = [];
185
+ const allMiddleware: { plugin: string; middleware: string }[] = [];
186
+
187
+ for (const p of plugins) {
188
+ const handlers = await extractHandlerNames(join(process.cwd(), p.path));
189
+ const middleware = await extractMiddlewareNames(join(process.cwd(), p.path));
190
+
191
+ for (const h of handlers) {
192
+ allHandlers.push({ plugin: p.exportName, handler: h });
193
+ }
194
+ for (const m of middleware) {
195
+ allMiddleware.push({ plugin: p.exportName, middleware: m });
196
+ }
197
+ }
198
+
199
+ const routeBuilderMethods = allHandlers
200
+ .map(
201
+ ({ plugin, handler }) => ` /** Custom handler from ${plugin} */
202
+ ${handler}(config: {
203
+ handle: InferHandlers<typeof ${plugin}>["${handler}"]["__signature"];
204
+ }): TRouter;`
205
+ )
206
+ .join("\n");
207
+
208
+ const middlewareBuilderMethods = allMiddleware
209
+ .map(
210
+ ({ plugin, middleware }) => ` /** Middleware from ${plugin} */
211
+ ${middleware}(config?: InferMiddleware<typeof ${plugin}>["${middleware}"]["__config"]): this;`
212
+ )
213
+ .join("\n");
214
+
215
+ const handlerUnion =
216
+ allHandlers.length > 0
217
+ ? `"typed" | "raw" | ${allHandlers.map((h) => `"${h.handler}"`).join(" | ")}`
218
+ : '"typed" | "raw"';
219
+
220
+ const middlewareUnion =
221
+ allMiddleware.length > 0
222
+ ? allMiddleware.map((m) => `"${m.middleware}"`).join(" | ")
223
+ : "never";
224
+
225
+ const content = `// Auto-generated by donkeylabs generate
226
+ import { type Register, type InferService, type InferSchema, type InferHandlers, type InferMiddleware, type InferDependencies } from "@donkeylabs/server";
227
+ ${importLines}
228
+
229
+ declare module "@donkeylabs/server" {
230
+ export interface PluginRegistry {
231
+ ${pluginRegistryEntries}
232
+ }
233
+
234
+ export interface PluginHandlerRegistry extends
235
+ ${handlerExtensions}
236
+ {}
237
+
238
+ export interface PluginMiddlewareRegistry extends
239
+ ${middlewareExtensions}
240
+ {}
241
+ }
242
+
243
+ export type AvailableHandlers = ${handlerUnion};
244
+ export type AvailableMiddleware = ${middlewareUnion};
245
+
246
+ declare module "@donkeylabs/server" {
247
+ export interface IRouteBuilder<TRouter> {
248
+ ${routeBuilderMethods}
249
+ }
250
+
251
+ export interface IMiddlewareBuilder<TRouter> {
252
+ ${middlewareBuilderMethods}
253
+ }
254
+ }
255
+ `;
256
+
257
+ await writeFile(join(outPath, "registry.d.ts"), content);
258
+ console.log(pc.green(" Generated:"), `${outPath}/registry.d.ts`);
259
+ }
260
+
261
+ async function generateContext(
262
+ plugins: { name: string; path: string; exportName: string }[],
263
+ outPath: string
264
+ ) {
265
+ const schemaIntersection =
266
+ plugins.map((p) => `PluginRegistry["${p.name}"]["schema"]`).join(" & ") ||
267
+ "{}";
268
+
269
+ const content = `// Auto-generated by donkeylabs generate
270
+
271
+ /// <reference path="./registry.d.ts" />
272
+ import type { PluginRegistry, CoreServices, Errors } from "@donkeylabs/server";
273
+ import type { Kysely } from "kysely";
274
+
275
+ type DatabaseSchema = ${schemaIntersection};
276
+
277
+ export interface GlobalContext {
278
+ db: Kysely<DatabaseSchema>;
279
+ plugins: {
280
+ [K in keyof PluginRegistry]: PluginRegistry[K]["service"];
281
+ };
282
+ core: CoreServices;
283
+ errors: Errors;
284
+ ip: string;
285
+ requestId?: string;
286
+ user?: any;
287
+ }
288
+ `;
289
+
290
+ await writeFile(join(outPath, "context.d.ts"), content);
291
+ console.log(pc.green(" Generated:"), `${outPath}/context.d.ts`);
292
+ }
293
+
294
+ // Route file structure: /src/routes/<namespace>/<route-name>/handler.ts
295
+ interface RouteInfo {
296
+ namespace: string;
297
+ name: string;
298
+ path: string;
299
+ handlerType: "typed" | "raw";
300
+ inputSchema?: string;
301
+ outputSchema?: string;
302
+ }
303
+
304
+ async function findRoutes(pattern: string): Promise<RouteInfo[]> {
305
+ const routes: RouteInfo[] = [];
306
+ const routesDir = join(process.cwd(), "src/routes");
307
+
308
+ if (!existsSync(routesDir)) {
309
+ return routes;
310
+ }
311
+
312
+ // Scan routes directory structure: /routes/<namespace>/<route>/handler.ts
313
+ const namespaces = await readdir(routesDir, { withFileTypes: true });
314
+
315
+ for (const ns of namespaces) {
316
+ if (!ns.isDirectory() || ns.name.startsWith(".")) continue;
317
+
318
+ const namespaceDir = join(routesDir, ns.name);
319
+ const routeDirs = await readdir(namespaceDir, { withFileTypes: true });
320
+
321
+ for (const routeDir of routeDirs) {
322
+ if (!routeDir.isDirectory() || routeDir.name.startsWith(".")) continue;
323
+
324
+ const handlerPath = join(namespaceDir, routeDir.name, "handler.ts");
325
+ if (!existsSync(handlerPath)) continue;
326
+
327
+ const content = await readFile(handlerPath, "utf-8");
328
+
329
+ // Extract handler type and schemas
330
+ const handlerType = content.includes(".raw(") ? "raw" : "typed";
331
+
332
+ // Extract input/output schemas for typed handlers
333
+ let inputSchema: string | undefined;
334
+ let outputSchema: string | undefined;
335
+
336
+ if (handlerType === "typed") {
337
+ const inputMatch = content.match(/input:\s*(z\.[^,\n]+)/);
338
+ const outputMatch = content.match(/output:\s*(z\.[^,\n]+)/);
339
+ inputSchema = inputMatch?.[1];
340
+ outputSchema = outputMatch?.[1];
341
+ }
342
+
343
+ routes.push({
344
+ namespace: ns.name,
345
+ name: routeDir.name,
346
+ path: relative(process.cwd(), handlerPath),
347
+ handlerType,
348
+ inputSchema,
349
+ outputSchema,
350
+ });
351
+ }
352
+ }
353
+
354
+ return routes;
355
+ }
356
+
357
+ async function generateRouteTypes(routes: RouteInfo[], outPath: string): Promise<void> {
358
+ if (routes.length === 0) return;
359
+
360
+ // Group routes by namespace
361
+ const byNamespace = new Map<string, RouteInfo[]>();
362
+ for (const route of routes) {
363
+ if (!byNamespace.has(route.namespace)) {
364
+ byNamespace.set(route.namespace, []);
365
+ }
366
+ byNamespace.get(route.namespace)!.push(route);
367
+ }
368
+
369
+ // Generate route imports
370
+ const imports = routes.map((r) =>
371
+ `import type { handler as ${r.namespace}_${r.name} } from "${join(process.cwd(), r.path).replace(/\.ts$/, "")}";`
372
+ ).join("\n");
373
+
374
+ // Generate namespace types
375
+ const namespaceBlocks: string[] = [];
376
+ for (const [namespace, nsRoutes] of byNamespace) {
377
+ const routeTypes = nsRoutes.map((r) => {
378
+ const pascalName = r.name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase());
379
+ return ` ${r.name}: typeof ${namespace}_${r.name};`;
380
+ }).join("\n");
381
+
382
+ namespaceBlocks.push(` ${namespace}: {\n${routeTypes}\n };`);
383
+ }
384
+
385
+ const content = `// Auto-generated by donkeylabs generate
386
+ // Route type definitions
387
+
388
+ ${imports}
389
+
390
+ export interface RouteHandlers {
391
+ ${namespaceBlocks.join("\n")}
392
+ }
393
+
394
+ export type RouteNamespace = keyof RouteHandlers;
395
+ export type RouteName<NS extends RouteNamespace> = keyof RouteHandlers[NS];
396
+ `;
397
+
398
+ await writeFile(join(outPath, "routes.d.ts"), content);
399
+ console.log(pc.green(" Generated:"), `${outPath}/routes.d.ts`);
400
+ }
401
+
402
+ async function generateClient(
403
+ routes: RouteInfo[],
404
+ plugins: { name: string; path: string; exportName: string }[],
405
+ outputPath: string
406
+ ): Promise<void> {
407
+ // Group routes by namespace
408
+ const byNamespace = new Map<string, RouteInfo[]>();
409
+ for (const route of routes) {
410
+ if (!byNamespace.has(route.namespace)) {
411
+ byNamespace.set(route.namespace, []);
412
+ }
413
+ byNamespace.get(route.namespace)!.push(route);
414
+ }
415
+
416
+ // Generate method definitions
417
+ const namespaceBlocks: string[] = [];
418
+
419
+ for (const [namespace, nsRoutes] of byNamespace) {
420
+ const methods = nsRoutes
421
+ .filter((r) => r.handlerType === "typed")
422
+ .map((r) => {
423
+ const methodName = r.name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
424
+ return ` ${methodName}: (input: any) => this.request("${namespace}.${r.name}", input)`;
425
+ });
426
+
427
+ const rawMethods = nsRoutes
428
+ .filter((r) => r.handlerType === "raw")
429
+ .map((r) => {
430
+ const methodName = r.name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
431
+ return ` ${methodName}: (init?: RequestInit) => this.rawRequest("${namespace}.${r.name}", init)`;
432
+ });
433
+
434
+ if (methods.length > 0 || rawMethods.length > 0) {
435
+ namespaceBlocks.push(` ${namespace} = {\n${[...methods, ...rawMethods].join(",\n")}\n };`);
436
+ }
437
+ }
438
+
439
+ const content = `// Auto-generated by donkeylabs generate
440
+ // API Client
441
+
442
+ import { ApiClientBase } from "@donkeylabs/server/client";
443
+
444
+ export class ApiClient extends ApiClientBase<{}> {
445
+ constructor(baseUrl: string, options?: RequestInit) {
446
+ super(baseUrl, options);
447
+ }
448
+
449
+ ${namespaceBlocks.join("\n\n") || " // No routes defined"}
450
+ }
451
+
452
+ export function createApiClient(baseUrl: string, options?: RequestInit) {
453
+ return new ApiClient(baseUrl, options);
454
+ }
455
+ `;
456
+
457
+ const outputDir = dirname(outputPath);
458
+ await mkdir(outputDir, { recursive: true });
459
+ await writeFile(outputPath, content);
460
+ console.log(pc.green(" Generated:"), outputPath);
461
+ }