@donkeylabs/server 0.1.0 → 0.1.2
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/cli/commands/init.ts +201 -12
- package/cli/donkeylabs +6 -0
- package/context.d.ts +17 -0
- package/docs/api-client.md +520 -0
- package/docs/cache.md +437 -0
- package/docs/cli.md +353 -0
- package/docs/core-services.md +338 -0
- package/docs/cron.md +465 -0
- package/docs/errors.md +303 -0
- package/docs/events.md +460 -0
- package/docs/handlers.md +549 -0
- package/docs/jobs.md +556 -0
- package/docs/logger.md +316 -0
- package/docs/middleware.md +682 -0
- package/docs/plugins.md +524 -0
- package/docs/project-structure.md +493 -0
- package/docs/rate-limiter.md +525 -0
- package/docs/router.md +566 -0
- package/docs/sse.md +542 -0
- package/docs/svelte-frontend.md +324 -0
- package/package.json +12 -9
- package/registry.d.ts +11 -0
- package/src/index.ts +1 -1
- package/src/server.ts +1 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Svelte 5 Frontend
|
|
2
|
+
|
|
3
|
+
Guide for building Svelte 5 frontends with the generated API client.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### Create SvelteKit App
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bunx sv create frontend
|
|
11
|
+
cd frontend
|
|
12
|
+
bun install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Generate API Client
|
|
16
|
+
|
|
17
|
+
From your server project:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun run gen:client --output ../frontend/src/lib/api
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This creates `frontend/src/lib/api/index.ts` with typed methods.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
### Initialize Client
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// src/lib/api.ts
|
|
33
|
+
import { createApiClient } from "./api";
|
|
34
|
+
|
|
35
|
+
export const api = createApiClient({
|
|
36
|
+
baseUrl: "http://localhost:3000",
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Use in Components
|
|
41
|
+
|
|
42
|
+
```svelte
|
|
43
|
+
<script lang="ts">
|
|
44
|
+
import { api } from "$lib/api";
|
|
45
|
+
|
|
46
|
+
let items = $state<Item[]>([]);
|
|
47
|
+
let loading = $state(true);
|
|
48
|
+
let error = $state<string | null>(null);
|
|
49
|
+
|
|
50
|
+
async function load() {
|
|
51
|
+
try {
|
|
52
|
+
const result = await api.items.list({});
|
|
53
|
+
items = result.items;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
error = e instanceof Error ? e.message : "Failed to load";
|
|
56
|
+
} finally {
|
|
57
|
+
loading = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$effect(() => {
|
|
62
|
+
load();
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
{#if loading}
|
|
67
|
+
<p>Loading...</p>
|
|
68
|
+
{:else if error}
|
|
69
|
+
<p class="error">{error}</p>
|
|
70
|
+
{:else}
|
|
71
|
+
<ul>
|
|
72
|
+
{#each items as item (item.id)}
|
|
73
|
+
<li>{item.name}</li>
|
|
74
|
+
{/each}
|
|
75
|
+
</ul>
|
|
76
|
+
{/if}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Svelte 5 Patterns
|
|
82
|
+
|
|
83
|
+
### State with $state
|
|
84
|
+
|
|
85
|
+
```svelte
|
|
86
|
+
<script lang="ts">
|
|
87
|
+
// Reactive state
|
|
88
|
+
let count = $state(0);
|
|
89
|
+
let user = $state<User | null>(null);
|
|
90
|
+
let items = $state<Item[]>([]);
|
|
91
|
+
|
|
92
|
+
// Derived values
|
|
93
|
+
let doubled = $derived(count * 2);
|
|
94
|
+
let hasItems = $derived(items.length > 0);
|
|
95
|
+
</script>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Effects with $effect
|
|
99
|
+
|
|
100
|
+
```svelte
|
|
101
|
+
<script lang="ts">
|
|
102
|
+
let id = $state(1);
|
|
103
|
+
|
|
104
|
+
// Runs when id changes
|
|
105
|
+
$effect(() => {
|
|
106
|
+
loadUser(id);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Cleanup pattern
|
|
110
|
+
$effect(() => {
|
|
111
|
+
const interval = setInterval(refresh, 5000);
|
|
112
|
+
return () => clearInterval(interval);
|
|
113
|
+
});
|
|
114
|
+
</script>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Form Handling
|
|
118
|
+
|
|
119
|
+
```svelte
|
|
120
|
+
<script lang="ts">
|
|
121
|
+
let title = $state("");
|
|
122
|
+
let submitting = $state(false);
|
|
123
|
+
|
|
124
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
if (!title.trim()) return;
|
|
127
|
+
|
|
128
|
+
submitting = true;
|
|
129
|
+
try {
|
|
130
|
+
await api.items.create({ title });
|
|
131
|
+
title = "";
|
|
132
|
+
await load(); // Refresh list
|
|
133
|
+
} catch (e) {
|
|
134
|
+
error = e instanceof Error ? e.message : "Failed to create";
|
|
135
|
+
} finally {
|
|
136
|
+
submitting = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<form onsubmit={handleSubmit}>
|
|
142
|
+
<input bind:value={title} placeholder="Title" />
|
|
143
|
+
<button disabled={submitting}>
|
|
144
|
+
{submitting ? "Creating..." : "Create"}
|
|
145
|
+
</button>
|
|
146
|
+
</form>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Real-time Updates (SSE)
|
|
152
|
+
|
|
153
|
+
### Connect on Mount
|
|
154
|
+
|
|
155
|
+
```svelte
|
|
156
|
+
<script lang="ts">
|
|
157
|
+
import { api } from "$lib/api";
|
|
158
|
+
import { onMount } from "svelte";
|
|
159
|
+
|
|
160
|
+
let items = $state<Item[]>([]);
|
|
161
|
+
|
|
162
|
+
onMount(() => {
|
|
163
|
+
// Connect to SSE
|
|
164
|
+
api.connect();
|
|
165
|
+
|
|
166
|
+
// Listen for updates
|
|
167
|
+
const unsubscribe = api.on("items.changed", (data) => {
|
|
168
|
+
if (data.action === "created") {
|
|
169
|
+
items = [data.item, ...items];
|
|
170
|
+
} else if (data.action === "deleted") {
|
|
171
|
+
items = items.filter((i) => i.id !== data.item.id);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Cleanup
|
|
176
|
+
return () => {
|
|
177
|
+
unsubscribe();
|
|
178
|
+
api.disconnect();
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
</script>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Connection Status
|
|
185
|
+
|
|
186
|
+
```svelte
|
|
187
|
+
<script lang="ts">
|
|
188
|
+
let connected = $state(false);
|
|
189
|
+
|
|
190
|
+
onMount(() => {
|
|
191
|
+
api.connect({
|
|
192
|
+
onConnect: () => (connected = true),
|
|
193
|
+
onDisconnect: () => (connected = false),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return () => api.disconnect();
|
|
197
|
+
});
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<span class={connected ? "online" : "offline"}>
|
|
201
|
+
{connected ? "Connected" : "Disconnected"}
|
|
202
|
+
</span>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Error Handling
|
|
208
|
+
|
|
209
|
+
```svelte
|
|
210
|
+
<script lang="ts">
|
|
211
|
+
import { ApiError, ValidationError } from "$lib/api";
|
|
212
|
+
|
|
213
|
+
async function save() {
|
|
214
|
+
try {
|
|
215
|
+
await api.items.create({ title });
|
|
216
|
+
} catch (e) {
|
|
217
|
+
if (e instanceof ValidationError) {
|
|
218
|
+
// Show field errors
|
|
219
|
+
errors = e.details; // [{ path: ["title"], message: "Required" }]
|
|
220
|
+
} else if (e instanceof ApiError) {
|
|
221
|
+
if (e.status === 404) {
|
|
222
|
+
error = "Not found";
|
|
223
|
+
} else if (e.status === 401) {
|
|
224
|
+
goto("/login");
|
|
225
|
+
} else {
|
|
226
|
+
error = e.body?.message || "Server error";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
</script>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Complete Example
|
|
237
|
+
|
|
238
|
+
```svelte
|
|
239
|
+
<!-- src/routes/+page.svelte -->
|
|
240
|
+
<script lang="ts">
|
|
241
|
+
import { api, type Item } from "$lib/api";
|
|
242
|
+
import { onMount } from "svelte";
|
|
243
|
+
|
|
244
|
+
let items = $state<Item[]>([]);
|
|
245
|
+
let loading = $state(true);
|
|
246
|
+
let error = $state<string | null>(null);
|
|
247
|
+
let newTitle = $state("");
|
|
248
|
+
|
|
249
|
+
async function load() {
|
|
250
|
+
try {
|
|
251
|
+
const result = await api.items.list({});
|
|
252
|
+
items = result.items;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
error = e instanceof Error ? e.message : "Failed to load";
|
|
255
|
+
} finally {
|
|
256
|
+
loading = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function create() {
|
|
261
|
+
if (!newTitle.trim()) return;
|
|
262
|
+
try {
|
|
263
|
+
await api.items.create({ title: newTitle });
|
|
264
|
+
newTitle = "";
|
|
265
|
+
await load();
|
|
266
|
+
} catch (e) {
|
|
267
|
+
error = e instanceof Error ? e.message : "Failed to create";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function remove(id: number) {
|
|
272
|
+
try {
|
|
273
|
+
await api.items.delete({ id });
|
|
274
|
+
items = items.filter((i) => i.id !== id);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
error = e instanceof Error ? e.message : "Failed to delete";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
onMount(() => {
|
|
281
|
+
load();
|
|
282
|
+
});
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
<main>
|
|
286
|
+
<h1>Items</h1>
|
|
287
|
+
|
|
288
|
+
{#if error}
|
|
289
|
+
<p class="error">{error}</p>
|
|
290
|
+
{/if}
|
|
291
|
+
|
|
292
|
+
<form onsubmit={(e) => { e.preventDefault(); create(); }}>
|
|
293
|
+
<input bind:value={newTitle} placeholder="New item" />
|
|
294
|
+
<button>Add</button>
|
|
295
|
+
</form>
|
|
296
|
+
|
|
297
|
+
{#if loading}
|
|
298
|
+
<p>Loading...</p>
|
|
299
|
+
{:else}
|
|
300
|
+
<ul>
|
|
301
|
+
{#each items as item (item.id)}
|
|
302
|
+
<li>
|
|
303
|
+
{item.title}
|
|
304
|
+
<button onclick={() => remove(item.id)}>Delete</button>
|
|
305
|
+
</li>
|
|
306
|
+
{/each}
|
|
307
|
+
</ul>
|
|
308
|
+
{/if}
|
|
309
|
+
</main>
|
|
310
|
+
|
|
311
|
+
<style>
|
|
312
|
+
.error { color: red; }
|
|
313
|
+
</style>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Guidelines
|
|
319
|
+
|
|
320
|
+
- **Keep components focused** - one concern per component
|
|
321
|
+
- **Handle loading states** - always show loading/error feedback
|
|
322
|
+
- **Clean up on unmount** - disconnect SSE, clear intervals
|
|
323
|
+
- **Use $derived for computed values** - not $state
|
|
324
|
+
- **Type your state** - `$state<Type[]>([])` not `$state([])`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donkeylabs/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Type-safe plugin system for building RPC-style APIs with Bun",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
"src",
|
|
28
28
|
"cli",
|
|
29
29
|
"mcp",
|
|
30
|
+
"docs",
|
|
30
31
|
"templates",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
32
|
+
"context.d.ts",
|
|
33
|
+
"registry.d.ts"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
36
|
"gen:registry": "bun scripts/generate-registry.ts",
|
|
@@ -45,18 +46,20 @@
|
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/bun": "latest",
|
|
47
48
|
"@types/prompts": "^2.4.9",
|
|
48
|
-
"kysely
|
|
49
|
+
"kysely": "^0.27.6",
|
|
50
|
+
"kysely-bun-sqlite": "^0.3.2",
|
|
51
|
+
"kysely-codegen": "^0.19.0",
|
|
52
|
+
"zod": "^3.24.0"
|
|
49
53
|
},
|
|
50
54
|
"peerDependencies": {
|
|
51
|
-
"typescript": "^5"
|
|
55
|
+
"typescript": "^5",
|
|
56
|
+
"kysely": "^0.27.0 || ^0.28.0",
|
|
57
|
+
"zod": "^3.20.0"
|
|
52
58
|
},
|
|
53
59
|
"dependencies": {
|
|
54
60
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
55
|
-
"kysely": "^0.28.9",
|
|
56
|
-
"kysely-bun-sqlite": "^0.4.0",
|
|
57
61
|
"picocolors": "^1.1.1",
|
|
58
|
-
"prompts": "^2.4.2"
|
|
59
|
-
"zod": "^4.3.5"
|
|
62
|
+
"prompts": "^2.4.2"
|
|
60
63
|
},
|
|
61
64
|
"keywords": [
|
|
62
65
|
"api",
|
package/registry.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin and handler registry.
|
|
3
|
+
* This is a stub - run `donkeylabs generate` to create project-specific types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Extend PluginHandlerRegistry in your project's generated types
|
|
7
|
+
declare module "./src/core" {
|
|
8
|
+
interface PluginHandlerRegistry {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {};
|
package/src/index.ts
CHANGED