@kuratchi/js 0.0.2 → 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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Compiler — scans a project's routes/ directory, parses .html files,
2
+ * Compiler â€" scans a project's routes/ directory, parses .html files,
3
3
  * and generates a single Worker entry point.
4
4
  */
5
5
  import { parseFile } from './parser.js';
@@ -31,10 +31,36 @@ function compactInlineJs(source) {
31
31
  .replace(/\s*([{}();,:])\s*/g, '$1')
32
32
  .trim();
33
33
  }
34
+ function parseNamedImportBindings(line) {
35
+ const namesMatch = line.match(/import\s*\{([^}]+)\}/);
36
+ if (!namesMatch)
37
+ return [];
38
+ return namesMatch[1]
39
+ .split(',')
40
+ .map(n => n.trim())
41
+ .filter(Boolean)
42
+ .map(n => {
43
+ const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
44
+ return parts[1] || parts[0] || '';
45
+ })
46
+ .filter(Boolean);
47
+ }
48
+ function filterClientImportsForServer(imports, neededFns) {
49
+ const selected = [];
50
+ for (const line of imports) {
51
+ const bindings = parseNamedImportBindings(line);
52
+ if (bindings.length === 0)
53
+ continue;
54
+ if (bindings.some(name => neededFns.has(name))) {
55
+ selected.push(line);
56
+ }
57
+ }
58
+ return selected;
59
+ }
34
60
  /**
35
61
  * Compile a project's src/routes/ into .kuratchi/routes.js
36
62
  *
37
- * The generated module exports { app } — an object with a fetch() method
63
+ * The generated module exports { app } â€" an object with a fetch() method
38
64
  * that handles routing, load functions, form actions, and rendering.
39
65
  * Returns the path to .kuratchi/worker.js — the stable wrangler entry point that
40
66
  * re-exports everything from routes.js (default fetch handler + named DO class exports).
@@ -49,21 +75,21 @@ export function compile(options) {
49
75
  }
50
76
  // Discover all .html route files
51
77
  const routeFiles = discoverRoutes(routesDir);
52
- // Component compilation cache — only compile components that are actually imported
78
+ // Component compilation cache â€" only compile components that are actually imported
53
79
  const libDir = path.join(srcDir, 'lib');
54
- const compiledComponentCache = new Map(); // fileName → compiled function code
55
- const componentStyleCache = new Map(); // fileName → escaped CSS string (or empty)
80
+ const compiledComponentCache = new Map(); // fileName â†' compiled function code
81
+ const componentStyleCache = new Map(); // fileName â†' escaped CSS string (or empty)
56
82
  // Tracks which prop names inside a component are used as action={propName}.
57
- // e.g. db-studio uses action={runQueryAction} → stores 'runQueryAction'.
83
+ // e.g. db-studio uses action={runQueryAction} â†' stores 'runQueryAction'.
58
84
  // When the route passes runQueryAction={runAdminSqlQuery}, the compiler knows
59
85
  // to add 'runAdminSqlQuery' to the route's actionFunctions.
60
- const componentActionCache = new Map(); // fileName → Set of action prop names
86
+ const componentActionCache = new Map(); // fileName â†' Set of action prop names
61
87
  function compileComponent(fileName) {
62
88
  if (compiledComponentCache.has(fileName))
63
89
  return compiledComponentCache.get(fileName);
64
90
  let filePath;
65
91
  let funcName;
66
- // Package component: "@kuratchi/ui:badge" → resolve from package
92
+ // Package component: "@kuratchi/ui:badge" â†' resolve from package
67
93
  const pkgMatch = fileName.match(/^(@[^:]+):(.+)$/);
68
94
  if (pkgMatch) {
69
95
  const pkgName = pkgMatch[1]; // e.g. "@kuratchi/ui"
@@ -96,7 +122,7 @@ export function compile(options) {
96
122
  : '';
97
123
  // template source (parseFile already removes the <script> block)
98
124
  let source = compParsed.template;
99
- // Extract optional <style> block — CSS is scoped and injected once per route at compile time
125
+ // Extract optional <style> block â€" CSS is scoped and injected once per route at compile time
100
126
  let styleBlock = '';
101
127
  const styleMatch = source.match(/<style[\s>][\s\S]*?<\/style>/i);
102
128
  if (styleMatch) {
@@ -124,8 +150,8 @@ export function compile(options) {
124
150
  : '';
125
151
  componentStyleCache.set(fileName, escapedStyle);
126
152
  // Replace <slot></slot> and <slot /> with children output
127
- source = source.replace(/<slot\s*><\/slot>/g, '{=html props.children || ""}');
128
- source = source.replace(/<slot\s*\/>/g, '{=html props.children || ""}');
153
+ source = source.replace(/<slot\s*><\/slot>/g, '{@raw props.children || ""}');
154
+ source = source.replace(/<slot\s*\/>/g, '{@raw props.children || ""}');
129
155
  // Build a sub-component map from the component's own component imports so that
130
156
  // <Alert>, <Badge>, <Dialog>, etc. get expanded instead of emitted as raw tags.
131
157
  const subComponentNames = new Map();
@@ -142,7 +168,7 @@ export function compile(options) {
142
168
  }
143
169
  }
144
170
  // Scan the component template for action={propName} uses.
145
- // These prop names are "action props" — when the route passes actionProp={routeFn},
171
+ // These prop names are "action props" â€" when the route passes actionProp={routeFn},
146
172
  // the compiler knows to add routeFn to the route's actionFunctions so it ends up
147
173
  // in the route's actions map and can be dispatched at runtime.
148
174
  const actionPropNames = new Set();
@@ -162,7 +188,7 @@ export function compile(options) {
162
188
  compiledComponentCache.set(fileName, compiled);
163
189
  return compiled;
164
190
  }
165
- // App layout: src/routes/layout.html (convention — wraps all routes automatically)
191
+ // App layout: src/routes/layout.html (convention â€" wraps all routes automatically)
166
192
  const layoutFile = path.join(routesDir, 'layout.html');
167
193
  let compiledLayout = null;
168
194
  const layoutComponentNames = new Map();
@@ -444,6 +470,17 @@ export function compile(options) {
444
470
  if(!msg) return;
445
471
  if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
446
472
  }, true);
473
+ document.addEventListener('submit', function(e){
474
+ if(e.defaultPrevented) return;
475
+ var f = e.target;
476
+ if(!f || !f.querySelector) return;
477
+ var aInput = f.querySelector('input[name="_action"]');
478
+ if(!aInput) return;
479
+ var aName = aInput.value;
480
+ if(!aName) return;
481
+ f.setAttribute('data-action-loading', aName);
482
+ Array.prototype.slice.call(f.querySelectorAll('button[type="submit"],button:not([type="button"]):not([type="reset"])')).forEach(function(b){ b.disabled = true; });
483
+ }, true);
447
484
  document.addEventListener('change', function(e){
448
485
  var t = e.target;
449
486
  if(!t || !t.getAttribute) return;
@@ -458,21 +495,118 @@ export function compile(options) {
458
495
  }, true);
459
496
  by('[data-select-all]').forEach(function(m){ var g = m.getAttribute('data-select-all'); if(g) syncGroup(g); });
460
497
  })();`;
498
+ const reactiveRuntimeSource = `(function(g){
499
+ if(g.__kuratchiReactive) return;
500
+ const targetMap = new WeakMap();
501
+ const proxyMap = new WeakMap();
502
+ let active = null;
503
+ const queue = new Set();
504
+ let flushing = false;
505
+ function queueRun(fn){
506
+ queue.add(fn);
507
+ if(flushing) return;
508
+ flushing = true;
509
+ Promise.resolve().then(function(){
510
+ try {
511
+ const jobs = Array.from(queue);
512
+ queue.clear();
513
+ for (const job of jobs) job();
514
+ } finally {
515
+ flushing = false;
516
+ }
517
+ });
518
+ }
519
+ function cleanup(effect){
520
+ const deps = effect.__deps || [];
521
+ for (const dep of deps) dep.delete(effect);
522
+ effect.__deps = [];
523
+ }
524
+ function track(target, key){
525
+ if(!active) return;
526
+ let depsMap = targetMap.get(target);
527
+ if(!depsMap){ depsMap = new Map(); targetMap.set(target, depsMap); }
528
+ let dep = depsMap.get(key);
529
+ if(!dep){ dep = new Set(); depsMap.set(key, dep); }
530
+ if(dep.has(active)) return;
531
+ dep.add(active);
532
+ if(!active.__deps) active.__deps = [];
533
+ active.__deps.push(dep);
534
+ }
535
+ function trigger(target, key){
536
+ const depsMap = targetMap.get(target);
537
+ if(!depsMap) return;
538
+ const effects = new Set();
539
+ const add = function(k){
540
+ const dep = depsMap.get(k);
541
+ if(dep) dep.forEach(function(e){ effects.add(e); });
542
+ };
543
+ add(key);
544
+ add('*');
545
+ effects.forEach(function(e){ queueRun(e); });
546
+ }
547
+ function isObject(value){ return value !== null && typeof value === 'object'; }
548
+ function proxify(value){
549
+ if(!isObject(value)) return value;
550
+ if(proxyMap.has(value)) return proxyMap.get(value);
551
+ const proxy = new Proxy(value, {
552
+ get(target, key, receiver){
553
+ track(target, key);
554
+ const out = Reflect.get(target, key, receiver);
555
+ return isObject(out) ? proxify(out) : out;
556
+ },
557
+ set(target, key, next, receiver){
558
+ const prev = target[key];
559
+ const result = Reflect.set(target, key, next, receiver);
560
+ if(prev !== next) trigger(target, key);
561
+ if(Array.isArray(target) && key !== 'length') trigger(target, 'length');
562
+ return result;
563
+ },
564
+ deleteProperty(target, key){
565
+ const had = Object.prototype.hasOwnProperty.call(target, key);
566
+ const result = Reflect.deleteProperty(target, key);
567
+ if(had) trigger(target, key);
568
+ return result;
569
+ }
570
+ });
571
+ proxyMap.set(value, proxy);
572
+ return proxy;
573
+ }
574
+ function effect(fn){
575
+ const run = function(){
576
+ cleanup(run);
577
+ active = run;
578
+ try { fn(); } finally { active = null; }
579
+ };
580
+ run.__deps = [];
581
+ run();
582
+ return function(){ cleanup(run); };
583
+ }
584
+ function state(initial){ return proxify(initial); }
585
+ function replace(_prev, next){ return proxify(next); }
586
+ g.__kuratchiReactive = { state, effect, replace };
587
+ })(window);`;
461
588
  const actionScript = `<script>${options.isDev ? bridgeSource : compactInlineJs(bridgeSource)}</script>`;
589
+ const reactiveRuntimeScript = `<script>${options.isDev ? reactiveRuntimeSource : compactInlineJs(reactiveRuntimeSource)}</script>`;
590
+ if (source.includes('</head>')) {
591
+ source = source.replace('</head>', reactiveRuntimeScript + '\n</head>');
592
+ }
593
+ else {
594
+ source = reactiveRuntimeScript + '\n' + source;
595
+ }
462
596
  source = source.replace('</body>', actionScript + '\n</body>');
463
597
  // Parse layout for <script> block (component imports + data vars)
464
598
  const layoutParsed = parseFile(source);
465
599
  const hasLayoutScript = layoutParsed.script && (Object.keys(layoutParsed.componentImports).length > 0 || layoutParsed.hasLoad);
466
600
  if (hasLayoutScript) {
467
- // Dynamic layout — has component imports and/or data declarations
601
+ // Dynamic layout â€" has component imports and/or data declarations
468
602
  // Compile component imports from layout
469
603
  for (const [pascalName, fileName] of Object.entries(layoutParsed.componentImports)) {
470
604
  compileComponent(fileName);
471
605
  layoutComponentNames.set(pascalName, fileName);
472
606
  }
473
607
  // Replace <slot></slot> with content parameter injection
474
- let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{=html __content}');
475
- layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{=html __content}');
608
+ let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{@raw __content}');
609
+ layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{@raw __content}');
476
610
  // Build layout action names so action={fn} works in layouts
477
611
  const layoutActionNames = new Set(layoutParsed.actionFunctions);
478
612
  // Compile the layout template with component + action support
@@ -500,7 +634,7 @@ export function compile(options) {
500
634
  }`;
501
635
  }
502
636
  else {
503
- // Static layout — no components, use fast string split (original behavior)
637
+ // Static layout â€" no components, use fast string split (original behavior)
504
638
  const slotMarker = '<slot></slot>';
505
639
  const slotIdx = source.indexOf(slotMarker);
506
640
  if (slotIdx === -1) {
@@ -513,7 +647,7 @@ export function compile(options) {
513
647
  }
514
648
  }
515
649
  // Custom error pages: src/routes/NNN.html (e.g. 404.html, 500.html, 401.html, 403.html)
516
- // Only compiled if the user explicitly creates them — otherwise the framework's built-in default is used
650
+ // Only compiled if the user explicitly creates them â€" otherwise the framework's built-in default is used
517
651
  const compiledErrorPages = new Map();
518
652
  for (const file of fs.readdirSync(routesDir)) {
519
653
  const match = file.match(/^(\d{3})\.html$/);
@@ -531,6 +665,8 @@ export function compile(options) {
531
665
  const authConfig = readAuthConfig(projectDir);
532
666
  // Read Durable Object config and discover handler files
533
667
  const doConfig = readDoConfig(projectDir);
668
+ const containerConfig = readWorkerClassConfig(projectDir, 'containers');
669
+ const workflowConfig = readWorkerClassConfig(projectDir, 'workflows');
534
670
  const doHandlers = doConfig.length > 0
535
671
  ? discoverDoHandlers(srcDir, doConfig, ormDatabases)
536
672
  : [];
@@ -571,11 +707,125 @@ export function compile(options) {
571
707
  }
572
708
  }
573
709
  }
710
+ const resolveExistingModuleFile = (absBase) => {
711
+ const candidates = [
712
+ absBase,
713
+ absBase + '.ts',
714
+ absBase + '.js',
715
+ absBase + '.mjs',
716
+ absBase + '.cjs',
717
+ path.join(absBase, 'index.ts'),
718
+ path.join(absBase, 'index.js'),
719
+ path.join(absBase, 'index.mjs'),
720
+ path.join(absBase, 'index.cjs'),
721
+ ];
722
+ for (const candidate of candidates) {
723
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
724
+ return candidate;
725
+ }
726
+ return null;
727
+ };
728
+ const toModuleSpecifier = (fromFileAbs, toFileAbs) => {
729
+ let rel = path.relative(path.dirname(fromFileAbs), toFileAbs).replace(/\\/g, '/');
730
+ if (!rel.startsWith('.'))
731
+ rel = './' + rel;
732
+ return rel;
733
+ };
734
+ const transformedServerModules = new Map();
735
+ const modulesOutDir = path.join(projectDir, '.kuratchi', 'modules');
736
+ const resolveDoProxyTarget = (absPath) => {
737
+ const normalizedNoExt = absPath.replace(/\\/g, '/').replace(/\.[^.\/]+$/, '');
738
+ const proxyNoExt = doHandlerProxyPaths.get(normalizedNoExt);
739
+ if (!proxyNoExt)
740
+ return null;
741
+ return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.js') ? proxyNoExt + '.js' : null);
742
+ };
743
+ const resolveImportTarget = (importerAbs, spec) => {
744
+ if (spec.startsWith('$')) {
745
+ const slashIdx = spec.indexOf('/');
746
+ const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
747
+ const rest = slashIdx === -1 ? '' : spec.slice(slashIdx + 1);
748
+ if (!folder)
749
+ return null;
750
+ const abs = path.join(srcDir, folder, rest);
751
+ return resolveExistingModuleFile(abs) ?? abs;
752
+ }
753
+ if (spec.startsWith('.')) {
754
+ const abs = path.resolve(path.dirname(importerAbs), spec);
755
+ return resolveExistingModuleFile(abs) ?? abs;
756
+ }
757
+ return null;
758
+ };
759
+ const transformServerModule = (entryAbsPath) => {
760
+ const resolved = resolveExistingModuleFile(entryAbsPath) ?? entryAbsPath;
761
+ const normalized = resolved.replace(/\\/g, '/');
762
+ const cached = transformedServerModules.get(normalized);
763
+ if (cached)
764
+ return cached;
765
+ const relFromProject = path.relative(projectDir, resolved);
766
+ const outPath = path.join(modulesOutDir, relFromProject);
767
+ transformedServerModules.set(normalized, outPath);
768
+ const outDir = path.dirname(outPath);
769
+ if (!fs.existsSync(outDir))
770
+ fs.mkdirSync(outDir, { recursive: true });
771
+ if (!/\.(ts|js|mjs|cjs)$/i.test(resolved) || !fs.existsSync(resolved)) {
772
+ const passthrough = resolved;
773
+ transformedServerModules.set(normalized, passthrough);
774
+ return passthrough;
775
+ }
776
+ const source = fs.readFileSync(resolved, 'utf-8');
777
+ const rewriteSpecifier = (spec) => {
778
+ const target = resolveImportTarget(resolved, spec);
779
+ if (!target)
780
+ return spec;
781
+ const doProxyTarget = resolveDoProxyTarget(target);
782
+ if (doProxyTarget)
783
+ return toModuleSpecifier(outPath, doProxyTarget);
784
+ const normalizedTarget = target.replace(/\\/g, '/');
785
+ const inProject = normalizedTarget.startsWith(projectDir.replace(/\\/g, '/') + '/');
786
+ if (!inProject)
787
+ return spec;
788
+ const targetResolved = resolveExistingModuleFile(target) ?? target;
789
+ if (!/\.(ts|js|mjs|cjs)$/i.test(targetResolved))
790
+ return spec;
791
+ const rewrittenTarget = transformServerModule(targetResolved);
792
+ return toModuleSpecifier(outPath, rewrittenTarget);
793
+ };
794
+ let rewritten = source.replace(/(from\s+)(['"])([^'"]+)\2/g, (_m, p1, q, spec) => {
795
+ return `${p1}${q}${rewriteSpecifier(spec)}${q}`;
796
+ });
797
+ rewritten = rewritten.replace(/(import\s*\(\s*)(['"])([^'"]+)\2(\s*\))/g, (_m, p1, q, spec, p4) => {
798
+ return `${p1}${q}${rewriteSpecifier(spec)}${q}${p4}`;
799
+ });
800
+ writeIfChanged(outPath, rewritten);
801
+ return outPath;
802
+ };
803
+ const resolveCompiledImportPath = (origPath, importerDir, outFileDir) => {
804
+ const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
805
+ if (isBareModule)
806
+ return origPath;
807
+ let absImport;
808
+ if (origPath.startsWith('$')) {
809
+ const slashIdx = origPath.indexOf('/');
810
+ const folder = slashIdx === -1 ? origPath.slice(1) : origPath.slice(1, slashIdx);
811
+ const rest = slashIdx === -1 ? '' : origPath.slice(slashIdx + 1);
812
+ absImport = path.join(srcDir, folder, rest);
813
+ }
814
+ else {
815
+ absImport = path.resolve(importerDir, origPath);
816
+ }
817
+ const doProxyTarget = resolveDoProxyTarget(absImport);
818
+ const target = doProxyTarget ?? transformServerModule(absImport);
819
+ let relPath = path.relative(outFileDir, target).replace(/\\/g, '/');
820
+ if (!relPath.startsWith('.'))
821
+ relPath = './' + relPath;
822
+ return relPath;
823
+ };
574
824
  // Parse and compile each route
575
825
  const compiledRoutes = [];
576
826
  const allImports = [];
577
827
  let moduleCounter = 0;
578
- // Layout server import resolution — resolve non-component imports to module IDs
828
+ // Layout server import resolution â€" resolve non-component imports to module IDs
579
829
  let isLayoutAsync = false;
580
830
  let compiledLayoutActions = null;
581
831
  if (compiledLayout && fs.existsSync(path.join(routesDir, 'layout.html'))) {
@@ -590,39 +840,7 @@ export function compile(options) {
590
840
  if (!pathMatch)
591
841
  continue;
592
842
  const origPath = pathMatch[1];
593
- const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
594
- let importPath;
595
- if (isBareModule) {
596
- importPath = origPath;
597
- }
598
- else {
599
- let absImport;
600
- if (origPath.startsWith('$')) {
601
- const slashIdx = origPath.indexOf('/');
602
- const folder = origPath.slice(1, slashIdx);
603
- const rest = origPath.slice(slashIdx + 1);
604
- absImport = path.join(srcDir, folder, rest);
605
- // Redirect DO handler imports to generated proxy modules
606
- const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
607
- if (doProxyPath) {
608
- absImport = doProxyPath;
609
- }
610
- }
611
- else {
612
- absImport = path.resolve(layoutFileDir, origPath);
613
- }
614
- let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
615
- if (!relPath.startsWith('.'))
616
- relPath = './' + relPath;
617
- let resolvedExt = '';
618
- for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
619
- if (fs.existsSync(absImport + ext)) {
620
- resolvedExt = ext;
621
- break;
622
- }
623
- }
624
- importPath = relPath + resolvedExt;
625
- }
843
+ const importPath = resolveCompiledImportPath(origPath, layoutFileDir, outFileDir);
626
844
  const moduleId = `__m${moduleCounter++}`;
627
845
  allImports.push(`import * as ${moduleId} from '${importPath}';`);
628
846
  const namesMatch = imp.match(/import\s*\{([^}]+)\}/);
@@ -663,7 +881,7 @@ export function compile(options) {
663
881
  }
664
882
  }
665
883
  }
666
- // Detect if the compiled layout uses await → make it async
884
+ // Detect if the compiled layout uses await â†' make it async
667
885
  isLayoutAsync = /\bawait\b/.test(compiledLayout);
668
886
  if (isLayoutAsync) {
669
887
  compiledLayout = compiledLayout.replace(/^function __layout\(/, 'async function __layout(');
@@ -672,72 +890,62 @@ export function compile(options) {
672
890
  for (let i = 0; i < routeFiles.length; i++) {
673
891
  const rf = routeFiles[i];
674
892
  const fullPath = path.join(routesDir, rf.file);
893
+ const pattern = filePathToPattern(rf.name);
894
+ // -- API route (index.ts / index.js) --
895
+ if (rf.type === 'api') {
896
+ const outFileDir = path.join(projectDir, '.kuratchi');
897
+ // Resolve the route file's absolute path through transformServerModule
898
+ // so that $-prefixed imports inside it get rewritten correctly
899
+ const absRoutePath = transformServerModule(fullPath);
900
+ let importPath = path.relative(outFileDir, absRoutePath).replace(/\\/g, '/');
901
+ if (!importPath.startsWith('.'))
902
+ importPath = './' + importPath;
903
+ const moduleId = `__m${moduleCounter++}`;
904
+ allImports.push(`import * as ${moduleId} from '${importPath}';`);
905
+ // Scan the source for exported method handlers (only include what exists)
906
+ const apiSource = fs.readFileSync(fullPath, 'utf-8');
907
+ const allMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
908
+ const exportedMethods = allMethods.filter(m => {
909
+ // Match: export function GET, export async function GET, export { ... as GET }
910
+ const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`);
911
+ const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${m}\\b`);
912
+ const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${m}\\b`);
913
+ return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
914
+ });
915
+ const methodEntries = exportedMethods
916
+ .map(m => `${m}: ${moduleId}.${m}`)
917
+ .join(', ');
918
+ compiledRoutes.push(`{ pattern: '${pattern}', __api: true, ${methodEntries} }`);
919
+ continue;
920
+ }
921
+ // ── Page route (page.html) ──
675
922
  const source = fs.readFileSync(fullPath, 'utf-8');
676
923
  const parsed = parseFile(source);
677
- const pattern = filePathToPattern(rf.name);
678
- // Build a mapping: functionName → moduleId for all imports in this route
924
+ // Build a mapping: functionName â†' moduleId for all imports in this route
679
925
  const fnToModule = {};
680
926
  const outFileDir = path.join(projectDir, '.kuratchi');
681
- if (parsed.serverImports.length > 0) {
927
+ const neededServerFns = new Set([
928
+ ...parsed.actionFunctions,
929
+ ...parsed.pollFunctions,
930
+ ...parsed.dataGetQueries.map((q) => q.fnName),
931
+ ]);
932
+ const routeServerImports = parsed.serverImports.length > 0
933
+ ? parsed.serverImports
934
+ : filterClientImportsForServer(parsed.clientImports, neededServerFns);
935
+ if (routeServerImports.length > 0) {
682
936
  const routeFileDir = path.dirname(fullPath);
683
- for (const imp of parsed.serverImports) {
937
+ for (const imp of routeServerImports) {
684
938
  const pathMatch = imp.match(/from\s+['"]([^'"]+)['"]/);
685
939
  if (!pathMatch)
686
940
  continue;
687
941
  const origPath = pathMatch[1];
688
- // Bare module specifiers (packages) — pass through as-is
689
- const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
690
- let importPath;
691
- if (isBareModule) {
692
- // Package import: @kuratchi/auth, KuratchiJS, cloudflare:workers, etc.
693
- importPath = origPath;
694
- }
695
- else {
696
- let absImport;
697
- if (origPath.startsWith('$')) {
698
- // Dynamic $folder/ alias → src/folder/
699
- const slashIdx = origPath.indexOf('/');
700
- const folder = origPath.slice(1, slashIdx);
701
- const rest = origPath.slice(slashIdx + 1);
702
- absImport = path.join(srcDir, folder, rest);
703
- // Redirect DO handler imports to generated proxy modules
704
- const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
705
- if (doProxyPath) {
706
- absImport = doProxyPath;
707
- }
708
- }
709
- else {
710
- // Resolve the import relative to the route file
711
- absImport = path.resolve(routeFileDir, origPath);
712
- }
713
- // Make it relative to the output directory
714
- let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
715
- if (!relPath.startsWith('.'))
716
- relPath = './' + relPath;
717
- // Check if the resolved file exists (try .ts, .js extensions)
718
- let resolvedExt = '';
719
- for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
720
- if (fs.existsSync(absImport + ext)) {
721
- resolvedExt = ext;
722
- break;
723
- }
724
- }
725
- importPath = relPath + resolvedExt;
726
- }
942
+ // Bare module specifiers (packages) â€" pass through as-is
943
+ const importPath = resolveCompiledImportPath(origPath, routeFileDir, outFileDir);
727
944
  const moduleId = `__m${moduleCounter++}`;
728
945
  allImports.push(`import * as ${moduleId} from '${importPath}';`);
729
946
  // Extract named imports and map them to this module
730
- const namesMatch = imp.match(/import\s*\{([^}]+)\}/);
731
- if (namesMatch) {
732
- const names = namesMatch[1]
733
- .split(',')
734
- .map(n => n.trim())
735
- .filter(Boolean)
736
- .map(n => {
737
- const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
738
- return parts[1] || parts[0] || '';
739
- })
740
- .filter(Boolean);
947
+ const names = parseNamedImportBindings(imp);
948
+ if (names.length > 0) {
741
949
  for (const name of names) {
742
950
  fnToModule[name] = moduleId;
743
951
  }
@@ -750,7 +958,7 @@ export function compile(options) {
750
958
  }
751
959
  }
752
960
  // Build per-route component names from explicit imports
753
- // componentImports: { StatCard: 'stat-card' } → componentNames maps PascalCase → fileName
961
+ // componentImports: { StatCard: 'stat-card' } â†' componentNames maps PascalCase â†' fileName
754
962
  const routeComponentNames = new Map();
755
963
  for (const [pascalName, fileName] of Object.entries(parsed.componentImports)) {
756
964
  // Compile the component on first use
@@ -765,22 +973,22 @@ export function compile(options) {
765
973
  // We then scan the route template for that component's usage and extract the bound values.
766
974
  for (const [pascalName, compFileName] of routeComponentNames.entries()) {
767
975
  const actionPropNames = componentActionCache.get(compFileName);
768
- if (!actionPropNames || actionPropNames.size === 0)
769
- continue;
770
976
  // Find all usages of <PascalName ...> in the route template and extract prop bindings.
771
977
  // Match <ComponentName ... propName={value} ... > across multiple lines.
772
978
  const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)/>`, 'g');
773
979
  for (const tagMatch of parsed.template.matchAll(compTagRegex)) {
774
980
  const attrs = tagMatch[1];
775
- for (const propName of actionPropNames) {
776
- // Find propName={identifier} binding
777
- const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
778
- const propMatch = attrs.match(propRegex);
779
- if (propMatch) {
780
- const routeFnName = propMatch[1];
781
- // Only add if this function is actually imported by the route
782
- if (routeFnName in fnToModule && !parsed.actionFunctions.includes(routeFnName)) {
783
- parsed.actionFunctions.push(routeFnName);
981
+ if (actionPropNames && actionPropNames.size > 0) {
982
+ for (const propName of actionPropNames) {
983
+ // Find propName={identifier} binding
984
+ const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
985
+ const propMatch = attrs.match(propRegex);
986
+ if (propMatch) {
987
+ const routeFnName = propMatch[1];
988
+ // Only add if this function is actually imported by the route
989
+ if (routeFnName in fnToModule && !parsed.actionFunctions.includes(routeFnName)) {
990
+ parsed.actionFunctions.push(routeFnName);
991
+ }
784
992
  }
785
993
  }
786
994
  }
@@ -789,7 +997,7 @@ export function compile(options) {
789
997
  // Compile template to render function body (pass component names and action names)
790
998
  // An identifier is a valid server action if it is either:
791
999
  // 1. Directly imported (present in fnToModule), or
792
- // 2. A top-level script declaration (present in dataVars) — covers cases like
1000
+ // 2. A top-level script declaration (present in dataVars) â€" covers cases like
793
1001
  // `const fn = importedFn` or `async function fn() {}` where the binding
794
1002
  // is locally declared but delegates to an imported function.
795
1003
  const dataVarsSet = new Set(parsed.dataVars);
@@ -867,6 +1075,17 @@ export function compile(options) {
867
1075
  // Collect only the components that were actually imported by routes
868
1076
  const compiledComponents = Array.from(compiledComponentCache.values());
869
1077
  // Generate the routes module
1078
+ const rawRuntimeImportPath = resolveRuntimeImportPath(projectDir);
1079
+ let runtimeImportPath;
1080
+ if (rawRuntimeImportPath) {
1081
+ // Resolve the runtime file's absolute path and pass it through transformServerModule
1082
+ // so that $durable-objects/* and other project imports get rewritten to their proxies.
1083
+ const runtimeAbs = path.resolve(path.join(projectDir, '.kuratchi'), rawRuntimeImportPath);
1084
+ const transformedRuntimePath = transformServerModule(runtimeAbs);
1085
+ const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
1086
+ runtimeImportPath = toModuleSpecifier(outFile, transformedRuntimePath);
1087
+ }
1088
+ const hasRuntime = !!runtimeImportPath;
870
1089
  const output = generateRoutesModule({
871
1090
  projectDir,
872
1091
  serverImports: allImports,
@@ -882,6 +1101,8 @@ export function compile(options) {
882
1101
  isDev: options.isDev ?? false,
883
1102
  isLayoutAsync,
884
1103
  compiledLayoutActions,
1104
+ hasRuntime,
1105
+ runtimeImportPath,
885
1106
  });
886
1107
  // Write to .kuratchi/routes.js
887
1108
  const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
@@ -895,16 +1116,25 @@ export function compile(options) {
895
1116
  // worker.js explicitly re-exports them so wrangler.jsonc can reference a
896
1117
  // stable filename while routes.js is freely regenerated.
897
1118
  const workerFile = path.join(outDir, 'worker.js');
1119
+ const workerClassExports = [...containerConfig, ...workflowConfig]
1120
+ .map((entry) => {
1121
+ const importPath = toWorkerImportPath(projectDir, outDir, entry.file);
1122
+ if (entry.exportKind === 'default') {
1123
+ return `export { default as ${entry.className} } from '${importPath}';`;
1124
+ }
1125
+ return `export { ${entry.className} } from '${importPath}';`;
1126
+ });
898
1127
  const workerLines = [
899
1128
  '// Auto-generated by kuratchi \u2014 do not edit.',
900
1129
  "export { default } from './routes.js';",
901
1130
  ...doConfig.map(c => `export { ${c.className} } from './routes.js';`),
1131
+ ...workerClassExports,
902
1132
  '',
903
1133
  ];
904
1134
  writeIfChanged(workerFile, workerLines.join('\n'));
905
1135
  return workerFile;
906
1136
  }
907
- // ── Helpers ─────────────────────────────────────────────────────────────
1137
+ // â"€â"€ Helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
908
1138
  /**
909
1139
  * Write a file only if its content has changed.
910
1140
  * Prevents unnecessary filesystem events that would retrigger wrangler's file watcher.
@@ -998,7 +1228,7 @@ function readUiTheme(projectDir) {
998
1228
  console.warn('[kuratchi] ui.theme: "default" configured but @kuratchi/ui theme.css not found');
999
1229
  return null;
1000
1230
  }
1001
- // Custom path — resolve relative to project root
1231
+ // Custom path â€" resolve relative to project root
1002
1232
  const customPath = path.resolve(projectDir, themeValue);
1003
1233
  if (fs.existsSync(customPath)) {
1004
1234
  return fs.readFileSync(customPath, 'utf-8');
@@ -1016,7 +1246,7 @@ function resolvePackageComponent(projectDir, pkgName, componentFile) {
1016
1246
  if (fs.existsSync(nmPath))
1017
1247
  return nmPath;
1018
1248
  // 2. Try workspace layout: project is in apps/X or packages/X, sibling packages in packages/
1019
- // @kuratchi/ui → kuratchi-ui (convention: scope stripped, slash → dash)
1249
+ // @kuratchi/ui â†' kuratchi-ui (convention: scope stripped, slash â†' dash)
1020
1250
  const pkgDirName = pkgName.replace(/^@/, '').replace(/\//g, '-');
1021
1251
  const workspaceRoot = path.resolve(projectDir, '../..');
1022
1252
  const wsPath = path.join(workspaceRoot, 'packages', pkgDirName, 'src', 'lib', componentFile + '.html');
@@ -1054,32 +1284,49 @@ function discoverRoutes(routesDir) {
1054
1284
  for (const entry of entries) {
1055
1285
  if (entry.isDirectory()) {
1056
1286
  const childPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
1057
- // Folder-based route: routes/db/page.html → /db
1287
+ // Folder-based page route: routes/db/page.html /db
1058
1288
  const pageFile = path.join(dir, entry.name, 'page.html');
1059
1289
  if (fs.existsSync(pageFile)) {
1060
1290
  const routeFile = `${childPrefix}/page.html`;
1061
1291
  if (!registered.has(routeFile)) {
1062
1292
  registered.add(routeFile);
1063
- results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix) });
1293
+ results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix), type: 'page' });
1294
+ }
1295
+ }
1296
+ // Folder-based API route: routes/api/v1/health/index.ts -> /api/v1/health
1297
+ const apiFile = ['index.ts', 'index.js'].find(f => fs.existsSync(path.join(dir, entry.name, f)));
1298
+ if (apiFile && !fs.existsSync(pageFile)) {
1299
+ const routeFile = `${childPrefix}/${apiFile}`;
1300
+ if (!registered.has(routeFile)) {
1301
+ registered.add(routeFile);
1302
+ results.push({ file: routeFile, name: childPrefix, layouts: [], type: 'api' });
1064
1303
  }
1065
1304
  }
1066
1305
  // Always recurse into subdirectory (for nested routes like /admin/roles)
1067
1306
  walk(path.join(dir, entry.name), childPrefix);
1068
1307
  }
1069
1308
  else if (entry.name === 'layout.html' || entry.name === '404.html' || entry.name === '500.html') {
1070
- // Skip — layout.html is the app layout, 404/500 are error pages, not routes
1309
+ // Skip layout.html is the app layout, 404/500 are error pages, not routes
1071
1310
  continue;
1072
1311
  }
1312
+ else if (entry.name === 'index.ts' || entry.name === 'index.js') {
1313
+ // API route file in current directory -> index API route for this prefix
1314
+ const routeFile = prefix ? `${prefix}/${entry.name}` : entry.name;
1315
+ if (!registered.has(routeFile)) {
1316
+ registered.add(routeFile);
1317
+ results.push({ file: routeFile, name: prefix || 'index', layouts: [], type: 'api' });
1318
+ }
1319
+ }
1073
1320
  else if (entry.name === 'page.html') {
1074
- // page.html in current directory → index route for this prefix
1321
+ // page.html in current directory index route for this prefix
1075
1322
  const routeFile = prefix ? `${prefix}/page.html` : 'page.html';
1076
1323
  if (!registered.has(routeFile)) {
1077
1324
  registered.add(routeFile);
1078
- results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix) });
1325
+ results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix), type: 'page' });
1079
1326
  }
1080
1327
  }
1081
1328
  else if (entry.name.endsWith('.html') && entry.name !== 'page.html') {
1082
- // File-based route: routes/about.html → /about (fallback)
1329
+ // File-based route: routes/about.html /about (fallback)
1083
1330
  const name = prefix
1084
1331
  ? `${prefix}/${entry.name.replace('.html', '')}`
1085
1332
  : entry.name.replace('.html', '');
@@ -1087,6 +1334,7 @@ function discoverRoutes(routesDir) {
1087
1334
  file: prefix ? `${prefix}/${entry.name}` : entry.name,
1088
1335
  name,
1089
1336
  layouts: getLayoutsForPrefix(prefix),
1337
+ type: 'page',
1090
1338
  });
1091
1339
  }
1092
1340
  }
@@ -1105,18 +1353,33 @@ function buildRouteObject(opts) {
1105
1353
  const hasFns = Object.keys(fnToModule).length > 0;
1106
1354
  const parts = [];
1107
1355
  parts.push(` pattern: '${pattern}'`);
1108
- // Load function — generated from the script block's top-level code + data-get queries
1356
+ const queryVars = parsed.dataGetQueries?.map((q) => q.asName) ?? [];
1357
+ let scriptBody = parsed.script
1358
+ ? parsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim()
1359
+ : '';
1360
+ for (const [fnName, moduleId] of Object.entries(fnToModule)) {
1361
+ if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
1362
+ continue;
1363
+ const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
1364
+ scriptBody = scriptBody.replace(callRegex, `${moduleId}.${fnName}(`);
1365
+ }
1366
+ const scriptUsesAwait = /\bawait\b/.test(scriptBody);
1367
+ const scriptReturnVars = parsed.script
1368
+ ? parsed.dataVars.filter((v) => !queryVars.includes(v))
1369
+ : [];
1370
+ // Load function â€" internal server prepass for async route script bodies
1371
+ // and data-get query state hydration.
1109
1372
  const hasDataGetQueries = Array.isArray(parsed.dataGetQueries) && parsed.dataGetQueries.length > 0;
1110
- if ((parsed.hasLoad && parsed.script) || hasDataGetQueries) {
1111
- // Get script body (everything except imports)
1112
- let body = parsed.script
1113
- ? parsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim()
1114
- : '';
1373
+ if ((scriptBody && scriptUsesAwait) || hasDataGetQueries) {
1374
+ let loadBody = '';
1375
+ if (scriptBody && scriptUsesAwait) {
1376
+ loadBody = scriptBody;
1377
+ }
1115
1378
  // Inject data-get query state blocks into load scope.
1116
1379
  // Each query exposes:
1117
1380
  // { state, loading, error, data, empty, success }
1381
+ const queries = parsed.dataGetQueries;
1118
1382
  if (hasDataGetQueries) {
1119
- const queries = parsed.dataGetQueries;
1120
1383
  const queryLines = [];
1121
1384
  for (const q of queries) {
1122
1385
  const fnName = q.fnName;
@@ -1139,25 +1402,13 @@ function buildRouteObject(opts) {
1139
1402
  queryLines.push(` }`);
1140
1403
  queryLines.push(`}`);
1141
1404
  }
1142
- body = [body, queryLines.join('\n')].filter(Boolean).join('\n');
1405
+ loadBody = [loadBody, queryLines.join('\n')].filter(Boolean).join('\n');
1143
1406
  }
1144
- // Rewrite imported function calls: fnName( → __mN.fnName(
1145
- // Rewrite imported function calls to use the module namespace
1146
- for (const [fnName, moduleId] of Object.entries(fnToModule)) {
1147
- if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
1148
- continue;
1149
- const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
1150
- body = body.replace(callRegex, `${moduleId}.${fnName}(`);
1151
- }
1152
- // Determine if body uses await
1153
- const isAsync = /\bawait\b/.test(body) || hasDataGetQueries;
1154
- // Return an object with all declared data variables
1155
- const returnObj = parsed.dataVars.length > 0
1156
- ? `\n return { ${parsed.dataVars.join(', ')} };`
1157
- : '';
1158
- parts.push(` ${isAsync ? 'async ' : ''}load(params = {}) {\n ${body}${returnObj}\n }`);
1407
+ const loadReturnVars = [...scriptReturnVars, ...queryVars];
1408
+ const returnObj = loadReturnVars.length > 0 ? `\n return { ${loadReturnVars.join(', ')} };` : '';
1409
+ parts.push(` async load(params = {}) {\n ${loadBody}${returnObj}\n }`);
1159
1410
  }
1160
- // Actions — functions referenced via action={fn} in the template
1411
+ // Actions â€" functions referenced via action={fn} in the template
1161
1412
  if (hasFns && parsed.actionFunctions.length > 0) {
1162
1413
  const actionEntries = parsed.actionFunctions
1163
1414
  .map(fn => {
@@ -1167,7 +1418,7 @@ function buildRouteObject(opts) {
1167
1418
  .join(', ');
1168
1419
  parts.push(` actions: { ${actionEntries} }`);
1169
1420
  }
1170
- // RPC — functions referenced via data-poll={fn(args)} in the template
1421
+ // RPC â€" functions referenced via data-poll={fn(args)} in the template
1171
1422
  if (hasFns && parsed.pollFunctions.length > 0) {
1172
1423
  const rpcEntries = parsed.pollFunctions
1173
1424
  .map(fn => {
@@ -1178,12 +1429,21 @@ function buildRouteObject(opts) {
1178
1429
  .join(', ');
1179
1430
  parts.push(` rpc: { ${rpcEntries} }`);
1180
1431
  }
1181
- // Render function — template compiled to JS with native flow control
1432
+ // Render function â€" template compiled to JS with native flow control
1182
1433
  // Destructure data vars so templates reference them directly (e.g., {todos} not {data.todos})
1183
- // Always include __error so templates can show form action errors via {__error}
1184
- const allVars = [...parsed.dataVars];
1185
- if (!allVars.includes('__error'))
1186
- allVars.push('__error');
1434
+ // Auto-inject action state objects so templates can reference signIn.error, signIn.loading, etc.
1435
+ const renderPrelude = (scriptBody && !scriptUsesAwait) ? scriptBody : '';
1436
+ const allVars = [...queryVars];
1437
+ if (scriptUsesAwait) {
1438
+ for (const v of scriptReturnVars) {
1439
+ if (!allVars.includes(v))
1440
+ allVars.push(v);
1441
+ }
1442
+ }
1443
+ for (const fn of parsed.actionFunctions) {
1444
+ if (!allVars.includes(fn))
1445
+ allVars.push(fn);
1446
+ }
1187
1447
  if (!allVars.includes('params'))
1188
1448
  allVars.push('params');
1189
1449
  if (!allVars.includes('breadcrumbs'))
@@ -1198,7 +1458,7 @@ function buildRouteObject(opts) {
1198
1458
  finalRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
1199
1459
  }
1200
1460
  parts.push(` render(data) {
1201
- ${destructure}${finalRenderBody}
1461
+ ${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
1202
1462
  return __html;
1203
1463
  }`);
1204
1464
  return ` {\n${parts.join(',\n')}\n }`;
@@ -1212,7 +1472,7 @@ function readOrmConfig(projectDir) {
1212
1472
  if (!ormBlock)
1213
1473
  return [];
1214
1474
  // Extract schema imports: import { todoSchema } from './src/schemas/todo';
1215
- const importMap = new Map(); // exportName → importPath
1475
+ const importMap = new Map(); // exportName â†' importPath
1216
1476
  const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
1217
1477
  let m;
1218
1478
  while ((m = importRegex.exec(source)) !== null) {
@@ -1349,7 +1609,7 @@ function readDoConfig(projectDir) {
1349
1609
  if (list.length > 0)
1350
1610
  entry.files = list;
1351
1611
  }
1352
- // (inject config removed — DO methods are org-scoped, no auto-injection needed)
1612
+ // (inject config removed â€" DO methods are org-scoped, no auto-injection needed)
1353
1613
  entries.push(entry);
1354
1614
  }
1355
1615
  // Match string shorthand: BINDING: 'ClassName' (skip bindings already found)
@@ -1365,6 +1625,89 @@ function readDoConfig(projectDir) {
1365
1625
  }
1366
1626
  return entries;
1367
1627
  }
1628
+ function readWorkerClassConfig(projectDir, key) {
1629
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
1630
+ if (!fs.existsSync(configPath))
1631
+ return [];
1632
+ const source = fs.readFileSync(configPath, 'utf-8');
1633
+ const keyIdx = source.search(new RegExp(`\\b${key}\\s*:\\s*\\{`));
1634
+ if (keyIdx === -1)
1635
+ return [];
1636
+ const braceStart = source.indexOf('{', keyIdx);
1637
+ if (braceStart === -1)
1638
+ return [];
1639
+ const body = extractBalancedBody(source, braceStart, '{', '}');
1640
+ if (body == null)
1641
+ return [];
1642
+ const entries = [];
1643
+ const expectedSuffix = key === 'containers' ? '.container' : '.workflow';
1644
+ const allowedExt = /\.(ts|js|mjs|cjs)$/i;
1645
+ const requiredFilePattern = new RegExp(`\\${expectedSuffix}\\.(ts|js|mjs|cjs)$`, 'i');
1646
+ const resolveClassFromFile = (binding, filePath) => {
1647
+ if (!requiredFilePattern.test(filePath)) {
1648
+ throw new Error(`[kuratchi] ${key}.${binding} must reference a file ending in "${expectedSuffix}.ts|js|mjs|cjs". Received: ${filePath}`);
1649
+ }
1650
+ if (!allowedExt.test(filePath)) {
1651
+ throw new Error(`[kuratchi] ${key}.${binding} file must be a TypeScript or JavaScript module. Received: ${filePath}`);
1652
+ }
1653
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
1654
+ if (!fs.existsSync(absPath)) {
1655
+ throw new Error(`[kuratchi] ${key}.${binding} file not found: ${filePath}`);
1656
+ }
1657
+ const fileSource = fs.readFileSync(absPath, 'utf-8');
1658
+ const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
1659
+ if (defaultClass) {
1660
+ return { className: defaultClass[1], exportKind: 'default' };
1661
+ }
1662
+ const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
1663
+ if (namedClass) {
1664
+ return { className: namedClass[1], exportKind: 'named' };
1665
+ }
1666
+ throw new Error(`[kuratchi] ${key}.${binding} must export a class via "export class X" or "export default class X". File: ${filePath}`);
1667
+ };
1668
+ // Object form:
1669
+ // containers: { WORDPRESS_CONTAINER: { file: 'src/server/containers/wordpress.container.ts', className?: 'WordPressContainer' } }
1670
+ // workflows: { NEW_SITE_WORKFLOW: { file: 'src/server/workflows/new-site.workflow.ts', className?: 'NewSiteWorkflow' } }
1671
+ const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
1672
+ let m;
1673
+ while ((m = objRegex.exec(body)) !== null) {
1674
+ const binding = m[1];
1675
+ const entryBody = m[2];
1676
+ const fileMatch = entryBody.match(/file\s*:\s*['"]([^'"]+)['"]/);
1677
+ if (!fileMatch)
1678
+ continue;
1679
+ const inferred = resolveClassFromFile(binding, fileMatch[1]);
1680
+ const classMatch = entryBody.match(/className\s*:\s*['"](\w+)['"]/);
1681
+ const className = classMatch?.[1] ?? inferred.className;
1682
+ entries.push({
1683
+ binding,
1684
+ className,
1685
+ file: fileMatch[1],
1686
+ exportKind: inferred.exportKind,
1687
+ });
1688
+ }
1689
+ // String shorthand:
1690
+ // containers: { WORDPRESS_CONTAINER: 'src/server/containers/wordpress.container.ts' }
1691
+ // workflows: { NEW_SITE_WORKFLOW: 'src/server/workflows/new-site.workflow.ts' }
1692
+ const foundBindings = new Set(entries.map((e) => e.binding));
1693
+ const pairRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]\s*[,}\n]/g;
1694
+ while ((m = pairRegex.exec(body)) !== null) {
1695
+ const binding = m[1];
1696
+ const file = m[2];
1697
+ if (foundBindings.has(binding))
1698
+ continue;
1699
+ if (binding === 'file' || binding === 'className')
1700
+ continue;
1701
+ const inferred = resolveClassFromFile(binding, file);
1702
+ entries.push({
1703
+ binding,
1704
+ className: inferred.className,
1705
+ file,
1706
+ exportKind: inferred.exportKind,
1707
+ });
1708
+ }
1709
+ return entries;
1710
+ }
1368
1711
  function discoverFilesWithSuffix(dir, suffix) {
1369
1712
  if (!fs.existsSync(dir))
1370
1713
  return [];
@@ -1384,9 +1727,9 @@ function discoverFilesWithSuffix(dir, suffix) {
1384
1727
  return out;
1385
1728
  }
1386
1729
  /**
1387
- * Scan for files that extend kuratchiDO.
1388
- * Primary discovery is recursive under `src/server` for files ending in `.do.ts`.
1389
- * Legacy fallback keeps `src/durable-objects/*.ts` compatible.
1730
+ * Scan DO handler files.
1731
+ * - Class mode: default class extends kuratchiDO
1732
+ * - Function mode: exported functions in *.do.ts files (compiler wraps into DO class)
1390
1733
  */
1391
1734
  function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
1392
1735
  const serverDir = path.join(srcDir, 'server');
@@ -1414,14 +1757,15 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
1414
1757
  for (const absPath of discoveredFiles) {
1415
1758
  const file = path.basename(absPath);
1416
1759
  const source = fs.readFileSync(absPath, 'utf-8');
1417
- // Check if this file extends kuratchiDO
1418
- if (!/extends\s+kuratchiDO\b/.test(source))
1760
+ const exportedFunctions = extractExportedFunctions(source);
1761
+ const hasClass = /extends\s+kuratchiDO\b/.test(source);
1762
+ if (!hasClass && exportedFunctions.length === 0)
1419
1763
  continue;
1420
- // Extract class name: export default class Sites extends kuratchiDO
1764
+ // Extract class name when class mode is used.
1421
1765
  const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+kuratchiDO/);
1422
- if (!classMatch)
1766
+ const className = classMatch?.[1] ?? null;
1767
+ if (hasClass && !className)
1423
1768
  continue;
1424
- const className = classMatch[1];
1425
1769
  // Binding resolution:
1426
1770
  // 1) explicit static binding in class
1427
1771
  // 2) config-mapped file name (supports .do.ts convention)
@@ -1446,10 +1790,8 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
1446
1790
  continue;
1447
1791
  if (!bindings.has(binding))
1448
1792
  continue;
1449
- // Extract class methods — find the class body and parse method declarations
1450
- const classMethods = extractClassMethods(source, className);
1451
- // Extract named exports (custom worker-side helpers)
1452
- const namedExports = extractNamedExports(source);
1793
+ // Extract class methods in class mode
1794
+ const classMethods = className ? extractClassMethods(source, className) : [];
1453
1795
  const fileName = file.replace(/\.ts$/, '');
1454
1796
  const existing = fileNameToAbsPath.get(fileName);
1455
1797
  if (existing && existing !== absPath) {
@@ -1460,9 +1802,10 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
1460
1802
  fileName,
1461
1803
  absPath,
1462
1804
  binding,
1463
- className,
1805
+ mode: hasClass ? 'class' : 'function',
1806
+ className: className ?? undefined,
1464
1807
  classMethods,
1465
- namedExports,
1808
+ exportedFunctions,
1466
1809
  });
1467
1810
  }
1468
1811
  return handlers;
@@ -1554,40 +1897,33 @@ function extractClassMethods(source, className) {
1554
1897
  }
1555
1898
  return methods;
1556
1899
  }
1557
- /**
1558
- * Extract explicitly exported function/const names from a file.
1559
- */
1560
- function extractNamedExports(source) {
1561
- const exports = [];
1562
- // export async function name / export function name
1900
+ function extractExportedFunctions(source) {
1901
+ const out = [];
1563
1902
  const fnRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
1564
1903
  let m;
1565
1904
  while ((m = fnRegex.exec(source)) !== null)
1566
- exports.push(m[1]);
1567
- // export const name
1568
- const constRegex = /export\s+const\s+(\w+)/g;
1569
- while ((m = constRegex.exec(source)) !== null)
1570
- exports.push(m[1]);
1571
- return exports;
1905
+ out.push(m[1]);
1906
+ return out;
1572
1907
  }
1573
1908
  /**
1574
1909
  * Generate a proxy module for a DO handler file.
1575
1910
  *
1576
- * The proxy provides:
1577
- * - Auto-RPC function exports for each public class method
1578
- * - Re-exports of the user's custom named exports
1579
- *
1580
- * Methods that collide with custom exports are skipped (user's export wins).
1911
+ * The proxy provides auto-RPC function exports.
1912
+ * - Class mode: public class methods become RPC exports.
1913
+ * - Function mode: exported functions become RPC exports, excluding lifecycle hooks.
1581
1914
  */
1582
1915
  function generateHandlerProxy(handler, projectDir) {
1583
1916
  const doDir = path.join(projectDir, '.kuratchi', 'do');
1584
1917
  const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
1585
1918
  const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
1586
- const customSet = new Set(handler.namedExports);
1919
+ const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
1920
+ const rpcFunctions = handler.mode === 'function'
1921
+ ? handler.exportedFunctions.filter((n) => !lifecycle.has(n))
1922
+ : handler.classMethods.filter((m) => m.visibility === 'public').map((m) => m.name);
1587
1923
  const methods = handler.classMethods.map((m) => ({ ...m }));
1588
1924
  const methodMap = new Map(methods.map((m) => [m.name, m]));
1589
1925
  let changed = true;
1590
- while (changed) {
1926
+ while (changed && handler.mode === 'class') {
1591
1927
  changed = false;
1592
1928
  for (const m of methods) {
1593
1929
  if (m.hasWorkerContextCalls)
@@ -1602,15 +1938,56 @@ function generateHandlerProxy(handler, projectDir) {
1602
1938
  }
1603
1939
  }
1604
1940
  }
1605
- const publicMethods = methods.filter((m) => m.visibility === 'public').map((m) => m.name);
1606
- const workerContextMethods = methods
1607
- .filter((m) => m.visibility === 'public' && m.hasWorkerContextCalls)
1608
- .map((m) => m.name);
1609
- const asyncMethods = methods.filter((m) => m.isAsync).map((m) => m.name);
1941
+ const workerContextMethods = handler.mode === 'class'
1942
+ ? methods.filter((m) => m.visibility === 'public' && m.hasWorkerContextCalls).map((m) => m.name)
1943
+ : [];
1944
+ const asyncMethods = handler.mode === 'class'
1945
+ ? methods.filter((m) => m.isAsync).map((m) => m.name)
1946
+ : [];
1610
1947
  const lines = [
1611
- `// Auto-generated by KuratchiJS compiler — do not edit.`,
1948
+ `// Auto-generated by KuratchiJS compiler â€" do not edit.`,
1612
1949
  `import { __getDoStub } from '${RUNTIME_DO_IMPORT}';`,
1613
- `import ${handlerLocal} from '${origRelPath}';`,
1950
+ ...(handler.mode === 'class' ? [`import ${handlerLocal} from '${origRelPath}';`] : []),
1951
+ ``,
1952
+ `const __FD_TAG = '__kuratchi_form_data__';`,
1953
+ `function __isPlainObject(__v) {`,
1954
+ ` if (!__v || typeof __v !== 'object') return false;`,
1955
+ ` const __proto = Object.getPrototypeOf(__v);`,
1956
+ ` return __proto === Object.prototype || __proto === null;`,
1957
+ `}`,
1958
+ `function __encodeArg(__v, __seen = new WeakSet()) {`,
1959
+ ` if (typeof FormData !== 'undefined' && __v instanceof FormData) {`,
1960
+ ` return { [__FD_TAG]: Array.from(__v.entries()) };`,
1961
+ ` }`,
1962
+ ` if (Array.isArray(__v)) return __v.map((__x) => __encodeArg(__x, __seen));`,
1963
+ ` if (__isPlainObject(__v)) {`,
1964
+ ` if (__seen.has(__v)) throw new Error('[KuratchiJS] Circular object passed to DO RPC');`,
1965
+ ` __seen.add(__v);`,
1966
+ ` const __out = {};`,
1967
+ ` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __encodeArg(__val, __seen);`,
1968
+ ` __seen.delete(__v);`,
1969
+ ` return __out;`,
1970
+ ` }`,
1971
+ ` return __v;`,
1972
+ `}`,
1973
+ `function __decodeArg(__v) {`,
1974
+ ` if (Array.isArray(__v)) return __v.map(__decodeArg);`,
1975
+ ` if (__isPlainObject(__v)) {`,
1976
+ ` const __obj = __v;`,
1977
+ ` if (__FD_TAG in __obj) {`,
1978
+ ` const __fd = new FormData();`,
1979
+ ` const __entries = Array.isArray(__obj[__FD_TAG]) ? __obj[__FD_TAG] : [];`,
1980
+ ` for (const __pair of __entries) {`,
1981
+ ` if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]);`,
1982
+ ` }`,
1983
+ ` return __fd;`,
1984
+ ` }`,
1985
+ ` const __out = {};`,
1986
+ ` for (const [__k, __val] of Object.entries(__obj)) __out[__k] = __decodeArg(__val);`,
1987
+ ` return __out;`,
1988
+ ` }`,
1989
+ ` return __v;`,
1990
+ `}`,
1614
1991
  ``,
1615
1992
  ];
1616
1993
  if (workerContextMethods.length > 0) {
@@ -1630,29 +2007,22 @@ function generateHandlerProxy(handler, projectDir) {
1630
2007
  lines.push(` if (typeof __local === 'function' && !__asyncMethods.has(__k)) {`);
1631
2008
  lines.push(` return (...__a) => __local.apply(__self, __a);`);
1632
2009
  lines.push(` }`);
1633
- lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a); };`);
2010
+ lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a.map((__x) => __encodeArg(__x))); };`);
1634
2011
  lines.push(` },`);
1635
2012
  lines.push(` });`);
1636
- lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args);`);
2013
+ lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args.map(__decodeArg));`);
1637
2014
  lines.push(`}`);
1638
2015
  lines.push(``);
1639
2016
  }
1640
- // Export class methods (skip if a custom export has the same name)
1641
- for (const method of publicMethods) {
1642
- if (customSet.has(method))
1643
- continue; // user's export wins
2017
+ // Export RPC methods
2018
+ for (const method of rpcFunctions) {
1644
2019
  if (workerContextMethods.includes(method)) {
1645
2020
  lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', a); }`);
1646
2021
  }
1647
2022
  else {
1648
- lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a); }`);
2023
+ lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a.map((__x) => __encodeArg(__x))); }`);
1649
2024
  }
1650
2025
  }
1651
- // Re-export custom named exports from the original file
1652
- if (handler.namedExports.length > 0) {
1653
- lines.push(``);
1654
- lines.push(`export { ${handler.namedExports.join(', ')} } from '${origRelPath}';`);
1655
- }
1656
2026
  return lines.join('\n') + '\n';
1657
2027
  }
1658
2028
  function generateRoutesModule(opts) {
@@ -1665,13 +2035,16 @@ function generateRoutesModule(opts) {
1665
2035
  .map(([status, fn]) => fn)
1666
2036
  .join('\n\n');
1667
2037
  // Resolve path to the framework's context module from the output directory
1668
- const contextImport = `import { __setRequestContext, __esc, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
1669
- // Auth session init — thin cookie parsing injected into Worker entry
2038
+ const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
2039
+ const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
2040
+ ? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
2041
+ : '';
2042
+ // Auth session init â€" thin cookie parsing injected into Worker entry
1670
2043
  let authInit = '';
1671
2044
  if (opts.authConfig && opts.authConfig.sessionEnabled) {
1672
2045
  const cookieName = opts.authConfig.cookieName;
1673
2046
  authInit = `
1674
- // ── Auth Session Init ───────────────────────────────────────
2047
+ // â"€â"€ Auth Session Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1675
2048
 
1676
2049
  function __parseCookies(header) {
1677
2050
  const map = {};
@@ -1722,7 +2095,7 @@ function __initAuth(request) {
1722
2095
  ...schemaImports,
1723
2096
  ].join('\n');
1724
2097
  migrationInit = `
1725
- // ── ORM Auto-Migration ──────────────────────────────────────
2098
+ // â"€â"€ ORM Auto-Migration â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1726
2099
 
1727
2100
  let __migrated = false;
1728
2101
  const __ormDatabases = [
@@ -1756,7 +2129,7 @@ async function __runMigrations() {
1756
2129
  `;
1757
2130
  }
1758
2131
  }
1759
- // Auth plugin init — import config + call @kuratchi/auth setup functions
2132
+ // Auth plugin init â€" import config + call @kuratchi/auth setup functions
1760
2133
  let authPluginImports = '';
1761
2134
  let authPluginInit = '';
1762
2135
  const ac = opts.authConfig;
@@ -1808,14 +2181,14 @@ async function __runMigrations() {
1808
2181
  }
1809
2182
  authPluginImports = imports.join('\n');
1810
2183
  authPluginInit = `
1811
- // ── Auth Plugin Init ───────────────────────────────────────
2184
+ // â"€â"€ Auth Plugin Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1812
2185
 
1813
2186
  function __initAuthPlugins() {
1814
2187
  ${initLines.join('\n')}
1815
2188
  }
1816
2189
  `;
1817
2190
  }
1818
- // ── Durable Object class generation ───────────────────────────
2191
+ // â"€â"€ Durable Object class generation â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1819
2192
  let doImports = '';
1820
2193
  let doClassCode = '';
1821
2194
  let doResolverInit = '';
@@ -1825,7 +2198,28 @@ ${initLines.join('\n')}
1825
2198
  const doResolverLines = [];
1826
2199
  doImportLines.push(`import { DurableObject as __DO } from 'cloudflare:workers';`);
1827
2200
  doImportLines.push(`import { initDO as __initDO } from '@kuratchi/orm';`);
1828
- doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding } from '${RUNTIME_DO_IMPORT}';`);
2201
+ doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding, __setDoContext } from '${RUNTIME_DO_IMPORT}';`);
2202
+ doImportLines.push(`const __DO_FD_TAG = '__kuratchi_form_data__';`);
2203
+ doImportLines.push(`function __isDoPlainObject(__v) {`);
2204
+ doImportLines.push(` if (!__v || typeof __v !== 'object') return false;`);
2205
+ doImportLines.push(` const __proto = Object.getPrototypeOf(__v);`);
2206
+ doImportLines.push(` return __proto === Object.prototype || __proto === null;`);
2207
+ doImportLines.push(`}`);
2208
+ doImportLines.push(`function __decodeDoArg(__v) {`);
2209
+ doImportLines.push(` if (Array.isArray(__v)) return __v.map(__decodeDoArg);`);
2210
+ doImportLines.push(` if (__isDoPlainObject(__v)) {`);
2211
+ doImportLines.push(` if (__DO_FD_TAG in __v) {`);
2212
+ doImportLines.push(` const __fd = new FormData();`);
2213
+ doImportLines.push(` const __entries = Array.isArray(__v[__DO_FD_TAG]) ? __v[__DO_FD_TAG] : [];`);
2214
+ doImportLines.push(` for (const __pair of __entries) { if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]); }`);
2215
+ doImportLines.push(` return __fd;`);
2216
+ doImportLines.push(` }`);
2217
+ doImportLines.push(` const __out = {};`);
2218
+ doImportLines.push(` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __decodeDoArg(__val);`);
2219
+ doImportLines.push(` return __out;`);
2220
+ doImportLines.push(` }`);
2221
+ doImportLines.push(` return __v;`);
2222
+ doImportLines.push(`}`);
1829
2223
  // We need getCurrentUser and getOrgStubByName for stub resolvers
1830
2224
  doImportLines.push(`import { getCurrentUser as __getCU, getOrgStubByName as __getOSBN } from '@kuratchi/auth';`);
1831
2225
  // Group handlers by binding
@@ -1839,6 +2233,10 @@ ${initLines.join('\n')}
1839
2233
  for (const doEntry of opts.doConfig) {
1840
2234
  const handlers = handlersByBinding.get(doEntry.binding) ?? [];
1841
2235
  const ormDb = opts.ormDatabases.find(d => d.binding === doEntry.binding);
2236
+ const fnHandlers = handlers.filter((h) => h.mode === 'function');
2237
+ const initHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onInit'));
2238
+ const alarmHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onAlarm'));
2239
+ const messageHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onMessage'));
1842
2240
  // Import schema (paths are relative to project root; prefix ../ since we're in .kuratchi/)
1843
2241
  if (ormDb) {
1844
2242
  const schemaPath = ormDb.schemaImportPath.replace(/^\.\//, '../');
@@ -1853,7 +2251,12 @@ ${initLines.join('\n')}
1853
2251
  if (!handlerImportPath.startsWith('.'))
1854
2252
  handlerImportPath = './' + handlerImportPath;
1855
2253
  const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
1856
- doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
2254
+ if (h.mode === 'class') {
2255
+ doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
2256
+ }
2257
+ else {
2258
+ doImportLines.push(`import * as ${handlerVar} from '${handlerImportPath}';`);
2259
+ }
1857
2260
  }
1858
2261
  // Generate DO class
1859
2262
  doClassLines.push(`export class ${doEntry.className} extends __DO {`);
@@ -1862,6 +2265,11 @@ ${initLines.join('\n')}
1862
2265
  if (ormDb) {
1863
2266
  doClassLines.push(` this.db = __initDO(ctx.storage.sql, __doSchema_${doEntry.binding});`);
1864
2267
  }
2268
+ for (const h of initHandlers) {
2269
+ const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2270
+ doClassLines.push(` __setDoContext(this);`);
2271
+ doClassLines.push(` Promise.resolve(${handlerVar}.onInit.call(this)).catch((err) => console.error('[kuratchi] DO onInit failed:', err?.message || err));`);
2272
+ }
1865
2273
  doClassLines.push(` }`);
1866
2274
  if (ormDb) {
1867
2275
  doClassLines.push(` async __kuratchiLogActivity(payload) {`);
@@ -1900,16 +2308,45 @@ ${initLines.join('\n')}
1900
2308
  doClassLines.push(` return rows;`);
1901
2309
  doClassLines.push(` }`);
1902
2310
  }
2311
+ // Function-mode lifecycle dispatchers
2312
+ if (alarmHandlers.length > 0) {
2313
+ doClassLines.push(` async alarm(...args) {`);
2314
+ doClassLines.push(` __setDoContext(this);`);
2315
+ for (const h of alarmHandlers) {
2316
+ const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2317
+ doClassLines.push(` await ${handlerVar}.onAlarm.call(this, ...args);`);
2318
+ }
2319
+ doClassLines.push(` }`);
2320
+ }
2321
+ if (messageHandlers.length > 0) {
2322
+ doClassLines.push(` webSocketMessage(...args) {`);
2323
+ doClassLines.push(` __setDoContext(this);`);
2324
+ for (const h of messageHandlers) {
2325
+ const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2326
+ doClassLines.push(` ${handlerVar}.onMessage.call(this, ...args);`);
2327
+ }
2328
+ doClassLines.push(` }`);
2329
+ }
1903
2330
  doClassLines.push(`}`);
1904
- // Apply handler methods to prototype
2331
+ // Apply handler methods to prototype (outside class body)
1905
2332
  for (const h of handlers) {
1906
2333
  const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
1907
- doClassLines.push(`for (const __k of Object.getOwnPropertyNames(${handlerVar}.prototype)) { if (__k !== 'constructor') ${doEntry.className}.prototype[__k] = ${handlerVar}.prototype[__k]; }`);
1908
- doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
2334
+ if (h.mode === 'class') {
2335
+ doClassLines.push(`for (const __k of Object.getOwnPropertyNames(${handlerVar}.prototype)) { if (__k !== 'constructor') ${doEntry.className}.prototype[__k] = function(...__a){ __setDoContext(this); return ${handlerVar}.prototype[__k].apply(this, __a.map(__decodeDoArg)); }; }`);
2336
+ doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
2337
+ }
2338
+ else {
2339
+ const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
2340
+ for (const fn of h.exportedFunctions) {
2341
+ if (lifecycle.has(fn))
2342
+ continue;
2343
+ doClassLines.push(`${doEntry.className}.prototype[${JSON.stringify(fn)}] = function(...__a){ __setDoContext(this); return ${handlerVar}.${fn}.apply(this, __a.map(__decodeDoArg)); };`);
2344
+ }
2345
+ }
1909
2346
  }
1910
2347
  // Register stub resolver
1911
2348
  if (doEntry.stubId) {
1912
- // Config-driven: e.g. stubId: 'user.orgId' → __u.orgId
2349
+ // Config-driven: e.g. stubId: 'user.orgId' â†' __u.orgId
1913
2350
  const fieldPath = doEntry.stubId.startsWith('user.') ? `__u.${doEntry.stubId.slice(5)}` : doEntry.stubId;
1914
2351
  const checkField = doEntry.stubId.startsWith('user.') ? doEntry.stubId.slice(5) : doEntry.stubId;
1915
2352
  doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
@@ -1919,37 +2356,37 @@ ${initLines.join('\n')}
1919
2356
  doResolverLines.push(` });`);
1920
2357
  }
1921
2358
  else {
1922
- // No stubId config — stub must be obtained manually
1923
- doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} — stub must be obtained manually`);
2359
+ // No stubId config â€" stub must be obtained manually
2360
+ doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} â€" stub must be obtained manually`);
1924
2361
  }
1925
2362
  }
1926
2363
  doImports = doImportLines.join('\n');
1927
- doClassCode = `\n// ── Durable Object Classes (generated) ──────────────────────────\n\n` + doClassLines.join('\n') + '\n';
2364
+ doClassCode = `\n// â"€â"€ Durable Object Classes (generated) â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n` + doClassLines.join('\n') + '\n';
1928
2365
  doResolverInit = `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`;
1929
2366
  }
1930
- return `// Generated by KuratchiJS compiler — do not edit.
2367
+ return `// Generated by KuratchiJS compiler â€" do not edit.
1931
2368
  ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
1932
2369
  ${workerImport}
1933
2370
  ${contextImport}
1934
- ${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
2371
+ ${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
1935
2372
 
1936
- // ── Assets ──────────────────────────────────────────────────────
2373
+ // â"€â"€ Assets â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1937
2374
 
1938
2375
  const __assets = {
1939
2376
  ${opts.compiledAssets.map(a => ` ${JSON.stringify(a.name)}: { content: ${JSON.stringify(a.content)}, mime: ${JSON.stringify(a.mime)}, etag: ${JSON.stringify(a.etag)} }`).join(',\n')}
1940
2377
  };
1941
2378
 
1942
- // ── Router ──────────────────────────────────────────────────────
2379
+ // â"€â"€ Router â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1943
2380
 
1944
- const __staticRoutes = new Map(); // exact path → index (O(1) lookup)
2381
+ const __staticRoutes = new Map(); // exact path â†' index (O(1) lookup)
1945
2382
  const __dynamicRoutes = []; // regex-based routes (params/wildcards)
1946
2383
 
1947
2384
  function __addRoute(pattern, index) {
1948
2385
  if (!pattern.includes(':') && !pattern.includes('*')) {
1949
- // Static route — direct Map lookup, no regex needed
2386
+ // Static route â€" direct Map lookup, no regex needed
1950
2387
  __staticRoutes.set(pattern, index);
1951
2388
  } else {
1952
- // Dynamic route — build regex for param extraction
2389
+ // Dynamic route â€" build regex for param extraction
1953
2390
  const paramNames = [];
1954
2391
  let regexStr = pattern
1955
2392
  .replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
@@ -1975,13 +2412,13 @@ function __match(pathname) {
1975
2412
  return null;
1976
2413
  }
1977
2414
 
1978
- // ── Layout ──────────────────────────────────────────────────────
2415
+ // â"€â"€ Layout â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1979
2416
 
1980
2417
  ${layoutBlock}
1981
2418
 
1982
2419
  ${layoutActionsBlock}
1983
2420
 
1984
- // ── Error pages ─────────────────────────────────────────────────
2421
+ // â"€â"€ Error pages â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
1985
2422
 
1986
2423
  const __errorMessages = {
1987
2424
  400: 'Bad Request',
@@ -1996,7 +2433,7 @@ const __errorMessages = {
1996
2433
  503: 'Service Unavailable',
1997
2434
  };
1998
2435
 
1999
- // Built-in default error page — clean, dark, minimal, centered
2436
+ // Built-in default error page â€" clean, dark, minimal, centered
2000
2437
  function __errorPage(status, detail) {
2001
2438
  const title = __errorMessages[status] || 'Error';
2002
2439
  const detailHtml = detail ? '<p style="font-family:ui-monospace,monospace;font-size:0.8rem;color:#555;background:#111;padding:0.5rem 1rem;border-radius:6px;max-width:480px;margin:1rem auto 0;word-break:break-word">' + __esc(detail) + '</p>' : '';
@@ -2018,8 +2455,8 @@ function __error(status, detail) {
2018
2455
  return __errorPage(status, detail);
2019
2456
  }
2020
2457
 
2021
- ${opts.compiledComponents.length > 0 ? '// ── Components ──────────────────────────────────────────────────\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
2022
- // ── Route definitions ───────────────────────────────────────────
2458
+ ${opts.compiledComponents.length > 0 ? '// â"€â"€ Components â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
2459
+ // â"€â"€ Route definitions â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
2023
2460
 
2024
2461
  const routes = [
2025
2462
  ${opts.compiledRoutes.join(',\n')}
@@ -2027,7 +2464,7 @@ ${opts.compiledRoutes.join(',\n')}
2027
2464
 
2028
2465
  for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
2029
2466
 
2030
- // ── Response helpers ────────────────────────────────────────────
2467
+ // â"€â"€ Response helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
2031
2468
 
2032
2469
  const __defaultSecHeaders = {
2033
2470
  'X-Content-Type-Options': 'nosniff',
@@ -2075,141 +2512,269 @@ ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
2075
2512
  return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
2076
2513
  }
2077
2514
 
2078
- // ── Exported Worker entrypoint ──────────────────────────────────
2515
+ const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
2516
+ const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
2517
+
2518
+ async function __runRuntimeRequest(ctx, next) {
2519
+ let idx = -1;
2520
+ async function __dispatch(i) {
2521
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
2522
+ idx = i;
2523
+ const entry = __runtimeEntries[i];
2524
+ if (!entry) return next();
2525
+ const [, step] = entry;
2526
+ if (typeof step.request !== 'function') return __dispatch(i + 1);
2527
+ return await step.request(ctx, () => __dispatch(i + 1));
2528
+ }
2529
+ return __dispatch(0);
2530
+ }
2531
+
2532
+ async function __runRuntimeRoute(ctx, next) {
2533
+ let idx = -1;
2534
+ async function __dispatch(i) {
2535
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
2536
+ idx = i;
2537
+ const entry = __runtimeEntries[i];
2538
+ if (!entry) return next();
2539
+ const [, step] = entry;
2540
+ if (typeof step.route !== 'function') return __dispatch(i + 1);
2541
+ return await step.route(ctx, () => __dispatch(i + 1));
2542
+ }
2543
+ return __dispatch(0);
2544
+ }
2545
+
2546
+ async function __runRuntimeResponse(ctx, response) {
2547
+ let out = response;
2548
+ for (const [, step] of __runtimeEntries) {
2549
+ if (typeof step.response !== 'function') continue;
2550
+ out = await step.response(ctx, out);
2551
+ if (!(out instanceof Response)) {
2552
+ throw new Error('[kuratchi runtime] response handlers must return a Response');
2553
+ }
2554
+ }
2555
+ return out;
2556
+ }
2557
+
2558
+ async function __runRuntimeError(ctx, error) {
2559
+ for (const [name, step] of __runtimeEntries) {
2560
+ if (typeof step.error !== 'function') continue;
2561
+ try {
2562
+ const handled = await step.error(ctx, error);
2563
+ if (handled instanceof Response) return handled;
2564
+ } catch (hookErr) {
2565
+ console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
2566
+ }
2567
+ }
2568
+ return null;
2569
+ }
2570
+
2571
+ // â"€â"€ Exported Worker entrypoint â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
2079
2572
 
2080
2573
  export default class extends WorkerEntrypoint {
2081
2574
  async fetch(request) {
2082
2575
  __setRequestContext(this.ctx, request);
2576
+ globalThis.__cloudflare_env__ = __env;
2083
2577
  ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
2084
- const url = new URL(request.url);
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' : ''}
2578
+ const __runtimeCtx = {
2579
+ request,
2580
+ env: __env,
2581
+ ctx: this.ctx,
2582
+ url: new URL(request.url),
2583
+ params: {},
2584
+ locals: __getLocals(),
2585
+ };
2086
2586
 
2087
- // Serve static assets from src/assets/ at /_assets/*
2088
- if (url.pathname.startsWith('/_assets/')) {
2089
- const name = url.pathname.slice('/_assets/'.length);
2090
- const asset = __assets[name];
2091
- if (asset) {
2092
- if (request.headers.get('if-none-match') === asset.etag) {
2093
- return new Response(null, { status: 304 });
2587
+ const __coreFetch = async () => {
2588
+ const request = __runtimeCtx.request;
2589
+ const url = __runtimeCtx.url;
2590
+ ${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' : ''}
2591
+
2592
+ // Serve static assets from src/assets/ at /_assets/*
2593
+ if (url.pathname.startsWith('/_assets/')) {
2594
+ const name = url.pathname.slice('/_assets/'.length);
2595
+ const asset = __assets[name];
2596
+ if (asset) {
2597
+ if (request.headers.get('if-none-match') === asset.etag) {
2598
+ return new Response(null, { status: 304 });
2599
+ }
2600
+ return new Response(asset.content, {
2601
+ headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2602
+ });
2094
2603
  }
2095
- return new Response(asset.content, {
2096
- headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2097
- });
2604
+ return __secHeaders(new Response('Not Found', { status: 404 }));
2098
2605
  }
2099
- return __secHeaders(new Response('Not Found', { status: 404 }));
2100
- }
2101
2606
 
2102
- const match = __match(url.pathname);
2607
+ const match = __match(url.pathname);
2103
2608
 
2104
- if (!match) {
2105
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2106
- }
2609
+ if (!match) {
2610
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2611
+ }
2107
2612
 
2108
- const route = routes[match.index];
2109
- __setLocal('params', match.params);
2110
- const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
2111
- const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
2112
- let __qArgs = [];
2113
- try {
2114
- const __parsed = JSON.parse(__qArgsRaw);
2115
- __qArgs = Array.isArray(__parsed) ? __parsed : [];
2116
- } catch {}
2117
- __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
2118
- if (!__getLocals().__breadcrumbs) {
2119
- __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
2120
- }
2613
+ __runtimeCtx.params = match.params;
2614
+ const route = routes[match.index];
2615
+ __setLocal('params', match.params);
2121
2616
 
2122
- // RPC call: GET ?_rpc=fnName&_args=[...] → JSON response
2123
- const __rpcName = url.searchParams.get(‘_rpc’);
2124
- if (request.method === ‘GET’ && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
2125
- if (request.headers.get('x-kuratchi-rpc') !== '1') {
2126
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
2127
- status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2128
- }));
2129
- }
2130
- try {
2131
- const __rpcArgsStr = url.searchParams.get('_args');
2132
- let __rpcArgs = [];
2133
- if (__rpcArgsStr) {
2134
- const __parsed = JSON.parse(__rpcArgsStr);
2135
- __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
2617
+ // API route: dispatch to method handler
2618
+ if (route.__api) {
2619
+ const method = request.method;
2620
+ if (method === 'OPTIONS') {
2621
+ const handler = route['OPTIONS'];
2622
+ if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
2623
+ const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
2624
+ return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
2136
2625
  }
2137
- const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
2138
- return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
2139
- headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2140
- }));
2141
- } catch (err) {
2142
- console.error('[kuratchi] RPC error:', err);
2143
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2144
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2145
- status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2146
- }));
2626
+ const handler = route[method];
2627
+ if (typeof handler !== 'function') {
2628
+ const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
2629
+ return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
2630
+ }
2631
+ return __secHeaders(await handler(__runtimeCtx));
2147
2632
  }
2148
- }
2149
2633
 
2150
- // Form action: POST with hidden _action field in form body
2151
- if (request.method === 'POST') {
2152
- if (!__isSameOrigin(request, url)) {
2153
- return __secHeaders(new Response('Forbidden', { status: 403 }));
2634
+ const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
2635
+ const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
2636
+ let __qArgs = [];
2637
+ try {
2638
+ const __parsed = JSON.parse(__qArgsRaw);
2639
+ __qArgs = Array.isArray(__parsed) ? __parsed : [];
2640
+ } catch {}
2641
+ __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
2642
+ if (!__getLocals().__breadcrumbs) {
2643
+ __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
2154
2644
  }
2155
- const formData = await request.formData();
2156
- const actionName = formData.get('_action');
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);
2159
- if (actionName && __actionFn) {
2160
- // Check if this is a fetch-based action call (onclick) with JSON args
2161
- const argsStr = formData.get('_args');
2162
- const isFetchAction = argsStr !== null;
2645
+
2646
+ // RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
2647
+ const __rpcName = url.searchParams.get('_rpc');
2648
+ if (request.method === 'GET' && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
2649
+ if (request.headers.get('x-kuratchi-rpc') !== '1') {
2650
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
2651
+ status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2652
+ }));
2653
+ }
2163
2654
  try {
2164
- if (isFetchAction) {
2165
- const __parsed = JSON.parse(argsStr);
2166
- const args = Array.isArray(__parsed) ? __parsed : [];
2167
- await __actionFn(...args);
2168
- } else {
2169
- await __actionFn(formData);
2655
+ const __rpcArgsStr = url.searchParams.get('_args');
2656
+ let __rpcArgs = [];
2657
+ if (__rpcArgsStr) {
2658
+ const __parsed = JSON.parse(__rpcArgsStr);
2659
+ __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
2170
2660
  }
2661
+ const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
2662
+ return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
2663
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2664
+ }));
2171
2665
  } catch (err) {
2172
- console.error('[kuratchi] Action error:', err);
2666
+ console.error('[kuratchi] RPC error:', err);
2667
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2668
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2669
+ status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2670
+ }));
2671
+ }
2672
+ }
2673
+
2674
+ // Form action: POST with hidden _action field in form body
2675
+ if (request.method === 'POST') {
2676
+ if (!__isSameOrigin(request, url)) {
2677
+ return __secHeaders(new Response('Forbidden', { status: 403 }));
2678
+ }
2679
+ const formData = await request.formData();
2680
+ const actionName = formData.get('_action');
2681
+ const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
2682
+ || (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
2683
+ if (actionName && __actionFn) {
2684
+ // Check if this is a fetch-based action call (onclick) with JSON args
2685
+ const argsStr = formData.get('_args');
2686
+ const isFetchAction = argsStr !== null;
2687
+ try {
2688
+ if (isFetchAction) {
2689
+ const __parsed = JSON.parse(argsStr);
2690
+ const args = Array.isArray(__parsed) ? __parsed : [];
2691
+ await __actionFn(...args);
2692
+ } else {
2693
+ await __actionFn(formData);
2694
+ }
2695
+ } catch (err) {
2696
+ console.error('[kuratchi] Action error:', err);
2697
+ if (isFetchAction) {
2698
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2699
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2700
+ status: 500, headers: { 'content-type': 'application/json' }
2701
+ }));
2702
+ }
2703
+ const __loaded = route.load ? await route.load(match.params) : {};
2704
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2705
+ data.params = match.params;
2706
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2707
+ const __allActions = Object.assign({}, route.actions, __layoutActions || {});
2708
+ Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
2709
+ const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
2710
+ data[actionName] = { error: __errMsg, loading: false, success: false };
2711
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2712
+ }
2713
+ // Fetch-based actions return lightweight JSON (no page re-render)
2173
2714
  if (isFetchAction) {
2174
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2175
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2176
- status: 500, headers: { 'content-type': 'application/json' }
2715
+ return __attachCookies(new Response(JSON.stringify({ ok: true }), {
2716
+ headers: { 'content-type': 'application/json' }
2177
2717
  }));
2178
2718
  }
2179
- const __loaded = route.load ? await route.load(match.params) : {};
2180
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2181
- data.params = match.params;
2182
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2183
- data.__error = (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
2184
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2185
- }
2186
- // Fetch-based actions return lightweight JSON (no page re-render)
2187
- if (isFetchAction) {
2188
- return __attachCookies(new Response(JSON.stringify({ ok: true }), {
2189
- headers: { 'content-type': 'application/json' }
2190
- }));
2719
+ // POST-Redirect-GET: redirect to custom target or back to same URL
2720
+ const __locals = __getLocals();
2721
+ const redirectTo = __locals.__redirectTo || url.pathname;
2722
+ const redirectStatus = Number(__locals.__redirectStatus) || 303;
2723
+ return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
2191
2724
  }
2192
- // POST-Redirect-GET: redirect to custom target or back to same URL
2193
- const __locals = __getLocals();
2194
- const redirectTo = __locals.__redirectTo || url.pathname;
2195
- const redirectStatus = Number(__locals.__redirectStatus) || 303;
2196
- return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
2197
2725
  }
2198
- }
2199
2726
 
2200
- // GET (or unmatched POST): load + render
2727
+ // GET (or unmatched POST): load + render
2728
+ try {
2729
+ const __loaded = route.load ? await route.load(match.params) : {};
2730
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2731
+ data.params = match.params;
2732
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2733
+ const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
2734
+ Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
2735
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2736
+ } catch (err) {
2737
+ console.error('[kuratchi] Route load/render error:', err);
2738
+ const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
2739
+ const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
2740
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2741
+ }
2742
+ };
2743
+
2201
2744
  try {
2202
- const __loaded = route.load ? await route.load(match.params) : {};
2203
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2204
- data.params = match.params;
2205
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2206
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2745
+ const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
2746
+ return __runRuntimeRoute(__runtimeCtx, __coreFetch);
2747
+ });
2748
+ return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
2207
2749
  } catch (err) {
2208
- console.error('[kuratchi] Route load/render error:', err);
2209
- const __errDetail = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : undefined;
2210
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(500, __errDetail)), { status: 500, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2750
+ const __handled = await __runRuntimeError(__runtimeCtx, err);
2751
+ if (__handled) return __secHeaders(__handled);
2752
+ throw err;
2211
2753
  }
2212
2754
  }
2213
2755
  }
2214
2756
  `;
2215
2757
  }
2758
+ function resolveRuntimeImportPath(projectDir) {
2759
+ const candidates = [
2760
+ { file: 'src/kuratchi.runtime.ts', importPath: '../src/kuratchi.runtime' },
2761
+ { file: 'src/kuratchi.runtime.js', importPath: '../src/kuratchi.runtime' },
2762
+ { file: 'src/kuratchi.runtime.mjs', importPath: '../src/kuratchi.runtime' },
2763
+ { file: 'kuratchi.runtime.ts', importPath: '../kuratchi.runtime' },
2764
+ { file: 'kuratchi.runtime.js', importPath: '../kuratchi.runtime' },
2765
+ { file: 'kuratchi.runtime.mjs', importPath: '../kuratchi.runtime' },
2766
+ ];
2767
+ for (const candidate of candidates) {
2768
+ if (fs.existsSync(path.join(projectDir, candidate.file))) {
2769
+ return candidate.importPath;
2770
+ }
2771
+ }
2772
+ return null;
2773
+ }
2774
+ function toWorkerImportPath(projectDir, outDir, filePath) {
2775
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
2776
+ let rel = path.relative(outDir, absPath).replace(/\\/g, '/');
2777
+ if (!rel.startsWith('.'))
2778
+ rel = `./${rel}`;
2779
+ return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
2780
+ }