@kuratchi/js 0.0.13 → 0.0.14

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,770 @@
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
+ 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
+
513
+ ## Workflows
514
+
515
+ Kuratchi auto-discovers `.workflow.ts` files in `src/server/`. **No config needed.**
516
+
517
+ ```ts
518
+ // src/server/migration.workflow.ts
519
+ import { WorkflowEntrypoint, WorkflowStep } from 'cloudflare:workers';
520
+ import type { WorkflowEvent } from 'cloudflare:workers';
521
+
522
+ export class MigrationWorkflow extends WorkflowEntrypoint<Env, MigrationParams> {
523
+ async run(event: WorkflowEvent<MigrationParams>, step: WorkflowStep) {
524
+ // workflow steps...
525
+ }
526
+ }
527
+ ```
528
+
529
+ On build, Kuratchi:
530
+ 1. Scans `src/server/` for `.workflow.ts` files
531
+ 2. Derives binding from filename: `migration.workflow.ts` → `MIGRATION_WORKFLOW`
532
+ 3. Infers class name from the exported class
533
+ 4. Auto-adds/updates the workflow entry in `wrangler.jsonc`
534
+
535
+ **Zero config required.** Just create the file and the framework handles everything:
536
+ - `name`: derived from binding (e.g., `MIGRATION_WORKFLOW` → `migration-workflow`)
537
+ - `binding`: derived from filename (e.g., `migration.workflow.ts` → `MIGRATION_WORKFLOW`)
538
+ - `class_name`: inferred from the exported class
539
+
540
+ Examples:
541
+ - `migration.workflow.ts` `MIGRATION_WORKFLOW` binding
542
+ - `bond.workflow.ts` `BOND_WORKFLOW` binding
543
+ - `new-site.workflow.ts` `NEW_SITE_WORKFLOW` binding
544
+
545
+ ## Containers
546
+
547
+ Kuratchi auto-discovers `.container.ts` files in `src/server/`. **No config needed.**
548
+
549
+ ```ts
550
+ // src/server/wordpress.container.ts
551
+ import { Container } from 'cloudflare:workers';
552
+
553
+ export class WordPressContainer extends Container {
554
+ // container implementation...
555
+ }
556
+ ```
557
+
558
+ On build, Kuratchi derives the binding from the filename:
559
+ - `wordpress.container.ts` → `WORDPRESS_CONTAINER` binding
560
+ - `redis.container.ts` → `REDIS_CONTAINER` binding
561
+
562
+ ## Convention-Based Auto-Discovery
563
+
564
+ Kuratchi uses file suffixes to auto-discover and register worker classes. **No config needed** — just create the file:
565
+
566
+ | Suffix | Location | Binding Pattern | Example |
567
+ |--------|----------|-----------------|---------|
568
+ | `.workflow.ts` | `src/server/**/*.workflow.ts` | `FILENAME_WORKFLOW` | `migration.workflow.ts` → `MIGRATION_WORKFLOW` |
569
+ | `.container.ts` | `src/server/**/*.container.ts` | `FILENAME_CONTAINER` | `wordpress.container.ts` → `WORDPRESS_CONTAINER` |
570
+ | `.agent.ts` | `src/server/**/*.agent.ts` | (manual wrangler config) | `session.agent.ts` |
571
+ | `.do.ts` | `src/server/**/*.do.ts` | (via `durableObjects` config) | `auth.do.ts` |
572
+
573
+ ## Automatic Wrangler Config Sync
574
+
575
+ Kuratchi automatically syncs `wrangler.jsonc` during every build. This eliminates duplicate configuration for:
576
+
577
+ - **Workflows** — auto-discovered from `.workflow.ts` files
578
+ - **Containers** — auto-discovered from `.container.ts` files
579
+ - **Durable Objects** — `durableObjects` in kuratchi.config.ts
580
+
581
+ The sync is additive and non-destructive:
582
+ - New entries are added automatically
583
+ - Existing entries are updated if the class name changes
584
+ - Manually-added wrangler config (D1, KV, R2, vars, etc.) is preserved
585
+ - Removed entries are cleaned up from wrangler.jsonc
586
+
587
+ Requirements:
588
+ - Uses `wrangler.jsonc` or `wrangler.json` (TOML is not supported for auto-sync)
589
+ - Creates `wrangler.jsonc` if no wrangler config exists
590
+
591
+ ## Runtime APIs
592
+
593
+ These are available anywhere in server-side route code:
594
+
595
+ ```ts
596
+ import {
597
+ getCtx, // ExecutionContext
598
+ getRequest, // Request
599
+ getLocals, // mutable locals bag for the current request
600
+ getParams, // URL params ({ slug: 'foo' })
601
+ getParam, // getParam('slug')
602
+ RedirectError, // redirect signal thrown by redirect()\r\n redirect, // redirect('/path', 302)\r\n goto, // same as redirect()
603
+ goto, // same as redirect — alias
604
+ } from '@kuratchi/js';
605
+ ```
606
+
607
+ ### Request helpers
608
+
609
+ For a batteries-included request layer, import pre-parsed request state from `@kuratchi/js/request`:
610
+
611
+ ```ts
612
+ import { url, pathname, searchParams, slug } from '@kuratchi/js/request';
613
+
614
+ const page = pathname;
615
+ const tab = searchParams.get('tab');
616
+ const postSlug = slug;
617
+ ```
618
+
619
+ - `url` is the parsed `URL` for the current request.
620
+ - `pathname` is the full path, like `/blog/hello-world`.
621
+ - `searchParams` is `url.searchParams` for the current request.
622
+ - `slug` is `params.slug` when the matched route defines a `slug` param.
623
+ - `headers`, `method`, and `params` are also exported from `@kuratchi/js/request`.
624
+ - Use `getRequest()` when you want the raw native `Request` object.
625
+
626
+ ## Runtime Hook
627
+
628
+ Optional server runtime hook file. Export a `RuntimeDefinition` from `src/server/runtime.hook.ts`
629
+ to intercept requests before they reach the framework router. Use it for agent routing,
630
+ pre-route auth, or custom response/error handling.
631
+
632
+ ```ts
633
+ import type { RuntimeDefinition } from '@kuratchi/js';
634
+
635
+ const runtime: RuntimeDefinition = {
636
+ agents: {
637
+ async request(ctx, next) {
638
+ if (!ctx.url.pathname.startsWith('/agents/')) {
639
+ return next();
640
+ }
641
+
642
+ return new Response('Agent response');
643
+ },
644
+ },
645
+ };
646
+
647
+ export default runtime;
648
+ ```
649
+
650
+ `ctx` includes:
651
+
652
+ - `ctx.url` - parsed URL
653
+ - `ctx.request` - raw Request
654
+ - `ctx.env` - Cloudflare env bindings
655
+ - `next()` - pass control to the next handler
656
+
657
+ ## Environment bindings
658
+
659
+ Cloudflare env is server-only.
660
+
661
+ - Route top-level `<script>`, route `load()` functions, server actions, API handlers, and other server modules can read env.
662
+ - Templates, components, and client `<script>` blocks cannot read env directly.
663
+ - 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.
664
+
665
+ ```html
666
+ <script>
667
+ import { env } from 'cloudflare:workers';
668
+ const turnstileSiteKey = env.TURNSTILE_SITE_KEY || '';
669
+ </script>
670
+
671
+ if (turnstileSiteKey) {
672
+ <div class="cf-turnstile" data-sitekey={turnstileSiteKey}></div>
673
+ }
674
+ ```
675
+
676
+ Server modules can still access env directly:
677
+
678
+ ```ts
679
+ import { env } from 'cloudflare:workers';
680
+
681
+ const result = await env.DB.prepare('SELECT 1').run();
682
+ ```
683
+
684
+ ## Framework environment
685
+
686
+ Kuratchi also exposes a framework build-mode flag:
687
+
688
+ ```html
689
+ <script>
690
+ import { dev } from '@kuratchi/js/environment';
691
+ import { env } from 'cloudflare:workers';
692
+
693
+ const turnstileSiteKey = dev ? '' : (env.TURNSTILE_SITE_KEY || '');
694
+ </script>
695
+ ```
696
+
697
+ - `dev` is `true` for Kuratchi development builds
698
+ - `dev` is `false` for production builds
699
+ - `dev` is compile-time framework state, not a generic process env var
700
+ - `@kuratchi/js/environment` is intended for server route code, not client `$:` scripts
701
+
702
+ ## `kuratchi.config.ts`
703
+
704
+ Optional. Required only when using framework integrations or Durable Objects.
705
+
706
+ ```ts
707
+ import { defineConfig } from '@kuratchi/js';
708
+ import { kuratchiUiConfig } from '@kuratchi/ui/adapter';
709
+ import { kuratchiOrmConfig } from '@kuratchi/orm/adapter';
710
+ import { kuratchiAuthConfig } from '@kuratchi/auth/adapter';
711
+
712
+ export default defineConfig({
713
+ ui: kuratchiUiConfig({ theme: 'default' }),
714
+ orm: kuratchiOrmConfig({
715
+ databases: {
716
+ DB: { schema: appSchema },
717
+ NOTES_DO: { schema: notesSchema, type: 'do' },
718
+ },
719
+ }),
720
+ durableObjects: {
721
+ NOTES_DO: {
722
+ className: 'NotesDO',
723
+ files: ['notes.do.ts'],
724
+ },
725
+ },
726
+ auth: kuratchiAuthConfig({
727
+ cookieName: 'kuratchi_session',
728
+ sessionEnabled: true,
729
+ }),
730
+ });
731
+ ```
732
+
733
+ Without `kuratchi.config.ts` the compiler falls back to defaults — just drop your route files in `src/routes/` and run `kuratchi build`.
734
+
735
+ ## CLI
736
+
737
+ ```bash
738
+ npx kuratchi build # one-shot build
739
+ npx kuratchi watch # watch mode (for use with wrangler dev)
740
+ ```
741
+
742
+ ## Testing the Framework
743
+
744
+ Run framework tests from `packages/kuratchi-js`:
745
+
746
+ ```bash
747
+ bun run test
748
+ ```
749
+
750
+ Watch mode:
751
+
752
+ ```bash
753
+ bun run test:watch
754
+ ```
755
+
756
+ ## TypeScript & Worker types
757
+
758
+ ```bash
759
+ npx wrangler types
760
+ ```
761
+
762
+ Then include the generated types in `tsconfig.json`:
763
+
764
+ ```json
765
+ {
766
+ "compilerOptions": {
767
+ "types": ["./worker-configuration.d.ts"]
768
+ }
769
+ }
770
+ ```