@kuratchi/js 0.0.13 → 0.0.15
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 +835 -691
- package/dist/compiler/index.js +1297 -987
- package/dist/compiler/template.js +82 -30
- package/dist/runtime/types.d.ts +0 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,691 +1,835 @@
|
|
|
1
|
-
# @kuratchi/js
|
|
2
|
-
|
|
3
|
-
Cloudflare Workers-native web framework with file-based routing, server actions, and Durable Object support.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @kuratchi/js
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick start
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx kuratchi create my-app
|
|
15
|
-
cd my-app
|
|
16
|
-
bun run dev
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## How it works
|
|
20
|
-
|
|
21
|
-
`kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
|
|
22
|
-
|
|
23
|
-
| File | Purpose |
|
|
24
|
-
|---|---|
|
|
25
|
-
| `.kuratchi/routes.js` | Compiled routes, actions, RPC handlers, and render functions |
|
|
26
|
-
| `.kuratchi/worker.js` | Stable wrangler entry - re-exports the fetch handler plus all Durable Object and Agent classes |
|
|
27
|
-
| `.kuratchi/do/*.js` | Generated Durable Object RPC proxy modules for `$durable-objects/*` imports |
|
|
28
|
-
|
|
29
|
-
Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
|
|
30
|
-
|
|
31
|
-
```jsonc
|
|
32
|
-
// wrangler.jsonc
|
|
33
|
-
{
|
|
34
|
-
"main": ".kuratchi/worker.js"
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Routes
|
|
39
|
-
|
|
40
|
-
Place `.html` files inside `src/routes/`. The file path becomes the URL pattern.
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
src/routes/page.html → /
|
|
44
|
-
src/routes/items/page.html → /items
|
|
45
|
-
src/routes/blog/[slug]/page.html → /blog/:slug
|
|
46
|
-
src/routes/layout.html → shared layout wrapping all routes
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Execution model
|
|
50
|
-
|
|
51
|
-
Kuratchi routes are server-first.
|
|
52
|
-
|
|
53
|
-
- `src/routes` defines server-rendered route modules.
|
|
54
|
-
- Top-level route `<script>` blocks run on the server.
|
|
55
|
-
- Template expressions, `if`, and `for` blocks render on the server.
|
|
56
|
-
- `src/server` is for private server-only modules and reusable backend logic.
|
|
57
|
-
- `src/server/runtime.hook.ts` is the server runtime hook entrypoint for request interception.
|
|
58
|
-
- Reactive `$:` code is the browser-only escape hatch.
|
|
59
|
-
|
|
60
|
-
Route files are not client files. They are server-rendered routes that can opt into small browser-side reactive behavior when needed.
|
|
61
|
-
|
|
62
|
-
### Route file structure
|
|
63
|
-
|
|
64
|
-
```html
|
|
65
|
-
<script>
|
|
66
|
-
import { getItems, addItem, deleteItem } from '$database/items';
|
|
67
|
-
|
|
68
|
-
const items = await getItems();
|
|
69
|
-
</script>
|
|
70
|
-
|
|
71
|
-
<!-- Template — plain HTML with minimal extensions -->
|
|
72
|
-
<ul>
|
|
73
|
-
for (const item of items) {
|
|
74
|
-
<li>{item.title}</li>
|
|
75
|
-
}
|
|
76
|
-
</ul>
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
The `$database/` alias resolves to `src/database/`. You can use any path alias configured in your tsconfig.
|
|
80
|
-
Private server logic should live in `src/server/` and be imported into routes explicitly.
|
|
81
|
-
|
|
82
|
-
### Layout file
|
|
83
|
-
|
|
84
|
-
`src/routes/layout.html` wraps every page. Use `<slot></slot>` where page content renders:
|
|
85
|
-
|
|
86
|
-
```html
|
|
87
|
-
<!DOCTYPE html>
|
|
88
|
-
<html lang="en">
|
|
89
|
-
<head>
|
|
90
|
-
<meta charset="utf-8" />
|
|
91
|
-
<title>My App</title>
|
|
92
|
-
</head>
|
|
93
|
-
<body>
|
|
94
|
-
<nav>
|
|
95
|
-
<a href="/">Home</a>
|
|
96
|
-
<a href="/items">Items</a>
|
|
97
|
-
</nav>
|
|
98
|
-
<main>
|
|
99
|
-
<slot></slot>
|
|
100
|
-
</main>
|
|
101
|
-
</body>
|
|
102
|
-
</html>
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Template syntax
|
|
106
|
-
|
|
107
|
-
### Interpolation
|
|
108
|
-
|
|
109
|
-
```html
|
|
110
|
-
<p>{title}</p>
|
|
111
|
-
<p>{@html bodyHtml}</p> <!-- sanitized HTML -->
|
|
112
|
-
<p>{@raw trustedHtml}</p> <!-- unescaped, unsafe -->
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Conditionals
|
|
116
|
-
|
|
117
|
-
```html
|
|
118
|
-
if (items.length === 0) {
|
|
119
|
-
<p>Nothing here yet.</p>
|
|
120
|
-
} else {
|
|
121
|
-
<p>{items.length} items</p>
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Loops
|
|
126
|
-
|
|
127
|
-
```html
|
|
128
|
-
for (const item of items) {
|
|
129
|
-
<li>{item.title}</li>
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Components
|
|
134
|
-
|
|
135
|
-
Import `.html` components from your `src/lib/` directory or from packages:
|
|
136
|
-
|
|
137
|
-
```html
|
|
138
|
-
<script>
|
|
139
|
-
import Card from '$lib/card.html';
|
|
140
|
-
import Badge from '@kuratchi/ui/badge.html';
|
|
141
|
-
</script>
|
|
142
|
-
|
|
143
|
-
<Card title="Stack">
|
|
144
|
-
<Badge variant="success">Live</Badge>
|
|
145
|
-
</Card>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Client Reactivity (`$:`)
|
|
149
|
-
|
|
150
|
-
Inside client/browser `<script>` tags in the template markup, Kuratchi supports Svelte-style reactive labels:
|
|
151
|
-
|
|
152
|
-
```html
|
|
153
|
-
<script>
|
|
154
|
-
let users = ['Alice'];
|
|
155
|
-
|
|
156
|
-
$: console.log(`Users: ${users.length}`);
|
|
157
|
-
|
|
158
|
-
function addUser() {
|
|
159
|
-
users.push('Bob'); // reactive update, no reassignment required
|
|
160
|
-
}
|
|
161
|
-
</script>
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Block form is also supported:
|
|
165
|
-
|
|
166
|
-
```html
|
|
167
|
-
<script>
|
|
168
|
-
let form = { first: '', last: '' };
|
|
169
|
-
|
|
170
|
-
$: {
|
|
171
|
-
const fullName = `${form.first} ${form.last}`.trim();
|
|
172
|
-
console.log(fullName);
|
|
173
|
-
}
|
|
174
|
-
</script>
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Notes:
|
|
178
|
-
- Route files are server-rendered by default. `$:` is the only browser-side execution primitive in a route template.
|
|
179
|
-
- This reactivity runs in browser scripts rendered in the template markup, not in the top server route `<script>` block.
|
|
180
|
-
- Object/array `let` bindings are proxy-backed automatically when `$:` is used.
|
|
181
|
-
- `$: name = expr` works; when replacing proxy-backed values, the compiler preserves reactivity under the hood.
|
|
182
|
-
- You should not need `if (browser)` style guards in normal Kuratchi route code. If browser checks become necessary outside `$:`, the boundary is likely in the wrong place.
|
|
183
|
-
|
|
184
|
-
## Form actions
|
|
185
|
-
|
|
186
|
-
Export server functions from a route's `<script>` block and reference them with `action={fn}`. The compiler automatically registers them as dispatchable actions.
|
|
187
|
-
|
|
188
|
-
```html
|
|
189
|
-
<script>
|
|
190
|
-
import { addItem, deleteItem } from '$database/items';
|
|
191
|
-
</script>
|
|
192
|
-
|
|
193
|
-
<!-- Standard form — POST-Redirect-GET -->
|
|
194
|
-
<form action={addItem} method="POST">
|
|
195
|
-
<input type="text" name="title" required />
|
|
196
|
-
<button type="submit">Add</button>
|
|
197
|
-
</form>
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
The action function receives the raw `FormData`. Throw `ActionError` to surface a message back to the form — see [Error handling](#error-handling).
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
// src/database/items.ts
|
|
204
|
-
import { ActionError } from '@kuratchi/js';
|
|
205
|
-
|
|
206
|
-
export async function addItem(formData: FormData): Promise<void> {
|
|
207
|
-
const title = (formData.get('title') as string)?.trim();
|
|
208
|
-
if (!title) throw new ActionError('Title is required');
|
|
209
|
-
// write to DB...
|
|
210
|
-
}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### Redirect after action
|
|
214
|
-
|
|
215
|
-
Call `redirect()` inside an action or `load()` to immediately exit and send the user to a different URL. `throw redirect()` also works, but is redundant because `redirect()` already throws:
|
|
216
|
-
|
|
217
|
-
```ts
|
|
218
|
-
import { redirect } from '@kuratchi/js';
|
|
219
|
-
|
|
220
|
-
export async function createItem(formData: FormData): Promise<void> {
|
|
221
|
-
const id = await db.items.insert({ title: formData.get('title') });
|
|
222
|
-
redirect(`/items/${id}`);
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Error handling
|
|
227
|
-
|
|
228
|
-
### Action errors
|
|
229
|
-
|
|
230
|
-
Throw `ActionError` from a form action to surface a user-facing message in the template. The error message is bound directly to the action by name — if you have multiple forms on the same page, each has its own isolated error state.
|
|
231
|
-
|
|
232
|
-
```ts
|
|
233
|
-
import { ActionError } from '@kuratchi/js';
|
|
234
|
-
|
|
235
|
-
export async function signIn(formData: FormData) {
|
|
236
|
-
const email = formData.get('email') as string;
|
|
237
|
-
const password = formData.get('password') as string;
|
|
238
|
-
|
|
239
|
-
if (!email || !password) throw new ActionError('Email and password are required');
|
|
240
|
-
|
|
241
|
-
const user = await db.findUser(email);
|
|
242
|
-
if (!user || !await verify(password, user.passwordHash)) {
|
|
243
|
-
throw new ActionError('Invalid credentials');
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
In the template, the action's state object is available under its function name:
|
|
249
|
-
|
|
250
|
-
```html
|
|
251
|
-
<script>
|
|
252
|
-
import { signIn } from '$database/auth';
|
|
253
|
-
</script>
|
|
254
|
-
|
|
255
|
-
<form action={signIn}>
|
|
256
|
-
(signIn.error ? `<p class="error">${signIn.error}</p>` : '')
|
|
257
|
-
<input type="email" name="email" />
|
|
258
|
-
<input type="password" name="password" />
|
|
259
|
-
<button type="submit">Sign in</button>
|
|
260
|
-
</form>
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
The state object shape: `{ error?: string, loading: boolean, success: boolean }`.
|
|
264
|
-
|
|
265
|
-
- `actionName.error` — set on `ActionError` throw, cleared on next successful action
|
|
266
|
-
- `actionName.loading` — set by the client bridge during form submission (CSS target: `form[data-action-loading]`)
|
|
267
|
-
- `actionName.success` — reserved for future use
|
|
268
|
-
|
|
269
|
-
Throwing a plain `Error` instead of `ActionError` keeps the message hidden in production and shows a generic "Action failed" message. Use `ActionError` for expected validation failures; let plain errors propagate for unexpected crashes.
|
|
270
|
-
|
|
271
|
-
### Load errors
|
|
272
|
-
|
|
273
|
-
Throw `PageError` from a route's load scope to return the correct HTTP error page. Without it, any thrown error becomes a 500.
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
import { PageError } from '@kuratchi/js';
|
|
277
|
-
|
|
278
|
-
// In src/routes/posts/[id]/page.html <script> block:
|
|
279
|
-
const post = await db.posts.findOne({ id: params.id });
|
|
280
|
-
if (!post) throw new PageError(404);
|
|
281
|
-
if (!post.isPublished && !currentUser?.isAdmin) throw new PageError(403);
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
`PageError` accepts any HTTP status. The framework renders the matching custom error page (`src/routes/404.html`, `src/routes/500.html`, etc.) if one exists, otherwise falls back to the built-in error page.
|
|
285
|
-
|
|
286
|
-
```ts
|
|
287
|
-
throw new PageError(404); // → 404 page
|
|
288
|
-
throw new PageError(403, 'Admin only'); // → 403 page, message shown in dev
|
|
289
|
-
throw new PageError(401, 'Login required'); // → 401 page
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
For soft load failures where the page should still render (e.g. a widget that failed to fetch), return the error as data from `load()` and handle it in the template:
|
|
293
|
-
|
|
294
|
-
```html
|
|
295
|
-
<script>
|
|
296
|
-
const { data: recommendations, error: recError } = await safeGetRecommendations();
|
|
297
|
-
</script>
|
|
298
|
-
|
|
299
|
-
(recError ? '<p class="notice">Could not load recommendations.</p>' : '')
|
|
300
|
-
for (const rec of (recommendations ?? [])) {
|
|
301
|
-
<article>{rec.title}</article>
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
## Progressive enhancement
|
|
306
|
-
|
|
307
|
-
These `data-*` attributes wire up client-side interactivity without writing JavaScript.
|
|
308
|
-
|
|
309
|
-
### `data-action` — fetch action (no page reload)
|
|
310
|
-
|
|
311
|
-
Calls a server action via `fetch` and refreshes `data-refresh` targets when done:
|
|
312
|
-
|
|
313
|
-
```html
|
|
314
|
-
<button data-action="deleteItem" data-args={JSON.stringify([item.id])}>Delete</button>
|
|
315
|
-
<button data-action="toggleItem" data-args={JSON.stringify([item.id, true])}>Done</button>
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
The action function receives the args array as individual arguments:
|
|
319
|
-
|
|
320
|
-
```ts
|
|
321
|
-
export async function deleteItem(id: number): Promise<void> {
|
|
322
|
-
await db.items.delete({ id });
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export async function toggleItem(id: number, done: boolean): Promise<void> {
|
|
326
|
-
await db.items.update({ id }, { done });
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### `data-refresh` — partial refresh
|
|
331
|
-
|
|
332
|
-
After a `data-action` call succeeds, elements with `data-refresh` re-fetch their content:
|
|
333
|
-
|
|
334
|
-
```html
|
|
335
|
-
<section data-refresh="/items">
|
|
336
|
-
for (const item of items) {
|
|
337
|
-
<article>{item.title}</article>
|
|
338
|
-
}
|
|
339
|
-
</section>
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### `data-get` — client-side navigation
|
|
343
|
-
|
|
344
|
-
Navigate to a URL on click (respects `http:`/`https:` only):
|
|
345
|
-
|
|
346
|
-
```html
|
|
347
|
-
<div data-get="/items/{item.id}">Click to navigate</div>
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### `data-poll` — polling
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
```html
|
|
355
|
-
<div data-
|
|
356
|
-
{status}
|
|
357
|
-
</div>
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
```html
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
- `
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
##
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1
|
+
# @kuratchi/js
|
|
2
|
+
|
|
3
|
+
Cloudflare Workers-native web framework with file-based routing, server actions, and Durable Object support.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @kuratchi/js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx kuratchi create my-app
|
|
15
|
+
cd my-app
|
|
16
|
+
bun run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
`kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
|
|
22
|
+
|
|
23
|
+
| File | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `.kuratchi/routes.js` | Compiled routes, actions, RPC handlers, and render functions |
|
|
26
|
+
| `.kuratchi/worker.js` | Stable wrangler entry - re-exports the fetch handler plus all Durable Object and Agent classes |
|
|
27
|
+
| `.kuratchi/do/*.js` | Generated Durable Object RPC proxy modules for `$durable-objects/*` imports |
|
|
28
|
+
|
|
29
|
+
Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
// wrangler.jsonc
|
|
33
|
+
{
|
|
34
|
+
"main": ".kuratchi/worker.js"
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Routes
|
|
39
|
+
|
|
40
|
+
Place `.html` files inside `src/routes/`. The file path becomes the URL pattern.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
src/routes/page.html → /
|
|
44
|
+
src/routes/items/page.html → /items
|
|
45
|
+
src/routes/blog/[slug]/page.html → /blog/:slug
|
|
46
|
+
src/routes/layout.html → shared layout wrapping all routes
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Execution model
|
|
50
|
+
|
|
51
|
+
Kuratchi routes are server-first.
|
|
52
|
+
|
|
53
|
+
- `src/routes` defines server-rendered route modules.
|
|
54
|
+
- Top-level route `<script>` blocks run on the server.
|
|
55
|
+
- Template expressions, `if`, and `for` blocks render on the server.
|
|
56
|
+
- `src/server` is for private server-only modules and reusable backend logic.
|
|
57
|
+
- `src/server/runtime.hook.ts` is the server runtime hook entrypoint for request interception.
|
|
58
|
+
- Reactive `$:` code is the browser-only escape hatch.
|
|
59
|
+
|
|
60
|
+
Route files are not client files. They are server-rendered routes that can opt into small browser-side reactive behavior when needed.
|
|
61
|
+
|
|
62
|
+
### Route file structure
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<script>
|
|
66
|
+
import { getItems, addItem, deleteItem } from '$database/items';
|
|
67
|
+
|
|
68
|
+
const items = await getItems();
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<!-- Template — plain HTML with minimal extensions -->
|
|
72
|
+
<ul>
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
<li>{item.title}</li>
|
|
75
|
+
}
|
|
76
|
+
</ul>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `$database/` alias resolves to `src/database/`. You can use any path alias configured in your tsconfig.
|
|
80
|
+
Private server logic should live in `src/server/` and be imported into routes explicitly.
|
|
81
|
+
|
|
82
|
+
### Layout file
|
|
83
|
+
|
|
84
|
+
`src/routes/layout.html` wraps every page. Use `<slot></slot>` where page content renders:
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<!DOCTYPE html>
|
|
88
|
+
<html lang="en">
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="utf-8" />
|
|
91
|
+
<title>My App</title>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<nav>
|
|
95
|
+
<a href="/">Home</a>
|
|
96
|
+
<a href="/items">Items</a>
|
|
97
|
+
</nav>
|
|
98
|
+
<main>
|
|
99
|
+
<slot></slot>
|
|
100
|
+
</main>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Template syntax
|
|
106
|
+
|
|
107
|
+
### Interpolation
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<p>{title}</p>
|
|
111
|
+
<p>{@html bodyHtml}</p> <!-- sanitized HTML -->
|
|
112
|
+
<p>{@raw trustedHtml}</p> <!-- unescaped, unsafe -->
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Conditionals
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
if (items.length === 0) {
|
|
119
|
+
<p>Nothing here yet.</p>
|
|
120
|
+
} else {
|
|
121
|
+
<p>{items.length} items</p>
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Loops
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
<li>{item.title}</li>
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Components
|
|
134
|
+
|
|
135
|
+
Import `.html` components from your `src/lib/` directory or from packages:
|
|
136
|
+
|
|
137
|
+
```html
|
|
138
|
+
<script>
|
|
139
|
+
import Card from '$lib/card.html';
|
|
140
|
+
import Badge from '@kuratchi/ui/badge.html';
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<Card title="Stack">
|
|
144
|
+
<Badge variant="success">Live</Badge>
|
|
145
|
+
</Card>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Client Reactivity (`$:`)
|
|
149
|
+
|
|
150
|
+
Inside client/browser `<script>` tags in the template markup, Kuratchi supports Svelte-style reactive labels:
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<script>
|
|
154
|
+
let users = ['Alice'];
|
|
155
|
+
|
|
156
|
+
$: console.log(`Users: ${users.length}`);
|
|
157
|
+
|
|
158
|
+
function addUser() {
|
|
159
|
+
users.push('Bob'); // reactive update, no reassignment required
|
|
160
|
+
}
|
|
161
|
+
</script>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Block form is also supported:
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<script>
|
|
168
|
+
let form = { first: '', last: '' };
|
|
169
|
+
|
|
170
|
+
$: {
|
|
171
|
+
const fullName = `${form.first} ${form.last}`.trim();
|
|
172
|
+
console.log(fullName);
|
|
173
|
+
}
|
|
174
|
+
</script>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Notes:
|
|
178
|
+
- Route files are server-rendered by default. `$:` is the only browser-side execution primitive in a route template.
|
|
179
|
+
- This reactivity runs in browser scripts rendered in the template markup, not in the top server route `<script>` block.
|
|
180
|
+
- Object/array `let` bindings are proxy-backed automatically when `$:` is used.
|
|
181
|
+
- `$: name = expr` works; when replacing proxy-backed values, the compiler preserves reactivity under the hood.
|
|
182
|
+
- You should not need `if (browser)` style guards in normal Kuratchi route code. If browser checks become necessary outside `$:`, the boundary is likely in the wrong place.
|
|
183
|
+
|
|
184
|
+
## Form actions
|
|
185
|
+
|
|
186
|
+
Export server functions from a route's `<script>` block and reference them with `action={fn}`. The compiler automatically registers them as dispatchable actions.
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<script>
|
|
190
|
+
import { addItem, deleteItem } from '$database/items';
|
|
191
|
+
</script>
|
|
192
|
+
|
|
193
|
+
<!-- Standard form — POST-Redirect-GET -->
|
|
194
|
+
<form action={addItem} method="POST">
|
|
195
|
+
<input type="text" name="title" required />
|
|
196
|
+
<button type="submit">Add</button>
|
|
197
|
+
</form>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The action function receives the raw `FormData`. Throw `ActionError` to surface a message back to the form — see [Error handling](#error-handling).
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// src/database/items.ts
|
|
204
|
+
import { ActionError } from '@kuratchi/js';
|
|
205
|
+
|
|
206
|
+
export async function addItem(formData: FormData): Promise<void> {
|
|
207
|
+
const title = (formData.get('title') as string)?.trim();
|
|
208
|
+
if (!title) throw new ActionError('Title is required');
|
|
209
|
+
// write to DB...
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Redirect after action
|
|
214
|
+
|
|
215
|
+
Call `redirect()` inside an action or `load()` to immediately exit and send the user to a different URL. `throw redirect()` also works, but is redundant because `redirect()` already throws:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { redirect } from '@kuratchi/js';
|
|
219
|
+
|
|
220
|
+
export async function createItem(formData: FormData): Promise<void> {
|
|
221
|
+
const id = await db.items.insert({ title: formData.get('title') });
|
|
222
|
+
redirect(`/items/${id}`);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Error handling
|
|
227
|
+
|
|
228
|
+
### Action errors
|
|
229
|
+
|
|
230
|
+
Throw `ActionError` from a form action to surface a user-facing message in the template. The error message is bound directly to the action by name — if you have multiple forms on the same page, each has its own isolated error state.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { ActionError } from '@kuratchi/js';
|
|
234
|
+
|
|
235
|
+
export async function signIn(formData: FormData) {
|
|
236
|
+
const email = formData.get('email') as string;
|
|
237
|
+
const password = formData.get('password') as string;
|
|
238
|
+
|
|
239
|
+
if (!email || !password) throw new ActionError('Email and password are required');
|
|
240
|
+
|
|
241
|
+
const user = await db.findUser(email);
|
|
242
|
+
if (!user || !await verify(password, user.passwordHash)) {
|
|
243
|
+
throw new ActionError('Invalid credentials');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
In the template, the action's state object is available under its function name:
|
|
249
|
+
|
|
250
|
+
```html
|
|
251
|
+
<script>
|
|
252
|
+
import { signIn } from '$database/auth';
|
|
253
|
+
</script>
|
|
254
|
+
|
|
255
|
+
<form action={signIn}>
|
|
256
|
+
(signIn.error ? `<p class="error">${signIn.error}</p>` : '')
|
|
257
|
+
<input type="email" name="email" />
|
|
258
|
+
<input type="password" name="password" />
|
|
259
|
+
<button type="submit">Sign in</button>
|
|
260
|
+
</form>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The state object shape: `{ error?: string, loading: boolean, success: boolean }`.
|
|
264
|
+
|
|
265
|
+
- `actionName.error` — set on `ActionError` throw, cleared on next successful action
|
|
266
|
+
- `actionName.loading` — set by the client bridge during form submission (CSS target: `form[data-action-loading]`)
|
|
267
|
+
- `actionName.success` — reserved for future use
|
|
268
|
+
|
|
269
|
+
Throwing a plain `Error` instead of `ActionError` keeps the message hidden in production and shows a generic "Action failed" message. Use `ActionError` for expected validation failures; let plain errors propagate for unexpected crashes.
|
|
270
|
+
|
|
271
|
+
### Load errors
|
|
272
|
+
|
|
273
|
+
Throw `PageError` from a route's load scope to return the correct HTTP error page. Without it, any thrown error becomes a 500.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { PageError } from '@kuratchi/js';
|
|
277
|
+
|
|
278
|
+
// In src/routes/posts/[id]/page.html <script> block:
|
|
279
|
+
const post = await db.posts.findOne({ id: params.id });
|
|
280
|
+
if (!post) throw new PageError(404);
|
|
281
|
+
if (!post.isPublished && !currentUser?.isAdmin) throw new PageError(403);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`PageError` accepts any HTTP status. The framework renders the matching custom error page (`src/routes/404.html`, `src/routes/500.html`, etc.) if one exists, otherwise falls back to the built-in error page.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
throw new PageError(404); // → 404 page
|
|
288
|
+
throw new PageError(403, 'Admin only'); // → 403 page, message shown in dev
|
|
289
|
+
throw new PageError(401, 'Login required'); // → 401 page
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
For soft load failures where the page should still render (e.g. a widget that failed to fetch), return the error as data from `load()` and handle it in the template:
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
<script>
|
|
296
|
+
const { data: recommendations, error: recError } = await safeGetRecommendations();
|
|
297
|
+
</script>
|
|
298
|
+
|
|
299
|
+
(recError ? '<p class="notice">Could not load recommendations.</p>' : '')
|
|
300
|
+
for (const rec of (recommendations ?? [])) {
|
|
301
|
+
<article>{rec.title}</article>
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Progressive enhancement
|
|
306
|
+
|
|
307
|
+
These `data-*` attributes wire up client-side interactivity without writing JavaScript.
|
|
308
|
+
|
|
309
|
+
### `data-action` — fetch action (no page reload)
|
|
310
|
+
|
|
311
|
+
Calls a server action via `fetch` and refreshes `data-refresh` targets when done:
|
|
312
|
+
|
|
313
|
+
```html
|
|
314
|
+
<button data-action="deleteItem" data-args={JSON.stringify([item.id])}>Delete</button>
|
|
315
|
+
<button data-action="toggleItem" data-args={JSON.stringify([item.id, true])}>Done</button>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The action function receives the args array as individual arguments:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
export async function deleteItem(id: number): Promise<void> {
|
|
322
|
+
await db.items.delete({ id });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function toggleItem(id: number, done: boolean): Promise<void> {
|
|
326
|
+
await db.items.update({ id }, { done });
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### `data-refresh` — partial refresh
|
|
331
|
+
|
|
332
|
+
After a `data-action` call succeeds, elements with `data-refresh` re-fetch their content:
|
|
333
|
+
|
|
334
|
+
```html
|
|
335
|
+
<section data-refresh="/items">
|
|
336
|
+
for (const item of items) {
|
|
337
|
+
<article>{item.title}</article>
|
|
338
|
+
}
|
|
339
|
+
</section>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### `data-get` — client-side navigation
|
|
343
|
+
|
|
344
|
+
Navigate to a URL on click (respects `http:`/`https:` only):
|
|
345
|
+
|
|
346
|
+
```html
|
|
347
|
+
<div data-get="/items/{item.id}">Click to navigate</div>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### `data-poll` — polling
|
|
351
|
+
|
|
352
|
+
Poll and update an element's content at a human-readable interval with automatic exponential backoff:
|
|
353
|
+
|
|
354
|
+
```html
|
|
355
|
+
<div data-poll={getStatus(itemId)} data-interval="2s">
|
|
356
|
+
{status}
|
|
357
|
+
</div>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**How it works:**
|
|
361
|
+
1. Client sends a fragment request with `x-kuratchi-fragment` header
|
|
362
|
+
2. Server re-renders the route but returns **only the fragment's innerHTML** — not the full page
|
|
363
|
+
3. Client swaps the element's content — minimal payload, no full page reload
|
|
364
|
+
|
|
365
|
+
This fragment-based architecture is the foundation for partial rendering and scales to Astro-style islands.
|
|
366
|
+
|
|
367
|
+
**Interval formats:**
|
|
368
|
+
- `2s` — 2 seconds
|
|
369
|
+
- `500ms` — 500 milliseconds
|
|
370
|
+
- `1m` — 1 minute
|
|
371
|
+
- Default: `30s` with exponential backoff (30s → 45s → 67s → ... capped at 5 minutes)
|
|
372
|
+
|
|
373
|
+
**Options:**
|
|
374
|
+
- `data-interval` — polling interval (human-readable, default `30s`)
|
|
375
|
+
- `data-backoff="false"` — disable exponential backoff
|
|
376
|
+
|
|
377
|
+
### `data-select-all` / `data-select-item` — checkbox groups
|
|
378
|
+
|
|
379
|
+
Sync a "select all" checkbox with a group of item checkboxes:
|
|
380
|
+
|
|
381
|
+
```html
|
|
382
|
+
<input type="checkbox" data-select-all="todos" />
|
|
383
|
+
|
|
384
|
+
for (const todo of todos) {
|
|
385
|
+
<input type="checkbox" data-select-item="todos" value={todo.id} />
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## RPC
|
|
390
|
+
|
|
391
|
+
For Durable Objects, RPC is file-driven and automatic.
|
|
392
|
+
|
|
393
|
+
- Put handler logic in a `.do.ts` file.
|
|
394
|
+
- Exported functions in that file become RPC methods.
|
|
395
|
+
- Import RPC methods from `$durable-objects/<file-name-without-.do>`.
|
|
396
|
+
- RPC methods are still server-side code. They are exposed intentionally by the framework runtime, not because route files are client-side.
|
|
397
|
+
|
|
398
|
+
```html
|
|
399
|
+
<script>
|
|
400
|
+
import { getOrgUsers, createOrgUser } from '$durable-objects/auth';
|
|
401
|
+
const users = await getOrgUsers();
|
|
402
|
+
</script>
|
|
403
|
+
|
|
404
|
+
<form action={createOrgUser} method="POST">
|
|
405
|
+
<input type="email" name="email" required />
|
|
406
|
+
<button type="submit">Create</button>
|
|
407
|
+
</form>
|
|
408
|
+
```
|
|
409
|
+
## Durable Objects
|
|
410
|
+
|
|
411
|
+
Durable Object behavior is enabled by filename suffix.
|
|
412
|
+
|
|
413
|
+
- Any file ending in `.do.ts` is treated as a Durable Object handler file.
|
|
414
|
+
- Any file not ending in `.do.ts` is treated as a normal server module.
|
|
415
|
+
- No required folder name. `src/server/auth.do.ts`, `src/server/foo/bar/sites.do.ts`, etc. all work.
|
|
416
|
+
|
|
417
|
+
### Writing a Durable Object
|
|
418
|
+
|
|
419
|
+
Extend the native Cloudflare `DurableObject` class. Public methods automatically become RPC-accessible:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
// src/server/user.do.ts
|
|
423
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
424
|
+
|
|
425
|
+
export default class UserDO extends DurableObject {
|
|
426
|
+
async getName() {
|
|
427
|
+
return await this.ctx.storage.get('name');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async setName(name: string) {
|
|
431
|
+
this._validate(name);
|
|
432
|
+
await this.ctx.storage.put('name', name);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// NOT RPC-accessible (underscore prefix)
|
|
436
|
+
_validate(name: string) {
|
|
437
|
+
if (!name) throw new Error('Name required');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// NOT RPC-accessible (lifecycle method)
|
|
441
|
+
async alarm() {
|
|
442
|
+
// Handle alarm
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**RPC rules:**
|
|
448
|
+
- **Public methods** (`getName`, `setName`) → RPC-accessible
|
|
449
|
+
- **Underscore prefix** (`_validate`) → NOT RPC-accessible
|
|
450
|
+
- **Private/protected** (`private foo()`) → NOT RPC-accessible
|
|
451
|
+
- **Lifecycle methods** (`constructor`, `fetch`, `alarm`, `webSocketMessage`, etc.) → NOT RPC-accessible
|
|
452
|
+
|
|
453
|
+
### Using from routes
|
|
454
|
+
|
|
455
|
+
Import from `$do/<filename>` (without the `.do` suffix):
|
|
456
|
+
|
|
457
|
+
```html
|
|
458
|
+
<script server>
|
|
459
|
+
import { getName, setName } from '$do/user';
|
|
460
|
+
|
|
461
|
+
const name = await getName();
|
|
462
|
+
</script>
|
|
463
|
+
|
|
464
|
+
<h1>Hello, {name}</h1>
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
The framework handles RPC wiring automatically.
|
|
468
|
+
|
|
469
|
+
### Auto-Discovery
|
|
470
|
+
|
|
471
|
+
Durable Objects are auto-discovered from `.do.ts` files. **No config needed.**
|
|
472
|
+
|
|
473
|
+
**Naming convention:**
|
|
474
|
+
- `user.do.ts` → binding `USER_DO`
|
|
475
|
+
- `org-settings.do.ts` → binding `ORG_SETTINGS_DO`
|
|
476
|
+
|
|
477
|
+
**Override binding name** with `static binding`:
|
|
478
|
+
```ts
|
|
479
|
+
export default class UserDO extends DurableObject {
|
|
480
|
+
static binding = 'CUSTOM_BINDING'; // Optional override
|
|
481
|
+
// ...
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The framework auto-syncs discovered DOs to `wrangler.jsonc`.
|
|
486
|
+
|
|
487
|
+
### Optional: stubId for auth integration
|
|
488
|
+
|
|
489
|
+
If you need automatic stub resolution based on user context, add `stubId` in `kuratchi.config.ts`:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
// kuratchi.config.ts
|
|
493
|
+
export default defineConfig({
|
|
494
|
+
durableObjects: {
|
|
495
|
+
USER_DO: { stubId: 'user.orgId' }, // Only needed for auth integration
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Agents
|
|
501
|
+
|
|
502
|
+
Kuratchi treats `src/server/**/*.agent.ts` as a first-class Worker export convention.
|
|
503
|
+
|
|
504
|
+
- Any `.agent.ts` file under `src/server/` is scanned during build.
|
|
505
|
+
- The file must export a class with either `export class MyAgent` or `export default class MyAgent`.
|
|
506
|
+
- The compiler re-exports that class from `.kuratchi/worker.js`, so Wrangler can bind it directly.
|
|
507
|
+
- `.agent.ts` files are not route modules and are not converted into `$durable-objects/*` RPC proxies.
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
// src/server/ai/session.agent.ts
|
|
511
|
+
import { Agent } from 'agents';
|
|
512
|
+
|
|
513
|
+
export class SessionAgent extends Agent {
|
|
514
|
+
async onRequest() {
|
|
515
|
+
return Response.json({ ok: true });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
```jsonc
|
|
521
|
+
// wrangler.jsonc
|
|
522
|
+
{
|
|
523
|
+
"durable_objects": {
|
|
524
|
+
"bindings": [{ "name": "AI_SESSION", "class_name": "SessionAgent" }]
|
|
525
|
+
},
|
|
526
|
+
"migrations": [
|
|
527
|
+
{ "tag": "v1", "new_sqlite_classes": ["SessionAgent"] }
|
|
528
|
+
]
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Failure and edge behavior:
|
|
533
|
+
|
|
534
|
+
- If a `.agent.ts` file does not export a class, the build fails.
|
|
535
|
+
- Kuratchi only auto-discovers `.agent.ts` files under `src/server/`.
|
|
536
|
+
- You still need Wrangler Durable Object bindings and migrations because Agents run as Durable Objects.
|
|
537
|
+
|
|
538
|
+
## Workflows
|
|
539
|
+
|
|
540
|
+
Kuratchi auto-discovers `.workflow.ts` files in `src/server/`. **No config needed.**
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
// src/server/migration.workflow.ts
|
|
544
|
+
import { WorkflowEntrypoint, WorkflowStep } from 'cloudflare:workers';
|
|
545
|
+
import type { WorkflowEvent } from 'cloudflare:workers';
|
|
546
|
+
|
|
547
|
+
export class MigrationWorkflow extends WorkflowEntrypoint<Env, MigrationParams> {
|
|
548
|
+
async run(event: WorkflowEvent<MigrationParams>, step: WorkflowStep) {
|
|
549
|
+
// workflow steps...
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
On build, Kuratchi:
|
|
555
|
+
1. Scans `src/server/` for `.workflow.ts` files
|
|
556
|
+
2. Derives binding from filename: `migration.workflow.ts` → `MIGRATION_WORKFLOW`
|
|
557
|
+
3. Infers class name from the exported class
|
|
558
|
+
4. Auto-adds/updates the workflow entry in `wrangler.jsonc`
|
|
559
|
+
|
|
560
|
+
**Zero config required.** Just create the file and the framework handles everything:
|
|
561
|
+
- `name`: derived from binding (e.g., `MIGRATION_WORKFLOW` → `migration-workflow`)
|
|
562
|
+
- `binding`: derived from filename (e.g., `migration.workflow.ts` → `MIGRATION_WORKFLOW`)
|
|
563
|
+
- `class_name`: inferred from the exported class
|
|
564
|
+
|
|
565
|
+
Examples:
|
|
566
|
+
- `migration.workflow.ts` → `MIGRATION_WORKFLOW` binding
|
|
567
|
+
- `bond.workflow.ts` → `BOND_WORKFLOW` binding
|
|
568
|
+
- `new-site.workflow.ts` → `NEW_SITE_WORKFLOW` binding
|
|
569
|
+
|
|
570
|
+
### Workflow Status Polling
|
|
571
|
+
|
|
572
|
+
Kuratchi auto-generates status polling RPCs for each discovered workflow. Poll workflow status with zero setup:
|
|
573
|
+
|
|
574
|
+
```html
|
|
575
|
+
<div data-poll={migrationWorkflowStatus(instanceId)} data-interval="2s">
|
|
576
|
+
if (workflowStatus.status === 'running') {
|
|
577
|
+
<div class="spinner">Running...</div>
|
|
578
|
+
} else if (workflowStatus.status === 'complete') {
|
|
579
|
+
<div>✓ Complete</div>
|
|
580
|
+
}
|
|
581
|
+
</div>
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The element's innerHTML updates automatically when the workflow status changes — no page reload needed.
|
|
585
|
+
|
|
586
|
+
**Auto-generated RPC naming** (camelCase):
|
|
587
|
+
- `migration.workflow.ts` → `migrationWorkflowStatus(instanceId)`
|
|
588
|
+
- `james-bond.workflow.ts` → `jamesBondWorkflowStatus(instanceId)`
|
|
589
|
+
- `site.workflow.ts` → `siteWorkflowStatus(instanceId)`
|
|
590
|
+
|
|
591
|
+
**Multiple workflows on one page:** Each `data-poll` element is independent. You can poll multiple workflow instances without collision:
|
|
592
|
+
|
|
593
|
+
```html
|
|
594
|
+
for (const job of jobs) {
|
|
595
|
+
<div data-poll={migrationWorkflowStatus(job.instanceId)} data-interval="2s">
|
|
596
|
+
{job.name}: polling...
|
|
597
|
+
</div>
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
The status RPC returns the Cloudflare `InstanceStatus` object:
|
|
602
|
+
```ts
|
|
603
|
+
{
|
|
604
|
+
status: 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'unknown';
|
|
605
|
+
error?: { name: string; message: string; };
|
|
606
|
+
output?: unknown;
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Containers
|
|
611
|
+
|
|
612
|
+
Kuratchi auto-discovers `.container.ts` files in `src/server/`. **No config needed.**
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
// src/server/wordpress.container.ts
|
|
616
|
+
import { Container } from 'cloudflare:workers';
|
|
617
|
+
|
|
618
|
+
export class WordPressContainer extends Container {
|
|
619
|
+
// container implementation...
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
On build, Kuratchi derives the binding from the filename:
|
|
624
|
+
- `wordpress.container.ts` → `WORDPRESS_CONTAINER` binding
|
|
625
|
+
- `redis.container.ts` → `REDIS_CONTAINER` binding
|
|
626
|
+
|
|
627
|
+
## Convention-Based Auto-Discovery
|
|
628
|
+
|
|
629
|
+
Kuratchi uses file suffixes to auto-discover and register worker classes. **No config needed** — just create the file:
|
|
630
|
+
|
|
631
|
+
| Suffix | Location | Binding Pattern | Example |
|
|
632
|
+
|--------|----------|-----------------|---------|
|
|
633
|
+
| `.workflow.ts` | `src/server/**/*.workflow.ts` | `FILENAME_WORKFLOW` | `migration.workflow.ts` → `MIGRATION_WORKFLOW` |
|
|
634
|
+
| `.container.ts` | `src/server/**/*.container.ts` | `FILENAME_CONTAINER` | `wordpress.container.ts` → `WORDPRESS_CONTAINER` |
|
|
635
|
+
| `.agent.ts` | `src/server/**/*.agent.ts` | (manual wrangler config) | `session.agent.ts` |
|
|
636
|
+
| `.do.ts` | `src/server/**/*.do.ts` | (via `durableObjects` config) | `auth.do.ts` |
|
|
637
|
+
|
|
638
|
+
## Automatic Wrangler Config Sync
|
|
639
|
+
|
|
640
|
+
Kuratchi automatically syncs `wrangler.jsonc` during every build. This eliminates duplicate configuration for:
|
|
641
|
+
|
|
642
|
+
- **Workflows** — auto-discovered from `.workflow.ts` files
|
|
643
|
+
- **Containers** — auto-discovered from `.container.ts` files
|
|
644
|
+
- **Durable Objects** — `durableObjects` in kuratchi.config.ts
|
|
645
|
+
|
|
646
|
+
The sync is additive and non-destructive:
|
|
647
|
+
- New entries are added automatically
|
|
648
|
+
- Existing entries are updated if the class name changes
|
|
649
|
+
- Manually-added wrangler config (D1, KV, R2, vars, etc.) is preserved
|
|
650
|
+
- Removed entries are cleaned up from wrangler.jsonc
|
|
651
|
+
|
|
652
|
+
Requirements:
|
|
653
|
+
- Uses `wrangler.jsonc` or `wrangler.json` (TOML is not supported for auto-sync)
|
|
654
|
+
- Creates `wrangler.jsonc` if no wrangler config exists
|
|
655
|
+
|
|
656
|
+
## Runtime APIs
|
|
657
|
+
|
|
658
|
+
These are available anywhere in server-side route code:
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
import {
|
|
662
|
+
getCtx, // ExecutionContext
|
|
663
|
+
getRequest, // Request
|
|
664
|
+
getLocals, // mutable locals bag for the current request
|
|
665
|
+
getParams, // URL params ({ slug: 'foo' })
|
|
666
|
+
getParam, // getParam('slug')
|
|
667
|
+
RedirectError, // redirect signal thrown by redirect()\r\n redirect, // redirect('/path', 302)\r\n goto, // same as redirect()
|
|
668
|
+
goto, // same as redirect — alias
|
|
669
|
+
} from '@kuratchi/js';
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Request helpers
|
|
673
|
+
|
|
674
|
+
For a batteries-included request layer, import pre-parsed request state from `@kuratchi/js/request`:
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
import { url, pathname, searchParams, slug } from '@kuratchi/js/request';
|
|
678
|
+
|
|
679
|
+
const page = pathname;
|
|
680
|
+
const tab = searchParams.get('tab');
|
|
681
|
+
const postSlug = slug;
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
- `url` is the parsed `URL` for the current request.
|
|
685
|
+
- `pathname` is the full path, like `/blog/hello-world`.
|
|
686
|
+
- `searchParams` is `url.searchParams` for the current request.
|
|
687
|
+
- `slug` is `params.slug` when the matched route defines a `slug` param.
|
|
688
|
+
- `headers`, `method`, and `params` are also exported from `@kuratchi/js/request`.
|
|
689
|
+
- Use `getRequest()` when you want the raw native `Request` object.
|
|
690
|
+
|
|
691
|
+
## Runtime Hook
|
|
692
|
+
|
|
693
|
+
Optional server runtime hook file. Export a `RuntimeDefinition` from `src/server/runtime.hook.ts`
|
|
694
|
+
to intercept requests before they reach the framework router. Use it for agent routing,
|
|
695
|
+
pre-route auth, or custom response/error handling.
|
|
696
|
+
|
|
697
|
+
```ts
|
|
698
|
+
import type { RuntimeDefinition } from '@kuratchi/js';
|
|
699
|
+
|
|
700
|
+
const runtime: RuntimeDefinition = {
|
|
701
|
+
agents: {
|
|
702
|
+
async request(ctx, next) {
|
|
703
|
+
if (!ctx.url.pathname.startsWith('/agents/')) {
|
|
704
|
+
return next();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return new Response('Agent response');
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
export default runtime;
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
`ctx` includes:
|
|
716
|
+
|
|
717
|
+
- `ctx.url` - parsed URL
|
|
718
|
+
- `ctx.request` - raw Request
|
|
719
|
+
- `ctx.env` - Cloudflare env bindings
|
|
720
|
+
- `next()` - pass control to the next handler
|
|
721
|
+
|
|
722
|
+
## Environment bindings
|
|
723
|
+
|
|
724
|
+
Cloudflare env is server-only.
|
|
725
|
+
|
|
726
|
+
- Route top-level `<script>`, route `load()` functions, server actions, API handlers, and other server modules can read env.
|
|
727
|
+
- Templates, components, and client `<script>` blocks cannot read env directly.
|
|
728
|
+
- If a value must reach the browser, compute it in the server route script and reference it in the template, or return it from `load()` explicitly.
|
|
729
|
+
|
|
730
|
+
```html
|
|
731
|
+
<script>
|
|
732
|
+
import { env } from 'cloudflare:workers';
|
|
733
|
+
const turnstileSiteKey = env.TURNSTILE_SITE_KEY || '';
|
|
734
|
+
</script>
|
|
735
|
+
|
|
736
|
+
if (turnstileSiteKey) {
|
|
737
|
+
<div class="cf-turnstile" data-sitekey={turnstileSiteKey}></div>
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Server modules can still access env directly:
|
|
742
|
+
|
|
743
|
+
```ts
|
|
744
|
+
import { env } from 'cloudflare:workers';
|
|
745
|
+
|
|
746
|
+
const result = await env.DB.prepare('SELECT 1').run();
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
## Framework environment
|
|
750
|
+
|
|
751
|
+
Kuratchi also exposes a framework build-mode flag:
|
|
752
|
+
|
|
753
|
+
```html
|
|
754
|
+
<script>
|
|
755
|
+
import { dev } from '@kuratchi/js/environment';
|
|
756
|
+
import { env } from 'cloudflare:workers';
|
|
757
|
+
|
|
758
|
+
const turnstileSiteKey = dev ? '' : (env.TURNSTILE_SITE_KEY || '');
|
|
759
|
+
</script>
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
- `dev` is `true` for Kuratchi development builds
|
|
763
|
+
- `dev` is `false` for production builds
|
|
764
|
+
- `dev` is compile-time framework state, not a generic process env var
|
|
765
|
+
- `@kuratchi/js/environment` is intended for server route code, not client `$:` scripts
|
|
766
|
+
|
|
767
|
+
## `kuratchi.config.ts`
|
|
768
|
+
|
|
769
|
+
Optional. Required only when using framework integrations (ORM, auth, UI).
|
|
770
|
+
|
|
771
|
+
**Durable Objects are auto-discovered** — no config needed unless you need `stubId` for auth integration.
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
import { defineConfig } from '@kuratchi/js';
|
|
775
|
+
import { kuratchiUiConfig } from '@kuratchi/ui/adapter';
|
|
776
|
+
import { kuratchiOrmConfig } from '@kuratchi/orm/adapter';
|
|
777
|
+
import { kuratchiAuthConfig } from '@kuratchi/auth/adapter';
|
|
778
|
+
|
|
779
|
+
export default defineConfig({
|
|
780
|
+
ui: kuratchiUiConfig({ theme: 'default' }),
|
|
781
|
+
orm: kuratchiOrmConfig({
|
|
782
|
+
databases: {
|
|
783
|
+
DB: { schema: appSchema },
|
|
784
|
+
NOTES_DO: { schema: notesSchema, type: 'do' },
|
|
785
|
+
},
|
|
786
|
+
}),
|
|
787
|
+
auth: kuratchiAuthConfig({
|
|
788
|
+
cookieName: 'kuratchi_session',
|
|
789
|
+
sessionEnabled: true,
|
|
790
|
+
}),
|
|
791
|
+
// Optional: only needed for auth-based stub resolution
|
|
792
|
+
durableObjects: {
|
|
793
|
+
NOTES_DO: { stubId: 'user.orgId' },
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
Without `kuratchi.config.ts` the compiler falls back to defaults — just drop your route files in `src/routes/` and run `kuratchi build`.
|
|
799
|
+
|
|
800
|
+
## CLI
|
|
801
|
+
|
|
802
|
+
```bash
|
|
803
|
+
npx kuratchi build # one-shot build
|
|
804
|
+
npx kuratchi watch # watch mode (for use with wrangler dev)
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
## Testing the Framework
|
|
808
|
+
|
|
809
|
+
Run framework tests from `packages/kuratchi-js`:
|
|
810
|
+
|
|
811
|
+
```bash
|
|
812
|
+
bun run test
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Watch mode:
|
|
816
|
+
|
|
817
|
+
```bash
|
|
818
|
+
bun run test:watch
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
## TypeScript & Worker types
|
|
822
|
+
|
|
823
|
+
```bash
|
|
824
|
+
npx wrangler types
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Then include the generated types in `tsconfig.json`:
|
|
828
|
+
|
|
829
|
+
```json
|
|
830
|
+
{
|
|
831
|
+
"compilerOptions": {
|
|
832
|
+
"types": ["./worker-configuration.d.ts"]
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
```
|