@kuratchi/js 0.0.9 → 0.0.10

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,8 +1,8 @@
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
- import { parseFile } from './parser.js';
5
+ import { parseFile, stripTopLevelImports } from './parser.js';
6
6
  import { compileTemplate } from './template.js';
7
7
  import { transpileTypeScript } from './transpile.js';
8
8
  import { filePathToPattern } from '../runtime/router.js';
@@ -47,7 +47,8 @@ function rewriteWorkerEnvAliases(source, aliases) {
47
47
  for (const alias of aliases) {
48
48
  if (!/^[A-Za-z_$][\w$]*$/.test(alias))
49
49
  continue;
50
- const aliasRegex = new RegExp(`\\b${alias}\\b`, 'g');
50
+ // Negative lookbehind: don't rewrite property accesses like __m16.env
51
+ const aliasRegex = new RegExp(`(?<!\\.)\\b${alias}\\b`, 'g');
51
52
  out = out.replace(aliasRegex, '__env');
52
53
  }
53
54
  return out;
@@ -67,9 +68,12 @@ function parseNamedImportBindings(line) {
67
68
  .filter(Boolean)
68
69
  .map(n => {
69
70
  const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
70
- return parts[1] || parts[0] || '';
71
+ return {
72
+ imported: parts[0] || '',
73
+ local: parts[1] || parts[0] || '',
74
+ };
71
75
  })
72
- .filter(Boolean);
76
+ .filter((binding) => !!binding.imported && !!binding.local);
73
77
  }
74
78
  function filterClientImportsForServer(imports, neededFns) {
75
79
  const selected = [];
@@ -77,7 +81,7 @@ function filterClientImportsForServer(imports, neededFns) {
77
81
  const bindings = parseNamedImportBindings(line);
78
82
  if (bindings.length === 0)
79
83
  continue;
80
- if (bindings.some(name => neededFns.has(name))) {
84
+ if (bindings.some((binding) => neededFns.has(binding.local))) {
81
85
  selected.push(line);
82
86
  }
83
87
  }
@@ -86,9 +90,9 @@ function filterClientImportsForServer(imports, neededFns) {
86
90
  /**
87
91
  * Compile a project's src/routes/ into .kuratchi/routes.js
88
92
  *
89
- * The generated module exports { app } â€" an object with a fetch() method
93
+ * The generated module exports { app } " an object with a fetch() method
90
94
  * that handles routing, load functions, form actions, and rendering.
91
- * Returns the path to .kuratchi/worker.js the stable wrangler entry point that
95
+ * Returns the path to .kuratchi/worker.js the stable wrangler entry point that
92
96
  * re-exports everything from routes.js (default fetch handler + named DO class exports).
93
97
  * No src/index.ts is needed in user projects.
94
98
  */
@@ -101,21 +105,21 @@ export function compile(options) {
101
105
  }
102
106
  // Discover all .html route files
103
107
  const routeFiles = discoverRoutes(routesDir);
104
- // Component compilation cache â€" only compile components that are actually imported
108
+ // Component compilation cache " only compile components that are actually imported
105
109
  const libDir = path.join(srcDir, 'lib');
106
- const compiledComponentCache = new Map(); // fileName â†' compiled function code
107
- const componentStyleCache = new Map(); // fileName â†' escaped CSS string (or empty)
110
+ const compiledComponentCache = new Map(); // fileName ' compiled function code
111
+ const componentStyleCache = new Map(); // fileName ' escaped CSS string (or empty)
108
112
  // Tracks which prop names inside a component are used as action={propName}.
109
- // e.g. db-studio uses action={runQueryAction} â†' stores 'runQueryAction'.
113
+ // e.g. db-studio uses action={runQueryAction} ' stores 'runQueryAction'.
110
114
  // When the route passes runQueryAction={runAdminSqlQuery}, the compiler knows
111
115
  // to add 'runAdminSqlQuery' to the route's actionFunctions.
112
- const componentActionCache = new Map(); // fileName â†' Set of action prop names
116
+ const componentActionCache = new Map(); // fileName ' Set of action prop names
113
117
  function compileComponent(fileName) {
114
118
  if (compiledComponentCache.has(fileName))
115
119
  return compiledComponentCache.get(fileName);
116
120
  let filePath;
117
121
  let funcName;
118
- // Package component: "@kuratchi/ui:badge" â†' resolve from package
122
+ // Package component: "@kuratchi/ui:badge" ' resolve from package
119
123
  const pkgMatch = fileName.match(/^(@[^:]+):(.+)$/);
120
124
  if (pkgMatch) {
121
125
  const pkgName = pkgMatch[1]; // e.g. "@kuratchi/ui"
@@ -142,9 +146,7 @@ export function compile(options) {
142
146
  const compParsed = parseFile(rawSource, { kind: 'component', filePath });
143
147
  // propsCode = script body with all import lines stripped out
144
148
  const propsCode = compParsed.script
145
- ? compParsed.script
146
- .replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '')
147
- .trim()
149
+ ? stripTopLevelImports(compParsed.script)
148
150
  : '';
149
151
  const devDecls = buildDevAliasDeclarations(compParsed.devAliases, !!options.isDev);
150
152
  const effectivePropsCode = [devDecls, propsCode].filter(Boolean).join('\n');
@@ -155,7 +157,7 @@ export function compile(options) {
155
157
  : '';
156
158
  // template source (parseFile already removes the <script> block)
157
159
  let source = compParsed.template;
158
- // Extract optional <style> block â€" CSS is scoped and injected once per route at compile time
160
+ // Extract optional <style> block " CSS is scoped and injected once per route at compile time
159
161
  let styleBlock = '';
160
162
  const styleMatch = source.match(/<style[\s>][\s\S]*?<\/style>/i);
161
163
  if (styleMatch) {
@@ -201,7 +203,7 @@ export function compile(options) {
201
203
  }
202
204
  }
203
205
  // Scan the component template for action={propName} uses.
204
- // These prop names are "action props" â€" when the route passes actionProp={routeFn},
206
+ // These prop names are "action props" " when the route passes actionProp={routeFn},
205
207
  // the compiler knows to add routeFn to the route's actionFunctions so it ends up
206
208
  // in the route's actions map and can be dispatched at runtime.
207
209
  const actionPropNames = new Set();
@@ -221,7 +223,7 @@ export function compile(options) {
221
223
  compiledComponentCache.set(fileName, compiled);
222
224
  return compiled;
223
225
  }
224
- // App layout: src/routes/layout.html (convention â€" wraps all routes automatically)
226
+ // App layout: src/routes/layout.html (convention " wraps all routes automatically)
225
227
  const layoutFile = path.join(routesDir, 'layout.html');
226
228
  let compiledLayout = null;
227
229
  const layoutComponentNames = new Map();
@@ -229,6 +231,17 @@ export function compile(options) {
229
231
  let source = fs.readFileSync(layoutFile, 'utf-8');
230
232
  // Inject UI theme CSS if configured in kuratchi.config.ts
231
233
  const themeCSS = readUiTheme(projectDir);
234
+ const uiConfigValues = readUiConfigValues(projectDir);
235
+ // Patch <html> tag: set server-default theme class and data-radius from config
236
+ if (uiConfigValues) {
237
+ source = patchHtmlTag(source, uiConfigValues.theme, uiConfigValues.radius);
238
+ }
239
+ // Inject anti-FOUC theme init before CSS so saved light/dark/system preference
240
+ // is restored before first paint, preventing a flash on hard navigations.
241
+ if (uiConfigValues) {
242
+ const themeInitScript = `<script>(function(){try{var d=document.documentElement;var s=localStorage.getItem('kui-theme');var fallback=d.getAttribute('data-theme')==='system'?'system':(d.classList.contains('dark')?'dark':'light');var p=(s==='light'||s==='dark'||s==='system')?s:fallback;d.classList.remove('dark');d.removeAttribute('data-theme');if(p==='dark'){d.classList.add('dark');}else if(p==='system'){d.setAttribute('data-theme','system');}}catch(e){}})()</script>`;
243
+ source = source.replace('</head>', themeInitScript + '\n</head>');
244
+ }
232
245
  if (themeCSS) {
233
246
  source = source.replace('</head>', `<style>${themeCSS}</style>\n</head>`);
234
247
  }
@@ -239,384 +252,385 @@ export function compile(options) {
239
252
  // - server actions bound via onX={serverAction(...)} -> [data-action][data-action-event]
240
253
  // - declarative confirm="..."
241
254
  // - declarative checkbox groups: data-select-all / data-select-item
242
- const bridgeSource = `(function(){
243
- function by(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
244
- var __refreshSeq = Object.create(null);
245
- function syncGroup(group){
246
- var items = by('[data-select-item]').filter(function(el){ return el.getAttribute('data-select-item') === group; });
247
- var masters = by('[data-select-all]').filter(function(el){ return el.getAttribute('data-select-all') === group; });
248
- if(!items.length || !masters.length) return;
249
- var all = items.every(function(i){ return !!i.checked; });
250
- var any = items.some(function(i){ return !!i.checked; });
251
- masters.forEach(function(m){ m.checked = all; m.indeterminate = any && !all; });
252
- }
253
- function inferQueryKey(getName, argsRaw){
254
- if(!getName) return '';
255
- return 'query:' + String(getName) + '|' + (argsRaw || '[]');
256
- }
257
- function blockKey(el){
258
- if(!el || !el.getAttribute) return '';
259
- var explicit = el.getAttribute('data-key');
260
- if(explicit) return 'key:' + explicit;
261
- var inferred = inferQueryKey(el.getAttribute('data-get'), el.getAttribute('data-get-args'));
262
- if(inferred) return inferred;
263
- var asName = el.getAttribute('data-as');
264
- if(asName) return 'as:' + asName;
265
- return '';
266
- }
267
- function escHtml(v){
268
- return String(v || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
269
- }
270
- function setBlocksLoading(blocks){
271
- blocks.forEach(function(el){
272
- el.setAttribute('aria-busy','true');
273
- el.setAttribute('data-kuratchi-loading','1');
274
- var text = el.getAttribute('data-loading-text');
275
- if(text && !el.hasAttribute('data-as')){ el.innerHTML = '<p>' + escHtml(text) + '</p>'; return; }
276
- el.style.opacity = '0.6';
277
- });
278
- }
279
- function clearBlocksLoading(blocks){
280
- blocks.forEach(function(el){
281
- el.removeAttribute('aria-busy');
282
- el.removeAttribute('data-kuratchi-loading');
283
- el.style.opacity = '';
284
- });
285
- }
286
- function replaceBlocksWithKey(key){
287
- if(!key || typeof DOMParser === 'undefined'){ location.reload(); return Promise.resolve(); }
288
- var oldBlocks = by('[data-get]').filter(function(el){ return blockKey(el) === key; });
289
- if(!oldBlocks.length){ location.reload(); return Promise.resolve(); }
290
- var first = oldBlocks[0];
291
- var qFn = first ? (first.getAttribute('data-get') || '') : '';
292
- var qArgs = first ? String(first.getAttribute('data-get-args') || '[]') : '[]';
293
- var seq = (__refreshSeq[key] || 0) + 1;
294
- __refreshSeq[key] = seq;
295
- setBlocksLoading(oldBlocks);
296
- var headers = { 'x-kuratchi-refresh': '1' };
297
- if(qFn){ headers['x-kuratchi-query-fn'] = String(qFn); headers['x-kuratchi-query-args'] = qArgs; }
298
- return fetch(location.pathname + location.search, { headers: headers })
299
- .then(function(r){ if(!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
300
- .then(function(html){
301
- if(__refreshSeq[key] !== seq) return;
302
- var doc = new DOMParser().parseFromString(html, 'text/html');
303
- var newBlocks = by('[data-get]', doc).filter(function(el){ return blockKey(el) === key; });
304
- if(!oldBlocks.length || !newBlocks.length){ location.reload(); return; }
305
- for(var i=0;i<oldBlocks.length;i++){ if(newBlocks[i]) oldBlocks[i].outerHTML = newBlocks[i].outerHTML; }
306
- by('[data-select-all]').forEach(function(m){ var g=m.getAttribute('data-select-all'); if(g) syncGroup(g); });
307
- })
308
- .catch(function(){
309
- if(__refreshSeq[key] === seq) clearBlocksLoading(oldBlocks);
310
- location.reload();
311
- });
312
- }
313
- function replaceBlocksByDescriptor(fnName, argsRaw){
314
- if(!fnName || typeof DOMParser === 'undefined'){ location.reload(); return Promise.resolve(); }
315
- var normalizedArgs = String(argsRaw || '[]');
316
- var oldBlocks = by('[data-get]').filter(function(el){
317
- return (el.getAttribute('data-get') || '') === String(fnName) &&
318
- String(el.getAttribute('data-get-args') || '[]') === normalizedArgs;
319
- });
320
- if(!oldBlocks.length){ location.reload(); return Promise.resolve(); }
321
- var key = 'fn:' + String(fnName) + '|' + normalizedArgs;
322
- var seq = (__refreshSeq[key] || 0) + 1;
323
- __refreshSeq[key] = seq;
324
- setBlocksLoading(oldBlocks);
325
- return fetch(location.pathname + location.search, {
326
- headers: {
327
- 'x-kuratchi-refresh': '1',
328
- 'x-kuratchi-query-fn': String(fnName),
329
- 'x-kuratchi-query-args': normalizedArgs,
330
- }
331
- })
332
- .then(function(r){ if(!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
333
- .then(function(html){
334
- if(__refreshSeq[key] !== seq) return;
335
- var doc = new DOMParser().parseFromString(html, 'text/html');
336
- var newBlocks = by('[data-get]', doc).filter(function(el){
337
- return (el.getAttribute('data-get') || '') === String(fnName) &&
338
- String(el.getAttribute('data-get-args') || '[]') === normalizedArgs;
339
- });
340
- if(!newBlocks.length){ location.reload(); return; }
341
- for(var i=0;i<oldBlocks.length;i++){ if(newBlocks[i]) oldBlocks[i].outerHTML = newBlocks[i].outerHTML; }
342
- by('[data-select-all]').forEach(function(m){ var g=m.getAttribute('data-select-all'); if(g) syncGroup(g); });
343
- })
344
- .catch(function(){
345
- if(__refreshSeq[key] === seq) clearBlocksLoading(oldBlocks);
346
- location.reload();
347
- });
348
- }
349
- function refreshByDescriptor(fnName, argsRaw){
350
- if(!fnName) { location.reload(); return Promise.resolve(); }
351
- return replaceBlocksByDescriptor(fnName, argsRaw || '[]');
352
- }
353
- function refreshNearest(el){
354
- var host = el && el.closest ? el.closest('[data-get]') : null;
355
- if(!host){ location.reload(); return Promise.resolve(); }
356
- return replaceBlocksWithKey(blockKey(host));
357
- }
358
- function refreshTargets(raw){
359
- if(!raw){ location.reload(); return Promise.resolve(); }
360
- var keys = String(raw).split(',').map(function(v){ return v.trim(); }).filter(Boolean);
361
- if(!keys.length){ location.reload(); return Promise.resolve(); }
362
- return Promise.all(keys.map(function(k){ return replaceBlocksWithKey('key:' + k); })).then(function(){});
363
- }
364
- function act(e){
365
- if(e.type === 'click'){
366
- var g = e.target && e.target.closest ? e.target.closest('[data-get]') : null;
367
- if(g && !g.hasAttribute('data-as') && !g.hasAttribute('data-action')){
368
- var getUrl = g.getAttribute('data-get');
369
- if(getUrl){
370
- if(/^[a-z][a-z0-9+\-.]*:/i.test(getUrl) && !/^https?:/i.test(getUrl)) return;
371
- e.preventDefault();
372
- location.assign(getUrl);
373
- return;
374
- }
375
- }
376
- var r = e.target && e.target.closest ? e.target.closest('[data-refresh]') : null;
377
- if(r && !r.hasAttribute('data-action')){
378
- e.preventDefault();
379
- var rf = r.getAttribute('data-refresh');
380
- var ra = r.getAttribute('data-refresh-args');
381
- if(ra !== null){ refreshByDescriptor(rf, ra || '[]'); return; }
382
- if(rf && rf.trim()){ refreshTargets(rf); return; }
383
- refreshNearest(r);
384
- return;
385
- }
386
- }
387
- var sel = '[data-action][data-action-event="' + e.type + '"]';
388
- var b = e.target && e.target.closest ? e.target.closest(sel) : null;
389
- if(!b) return;
390
- e.preventDefault();
391
- var fd = new FormData();
392
- fd.append('_action', b.getAttribute('data-action') || '');
393
- fd.append('_args', b.getAttribute('data-args') || '[]');
394
- var m = b.getAttribute('data-action-method');
395
- if(m) fd.append('_method', String(m).toUpperCase());
396
- fetch(location.pathname, { method: 'POST', body: fd })
397
- .then(function(r){
398
- if(!r.ok){
399
- return r.json().then(function(j){ throw new Error((j && j.error) || ('HTTP ' + r.status)); }).catch(function(){ throw new Error('HTTP ' + r.status); });
400
- }
401
- return r.json();
402
- })
403
- .then(function(){
404
- if(!b.hasAttribute('data-refresh')) return;
405
- var refreshFn = b.getAttribute('data-refresh');
406
- var refreshArgs = b.getAttribute('data-refresh-args');
407
- if(refreshArgs !== null){ return refreshByDescriptor(refreshFn, refreshArgs || '[]'); }
408
- if(refreshFn && refreshFn.trim()){ return refreshTargets(refreshFn); }
409
- return refreshNearest(b);
410
- })
411
- .catch(function(err){ console.error('[kuratchi] client action error:', err); });
412
- }
413
- ['click','change','input','focus','blur'].forEach(function(ev){ document.addEventListener(ev, act, true); });
414
- function autoLoadQueries(){
415
- var seen = Object.create(null);
416
- by('[data-get][data-as]').forEach(function(el){
417
- var fn = el.getAttribute('data-get');
418
- if(!fn) return;
419
- var args = String(el.getAttribute('data-get-args') || '[]');
420
- var key = String(fn) + '|' + args;
421
- if(seen[key]) return;
422
- seen[key] = true;
423
- replaceBlocksByDescriptor(fn, args);
424
- });
425
- }
426
- if(document.readyState === 'loading'){
427
- document.addEventListener('DOMContentLoaded', autoLoadQueries, { once: true });
428
- } else {
429
- autoLoadQueries();
430
- }
431
- document.addEventListener('click', function(e){
432
- var b = e.target && e.target.closest ? e.target.closest('[command="fill-dialog"]') : null;
433
- if(!b) return;
434
- var targetId = b.getAttribute('commandfor');
435
- if(!targetId) return;
436
- var dialog = document.getElementById(targetId);
437
- if(!dialog) return;
438
- var raw = b.getAttribute('data-dialog-data');
439
- if(!raw) return;
440
- var data;
441
- try { data = JSON.parse(raw); } catch(_err) { return; }
442
- Object.keys(data).forEach(function(k){
443
- var inp = dialog.querySelector('[name="col_' + k + '"]');
444
- if(inp){
445
- inp.value = data[k] === null || data[k] === undefined ? '' : String(data[k]);
446
- inp.placeholder = data[k] === null || data[k] === undefined ? 'NULL' : '';
447
- }
448
- var hidden = dialog.querySelector('#dialog-field-' + k);
449
- if(hidden){
450
- hidden.value = data[k] === null || data[k] === undefined ? '' : String(data[k]);
451
- }
452
- });
453
- var rowidInp = dialog.querySelector('[name="rowid"]');
454
- if(rowidInp && data.rowid !== undefined) rowidInp.value = String(data.rowid);
455
- if(typeof dialog.showModal === 'function') dialog.showModal();
456
- }, true);
457
- (function initPoll(){
458
- var prev = {};
459
- function bindPollEl(el){
460
- if(!el || !el.getAttribute) return;
461
- if(el.getAttribute('data-kuratchi-poll-bound') === '1') return;
462
- var fn = el.getAttribute('data-poll');
463
- if(!fn) return;
464
- el.setAttribute('data-kuratchi-poll-bound', '1');
465
- var args = el.getAttribute('data-poll-args') || '[]';
466
- var iv = parseInt(el.getAttribute('data-interval') || '', 10) || 3000;
467
- var key = String(fn) + args;
468
- if(!(key in prev)) prev[key] = null;
469
- (function tick(){
470
- setTimeout(function(){
471
- fetch(location.pathname + '?_rpc=' + encodeURIComponent(String(fn)) + '&_args=' + encodeURIComponent(args), { headers: { 'x-kuratchi-rpc': '1' } })
472
- .then(function(r){ return r.json(); })
473
- .then(function(j){
474
- if(j && j.ok){
475
- var s = JSON.stringify(j.data);
476
- if(prev[key] !== null && prev[key] !== s){ location.reload(); return; }
477
- prev[key] = s;
478
- }
479
- tick();
480
- })
481
- .catch(function(){ tick(); });
482
- }, iv);
483
- })();
484
- }
485
- function scan(){
486
- by('[data-poll]').forEach(bindPollEl);
487
- }
488
- scan();
489
- setInterval(scan, 500);
490
- })();
491
- function confirmClick(e){
492
- var el = e.target && e.target.closest ? e.target.closest('[confirm]') : null;
493
- if(!el) return;
494
- var msg = el.getAttribute('confirm');
495
- if(!msg) return;
496
- if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
497
- }
498
- document.addEventListener('click', confirmClick, true);
499
- document.addEventListener('submit', function(e){
500
- var f = e.target && e.target.matches && e.target.matches('form[confirm]') ? e.target : null;
501
- if(!f) return;
502
- var msg = f.getAttribute('confirm');
503
- if(!msg) return;
504
- if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
505
- }, true);
506
- document.addEventListener('submit', function(e){
507
- if(e.defaultPrevented) return;
508
- var f = e.target;
509
- if(!f || !f.querySelector) return;
510
- var aInput = f.querySelector('input[name="_action"]');
511
- if(!aInput) return;
512
- var aName = aInput.value;
513
- if(!aName) return;
514
- f.setAttribute('data-action-loading', aName);
515
- Array.prototype.slice.call(f.querySelectorAll('button[type="submit"],button:not([type="button"]):not([type="reset"])')).forEach(function(b){ b.disabled = true; });
516
- }, true);
517
- document.addEventListener('change', function(e){
518
- var t = e.target;
519
- if(!t || !t.getAttribute) return;
520
- var gAll = t.getAttribute('data-select-all');
521
- if(gAll){
522
- by('[data-select-item]').filter(function(i){ return i.getAttribute('data-select-item') === gAll; }).forEach(function(i){ i.checked = !!t.checked; });
523
- syncGroup(gAll);
524
- return;
525
- }
526
- var gItem = t.getAttribute('data-select-item');
527
- if(gItem) syncGroup(gItem);
528
- }, true);
529
- by('[data-select-all]').forEach(function(m){ var g = m.getAttribute('data-select-all'); if(g) syncGroup(g); });
255
+ const bridgeSource = `(function(){
256
+ function by(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
257
+ var __refreshSeq = Object.create(null);
258
+ function syncGroup(group){
259
+ var items = by('[data-select-item]').filter(function(el){ return el.getAttribute('data-select-item') === group; });
260
+ var masters = by('[data-select-all]').filter(function(el){ return el.getAttribute('data-select-all') === group; });
261
+ if(!items.length || !masters.length) return;
262
+ var all = items.every(function(i){ return !!i.checked; });
263
+ var any = items.some(function(i){ return !!i.checked; });
264
+ masters.forEach(function(m){ m.checked = all; m.indeterminate = any && !all; });
265
+ }
266
+ function inferQueryKey(getName, argsRaw){
267
+ if(!getName) return '';
268
+ return 'query:' + String(getName) + '|' + (argsRaw || '[]');
269
+ }
270
+ function blockKey(el){
271
+ if(!el || !el.getAttribute) return '';
272
+ var explicit = el.getAttribute('data-key');
273
+ if(explicit) return 'key:' + explicit;
274
+ var inferred = inferQueryKey(el.getAttribute('data-get'), el.getAttribute('data-get-args'));
275
+ if(inferred) return inferred;
276
+ var asName = el.getAttribute('data-as');
277
+ if(asName) return 'as:' + asName;
278
+ return '';
279
+ }
280
+ function escHtml(v){
281
+ return String(v || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
282
+ }
283
+ function setBlocksLoading(blocks){
284
+ blocks.forEach(function(el){
285
+ el.setAttribute('aria-busy','true');
286
+ el.setAttribute('data-kuratchi-loading','1');
287
+ var text = el.getAttribute('data-loading-text');
288
+ if(text && !el.hasAttribute('data-as')){ el.innerHTML = '<p>' + escHtml(text) + '</p>'; return; }
289
+ el.style.opacity = '0.6';
290
+ });
291
+ }
292
+ function clearBlocksLoading(blocks){
293
+ blocks.forEach(function(el){
294
+ el.removeAttribute('aria-busy');
295
+ el.removeAttribute('data-kuratchi-loading');
296
+ el.style.opacity = '';
297
+ });
298
+ }
299
+ function replaceBlocksWithKey(key){
300
+ if(!key || typeof DOMParser === 'undefined'){ location.reload(); return Promise.resolve(); }
301
+ var oldBlocks = by('[data-get]').filter(function(el){ return blockKey(el) === key; });
302
+ if(!oldBlocks.length){ location.reload(); return Promise.resolve(); }
303
+ var first = oldBlocks[0];
304
+ var qFn = first ? (first.getAttribute('data-get') || '') : '';
305
+ var qArgs = first ? String(first.getAttribute('data-get-args') || '[]') : '[]';
306
+ var seq = (__refreshSeq[key] || 0) + 1;
307
+ __refreshSeq[key] = seq;
308
+ setBlocksLoading(oldBlocks);
309
+ var headers = { 'x-kuratchi-refresh': '1' };
310
+ if(qFn){ headers['x-kuratchi-query-fn'] = String(qFn); headers['x-kuratchi-query-args'] = qArgs; }
311
+ return fetch(location.pathname + location.search, { headers: headers })
312
+ .then(function(r){ if(!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
313
+ .then(function(html){
314
+ if(__refreshSeq[key] !== seq) return;
315
+ var doc = new DOMParser().parseFromString(html, 'text/html');
316
+ var newBlocks = by('[data-get]', doc).filter(function(el){ return blockKey(el) === key; });
317
+ if(!oldBlocks.length || !newBlocks.length){ location.reload(); return; }
318
+ for(var i=0;i<oldBlocks.length;i++){ if(newBlocks[i]) oldBlocks[i].outerHTML = newBlocks[i].outerHTML; }
319
+ by('[data-select-all]').forEach(function(m){ var g=m.getAttribute('data-select-all'); if(g) syncGroup(g); });
320
+ })
321
+ .catch(function(){
322
+ if(__refreshSeq[key] === seq) clearBlocksLoading(oldBlocks);
323
+ location.reload();
324
+ });
325
+ }
326
+ function replaceBlocksByDescriptor(fnName, argsRaw){
327
+ if(!fnName || typeof DOMParser === 'undefined'){ location.reload(); return Promise.resolve(); }
328
+ var normalizedArgs = String(argsRaw || '[]');
329
+ var oldBlocks = by('[data-get]').filter(function(el){
330
+ return (el.getAttribute('data-get') || '') === String(fnName) &&
331
+ String(el.getAttribute('data-get-args') || '[]') === normalizedArgs;
332
+ });
333
+ if(!oldBlocks.length){ location.reload(); return Promise.resolve(); }
334
+ var key = 'fn:' + String(fnName) + '|' + normalizedArgs;
335
+ var seq = (__refreshSeq[key] || 0) + 1;
336
+ __refreshSeq[key] = seq;
337
+ setBlocksLoading(oldBlocks);
338
+ return fetch(location.pathname + location.search, {
339
+ headers: {
340
+ 'x-kuratchi-refresh': '1',
341
+ 'x-kuratchi-query-fn': String(fnName),
342
+ 'x-kuratchi-query-args': normalizedArgs,
343
+ }
344
+ })
345
+ .then(function(r){ if(!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
346
+ .then(function(html){
347
+ if(__refreshSeq[key] !== seq) return;
348
+ var doc = new DOMParser().parseFromString(html, 'text/html');
349
+ var newBlocks = by('[data-get]', doc).filter(function(el){
350
+ return (el.getAttribute('data-get') || '') === String(fnName) &&
351
+ String(el.getAttribute('data-get-args') || '[]') === normalizedArgs;
352
+ });
353
+ if(!newBlocks.length){ location.reload(); return; }
354
+ for(var i=0;i<oldBlocks.length;i++){ if(newBlocks[i]) oldBlocks[i].outerHTML = newBlocks[i].outerHTML; }
355
+ by('[data-select-all]').forEach(function(m){ var g=m.getAttribute('data-select-all'); if(g) syncGroup(g); });
356
+ })
357
+ .catch(function(){
358
+ if(__refreshSeq[key] === seq) clearBlocksLoading(oldBlocks);
359
+ location.reload();
360
+ });
361
+ }
362
+ function refreshByDescriptor(fnName, argsRaw){
363
+ if(!fnName) { location.reload(); return Promise.resolve(); }
364
+ return replaceBlocksByDescriptor(fnName, argsRaw || '[]');
365
+ }
366
+ function refreshNearest(el){
367
+ var host = el && el.closest ? el.closest('[data-get]') : null;
368
+ if(!host){ location.reload(); return Promise.resolve(); }
369
+ return replaceBlocksWithKey(blockKey(host));
370
+ }
371
+ function refreshTargets(raw){
372
+ if(!raw){ location.reload(); return Promise.resolve(); }
373
+ var keys = String(raw).split(',').map(function(v){ return v.trim(); }).filter(Boolean);
374
+ if(!keys.length){ location.reload(); return Promise.resolve(); }
375
+ return Promise.all(keys.map(function(k){ return replaceBlocksWithKey('key:' + k); })).then(function(){});
376
+ }
377
+ function act(e){
378
+ if(e.type === 'click'){
379
+ var g = e.target && e.target.closest ? e.target.closest('[data-get]') : null;
380
+ if(g && !g.hasAttribute('data-as') && !g.hasAttribute('data-action')){
381
+ var getUrl = g.getAttribute('data-get');
382
+ if(getUrl){
383
+ if(/^[a-z][a-z0-9+\-.]*:/i.test(getUrl) && !/^https?:/i.test(getUrl)) return;
384
+ e.preventDefault();
385
+ location.assign(getUrl);
386
+ return;
387
+ }
388
+ }
389
+ var r = e.target && e.target.closest ? e.target.closest('[data-refresh]') : null;
390
+ if(r && !r.hasAttribute('data-action')){
391
+ e.preventDefault();
392
+ var rf = r.getAttribute('data-refresh');
393
+ var ra = r.getAttribute('data-refresh-args');
394
+ if(ra !== null){ refreshByDescriptor(rf, ra || '[]'); return; }
395
+ if(rf && rf.trim()){ refreshTargets(rf); return; }
396
+ refreshNearest(r);
397
+ return;
398
+ }
399
+ }
400
+ var sel = '[data-action][data-action-event="' + e.type + '"]';
401
+ var b = e.target && e.target.closest ? e.target.closest(sel) : null;
402
+ if(!b) return;
403
+ e.preventDefault();
404
+ var fd = new FormData();
405
+ fd.append('_action', b.getAttribute('data-action') || '');
406
+ fd.append('_args', b.getAttribute('data-args') || '[]');
407
+ var m = b.getAttribute('data-action-method');
408
+ if(m) fd.append('_method', String(m).toUpperCase());
409
+ fetch(location.pathname, { method: 'POST', body: fd })
410
+ .then(function(r){
411
+ if(!r.ok){
412
+ return r.json().then(function(j){ throw new Error((j && j.error) || ('HTTP ' + r.status)); }).catch(function(){ throw new Error('HTTP ' + r.status); });
413
+ }
414
+ return r.json();
415
+ })
416
+ .then(function(j){
417
+ if(j && j.redirectTo){ location.assign(j.redirectTo); return; }
418
+ if(!b.hasAttribute('data-refresh')) return;
419
+ var refreshFn = b.getAttribute('data-refresh');
420
+ var refreshArgs = b.getAttribute('data-refresh-args');
421
+ if(refreshArgs !== null){ return refreshByDescriptor(refreshFn, refreshArgs || '[]'); }
422
+ if(refreshFn && refreshFn.trim()){ return refreshTargets(refreshFn); }
423
+ return refreshNearest(b);
424
+ })
425
+ .catch(function(err){ console.error('[kuratchi] client action error:', err); });
426
+ }
427
+ ['click','change','input','focus','blur'].forEach(function(ev){ document.addEventListener(ev, act, true); });
428
+ function autoLoadQueries(){
429
+ var seen = Object.create(null);
430
+ by('[data-get][data-as]').forEach(function(el){
431
+ var fn = el.getAttribute('data-get');
432
+ if(!fn) return;
433
+ var args = String(el.getAttribute('data-get-args') || '[]');
434
+ var key = String(fn) + '|' + args;
435
+ if(seen[key]) return;
436
+ seen[key] = true;
437
+ replaceBlocksByDescriptor(fn, args);
438
+ });
439
+ }
440
+ if(document.readyState === 'loading'){
441
+ document.addEventListener('DOMContentLoaded', autoLoadQueries, { once: true });
442
+ } else {
443
+ autoLoadQueries();
444
+ }
445
+ document.addEventListener('click', function(e){
446
+ var b = e.target && e.target.closest ? e.target.closest('[command="fill-dialog"]') : null;
447
+ if(!b) return;
448
+ var targetId = b.getAttribute('commandfor');
449
+ if(!targetId) return;
450
+ var dialog = document.getElementById(targetId);
451
+ if(!dialog) return;
452
+ var raw = b.getAttribute('data-dialog-data');
453
+ if(!raw) return;
454
+ var data;
455
+ try { data = JSON.parse(raw); } catch(_err) { return; }
456
+ Object.keys(data).forEach(function(k){
457
+ var inp = dialog.querySelector('[name="col_' + k + '"]');
458
+ if(inp){
459
+ inp.value = data[k] === null || data[k] === undefined ? '' : String(data[k]);
460
+ inp.placeholder = data[k] === null || data[k] === undefined ? 'NULL' : '';
461
+ }
462
+ var hidden = dialog.querySelector('#dialog-field-' + k);
463
+ if(hidden){
464
+ hidden.value = data[k] === null || data[k] === undefined ? '' : String(data[k]);
465
+ }
466
+ });
467
+ var rowidInp = dialog.querySelector('[name="rowid"]');
468
+ if(rowidInp && data.rowid !== undefined) rowidInp.value = String(data.rowid);
469
+ if(typeof dialog.showModal === 'function') dialog.showModal();
470
+ }, true);
471
+ (function initPoll(){
472
+ var prev = {};
473
+ function bindPollEl(el){
474
+ if(!el || !el.getAttribute) return;
475
+ if(el.getAttribute('data-kuratchi-poll-bound') === '1') return;
476
+ var fn = el.getAttribute('data-poll');
477
+ if(!fn) return;
478
+ el.setAttribute('data-kuratchi-poll-bound', '1');
479
+ var args = el.getAttribute('data-poll-args') || '[]';
480
+ var iv = parseInt(el.getAttribute('data-interval') || '', 10) || 3000;
481
+ var key = String(fn) + args;
482
+ if(!(key in prev)) prev[key] = null;
483
+ (function tick(){
484
+ setTimeout(function(){
485
+ fetch(location.pathname + '?_rpc=' + encodeURIComponent(String(fn)) + '&_args=' + encodeURIComponent(args), { headers: { 'x-kuratchi-rpc': '1' } })
486
+ .then(function(r){ return r.json(); })
487
+ .then(function(j){
488
+ if(j && j.ok){
489
+ var s = JSON.stringify(j.data);
490
+ if(prev[key] !== null && prev[key] !== s){ location.reload(); return; }
491
+ prev[key] = s;
492
+ }
493
+ tick();
494
+ })
495
+ .catch(function(){ tick(); });
496
+ }, iv);
497
+ })();
498
+ }
499
+ function scan(){
500
+ by('[data-poll]').forEach(bindPollEl);
501
+ }
502
+ scan();
503
+ setInterval(scan, 500);
504
+ })();
505
+ function confirmClick(e){
506
+ var el = e.target && e.target.closest ? e.target.closest('[confirm]') : null;
507
+ if(!el) return;
508
+ var msg = el.getAttribute('confirm');
509
+ if(!msg) return;
510
+ if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
511
+ }
512
+ document.addEventListener('click', confirmClick, true);
513
+ document.addEventListener('submit', function(e){
514
+ var f = e.target && e.target.matches && e.target.matches('form[confirm]') ? e.target : null;
515
+ if(!f) return;
516
+ var msg = f.getAttribute('confirm');
517
+ if(!msg) return;
518
+ if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
519
+ }, true);
520
+ document.addEventListener('submit', function(e){
521
+ if(e.defaultPrevented) return;
522
+ var f = e.target;
523
+ if(!f || !f.querySelector) return;
524
+ var aInput = f.querySelector('input[name="_action"]');
525
+ if(!aInput) return;
526
+ var aName = aInput.value;
527
+ if(!aName) return;
528
+ f.setAttribute('data-action-loading', aName);
529
+ Array.prototype.slice.call(f.querySelectorAll('button[type="submit"],button:not([type="button"]):not([type="reset"])')).forEach(function(b){ b.disabled = true; });
530
+ }, true);
531
+ document.addEventListener('change', function(e){
532
+ var t = e.target;
533
+ if(!t || !t.getAttribute) return;
534
+ var gAll = t.getAttribute('data-select-all');
535
+ if(gAll){
536
+ by('[data-select-item]').filter(function(i){ return i.getAttribute('data-select-item') === gAll; }).forEach(function(i){ i.checked = !!t.checked; });
537
+ syncGroup(gAll);
538
+ return;
539
+ }
540
+ var gItem = t.getAttribute('data-select-item');
541
+ if(gItem) syncGroup(gItem);
542
+ }, true);
543
+ by('[data-select-all]').forEach(function(m){ var g = m.getAttribute('data-select-all'); if(g) syncGroup(g); });
530
544
  })();`;
531
- const reactiveRuntimeSource = `(function(g){
532
- if(g.__kuratchiReactive) return;
533
- const targetMap = new WeakMap();
534
- const proxyMap = new WeakMap();
535
- let active = null;
536
- const queue = new Set();
537
- let flushing = false;
538
- function queueRun(fn){
539
- queue.add(fn);
540
- if(flushing) return;
541
- flushing = true;
542
- Promise.resolve().then(function(){
543
- try {
544
- const jobs = Array.from(queue);
545
- queue.clear();
546
- for (const job of jobs) job();
547
- } finally {
548
- flushing = false;
549
- }
550
- });
551
- }
552
- function cleanup(effect){
553
- const deps = effect.__deps || [];
554
- for (const dep of deps) dep.delete(effect);
555
- effect.__deps = [];
556
- }
557
- function track(target, key){
558
- if(!active) return;
559
- let depsMap = targetMap.get(target);
560
- if(!depsMap){ depsMap = new Map(); targetMap.set(target, depsMap); }
561
- let dep = depsMap.get(key);
562
- if(!dep){ dep = new Set(); depsMap.set(key, dep); }
563
- if(dep.has(active)) return;
564
- dep.add(active);
565
- if(!active.__deps) active.__deps = [];
566
- active.__deps.push(dep);
567
- }
568
- function trigger(target, key){
569
- const depsMap = targetMap.get(target);
570
- if(!depsMap) return;
571
- const effects = new Set();
572
- const add = function(k){
573
- const dep = depsMap.get(k);
574
- if(dep) dep.forEach(function(e){ effects.add(e); });
575
- };
576
- add(key);
577
- add('*');
578
- effects.forEach(function(e){ queueRun(e); });
579
- }
580
- function isObject(value){ return value !== null && typeof value === 'object'; }
581
- function proxify(value){
582
- if(!isObject(value)) return value;
583
- if(proxyMap.has(value)) return proxyMap.get(value);
584
- const proxy = new Proxy(value, {
585
- get(target, key, receiver){
586
- track(target, key);
587
- const out = Reflect.get(target, key, receiver);
588
- return isObject(out) ? proxify(out) : out;
589
- },
590
- set(target, key, next, receiver){
591
- const prev = target[key];
592
- const result = Reflect.set(target, key, next, receiver);
593
- if(prev !== next) trigger(target, key);
594
- if(Array.isArray(target) && key !== 'length') trigger(target, 'length');
595
- return result;
596
- },
597
- deleteProperty(target, key){
598
- const had = Object.prototype.hasOwnProperty.call(target, key);
599
- const result = Reflect.deleteProperty(target, key);
600
- if(had) trigger(target, key);
601
- return result;
602
- }
603
- });
604
- proxyMap.set(value, proxy);
605
- return proxy;
606
- }
607
- function effect(fn){
608
- const run = function(){
609
- cleanup(run);
610
- active = run;
611
- try { fn(); } finally { active = null; }
612
- };
613
- run.__deps = [];
614
- run();
615
- return function(){ cleanup(run); };
616
- }
617
- function state(initial){ return proxify(initial); }
618
- function replace(_prev, next){ return proxify(next); }
619
- g.__kuratchiReactive = { state, effect, replace };
545
+ const reactiveRuntimeSource = `(function(g){
546
+ if(g.__kuratchiReactive) return;
547
+ const targetMap = new WeakMap();
548
+ const proxyMap = new WeakMap();
549
+ let active = null;
550
+ const queue = new Set();
551
+ let flushing = false;
552
+ function queueRun(fn){
553
+ queue.add(fn);
554
+ if(flushing) return;
555
+ flushing = true;
556
+ Promise.resolve().then(function(){
557
+ try {
558
+ const jobs = Array.from(queue);
559
+ queue.clear();
560
+ for (const job of jobs) job();
561
+ } finally {
562
+ flushing = false;
563
+ }
564
+ });
565
+ }
566
+ function cleanup(effect){
567
+ const deps = effect.__deps || [];
568
+ for (const dep of deps) dep.delete(effect);
569
+ effect.__deps = [];
570
+ }
571
+ function track(target, key){
572
+ if(!active) return;
573
+ let depsMap = targetMap.get(target);
574
+ if(!depsMap){ depsMap = new Map(); targetMap.set(target, depsMap); }
575
+ let dep = depsMap.get(key);
576
+ if(!dep){ dep = new Set(); depsMap.set(key, dep); }
577
+ if(dep.has(active)) return;
578
+ dep.add(active);
579
+ if(!active.__deps) active.__deps = [];
580
+ active.__deps.push(dep);
581
+ }
582
+ function trigger(target, key){
583
+ const depsMap = targetMap.get(target);
584
+ if(!depsMap) return;
585
+ const effects = new Set();
586
+ const add = function(k){
587
+ const dep = depsMap.get(k);
588
+ if(dep) dep.forEach(function(e){ effects.add(e); });
589
+ };
590
+ add(key);
591
+ add('*');
592
+ effects.forEach(function(e){ queueRun(e); });
593
+ }
594
+ function isObject(value){ return value !== null && typeof value === 'object'; }
595
+ function proxify(value){
596
+ if(!isObject(value)) return value;
597
+ if(proxyMap.has(value)) return proxyMap.get(value);
598
+ const proxy = new Proxy(value, {
599
+ get(target, key, receiver){
600
+ track(target, key);
601
+ const out = Reflect.get(target, key, receiver);
602
+ return isObject(out) ? proxify(out) : out;
603
+ },
604
+ set(target, key, next, receiver){
605
+ const prev = target[key];
606
+ const result = Reflect.set(target, key, next, receiver);
607
+ if(prev !== next) trigger(target, key);
608
+ if(Array.isArray(target) && key !== 'length') trigger(target, 'length');
609
+ return result;
610
+ },
611
+ deleteProperty(target, key){
612
+ const had = Object.prototype.hasOwnProperty.call(target, key);
613
+ const result = Reflect.deleteProperty(target, key);
614
+ if(had) trigger(target, key);
615
+ return result;
616
+ }
617
+ });
618
+ proxyMap.set(value, proxy);
619
+ return proxy;
620
+ }
621
+ function effect(fn){
622
+ const run = function(){
623
+ cleanup(run);
624
+ active = run;
625
+ try { fn(); } finally { active = null; }
626
+ };
627
+ run.__deps = [];
628
+ run();
629
+ return function(){ cleanup(run); };
630
+ }
631
+ function state(initial){ return proxify(initial); }
632
+ function replace(_prev, next){ return proxify(next); }
633
+ g.__kuratchiReactive = { state, effect, replace };
620
634
  })(window);`;
621
635
  const actionScript = `<script>${options.isDev ? bridgeSource : compactInlineJs(bridgeSource)}</script>`;
622
636
  const reactiveRuntimeScript = `<script>${options.isDev ? reactiveRuntimeSource : compactInlineJs(reactiveRuntimeSource)}</script>`;
@@ -631,7 +645,7 @@ export function compile(options) {
631
645
  const layoutParsed = parseFile(source, { kind: 'layout', filePath: layoutFile });
632
646
  const hasLayoutScript = layoutParsed.script && (Object.keys(layoutParsed.componentImports).length > 0 || layoutParsed.hasLoad);
633
647
  if (hasLayoutScript) {
634
- // Dynamic layout â€" has component imports and/or data declarations
648
+ // Dynamic layout " has component imports and/or data declarations
635
649
  // Compile component imports from layout
636
650
  for (const [pascalName, fileName] of Object.entries(layoutParsed.componentImports)) {
637
651
  compileComponent(fileName);
@@ -640,8 +654,25 @@ export function compile(options) {
640
654
  // Replace <slot></slot> with content parameter injection
641
655
  let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{@raw __content}');
642
656
  layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{@raw __content}');
643
- // Build layout action names so action={fn} works in layouts
657
+ // Build layout action names so action={fn} works in layouts, including action props
658
+ // passed through child components like <Dashboard footerSignOutAction={signOut}>.
644
659
  const layoutActionNames = new Set(layoutParsed.actionFunctions);
660
+ for (const [pascalName, compFileName] of layoutComponentNames.entries()) {
661
+ const actionPropNames = componentActionCache.get(compFileName);
662
+ const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)(?:/?)>`, 'g');
663
+ for (const tagMatch of layoutParsed.template.matchAll(compTagRegex)) {
664
+ const attrs = tagMatch[1];
665
+ if (actionPropNames && actionPropNames.size > 0) {
666
+ for (const propName of actionPropNames) {
667
+ const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
668
+ const propMatch = attrs.match(propRegex);
669
+ if (propMatch) {
670
+ layoutActionNames.add(propMatch[1]);
671
+ }
672
+ }
673
+ }
674
+ }
675
+ }
645
676
  // Compile the layout template with component + action support
646
677
  const layoutRenderBody = compileTemplate(layoutTemplate, layoutComponentNames, layoutActionNames);
647
678
  // Collect component CSS for layout
@@ -659,17 +690,17 @@ export function compile(options) {
659
690
  finalLayoutBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
660
691
  }
661
692
  // Build the layout script body (data vars, etc.)
662
- let layoutScriptBody = layoutParsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim();
693
+ let layoutScriptBody = stripTopLevelImports(layoutParsed.script);
663
694
  const layoutDevDecls = buildDevAliasDeclarations(layoutParsed.devAliases, !!options.isDev);
664
695
  layoutScriptBody = [layoutDevDecls, layoutScriptBody].filter(Boolean).join('\n');
665
- compiledLayout = `function __layout(__content) {
666
- const __esc = (v) => { if (v == null) return ''; return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); };
667
- ${layoutScriptBody ? layoutScriptBody + '\n ' : ''}${finalLayoutBody}
668
- return __html;
696
+ compiledLayout = `function __layout(__content) {
697
+ const __esc = (v) => { if (v == null) return ''; return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); };
698
+ ${layoutScriptBody ? layoutScriptBody + '\n ' : ''}${finalLayoutBody}
699
+ return __html;
669
700
  }`;
670
701
  }
671
702
  else {
672
- // Static layout â€" no components, use fast string split (original behavior)
703
+ // Static layout " no components, use fast string split (original behavior)
673
704
  const slotMarker = '<slot></slot>';
674
705
  const slotIdx = source.indexOf(slotMarker);
675
706
  if (slotIdx === -1) {
@@ -682,7 +713,7 @@ export function compile(options) {
682
713
  }
683
714
  }
684
715
  // Custom error pages: src/routes/NNN.html (e.g. 404.html, 500.html, 401.html, 403.html)
685
- // Only compiled if the user explicitly creates them â€" otherwise the framework's built-in default is used
716
+ // Only compiled if the user explicitly creates them " otherwise the framework's built-in default is used
686
717
  const compiledErrorPages = new Map();
687
718
  for (const file of fs.readdirSync(routesDir)) {
688
719
  const match = file.match(/^(\d{3})\.html$/);
@@ -702,6 +733,7 @@ export function compile(options) {
702
733
  const doConfig = readDoConfig(projectDir);
703
734
  const containerConfig = readWorkerClassConfig(projectDir, 'containers');
704
735
  const workflowConfig = readWorkerClassConfig(projectDir, 'workflows');
736
+ const agentConfig = discoverConventionClassFiles(projectDir, path.join('src', 'server'), '.agent.ts', '.agent');
705
737
  const doHandlers = doConfig.length > 0
706
738
  ? discoverDoHandlers(srcDir, doConfig, ormDatabases)
707
739
  : [];
@@ -860,7 +892,7 @@ export function compile(options) {
860
892
  const compiledRoutes = [];
861
893
  const allImports = [];
862
894
  let moduleCounter = 0;
863
- // Layout server import resolution â€" resolve non-component imports to module IDs
895
+ // Layout server import resolution " resolve non-component imports to module IDs
864
896
  let isLayoutAsync = false;
865
897
  let compiledLayoutActions = null;
866
898
  if (compiledLayout && fs.existsSync(path.join(routesDir, 'layout.html'))) {
@@ -905,9 +937,27 @@ export function compile(options) {
905
937
  const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
906
938
  compiledLayout = compiledLayout.replace(callRegex, `${moduleId}.${fnName}(`);
907
939
  }
908
- // Generate layout actions map for action={fn} in layouts
909
- if (layoutParsedForImports.actionFunctions.length > 0) {
910
- const actionEntries = layoutParsedForImports.actionFunctions
940
+ // Generate layout actions map for action={fn} in layouts and action props passed
941
+ // through layout components.
942
+ const layoutActionNames = new Set(layoutParsedForImports.actionFunctions);
943
+ for (const [pascalName, compFileName] of layoutComponentNames.entries()) {
944
+ const actionPropNames = componentActionCache.get(compFileName);
945
+ const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)(?:/?)>`, 'g');
946
+ for (const tagMatch of layoutParsedForImports.template.matchAll(compTagRegex)) {
947
+ const attrs = tagMatch[1];
948
+ if (actionPropNames && actionPropNames.size > 0) {
949
+ for (const propName of actionPropNames) {
950
+ const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
951
+ const propMatch = attrs.match(propRegex);
952
+ if (propMatch) {
953
+ layoutActionNames.add(propMatch[1]);
954
+ }
955
+ }
956
+ }
957
+ }
958
+ }
959
+ if (layoutActionNames.size > 0) {
960
+ const actionEntries = Array.from(layoutActionNames)
911
961
  .filter(fn => fn in layoutFnToModule)
912
962
  .map(fn => `'${fn}': ${layoutFnToModule[fn]}.${fn}`)
913
963
  .join(', ');
@@ -916,7 +966,7 @@ export function compile(options) {
916
966
  }
917
967
  }
918
968
  }
919
- // Detect if the compiled layout uses await â†' make it async
969
+ // Detect if the compiled layout uses await ' make it async
920
970
  isLayoutAsync = /\bawait\b/.test(compiledLayout);
921
971
  if (isLayoutAsync) {
922
972
  compiledLayout = compiledLayout.replace(/^function __layout\(/, 'async function __layout(');
@@ -953,119 +1003,184 @@ export function compile(options) {
953
1003
  compiledRoutes.push(`{ pattern: '${pattern}', __api: true, ${methodEntries} }`);
954
1004
  continue;
955
1005
  }
956
- // ── Page route (page.html) ──
1006
+ // -- Page route (page.html) --
957
1007
  const source = fs.readFileSync(fullPath, 'utf-8');
958
1008
  const parsed = parseFile(source, { kind: 'route', filePath: fullPath });
959
- // Build a mapping: functionName â†' moduleId for all imports in this route
1009
+ let effectiveTemplate = parsed.template;
1010
+ const routeScriptParts = [];
1011
+ const routeScriptSegments = [];
1012
+ const routeServerImportEntries = parsed.serverImports.map((line) => ({
1013
+ line,
1014
+ importerDir: path.dirname(fullPath),
1015
+ }));
1016
+ const routeClientImportEntries = parsed.clientImports.map((line) => ({
1017
+ line,
1018
+ importerDir: path.dirname(fullPath),
1019
+ }));
1020
+ const mergedActionFunctions = [...parsed.actionFunctions];
1021
+ const mergedDataVars = [...parsed.dataVars];
1022
+ const mergedPollFunctions = [...parsed.pollFunctions];
1023
+ const mergedDataGetQueries = parsed.dataGetQueries.map((query) => ({ ...query }));
1024
+ const mergedComponentImports = { ...parsed.componentImports };
1025
+ const mergedWorkerEnvAliases = [...parsed.workerEnvAliases];
1026
+ const mergedDevAliases = [...parsed.devAliases];
1027
+ for (const layoutRelPath of rf.layouts) {
1028
+ if (layoutRelPath === 'layout.html')
1029
+ continue;
1030
+ const layoutPath = path.join(routesDir, layoutRelPath);
1031
+ if (!fs.existsSync(layoutPath))
1032
+ continue;
1033
+ const layoutSource = fs.readFileSync(layoutPath, 'utf-8');
1034
+ const layoutParsed = parseFile(layoutSource, { kind: 'layout', filePath: layoutPath });
1035
+ if (layoutParsed.loadFunction) {
1036
+ throw new Error(`${layoutRelPath} cannot export load(); nested layouts currently share the child route load lifecycle.`);
1037
+ }
1038
+ const layoutSlot = layoutParsed.template.match(/<slot\s*><\/slot>|<slot\s*\/>/);
1039
+ if (!layoutSlot) {
1040
+ throw new Error(`${layoutRelPath} must contain <slot></slot> or <slot />`);
1041
+ }
1042
+ if (layoutParsed.script) {
1043
+ routeScriptParts.push(layoutParsed.script);
1044
+ routeScriptSegments.push({ script: layoutParsed.script, dataVars: [...layoutParsed.dataVars] });
1045
+ }
1046
+ for (const line of layoutParsed.serverImports) {
1047
+ routeServerImportEntries.push({ line, importerDir: path.dirname(layoutPath) });
1048
+ }
1049
+ for (const line of layoutParsed.clientImports) {
1050
+ routeClientImportEntries.push({ line, importerDir: path.dirname(layoutPath) });
1051
+ }
1052
+ for (const fnName of layoutParsed.actionFunctions) {
1053
+ if (!mergedActionFunctions.includes(fnName))
1054
+ mergedActionFunctions.push(fnName);
1055
+ }
1056
+ for (const varName of layoutParsed.dataVars) {
1057
+ if (!mergedDataVars.includes(varName))
1058
+ mergedDataVars.push(varName);
1059
+ }
1060
+ for (const fnName of layoutParsed.pollFunctions) {
1061
+ if (!mergedPollFunctions.includes(fnName))
1062
+ mergedPollFunctions.push(fnName);
1063
+ }
1064
+ for (const query of layoutParsed.dataGetQueries) {
1065
+ if (!mergedDataGetQueries.some((existing) => existing.asName === query.asName)) {
1066
+ mergedDataGetQueries.push({ ...query });
1067
+ }
1068
+ }
1069
+ for (const [pascalName, fileName] of Object.entries(layoutParsed.componentImports)) {
1070
+ mergedComponentImports[pascalName] = fileName;
1071
+ }
1072
+ for (const alias of layoutParsed.workerEnvAliases) {
1073
+ if (!mergedWorkerEnvAliases.includes(alias))
1074
+ mergedWorkerEnvAliases.push(alias);
1075
+ }
1076
+ for (const alias of layoutParsed.devAliases) {
1077
+ if (!mergedDevAliases.includes(alias))
1078
+ mergedDevAliases.push(alias);
1079
+ }
1080
+ effectiveTemplate = layoutParsed.template.replace(layoutSlot[0], effectiveTemplate);
1081
+ }
1082
+ if (parsed.script) {
1083
+ routeScriptParts.push(parsed.script);
1084
+ routeScriptSegments.push({ script: parsed.script, dataVars: [...parsed.dataVars] });
1085
+ }
1086
+ const routeImportDecls = [];
1087
+ const routeImportDeclMap = new Map();
1088
+ const routeScriptReferenceSource = [...routeScriptParts.map((script) => stripTopLevelImports(script)), parsed.loadFunction || ''].join('\n');
1089
+ const mergedParsed = {
1090
+ ...parsed,
1091
+ template: effectiveTemplate,
1092
+ script: routeScriptParts.length > 0 ? routeScriptParts.join('\n\n') : parsed.script,
1093
+ serverImports: routeServerImportEntries.map((entry) => entry.line),
1094
+ clientImports: routeClientImportEntries.map((entry) => entry.line),
1095
+ actionFunctions: mergedActionFunctions,
1096
+ dataVars: mergedDataVars,
1097
+ componentImports: mergedComponentImports,
1098
+ pollFunctions: mergedPollFunctions,
1099
+ dataGetQueries: mergedDataGetQueries,
1100
+ workerEnvAliases: mergedWorkerEnvAliases,
1101
+ devAliases: mergedDevAliases,
1102
+ scriptImportDecls: routeImportDecls,
1103
+ scriptSegments: routeScriptSegments,
1104
+ };
1105
+ // Build a mapping: functionName ? moduleId for all imports in this route
960
1106
  const fnToModule = {};
961
1107
  const outFileDir = path.join(projectDir, '.kuratchi');
962
1108
  const neededServerFns = new Set([
963
- ...parsed.actionFunctions,
964
- ...parsed.pollFunctions,
965
- ...parsed.dataGetQueries.map((q) => q.fnName),
1109
+ ...mergedActionFunctions,
1110
+ ...mergedPollFunctions,
1111
+ ...mergedDataGetQueries.map((q) => q.fnName),
966
1112
  ]);
967
- const routeServerImports = parsed.serverImports.length > 0
968
- ? parsed.serverImports
969
- : filterClientImportsForServer(parsed.clientImports, neededServerFns);
1113
+ const routeServerImports = routeServerImportEntries.length > 0
1114
+ ? routeServerImportEntries
1115
+ : routeClientImportEntries.filter((entry) => filterClientImportsForServer([entry.line], neededServerFns).length > 0);
970
1116
  if (routeServerImports.length > 0) {
971
- const routeFileDir = path.dirname(fullPath);
972
- for (const imp of routeServerImports) {
1117
+ for (const entry of routeServerImports) {
1118
+ const imp = entry.line;
973
1119
  const pathMatch = imp.match(/from\s+['"]([^'"]+)['"]/);
974
1120
  if (!pathMatch)
975
1121
  continue;
976
1122
  const origPath = pathMatch[1];
977
- // Bare module specifiers (packages) â€" pass through as-is
978
- const importPath = resolveCompiledImportPath(origPath, routeFileDir, outFileDir);
1123
+ const importPath = resolveCompiledImportPath(origPath, entry.importerDir, outFileDir);
979
1124
  const moduleId = `__m${moduleCounter++}`;
980
1125
  allImports.push(`import * as ${moduleId} from '${importPath}';`);
981
- // Extract named imports and map them to this module
982
- const names = parseNamedImportBindings(imp);
983
- if (names.length > 0) {
984
- for (const name of names) {
985
- fnToModule[name] = moduleId;
1126
+ const namedBindings = parseNamedImportBindings(imp);
1127
+ if (namedBindings.length > 0) {
1128
+ for (const binding of namedBindings) {
1129
+ fnToModule[binding.local] = moduleId;
1130
+ if (routeScriptReferenceSource.includes(binding.local) && !routeImportDeclMap.has(binding.local)) {
1131
+ routeImportDeclMap.set(binding.local, `const ${binding.local} = ${moduleId}.${binding.imported};`);
1132
+ }
986
1133
  }
987
1134
  }
988
- // Handle: import * as X from '...'
989
1135
  const starMatch = imp.match(/import\s*\*\s*as\s+(\w+)/);
990
1136
  if (starMatch) {
991
1137
  fnToModule[starMatch[1]] = moduleId;
1138
+ if (routeScriptReferenceSource.includes(starMatch[1]) && !routeImportDeclMap.has(starMatch[1])) {
1139
+ routeImportDeclMap.set(starMatch[1], `const ${starMatch[1]} = ${moduleId};`);
1140
+ }
992
1141
  }
993
1142
  }
994
1143
  }
995
- // Build per-route component names from explicit imports
996
- // componentImports: { StatCard: 'stat-card' } â†' componentNames maps PascalCase â†' fileName
1144
+ routeImportDecls.push(...routeImportDeclMap.values());
997
1145
  const routeComponentNames = new Map();
998
- for (const [pascalName, fileName] of Object.entries(parsed.componentImports)) {
999
- // Compile the component on first use
1146
+ for (const [pascalName, fileName] of Object.entries(mergedComponentImports)) {
1000
1147
  compileComponent(fileName);
1001
1148
  routeComponentNames.set(pascalName, fileName);
1002
1149
  }
1003
- // Discover action functions passed as props to components.
1004
- // A component like db-studio uses action={runQueryAction} where runQueryAction is a prop.
1005
- // When the route passes runQueryAction={runAdminSqlQuery}, we need runAdminSqlQuery in
1006
- // the route's actions map so the runtime can dispatch it.
1007
- // Strategy: for each component, we know which prop names are action props (from cache).
1008
- // We then scan the route template for that component's usage and extract the bound values.
1009
1150
  for (const [pascalName, compFileName] of routeComponentNames.entries()) {
1010
1151
  const actionPropNames = componentActionCache.get(compFileName);
1011
- // Find all usages of <PascalName ...> in the route template and extract prop bindings.
1012
- // Match <ComponentName ... propName={value} ... > across multiple lines.
1013
- const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)/>`, 'g');
1014
- for (const tagMatch of parsed.template.matchAll(compTagRegex)) {
1152
+ const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)(?:/?)>`, 'g');
1153
+ for (const tagMatch of effectiveTemplate.matchAll(compTagRegex)) {
1015
1154
  const attrs = tagMatch[1];
1016
1155
  if (actionPropNames && actionPropNames.size > 0) {
1017
1156
  for (const propName of actionPropNames) {
1018
- // Find propName={identifier} binding
1019
1157
  const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
1020
1158
  const propMatch = attrs.match(propRegex);
1021
1159
  if (propMatch) {
1022
1160
  const routeFnName = propMatch[1];
1023
- // Only add if this function is actually imported by the route
1024
- if (routeFnName in fnToModule && !parsed.actionFunctions.includes(routeFnName)) {
1025
- parsed.actionFunctions.push(routeFnName);
1161
+ if (routeFnName in fnToModule && !mergedActionFunctions.includes(routeFnName)) {
1162
+ mergedActionFunctions.push(routeFnName);
1026
1163
  }
1027
1164
  }
1028
1165
  }
1029
1166
  }
1030
1167
  }
1031
1168
  }
1032
- // Compile template to render function body (pass component names and action names)
1033
- // An identifier is a valid server action if it is either:
1034
- // 1. Directly imported (present in fnToModule), or
1035
- // 2. A top-level script declaration (present in dataVars) â€" covers cases like
1036
- // `const fn = importedFn` or `async function fn() {}` where the binding
1037
- // is locally declared but delegates to an imported function.
1038
- const dataVarsSet = new Set(parsed.dataVars);
1039
- const actionNames = new Set(parsed.actionFunctions.filter(fn => fn in fnToModule || dataVarsSet.has(fn)));
1040
- // Opaque per-route RPC IDs keep implementation details out of rendered HTML.
1169
+ const dataVarsSet = new Set(mergedDataVars);
1170
+ const actionNames = new Set(mergedActionFunctions.filter(fn => fn in fnToModule || dataVarsSet.has(fn)));
1041
1171
  const rpcNameMap = new Map();
1042
1172
  let rpcCounter = 0;
1043
- for (const fnName of parsed.pollFunctions) {
1173
+ for (const fnName of mergedPollFunctions) {
1044
1174
  if (!rpcNameMap.has(fnName)) {
1045
1175
  rpcNameMap.set(fnName, `rpc_${i}_${rpcCounter++}`);
1046
1176
  }
1047
1177
  }
1048
- for (const q of parsed.dataGetQueries) {
1178
+ for (const q of mergedDataGetQueries) {
1049
1179
  if (!rpcNameMap.has(q.fnName)) {
1050
1180
  rpcNameMap.set(q.fnName, `rpc_${i}_${rpcCounter++}`);
1051
1181
  }
1052
1182
  q.rpcId = rpcNameMap.get(q.fnName);
1053
1183
  }
1054
- // Apply nested route layouts (excluding root layout.html which is handled globally)
1055
- let effectiveTemplate = parsed.template;
1056
- for (const layoutRelPath of rf.layouts) {
1057
- if (layoutRelPath === 'layout.html')
1058
- continue;
1059
- const layoutPath = path.join(routesDir, layoutRelPath);
1060
- if (!fs.existsSync(layoutPath))
1061
- continue;
1062
- const layoutSource = fs.readFileSync(layoutPath, 'utf-8');
1063
- const layoutSlot = layoutSource.match(/<slot\s*><\/slot>|<slot\s*\/>/);
1064
- if (!layoutSlot) {
1065
- throw new Error(`${layoutRelPath} must contain <slot></slot> or <slot />`);
1066
- }
1067
- effectiveTemplate = layoutSource.replace(layoutSlot[0], effectiveTemplate);
1068
- }
1069
1184
  const renderBody = compileTemplate(effectiveTemplate, routeComponentNames, actionNames, rpcNameMap);
1070
1185
  // Collect component CSS for this route (compile-time dedup)
1071
1186
  const routeComponentStyles = [];
@@ -1080,7 +1195,7 @@ export function compile(options) {
1080
1195
  pattern,
1081
1196
  renderBody,
1082
1197
  isDev: !!options.isDev,
1083
- parsed,
1198
+ parsed: mergedParsed,
1084
1199
  fnToModule,
1085
1200
  rpcNameMap,
1086
1201
  componentStyles: routeComponentStyles,
@@ -1147,12 +1262,12 @@ export function compile(options) {
1147
1262
  fs.mkdirSync(outDir, { recursive: true });
1148
1263
  }
1149
1264
  writeIfChanged(outFile, output);
1150
- // Generate .kuratchi/worker.js the stable wrangler entry point.
1265
+ // Generate .kuratchi/worker.js the stable wrangler entry point.
1151
1266
  // routes.js already exports the default fetch handler and all named DO classes;
1152
1267
  // worker.js explicitly re-exports them so wrangler.jsonc can reference a
1153
1268
  // stable filename while routes.js is freely regenerated.
1154
1269
  const workerFile = path.join(outDir, 'worker.js');
1155
- const workerClassExports = [...containerConfig, ...workflowConfig]
1270
+ const workerClassExports = [...agentConfig, ...containerConfig, ...workflowConfig]
1156
1271
  .map((entry) => {
1157
1272
  const importPath = toWorkerImportPath(projectDir, outDir, entry.file);
1158
1273
  if (entry.exportKind === 'default') {
@@ -1170,7 +1285,7 @@ export function compile(options) {
1170
1285
  writeIfChanged(workerFile, workerLines.join('\n'));
1171
1286
  return workerFile;
1172
1287
  }
1173
- // â"€â" Helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
1288
+ // "��" Helpers "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
1174
1289
  /**
1175
1290
  * Write a file only if its content has changed.
1176
1291
  * Prevents unnecessary filesystem events that would retrigger wrangler's file watcher.
@@ -1246,10 +1361,10 @@ function readUiTheme(projectDir) {
1246
1361
  const uiBlock = readConfigBlock(source, 'ui');
1247
1362
  if (!uiBlock)
1248
1363
  return null;
1249
- // Adapter form defaults to "default" theme when ui config is present.
1364
+ // Adapter form defaults to the bundled Kuratchi UI theme when ui config is present.
1250
1365
  const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
1251
1366
  const themeValue = themeMatch?.[1] ?? 'default';
1252
- if (themeValue === 'default') {
1367
+ if (themeValue === 'default' || themeValue === 'dark' || themeValue === 'light' || themeValue === 'system') {
1253
1368
  // Resolve @kuratchi/ui/src/styles/theme.css from package
1254
1369
  const candidates = [
1255
1370
  path.join(projectDir, 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
@@ -1261,10 +1376,10 @@ function readUiTheme(projectDir) {
1261
1376
  return fs.readFileSync(candidate, 'utf-8');
1262
1377
  }
1263
1378
  }
1264
- console.warn('[kuratchi] ui.theme: "default" configured but @kuratchi/ui theme.css not found');
1379
+ console.warn(`[kuratchi] ui.theme: "${themeValue}" configured but @kuratchi/ui theme.css not found`);
1265
1380
  return null;
1266
1381
  }
1267
- // Custom path â€" resolve relative to project root
1382
+ // Custom path " resolve relative to project root
1268
1383
  const customPath = path.resolve(projectDir, themeValue);
1269
1384
  if (fs.existsSync(customPath)) {
1270
1385
  return fs.readFileSync(customPath, 'utf-8');
@@ -1272,6 +1387,74 @@ function readUiTheme(projectDir) {
1272
1387
  console.warn(`[kuratchi] ui.theme: "${themeValue}" not found at ${customPath}`);
1273
1388
  return null;
1274
1389
  }
1390
+ /**
1391
+ * Read ui.theme and ui.radius config values from kuratchi.config.ts.
1392
+ * Returns null if no ui block is present.
1393
+ */
1394
+ function readUiConfigValues(projectDir) {
1395
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
1396
+ if (!fs.existsSync(configPath))
1397
+ return null;
1398
+ const source = fs.readFileSync(configPath, 'utf-8');
1399
+ const uiBlock = readConfigBlock(source, 'ui');
1400
+ if (!uiBlock)
1401
+ return null;
1402
+ const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
1403
+ const radiusMatch = uiBlock.body.match(/radius\s*:\s*['"]([^'"]+)['"]/);
1404
+ return {
1405
+ theme: themeMatch?.[1] ?? 'dark',
1406
+ radius: radiusMatch?.[1] ?? 'default',
1407
+ };
1408
+ }
1409
+ /**
1410
+ * Patch the opening <html> tag in a layout source string to reflect ui config.
1411
+ * theme='dark' ? ensures class="dark" is present, removes data-theme.
1412
+ * theme='light' ? ensures class="dark" is absent, removes data-theme.
1413
+ * theme='system' ? removes class="dark", sets data-theme="system".
1414
+ * radius='none'|'full' ? sets data-radius; radius='default' ? removes it.
1415
+ */
1416
+ function patchHtmlTag(source, theme, radius) {
1417
+ return source.replace(/(<html\b)([^>]*)(>)/i, (_m, open, attrs, close) => {
1418
+ if (theme === 'dark') {
1419
+ if (/\bclass\s*=\s*"([^"]*)"/i.test(attrs)) {
1420
+ attrs = attrs.replace(/class\s*=\s*"([^"]*)"/i, (_mc, cls) => {
1421
+ const classes = cls.split(/\s+/).filter(Boolean);
1422
+ if (!classes.includes('dark'))
1423
+ classes.unshift('dark');
1424
+ return `class="${classes.join(' ')}"`;
1425
+ });
1426
+ }
1427
+ else {
1428
+ attrs += ' class="dark"';
1429
+ }
1430
+ attrs = attrs.replace(/\s*data-theme\s*=\s*"[^"]*"/i, '');
1431
+ }
1432
+ else if (theme === 'light') {
1433
+ attrs = attrs.replace(/class\s*=\s*"([^"]*)"/i, (_mc, cls) => {
1434
+ const classes = cls.split(/\s+/).filter(Boolean).filter((c) => c !== 'dark');
1435
+ return classes.length ? `class="${classes.join(' ')}"` : '';
1436
+ });
1437
+ attrs = attrs.replace(/\s*data-theme\s*=\s*"[^"]*"/i, '');
1438
+ }
1439
+ else if (theme === 'system') {
1440
+ attrs = attrs.replace(/class\s*=\s*"([^"]*)"/i, (_mc, cls) => {
1441
+ const classes = cls.split(/\s+/).filter(Boolean).filter((c) => c !== 'dark');
1442
+ return classes.length ? `class="${classes.join(' ')}"` : '';
1443
+ });
1444
+ if (/data-theme\s*=/i.test(attrs)) {
1445
+ attrs = attrs.replace(/data-theme\s*=\s*"[^"]*"/i, 'data-theme="system"');
1446
+ }
1447
+ else {
1448
+ attrs += ' data-theme="system"';
1449
+ }
1450
+ }
1451
+ attrs = attrs.replace(/\s*data-radius\s*=\s*"[^"]*"/i, '');
1452
+ if (radius === 'none' || radius === 'full') {
1453
+ attrs += ` data-radius="${radius}"`;
1454
+ }
1455
+ return open + attrs + close;
1456
+ });
1457
+ }
1275
1458
  /**
1276
1459
  * Resolve a component .html file from a package (e.g. @kuratchi/ui).
1277
1460
  * Searches: node_modules, then workspace siblings (../../packages/).
@@ -1282,7 +1465,7 @@ function resolvePackageComponent(projectDir, pkgName, componentFile) {
1282
1465
  if (fs.existsSync(nmPath))
1283
1466
  return nmPath;
1284
1467
  // 2. Try workspace layout: project is in apps/X or packages/X, sibling packages in packages/
1285
- // @kuratchi/ui â†' kuratchi-ui (convention: scope stripped, slash â†' dash)
1468
+ // @kuratchi/ui ' kuratchi-ui (convention: scope stripped, slash ' dash)
1286
1469
  const pkgDirName = pkgName.replace(/^@/, '').replace(/\//g, '-');
1287
1470
  const workspaceRoot = path.resolve(projectDir, '../..');
1288
1471
  const wsPath = path.join(workspaceRoot, 'packages', pkgDirName, 'src', 'lib', componentFile + '.html');
@@ -1320,7 +1503,7 @@ function discoverRoutes(routesDir) {
1320
1503
  for (const entry of entries) {
1321
1504
  if (entry.isDirectory()) {
1322
1505
  const childPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
1323
- // Folder-based page route: routes/db/page.html /db
1506
+ // Folder-based page route: routes/db/page.html ? /db
1324
1507
  const pageFile = path.join(dir, entry.name, 'page.html');
1325
1508
  if (fs.existsSync(pageFile)) {
1326
1509
  const routeFile = `${childPrefix}/page.html`;
@@ -1342,7 +1525,7 @@ function discoverRoutes(routesDir) {
1342
1525
  walk(path.join(dir, entry.name), childPrefix);
1343
1526
  }
1344
1527
  else if (entry.name === 'layout.html' || entry.name === '404.html' || entry.name === '500.html') {
1345
- // Skip layout.html is the app layout, 404/500 are error pages, not routes
1528
+ // Skip layout.html is the app layout, 404/500 are error pages, not routes
1346
1529
  continue;
1347
1530
  }
1348
1531
  else if (entry.name === 'index.ts' || entry.name === 'index.js') {
@@ -1354,7 +1537,7 @@ function discoverRoutes(routesDir) {
1354
1537
  }
1355
1538
  }
1356
1539
  else if (entry.name === 'page.html') {
1357
- // page.html in current directory index route for this prefix
1540
+ // page.html in current directory ? index route for this prefix
1358
1541
  const routeFile = prefix ? `${prefix}/page.html` : 'page.html';
1359
1542
  if (!registered.has(routeFile)) {
1360
1543
  registered.add(routeFile);
@@ -1362,7 +1545,7 @@ function discoverRoutes(routesDir) {
1362
1545
  }
1363
1546
  }
1364
1547
  else if (entry.name.endsWith('.html') && entry.name !== 'page.html') {
1365
- // File-based route: routes/about.html /about (fallback)
1548
+ // File-based route: routes/about.html ? /about (fallback)
1366
1549
  const name = prefix
1367
1550
  ? `${prefix}/${entry.name.replace('.html', '')}`
1368
1551
  : entry.name.replace('.html', '');
@@ -1384,19 +1567,79 @@ function discoverRoutes(routesDir) {
1384
1567
  });
1385
1568
  return results;
1386
1569
  }
1570
+ function buildSegmentedScriptBody(opts) {
1571
+ const { segments, fnToModule, importDecls, workerEnvAliases, devAliases, isDev, asyncMode } = opts;
1572
+ const lines = [];
1573
+ const routeDevDecls = buildDevAliasDeclarations(devAliases, isDev);
1574
+ if (routeDevDecls)
1575
+ lines.push(routeDevDecls);
1576
+ if (importDecls)
1577
+ lines.push(importDecls);
1578
+ lines.push('const __segmentData: Record<string, any> = {};');
1579
+ const availableVars = [];
1580
+ let segmentIndex = 0;
1581
+ for (const segment of segments) {
1582
+ if (!segment.script)
1583
+ continue;
1584
+ let segmentBody = stripTopLevelImports(segment.script);
1585
+ segmentBody = rewriteImportedFunctionCalls(segmentBody, fnToModule);
1586
+ segmentBody = rewriteWorkerEnvAliases(segmentBody, workerEnvAliases);
1587
+ if (!segmentBody.trim())
1588
+ continue;
1589
+ const returnVars = segment.dataVars.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
1590
+ const segmentVar = '__segment_' + segmentIndex++;
1591
+ const invokePrefix = asyncMode ? 'await ' : '';
1592
+ const factoryPrefix = asyncMode ? 'async ' : '';
1593
+ lines.push('const ' + segmentVar + ' = ' + invokePrefix + '(' + factoryPrefix + '(__ctx: Record<string, any>) => {');
1594
+ lines.push(segmentBody);
1595
+ lines.push(returnVars.length > 0 ? 'return { ' + returnVars.join(', ') + ' };' : 'return {};');
1596
+ lines.push('})(__segmentData);');
1597
+ lines.push('Object.assign(__segmentData, ' + segmentVar + ');');
1598
+ for (const name of returnVars) {
1599
+ if (!availableVars.includes(name))
1600
+ availableVars.push(name);
1601
+ }
1602
+ }
1603
+ if (!asyncMode && availableVars.length > 0) {
1604
+ lines.push('const { ' + availableVars.join(', ') + ' } = __segmentData;');
1605
+ }
1606
+ return lines.join('\n');
1607
+ }
1387
1608
  function buildRouteObject(opts) {
1388
1609
  const { pattern, renderBody, isDev, parsed, fnToModule, rpcNameMap, componentStyles } = opts;
1389
1610
  const hasFns = Object.keys(fnToModule).length > 0;
1390
1611
  const parts = [];
1391
1612
  parts.push(` pattern: '${pattern}'`);
1392
1613
  const queryVars = parsed.dataGetQueries?.map((q) => q.asName) ?? [];
1393
- let scriptBody = parsed.script
1394
- ? parsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim()
1395
- : '';
1614
+ const scriptSegments = (parsed.scriptSegments ?? [])
1615
+ .filter((segment) => !!segment.script);
1616
+ const hasSegmentedScripts = scriptSegments.length > 1;
1396
1617
  const routeDevDecls = buildDevAliasDeclarations(parsed.devAliases, isDev);
1397
- scriptBody = [routeDevDecls, scriptBody].filter(Boolean).join('\n');
1398
- scriptBody = rewriteImportedFunctionCalls(scriptBody, fnToModule);
1399
- scriptBody = rewriteWorkerEnvAliases(scriptBody, parsed.workerEnvAliases);
1618
+ const routeImportDecls = (parsed.scriptImportDecls ?? []).join('\n');
1619
+ let scriptBody = '';
1620
+ let scriptUsesAwait = false;
1621
+ if (hasSegmentedScripts) {
1622
+ const combinedScript = scriptSegments.map((segment) => stripTopLevelImports(segment.script)).join('\n');
1623
+ scriptUsesAwait = /\bawait\b/.test(combinedScript);
1624
+ scriptBody = buildSegmentedScriptBody({
1625
+ segments: scriptSegments,
1626
+ fnToModule,
1627
+ importDecls: routeImportDecls,
1628
+ workerEnvAliases: parsed.workerEnvAliases,
1629
+ devAliases: parsed.devAliases,
1630
+ isDev,
1631
+ asyncMode: scriptUsesAwait,
1632
+ });
1633
+ }
1634
+ else {
1635
+ scriptBody = parsed.script
1636
+ ? stripTopLevelImports(parsed.script)
1637
+ : '';
1638
+ scriptBody = [routeDevDecls, routeImportDecls, scriptBody].filter(Boolean).join('\n');
1639
+ scriptBody = rewriteImportedFunctionCalls(scriptBody, fnToModule);
1640
+ scriptBody = rewriteWorkerEnvAliases(scriptBody, parsed.workerEnvAliases);
1641
+ scriptUsesAwait = /\bawait\b/.test(scriptBody);
1642
+ }
1400
1643
  let explicitLoadFunction = parsed.loadFunction
1401
1644
  ? parsed.loadFunction.replace(/^export\s+/, '').trim()
1402
1645
  : '';
@@ -1404,8 +1647,10 @@ function buildRouteObject(opts) {
1404
1647
  explicitLoadFunction = [routeDevDecls, explicitLoadFunction].filter(Boolean).join('\n');
1405
1648
  explicitLoadFunction = rewriteImportedFunctionCalls(explicitLoadFunction, fnToModule);
1406
1649
  explicitLoadFunction = rewriteWorkerEnvAliases(explicitLoadFunction, parsed.workerEnvAliases);
1650
+ if (routeImportDecls)
1651
+ explicitLoadFunction = explicitLoadFunction.replace('{', `{\n${routeImportDecls}\n`);
1407
1652
  }
1408
- if (explicitLoadFunction && /\bawait\b/.test(scriptBody)) {
1653
+ if (explicitLoadFunction && scriptUsesAwait) {
1409
1654
  throw new Error(`[kuratchi compiler] ${pattern}\nTop-level await cannot be mixed with export async function load(). Move async server work into load().`);
1410
1655
  }
1411
1656
  if (scriptBody) {
@@ -1414,11 +1659,10 @@ function buildRouteObject(opts) {
1414
1659
  if (explicitLoadFunction) {
1415
1660
  explicitLoadFunction = transpileTypeScript(explicitLoadFunction, `route-load:${pattern}.ts`);
1416
1661
  }
1417
- const scriptUsesAwait = /\bawait\b/.test(scriptBody);
1418
1662
  const scriptReturnVars = parsed.script
1419
1663
  ? parsed.dataVars.filter((v) => !queryVars.includes(v))
1420
1664
  : [];
1421
- // Load function â€" internal server prepass for async route script bodies
1665
+ // Load function " internal server prepass for async route script bodies
1422
1666
  // and data-get query state hydration.
1423
1667
  const hasDataGetQueries = Array.isArray(parsed.dataGetQueries) && parsed.dataGetQueries.length > 0;
1424
1668
  if (explicitLoadFunction) {
@@ -1441,13 +1685,15 @@ function buildRouteObject(opts) {
1441
1685
  const argsExpr = (q.argsExpr || '').trim();
1442
1686
  const asName = q.asName;
1443
1687
  const defaultArgs = argsExpr ? `[${argsExpr}]` : '[]';
1688
+ const moduleId = fnToModule[fnName];
1689
+ const qualifiedFn = moduleId ? `${moduleId}.${fnName}` : fnName;
1444
1690
  queryLines.push(`let ${asName} = { state: 'loading', loading: true, error: null, data: null, empty: false, success: false };`);
1445
1691
  queryLines.push(`const __qOverride_${asName} = __getLocals().__queryOverride;`);
1446
1692
  queryLines.push(`const __qArgs_${asName} = ${defaultArgs};`);
1447
1693
  queryLines.push(`const __qShouldRun_${asName} = !!(__qOverride_${asName} && __qOverride_${asName}.fn === '${rpcId}' && Array.isArray(__qOverride_${asName}.args) && JSON.stringify(__qOverride_${asName}.args) === JSON.stringify(__qArgs_${asName}));`);
1448
1694
  queryLines.push(`if (__qShouldRun_${asName}) {`);
1449
1695
  queryLines.push(` try {`);
1450
- queryLines.push(` const __qData_${asName} = await ${fnName}(...__qArgs_${asName});`);
1696
+ queryLines.push(` const __qData_${asName} = await ${qualifiedFn}(...__qArgs_${asName});`);
1451
1697
  queryLines.push(` const __qEmpty_${asName} = Array.isArray(__qData_${asName}) ? __qData_${asName}.length === 0 : (__qData_${asName} == null);`);
1452
1698
  queryLines.push(` ${asName} = { state: __qEmpty_${asName} ? 'empty' : 'success', loading: false, error: null, data: __qData_${asName}, empty: __qEmpty_${asName}, success: !__qEmpty_${asName} };`);
1453
1699
  queryLines.push(` } catch (err) {`);
@@ -1459,10 +1705,26 @@ function buildRouteObject(opts) {
1459
1705
  loadBody = [loadBody, queryLines.join('\n')].filter(Boolean).join('\n');
1460
1706
  }
1461
1707
  const loadReturnVars = [...scriptReturnVars, ...queryVars];
1462
- const returnObj = loadReturnVars.length > 0 ? `\n return { ${loadReturnVars.join(', ')} };` : '';
1463
- parts.push(` async load(params = {}) {\n ${loadBody}${returnObj}\n }`);
1708
+ let returnObj = '';
1709
+ if (loadReturnVars.length > 0) {
1710
+ if (hasSegmentedScripts && scriptUsesAwait) {
1711
+ const segmentReturnEntries = scriptReturnVars.map((name) => name + ': __segmentData.' + name);
1712
+ const queryReturnEntries = queryVars
1713
+ .filter((name) => !scriptReturnVars.includes(name))
1714
+ .map((name) => name);
1715
+ returnObj = `
1716
+ return { ${[...segmentReturnEntries, ...queryReturnEntries].join(', ')} };`;
1717
+ }
1718
+ else {
1719
+ returnObj = `
1720
+ return { ${loadReturnVars.join(', ')} };`;
1721
+ }
1722
+ }
1723
+ parts.push(` async load(params = {}) {
1724
+ ${loadBody}${returnObj}
1725
+ }`);
1464
1726
  }
1465
- // Actions â€" functions referenced via action={fn} in the template
1727
+ // Actions " functions referenced via action={fn} in the template
1466
1728
  if (hasFns && parsed.actionFunctions.length > 0) {
1467
1729
  const actionEntries = parsed.actionFunctions
1468
1730
  .map(fn => {
@@ -1472,7 +1734,7 @@ function buildRouteObject(opts) {
1472
1734
  .join(', ');
1473
1735
  parts.push(` actions: { ${actionEntries} }`);
1474
1736
  }
1475
- // RPC â€" functions referenced via data-poll={fn(args)} in the template
1737
+ // RPC " functions referenced via data-poll={fn(args)} in the template
1476
1738
  if (hasFns && parsed.pollFunctions.length > 0) {
1477
1739
  const rpcEntries = parsed.pollFunctions
1478
1740
  .map(fn => {
@@ -1483,7 +1745,7 @@ function buildRouteObject(opts) {
1483
1745
  .join(', ');
1484
1746
  parts.push(` rpc: { ${rpcEntries} }`);
1485
1747
  }
1486
- // Render function â€" template compiled to JS with native flow control
1748
+ // Render function " template compiled to JS with native flow control
1487
1749
  // Destructure data vars so templates reference them directly (e.g., {todos} not {data.todos})
1488
1750
  // Auto-inject action state objects so templates can reference signIn.error, signIn.loading, etc.
1489
1751
  const renderPrelude = (scriptBody && !scriptUsesAwait) ? scriptBody : '';
@@ -1511,9 +1773,9 @@ function buildRouteObject(opts) {
1511
1773
  const styleLines = componentStyles.map(css => `__html += \`${css}\\n\`;`);
1512
1774
  finalRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
1513
1775
  }
1514
- parts.push(` render(data) {
1515
- ${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
1516
- return __html;
1776
+ parts.push(` render(data) {
1777
+ ${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
1778
+ return __html;
1517
1779
  }`);
1518
1780
  return ` {\n${parts.join(',\n')}\n }`;
1519
1781
  }
@@ -1526,7 +1788,7 @@ function readOrmConfig(projectDir) {
1526
1788
  if (!ormBlock)
1527
1789
  return [];
1528
1790
  // Extract schema imports: import { todoSchema } from './src/schemas/todo';
1529
- const importMap = new Map(); // exportName â†' importPath
1791
+ const importMap = new Map(); // exportName ' importPath
1530
1792
  const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
1531
1793
  let m;
1532
1794
  while ((m = importRegex.exec(source)) !== null) {
@@ -1663,7 +1925,7 @@ function readDoConfig(projectDir) {
1663
1925
  if (list.length > 0)
1664
1926
  entry.files = list;
1665
1927
  }
1666
- // (inject config removed â€" DO methods are org-scoped, no auto-injection needed)
1928
+ // (inject config removed " DO methods are org-scoped, no auto-injection needed)
1667
1929
  entries.push(entry);
1668
1930
  }
1669
1931
  // Match string shorthand: BINDING: 'ClassName' (skip bindings already found)
@@ -1762,6 +2024,35 @@ function readWorkerClassConfig(projectDir, key) {
1762
2024
  }
1763
2025
  return entries;
1764
2026
  }
2027
+ function resolveClassExportFromFile(absPath, errorLabel) {
2028
+ if (!fs.existsSync(absPath)) {
2029
+ throw new Error(`[kuratchi] ${errorLabel} file not found: ${absPath}`);
2030
+ }
2031
+ const fileSource = fs.readFileSync(absPath, 'utf-8');
2032
+ const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
2033
+ if (defaultClass) {
2034
+ return { className: defaultClass[1], exportKind: 'default' };
2035
+ }
2036
+ const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
2037
+ if (namedClass) {
2038
+ return { className: namedClass[1], exportKind: 'named' };
2039
+ }
2040
+ throw new Error(`[kuratchi] ${errorLabel} must export a class via "export class X" or "export default class X". File: ${absPath}`);
2041
+ }
2042
+ function discoverConventionClassFiles(projectDir, dir, suffix, errorLabel) {
2043
+ const absDir = path.join(projectDir, dir);
2044
+ const files = discoverFilesWithSuffix(absDir, suffix);
2045
+ if (files.length === 0)
2046
+ return [];
2047
+ return files.map((absPath) => {
2048
+ const resolved = resolveClassExportFromFile(absPath, errorLabel);
2049
+ return {
2050
+ className: resolved.className,
2051
+ file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
2052
+ exportKind: resolved.exportKind,
2053
+ };
2054
+ });
2055
+ }
1765
2056
  function discoverFilesWithSuffix(dir, suffix) {
1766
2057
  if (!fs.existsSync(dir))
1767
2058
  return [];
@@ -1999,7 +2290,7 @@ function generateHandlerProxy(handler, projectDir) {
1999
2290
  ? methods.filter((m) => m.isAsync).map((m) => m.name)
2000
2291
  : [];
2001
2292
  const lines = [
2002
- `// Auto-generated by KuratchiJS compiler â€" do not edit.`,
2293
+ `// Auto-generated by KuratchiJS compiler " do not edit.`,
2003
2294
  `import { __getDoStub } from '${RUNTIME_DO_IMPORT}';`,
2004
2295
  ...(handler.mode === 'class' ? [`import ${handlerLocal} from '${origRelPath}';`] : []),
2005
2296
  ``,
@@ -2093,34 +2384,34 @@ function generateRoutesModule(opts) {
2093
2384
  const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
2094
2385
  ? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
2095
2386
  : '';
2096
- // Auth session init â€" thin cookie parsing injected into Worker entry
2387
+ // Auth session init " thin cookie parsing injected into Worker entry
2097
2388
  let authInit = '';
2098
2389
  if (opts.authConfig && opts.authConfig.sessionEnabled) {
2099
2390
  const cookieName = opts.authConfig.cookieName;
2100
- authInit = `
2101
- // â"€â" Auth Session Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2102
-
2103
- function __parseCookies(header) {
2104
- const map = {};
2105
- if (!header) return map;
2106
- for (const pair of header.split(';')) {
2107
- const eq = pair.indexOf('=');
2108
- if (eq === -1) continue;
2109
- map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
2110
- }
2111
- return map;
2112
- }
2113
-
2114
- function __initAuth(request) {
2115
- const cookies = __parseCookies(request.headers.get('cookie'));
2116
- __setLocal('session', null);
2117
- __setLocal('user', null);
2118
- __setLocal('auth', {
2119
- cookies,
2120
- sessionCookie: cookies['${cookieName}'] || null,
2121
- cookieName: '${cookieName}',
2122
- });
2123
- }
2391
+ authInit = `
2392
+ // "��" Auth Session Init "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2393
+
2394
+ function __parseCookies(header) {
2395
+ const map = {};
2396
+ if (!header) return map;
2397
+ for (const pair of header.split(';')) {
2398
+ const eq = pair.indexOf('=');
2399
+ if (eq === -1) continue;
2400
+ map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
2401
+ }
2402
+ return map;
2403
+ }
2404
+
2405
+ function __initAuth(request) {
2406
+ const cookies = __parseCookies(request.headers.get('cookie'));
2407
+ __setLocal('session', null);
2408
+ __setLocal('user', null);
2409
+ __setLocal('auth', {
2410
+ cookies,
2411
+ sessionCookie: cookies['${cookieName}'] || null,
2412
+ cookieName: '${cookieName}',
2413
+ });
2414
+ }
2124
2415
  `;
2125
2416
  }
2126
2417
  const workerImport = `import { WorkerEntrypoint, env as __env } from 'cloudflare:workers';`;
@@ -2148,42 +2439,42 @@ function __initAuth(request) {
2148
2439
  `import { kuratchiORM } from '@kuratchi/orm';`,
2149
2440
  ...schemaImports,
2150
2441
  ].join('\n');
2151
- migrationInit = `
2152
- // â"€â" ORM Auto-Migration â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2153
-
2154
- let __migrated = false;
2155
- const __ormDatabases = [
2156
- ${migrateEntries.join(',\n')}
2157
- ];
2158
-
2159
- async function __runMigrations() {
2160
- if (__migrated) return;
2161
- __migrated = true;
2162
- for (const db of __ormDatabases) {
2163
- const binding = __env[db.binding];
2164
- if (!binding) continue;
2165
- try {
2166
- const executor = (sql, params) => {
2167
- let stmt = binding.prepare(sql);
2168
- if (params?.length) stmt = stmt.bind(...params);
2169
- return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
2170
- };
2171
- const result = await runMigrations({ execute: executor, schema: db.schema });
2172
- if (result.applied) {
2173
- console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
2174
- }
2175
- if (result.warnings.length) {
2176
- result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
2177
- }
2178
- } catch (err) {
2179
- console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
2180
- }
2181
- }
2182
- }
2442
+ migrationInit = `
2443
+ // "��" ORM Auto-Migration "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2444
+
2445
+ let __migrated = false;
2446
+ const __ormDatabases = [
2447
+ ${migrateEntries.join(',\n')}
2448
+ ];
2449
+
2450
+ async function __runMigrations() {
2451
+ if (__migrated) return;
2452
+ __migrated = true;
2453
+ for (const db of __ormDatabases) {
2454
+ const binding = __env[db.binding];
2455
+ if (!binding) continue;
2456
+ try {
2457
+ const executor = (sql, params) => {
2458
+ let stmt = binding.prepare(sql);
2459
+ if (params?.length) stmt = stmt.bind(...params);
2460
+ return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
2461
+ };
2462
+ const result = await runMigrations({ execute: executor, schema: db.schema });
2463
+ if (result.applied) {
2464
+ console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
2465
+ }
2466
+ if (result.warnings.length) {
2467
+ result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
2468
+ }
2469
+ } catch (err) {
2470
+ console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
2471
+ }
2472
+ }
2473
+ }
2183
2474
  `;
2184
2475
  }
2185
2476
  }
2186
- // Auth plugin init â€" import config + call @kuratchi/auth setup functions
2477
+ // Auth plugin init " import config + call @kuratchi/auth setup functions
2187
2478
  let authPluginImports = '';
2188
2479
  let authPluginInit = '';
2189
2480
  const ac = opts.authConfig;
@@ -2234,15 +2525,15 @@ async function __runMigrations() {
2234
2525
  initLines.push(` if (__kuratchiConfig.auth?.organizations) __configOrg(__kuratchiConfig.auth.organizations);`);
2235
2526
  }
2236
2527
  authPluginImports = imports.join('\n');
2237
- authPluginInit = `
2238
- // â"€â" Auth Plugin Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2239
-
2240
- function __initAuthPlugins() {
2241
- ${initLines.join('\n')}
2242
- }
2528
+ authPluginInit = `
2529
+ // "��" Auth Plugin Init "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2530
+
2531
+ function __initAuthPlugins() {
2532
+ ${initLines.join('\n')}
2533
+ }
2243
2534
  `;
2244
2535
  }
2245
- // â"€â" Durable Object class generation â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2536
+ // "��" Durable Object class generation "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2246
2537
  let doImports = '';
2247
2538
  let doClassCode = '';
2248
2539
  let doResolverInit = '';
@@ -2400,7 +2691,7 @@ ${initLines.join('\n')}
2400
2691
  }
2401
2692
  // Register stub resolver
2402
2693
  if (doEntry.stubId) {
2403
- // Config-driven: e.g. stubId: 'user.orgId' â†' __u.orgId
2694
+ // Config-driven: e.g. stubId: 'user.orgId' ' __u.orgId
2404
2695
  const fieldPath = doEntry.stubId.startsWith('user.') ? `__u.${doEntry.stubId.slice(5)}` : doEntry.stubId;
2405
2696
  const checkField = doEntry.stubId.startsWith('user.') ? doEntry.stubId.slice(5) : doEntry.stubId;
2406
2697
  doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
@@ -2410,402 +2701,417 @@ ${initLines.join('\n')}
2410
2701
  doResolverLines.push(` });`);
2411
2702
  }
2412
2703
  else {
2413
- // No stubId config â€" stub must be obtained manually
2414
- doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} â€" stub must be obtained manually`);
2704
+ // No stubId config " stub must be obtained manually
2705
+ doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} " stub must be obtained manually`);
2415
2706
  }
2416
2707
  }
2417
2708
  doImports = doImportLines.join('\n');
2418
- doClassCode = `\n// â"€â" Durable Object Classes (generated) â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n` + doClassLines.join('\n') + '\n';
2709
+ doClassCode = `\n// "��" Durable Object Classes (generated) "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�\n\n` + doClassLines.join('\n') + '\n';
2419
2710
  doResolverInit = `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`;
2420
2711
  }
2421
- return `// Generated by KuratchiJS compiler â€" do not edit.
2422
- ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
2423
- ${workerImport}
2424
- ${contextImport}
2425
- ${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
2426
-
2427
- // â"€â" Assets â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2428
-
2429
- const __assets = {
2430
- ${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')}
2431
- };
2432
-
2433
- // â"€â" Router â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2434
-
2435
- const __staticRoutes = new Map(); // exact path â†' index (O(1) lookup)
2436
- const __dynamicRoutes = []; // regex-based routes (params/wildcards)
2437
-
2438
- function __addRoute(pattern, index) {
2439
- if (!pattern.includes(':') && !pattern.includes('*')) {
2440
- // Static route â€" direct Map lookup, no regex needed
2441
- __staticRoutes.set(pattern, index);
2442
- } else {
2443
- // Dynamic route â€" build regex for param extraction
2444
- const paramNames = [];
2445
- let regexStr = pattern
2446
- .replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
2447
- .replace(/:(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>[^/]+)'; });
2448
- __dynamicRoutes.push({ regex: new RegExp('^' + regexStr + '$'), paramNames, index });
2449
- }
2450
- }
2451
-
2452
- function __match(pathname) {
2453
- const normalized = pathname === '/' ? '/' : pathname.replace(/\\/$/, '');
2454
- // Fast path: static routes (most common)
2455
- const staticIdx = __staticRoutes.get(normalized);
2456
- if (staticIdx !== undefined) return { params: {}, index: staticIdx };
2457
- // Slow path: dynamic routes with params
2458
- for (const route of __dynamicRoutes) {
2459
- const m = normalized.match(route.regex);
2460
- if (m) {
2461
- const params = {};
2462
- for (const name of route.paramNames) params[name] = m.groups?.[name] ?? '';
2463
- return { params, index: route.index };
2464
- }
2465
- }
2466
- return null;
2467
- }
2468
-
2469
- // â"€â" Layout â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2470
-
2471
- ${layoutBlock}
2472
-
2473
- ${layoutActionsBlock}
2474
-
2475
- // â"€â" Error pages â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2476
-
2477
- const __errorMessages = {
2478
- 400: 'Bad Request',
2479
- 401: 'Unauthorized',
2480
- 403: 'Forbidden',
2481
- 404: 'Not Found',
2482
- 405: 'Method Not Allowed',
2483
- 408: 'Request Timeout',
2484
- 429: 'Too Many Requests',
2485
- 500: 'Internal Server Error',
2486
- 502: 'Bad Gateway',
2487
- 503: 'Service Unavailable',
2488
- };
2489
-
2490
- // Built-in default error page â€" clean, dark, minimal, centered
2491
- function __errorPage(status, detail) {
2492
- const title = __errorMessages[status] || 'Error';
2493
- 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>' : '';
2494
- return '<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:2rem">'
2495
- + '<div>'
2496
- + '<p style="font-size:5rem;font-weight:700;margin:0;color:#333;line-height:1">' + status + '</p>'
2497
- + '<p style="font-size:1rem;color:#555;margin:0.5rem 0 0;letter-spacing:0.05em">' + __esc(title) + '</p>'
2498
- + detailHtml
2499
- + '</div>'
2500
- + '</div>';
2501
- }
2502
-
2503
- ${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}
2504
- // Dispatch: use custom override if it exists, otherwise built-in default
2505
- const __customErrors = {${Array.from(opts.compiledErrorPages.keys()).map(s => ` ${s}: __error_${s}`).join(',')} };
2506
-
2507
- function __error(status, detail) {
2508
- if (__customErrors[status]) return __customErrors[status](detail);
2509
- return __errorPage(status, detail);
2510
- }
2511
-
2512
- ${opts.compiledComponents.length > 0 ? '// â"€â" Components â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
2513
- // â"€â" Route definitions â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2514
-
2515
- const routes = [
2516
- ${opts.compiledRoutes.join(',\n')}
2517
- ];
2518
-
2519
- for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
2520
-
2521
- // â"€â" Response helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2522
-
2523
- const __defaultSecHeaders = {
2524
- 'X-Content-Type-Options': 'nosniff',
2525
- 'X-Frame-Options': 'DENY',
2526
- 'Referrer-Policy': 'strict-origin-when-cross-origin',
2527
- };
2528
-
2529
- function __secHeaders(response) {
2530
- for (const [k, v] of Object.entries(__defaultSecHeaders)) {
2531
- if (!response.headers.has(k)) response.headers.set(k, v);
2532
- }
2533
- return response;
2534
- }
2535
-
2536
- function __attachCookies(response) {
2537
- const cookies = __getLocals().__setCookieHeaders;
2538
- if (cookies && cookies.length > 0) {
2539
- const newResponse = new Response(response.body, response);
2540
- for (const h of cookies) newResponse.headers.append('Set-Cookie', h);
2541
- return __secHeaders(newResponse);
2542
- }
2543
- return __secHeaders(response);
2544
- }
2545
-
2546
- function __isSameOrigin(request, url) {
2547
- const fetchSite = request.headers.get('sec-fetch-site');
2548
- if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
2549
- return false;
2550
- }
2551
- const origin = request.headers.get('origin');
2552
- if (!origin) return true;
2553
- try { return new URL(origin).origin === url.origin; } catch { return false; }
2554
- }
2555
-
2556
- ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
2557
- let html = route.render(data);
2558
- const headMatch = html.match(/<head>([\\s\\S]*?)<\\/head>/);
2559
- if (headMatch) {
2560
- html = html.replace(headMatch[0], '');
2561
- const layoutHtml = ${opts.isLayoutAsync ? 'await ' : ''}__layout(html);
2562
- return __attachCookies(new Response(layoutHtml.replace('</head>', headMatch[1] + '</head>'), {
2563
- headers: { 'content-type': 'text/html; charset=utf-8' }
2564
- }));
2565
- }
2566
- return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
2567
- }
2568
-
2569
- const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
2570
- const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
2571
-
2572
- async function __runRuntimeRequest(ctx, next) {
2573
- let idx = -1;
2574
- async function __dispatch(i) {
2575
- if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
2576
- idx = i;
2577
- const entry = __runtimeEntries[i];
2578
- if (!entry) return next();
2579
- const [, step] = entry;
2580
- if (typeof step.request !== 'function') return __dispatch(i + 1);
2581
- return await step.request(ctx, () => __dispatch(i + 1));
2582
- }
2583
- return __dispatch(0);
2584
- }
2585
-
2586
- async function __runRuntimeRoute(ctx, next) {
2587
- let idx = -1;
2588
- async function __dispatch(i) {
2589
- if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
2590
- idx = i;
2591
- const entry = __runtimeEntries[i];
2592
- if (!entry) return next();
2593
- const [, step] = entry;
2594
- if (typeof step.route !== 'function') return __dispatch(i + 1);
2595
- return await step.route(ctx, () => __dispatch(i + 1));
2596
- }
2597
- return __dispatch(0);
2598
- }
2599
-
2600
- async function __runRuntimeResponse(ctx, response) {
2601
- let out = response;
2602
- for (const [, step] of __runtimeEntries) {
2603
- if (typeof step.response !== 'function') continue;
2604
- out = await step.response(ctx, out);
2605
- if (!(out instanceof Response)) {
2606
- throw new Error('[kuratchi runtime] response handlers must return a Response');
2607
- }
2608
- }
2609
- return out;
2610
- }
2611
-
2612
- async function __runRuntimeError(ctx, error) {
2613
- for (const [name, step] of __runtimeEntries) {
2614
- if (typeof step.error !== 'function') continue;
2615
- try {
2616
- const handled = await step.error(ctx, error);
2617
- if (handled instanceof Response) return handled;
2618
- } catch (hookErr) {
2619
- console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
2620
- }
2621
- }
2622
- return null;
2623
- }
2624
-
2625
- // â"€â" Exported Worker entrypoint â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"
2626
-
2712
+ return `// Generated by KuratchiJS compiler " do not edit.
2713
+ ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
2714
+ ${workerImport}
2715
+ ${contextImport}
2716
+ ${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
2717
+
2718
+ // "��" Assets "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2719
+
2720
+ const __assets = {
2721
+ ${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')}
2722
+ };
2723
+
2724
+ // "��" Router "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2725
+
2726
+ const __staticRoutes = new Map(); // exact path ' index (O(1) lookup)
2727
+ const __dynamicRoutes = []; // regex-based routes (params/wildcards)
2728
+
2729
+ function __addRoute(pattern, index) {
2730
+ if (!pattern.includes(':') && !pattern.includes('*')) {
2731
+ // Static route " direct Map lookup, no regex needed
2732
+ __staticRoutes.set(pattern, index);
2733
+ } else {
2734
+ // Dynamic route " build regex for param extraction
2735
+ const paramNames = [];
2736
+ let regexStr = pattern
2737
+ .replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
2738
+ .replace(/:(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>[^/]+)'; });
2739
+ __dynamicRoutes.push({ regex: new RegExp('^' + regexStr + '$'), paramNames, index });
2740
+ }
2741
+ }
2742
+
2743
+ function __match(pathname) {
2744
+ const normalized = pathname === '/' ? '/' : pathname.replace(/\\/$/, '');
2745
+ // Fast path: static routes (most common)
2746
+ const staticIdx = __staticRoutes.get(normalized);
2747
+ if (staticIdx !== undefined) return { params: {}, index: staticIdx };
2748
+ // Slow path: dynamic routes with params
2749
+ for (const route of __dynamicRoutes) {
2750
+ const m = normalized.match(route.regex);
2751
+ if (m) {
2752
+ const params = {};
2753
+ for (const name of route.paramNames) params[name] = m.groups?.[name] ?? '';
2754
+ return { params, index: route.index };
2755
+ }
2756
+ }
2757
+ return null;
2758
+ }
2759
+
2760
+ // "��" Layout "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2761
+
2762
+ ${layoutBlock}
2763
+
2764
+ ${layoutActionsBlock}
2765
+
2766
+ // "��" Error pages "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2767
+
2768
+ const __errorMessages = {
2769
+ 400: 'Bad Request',
2770
+ 401: 'Unauthorized',
2771
+ 403: 'Forbidden',
2772
+ 404: 'Not Found',
2773
+ 405: 'Method Not Allowed',
2774
+ 408: 'Request Timeout',
2775
+ 429: 'Too Many Requests',
2776
+ 500: 'Internal Server Error',
2777
+ 502: 'Bad Gateway',
2778
+ 503: 'Service Unavailable',
2779
+ };
2780
+
2781
+ // Built-in default error page " clean, dark, minimal, centered
2782
+ function __errorPage(status, detail) {
2783
+ const title = __errorMessages[status] || 'Error';
2784
+ 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>' : '';
2785
+ return '<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:2rem">'
2786
+ + '<div>'
2787
+ + '<p style="font-size:5rem;font-weight:700;margin:0;color:#333;line-height:1">' + status + '</p>'
2788
+ + '<p style="font-size:1rem;color:#555;margin:0.5rem 0 0;letter-spacing:0.05em">' + __esc(title) + '</p>'
2789
+ + detailHtml
2790
+ + '</div>'
2791
+ + '</div>';
2792
+ }
2793
+
2794
+ ${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}
2795
+ // Dispatch: use custom override if it exists, otherwise built-in default
2796
+ const __customErrors = {${Array.from(opts.compiledErrorPages.keys()).map(s => ` ${s}: __error_${s}`).join(',')} };
2797
+
2798
+ function __error(status, detail) {
2799
+ if (__customErrors[status]) return __customErrors[status](detail);
2800
+ return __errorPage(status, detail);
2801
+ }
2802
+
2803
+ ${opts.compiledComponents.length > 0 ? '// "��" Components "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
2804
+ // "��" Route definitions "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2805
+
2806
+ const routes = [
2807
+ ${opts.compiledRoutes.join(',\n')}
2808
+ ];
2809
+
2810
+ for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
2811
+
2812
+ // "��" Response helpers "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2813
+
2814
+ const __defaultSecHeaders = {
2815
+ 'X-Content-Type-Options': 'nosniff',
2816
+ 'X-Frame-Options': 'DENY',
2817
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
2818
+ };
2819
+
2820
+ function __secHeaders(response) {
2821
+ for (const [k, v] of Object.entries(__defaultSecHeaders)) {
2822
+ if (!response.headers.has(k)) response.headers.set(k, v);
2823
+ }
2824
+ return response;
2825
+ }
2826
+
2827
+ function __attachCookies(response) {
2828
+ const cookies = __getLocals().__setCookieHeaders;
2829
+ if (cookies && cookies.length > 0) {
2830
+ const newResponse = new Response(response.body, response);
2831
+ for (const h of cookies) newResponse.headers.append('Set-Cookie', h);
2832
+ return __secHeaders(newResponse);
2833
+ }
2834
+ return __secHeaders(response);
2835
+ }
2836
+
2837
+ function __isSameOrigin(request, url) {
2838
+ const fetchSite = request.headers.get('sec-fetch-site');
2839
+ if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
2840
+ return false;
2841
+ }
2842
+ const origin = request.headers.get('origin');
2843
+ if (!origin) return true;
2844
+ try { return new URL(origin).origin === url.origin; } catch { return false; }
2845
+ }
2846
+
2847
+ ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
2848
+ let html = route.render(data);
2849
+ const headMatch = html.match(/<head>([\\s\\S]*?)<\\/head>/);
2850
+ if (headMatch) {
2851
+ html = html.replace(headMatch[0], '');
2852
+ const layoutHtml = ${opts.isLayoutAsync ? 'await ' : ''}__layout(html);
2853
+ return __attachCookies(new Response(layoutHtml.replace('</head>', headMatch[1] + '</head>'), {
2854
+ headers: { 'content-type': 'text/html; charset=utf-8' }
2855
+ }));
2856
+ }
2857
+ return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
2858
+ }
2859
+
2860
+ const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
2861
+ const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
2862
+
2863
+ async function __runRuntimeRequest(ctx, next) {
2864
+ let idx = -1;
2865
+ async function __dispatch(i) {
2866
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
2867
+ idx = i;
2868
+ const entry = __runtimeEntries[i];
2869
+ if (!entry) return next();
2870
+ const [, step] = entry;
2871
+ if (typeof step.request !== 'function') return __dispatch(i + 1);
2872
+ return await step.request(ctx, () => __dispatch(i + 1));
2873
+ }
2874
+ return __dispatch(0);
2875
+ }
2876
+
2877
+ async function __runRuntimeRoute(ctx, next) {
2878
+ let idx = -1;
2879
+ async function __dispatch(i) {
2880
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
2881
+ idx = i;
2882
+ const entry = __runtimeEntries[i];
2883
+ if (!entry) return next();
2884
+ const [, step] = entry;
2885
+ if (typeof step.route !== 'function') return __dispatch(i + 1);
2886
+ return await step.route(ctx, () => __dispatch(i + 1));
2887
+ }
2888
+ return __dispatch(0);
2889
+ }
2890
+
2891
+ async function __runRuntimeResponse(ctx, response) {
2892
+ let out = response;
2893
+ for (const [, step] of __runtimeEntries) {
2894
+ if (typeof step.response !== 'function') continue;
2895
+ out = await step.response(ctx, out);
2896
+ if (!(out instanceof Response)) {
2897
+ throw new Error('[kuratchi runtime] response handlers must return a Response');
2898
+ }
2899
+ }
2900
+ return out;
2901
+ }
2902
+
2903
+ async function __runRuntimeError(ctx, error) {
2904
+ for (const [name, step] of __runtimeEntries) {
2905
+ if (typeof step.error !== 'function') continue;
2906
+ try {
2907
+ const handled = await step.error(ctx, error);
2908
+ if (handled instanceof Response) return handled;
2909
+ } catch (hookErr) {
2910
+ console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
2911
+ }
2912
+ }
2913
+ return null;
2914
+ }
2915
+
2916
+ // "��" Exported Worker entrypoint "��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"
2917
+
2627
2918
  export default class extends WorkerEntrypoint {
2628
2919
  async fetch(request) {
2629
2920
  __setRequestContext(this.ctx, request, __env);
2630
2921
  ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
2631
- const __runtimeCtx = {
2632
- request,
2633
- env: __env,
2634
- ctx: this.ctx,
2635
- url: new URL(request.url),
2636
- params: {},
2637
- locals: __getLocals(),
2638
- };
2639
-
2640
- const __coreFetch = async () => {
2641
- const request = __runtimeCtx.request;
2642
- const url = __runtimeCtx.url;
2643
- ${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' : ''}
2644
-
2645
- // Serve static assets from src/assets/ at /_assets/*
2646
- if (url.pathname.startsWith('/_assets/')) {
2647
- const name = url.pathname.slice('/_assets/'.length);
2648
- const asset = __assets[name];
2649
- if (asset) {
2650
- if (request.headers.get('if-none-match') === asset.etag) {
2651
- return new Response(null, { status: 304 });
2652
- }
2653
- return new Response(asset.content, {
2654
- headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2655
- });
2656
- }
2657
- return __secHeaders(new Response('Not Found', { status: 404 }));
2658
- }
2659
-
2660
- const match = __match(url.pathname);
2661
-
2662
- if (!match) {
2663
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2664
- }
2665
-
2666
- __runtimeCtx.params = match.params;
2667
- const route = routes[match.index];
2668
- __setLocal('params', match.params);
2669
-
2670
- // API route: dispatch to method handler
2671
- if (route.__api) {
2672
- const method = request.method;
2673
- if (method === 'OPTIONS') {
2674
- const handler = route['OPTIONS'];
2675
- if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
2676
- const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
2677
- return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
2678
- }
2679
- const handler = route[method];
2680
- if (typeof handler !== 'function') {
2681
- const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
2682
- return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
2683
- }
2684
- return __secHeaders(await handler(__runtimeCtx));
2685
- }
2686
-
2687
- const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
2688
- const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
2689
- let __qArgs = [];
2690
- try {
2691
- const __parsed = JSON.parse(__qArgsRaw);
2692
- __qArgs = Array.isArray(__parsed) ? __parsed : [];
2693
- } catch {}
2694
- __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
2695
- if (!__getLocals().__breadcrumbs) {
2696
- __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
2697
- }
2698
-
2699
- // RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
2700
- const __rpcName = url.searchParams.get('_rpc');
2701
- if (request.method === 'GET' && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
2702
- if (request.headers.get('x-kuratchi-rpc') !== '1') {
2703
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
2704
- status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2705
- }));
2706
- }
2707
- try {
2708
- const __rpcArgsStr = url.searchParams.get('_args');
2709
- let __rpcArgs = [];
2710
- if (__rpcArgsStr) {
2711
- const __parsed = JSON.parse(__rpcArgsStr);
2712
- __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
2713
- }
2714
- const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
2715
- return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
2716
- headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2717
- }));
2718
- } catch (err) {
2719
- console.error('[kuratchi] RPC error:', err);
2720
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2721
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2722
- status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2723
- }));
2724
- }
2725
- }
2726
-
2727
- // Form action: POST with hidden _action field in form body
2728
- if (request.method === 'POST') {
2729
- if (!__isSameOrigin(request, url)) {
2730
- return __secHeaders(new Response('Forbidden', { status: 403 }));
2731
- }
2732
- const formData = await request.formData();
2733
- const actionName = formData.get('_action');
2734
- const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
2735
- || (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
2736
- if (actionName && __actionFn) {
2737
- // Check if this is a fetch-based action call (onclick) with JSON args
2738
- const argsStr = formData.get('_args');
2739
- const isFetchAction = argsStr !== null;
2740
- try {
2741
- if (isFetchAction) {
2742
- const __parsed = JSON.parse(argsStr);
2743
- const args = Array.isArray(__parsed) ? __parsed : [];
2744
- await __actionFn(...args);
2745
- } else {
2746
- await __actionFn(formData);
2747
- }
2748
- } catch (err) {
2749
- console.error('[kuratchi] Action error:', err);
2750
- if (isFetchAction) {
2751
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2752
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2753
- status: 500, headers: { 'content-type': 'application/json' }
2754
- }));
2755
- }
2756
- const __loaded = route.load ? await route.load(match.params) : {};
2757
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2758
- data.params = match.params;
2759
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2760
- const __allActions = Object.assign({}, route.actions, __layoutActions || {});
2761
- Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
2762
- const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
2763
- data[actionName] = { error: __errMsg, loading: false, success: false };
2764
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2765
- }
2766
- // Fetch-based actions return lightweight JSON (no page re-render)
2767
- if (isFetchAction) {
2768
- return __attachCookies(new Response(JSON.stringify({ ok: true }), {
2769
- headers: { 'content-type': 'application/json' }
2770
- }));
2771
- }
2772
- // POST-Redirect-GET: redirect to custom target or back to same URL
2773
- const __locals = __getLocals();
2774
- const redirectTo = __locals.__redirectTo || url.pathname;
2775
- const redirectStatus = Number(__locals.__redirectStatus) || 303;
2776
- return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
2777
- }
2778
- }
2779
-
2780
- // GET (or unmatched POST): load + render
2781
- try {
2782
- const __loaded = route.load ? await route.load(match.params) : {};
2783
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2784
- data.params = match.params;
2785
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2786
- const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
2787
- Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
2788
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2789
- } catch (err) {
2790
- console.error('[kuratchi] Route load/render error:', err);
2791
- const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
2792
- const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
2793
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2794
- }
2795
- };
2796
-
2797
- try {
2798
- const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
2799
- return __runRuntimeRoute(__runtimeCtx, __coreFetch);
2800
- });
2801
- return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
2802
- } catch (err) {
2803
- const __handled = await __runRuntimeError(__runtimeCtx, err);
2804
- if (__handled) return __secHeaders(__handled);
2805
- throw err;
2806
- }
2807
- }
2808
- }
2922
+ const __runtimeCtx = {
2923
+ request,
2924
+ env: __env,
2925
+ ctx: this.ctx,
2926
+ url: new URL(request.url),
2927
+ params: {},
2928
+ locals: __getLocals(),
2929
+ };
2930
+
2931
+ const __coreFetch = async () => {
2932
+ const request = __runtimeCtx.request;
2933
+ const url = __runtimeCtx.url;
2934
+ ${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' : ''}
2935
+
2936
+ // Serve static assets from src/assets/ at /_assets/*
2937
+ if (url.pathname.startsWith('/_assets/')) {
2938
+ const name = url.pathname.slice('/_assets/'.length);
2939
+ const asset = __assets[name];
2940
+ if (asset) {
2941
+ if (request.headers.get('if-none-match') === asset.etag) {
2942
+ return new Response(null, { status: 304 });
2943
+ }
2944
+ return new Response(asset.content, {
2945
+ headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2946
+ });
2947
+ }
2948
+ return __secHeaders(new Response('Not Found', { status: 404 }));
2949
+ }
2950
+
2951
+ const match = __match(url.pathname);
2952
+
2953
+ if (!match) {
2954
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2955
+ }
2956
+
2957
+ __runtimeCtx.params = match.params;
2958
+ const route = routes[match.index];
2959
+ __setLocal('params', match.params);
2960
+
2961
+ // API route: dispatch to method handler
2962
+ if (route.__api) {
2963
+ const method = request.method;
2964
+ if (method === 'OPTIONS') {
2965
+ const handler = route['OPTIONS'];
2966
+ if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
2967
+ const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
2968
+ return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
2969
+ }
2970
+ const handler = route[method];
2971
+ if (typeof handler !== 'function') {
2972
+ const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
2973
+ return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
2974
+ }
2975
+ return __secHeaders(await handler(__runtimeCtx));
2976
+ }
2977
+
2978
+ const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
2979
+ const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
2980
+ let __qArgs = [];
2981
+ try {
2982
+ const __parsed = JSON.parse(__qArgsRaw);
2983
+ __qArgs = Array.isArray(__parsed) ? __parsed : [];
2984
+ } catch {}
2985
+ __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
2986
+ if (!__getLocals().__breadcrumbs) {
2987
+ __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
2988
+ }
2989
+
2990
+ // RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
2991
+ const __rpcName = url.searchParams.get('_rpc');
2992
+ if (request.method === 'GET' && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
2993
+ if (request.headers.get('x-kuratchi-rpc') !== '1') {
2994
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
2995
+ status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2996
+ }));
2997
+ }
2998
+ try {
2999
+ const __rpcArgsStr = url.searchParams.get('_args');
3000
+ let __rpcArgs = [];
3001
+ if (__rpcArgsStr) {
3002
+ const __parsed = JSON.parse(__rpcArgsStr);
3003
+ __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
3004
+ }
3005
+ const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
3006
+ return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
3007
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3008
+ }));
3009
+ } catch (err) {
3010
+ console.error('[kuratchi] RPC error:', err);
3011
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
3012
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
3013
+ status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3014
+ }));
3015
+ }
3016
+ }
3017
+
3018
+ // Form action: POST with hidden _action field in form body
3019
+ if (request.method === 'POST') {
3020
+ if (!__isSameOrigin(request, url)) {
3021
+ return __secHeaders(new Response('Forbidden', { status: 403 }));
3022
+ }
3023
+ const formData = await request.formData();
3024
+ const actionName = formData.get('_action');
3025
+ const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
3026
+ || (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
3027
+ if (actionName && __actionFn) {
3028
+ // Check if this is a fetch-based action call (onclick) with JSON args
3029
+ const argsStr = formData.get('_args');
3030
+ const isFetchAction = argsStr !== null;
3031
+ try {
3032
+ if (isFetchAction) {
3033
+ const __parsed = JSON.parse(argsStr);
3034
+ const args = Array.isArray(__parsed) ? __parsed : [];
3035
+ await __actionFn(...args);
3036
+ } else {
3037
+ await __actionFn(formData);
3038
+ }
3039
+ } catch (err) {
3040
+ if (err && err.isRedirectError) {
3041
+ const __redirectTo = err.location || url.pathname;
3042
+ const __redirectStatus = Number(err.status) || 303;
3043
+ if (isFetchAction) {
3044
+ return __attachCookies(__secHeaders(new Response(JSON.stringify({ ok: true, redirectTo: __redirectTo, redirectStatus: __redirectStatus }), {
3045
+ headers: { 'content-type': 'application/json' }
3046
+ })));
3047
+ }
3048
+ return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
3049
+ }
3050
+ console.error('[kuratchi] Action error:', err);
3051
+ if (isFetchAction) {
3052
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' && err && err.message ? err.message : 'Internal Server Error';
3053
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
3054
+ status: 500, headers: { 'content-type': 'application/json' }
3055
+ }));
3056
+ }
3057
+ const __loaded = route.load ? await route.load(match.params) : {};
3058
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
3059
+ data.params = match.params;
3060
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
3061
+ const __allActions = Object.assign({}, route.actions, __layoutActions || {});
3062
+ Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
3063
+ const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
3064
+ data[actionName] = { error: __errMsg, loading: false, success: false };
3065
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
3066
+ }
3067
+ // Fetch-based actions return lightweight JSON (no page re-render)
3068
+ if (isFetchAction) {
3069
+ return __attachCookies(new Response(JSON.stringify({ ok: true }), {
3070
+ headers: { 'content-type': 'application/json' }
3071
+ }));
3072
+ }
3073
+ // POST-Redirect-GET: redirect to custom target or back to same URL
3074
+ const __locals = __getLocals();
3075
+ const redirectTo = __locals.__redirectTo || url.pathname;
3076
+ const redirectStatus = Number(__locals.__redirectStatus) || 303;
3077
+ return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
3078
+ }
3079
+ }
3080
+
3081
+ // GET (or unmatched POST): load + render
3082
+ try {
3083
+ const __loaded = route.load ? await route.load(match.params) : {};
3084
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
3085
+ data.params = match.params;
3086
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
3087
+ const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
3088
+ Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
3089
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
3090
+ } catch (err) {
3091
+ if (err && err.isRedirectError) {
3092
+ const __redirectTo = err.location || url.pathname;
3093
+ const __redirectStatus = Number(err.status) || 303;
3094
+ return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
3095
+ }
3096
+ console.error('[kuratchi] Route load/render error:', err);
3097
+ const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
3098
+ const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
3099
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
3100
+ }
3101
+ };
3102
+
3103
+ try {
3104
+ const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
3105
+ return __runRuntimeRoute(__runtimeCtx, __coreFetch);
3106
+ });
3107
+ return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
3108
+ } catch (err) {
3109
+ const __handled = await __runRuntimeError(__runtimeCtx, err);
3110
+ if (__handled) return __secHeaders(__handled);
3111
+ throw err;
3112
+ }
3113
+ }
3114
+ }
2809
3115
  `;
2810
3116
  }
2811
3117
  function resolveRuntimeImportPath(projectDir) {