@donkeylabs/server 0.1.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/README.md +15 -0
- package/cli/commands/generate.ts +461 -0
- package/cli/commands/init.ts +287 -0
- package/cli/commands/interactive.ts +223 -0
- package/cli/commands/plugin.ts +192 -0
- package/cli/donkeylabs +100 -0
- package/cli/index.ts +100 -0
- package/mcp/donkeylabs-mcp +3238 -0
- package/mcp/server.ts +3238 -0
- package/package.json +74 -0
- package/src/client/base.ts +481 -0
- package/src/client/index.ts +150 -0
- package/src/core/cache.ts +183 -0
- package/src/core/cron.ts +255 -0
- package/src/core/errors.ts +320 -0
- package/src/core/events.ts +163 -0
- package/src/core/index.ts +94 -0
- package/src/core/jobs.ts +334 -0
- package/src/core/logger.ts +131 -0
- package/src/core/rate-limiter.ts +193 -0
- package/src/core/sse.ts +210 -0
- package/src/core.ts +428 -0
- package/src/handlers.ts +87 -0
- package/src/harness.ts +70 -0
- package/src/index.ts +38 -0
- package/src/middleware.ts +34 -0
- package/src/registry.ts +13 -0
- package/src/router.ts +155 -0
- package/src/server.ts +233 -0
- package/templates/init/donkeylabs.config.ts.template +14 -0
- package/templates/init/index.ts.template +41 -0
- package/templates/plugin/index.ts.template +25 -0
package/README.md
ADDED
|
@@ -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
|
+
}
|