@kuratchi/js 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +392 -29
- package/dist/cli.js +32 -8
- package/dist/compiler/index.d.ts +4 -2
- package/dist/compiler/index.js +30 -15
- package/dist/create.js +156 -18
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -3
- package/dist/runtime/context.d.ts +0 -7
- package/dist/runtime/context.js +0 -11
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/package.json +2 -6
package/README.md
CHANGED
|
@@ -1,29 +1,392 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Cloudflare Workers-native web framework with
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @kuratchi/js
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx kuratchi create my-app
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 two files:
|
|
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 and all Durable Object classes |
|
|
27
|
+
|
|
28
|
+
Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
|
|
29
|
+
|
|
30
|
+
```jsonc
|
|
31
|
+
// wrangler.jsonc
|
|
32
|
+
{
|
|
33
|
+
"main": ".kuratchi/worker.js"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Routes
|
|
38
|
+
|
|
39
|
+
Place `.html` files inside `src/routes/`. The file path becomes the URL pattern.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
src/routes/page.html → /
|
|
43
|
+
src/routes/items/page.html → /items
|
|
44
|
+
src/routes/blog/[slug]/page.html → /blog/:slug
|
|
45
|
+
src/routes/layout.html → shared layout wrapping all routes
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Route file structure
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<script>
|
|
52
|
+
// Imports and server-side logic run on every request.
|
|
53
|
+
// Exported functions become actions or RPC handlers automatically.
|
|
54
|
+
import { getItems, addItem, deleteItem } from '$database/items';
|
|
55
|
+
|
|
56
|
+
const items = await getItems();
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<!-- Template — plain HTML with minimal extensions -->
|
|
60
|
+
<ul>
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
<li>{item.title}</li>
|
|
63
|
+
}
|
|
64
|
+
</ul>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `$database/` alias resolves to `src/database/`. You can use any path alias configured in your tsconfig.
|
|
68
|
+
|
|
69
|
+
### Layout file
|
|
70
|
+
|
|
71
|
+
`src/routes/layout.html` wraps every page. Use `<slot></slot>` where page content renders:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!DOCTYPE html>
|
|
75
|
+
<html lang="en">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="utf-8" />
|
|
78
|
+
<title>My App</title>
|
|
79
|
+
</head>
|
|
80
|
+
<body>
|
|
81
|
+
<nav>
|
|
82
|
+
<a href="/">Home</a>
|
|
83
|
+
<a href="/items">Items</a>
|
|
84
|
+
</nav>
|
|
85
|
+
<main>
|
|
86
|
+
<slot></slot>
|
|
87
|
+
</main>
|
|
88
|
+
</body>
|
|
89
|
+
</html>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Template syntax
|
|
93
|
+
|
|
94
|
+
### Interpolation
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<p>{title}</p>
|
|
98
|
+
<p>{=html rawHtml}</p> <!-- unescaped, use carefully -->
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Conditionals
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
if (items.length === 0) {
|
|
105
|
+
<p>Nothing here yet.</p>
|
|
106
|
+
} else {
|
|
107
|
+
<p>{items.length} items</p>
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Loops
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
<li>{item.title}</li>
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Components
|
|
120
|
+
|
|
121
|
+
Import `.html` components from your `src/lib/` directory or from packages:
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<script>
|
|
125
|
+
import Card from '$lib/card.html';
|
|
126
|
+
import Badge from '@kuratchi/ui/badge.html';
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<Card title="Stack">
|
|
130
|
+
<Badge variant="success">Live</Badge>
|
|
131
|
+
</Card>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Form actions
|
|
135
|
+
|
|
136
|
+
Export server functions from a route's `<script>` block and reference them with `action={fn}`. The compiler automatically registers them as dispatchable actions.
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<script>
|
|
140
|
+
import { addItem, deleteItem } from '$database/items';
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<!-- Standard form — POST-Redirect-GET -->
|
|
144
|
+
<form action={addItem} method="POST">
|
|
145
|
+
<input type="text" name="title" required />
|
|
146
|
+
<button type="submit">Add</button>
|
|
147
|
+
</form>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The action function receives the raw `FormData`:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// src/database/items.ts
|
|
154
|
+
export async function addItem(formData: FormData): Promise<void> {
|
|
155
|
+
const title = formData.get('title') as string;
|
|
156
|
+
// write to DB...
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Redirect after action
|
|
161
|
+
|
|
162
|
+
Call `redirect()` inside an action to send the user to a different URL after the POST:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { redirect } from '@kuratchi/js';
|
|
166
|
+
|
|
167
|
+
export async function createItem(formData: FormData): Promise<void> {
|
|
168
|
+
const id = await db.items.insert({ title: formData.get('title') });
|
|
169
|
+
redirect(`/items/${id}`);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Progressive enhancement
|
|
174
|
+
|
|
175
|
+
These `data-*` attributes wire up client-side interactivity without writing JavaScript.
|
|
176
|
+
|
|
177
|
+
### `data-action` — fetch action (no page reload)
|
|
178
|
+
|
|
179
|
+
Calls a server action via `fetch` and refreshes `data-refresh` targets when done:
|
|
180
|
+
|
|
181
|
+
```html
|
|
182
|
+
<button data-action="deleteItem" data-args={JSON.stringify([item.id])}>Delete</button>
|
|
183
|
+
<button data-action="toggleItem" data-args={JSON.stringify([item.id, true])}>Done</button>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The action function receives the args array as individual arguments:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
export async function deleteItem(id: number): Promise<void> {
|
|
190
|
+
await db.items.delete({ id });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function toggleItem(id: number, done: boolean): Promise<void> {
|
|
194
|
+
await db.items.update({ id }, { done });
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `data-refresh` — partial refresh
|
|
199
|
+
|
|
200
|
+
After a `data-action` call succeeds, elements with `data-refresh` re-fetch their content:
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<section data-refresh="/items">
|
|
204
|
+
for (const item of items) {
|
|
205
|
+
<article>{item.title}</article>
|
|
206
|
+
}
|
|
207
|
+
</section>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `data-get` — client-side navigation
|
|
211
|
+
|
|
212
|
+
Navigate to a URL on click (respects `http:`/`https:` only):
|
|
213
|
+
|
|
214
|
+
```html
|
|
215
|
+
<div data-get="/items/{item.id}">Click to navigate</div>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### `data-poll` — polling
|
|
219
|
+
|
|
220
|
+
Refresh a section automatically on an interval (milliseconds):
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<div data-refresh="/status" data-poll="3000">
|
|
224
|
+
{status}
|
|
225
|
+
</div>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `data-select-all` / `data-select-item` — checkbox groups
|
|
229
|
+
|
|
230
|
+
Sync a "select all" checkbox with a group of item checkboxes:
|
|
231
|
+
|
|
232
|
+
```html
|
|
233
|
+
<input type="checkbox" data-select-all="todos" />
|
|
234
|
+
|
|
235
|
+
for (const todo of todos) {
|
|
236
|
+
<input type="checkbox" data-select-item="todos" value={todo.id} />
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## RPC
|
|
241
|
+
|
|
242
|
+
Export an `rpc` object from a route to expose server functions callable from client-side JavaScript. RPC names are replaced with opaque IDs at compile time — real function names are never exposed to the client.
|
|
243
|
+
|
|
244
|
+
```html
|
|
245
|
+
<script>
|
|
246
|
+
import { getCount } from '$database/items';
|
|
247
|
+
|
|
248
|
+
export const rpc = {
|
|
249
|
+
getCount,
|
|
250
|
+
};
|
|
251
|
+
</script>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Call from the client using the generated `rpc` helper:
|
|
255
|
+
|
|
256
|
+
```html
|
|
257
|
+
<script client>
|
|
258
|
+
const result = await rpc.getCount();
|
|
259
|
+
</script>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Durable Objects
|
|
263
|
+
|
|
264
|
+
Extend `kuratchiDO` to create a Durable Object class backed by SQLite. The `static binding` name must match the binding in `wrangler.jsonc`.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// src/server/notes.do.ts
|
|
268
|
+
import { kuratchiDO } from '@kuratchi/js';
|
|
269
|
+
import type { Note } from '../schemas/notes';
|
|
270
|
+
|
|
271
|
+
export default class NotesDO extends kuratchiDO {
|
|
272
|
+
static binding = 'NOTES_DO';
|
|
273
|
+
|
|
274
|
+
async getNotes(): Promise<Note[]> {
|
|
275
|
+
return (await this.db.notes.orderBy({ created_at: 'desc' }).many()).data ?? [];
|
|
276
|
+
}
|
|
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
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Declare it in `kuratchi.config.ts` and in `wrangler.jsonc`. The compiler exports it from `.kuratchi/worker.js` automatically.
|
|
289
|
+
|
|
290
|
+
```jsonc
|
|
291
|
+
// wrangler.jsonc
|
|
292
|
+
{
|
|
293
|
+
"durable_objects": {
|
|
294
|
+
"bindings": [{ "name": "NOTES_DO", "class_name": "NotesDO" }]
|
|
295
|
+
},
|
|
296
|
+
"migrations": [
|
|
297
|
+
{ "tag": "v1", "new_sqlite_classes": ["NotesDO"] }
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
```
|
|
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
|
+
## Runtime APIs
|
|
318
|
+
|
|
319
|
+
These are available anywhere in server-side route code:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import {
|
|
323
|
+
getCtx, // ExecutionContext
|
|
324
|
+
getRequest, // Request
|
|
325
|
+
getLocals, // mutable locals bag for the current request
|
|
326
|
+
getParams, // URL params ({ slug: 'foo' })
|
|
327
|
+
getParam, // getParam('slug')
|
|
328
|
+
redirect, // redirect('/path', 302)
|
|
329
|
+
goto, // same as redirect — alias
|
|
330
|
+
} from '@kuratchi/js';
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Environment bindings are accessed directly via the Cloudflare Workers API — no framework wrapper needed:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { env } from 'cloudflare:workers';
|
|
337
|
+
|
|
338
|
+
const result = await env.DB.prepare('SELECT 1').run();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## `kuratchi.config.ts`
|
|
342
|
+
|
|
343
|
+
Optional. Required only when using framework integrations or Durable Objects.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { defineConfig } from '@kuratchi/js';
|
|
347
|
+
import { kuratchiUiConfig } from '@kuratchi/ui/adapter';
|
|
348
|
+
import { kuratchiOrmConfig } from '@kuratchi/orm/adapter';
|
|
349
|
+
import { kuratchiAuthConfig } from '@kuratchi/auth/adapter';
|
|
350
|
+
|
|
351
|
+
export default defineConfig({
|
|
352
|
+
ui: kuratchiUiConfig({ theme: 'default' }),
|
|
353
|
+
orm: kuratchiOrmConfig({
|
|
354
|
+
databases: {
|
|
355
|
+
DB: { schema: appSchema },
|
|
356
|
+
NOTES_DO: { schema: notesSchema, type: 'do' },
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
durableObjects: {
|
|
360
|
+
NOTES_DO: { className: 'NotesDO' },
|
|
361
|
+
},
|
|
362
|
+
auth: kuratchiAuthConfig({
|
|
363
|
+
cookieName: 'kuratchi_session',
|
|
364
|
+
sessionEnabled: true,
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Without `kuratchi.config.ts` the compiler falls back to defaults — just drop your route files in `src/routes/` and run `kuratchi build`.
|
|
370
|
+
|
|
371
|
+
## CLI
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
npx kuratchi build # one-shot build
|
|
375
|
+
npx kuratchi watch # watch mode (for use with wrangler dev)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## TypeScript & Worker types
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npx wrangler types
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Then include the generated types in `tsconfig.json`:
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
{
|
|
388
|
+
"compilerOptions": {
|
|
389
|
+
"types": ["./worker-configuration.d.ts"]
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
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 { spawn } from 'node:child_process';
|
|
8
9
|
const args = process.argv.slice(2);
|
|
9
10
|
const command = args[0];
|
|
10
11
|
const projectDir = process.cwd();
|
|
@@ -13,8 +14,10 @@ switch (command) {
|
|
|
13
14
|
runBuild();
|
|
14
15
|
break;
|
|
15
16
|
case 'watch':
|
|
17
|
+
runWatch(false);
|
|
18
|
+
break;
|
|
16
19
|
case 'dev':
|
|
17
|
-
runWatch();
|
|
20
|
+
runWatch(true);
|
|
18
21
|
break;
|
|
19
22
|
case 'create':
|
|
20
23
|
runCreate();
|
|
@@ -26,7 +29,8 @@ KuratchiJS CLI
|
|
|
26
29
|
Usage:
|
|
27
30
|
kuratchi create [name] Scaffold a new KuratchiJS project
|
|
28
31
|
kuratchi build Compile routes once
|
|
29
|
-
kuratchi
|
|
32
|
+
kuratchi dev Compile, watch for changes, and start wrangler dev server
|
|
33
|
+
kuratchi watch Compile + watch only (no wrangler — for custom setups)
|
|
30
34
|
`);
|
|
31
35
|
process.exit(1);
|
|
32
36
|
}
|
|
@@ -48,11 +52,11 @@ function runBuild(isDev = false) {
|
|
|
48
52
|
process.exit(1);
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
function runWatch() {
|
|
55
|
+
function runWatch(withWrangler = false) {
|
|
52
56
|
runBuild(true);
|
|
53
57
|
const routesDir = path.join(projectDir, 'src', 'routes');
|
|
54
|
-
const
|
|
55
|
-
const watchDirs = [routesDir].filter(d => fs.existsSync(d));
|
|
58
|
+
const serverDir = path.join(projectDir, 'src', 'server');
|
|
59
|
+
const watchDirs = [routesDir, serverDir].filter(d => fs.existsSync(d));
|
|
56
60
|
let rebuildTimeout = null;
|
|
57
61
|
const triggerRebuild = () => {
|
|
58
62
|
if (rebuildTimeout)
|
|
@@ -71,8 +75,28 @@ function runWatch() {
|
|
|
71
75
|
for (const dir of watchDirs) {
|
|
72
76
|
fs.watch(dir, { recursive: true }, triggerRebuild);
|
|
73
77
|
}
|
|
74
|
-
if (fs.existsSync(layoutFile)) {
|
|
75
|
-
fs.watch(layoutFile, triggerRebuild);
|
|
76
|
-
}
|
|
77
78
|
console.log('[kuratchi] Watching for changes...');
|
|
79
|
+
// `kuratchi dev` also starts the wrangler dev server.
|
|
80
|
+
// `kuratchi watch` is the compiler-only mode for custom setups.
|
|
81
|
+
if (withWrangler) {
|
|
82
|
+
const wranglerArgs = ['wrangler', 'dev', '--port', '8787'];
|
|
83
|
+
const wrangler = spawn('npx', wranglerArgs, {
|
|
84
|
+
cwd: projectDir,
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
shell: process.platform === 'win32',
|
|
87
|
+
});
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
if (!wrangler.killed)
|
|
90
|
+
wrangler.kill();
|
|
91
|
+
};
|
|
92
|
+
process.on('exit', cleanup);
|
|
93
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
94
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
95
|
+
wrangler.on('exit', (code) => {
|
|
96
|
+
if (code !== 0 && code !== null) {
|
|
97
|
+
console.error(`[kuratchi] wrangler exited with code ${code}`);
|
|
98
|
+
}
|
|
99
|
+
process.exit(code ?? 0);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
78
102
|
}
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { compileTemplate, generateRenderFunction } from './template.js';
|
|
|
7
7
|
export interface CompileOptions {
|
|
8
8
|
/** Absolute path to the project root */
|
|
9
9
|
projectDir: string;
|
|
10
|
-
/**
|
|
10
|
+
/** Override path for routes.js (default: .kuratchi/routes.js). worker.js is always co-located. */
|
|
11
11
|
outFile?: string;
|
|
12
12
|
/** Whether this is a dev build (sets __kuratchi_DEV__ global) */
|
|
13
13
|
isDev?: boolean;
|
|
@@ -29,6 +29,8 @@ export interface CompiledRoute {
|
|
|
29
29
|
*
|
|
30
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
|
+
* re-exports everything from routes.js (default fetch handler + named DO class exports).
|
|
34
|
+
* No src/index.ts is needed in user projects.
|
|
33
35
|
*/
|
|
34
36
|
export declare function compile(options: CompileOptions): string;
|
package/dist/compiler/index.js
CHANGED
|
@@ -17,10 +17,10 @@ function getFrameworkPackageName() {
|
|
|
17
17
|
try {
|
|
18
18
|
const raw = fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf-8');
|
|
19
19
|
const parsed = JSON.parse(raw);
|
|
20
|
-
return parsed.name || '
|
|
20
|
+
return parsed.name || '@kuratchi/js';
|
|
21
21
|
}
|
|
22
22
|
catch {
|
|
23
|
-
return '
|
|
23
|
+
return '@kuratchi/js';
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
function compactInlineJs(source) {
|
|
@@ -36,7 +36,9 @@ function compactInlineJs(source) {
|
|
|
36
36
|
*
|
|
37
37
|
* The generated module exports { app } — an object with a fetch() method
|
|
38
38
|
* that handles routing, load functions, form actions, and rendering.
|
|
39
|
-
*
|
|
39
|
+
* Returns the path to .kuratchi/worker.js — the stable wrangler entry point that
|
|
40
|
+
* re-exports everything from routes.js (default fetch handler + named DO class exports).
|
|
41
|
+
* No src/index.ts is needed in user projects.
|
|
40
42
|
*/
|
|
41
43
|
export function compile(options) {
|
|
42
44
|
const { projectDir } = options;
|
|
@@ -182,8 +184,8 @@ export function compile(options) {
|
|
|
182
184
|
function by(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
|
|
183
185
|
var __refreshSeq = Object.create(null);
|
|
184
186
|
function syncGroup(group){
|
|
185
|
-
var items = by('[data-select-item
|
|
186
|
-
var masters = by('[data-select-all
|
|
187
|
+
var items = by('[data-select-item]').filter(function(el){ return el.getAttribute('data-select-item') === group; });
|
|
188
|
+
var masters = by('[data-select-all]').filter(function(el){ return el.getAttribute('data-select-all') === group; });
|
|
187
189
|
if(!items.length || !masters.length) return;
|
|
188
190
|
var all = items.every(function(i){ return !!i.checked; });
|
|
189
191
|
var any = items.some(function(i){ return !!i.checked; });
|
|
@@ -306,6 +308,7 @@ export function compile(options) {
|
|
|
306
308
|
if(g && !g.hasAttribute('data-as') && !g.hasAttribute('data-action')){
|
|
307
309
|
var getUrl = g.getAttribute('data-get');
|
|
308
310
|
if(getUrl){
|
|
311
|
+
if(/^[a-z][a-z0-9+\-.]*:/i.test(getUrl) && !/^https?:/i.test(getUrl)) return;
|
|
309
312
|
e.preventDefault();
|
|
310
313
|
location.assign(getUrl);
|
|
311
314
|
return;
|
|
@@ -446,7 +449,7 @@ export function compile(options) {
|
|
|
446
449
|
if(!t || !t.getAttribute) return;
|
|
447
450
|
var gAll = t.getAttribute('data-select-all');
|
|
448
451
|
if(gAll){
|
|
449
|
-
by('[data-select-item
|
|
452
|
+
by('[data-select-item]').filter(function(i){ return i.getAttribute('data-select-item') === gAll; }).forEach(function(i){ i.checked = !!t.checked; });
|
|
450
453
|
syncGroup(gAll);
|
|
451
454
|
return;
|
|
452
455
|
}
|
|
@@ -887,7 +890,19 @@ export function compile(options) {
|
|
|
887
890
|
fs.mkdirSync(outDir, { recursive: true });
|
|
888
891
|
}
|
|
889
892
|
writeIfChanged(outFile, output);
|
|
890
|
-
|
|
893
|
+
// Generate .kuratchi/worker.js — the stable wrangler entry point.
|
|
894
|
+
// routes.js already exports the default fetch handler and all named DO classes;
|
|
895
|
+
// worker.js explicitly re-exports them so wrangler.jsonc can reference a
|
|
896
|
+
// stable filename while routes.js is freely regenerated.
|
|
897
|
+
const workerFile = path.join(outDir, 'worker.js');
|
|
898
|
+
const workerLines = [
|
|
899
|
+
'// Auto-generated by kuratchi \u2014 do not edit.',
|
|
900
|
+
"export { default } from './routes.js';",
|
|
901
|
+
...doConfig.map(c => `export { ${c.className} } from './routes.js';`),
|
|
902
|
+
'',
|
|
903
|
+
];
|
|
904
|
+
writeIfChanged(workerFile, workerLines.join('\n'));
|
|
905
|
+
return workerFile;
|
|
891
906
|
}
|
|
892
907
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
893
908
|
/**
|
|
@@ -1127,7 +1142,7 @@ function buildRouteObject(opts) {
|
|
|
1127
1142
|
body = [body, queryLines.join('\n')].filter(Boolean).join('\n');
|
|
1128
1143
|
}
|
|
1129
1144
|
// Rewrite imported function calls: fnName( → __mN.fnName(
|
|
1130
|
-
//
|
|
1145
|
+
// Rewrite imported function calls to use the module namespace
|
|
1131
1146
|
for (const [fnName, moduleId] of Object.entries(fnToModule)) {
|
|
1132
1147
|
if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
|
|
1133
1148
|
continue;
|
|
@@ -1650,7 +1665,7 @@ function generateRoutesModule(opts) {
|
|
|
1650
1665
|
.map(([status, fn]) => fn)
|
|
1651
1666
|
.join('\n\n');
|
|
1652
1667
|
// Resolve path to the framework's context module from the output directory
|
|
1653
|
-
const contextImport = `import { __setRequestContext,
|
|
1668
|
+
const contextImport = `import { __setRequestContext, __esc, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
1654
1669
|
// Auth session init — thin cookie parsing injected into Worker entry
|
|
1655
1670
|
let authInit = '';
|
|
1656
1671
|
if (opts.authConfig && opts.authConfig.sessionEnabled) {
|
|
@@ -2065,7 +2080,6 @@ ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
|
|
|
2065
2080
|
export default class extends WorkerEntrypoint {
|
|
2066
2081
|
async fetch(request) {
|
|
2067
2082
|
__setRequestContext(this.ctx, request);
|
|
2068
|
-
__setEnvCompat(this.env);
|
|
2069
2083
|
${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
|
|
2070
2084
|
const url = new URL(request.url);
|
|
2071
2085
|
${ac?.hasRateLimit ? '\n // Rate limiting — check before route handlers\n { const __rlRes = await __checkRL(); if (__rlRes) return __secHeaders(__rlRes); }\n' : ''}${ac?.hasTurnstile ? ' // Turnstile bot protection\n { const __tsRes = await __checkTS(); if (__tsRes) return __secHeaders(__tsRes); }\n' : ''}${ac?.hasGuards ? ' // Route guards — redirect if not authenticated\n { const __gRes = __checkGuard(); if (__gRes) return __secHeaders(__gRes); }\n' : ''}
|
|
@@ -2106,8 +2120,8 @@ ${ac?.hasRateLimit ? '\n // Rate limiting — check before route handler
|
|
|
2106
2120
|
}
|
|
2107
2121
|
|
|
2108
2122
|
// RPC call: GET ?_rpc=fnName&_args=[...] → JSON response
|
|
2109
|
-
const __rpcName = url.searchParams.get(
|
|
2110
|
-
if (request.method ===
|
|
2123
|
+
const __rpcName = url.searchParams.get(‘_rpc’);
|
|
2124
|
+
if (request.method === ‘GET’ && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
|
|
2111
2125
|
if (request.headers.get('x-kuratchi-rpc') !== '1') {
|
|
2112
2126
|
return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
|
|
2113
2127
|
status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
@@ -2140,7 +2154,8 @@ ${ac?.hasRateLimit ? '\n // Rate limiting — check before route handler
|
|
|
2140
2154
|
}
|
|
2141
2155
|
const formData = await request.formData();
|
|
2142
2156
|
const actionName = formData.get('_action');
|
|
2143
|
-
const __actionFn = route.actions
|
|
2157
|
+
const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
|
|
2158
|
+
|| (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
|
|
2144
2159
|
if (actionName && __actionFn) {
|
|
2145
2160
|
// Check if this is a fetch-based action call (onclick) with JSON args
|
|
2146
2161
|
const argsStr = formData.get('_args');
|
|
@@ -2170,9 +2185,9 @@ ${ac?.hasRateLimit ? '\n // Rate limiting — check before route handler
|
|
|
2170
2185
|
}
|
|
2171
2186
|
// Fetch-based actions return lightweight JSON (no page re-render)
|
|
2172
2187
|
if (isFetchAction) {
|
|
2173
|
-
return new Response(JSON.stringify({ ok: true }), {
|
|
2188
|
+
return __attachCookies(new Response(JSON.stringify({ ok: true }), {
|
|
2174
2189
|
headers: { 'content-type': 'application/json' }
|
|
2175
|
-
});
|
|
2190
|
+
}));
|
|
2176
2191
|
}
|
|
2177
2192
|
// POST-Redirect-GET: redirect to custom target or back to same URL
|
|
2178
2193
|
const __locals = __getLocals();
|
package/dist/create.js
CHANGED
|
@@ -14,10 +14,10 @@ function getFrameworkPackageName() {
|
|
|
14
14
|
try {
|
|
15
15
|
const raw = fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8');
|
|
16
16
|
const parsed = JSON.parse(raw);
|
|
17
|
-
return parsed.name || '
|
|
17
|
+
return parsed.name || '@kuratchi/js';
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
|
-
return '
|
|
20
|
+
return '@kuratchi/js';
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
// ── Prompt Helpers ──────────────────────────────────────────
|
|
@@ -43,6 +43,7 @@ function confirm(question, defaultYes = true) {
|
|
|
43
43
|
}
|
|
44
44
|
export async function create(projectName, flags = []) {
|
|
45
45
|
const autoYes = flags.includes('--yes') || flags.includes('-y');
|
|
46
|
+
const forceDO = flags.includes('--do');
|
|
46
47
|
console.log('\nâš¡ Create a new KuratchiJS project\n');
|
|
47
48
|
// Project name
|
|
48
49
|
const name = projectName || (autoYes ? 'my-kuratchi-app' : await ask('Project name', 'my-kuratchi-app'));
|
|
@@ -59,14 +60,20 @@ export async function create(projectName, flags = []) {
|
|
|
59
60
|
// Feature selection
|
|
60
61
|
const ui = autoYes ? true : await confirm('Include @kuratchi/ui theme?');
|
|
61
62
|
const orm = autoYes ? true : await confirm('Include @kuratchi/orm with D1?');
|
|
62
|
-
const
|
|
63
|
-
|
|
63
|
+
const enableDO = forceDO || (autoYes ? false : await confirm('Include Durable Objects (SQLite-backed DO)?', false));
|
|
64
|
+
const effectiveOrm = orm || enableDO;
|
|
65
|
+
const auth = autoYes ? true : (effectiveOrm ? await confirm('Include @kuratchi/auth (credentials login)?') : false);
|
|
66
|
+
if (enableDO && !orm) {
|
|
67
|
+
console.log(' ℹ DO uses @kuratchi/orm internally — enabling ORM.');
|
|
68
|
+
}
|
|
69
|
+
if (auth && !effectiveOrm) {
|
|
64
70
|
console.log(' ℹ Auth requires ORM — enabling ORM automatically');
|
|
65
71
|
}
|
|
66
72
|
console.log();
|
|
67
73
|
console.log(` Project: ${name}`);
|
|
68
74
|
console.log(` UI: ${ui ? '✓' : '—'}`);
|
|
69
|
-
console.log(` ORM:
|
|
75
|
+
console.log(` D1 ORM: ${orm ? '✓' : '—'}`);
|
|
76
|
+
console.log(` DO: ${enableDO ? '✓' : '—'}`);
|
|
70
77
|
console.log(` Auth: ${auth ? '✓' : '—'}`);
|
|
71
78
|
console.log();
|
|
72
79
|
if (!autoYes) {
|
|
@@ -82,7 +89,7 @@ export async function create(projectName, flags = []) {
|
|
|
82
89
|
const monorepoRoot = detectMonorepo(targetDir);
|
|
83
90
|
const isMonorepo = !!monorepoRoot;
|
|
84
91
|
// Scaffold files
|
|
85
|
-
const opts = { name, ui, orm, auth, monorepo: isMonorepo, monorepoRoot, projectDir: targetDir };
|
|
92
|
+
const opts = { name, ui, orm, do: enableDO, auth, monorepo: isMonorepo, monorepoRoot, projectDir: targetDir };
|
|
86
93
|
scaffold(targetDir, opts);
|
|
87
94
|
// ── Post-scaffold setup ─────────────────────────────────────
|
|
88
95
|
console.log();
|
|
@@ -118,7 +125,7 @@ export async function create(projectName, flags = []) {
|
|
|
118
125
|
// 4. Build routes
|
|
119
126
|
step('Building routes...');
|
|
120
127
|
if (isMonorepo && monorepoRoot) {
|
|
121
|
-
const cliPath = path.join(monorepoRoot, 'packages', '
|
|
128
|
+
const cliPath = path.join(monorepoRoot, 'packages', 'kuratchi-js', 'src', 'cli.ts');
|
|
122
129
|
if (fs.existsSync(cliPath)) {
|
|
123
130
|
run(`bun run --bun ${cliPath} build`, targetDir);
|
|
124
131
|
}
|
|
@@ -156,7 +163,7 @@ function detectMonorepo(targetDir) {
|
|
|
156
163
|
// Walk up from target to find a workspace root with packages/kuratchi-js
|
|
157
164
|
let dir = path.dirname(targetDir);
|
|
158
165
|
for (let i = 0; i < 5; i++) {
|
|
159
|
-
if (fs.existsSync(path.join(dir, 'packages', '
|
|
166
|
+
if (fs.existsSync(path.join(dir, 'packages', 'kuratchi-js', 'package.json'))) {
|
|
160
167
|
return dir;
|
|
161
168
|
}
|
|
162
169
|
const parent = path.dirname(dir);
|
|
@@ -175,14 +182,21 @@ function patchWranglerDbId(dir, dbId) {
|
|
|
175
182
|
// ── Scaffold ────────────────────────────────────────────────
|
|
176
183
|
function scaffold(dir, opts) {
|
|
177
184
|
const { name, ui, orm, auth } = opts;
|
|
185
|
+
const enableDO = opts.do;
|
|
178
186
|
// Create directory structure
|
|
179
187
|
const dirs = [
|
|
180
188
|
'',
|
|
181
189
|
'src',
|
|
182
190
|
'src/routes',
|
|
183
191
|
];
|
|
192
|
+
if (orm || enableDO) {
|
|
193
|
+
dirs.push('src/schemas');
|
|
194
|
+
}
|
|
184
195
|
if (orm) {
|
|
185
|
-
dirs.push('src/database'
|
|
196
|
+
dirs.push('src/database');
|
|
197
|
+
}
|
|
198
|
+
if (enableDO) {
|
|
199
|
+
dirs.push('src/server', 'src/routes/notes');
|
|
186
200
|
}
|
|
187
201
|
if (auth) {
|
|
188
202
|
dirs.push('src/routes/auth', 'src/routes/auth/login', 'src/routes/auth/signup', 'src/routes/admin');
|
|
@@ -196,7 +210,6 @@ function scaffold(dir, opts) {
|
|
|
196
210
|
write(dir, 'kuratchi.config.ts', genConfig(opts));
|
|
197
211
|
write(dir, 'tsconfig.json', genTsConfig());
|
|
198
212
|
write(dir, '.gitignore', genGitIgnore());
|
|
199
|
-
write(dir, 'src/index.ts', genWorkerEntry(opts));
|
|
200
213
|
write(dir, 'src/routes/layout.html', genLayout(opts));
|
|
201
214
|
write(dir, 'src/routes/page.html', genLandingPage(opts));
|
|
202
215
|
if (orm) {
|
|
@@ -204,6 +217,12 @@ function scaffold(dir, opts) {
|
|
|
204
217
|
write(dir, 'src/database/items.ts', genItemsCrud());
|
|
205
218
|
write(dir, 'src/routes/items/page.html', genItemsPage());
|
|
206
219
|
}
|
|
220
|
+
if (enableDO) {
|
|
221
|
+
write(dir, 'src/schemas/notes.ts', genNotesSchema());
|
|
222
|
+
write(dir, 'src/server/notes.do.ts', genNotesDoHandler());
|
|
223
|
+
write(dir, 'src/database/notes.ts', genNotesDb());
|
|
224
|
+
write(dir, 'src/routes/notes/page.html', genNotesPage());
|
|
225
|
+
}
|
|
207
226
|
if (auth) {
|
|
208
227
|
write(dir, '.dev.vars', genDevVars());
|
|
209
228
|
write(dir, 'src/database/auth.ts', genAuthFunctions());
|
|
@@ -235,7 +254,7 @@ function genPackageJson(opts) {
|
|
|
235
254
|
let devScript = 'kuratchi dev';
|
|
236
255
|
let buildScript = 'kuratchi build';
|
|
237
256
|
if (opts.monorepo && opts.monorepoRoot) {
|
|
238
|
-
const cliAbs = path.join(opts.monorepoRoot, 'packages', '
|
|
257
|
+
const cliAbs = path.join(opts.monorepoRoot, 'packages', 'kuratchi-js', 'src', 'cli.ts');
|
|
239
258
|
const relCli = path.relative(opts.projectDir, cliAbs).replace(/\\/g, '/');
|
|
240
259
|
devScript = `bun run --bun ${relCli} dev`;
|
|
241
260
|
buildScript = `bun run --bun ${relCli} build`;
|
|
@@ -259,7 +278,7 @@ function genPackageJson(opts) {
|
|
|
259
278
|
function genWrangler(opts) {
|
|
260
279
|
const config = {
|
|
261
280
|
name: opts.name,
|
|
262
|
-
main: '
|
|
281
|
+
main: '.kuratchi/worker.js',
|
|
263
282
|
compatibility_date: new Date().toISOString().split('T')[0],
|
|
264
283
|
compatibility_flags: ['nodejs_compat'],
|
|
265
284
|
};
|
|
@@ -272,6 +291,14 @@ function genWrangler(opts) {
|
|
|
272
291
|
},
|
|
273
292
|
];
|
|
274
293
|
}
|
|
294
|
+
if (opts.do) {
|
|
295
|
+
config.durable_objects = {
|
|
296
|
+
bindings: [{ name: 'NOTES_DO', class_name: 'NotesDO' }],
|
|
297
|
+
};
|
|
298
|
+
config.migrations = [
|
|
299
|
+
{ tag: 'v1', new_sqlite_classes: ['NotesDO'] },
|
|
300
|
+
];
|
|
301
|
+
}
|
|
275
302
|
return JSON.stringify(config, null, 2) + '\n';
|
|
276
303
|
}
|
|
277
304
|
function genConfig(opts) {
|
|
@@ -280,7 +307,7 @@ function genConfig(opts) {
|
|
|
280
307
|
if (opts.ui) {
|
|
281
308
|
lines.push(`import { kuratchiUiConfig } from '@kuratchi/ui/adapter';`);
|
|
282
309
|
}
|
|
283
|
-
if (opts.orm) {
|
|
310
|
+
if (opts.orm || opts.do) {
|
|
284
311
|
lines.push(`import { kuratchiOrmConfig } from '@kuratchi/orm/adapter';`);
|
|
285
312
|
}
|
|
286
313
|
if (opts.auth) {
|
|
@@ -289,6 +316,9 @@ function genConfig(opts) {
|
|
|
289
316
|
if (opts.orm) {
|
|
290
317
|
lines.push(`import { appSchema } from './src/schemas/app';`);
|
|
291
318
|
}
|
|
319
|
+
if (opts.do) {
|
|
320
|
+
lines.push(`import { notesSchema } from './src/schemas/notes';`);
|
|
321
|
+
}
|
|
292
322
|
lines.push('');
|
|
293
323
|
lines.push('export default defineConfig({');
|
|
294
324
|
// UI
|
|
@@ -297,14 +327,23 @@ function genConfig(opts) {
|
|
|
297
327
|
lines.push(" theme: 'default',");
|
|
298
328
|
lines.push(' }),');
|
|
299
329
|
}
|
|
300
|
-
// ORM
|
|
301
|
-
if (opts.orm) {
|
|
330
|
+
// ORM (D1 + DO databases combined into a single kuratchiOrmConfig call)
|
|
331
|
+
if (opts.orm || opts.do) {
|
|
302
332
|
lines.push(' orm: kuratchiOrmConfig({');
|
|
303
333
|
lines.push(' databases: {');
|
|
304
|
-
|
|
334
|
+
if (opts.orm)
|
|
335
|
+
lines.push(" DB: { schema: appSchema },");
|
|
336
|
+
if (opts.do)
|
|
337
|
+
lines.push(" NOTES_DO: { schema: notesSchema, type: 'do' },");
|
|
305
338
|
lines.push(' }');
|
|
306
339
|
lines.push(' }),');
|
|
307
340
|
}
|
|
341
|
+
// Durable Objects
|
|
342
|
+
if (opts.do) {
|
|
343
|
+
lines.push(' durableObjects: {');
|
|
344
|
+
lines.push(" NOTES_DO: { className: 'NotesDO' },");
|
|
345
|
+
lines.push(' },');
|
|
346
|
+
}
|
|
308
347
|
// Auth
|
|
309
348
|
if (opts.auth) {
|
|
310
349
|
lines.push(' auth: kuratchiAuthConfig({');
|
|
@@ -341,13 +380,112 @@ worker-configuration.d.ts
|
|
|
341
380
|
dist/
|
|
342
381
|
`;
|
|
343
382
|
}
|
|
344
|
-
|
|
345
|
-
|
|
383
|
+
// ── Durable Object templates ──────────────────────────────────────────────────
|
|
384
|
+
function genNotesSchema() {
|
|
385
|
+
return `import type { SchemaDsl } from '@kuratchi/orm';
|
|
386
|
+
|
|
387
|
+
export const notesSchema: SchemaDsl = {
|
|
388
|
+
name: 'notes',
|
|
389
|
+
version: 1,
|
|
390
|
+
tables: {
|
|
391
|
+
notes: {
|
|
392
|
+
id: 'integer primary key',
|
|
393
|
+
title: 'text not null',
|
|
394
|
+
created_at: 'text not null default now',
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export interface Note {
|
|
400
|
+
id: number;
|
|
401
|
+
title: string;
|
|
402
|
+
created_at: string;
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
function genNotesDoHandler() {
|
|
407
|
+
return `import { kuratchiDO } from '${FRAMEWORK_PACKAGE_NAME}';
|
|
408
|
+
import type { Note } from '../schemas/notes';
|
|
409
|
+
|
|
410
|
+
export default class NotesDO extends kuratchiDO {
|
|
411
|
+
static binding = 'NOTES_DO';
|
|
412
|
+
|
|
413
|
+
async getNotes(): Promise<Note[]> {
|
|
414
|
+
return (await this.db.notes.orderBy({ created_at: 'desc' }).many()).data ?? [];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async addNote(title: string): Promise<void> {
|
|
418
|
+
await this.db.notes.insert({ title });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async deleteNote(id: number): Promise<void> {
|
|
422
|
+
await this.db.notes.delete({ id });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
function genNotesDb() {
|
|
428
|
+
return `import { env } from 'cloudflare:workers';
|
|
429
|
+
import type { Note } from '../schemas/notes';
|
|
430
|
+
|
|
431
|
+
function getStub() {
|
|
432
|
+
return (env as any).NOTES_DO.get((env as any).NOTES_DO.idFromName('global'));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function getNotes(): Promise<Note[]> {
|
|
436
|
+
return getStub().getNotes();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export async function addNote(formData: FormData): Promise<void> {
|
|
440
|
+
const title = String(formData.get('title') || '').trim();
|
|
441
|
+
if (!title) throw new Error('Note is required');
|
|
442
|
+
await getStub().addNote(title);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export async function deleteNote(id: number): Promise<void> {
|
|
446
|
+
await getStub().deleteNote(Number(id));
|
|
447
|
+
}
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
function genNotesPage() {
|
|
451
|
+
return `<script>
|
|
452
|
+
import { getNotes, addNote, deleteNote } from '$database/notes';
|
|
453
|
+
|
|
454
|
+
const notes = await getNotes();
|
|
455
|
+
</script>
|
|
456
|
+
|
|
457
|
+
<header>
|
|
458
|
+
<div>
|
|
459
|
+
<h1>Notes</h1>
|
|
460
|
+
<p>Backed by a Cloudflare Durable Object with SQLite storage</p>
|
|
461
|
+
</div>
|
|
462
|
+
</header>
|
|
463
|
+
|
|
464
|
+
<form action={addNote} method="POST">
|
|
465
|
+
<input type="text" name="title" placeholder="New note..." required />
|
|
466
|
+
<button type="submit">Add</button>
|
|
467
|
+
</form>
|
|
468
|
+
|
|
469
|
+
if (notes.length === 0) {
|
|
470
|
+
<p style="opacity: 0.6">No notes yet.</p>
|
|
471
|
+
} else {
|
|
472
|
+
<section>
|
|
473
|
+
for (const note of notes) {
|
|
474
|
+
<article>
|
|
475
|
+
<span>{note.title}</span>
|
|
476
|
+
<button data-action="deleteNote" data-args={JSON.stringify([note.id])}>Remove</button>
|
|
477
|
+
</article>
|
|
478
|
+
}
|
|
479
|
+
</section>
|
|
480
|
+
}
|
|
481
|
+
`;
|
|
346
482
|
}
|
|
347
483
|
function genLayout(opts) {
|
|
348
484
|
const navLinks = [' <a href="/">Home</a>'];
|
|
349
485
|
if (opts.orm)
|
|
350
486
|
navLinks.push(' <a href="/items">Items</a>');
|
|
487
|
+
if (opts.do)
|
|
488
|
+
navLinks.push(' <a href="/notes">Notes (DO)</a>');
|
|
351
489
|
if (opts.auth)
|
|
352
490
|
navLinks.push(' <a href="/admin">Admin</a>');
|
|
353
491
|
return `<!DOCTYPE html>
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { createApp } from './runtime/app.js';
|
|
7
7
|
export { defineConfig } from './runtime/config.js';
|
|
8
|
-
export {
|
|
8
|
+
export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
9
9
|
export { kuratchiDO, doRpc } from './runtime/do.js';
|
|
10
10
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
11
11
|
export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule } from './runtime/types.js';
|
|
12
12
|
export type { RpcOf } from './runtime/do.js';
|
|
13
|
-
export { compile } from './compiler/index.js';
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
// Runtime
|
|
7
7
|
export { createApp } from './runtime/app.js';
|
|
8
8
|
export { defineConfig } from './runtime/config.js';
|
|
9
|
-
export {
|
|
9
|
+
export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
10
10
|
export { kuratchiDO, doRpc } from './runtime/do.js';
|
|
11
11
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
|
|
12
12
|
// Compatibility aliases
|
|
13
13
|
matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
14
|
-
// Compiler (for build tooling)
|
|
15
|
-
export { compile } from './compiler/index.js';
|
|
@@ -14,13 +14,6 @@ export interface BreadcrumbItem {
|
|
|
14
14
|
}
|
|
15
15
|
/** Called by the framework at the start of each request */
|
|
16
16
|
export declare function __setRequestContext(ctx: any, request: Request): void;
|
|
17
|
-
/**
|
|
18
|
-
* @deprecated Use `import { env } from 'cloudflare:workers'` instead.
|
|
19
|
-
* Kept for backward compatibility — delegates to the native env import.
|
|
20
|
-
*/
|
|
21
|
-
export declare function getEnv<T = any>(): T;
|
|
22
|
-
/** @internal — called by compiler to stash env ref for getEnv() compat */
|
|
23
|
-
export declare function __setEnvCompat(env: any): void;
|
|
24
17
|
/** Get the execution context (waitUntil, passThroughOnException) */
|
|
25
18
|
export declare function getCtx(): ExecutionContext;
|
|
26
19
|
/** Get the current request */
|
package/dist/runtime/context.js
CHANGED
|
@@ -22,17 +22,6 @@ export function __setRequestContext(ctx, request) {
|
|
|
22
22
|
get locals() { return __locals; },
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
* @deprecated Use `import { env } from 'cloudflare:workers'` instead.
|
|
27
|
-
* Kept for backward compatibility — delegates to the native env import.
|
|
28
|
-
*/
|
|
29
|
-
export function getEnv() {
|
|
30
|
-
return globalThis.__cloudflare_env__;
|
|
31
|
-
}
|
|
32
|
-
/** @internal — called by compiler to stash env ref for getEnv() compat */
|
|
33
|
-
export function __setEnvCompat(env) {
|
|
34
|
-
globalThis.__cloudflare_env__ = env;
|
|
35
|
-
}
|
|
36
25
|
/** Get the execution context (waitUntil, passThroughOnException) */
|
|
37
26
|
export function getCtx() {
|
|
38
27
|
if (!__ctx)
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createApp } from './app.js';
|
|
2
2
|
export { defineConfig } from './config.js';
|
|
3
3
|
export { Router, filePathToPattern } from './router.js';
|
|
4
|
-
export {
|
|
4
|
+
export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
|
|
5
5
|
export { kuratchiDO, doRpc } from './do.js';
|
|
6
6
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './containers.js';
|
|
7
7
|
export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, LayoutModule } from './types.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createApp } from './app.js';
|
|
2
2
|
export { defineConfig } from './config.js';
|
|
3
3
|
export { Router, filePathToPattern } from './router.js';
|
|
4
|
-
export {
|
|
4
|
+
export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
|
|
5
5
|
export { kuratchiDO, doRpc } from './do.js';
|
|
6
6
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
|
|
7
7
|
// Compatibility aliases
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuratchi/js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -43,8 +43,4 @@
|
|
|
43
43
|
"@types/node": "^24.4.0",
|
|
44
44
|
"typescript": "^5.8.0"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
}
|