@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 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
- Refresh a section automatically on an interval (milliseconds):
353
-
354
- ```html
355
- <div data-refresh="/status" data-poll="3000">
356
- {status}
357
- </div>
358
- ```
359
-
360
- ### `data-select-all` / `data-select-item` — checkbox groups
361
-
362
- Sync a "select all" checkbox with a group of item checkboxes:
363
-
364
- ```html
365
- <input type="checkbox" data-select-all="todos" />
366
-
367
- for (const todo of todos) {
368
- <input type="checkbox" data-select-item="todos" value={todo.id} />
369
- }
370
- ```
371
-
372
- ## RPC
373
-
374
- For Durable Objects, RPC is file-driven and automatic.
375
-
376
- - Put handler logic in a `.do.ts` file.
377
- - Exported functions in that file become RPC methods.
378
- - Import RPC methods from `$durable-objects/<file-name-without-.do>`.
379
- - RPC methods are still server-side code. They are exposed intentionally by the framework runtime, not because route files are client-side.
380
-
381
- ```html
382
- <script>
383
- import { getOrgUsers, createOrgUser } from '$durable-objects/auth';
384
- const users = await getOrgUsers();
385
- </script>
386
-
387
- <form action={createOrgUser} method="POST">
388
- <input type="email" name="email" required />
389
- <button type="submit">Create</button>
390
- </form>
391
- ```
392
- ## Durable Objects
393
-
394
- Durable Object behavior is enabled by filename suffix.
395
-
396
- - Any file ending in `.do.ts` is treated as a Durable Object handler file.
397
- - Any file not ending in `.do.ts` is treated as a normal server module.
398
- - No required folder name. `src/server/auth.do.ts`, `src/server/foo/bar/sites.do.ts`, etc. all work.
399
-
400
- ### Function mode (recommended)
401
-
402
- Write plain exported functions in a `.do.ts` file. Exported functions become DO RPC methods.
403
- Use `this.db`, `this.env`, and `this.ctx` inside those functions.
404
-
405
- ```ts
406
- // src/server/auth/auth.do.ts
407
- import { getCurrentUser, hashPassword } from '@kuratchi/auth';
408
- import { redirect } from '@kuratchi/js';
409
-
410
- async function randomPassword(length = 24): Promise<string> {
411
- const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
412
- const bytes = new Uint8Array(length);
413
- crypto.getRandomValues(bytes);
414
- let out = '';
415
- for (let i = 0; i < length; i++) out += alphabet[bytes[i] % alphabet.length];
416
- return out;
417
- }
418
-
419
- export async function getOrgUsers() {
420
- const result = await this.db.users.orderBy({ createdAt: 'asc' }).many();
421
- return result.data ?? [];
422
- }
423
-
424
- export async function createOrgUser(formData: FormData) {
425
- const user = await getCurrentUser();
426
- if (!user?.orgId) throw new Error('Not authenticated');
427
-
428
- const email = String(formData.get('email') ?? '').trim().toLowerCase();
429
- if (!email) throw new Error('Email is required');
430
-
431
- const passwordHash = await hashPassword(await randomPassword(), undefined, this.env.AUTH_SECRET);
432
- await this.db.users.insert({ email, role: 'member', passwordHash });
433
- redirect('/settings/users');
434
- }
435
- ```
436
-
437
- Optional lifecycle exports in function mode:
438
-
439
- - `export async function onInit()`
440
- - `export async function onAlarm(...args)`
441
- - `export function onMessage(...args)`
442
-
443
- These lifecycle names are not exposed as RPC methods.
444
-
445
- ### Class mode (optional)
446
-
447
- Class-based handlers are still supported in `.do.ts` files:
448
-
449
- ```ts
450
- import { kuratchiDO } from '@kuratchi/js';
451
-
452
- export default class NotesDO extends kuratchiDO {
453
- static binding = 'NOTES_DO';
454
-
455
- async getNotes() {
456
- return (await this.db.notes.orderBy({ created_at: 'desc' }).many()).data ?? [];
457
- }
458
- }
459
- ```
460
-
461
- Declare it in `kuratchi.config.ts` and in `wrangler.jsonc`. The compiler exports DO classes from `.kuratchi/worker.js` automatically.
462
-
463
- ```jsonc
464
- // wrangler.jsonc
465
- {
466
- "durable_objects": {
467
- "bindings": [{ "name": "NOTES_DO", "class_name": "NotesDO" }]
468
- },
469
- "migrations": [
470
- { "tag": "v1", "new_sqlite_classes": ["NotesDO"] }
471
- ]
472
- }
473
- ```
474
-
475
- ## Agents
476
-
477
- Kuratchi treats `src/server/**/*.agent.ts` as a first-class Worker export convention.
478
-
479
- - Any `.agent.ts` file under `src/server/` is scanned during build.
480
- - The file must export a class with either `export class MyAgent` or `export default class MyAgent`.
481
- - The compiler re-exports that class from `.kuratchi/worker.js`, so Wrangler can bind it directly.
482
- - `.agent.ts` files are not route modules and are not converted into `$durable-objects/*` RPC proxies.
483
-
484
- ```ts
485
- // src/server/ai/session.agent.ts
486
- import { Agent } from 'agents';
487
-
488
- export class SessionAgent extends Agent {
489
- async onRequest() {
490
- return Response.json({ ok: true });
491
- }
492
- }
493
- ```
494
-
495
- ```jsonc
496
- // wrangler.jsonc
497
- {
498
- "durable_objects": {
499
- "bindings": [{ "name": "AI_SESSION", "class_name": "SessionAgent" }]
500
- },
501
- "migrations": [
502
- { "tag": "v1", "new_sqlite_classes": ["SessionAgent"] }
503
- ]
504
- }
505
- ```
506
-
507
- Failure and edge behavior:
508
-
509
- - If a `.agent.ts` file does not export a class, the build fails.
510
- - Kuratchi only auto-discovers `.agent.ts` files under `src/server/`.
511
- - You still need Wrangler Durable Object bindings and migrations because Agents run as Durable Objects.
512
- ## Runtime APIs
513
-
514
- These are available anywhere in server-side route code:
515
-
516
- ```ts
517
- import {
518
- getCtx, // ExecutionContext
519
- getRequest, // Request
520
- getLocals, // mutable locals bag for the current request
521
- getParams, // URL params ({ slug: 'foo' })
522
- getParam, // getParam('slug')
523
- RedirectError, // redirect signal thrown by redirect()\r\n redirect, // redirect('/path', 302)\r\n goto, // same as redirect()
524
- goto, // same as redirect alias
525
- } from '@kuratchi/js';
526
- ```
527
-
528
- ### Request helpers
529
-
530
- For a batteries-included request layer, import pre-parsed request state from `@kuratchi/js/request`:
531
-
532
- ```ts
533
- import { url, pathname, searchParams, slug } from '@kuratchi/js/request';
534
-
535
- const page = pathname;
536
- const tab = searchParams.get('tab');
537
- const postSlug = slug;
538
- ```
539
-
540
- - `url` is the parsed `URL` for the current request.
541
- - `pathname` is the full path, like `/blog/hello-world`.
542
- - `searchParams` is `url.searchParams` for the current request.
543
- - `slug` is `params.slug` when the matched route defines a `slug` param.
544
- - `headers`, `method`, and `params` are also exported from `@kuratchi/js/request`.
545
- - Use `getRequest()` when you want the raw native `Request` object.
546
-
547
- ## Runtime Hook
548
-
549
- Optional server runtime hook file. Export a `RuntimeDefinition` from `src/server/runtime.hook.ts`
550
- to intercept requests before they reach the framework router. Use it for agent routing,
551
- pre-route auth, or custom response/error handling.
552
-
553
- ```ts
554
- import type { RuntimeDefinition } from '@kuratchi/js';
555
-
556
- const runtime: RuntimeDefinition = {
557
- agents: {
558
- async request(ctx, next) {
559
- if (!ctx.url.pathname.startsWith('/agents/')) {
560
- return next();
561
- }
562
-
563
- return new Response('Agent response');
564
- },
565
- },
566
- };
567
-
568
- export default runtime;
569
- ```
570
-
571
- `ctx` includes:
572
-
573
- - `ctx.url` - parsed URL
574
- - `ctx.request` - raw Request
575
- - `ctx.env` - Cloudflare env bindings
576
- - `next()` - pass control to the next handler
577
-
578
- ## Environment bindings
579
-
580
- Cloudflare env is server-only.
581
-
582
- - Route top-level `<script>`, route `load()` functions, server actions, API handlers, and other server modules can read env.
583
- - Templates, components, and client `<script>` blocks cannot read env directly.
584
- - 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.
585
-
586
- ```html
587
- <script>
588
- import { env } from 'cloudflare:workers';
589
- const turnstileSiteKey = env.TURNSTILE_SITE_KEY || '';
590
- </script>
591
-
592
- if (turnstileSiteKey) {
593
- <div class="cf-turnstile" data-sitekey={turnstileSiteKey}></div>
594
- }
595
- ```
596
-
597
- Server modules can still access env directly:
598
-
599
- ```ts
600
- import { env } from 'cloudflare:workers';
601
-
602
- const result = await env.DB.prepare('SELECT 1').run();
603
- ```
604
-
605
- ## Framework environment
606
-
607
- Kuratchi also exposes a framework build-mode flag:
608
-
609
- ```html
610
- <script>
611
- import { dev } from '@kuratchi/js/environment';
612
- import { env } from 'cloudflare:workers';
613
-
614
- const turnstileSiteKey = dev ? '' : (env.TURNSTILE_SITE_KEY || '');
615
- </script>
616
- ```
617
-
618
- - `dev` is `true` for Kuratchi development builds
619
- - `dev` is `false` for production builds
620
- - `dev` is compile-time framework state, not a generic process env var
621
- - `@kuratchi/js/environment` is intended for server route code, not client `$:` scripts
622
-
623
- ## `kuratchi.config.ts`
624
-
625
- Optional. Required only when using framework integrations or Durable Objects.
626
-
627
- ```ts
628
- import { defineConfig } from '@kuratchi/js';
629
- import { kuratchiUiConfig } from '@kuratchi/ui/adapter';
630
- import { kuratchiOrmConfig } from '@kuratchi/orm/adapter';
631
- import { kuratchiAuthConfig } from '@kuratchi/auth/adapter';
632
-
633
- export default defineConfig({
634
- ui: kuratchiUiConfig({ theme: 'default' }),
635
- orm: kuratchiOrmConfig({
636
- databases: {
637
- DB: { schema: appSchema },
638
- NOTES_DO: { schema: notesSchema, type: 'do' },
639
- },
640
- }),
641
- durableObjects: {
642
- NOTES_DO: {
643
- className: 'NotesDO',
644
- files: ['notes.do.ts'],
645
- },
646
- },
647
- auth: kuratchiAuthConfig({
648
- cookieName: 'kuratchi_session',
649
- sessionEnabled: true,
650
- }),
651
- });
652
- ```
653
-
654
- Without `kuratchi.config.ts` the compiler falls back to defaults — just drop your route files in `src/routes/` and run `kuratchi build`.
655
-
656
- ## CLI
657
-
658
- ```bash
659
- npx kuratchi build # one-shot build
660
- npx kuratchi watch # watch mode (for use with wrangler dev)
661
- ```
662
-
663
- ## Testing the Framework
664
-
665
- Run framework tests from `packages/kuratchi-js`:
666
-
667
- ```bash
668
- bun run test
669
- ```
670
-
671
- Watch mode:
672
-
673
- ```bash
674
- bun run test:watch
675
- ```
676
-
677
- ## TypeScript & Worker types
678
-
679
- ```bash
680
- npx wrangler types
681
- ```
682
-
683
- Then include the generated types in `tsconfig.json`:
684
-
685
- ```json
686
- {
687
- "compilerOptions": {
688
- "types": ["./worker-configuration.d.ts"]
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
+ ```