@kuratchi/js 0.0.14 → 0.0.16

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.
Files changed (65) hide show
  1. package/README.md +135 -68
  2. package/dist/cli.js +80 -47
  3. package/dist/compiler/api-route-pipeline.d.ts +8 -0
  4. package/dist/compiler/api-route-pipeline.js +23 -0
  5. package/dist/compiler/asset-pipeline.d.ts +7 -0
  6. package/dist/compiler/asset-pipeline.js +33 -0
  7. package/dist/compiler/client-module-pipeline.d.ts +25 -0
  8. package/dist/compiler/client-module-pipeline.js +257 -0
  9. package/dist/compiler/compiler-shared.d.ts +55 -0
  10. package/dist/compiler/compiler-shared.js +4 -0
  11. package/dist/compiler/component-pipeline.d.ts +15 -0
  12. package/dist/compiler/component-pipeline.js +163 -0
  13. package/dist/compiler/config-reading.d.ts +11 -0
  14. package/dist/compiler/config-reading.js +323 -0
  15. package/dist/compiler/convention-discovery.d.ts +9 -0
  16. package/dist/compiler/convention-discovery.js +83 -0
  17. package/dist/compiler/durable-object-pipeline.d.ts +9 -0
  18. package/dist/compiler/durable-object-pipeline.js +255 -0
  19. package/dist/compiler/error-page-pipeline.d.ts +1 -0
  20. package/dist/compiler/error-page-pipeline.js +16 -0
  21. package/dist/compiler/import-linking.d.ts +36 -0
  22. package/dist/compiler/import-linking.js +139 -0
  23. package/dist/compiler/index.d.ts +3 -3
  24. package/dist/compiler/index.js +137 -3265
  25. package/dist/compiler/layout-pipeline.d.ts +31 -0
  26. package/dist/compiler/layout-pipeline.js +155 -0
  27. package/dist/compiler/page-route-pipeline.d.ts +16 -0
  28. package/dist/compiler/page-route-pipeline.js +62 -0
  29. package/dist/compiler/parser.d.ts +4 -0
  30. package/dist/compiler/parser.js +433 -51
  31. package/dist/compiler/root-layout-pipeline.d.ts +10 -0
  32. package/dist/compiler/root-layout-pipeline.js +517 -0
  33. package/dist/compiler/route-discovery.d.ts +7 -0
  34. package/dist/compiler/route-discovery.js +87 -0
  35. package/dist/compiler/route-pipeline.d.ts +57 -0
  36. package/dist/compiler/route-pipeline.js +296 -0
  37. package/dist/compiler/route-state-pipeline.d.ts +25 -0
  38. package/dist/compiler/route-state-pipeline.js +139 -0
  39. package/dist/compiler/routes-module-feature-blocks.d.ts +2 -0
  40. package/dist/compiler/routes-module-feature-blocks.js +330 -0
  41. package/dist/compiler/routes-module-pipeline.d.ts +2 -0
  42. package/dist/compiler/routes-module-pipeline.js +6 -0
  43. package/dist/compiler/routes-module-runtime-shell.d.ts +2 -0
  44. package/dist/compiler/routes-module-runtime-shell.js +81 -0
  45. package/dist/compiler/routes-module-types.d.ts +44 -0
  46. package/dist/compiler/routes-module-types.js +1 -0
  47. package/dist/compiler/script-transform.d.ts +16 -0
  48. package/dist/compiler/script-transform.js +218 -0
  49. package/dist/compiler/server-module-pipeline.d.ts +13 -0
  50. package/dist/compiler/server-module-pipeline.js +124 -0
  51. package/dist/compiler/template.d.ts +13 -1
  52. package/dist/compiler/template.js +323 -60
  53. package/dist/compiler/worker-output-pipeline.d.ts +13 -0
  54. package/dist/compiler/worker-output-pipeline.js +37 -0
  55. package/dist/compiler/wrangler-sync.d.ts +14 -0
  56. package/dist/compiler/wrangler-sync.js +185 -0
  57. package/dist/runtime/app.js +15 -3
  58. package/dist/runtime/generated-worker.d.ts +33 -0
  59. package/dist/runtime/generated-worker.js +412 -0
  60. package/dist/runtime/index.d.ts +2 -1
  61. package/dist/runtime/index.js +1 -0
  62. package/dist/runtime/router.d.ts +2 -1
  63. package/dist/runtime/router.js +12 -3
  64. package/dist/runtime/types.d.ts +8 -2
  65. package/package.json +5 -1
package/README.md CHANGED
@@ -16,9 +16,9 @@ cd my-app
16
16
  bun run dev
17
17
  ```
18
18
 
19
- ## How it works
20
-
21
- `kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
19
+ ## How it works
20
+
21
+ `kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
22
22
 
23
23
  | File | Purpose |
24
24
  |---|---|
@@ -26,11 +26,13 @@ bun run dev
26
26
  | `.kuratchi/worker.js` | Stable wrangler entry - re-exports the fetch handler plus all Durable Object and Agent classes |
27
27
  | `.kuratchi/do/*.js` | Generated Durable Object RPC proxy modules for `$durable-objects/*` imports |
28
28
 
29
- Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
30
-
31
- ```jsonc
32
- // wrangler.jsonc
33
- {
29
+ Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
30
+
31
+ For the framework's internal compiler/runtime orchestration and tracked implementation roadmap, see [ARCHITECTURE.md](./ARCHITECTURE.md).
32
+
33
+ ```jsonc
34
+ // wrangler.jsonc
35
+ {
34
36
  "main": ".kuratchi/worker.js"
35
37
  }
36
38
  ```
@@ -349,14 +351,31 @@ Navigate to a URL on click (respects `http:`/`https:` only):
349
351
 
350
352
  ### `data-poll` — polling
351
353
 
352
- Refresh a section automatically on an interval (milliseconds):
354
+ Poll and update an element's content at a human-readable interval with automatic exponential backoff:
353
355
 
354
356
  ```html
355
- <div data-refresh="/status" data-poll="3000">
357
+ <div data-poll={getStatus(itemId)} data-interval="2s">
356
358
  {status}
357
359
  </div>
358
360
  ```
359
361
 
362
+ **How it works:**
363
+ 1. Client sends a fragment request with `x-kuratchi-fragment` header
364
+ 2. Server re-renders the route but returns **only the fragment's innerHTML** — not the full page
365
+ 3. Client swaps the element's content — minimal payload, no full page reload
366
+
367
+ This fragment-based architecture is the foundation for partial rendering and scales to Astro-style islands.
368
+
369
+ **Interval formats:**
370
+ - `2s` — 2 seconds
371
+ - `500ms` — 500 milliseconds
372
+ - `1m` — 1 minute
373
+ - Default: `30s` with exponential backoff (30s → 45s → 67s → ... capped at 5 minutes)
374
+
375
+ **Options:**
376
+ - `data-interval` — polling interval (human-readable, default `30s`)
377
+ - `data-backoff="false"` — disable exponential backoff
378
+
360
379
  ### `data-select-all` / `data-select-item` — checkbox groups
361
380
 
362
381
  Sync a "select all" checkbox with a group of item checkboxes:
@@ -397,79 +416,87 @@ Durable Object behavior is enabled by filename suffix.
397
416
  - Any file not ending in `.do.ts` is treated as a normal server module.
398
417
  - No required folder name. `src/server/auth.do.ts`, `src/server/foo/bar/sites.do.ts`, etc. all work.
399
418
 
400
- ### Function mode (recommended)
419
+ ### Writing a Durable Object
401
420
 
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.
421
+ Extend the native Cloudflare `DurableObject` class. Public methods automatically become RPC-accessible:
404
422
 
405
423
  ```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
- }
424
+ // src/server/user.do.ts
425
+ import { DurableObject } from 'cloudflare:workers';
418
426
 
419
- export async function getOrgUsers() {
420
- const result = await this.db.users.orderBy({ createdAt: 'asc' }).many();
421
- return result.data ?? [];
422
- }
427
+ export default class UserDO extends DurableObject {
428
+ async getName() {
429
+ return await this.ctx.storage.get('name');
430
+ }
423
431
 
424
- export async function createOrgUser(formData: FormData) {
425
- const user = await getCurrentUser();
426
- if (!user?.orgId) throw new Error('Not authenticated');
432
+ async setName(name: string) {
433
+ this._validate(name);
434
+ await this.ctx.storage.put('name', name);
435
+ }
427
436
 
428
- const email = String(formData.get('email') ?? '').trim().toLowerCase();
429
- if (!email) throw new Error('Email is required');
437
+ // NOT RPC-accessible (underscore prefix)
438
+ _validate(name: string) {
439
+ if (!name) throw new Error('Name required');
440
+ }
430
441
 
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');
442
+ // NOT RPC-accessible (lifecycle method)
443
+ async alarm() {
444
+ // Handle alarm
445
+ }
434
446
  }
435
447
  ```
436
448
 
437
- Optional lifecycle exports in function mode:
449
+ **RPC rules:**
450
+ - **Public methods** (`getName`, `setName`) → RPC-accessible
451
+ - **Underscore prefix** (`_validate`) → NOT RPC-accessible
452
+ - **Private/protected** (`private foo()`) → NOT RPC-accessible
453
+ - **Lifecycle methods** (`constructor`, `fetch`, `alarm`, `webSocketMessage`, etc.) → NOT RPC-accessible
438
454
 
439
- - `export async function onInit()`
440
- - `export async function onAlarm(...args)`
441
- - `export function onMessage(...args)`
455
+ ### Using from routes
442
456
 
443
- These lifecycle names are not exposed as RPC methods.
457
+ Import from `$do/<filename>` (without the `.do` suffix):
444
458
 
445
- ### Class mode (optional)
459
+ ```html
460
+ <script server>
461
+ import { getName, setName } from '$do/user';
446
462
 
447
- Class-based handlers are still supported in `.do.ts` files:
463
+ const name = await getName();
464
+ </script>
448
465
 
449
- ```ts
450
- import { kuratchiDO } from '@kuratchi/js';
466
+ <h1>Hello, {name}</h1>
467
+ ```
451
468
 
452
- export default class NotesDO extends kuratchiDO {
453
- static binding = 'NOTES_DO';
469
+ The framework handles RPC wiring automatically.
454
470
 
455
- async getNotes() {
456
- return (await this.db.notes.orderBy({ created_at: 'desc' }).many()).data ?? [];
457
- }
471
+ ### Auto-Discovery
472
+
473
+ Durable Objects are auto-discovered from `.do.ts` files. **No config needed.**
474
+
475
+ **Naming convention:**
476
+ - `user.do.ts` → binding `USER_DO`
477
+ - `org-settings.do.ts` → binding `ORG_SETTINGS_DO`
478
+
479
+ **Override binding name** with `static binding`:
480
+ ```ts
481
+ export default class UserDO extends DurableObject {
482
+ static binding = 'CUSTOM_BINDING'; // Optional override
483
+ // ...
458
484
  }
459
485
  ```
460
486
 
461
- Declare it in `kuratchi.config.ts` and in `wrangler.jsonc`. The compiler exports DO classes from `.kuratchi/worker.js` automatically.
487
+ The framework auto-syncs discovered DOs to `wrangler.jsonc`.
462
488
 
463
- ```jsonc
464
- // wrangler.jsonc
465
- {
466
- "durable_objects": {
467
- "bindings": [{ "name": "NOTES_DO", "class_name": "NotesDO" }]
489
+ ### Optional: stubId for auth integration
490
+
491
+ If you need automatic stub resolution based on user context, add `stubId` in `kuratchi.config.ts`:
492
+
493
+ ```ts
494
+ // kuratchi.config.ts
495
+ export default defineConfig({
496
+ durableObjects: {
497
+ USER_DO: { stubId: 'user.orgId' }, // Only needed for auth integration
468
498
  },
469
- "migrations": [
470
- { "tag": "v1", "new_sqlite_classes": ["NotesDO"] }
471
- ]
472
- }
499
+ });
473
500
  ```
474
501
 
475
502
  ## Agents
@@ -542,6 +569,46 @@ Examples:
542
569
  - `bond.workflow.ts` → `BOND_WORKFLOW` binding
543
570
  - `new-site.workflow.ts` → `NEW_SITE_WORKFLOW` binding
544
571
 
572
+ ### Workflow Status Polling
573
+
574
+ Kuratchi auto-generates status polling RPCs for each discovered workflow. Poll workflow status with zero setup:
575
+
576
+ ```html
577
+ <div data-poll={migrationWorkflowStatus(instanceId)} data-interval="2s">
578
+ if (workflowStatus.status === 'running') {
579
+ <div class="spinner">Running...</div>
580
+ } else if (workflowStatus.status === 'complete') {
581
+ <div>✓ Complete</div>
582
+ }
583
+ </div>
584
+ ```
585
+
586
+ The element's innerHTML updates automatically when the workflow status changes — no page reload needed.
587
+
588
+ **Auto-generated RPC naming** (camelCase):
589
+ - `migration.workflow.ts` → `migrationWorkflowStatus(instanceId)`
590
+ - `james-bond.workflow.ts` → `jamesBondWorkflowStatus(instanceId)`
591
+ - `site.workflow.ts` → `siteWorkflowStatus(instanceId)`
592
+
593
+ **Multiple workflows on one page:** Each `data-poll` element is independent. You can poll multiple workflow instances without collision:
594
+
595
+ ```html
596
+ for (const job of jobs) {
597
+ <div data-poll={migrationWorkflowStatus(job.instanceId)} data-interval="2s">
598
+ {job.name}: polling...
599
+ </div>
600
+ }
601
+ ```
602
+
603
+ The status RPC returns the Cloudflare `InstanceStatus` object:
604
+ ```ts
605
+ {
606
+ status: 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting' | 'unknown';
607
+ error?: { name: string; message: string; };
608
+ output?: unknown;
609
+ }
610
+ ```
611
+
545
612
  ## Containers
546
613
 
547
614
  Kuratchi auto-discovers `.container.ts` files in `src/server/`. **No config needed.**
@@ -701,7 +768,9 @@ Kuratchi also exposes a framework build-mode flag:
701
768
 
702
769
  ## `kuratchi.config.ts`
703
770
 
704
- Optional. Required only when using framework integrations or Durable Objects.
771
+ Optional. Required only when using framework integrations (ORM, auth, UI).
772
+
773
+ **Durable Objects are auto-discovered** — no config needed unless you need `stubId` for auth integration.
705
774
 
706
775
  ```ts
707
776
  import { defineConfig } from '@kuratchi/js';
@@ -717,16 +786,14 @@ export default defineConfig({
717
786
  NOTES_DO: { schema: notesSchema, type: 'do' },
718
787
  },
719
788
  }),
720
- durableObjects: {
721
- NOTES_DO: {
722
- className: 'NotesDO',
723
- files: ['notes.do.ts'],
724
- },
725
- },
726
789
  auth: kuratchiAuthConfig({
727
790
  cookieName: 'kuratchi_session',
728
791
  sessionEnabled: true,
729
792
  }),
793
+ // Optional: only needed for auth-based stub resolution
794
+ durableObjects: {
795
+ NOTES_DO: { stubId: 'user.orgId' },
796
+ },
730
797
  });
731
798
  ```
732
799
 
package/dist/cli.js CHANGED
@@ -6,34 +6,41 @@ import { compile } from './compiler/index.js';
6
6
  import * as path from 'node:path';
7
7
  import * as fs from 'node:fs';
8
8
  import * as net from 'node:net';
9
+ import { createRequire } from 'node:module';
9
10
  import { spawn } from 'node:child_process';
10
11
  const args = process.argv.slice(2);
11
12
  const command = args[0];
12
13
  const projectDir = process.cwd();
13
- switch (command) {
14
- case 'build':
15
- runBuild();
16
- break;
17
- case 'watch':
18
- runWatch(false);
19
- break;
20
- case 'dev':
21
- runWatch(true);
22
- break;
23
- case 'create':
24
- runCreate();
25
- break;
26
- default:
27
- console.log(`
28
- KuratchiJS CLI
29
-
30
- Usage:
31
- kuratchi create [name] Scaffold a new KuratchiJS project
32
- kuratchi build Compile routes once
33
- kuratchi dev Compile, watch for changes, and start wrangler dev server
34
- kuratchi watch Compile + watch only (no wrangler — for custom setups)
14
+ void main().catch((err) => {
15
+ console.error(`[kuratchi] ${err?.message ?? err}`);
16
+ process.exit(1);
17
+ });
18
+ async function main() {
19
+ switch (command) {
20
+ case 'build':
21
+ runBuild();
22
+ return;
23
+ case 'watch':
24
+ await runWatch(false);
25
+ return;
26
+ case 'dev':
27
+ await runWatch(true);
28
+ return;
29
+ case 'create':
30
+ await runCreate();
31
+ return;
32
+ default:
33
+ console.log(`
34
+ KuratchiJS CLI
35
+
36
+ Usage:
37
+ kuratchi create [name] Scaffold a new KuratchiJS project
38
+ kuratchi build Compile routes once
39
+ kuratchi dev Compile, watch for changes, and start wrangler dev server
40
+ kuratchi watch Compile + watch only (no wrangler — for custom setups)
35
41
  `);
36
- process.exit(1);
42
+ process.exit(1);
43
+ }
37
44
  }
38
45
  async function runCreate() {
39
46
  const { create } = await import('./create.js');
@@ -53,7 +60,7 @@ function runBuild(isDev = false) {
53
60
  process.exit(1);
54
61
  }
55
62
  }
56
- function runWatch(withWrangler = false) {
63
+ async function runWatch(withWrangler = false) {
57
64
  runBuild(true);
58
65
  const routesDir = path.join(projectDir, 'src', 'routes');
59
66
  const serverDir = path.join(projectDir, 'src', 'server');
@@ -80,11 +87,10 @@ function runWatch(withWrangler = false) {
80
87
  // `kuratchi dev` also starts the wrangler dev server.
81
88
  // `kuratchi watch` is the compiler-only mode for custom setups.
82
89
  if (withWrangler) {
83
- startWranglerDev().catch((err) => {
84
- console.error(`[kuratchi] Failed to start wrangler dev: ${err?.message ?? err}`);
85
- process.exit(1);
86
- });
90
+ await startWranglerDev();
91
+ return;
87
92
  }
93
+ await new Promise(() => { });
88
94
  }
89
95
  function hasPortFlag(inputArgs) {
90
96
  for (let i = 0; i < inputArgs.length; i++) {
@@ -117,25 +123,13 @@ async function findOpenPort(start = 8787, end = 8899) {
117
123
  }
118
124
  async function startWranglerDev() {
119
125
  const passthroughArgs = args.slice(1);
120
- const wranglerArgs = ['wrangler', 'dev', ...passthroughArgs];
126
+ const wranglerArgs = ['dev', ...passthroughArgs];
121
127
  if (!hasPortFlag(passthroughArgs)) {
122
128
  const port = await findOpenPort();
123
129
  wranglerArgs.push('--port', String(port));
124
130
  console.log(`[kuratchi] Starting wrangler dev on port ${port}`);
125
131
  }
126
- // Use 'pipe' for stdin so wrangler doesn't detect stdin EOF and exit
127
- // prematurely when launched via a script runner (e.g. `bun run dev`).
128
- const isWin = process.platform === 'win32';
129
- const wrangler = isWin
130
- ? spawn('npx ' + wranglerArgs.join(' '), {
131
- cwd: projectDir,
132
- stdio: ['pipe', 'inherit', 'inherit'],
133
- shell: true,
134
- })
135
- : spawn('npx', wranglerArgs, {
136
- cwd: projectDir,
137
- stdio: ['pipe', 'inherit', 'inherit'],
138
- });
132
+ const wrangler = spawnWranglerProcess(wranglerArgs);
139
133
  const cleanup = () => {
140
134
  if (!wrangler.killed)
141
135
  wrangler.kill();
@@ -143,10 +137,49 @@ async function startWranglerDev() {
143
137
  process.on('exit', cleanup);
144
138
  process.on('SIGINT', () => { cleanup(); process.exit(0); });
145
139
  process.on('SIGTERM', () => { cleanup(); process.exit(0); });
146
- wrangler.on('exit', (code) => {
147
- if (code !== 0 && code !== null) {
148
- console.error(`[kuratchi] wrangler exited with code ${code}`);
149
- }
150
- process.exit(code ?? 0);
140
+ await new Promise((resolve, reject) => {
141
+ wrangler.on('exit', (code) => {
142
+ if (code !== 0 && code !== null) {
143
+ reject(new Error(`wrangler exited with code ${code}`));
144
+ return;
145
+ }
146
+ resolve();
147
+ });
148
+ wrangler.on('error', (err) => {
149
+ reject(err);
150
+ });
151
+ }).catch((err) => {
152
+ console.error(`[kuratchi] Failed to start wrangler dev: ${err?.message ?? err}`);
153
+ process.exit(1);
154
+ });
155
+ }
156
+ function resolveWranglerBin() {
157
+ try {
158
+ const projectPackageJson = path.join(projectDir, 'package.json');
159
+ const projectRequire = createRequire(projectPackageJson);
160
+ return projectRequire.resolve('wrangler/bin/wrangler.js');
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ function getNodeExecutable() {
167
+ if (!process.versions.bun)
168
+ return process.execPath;
169
+ return 'node';
170
+ }
171
+ function spawnWranglerProcess(wranglerArgs) {
172
+ const localWranglerBin = resolveWranglerBin();
173
+ const stdio = ['pipe', 'inherit', 'inherit'];
174
+ if (localWranglerBin) {
175
+ return spawn(getNodeExecutable(), [localWranglerBin, ...wranglerArgs], {
176
+ cwd: projectDir,
177
+ stdio,
178
+ });
179
+ }
180
+ const fallbackCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
181
+ return spawn(fallbackCommand, ['wrangler', ...wranglerArgs], {
182
+ cwd: projectDir,
183
+ stdio,
151
184
  });
152
185
  }
@@ -0,0 +1,8 @@
1
+ export declare function compileApiRoute(opts: {
2
+ pattern: string;
3
+ fullPath: string;
4
+ projectDir: string;
5
+ transformModule: (entryAbsPath: string) => string;
6
+ allocateModuleId: () => string;
7
+ pushImport: (statement: string) => void;
8
+ }): string;
@@ -0,0 +1,23 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ const ALL_API_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
4
+ export function compileApiRoute(opts) {
5
+ const outFileDir = path.join(opts.projectDir, '.kuratchi');
6
+ const absRoutePath = opts.transformModule(opts.fullPath);
7
+ let importPath = path.relative(outFileDir, absRoutePath).replace(/\\/g, '/');
8
+ if (!importPath.startsWith('.'))
9
+ importPath = './' + importPath;
10
+ const moduleId = opts.allocateModuleId();
11
+ opts.pushImport(`import * as ${moduleId} from '${importPath}';`);
12
+ const apiSource = fs.readFileSync(opts.fullPath, 'utf-8');
13
+ const exportedMethods = ALL_API_METHODS.filter((method) => {
14
+ const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${method}\\b`);
15
+ const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${method}\\b`);
16
+ const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${method}\\b`);
17
+ return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
18
+ });
19
+ const methodEntries = exportedMethods
20
+ .map((method) => `${method}: ${moduleId}.${method}`)
21
+ .join(', ');
22
+ return `{ pattern: '${opts.pattern}', __api: true, ${methodEntries} }`;
23
+ }
@@ -0,0 +1,7 @@
1
+ export interface CompiledAsset {
2
+ name: string;
3
+ content: string;
4
+ mime: string;
5
+ etag: string;
6
+ }
7
+ export declare function compileAssets(assetsDir: string): CompiledAsset[];
@@ -0,0 +1,33 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ const MIME_TYPES = {
5
+ '.css': 'text/css; charset=utf-8',
6
+ '.js': 'text/javascript; charset=utf-8',
7
+ '.json': 'application/json; charset=utf-8',
8
+ '.svg': 'image/svg+xml',
9
+ '.txt': 'text/plain; charset=utf-8',
10
+ };
11
+ export function compileAssets(assetsDir) {
12
+ const compiledAssets = [];
13
+ if (!fs.existsSync(assetsDir))
14
+ return compiledAssets;
15
+ const scanAssets = (dir, prefix) => {
16
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
17
+ if (entry.isDirectory()) {
18
+ scanAssets(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
19
+ continue;
20
+ }
21
+ const ext = path.extname(entry.name).toLowerCase();
22
+ const mime = MIME_TYPES[ext];
23
+ if (!mime)
24
+ continue;
25
+ const content = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
26
+ const etag = '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"';
27
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
28
+ compiledAssets.push({ name, content, mime, etag });
29
+ }
30
+ };
31
+ scanAssets(assetsDir, '');
32
+ return compiledAssets;
33
+ }
@@ -0,0 +1,25 @@
1
+ import type { CompiledAsset } from './asset-pipeline.js';
2
+ import { type RouteImportEntry } from './import-linking.js';
3
+ export interface ClientEventRegistration {
4
+ routeId: string;
5
+ handlerId: string;
6
+ argsExpr: string | null;
7
+ }
8
+ export interface ClientModuleCompiler {
9
+ createRegistry(scopeId: string, importEntries: RouteImportEntry[]): ClientRouteRegistry;
10
+ createRouteRegistry(routeIndex: number, importEntries: RouteImportEntry[]): ClientRouteRegistry;
11
+ getCompiledAssets(): CompiledAsset[];
12
+ }
13
+ export interface ClientRouteRegistry {
14
+ hasBindings(): boolean;
15
+ hasBindingReference(expression: string): boolean;
16
+ registerEventHandler(eventName: string, expression: string): ClientEventRegistration | null;
17
+ buildEntryAsset(): {
18
+ assetName: string;
19
+ asset: CompiledAsset;
20
+ } | null;
21
+ }
22
+ export declare function createClientModuleCompiler(opts: {
23
+ projectDir: string;
24
+ srcDir: string;
25
+ }): ClientModuleCompiler;