@kuratchi/js 0.0.3 → 0.0.4
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 +206 -47
- package/dist/cli.js +59 -17
- package/dist/compiler/index.d.ts +2 -2
- package/dist/compiler/index.js +614 -254
- package/dist/compiler/parser.d.ts +8 -3
- package/dist/compiler/parser.js +29 -7
- package/dist/compiler/template.d.ts +3 -1
- package/dist/compiler/template.js +213 -14
- package/dist/create.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/runtime/action.d.ts +11 -0
- package/dist/runtime/action.js +14 -0
- package/dist/runtime/app.js +35 -8
- package/dist/runtime/context.d.ts +8 -2
- package/dist/runtime/context.js +31 -1
- package/dist/runtime/do.d.ts +6 -0
- package/dist/runtime/do.js +15 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/page-error.d.ts +16 -0
- package/dist/runtime/page-error.js +20 -0
- package/dist/runtime/types.d.ts +53 -33
- package/package.json +47 -45
package/README.md
CHANGED
|
@@ -18,12 +18,13 @@ bun run dev
|
|
|
18
18
|
|
|
19
19
|
## How it works
|
|
20
20
|
|
|
21
|
-
`kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates
|
|
21
|
+
`kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
|
|
22
22
|
|
|
23
23
|
| File | Purpose |
|
|
24
24
|
|---|---|
|
|
25
25
|
| `.kuratchi/routes.js` | Compiled routes, actions, RPC handlers, and render functions |
|
|
26
|
-
| `.kuratchi/worker.js` | Stable wrangler entry
|
|
26
|
+
| `.kuratchi/worker.js` | Stable wrangler entry - re-exports the fetch handler and all Durable Object classes |
|
|
27
|
+
| `.kuratchi/do/*.js` | Generated Durable Object RPC proxy modules for `$durable-objects/*` imports |
|
|
27
28
|
|
|
28
29
|
Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
|
|
29
30
|
|
|
@@ -95,7 +96,8 @@ The `$database/` alias resolves to `src/database/`. You can use any path alias c
|
|
|
95
96
|
|
|
96
97
|
```html
|
|
97
98
|
<p>{title}</p>
|
|
98
|
-
<p>{
|
|
99
|
+
<p>{@html bodyHtml}</p> <!-- sanitized HTML -->
|
|
100
|
+
<p>{@raw trustedHtml}</p> <!-- unescaped, unsafe -->
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
### Conditionals
|
|
@@ -131,6 +133,40 @@ Import `.html` components from your `src/lib/` directory or from packages:
|
|
|
131
133
|
</Card>
|
|
132
134
|
```
|
|
133
135
|
|
|
136
|
+
### Client Reactivity (`$:`)
|
|
137
|
+
|
|
138
|
+
Inside client/browser `<script>` tags in the template markup, Kuratchi supports Svelte-style reactive labels:
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<script>
|
|
142
|
+
let users = ['Alice'];
|
|
143
|
+
|
|
144
|
+
$: console.log(`Users: ${users.length}`);
|
|
145
|
+
|
|
146
|
+
function addUser() {
|
|
147
|
+
users.push('Bob'); // reactive update, no reassignment required
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Block form is also supported:
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<script>
|
|
156
|
+
let form = { first: '', last: '' };
|
|
157
|
+
|
|
158
|
+
$: {
|
|
159
|
+
const fullName = `${form.first} ${form.last}`.trim();
|
|
160
|
+
console.log(fullName);
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Notes:
|
|
166
|
+
- This reactivity runs in browser scripts rendered in the template markup, not in the top server route `<script>` load/action block.
|
|
167
|
+
- Object/array `let` bindings are proxy-backed automatically when `$:` is used.
|
|
168
|
+
- `$: name = expr` works; when replacing proxy-backed values, the compiler preserves reactivity under the hood.
|
|
169
|
+
|
|
134
170
|
## Form actions
|
|
135
171
|
|
|
136
172
|
Export server functions from a route's `<script>` block and reference them with `action={fn}`. The compiler automatically registers them as dispatchable actions.
|
|
@@ -147,12 +183,15 @@ Export server functions from a route's `<script>` block and reference them with
|
|
|
147
183
|
</form>
|
|
148
184
|
```
|
|
149
185
|
|
|
150
|
-
The action function receives the raw `FormData
|
|
186
|
+
The action function receives the raw `FormData`. Throw `ActionError` to surface a message back to the form — see [Error handling](#error-handling).
|
|
151
187
|
|
|
152
188
|
```ts
|
|
153
189
|
// src/database/items.ts
|
|
190
|
+
import { ActionError } from '@kuratchi/js';
|
|
191
|
+
|
|
154
192
|
export async function addItem(formData: FormData): Promise<void> {
|
|
155
|
-
const title = formData.get('title') as string;
|
|
193
|
+
const title = (formData.get('title') as string)?.trim();
|
|
194
|
+
if (!title) throw new ActionError('Title is required');
|
|
156
195
|
// write to DB...
|
|
157
196
|
}
|
|
158
197
|
```
|
|
@@ -170,6 +209,85 @@ export async function createItem(formData: FormData): Promise<void> {
|
|
|
170
209
|
}
|
|
171
210
|
```
|
|
172
211
|
|
|
212
|
+
## Error handling
|
|
213
|
+
|
|
214
|
+
### Action errors
|
|
215
|
+
|
|
216
|
+
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.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { ActionError } from '@kuratchi/js';
|
|
220
|
+
|
|
221
|
+
export async function signIn(formData: FormData) {
|
|
222
|
+
const email = formData.get('email') as string;
|
|
223
|
+
const password = formData.get('password') as string;
|
|
224
|
+
|
|
225
|
+
if (!email || !password) throw new ActionError('Email and password are required');
|
|
226
|
+
|
|
227
|
+
const user = await db.findUser(email);
|
|
228
|
+
if (!user || !await verify(password, user.passwordHash)) {
|
|
229
|
+
throw new ActionError('Invalid credentials');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
In the template, the action's state object is available under its function name:
|
|
235
|
+
|
|
236
|
+
```html
|
|
237
|
+
<script>
|
|
238
|
+
import { signIn } from '$database/auth';
|
|
239
|
+
</script>
|
|
240
|
+
|
|
241
|
+
<form action={signIn}>
|
|
242
|
+
(signIn.error ? `<p class="error">${signIn.error}</p>` : '')
|
|
243
|
+
<input type="email" name="email" />
|
|
244
|
+
<input type="password" name="password" />
|
|
245
|
+
<button type="submit">Sign in</button>
|
|
246
|
+
</form>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The state object shape: `{ error?: string, loading: boolean, success: boolean }`.
|
|
250
|
+
|
|
251
|
+
- `actionName.error` — set on `ActionError` throw, cleared on next successful action
|
|
252
|
+
- `actionName.loading` — set by the client bridge during form submission (CSS target: `form[data-action-loading]`)
|
|
253
|
+
- `actionName.success` — reserved for future use
|
|
254
|
+
|
|
255
|
+
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.
|
|
256
|
+
|
|
257
|
+
### Load errors
|
|
258
|
+
|
|
259
|
+
Throw `PageError` from a route's load scope to return the correct HTTP error page. Without it, any thrown error becomes a 500.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { PageError } from '@kuratchi/js';
|
|
263
|
+
|
|
264
|
+
// In src/routes/posts/[id]/page.html <script> block:
|
|
265
|
+
const post = await db.posts.findOne({ id: params.id });
|
|
266
|
+
if (!post) throw new PageError(404);
|
|
267
|
+
if (!post.isPublished && !currentUser?.isAdmin) throw new PageError(403);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`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.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
throw new PageError(404); // → 404 page
|
|
274
|
+
throw new PageError(403, 'Admin only'); // → 403 page, message shown in dev
|
|
275
|
+
throw new PageError(401, 'Login required'); // → 401 page
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
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:
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<script>
|
|
282
|
+
const { data: recommendations, error: recError } = await safeGetRecommendations();
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
(recError ? '<p class="notice">Could not load recommendations.</p>' : '')
|
|
286
|
+
for (const rec of (recommendations ?? [])) {
|
|
287
|
+
<article>{rec.title}</article>
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
173
291
|
## Progressive enhancement
|
|
174
292
|
|
|
175
293
|
These `data-*` attributes wire up client-side interactivity without writing JavaScript.
|
|
@@ -239,53 +357,93 @@ for (const todo of todos) {
|
|
|
239
357
|
|
|
240
358
|
## RPC
|
|
241
359
|
|
|
242
|
-
|
|
360
|
+
For Durable Objects, RPC is file-driven and automatic.
|
|
361
|
+
|
|
362
|
+
- Put handler logic in a `.do.ts` file.
|
|
363
|
+
- Exported functions in that file become RPC methods.
|
|
364
|
+
- Import RPC methods from `$durable-objects/<file-name-without-.do>`.
|
|
243
365
|
|
|
244
366
|
```html
|
|
245
367
|
<script>
|
|
246
|
-
import {
|
|
247
|
-
|
|
248
|
-
export const rpc = {
|
|
249
|
-
getCount,
|
|
250
|
-
};
|
|
368
|
+
import { getOrgUsers, createOrgUser } from '$durable-objects/auth';
|
|
369
|
+
const users = await getOrgUsers();
|
|
251
370
|
</script>
|
|
371
|
+
|
|
372
|
+
<form action={createOrgUser} method="POST">
|
|
373
|
+
<input type="email" name="email" required />
|
|
374
|
+
<button type="submit">Create</button>
|
|
375
|
+
</form>
|
|
252
376
|
```
|
|
377
|
+
## Durable Objects
|
|
253
378
|
|
|
254
|
-
|
|
379
|
+
Durable Object behavior is enabled by filename suffix.
|
|
255
380
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
381
|
+
- Any file ending in `.do.ts` is treated as a Durable Object handler file.
|
|
382
|
+
- Any file not ending in `.do.ts` is treated as a normal server module.
|
|
383
|
+
- No required folder name. `src/server/auth.do.ts`, `src/server/foo/bar/sites.do.ts`, etc. all work.
|
|
384
|
+
|
|
385
|
+
### Function mode (recommended)
|
|
386
|
+
|
|
387
|
+
Write plain exported functions in a `.do.ts` file. Exported functions become DO RPC methods.
|
|
388
|
+
Use `this.db`, `this.env`, and `this.ctx` inside those functions.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
// src/server/auth/auth.do.ts
|
|
392
|
+
import { getCurrentUser, hashPassword } from '@kuratchi/auth';
|
|
393
|
+
import { redirect } from '@kuratchi/js';
|
|
394
|
+
|
|
395
|
+
async function randomPassword(length = 24): Promise<string> {
|
|
396
|
+
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
|
|
397
|
+
const bytes = new Uint8Array(length);
|
|
398
|
+
crypto.getRandomValues(bytes);
|
|
399
|
+
let out = '';
|
|
400
|
+
for (let i = 0; i < length; i++) out += alphabet[bytes[i] % alphabet.length];
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export async function getOrgUsers() {
|
|
405
|
+
const result = await this.db.users.orderBy({ createdAt: 'asc' }).many();
|
|
406
|
+
return result.data ?? [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function createOrgUser(formData: FormData) {
|
|
410
|
+
const user = await getCurrentUser();
|
|
411
|
+
if (!user?.orgId) throw new Error('Not authenticated');
|
|
412
|
+
|
|
413
|
+
const email = String(formData.get('email') ?? '').trim().toLowerCase();
|
|
414
|
+
if (!email) throw new Error('Email is required');
|
|
415
|
+
|
|
416
|
+
const passwordHash = await hashPassword(await randomPassword(), undefined, this.env.AUTH_SECRET);
|
|
417
|
+
await this.db.users.insert({ email, role: 'member', passwordHash });
|
|
418
|
+
redirect('/settings/users');
|
|
419
|
+
}
|
|
260
420
|
```
|
|
261
421
|
|
|
262
|
-
|
|
422
|
+
Optional lifecycle exports in function mode:
|
|
263
423
|
|
|
264
|
-
|
|
424
|
+
- `export async function onInit()`
|
|
425
|
+
- `export async function onAlarm(...args)`
|
|
426
|
+
- `export function onMessage(...args)`
|
|
427
|
+
|
|
428
|
+
These lifecycle names are not exposed as RPC methods.
|
|
429
|
+
|
|
430
|
+
### Class mode (optional)
|
|
431
|
+
|
|
432
|
+
Class-based handlers are still supported in `.do.ts` files:
|
|
265
433
|
|
|
266
434
|
```ts
|
|
267
|
-
// src/server/notes.do.ts
|
|
268
435
|
import { kuratchiDO } from '@kuratchi/js';
|
|
269
|
-
import type { Note } from '../schemas/notes';
|
|
270
436
|
|
|
271
437
|
export default class NotesDO extends kuratchiDO {
|
|
272
438
|
static binding = 'NOTES_DO';
|
|
273
439
|
|
|
274
|
-
async getNotes()
|
|
440
|
+
async getNotes() {
|
|
275
441
|
return (await this.db.notes.orderBy({ created_at: 'desc' }).many()).data ?? [];
|
|
276
442
|
}
|
|
277
|
-
|
|
278
|
-
async addNote(title: string): Promise<void> {
|
|
279
|
-
await this.db.notes.insert({ title });
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async deleteNote(id: number): Promise<void> {
|
|
283
|
-
await this.db.notes.delete({ id });
|
|
284
|
-
}
|
|
285
443
|
}
|
|
286
444
|
```
|
|
287
445
|
|
|
288
|
-
Declare it in `kuratchi.config.ts` and in `wrangler.jsonc`. The compiler exports
|
|
446
|
+
Declare it in `kuratchi.config.ts` and in `wrangler.jsonc`. The compiler exports DO classes from `.kuratchi/worker.js` automatically.
|
|
289
447
|
|
|
290
448
|
```jsonc
|
|
291
449
|
// wrangler.jsonc
|
|
@@ -298,22 +456,6 @@ Declare it in `kuratchi.config.ts` and in `wrangler.jsonc`. The compiler exports
|
|
|
298
456
|
]
|
|
299
457
|
}
|
|
300
458
|
```
|
|
301
|
-
|
|
302
|
-
Call DO methods from your route via a stub module in `src/database/`:
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
// src/database/notes.ts
|
|
306
|
-
import { env } from 'cloudflare:workers';
|
|
307
|
-
|
|
308
|
-
function getStub() {
|
|
309
|
-
return (env as any).NOTES_DO.get((env as any).NOTES_DO.idFromName('global'));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export const getNotes = () => getStub().getNotes();
|
|
313
|
-
export const addNote = (title: string) => getStub().addNote(title);
|
|
314
|
-
export const deleteNote = (id: number) => getStub().deleteNote(id);
|
|
315
|
-
```
|
|
316
|
-
|
|
317
459
|
## Runtime APIs
|
|
318
460
|
|
|
319
461
|
These are available anywhere in server-side route code:
|
|
@@ -357,7 +499,10 @@ export default defineConfig({
|
|
|
357
499
|
},
|
|
358
500
|
}),
|
|
359
501
|
durableObjects: {
|
|
360
|
-
NOTES_DO: {
|
|
502
|
+
NOTES_DO: {
|
|
503
|
+
className: 'NotesDO',
|
|
504
|
+
files: ['notes.do.ts'],
|
|
505
|
+
},
|
|
361
506
|
},
|
|
362
507
|
auth: kuratchiAuthConfig({
|
|
363
508
|
cookieName: 'kuratchi_session',
|
|
@@ -375,6 +520,20 @@ npx kuratchi build # one-shot build
|
|
|
375
520
|
npx kuratchi watch # watch mode (for use with wrangler dev)
|
|
376
521
|
```
|
|
377
522
|
|
|
523
|
+
## Testing the Framework
|
|
524
|
+
|
|
525
|
+
Run framework tests from `packages/kuratchi-js`:
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
bun run test
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Watch mode:
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
bun run test:watch
|
|
535
|
+
```
|
|
536
|
+
|
|
378
537
|
## TypeScript & Worker types
|
|
379
538
|
|
|
380
539
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { compile } from './compiler/index.js';
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
|
+
import * as net from 'node:net';
|
|
8
9
|
import { spawn } from 'node:child_process';
|
|
9
10
|
const args = process.argv.slice(2);
|
|
10
11
|
const command = args[0];
|
|
@@ -79,24 +80,65 @@ function runWatch(withWrangler = false) {
|
|
|
79
80
|
// `kuratchi dev` also starts the wrangler dev server.
|
|
80
81
|
// `kuratchi watch` is the compiler-only mode for custom setups.
|
|
81
82
|
if (withWrangler) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
stdio: 'inherit',
|
|
86
|
-
shell: process.platform === 'win32',
|
|
83
|
+
startWranglerDev().catch((err) => {
|
|
84
|
+
console.error(`[kuratchi] Failed to start wrangler dev: ${err?.message ?? err}`);
|
|
85
|
+
process.exit(1);
|
|
87
86
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function hasPortFlag(inputArgs) {
|
|
90
|
+
for (let i = 0; i < inputArgs.length; i++) {
|
|
91
|
+
const arg = inputArgs[i];
|
|
92
|
+
if (arg === '--port' || arg === '-p')
|
|
93
|
+
return true;
|
|
94
|
+
if (arg.startsWith('--port='))
|
|
95
|
+
return true;
|
|
96
|
+
if (arg.startsWith('-p='))
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
function isPortAvailable(port) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const server = net.createServer();
|
|
104
|
+
server.once('error', () => resolve(false));
|
|
105
|
+
server.once('listening', () => {
|
|
106
|
+
server.close(() => resolve(true));
|
|
100
107
|
});
|
|
108
|
+
server.listen(port, '127.0.0.1');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function findOpenPort(start = 8787, end = 8899) {
|
|
112
|
+
for (let port = start; port <= end; port++) {
|
|
113
|
+
if (await isPortAvailable(port))
|
|
114
|
+
return port;
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`No open dev port found in range ${start}-${end}`);
|
|
117
|
+
}
|
|
118
|
+
async function startWranglerDev() {
|
|
119
|
+
const passthroughArgs = args.slice(1);
|
|
120
|
+
const wranglerArgs = ['wrangler', 'dev', ...passthroughArgs];
|
|
121
|
+
if (!hasPortFlag(passthroughArgs)) {
|
|
122
|
+
const port = await findOpenPort();
|
|
123
|
+
wranglerArgs.push('--port', String(port));
|
|
124
|
+
console.log(`[kuratchi] Starting wrangler dev on port ${port}`);
|
|
101
125
|
}
|
|
126
|
+
const wrangler = spawn('npx', wranglerArgs, {
|
|
127
|
+
cwd: projectDir,
|
|
128
|
+
stdio: 'inherit',
|
|
129
|
+
shell: process.platform === 'win32',
|
|
130
|
+
});
|
|
131
|
+
const cleanup = () => {
|
|
132
|
+
if (!wrangler.killed)
|
|
133
|
+
wrangler.kill();
|
|
134
|
+
};
|
|
135
|
+
process.on('exit', cleanup);
|
|
136
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
137
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
138
|
+
wrangler.on('exit', (code) => {
|
|
139
|
+
if (code !== 0 && code !== null) {
|
|
140
|
+
console.error(`[kuratchi] wrangler exited with code ${code}`);
|
|
141
|
+
}
|
|
142
|
+
process.exit(code ?? 0);
|
|
143
|
+
});
|
|
102
144
|
}
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compiler
|
|
2
|
+
* Compiler â€" scans a project's routes/ directory, parses .html files,
|
|
3
3
|
* and generates a single Worker entry point.
|
|
4
4
|
*/
|
|
5
5
|
export { parseFile } from './parser.js';
|
|
@@ -27,7 +27,7 @@ export interface CompiledRoute {
|
|
|
27
27
|
/**
|
|
28
28
|
* Compile a project's src/routes/ into .kuratchi/routes.js
|
|
29
29
|
*
|
|
30
|
-
* The generated module exports { app }
|
|
30
|
+
* The generated module exports { app } â€" an object with a fetch() method
|
|
31
31
|
* that handles routing, load functions, form actions, and rendering.
|
|
32
32
|
* Returns the path to .kuratchi/worker.js — the stable wrangler entry point that
|
|
33
33
|
* re-exports everything from routes.js (default fetch handler + named DO class exports).
|