@kuratchi/js 0.0.1 → 0.0.3

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,29 +1,392 @@
1
- # @kuratchi/js
2
-
3
- Cloudflare Workers-native web framework with a compiler, runtime, and CLI.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @kuratchi/js
9
- ```
10
-
11
- ## CLI
12
-
13
- ```bash
14
- npx kuratchi create my-app
15
- npx kuratchi build
16
- npx kuratchi watch
17
- ```
18
-
19
- ## Runtime APIs
20
-
21
- ```ts
22
- import { createApp, defineConfig } from '@kuratchi/js';
23
- import { getCtx, getEnv } from '@kuratchi/js/runtime/context.js';
24
- import { kuratchiDO, doRpc } from '@kuratchi/js/runtime/do.js';
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 watch Compile routes + watch for changes
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 layoutFile = path.join(projectDir, 'src', 'routes', 'layout.html');
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
  }
@@ -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
- /** Output file path (default: .kuratchi/worker.js) */
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
- * The project's src/index.ts imports this and re-exports it as the Worker default.
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;