@donkeylabs/cli 2.0.6 → 2.0.7
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 +1 -1
- package/src/commands/generate.ts +27 -114
- package/templates/starter/package.json +2 -2
- package/templates/sveltekit-app/package.json +4 -5
- package/templates/sveltekit-app/src/server/index.ts +5 -3
- package/templates/sveltekit-app/src/server/plugins/demo/index.ts +5 -1
- package/templates/sveltekit-app/src/server/routes/demo.ts +7 -1
- package/templates/sveltekit-app/scripts/dev.ts +0 -53
- package/templates/sveltekit-app/scripts/watch-server.ts +0 -79
package/package.json
CHANGED
package/src/commands/generate.ts
CHANGED
|
@@ -568,7 +568,7 @@ async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]>
|
|
|
568
568
|
timedOut = true;
|
|
569
569
|
child.kill("SIGTERM");
|
|
570
570
|
console.warn(pc.yellow(`Route extraction timed out after ${TIMEOUT_MS / 1000}s`));
|
|
571
|
-
console.warn(pc.dim("Make sure
|
|
571
|
+
console.warn(pc.dim("Make sure routes are registered with server.use() before any blocking operations"));
|
|
572
572
|
resolve([]);
|
|
573
573
|
}, TIMEOUT_MS);
|
|
574
574
|
|
|
@@ -767,31 +767,38 @@ export async function generateCommand(_args: string[]): Promise<void> {
|
|
|
767
767
|
// Determine client output path
|
|
768
768
|
const clientOutput = config.client?.output || join(outPath, "client.ts");
|
|
769
769
|
|
|
770
|
-
//
|
|
770
|
+
// Generate client using adapter (required for SvelteKit projects)
|
|
771
771
|
if (config.adapter) {
|
|
772
|
+
// Resolve the adapter path from the project's node_modules
|
|
773
|
+
const adapterPath = join(process.cwd(), "node_modules", config.adapter, "src/generator/index.ts");
|
|
774
|
+
|
|
775
|
+
if (!existsSync(adapterPath)) {
|
|
776
|
+
console.error(pc.red(`Adapter not found: ${config.adapter}`));
|
|
777
|
+
console.error(pc.dim(`Expected path: ${adapterPath}`));
|
|
778
|
+
console.error(pc.dim(`Run: bun install`));
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
|
|
772
782
|
try {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (adapterModule.generateClient) {
|
|
778
|
-
await adapterModule.generateClient(config, serverRoutes, clientOutput);
|
|
779
|
-
generated.push(`client (${config.adapter})`);
|
|
780
|
-
console.log(pc.green("Generated:"), generated.map(g => pc.dim(g)).join(", "));
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
+
const adapterModule = await import(adapterPath);
|
|
784
|
+
if (!adapterModule.generateClient) {
|
|
785
|
+
console.error(pc.red(`Adapter ${config.adapter} does not export generateClient`));
|
|
786
|
+
process.exit(1);
|
|
783
787
|
}
|
|
788
|
+
await adapterModule.generateClient(config, serverRoutes, clientOutput);
|
|
789
|
+
generated.push(`client (${config.adapter})`);
|
|
784
790
|
} catch (e: any) {
|
|
785
|
-
|
|
786
|
-
console.
|
|
787
|
-
console.
|
|
791
|
+
console.error(pc.red(`Failed to run adapter generator: ${config.adapter}`));
|
|
792
|
+
console.error(pc.dim(e.message));
|
|
793
|
+
if (e.stack) console.error(pc.dim(e.stack));
|
|
794
|
+
process.exit(1);
|
|
788
795
|
}
|
|
796
|
+
} else {
|
|
797
|
+
// No adapter - skip client generation (user must specify adapter for typed clients)
|
|
798
|
+
console.log(pc.yellow("No adapter specified - skipping client generation"));
|
|
799
|
+
console.log(pc.dim('Add adapter: "@donkeylabs/adapter-sveltekit" to donkeylabs.config.ts for typed client'));
|
|
789
800
|
}
|
|
790
801
|
|
|
791
|
-
// Default client generation
|
|
792
|
-
await generateClientFromRoutes(serverRoutes, clientOutput);
|
|
793
|
-
generated.push("client");
|
|
794
|
-
|
|
795
802
|
console.log(pc.green("Generated:"), generated.map(g => pc.dim(g)).join(", "));
|
|
796
803
|
}
|
|
797
804
|
|
|
@@ -938,7 +945,7 @@ async function generateContext(
|
|
|
938
945
|
const hasServices = serviceEntries.length > 0;
|
|
939
946
|
const servicesTypeDecl = hasServices
|
|
940
947
|
? `export interface AppServices {\n${serviceEntries.join("\n")}\n }`
|
|
941
|
-
: "export
|
|
948
|
+
: "// eslint-disable-next-line @typescript-eslint/no-empty-interface\n export interface AppServices {}";
|
|
942
949
|
|
|
943
950
|
const content = `// Auto-generated by donkeylabs generate
|
|
944
951
|
// App context - import as: import type { AppContext } from ".@donkeylabs/server/context";
|
|
@@ -1266,97 +1273,3 @@ ${eventRegistryEntries.join("\n")}
|
|
|
1266
1273
|
await writeFile(join(outPath, "events.ts"), content);
|
|
1267
1274
|
}
|
|
1268
1275
|
|
|
1269
|
-
async function generateClientFromRoutes(
|
|
1270
|
-
routes: ExtractedRoute[],
|
|
1271
|
-
outputPath: string
|
|
1272
|
-
): Promise<void> {
|
|
1273
|
-
// Group routes by namespace (first part of route name)
|
|
1274
|
-
// e.g., "api.hello.test" -> namespace "api", sub "hello", method "test"
|
|
1275
|
-
const tree = new Map<string, Map<string, { method: string; fullName: string }[]>>();
|
|
1276
|
-
|
|
1277
|
-
for (const route of routes) {
|
|
1278
|
-
const parts = route.name.split(".");
|
|
1279
|
-
if (parts.length < 2) {
|
|
1280
|
-
// Single-level route like "ping" -> namespace "", method "ping"
|
|
1281
|
-
const ns = "";
|
|
1282
|
-
if (!tree.has(ns)) tree.set(ns, new Map());
|
|
1283
|
-
const rootMethods = tree.get(ns)!;
|
|
1284
|
-
if (!rootMethods.has("")) rootMethods.set("", []);
|
|
1285
|
-
rootMethods.get("")!.push({ method: parts[0]!, fullName: route.name });
|
|
1286
|
-
} else if (parts.length === 2) {
|
|
1287
|
-
// Two-level route like "health.ping" -> namespace "health", method "ping"
|
|
1288
|
-
const [ns, method] = parts;
|
|
1289
|
-
if (!tree.has(ns!)) tree.set(ns!, new Map());
|
|
1290
|
-
const nsMethods = tree.get(ns!)!;
|
|
1291
|
-
if (!nsMethods.has("")) nsMethods.set("", []);
|
|
1292
|
-
nsMethods.get("")!.push({ method: method!, fullName: route.name });
|
|
1293
|
-
} else {
|
|
1294
|
-
// Multi-level route like "api.hello.test" -> namespace "api", sub "hello", method "test"
|
|
1295
|
-
const [ns, sub, ...rest] = parts;
|
|
1296
|
-
const method = rest.join(".");
|
|
1297
|
-
if (!tree.has(ns!)) tree.set(ns!, new Map());
|
|
1298
|
-
const nsMethods = tree.get(ns!)!;
|
|
1299
|
-
if (!nsMethods.has(sub!)) nsMethods.set(sub!, []);
|
|
1300
|
-
nsMethods.get(sub!)!.push({ method: method || sub!, fullName: route.name });
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// Generate method definitions
|
|
1305
|
-
const namespaceBlocks: string[] = [];
|
|
1306
|
-
|
|
1307
|
-
for (const [namespace, subNamespaces] of tree) {
|
|
1308
|
-
if (namespace === "") {
|
|
1309
|
-
// Root-level methods
|
|
1310
|
-
const rootMethods = subNamespaces.get("");
|
|
1311
|
-
if (rootMethods && rootMethods.length > 0) {
|
|
1312
|
-
for (const { method, fullName } of rootMethods) {
|
|
1313
|
-
const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
1314
|
-
namespaceBlocks.push(` ${methodName} = (input: any) => this.request("${fullName}", input);`);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
continue;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
const subBlocks: string[] = [];
|
|
1321
|
-
for (const [sub, methods] of subNamespaces) {
|
|
1322
|
-
if (sub === "") {
|
|
1323
|
-
// Direct methods on namespace
|
|
1324
|
-
for (const { method, fullName } of methods) {
|
|
1325
|
-
const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
1326
|
-
subBlocks.push(` ${methodName}: (input: any) => this.request("${fullName}", input)`);
|
|
1327
|
-
}
|
|
1328
|
-
} else {
|
|
1329
|
-
// Sub-namespace methods
|
|
1330
|
-
const subMethods = methods.map(({ method, fullName }) => {
|
|
1331
|
-
const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
1332
|
-
return ` ${methodName}: (input: any) => this.request("${fullName}", input)`;
|
|
1333
|
-
});
|
|
1334
|
-
subBlocks.push(` ${sub}: {\n${subMethods.join(",\n")}\n }`);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
namespaceBlocks.push(` ${namespace} = {\n${subBlocks.join(",\n")}\n };`);
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
const content = `// Auto-generated by donkeylabs generate
|
|
1342
|
-
// API Client
|
|
1343
|
-
|
|
1344
|
-
import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";
|
|
1345
|
-
|
|
1346
|
-
export class ApiClient extends ApiClientBase<{}> {
|
|
1347
|
-
constructor(baseUrl: string, options?: ApiClientOptions) {
|
|
1348
|
-
super(baseUrl, options);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
${namespaceBlocks.join("\n\n") || " // No routes defined"}
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
|
|
1355
|
-
return new ApiClient(baseUrl, options);
|
|
1356
|
-
}
|
|
1357
|
-
`;
|
|
1358
|
-
|
|
1359
|
-
const outputDir = dirname(outputPath);
|
|
1360
|
-
await mkdir(outputDir, { recursive: true });
|
|
1361
|
-
await writeFile(outputPath, content);
|
|
1362
|
-
}
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"gen:types": "donkeylabs generate"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@donkeylabs/server": "
|
|
12
|
+
"@donkeylabs/server": "^2.0.10",
|
|
13
13
|
"kysely": "^0.27.0",
|
|
14
14
|
"kysely-bun-sqlite": "^0.3.0",
|
|
15
15
|
"zod": "^3.24.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@donkeylabs/cli": "
|
|
18
|
+
"@donkeylabs/cli": "^2.0.7",
|
|
19
19
|
"@types/bun": "latest"
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
"version": "0.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "bun
|
|
8
|
-
"dev:watch": "bun --watch --no-clear-screen scripts/watch-server.ts",
|
|
7
|
+
"dev": "bun --bun vite dev",
|
|
9
8
|
"build": "bun run gen:types && vite build",
|
|
10
9
|
"preview": "bun build/server/entry.js",
|
|
11
10
|
"prepare": "bun --bun svelte-kit sync || echo ''",
|
|
@@ -24,9 +23,9 @@
|
|
|
24
23
|
"vite": "^7.2.6"
|
|
25
24
|
},
|
|
26
25
|
"dependencies": {
|
|
27
|
-
"@donkeylabs/cli": "^
|
|
28
|
-
"@donkeylabs/adapter-sveltekit": "^
|
|
29
|
-
"@donkeylabs/server": "^
|
|
26
|
+
"@donkeylabs/cli": "^2.0.7",
|
|
27
|
+
"@donkeylabs/adapter-sveltekit": "^2.0.9",
|
|
28
|
+
"@donkeylabs/server": "^2.0.10",
|
|
30
29
|
"bits-ui": "^2.15.4",
|
|
31
30
|
"clsx": "^2.1.1",
|
|
32
31
|
"kysely": "^0.27.6",
|
|
@@ -26,6 +26,11 @@ export const server = new AppServer({
|
|
|
26
26
|
generateTypes: {
|
|
27
27
|
output: "./src/lib/api.ts",
|
|
28
28
|
},
|
|
29
|
+
// Admin dashboard - enabled in dev mode
|
|
30
|
+
admin: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
prefix: "admin",
|
|
33
|
+
},
|
|
29
34
|
});
|
|
30
35
|
|
|
31
36
|
// =============================================================================
|
|
@@ -119,6 +124,3 @@ server.use(permissionsRouter);
|
|
|
119
124
|
server.use(tenantsRouter);
|
|
120
125
|
server.use(demoRoutes);
|
|
121
126
|
server.use(exampleRouter);
|
|
122
|
-
|
|
123
|
-
// Handle CLI type generation (must be after routes are registered)
|
|
124
|
-
server.handleGenerateMode();
|
|
@@ -157,7 +157,11 @@ export const demoPlugin = createPlugin.define({
|
|
|
157
157
|
return { success: true };
|
|
158
158
|
},
|
|
159
159
|
wsGetClients: (channel?: string) => {
|
|
160
|
-
const
|
|
160
|
+
const allClients = ctx.core.websocket.getClients();
|
|
161
|
+
// Filter by channel if provided
|
|
162
|
+
const clients = channel
|
|
163
|
+
? allClients.filter((c) => c.channels.includes(channel))
|
|
164
|
+
: allClients;
|
|
161
165
|
return {
|
|
162
166
|
count: clients.length,
|
|
163
167
|
clients,
|
|
@@ -435,7 +435,13 @@ demo.route("websocket.clients").typed(
|
|
|
435
435
|
}),
|
|
436
436
|
output: z.object({
|
|
437
437
|
count: z.number(),
|
|
438
|
-
clients: z.array(
|
|
438
|
+
clients: z.array(
|
|
439
|
+
z.object({
|
|
440
|
+
id: z.string(),
|
|
441
|
+
connectedAt: z.date(),
|
|
442
|
+
channels: z.array(z.string()),
|
|
443
|
+
})
|
|
444
|
+
),
|
|
439
445
|
}),
|
|
440
446
|
handle: async (input, ctx) => {
|
|
441
447
|
return ctx.plugins.demo.wsGetClients(input.channel);
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// Dev script that runs vite and watcher together, ensuring cleanup on exit
|
|
3
|
-
|
|
4
|
-
import { spawn, type Subprocess } from "bun";
|
|
5
|
-
|
|
6
|
-
const children: Subprocess[] = [];
|
|
7
|
-
|
|
8
|
-
function cleanup() {
|
|
9
|
-
for (const child of children) {
|
|
10
|
-
try {
|
|
11
|
-
child.kill();
|
|
12
|
-
} catch {}
|
|
13
|
-
}
|
|
14
|
-
process.exit(0);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Handle all exit signals
|
|
18
|
-
process.on("SIGTERM", cleanup);
|
|
19
|
-
process.on("SIGINT", cleanup);
|
|
20
|
-
process.on("exit", cleanup);
|
|
21
|
-
|
|
22
|
-
// Generate types first
|
|
23
|
-
console.log("\x1b[36m[dev]\x1b[0m Generating types...");
|
|
24
|
-
const genResult = Bun.spawnSync(["bun", "run", "gen:types"], {
|
|
25
|
-
stdout: "inherit",
|
|
26
|
-
stderr: "inherit",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (genResult.exitCode !== 0) {
|
|
30
|
-
console.error("\x1b[31m[dev]\x1b[0m Failed to generate types");
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Start watcher
|
|
35
|
-
console.log("\x1b[36m[dev]\x1b[0m Starting file watcher...");
|
|
36
|
-
const watcher = spawn(["bun", "--watch", "--no-clear-screen", "scripts/watch-server.ts"], {
|
|
37
|
-
stdout: "inherit",
|
|
38
|
-
stderr: "inherit",
|
|
39
|
-
});
|
|
40
|
-
children.push(watcher);
|
|
41
|
-
|
|
42
|
-
// Start vite
|
|
43
|
-
console.log("\x1b[36m[dev]\x1b[0m Starting Vite dev server...");
|
|
44
|
-
const vite = spawn(["bun", "--bun", "vite", "dev"], {
|
|
45
|
-
stdout: "inherit",
|
|
46
|
-
stderr: "inherit",
|
|
47
|
-
stdin: "inherit",
|
|
48
|
-
});
|
|
49
|
-
children.push(vite);
|
|
50
|
-
|
|
51
|
-
// When vite exits, cleanup everything
|
|
52
|
-
await vite.exited;
|
|
53
|
-
cleanup();
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// Watch server files and regenerate types on changes
|
|
2
|
-
import { watch } from "node:fs";
|
|
3
|
-
import { exec } from "node:child_process";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
|
|
7
|
-
const execAsync = promisify(exec);
|
|
8
|
-
|
|
9
|
-
const serverDir = join(import.meta.dir, "..", "src", "server");
|
|
10
|
-
let isGenerating = false;
|
|
11
|
-
let lastGenerationTime = 0;
|
|
12
|
-
|
|
13
|
-
// Files/patterns we generate - ignore changes to these
|
|
14
|
-
const IGNORED_PATTERNS = [
|
|
15
|
-
/schema\.ts$/, // Generated schema files
|
|
16
|
-
/\.d\.ts$/, // Type declaration files
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
// Cooldown period after generation (ms)
|
|
20
|
-
const COOLDOWN_MS = 2000;
|
|
21
|
-
const DEBOUNCE_MS = 500;
|
|
22
|
-
|
|
23
|
-
// Handle signals for clean shutdown (from parent dev.ts)
|
|
24
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
25
|
-
process.on("SIGINT", () => process.exit(0));
|
|
26
|
-
|
|
27
|
-
function shouldIgnoreFile(filename: string): boolean {
|
|
28
|
-
return IGNORED_PATTERNS.some(pattern => pattern.test(filename));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function regenerate() {
|
|
32
|
-
// Check cooldown
|
|
33
|
-
const now = Date.now();
|
|
34
|
-
if (now - lastGenerationTime < COOLDOWN_MS) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (isGenerating) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
isGenerating = true;
|
|
43
|
-
lastGenerationTime = now;
|
|
44
|
-
console.log("\x1b[36m[watch]\x1b[0m Server files changed, regenerating types...");
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await execAsync("bun run gen:types");
|
|
48
|
-
console.log("\x1b[32m[watch]\x1b[0m Types regenerated successfully");
|
|
49
|
-
} catch (e: any) {
|
|
50
|
-
console.error("\x1b[31m[watch]\x1b[0m Error regenerating types:", e.message);
|
|
51
|
-
} finally {
|
|
52
|
-
isGenerating = false;
|
|
53
|
-
lastGenerationTime = Date.now(); // Update after generation completes
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Debounce to avoid multiple rapid regenerations
|
|
58
|
-
let debounceTimer: Timer | null = null;
|
|
59
|
-
|
|
60
|
-
function debouncedRegenerate() {
|
|
61
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
62
|
-
debounceTimer = setTimeout(regenerate, DEBOUNCE_MS);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Watch server directory recursively
|
|
66
|
-
watch(serverDir, { recursive: true }, (eventType, filename) => {
|
|
67
|
-
if (!filename) return;
|
|
68
|
-
if (!filename.endsWith(".ts")) return;
|
|
69
|
-
|
|
70
|
-
// Ignore generated files
|
|
71
|
-
if (shouldIgnoreFile(filename)) return;
|
|
72
|
-
|
|
73
|
-
debouncedRegenerate();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
console.log("\x1b[36m[watch]\x1b[0m Watching src/server/ for changes...");
|
|
77
|
-
|
|
78
|
-
// Keep process alive
|
|
79
|
-
await new Promise(() => {});
|