@kuratchi/js 0.0.13 → 0.0.15

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.
@@ -252,385 +252,401 @@ export function compile(options) {
252
252
  // - server actions bound via onX={serverAction(...)} -> [data-action][data-action-event]
253
253
  // - declarative confirm="..."
254
254
  // - declarative checkbox groups: data-select-all / data-select-item
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); });
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
+ // Parse human-readable interval: 2s, 500ms, 1m, 30s (default 30s)
473
+ function parseInterval(str){
474
+ if(!str) return 30000;
475
+ var m = str.match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/i);
476
+ if(!m) return 30000;
477
+ var n = parseFloat(m[1]);
478
+ var u = (m[2] || 's').toLowerCase();
479
+ if(u === 'ms') return n;
480
+ if(u === 'm') return n * 60000;
481
+ return n * 1000;
482
+ }
483
+ function bindPollEl(el){
484
+ if(!el || !el.getAttribute) return;
485
+ if(el.getAttribute('data-kuratchi-poll-bound') === '1') return;
486
+ var fn = el.getAttribute('data-poll');
487
+ if(!fn) return;
488
+ el.setAttribute('data-kuratchi-poll-bound', '1');
489
+ var pollId = el.getAttribute('data-poll-id');
490
+ if(!pollId) return; // Server must provide stable poll ID
491
+ var baseIv = parseInterval(el.getAttribute('data-interval'));
492
+ var maxIv = Math.min(baseIv * 10, 300000); // cap at 5 minutes
493
+ var backoff = el.getAttribute('data-backoff') !== 'false';
494
+ var prevHtml = el.innerHTML;
495
+ var currentIv = baseIv;
496
+ (function tick(){
497
+ setTimeout(function(){
498
+ // Request only the fragment, not the full page
499
+ fetch(location.pathname + location.search, { headers: { 'x-kuratchi-fragment': pollId } })
500
+ .then(function(r){ return r.text(); })
501
+ .then(function(html){
502
+ if(prevHtml !== html){
503
+ el.innerHTML = html;
504
+ prevHtml = html;
505
+ currentIv = baseIv; // Reset backoff on change
506
+ } else if(backoff && currentIv < maxIv){
507
+ currentIv = Math.min(currentIv * 1.5, maxIv);
508
+ }
509
+ tick();
510
+ })
511
+ .catch(function(){ currentIv = baseIv; tick(); });
512
+ }, currentIv);
513
+ })();
514
+ }
515
+ function scan(){
516
+ by('[data-poll]').forEach(bindPollEl);
517
+ }
518
+ scan();
519
+ setInterval(scan, 500);
520
+ })();
521
+ function confirmClick(e){
522
+ var el = e.target && e.target.closest ? e.target.closest('[confirm]') : null;
523
+ if(!el) return;
524
+ var msg = el.getAttribute('confirm');
525
+ if(!msg) return;
526
+ if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
527
+ }
528
+ document.addEventListener('click', confirmClick, true);
529
+ document.addEventListener('submit', function(e){
530
+ var f = e.target && e.target.matches && e.target.matches('form[confirm]') ? e.target : null;
531
+ if(!f) return;
532
+ var msg = f.getAttribute('confirm');
533
+ if(!msg) return;
534
+ if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
535
+ }, true);
536
+ document.addEventListener('submit', function(e){
537
+ if(e.defaultPrevented) return;
538
+ var f = e.target;
539
+ if(!f || !f.querySelector) return;
540
+ var aInput = f.querySelector('input[name="_action"]');
541
+ if(!aInput) return;
542
+ var aName = aInput.value;
543
+ if(!aName) return;
544
+ f.setAttribute('data-action-loading', aName);
545
+ Array.prototype.slice.call(f.querySelectorAll('button[type="submit"],button:not([type="button"]):not([type="reset"])')).forEach(function(b){ b.disabled = true; });
546
+ }, true);
547
+ document.addEventListener('change', function(e){
548
+ var t = e.target;
549
+ if(!t || !t.getAttribute) return;
550
+ var gAll = t.getAttribute('data-select-all');
551
+ if(gAll){
552
+ by('[data-select-item]').filter(function(i){ return i.getAttribute('data-select-item') === gAll; }).forEach(function(i){ i.checked = !!t.checked; });
553
+ syncGroup(gAll);
554
+ return;
555
+ }
556
+ var gItem = t.getAttribute('data-select-item');
557
+ if(gItem) syncGroup(gItem);
558
+ }, true);
559
+ by('[data-select-all]').forEach(function(m){ var g = m.getAttribute('data-select-all'); if(g) syncGroup(g); });
544
560
  })();`;
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 };
561
+ const reactiveRuntimeSource = `(function(g){
562
+ if(g.__kuratchiReactive) return;
563
+ const targetMap = new WeakMap();
564
+ const proxyMap = new WeakMap();
565
+ let active = null;
566
+ const queue = new Set();
567
+ let flushing = false;
568
+ function queueRun(fn){
569
+ queue.add(fn);
570
+ if(flushing) return;
571
+ flushing = true;
572
+ Promise.resolve().then(function(){
573
+ try {
574
+ const jobs = Array.from(queue);
575
+ queue.clear();
576
+ for (const job of jobs) job();
577
+ } finally {
578
+ flushing = false;
579
+ }
580
+ });
581
+ }
582
+ function cleanup(effect){
583
+ const deps = effect.__deps || [];
584
+ for (const dep of deps) dep.delete(effect);
585
+ effect.__deps = [];
586
+ }
587
+ function track(target, key){
588
+ if(!active) return;
589
+ let depsMap = targetMap.get(target);
590
+ if(!depsMap){ depsMap = new Map(); targetMap.set(target, depsMap); }
591
+ let dep = depsMap.get(key);
592
+ if(!dep){ dep = new Set(); depsMap.set(key, dep); }
593
+ if(dep.has(active)) return;
594
+ dep.add(active);
595
+ if(!active.__deps) active.__deps = [];
596
+ active.__deps.push(dep);
597
+ }
598
+ function trigger(target, key){
599
+ const depsMap = targetMap.get(target);
600
+ if(!depsMap) return;
601
+ const effects = new Set();
602
+ const add = function(k){
603
+ const dep = depsMap.get(k);
604
+ if(dep) dep.forEach(function(e){ effects.add(e); });
605
+ };
606
+ add(key);
607
+ add('*');
608
+ effects.forEach(function(e){ queueRun(e); });
609
+ }
610
+ function isObject(value){ return value !== null && typeof value === 'object'; }
611
+ function proxify(value){
612
+ if(!isObject(value)) return value;
613
+ if(proxyMap.has(value)) return proxyMap.get(value);
614
+ const proxy = new Proxy(value, {
615
+ get(target, key, receiver){
616
+ track(target, key);
617
+ const out = Reflect.get(target, key, receiver);
618
+ return isObject(out) ? proxify(out) : out;
619
+ },
620
+ set(target, key, next, receiver){
621
+ const prev = target[key];
622
+ const result = Reflect.set(target, key, next, receiver);
623
+ if(prev !== next) trigger(target, key);
624
+ if(Array.isArray(target) && key !== 'length') trigger(target, 'length');
625
+ return result;
626
+ },
627
+ deleteProperty(target, key){
628
+ const had = Object.prototype.hasOwnProperty.call(target, key);
629
+ const result = Reflect.deleteProperty(target, key);
630
+ if(had) trigger(target, key);
631
+ return result;
632
+ }
633
+ });
634
+ proxyMap.set(value, proxy);
635
+ return proxy;
636
+ }
637
+ function effect(fn){
638
+ const run = function(){
639
+ cleanup(run);
640
+ active = run;
641
+ try { fn(); } finally { active = null; }
642
+ };
643
+ run.__deps = [];
644
+ run();
645
+ return function(){ cleanup(run); };
646
+ }
647
+ function state(initial){ return proxify(initial); }
648
+ function replace(_prev, next){ return proxify(next); }
649
+ g.__kuratchiReactive = { state, effect, replace };
634
650
  })(window);`;
635
651
  const actionScript = `<script>${options.isDev ? bridgeSource : compactInlineJs(bridgeSource)}</script>`;
636
652
  const reactiveRuntimeScript = `<script>${options.isDev ? reactiveRuntimeSource : compactInlineJs(reactiveRuntimeSource)}</script>`;
@@ -693,10 +709,10 @@ export function compile(options) {
693
709
  let layoutScriptBody = stripTopLevelImports(layoutParsed.script);
694
710
  const layoutDevDecls = buildDevAliasDeclarations(layoutParsed.devAliases, !!options.isDev);
695
711
  layoutScriptBody = [layoutDevDecls, layoutScriptBody].filter(Boolean).join('\n');
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;
712
+ compiledLayout = `function __layout(__content) {
713
+ 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;'); };
714
+ ${layoutScriptBody ? layoutScriptBody + '\n ' : ''}${finalLayoutBody}
715
+ return __html;
700
716
  }`;
701
717
  }
702
718
  else {
@@ -731,14 +747,13 @@ export function compile(options) {
731
747
  const ormDatabases = readOrmConfig(projectDir);
732
748
  // Read auth config from kuratchi.config.ts
733
749
  const authConfig = readAuthConfig(projectDir);
734
- // Read Durable Object config and discover handler files
735
- const doConfig = readDoConfig(projectDir);
736
- const containerConfig = readWorkerClassConfig(projectDir, 'containers');
737
- const workflowConfig = readWorkerClassConfig(projectDir, 'workflows');
750
+ // Auto-discover Durable Objects from .do.ts files (config optional, only needed for stubId)
751
+ const configDoEntries = readDoConfig(projectDir);
752
+ const { config: doConfig, handlers: doHandlers } = discoverDurableObjects(srcDir, configDoEntries, ormDatabases);
753
+ // Auto-discover convention-based worker class files (no config needed)
754
+ const containerConfig = discoverContainerFiles(projectDir);
755
+ const workflowConfig = discoverWorkflowFiles(projectDir);
738
756
  const agentConfig = discoverConventionClassFiles(projectDir, path.join('src', 'server'), '.agent.ts', '.agent');
739
- const doHandlers = doConfig.length > 0
740
- ? discoverDoHandlers(srcDir, doConfig, ormDatabases)
741
- : [];
742
757
  // Generate handler proxy modules in .kuratchi/do/ (must happen BEFORE route processing
743
758
  // so that $durable-objects/X imports can be redirected to the generated proxies)
744
759
  const doProxyDir = path.join(projectDir, '.kuratchi', 'do');
@@ -1259,6 +1274,7 @@ export function compile(options) {
1259
1274
  authConfig,
1260
1275
  doConfig,
1261
1276
  doHandlers,
1277
+ workflowConfig,
1262
1278
  isDev: options.isDev ?? false,
1263
1279
  isLayoutAsync,
1264
1280
  compiledLayoutActions,
@@ -1294,6 +1310,12 @@ export function compile(options) {
1294
1310
  '',
1295
1311
  ];
1296
1312
  writeIfChanged(workerFile, workerLines.join('\n'));
1313
+ // Auto-sync wrangler.jsonc with workflow/container/DO config from kuratchi.config.ts
1314
+ syncWranglerConfig(projectDir, {
1315
+ workflows: workflowConfig,
1316
+ containers: containerConfig,
1317
+ durableObjects: doConfig,
1318
+ });
1297
1319
  return workerFile;
1298
1320
  }
1299
1321
  // �"��"� Helpers �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
@@ -1725,16 +1747,16 @@ function buildRouteObject(opts) {
1725
1747
  const queryReturnEntries = queryVars
1726
1748
  .filter((name) => !scriptReturnVars.includes(name))
1727
1749
  .map((name) => name);
1728
- returnObj = `
1750
+ returnObj = `
1729
1751
  return { ${[...segmentReturnEntries, ...queryReturnEntries].join(', ')} };`;
1730
1752
  }
1731
1753
  else {
1732
- returnObj = `
1754
+ returnObj = `
1733
1755
  return { ${loadReturnVars.join(', ')} };`;
1734
1756
  }
1735
1757
  }
1736
- parts.push(` async load(__routeParams = {}) {
1737
- ${loadBody}${returnObj}
1758
+ parts.push(` async load(__routeParams = {}) {
1759
+ ${loadBody}${returnObj}
1738
1760
  }`);
1739
1761
  }
1740
1762
  // Actions �" functions referenced via action={fn} in the template
@@ -1786,9 +1808,9 @@ function buildRouteObject(opts) {
1786
1808
  const styleLines = componentStyles.map(css => `__html += \`${css}\\n\`;`);
1787
1809
  finalRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
1788
1810
  }
1789
- parts.push(` render(data) {
1790
- ${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
1791
- return __html;
1811
+ parts.push(` render(data) {
1812
+ ${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
1813
+ return __html;
1792
1814
  }`);
1793
1815
  return ` {\n${parts.join(',\n')}\n }`;
1794
1816
  }
@@ -2100,95 +2122,135 @@ function discoverFilesWithSuffix(dir, suffix) {
2100
2122
  return out;
2101
2123
  }
2102
2124
  /**
2103
- * Scan DO handler files.
2104
- * - Class mode: default class extends kuratchiDO
2105
- * - Function mode: exported functions in *.do.ts files (compiler wraps into DO class)
2125
+ * Auto-discover .workflow.ts files in src/server/.
2126
+ * Derives binding name from filename: migration.workflow.ts MIGRATION_WORKFLOW
2127
+ * Returns entries compatible with WorkerClassConfigEntry for worker.js export generation.
2128
+ */
2129
+ function discoverWorkflowFiles(projectDir) {
2130
+ const serverDir = path.join(projectDir, 'src', 'server');
2131
+ const files = discoverFilesWithSuffix(serverDir, '.workflow.ts');
2132
+ if (files.length === 0)
2133
+ return [];
2134
+ return files.map((absPath) => {
2135
+ const fileName = path.basename(absPath, '.workflow.ts');
2136
+ // Derive binding: migration.workflow.ts → MIGRATION_WORKFLOW
2137
+ const binding = fileName.toUpperCase().replace(/-/g, '_') + '_WORKFLOW';
2138
+ const resolved = resolveClassExportFromFile(absPath, `.workflow`);
2139
+ return {
2140
+ binding,
2141
+ className: resolved.className,
2142
+ file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
2143
+ exportKind: resolved.exportKind,
2144
+ };
2145
+ });
2146
+ }
2147
+ /**
2148
+ * Auto-discover .container.ts files in src/server/.
2149
+ * Derives binding name from filename: wordpress.container.ts → WORDPRESS_CONTAINER
2150
+ * Returns entries compatible with WorkerClassConfigEntry for worker.js export generation.
2106
2151
  */
2107
- function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
2152
+ function discoverContainerFiles(projectDir) {
2153
+ const serverDir = path.join(projectDir, 'src', 'server');
2154
+ const files = discoverFilesWithSuffix(serverDir, '.container.ts');
2155
+ if (files.length === 0)
2156
+ return [];
2157
+ return files.map((absPath) => {
2158
+ const fileName = path.basename(absPath, '.container.ts');
2159
+ // Derive binding: wordpress.container.ts → WORDPRESS_CONTAINER
2160
+ const binding = fileName.toUpperCase().replace(/-/g, '_') + '_CONTAINER';
2161
+ const resolved = resolveClassExportFromFile(absPath, `.container`);
2162
+ return {
2163
+ binding,
2164
+ className: resolved.className,
2165
+ file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
2166
+ exportKind: resolved.exportKind,
2167
+ };
2168
+ });
2169
+ }
2170
+ /**
2171
+ * Auto-discover Durable Objects from .do.ts files.
2172
+ * Returns both DoConfigEntry (for wrangler sync) and DoHandlerEntry (for code gen).
2173
+ *
2174
+ * Convention:
2175
+ * - File: user.do.ts → Binding: USER_DO
2176
+ * - Class: export default class UserDO extends DurableObject
2177
+ * - Optional: static binding = 'CUSTOM_BINDING' to override
2178
+ */
2179
+ function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
2108
2180
  const serverDir = path.join(srcDir, 'server');
2109
2181
  const legacyDir = path.join(srcDir, 'durable-objects');
2110
2182
  const serverDoFiles = discoverFilesWithSuffix(serverDir, '.do.ts');
2111
2183
  const legacyDoFiles = discoverFilesWithSuffix(legacyDir, '.ts');
2112
2184
  const discoveredFiles = Array.from(new Set([...serverDoFiles, ...legacyDoFiles]));
2113
- if (discoveredFiles.length === 0)
2114
- return [];
2115
- const bindings = new Set(doConfig.map(d => d.binding));
2116
- const fileToBinding = new Map();
2117
- for (const entry of doConfig) {
2118
- for (const rawFile of entry.files ?? []) {
2119
- const normalized = rawFile.trim().replace(/^\.?[\\/]/, '').replace(/\\/g, '/').toLowerCase();
2120
- if (!normalized)
2121
- continue;
2122
- fileToBinding.set(normalized, entry.binding);
2123
- const base = path.basename(normalized);
2124
- if (!fileToBinding.has(base))
2125
- fileToBinding.set(base, entry.binding);
2126
- }
2185
+ if (discoveredFiles.length === 0) {
2186
+ return { config: configDoEntries, handlers: [] };
2187
+ }
2188
+ // Build lookup from config for stubId (still needed for auth integration)
2189
+ const configByBinding = new Map();
2190
+ for (const entry of configDoEntries) {
2191
+ configByBinding.set(entry.binding, entry);
2127
2192
  }
2128
2193
  const handlers = [];
2194
+ const discoveredConfig = [];
2129
2195
  const fileNameToAbsPath = new Map();
2196
+ const seenBindings = new Set();
2130
2197
  for (const absPath of discoveredFiles) {
2131
2198
  const file = path.basename(absPath);
2132
2199
  const source = fs.readFileSync(absPath, 'utf-8');
2133
- const exportedFunctions = extractExportedFunctions(source);
2134
- const hasClass = /extends\s+kuratchiDO\b/.test(source);
2135
- if (!hasClass && exportedFunctions.length === 0)
2200
+ // Must extend DurableObject
2201
+ const hasClass = /extends\s+DurableObject\b/.test(source);
2202
+ if (!hasClass)
2136
2203
  continue;
2137
- // Extract class name when class mode is used.
2138
- const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+kuratchiDO/);
2204
+ // Extract class name
2205
+ const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+DurableObject/);
2139
2206
  const className = classMatch?.[1] ?? null;
2140
- if (hasClass && !className)
2207
+ if (!className)
2141
2208
  continue;
2142
- // Binding resolution:
2143
- // 1) explicit static binding in class
2144
- // 2) config-mapped file name (supports .do.ts convention)
2145
- // 3) if exactly one DO binding exists, infer that binding
2146
- let binding = null;
2209
+ // Derive binding from filename or static binding property
2210
+ // user.do.ts USER_DO
2147
2211
  const bindingMatch = source.match(/static\s+binding\s*=\s*['"](\w+)['"]/);
2148
- if (bindingMatch) {
2149
- binding = bindingMatch[1];
2150
- }
2151
- else {
2152
- const normalizedFile = file.replace(/\\/g, '/').toLowerCase();
2153
- const normalizedRelFromSrc = path
2154
- .relative(srcDir, absPath)
2155
- .replace(/\\/g, '/')
2156
- .toLowerCase();
2157
- binding = fileToBinding.get(normalizedRelFromSrc) ?? fileToBinding.get(normalizedFile) ?? null;
2158
- if (!binding && doConfig.length === 1) {
2159
- binding = doConfig[0].binding;
2160
- }
2161
- }
2162
- if (!binding)
2163
- continue;
2164
- if (!bindings.has(binding))
2165
- continue;
2166
- // Extract class methods in class mode
2167
- const classMethods = className ? extractClassMethods(source, className) : [];
2212
+ const baseName = file.replace(/\.do\.ts$/, '').replace(/\.ts$/, '');
2213
+ const derivedBinding = baseName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase() + '_DO';
2214
+ const binding = bindingMatch?.[1] ?? derivedBinding;
2215
+ if (seenBindings.has(binding)) {
2216
+ throw new Error(`[KuratchiJS] Duplicate DO binding '${binding}' detected. Use 'static binding = "UNIQUE_NAME"' in one of the classes.`);
2217
+ }
2218
+ seenBindings.add(binding);
2219
+ // Extract public class methods for RPC
2220
+ const classMethods = extractClassMethods(source, className);
2168
2221
  const fileName = file.replace(/\.ts$/, '');
2169
2222
  const existing = fileNameToAbsPath.get(fileName);
2170
2223
  if (existing && existing !== absPath) {
2171
2224
  throw new Error(`[KuratchiJS] Duplicate DO handler file name '${fileName}.ts' detected:\n- ${existing}\n- ${absPath}\nRename one file or move it to avoid proxy name collision.`);
2172
2225
  }
2173
2226
  fileNameToAbsPath.set(fileName, absPath);
2227
+ // Merge with config entry if exists (for stubId)
2228
+ const configEntry = configByBinding.get(binding);
2229
+ discoveredConfig.push({
2230
+ binding,
2231
+ className,
2232
+ stubId: configEntry?.stubId,
2233
+ files: [file],
2234
+ });
2174
2235
  handlers.push({
2175
2236
  fileName,
2176
2237
  absPath,
2177
2238
  binding,
2178
- mode: hasClass ? 'class' : 'function',
2179
- className: className ?? undefined,
2239
+ mode: 'class',
2240
+ className,
2180
2241
  classMethods,
2181
- exportedFunctions,
2242
+ exportedFunctions: [],
2182
2243
  });
2183
2244
  }
2184
- return handlers;
2245
+ return { config: discoveredConfig, handlers };
2185
2246
  }
2186
2247
  /**
2187
2248
  * Extract method names from a class body using brace-balanced parsing.
2249
+ * Only public methods (no private/protected/underscore prefix) are RPC-accessible.
2188
2250
  */
2189
2251
  function extractClassMethods(source, className) {
2190
- // Find: class ClassName extends kuratchiDO {
2191
- const classIdx = source.search(new RegExp(`class\\s+${className}\\s+extends\\s+kuratchiDO`));
2252
+ // Find: class ClassName extends DurableObject {
2253
+ const classIdx = source.search(new RegExp(`class\\s+${className}\\s+extends\\s+DurableObject`));
2192
2254
  if (classIdx === -1)
2193
2255
  return [];
2194
2256
  const braceStart = source.indexOf('{', classIdx);
@@ -2282,21 +2344,23 @@ function extractExportedFunctions(source) {
2282
2344
  * Generate a proxy module for a DO handler file.
2283
2345
  *
2284
2346
  * The proxy provides auto-RPC function exports.
2285
- * - Class mode: public class methods become RPC exports.
2286
- * - Function mode: exported functions become RPC exports, excluding lifecycle hooks.
2347
+ * Class mode only: public class methods become RPC exports.
2348
+ * Methods starting with underscore or marked private/protected are excluded.
2287
2349
  */
2288
2350
  function generateHandlerProxy(handler, projectDir) {
2289
2351
  const doDir = path.join(projectDir, '.kuratchi', 'do');
2290
2352
  const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
2291
2353
  const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
2292
- const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
2293
- const rpcFunctions = handler.mode === 'function'
2294
- ? handler.exportedFunctions.filter((n) => !lifecycle.has(n))
2295
- : handler.classMethods.filter((m) => m.visibility === 'public').map((m) => m.name);
2354
+ // Lifecycle methods excluded from RPC
2355
+ const lifecycle = new Set(['constructor', 'fetch', 'alarm', 'webSocketMessage', 'webSocketClose', 'webSocketError']);
2356
+ // Only public methods (not starting with _) are RPC-accessible
2357
+ const rpcFunctions = handler.classMethods
2358
+ .filter((m) => m.visibility === 'public' && !m.name.startsWith('_') && !lifecycle.has(m.name))
2359
+ .map((m) => m.name);
2296
2360
  const methods = handler.classMethods.map((m) => ({ ...m }));
2297
2361
  const methodMap = new Map(methods.map((m) => [m.name, m]));
2298
2362
  let changed = true;
2299
- while (changed && handler.mode === 'class') {
2363
+ while (changed) {
2300
2364
  changed = false;
2301
2365
  for (const m of methods) {
2302
2366
  if (m.hasWorkerContextCalls)
@@ -2311,16 +2375,14 @@ function generateHandlerProxy(handler, projectDir) {
2311
2375
  }
2312
2376
  }
2313
2377
  }
2314
- const workerContextMethods = handler.mode === 'class'
2315
- ? methods.filter((m) => m.visibility === 'public' && m.hasWorkerContextCalls).map((m) => m.name)
2316
- : [];
2317
- const asyncMethods = handler.mode === 'class'
2318
- ? methods.filter((m) => m.isAsync).map((m) => m.name)
2319
- : [];
2378
+ const workerContextMethods = methods
2379
+ .filter((m) => m.visibility === 'public' && m.hasWorkerContextCalls)
2380
+ .map((m) => m.name);
2381
+ const asyncMethods = methods.filter((m) => m.isAsync).map((m) => m.name);
2320
2382
  const lines = [
2321
2383
  `// Auto-generated by KuratchiJS compiler �" do not edit.`,
2322
2384
  `import { __getDoStub } from '${RUNTIME_DO_IMPORT}';`,
2323
- ...(handler.mode === 'class' ? [`import ${handlerLocal} from '${origRelPath}';`] : []),
2385
+ `import ${handlerLocal} from '${origRelPath}';`,
2324
2386
  ``,
2325
2387
  `const __FD_TAG = '__kuratchi_form_data__';`,
2326
2388
  `function __isPlainObject(__v) {`,
@@ -2416,30 +2478,30 @@ function generateRoutesModule(opts) {
2416
2478
  let authInit = '';
2417
2479
  if (opts.authConfig && opts.authConfig.sessionEnabled) {
2418
2480
  const cookieName = opts.authConfig.cookieName;
2419
- authInit = `
2420
- // �"��"� Auth Session Init �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2421
-
2422
- function __parseCookies(header) {
2423
- const map = {};
2424
- if (!header) return map;
2425
- for (const pair of header.split(';')) {
2426
- const eq = pair.indexOf('=');
2427
- if (eq === -1) continue;
2428
- map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
2429
- }
2430
- return map;
2431
- }
2432
-
2433
- function __initAuth(request) {
2434
- const cookies = __parseCookies(request.headers.get('cookie'));
2435
- __setLocal('session', null);
2436
- __setLocal('user', null);
2437
- __setLocal('auth', {
2438
- cookies,
2439
- sessionCookie: cookies['${cookieName}'] || null,
2440
- cookieName: '${cookieName}',
2441
- });
2442
- }
2481
+ authInit = `
2482
+ // �"��"� Auth Session Init �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2483
+
2484
+ function __parseCookies(header) {
2485
+ const map = {};
2486
+ if (!header) return map;
2487
+ for (const pair of header.split(';')) {
2488
+ const eq = pair.indexOf('=');
2489
+ if (eq === -1) continue;
2490
+ map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
2491
+ }
2492
+ return map;
2493
+ }
2494
+
2495
+ function __initAuth(request) {
2496
+ const cookies = __parseCookies(request.headers.get('cookie'));
2497
+ __setLocal('session', null);
2498
+ __setLocal('user', null);
2499
+ __setLocal('auth', {
2500
+ cookies,
2501
+ sessionCookie: cookies['${cookieName}'] || null,
2502
+ cookieName: '${cookieName}',
2503
+ });
2504
+ }
2443
2505
  `;
2444
2506
  }
2445
2507
  const workerImport = `import { WorkerEntrypoint, env as __env } from 'cloudflare:workers';`;
@@ -2467,38 +2529,38 @@ function __initAuth(request) {
2467
2529
  `import { kuratchiORM } from '@kuratchi/orm';`,
2468
2530
  ...schemaImports,
2469
2531
  ].join('\n');
2470
- migrationInit = `
2471
- // �"��"� ORM Auto-Migration �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2472
-
2473
- let __migrated = false;
2474
- const __ormDatabases = [
2475
- ${migrateEntries.join(',\n')}
2476
- ];
2477
-
2478
- async function __runMigrations() {
2479
- if (__migrated) return;
2480
- __migrated = true;
2481
- for (const db of __ormDatabases) {
2482
- const binding = __env[db.binding];
2483
- if (!binding) continue;
2484
- try {
2485
- const executor = (sql, params) => {
2486
- let stmt = binding.prepare(sql);
2487
- if (params?.length) stmt = stmt.bind(...params);
2488
- return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
2489
- };
2490
- const result = await runMigrations({ execute: executor, schema: db.schema });
2491
- if (result.applied) {
2492
- console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
2493
- }
2494
- if (result.warnings.length) {
2495
- result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
2496
- }
2497
- } catch (err) {
2498
- console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
2499
- }
2500
- }
2501
- }
2532
+ migrationInit = `
2533
+ // �"��"� ORM Auto-Migration �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2534
+
2535
+ let __migrated = false;
2536
+ const __ormDatabases = [
2537
+ ${migrateEntries.join(',\n')}
2538
+ ];
2539
+
2540
+ async function __runMigrations() {
2541
+ if (__migrated) return;
2542
+ __migrated = true;
2543
+ for (const db of __ormDatabases) {
2544
+ const binding = __env[db.binding];
2545
+ if (!binding) continue;
2546
+ try {
2547
+ const executor = (sql, params) => {
2548
+ let stmt = binding.prepare(sql);
2549
+ if (params?.length) stmt = stmt.bind(...params);
2550
+ return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
2551
+ };
2552
+ const result = await runMigrations({ execute: executor, schema: db.schema });
2553
+ if (result.applied) {
2554
+ console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
2555
+ }
2556
+ if (result.warnings.length) {
2557
+ result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
2558
+ }
2559
+ } catch (err) {
2560
+ console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
2561
+ }
2562
+ }
2563
+ }
2502
2564
  `;
2503
2565
  }
2504
2566
  }
@@ -2553,12 +2615,12 @@ async function __runMigrations() {
2553
2615
  initLines.push(` if (__kuratchiConfig.auth?.organizations) __configOrg(__kuratchiConfig.auth.organizations);`);
2554
2616
  }
2555
2617
  authPluginImports = imports.join('\n');
2556
- authPluginInit = `
2557
- // �"��"� Auth Plugin Init �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2558
-
2559
- function __initAuthPlugins() {
2560
- ${initLines.join('\n')}
2561
- }
2618
+ authPluginInit = `
2619
+ // �"��"� Auth Plugin Init �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2620
+
2621
+ function __initAuthPlugins() {
2622
+ ${initLines.join('\n')}
2623
+ }
2562
2624
  `;
2563
2625
  }
2564
2626
  // �"��"� Durable Object class generation �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
@@ -2602,20 +2664,16 @@ ${initLines.join('\n')}
2602
2664
  list.push(h);
2603
2665
  handlersByBinding.set(h.binding, list);
2604
2666
  }
2605
- // Import handler files + schema for each DO
2667
+ // Import handler files + schema for each DO (class mode only)
2606
2668
  for (const doEntry of opts.doConfig) {
2607
2669
  const handlers = handlersByBinding.get(doEntry.binding) ?? [];
2608
2670
  const ormDb = opts.ormDatabases.find(d => d.binding === doEntry.binding);
2609
- const fnHandlers = handlers.filter((h) => h.mode === 'function');
2610
- const initHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onInit'));
2611
- const alarmHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onAlarm'));
2612
- const messageHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onMessage'));
2613
2671
  // Import schema (paths are relative to project root; prefix ../ since we're in .kuratchi/)
2614
2672
  if (ormDb) {
2615
2673
  const schemaPath = ormDb.schemaImportPath.replace(/^\.\//, '../');
2616
2674
  doImportLines.push(`import { ${ormDb.schemaExportName} as __doSchema_${doEntry.binding} } from '${schemaPath}';`);
2617
2675
  }
2618
- // Import handler classes
2676
+ // Import handler classes (class mode only - extends DurableObject)
2619
2677
  for (const h of handlers) {
2620
2678
  let handlerImportPath = path
2621
2679
  .relative(path.join(opts.projectDir, '.kuratchi'), h.absPath)
@@ -2624,27 +2682,19 @@ ${initLines.join('\n')}
2624
2682
  if (!handlerImportPath.startsWith('.'))
2625
2683
  handlerImportPath = './' + handlerImportPath;
2626
2684
  const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2627
- if (h.mode === 'class') {
2628
- doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
2629
- }
2630
- else {
2631
- doImportLines.push(`import * as ${handlerVar} from '${handlerImportPath}';`);
2632
- }
2685
+ doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
2633
2686
  }
2634
- // Generate DO class
2635
- doClassLines.push(`export class ${doEntry.className} extends __DO {`);
2636
- doClassLines.push(` constructor(ctx, env) {`);
2637
- doClassLines.push(` super(ctx, env);`);
2687
+ // Generate DO class that extends the user's class (for ORM integration)
2688
+ // If no ORM, we just re-export the user's class directly
2638
2689
  if (ormDb) {
2690
+ const handler = handlers[0];
2691
+ const handlerVar = handler ? `__handler_${toSafeIdentifier(handler.fileName)}` : '__DO';
2692
+ const baseClass = handler ? handlerVar : '__DO';
2693
+ doClassLines.push(`export class ${doEntry.className} extends ${baseClass} {`);
2694
+ doClassLines.push(` constructor(ctx, env) {`);
2695
+ doClassLines.push(` super(ctx, env);`);
2639
2696
  doClassLines.push(` this.db = __initDO(ctx.storage.sql, __doSchema_${doEntry.binding});`);
2640
- }
2641
- for (const h of initHandlers) {
2642
- const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2643
- doClassLines.push(` __setDoContext(this);`);
2644
- doClassLines.push(` Promise.resolve(${handlerVar}.onInit.call(this)).catch((err) => console.error('[kuratchi] DO onInit failed:', err?.message || err));`);
2645
- }
2646
- doClassLines.push(` }`);
2647
- if (ormDb) {
2697
+ doClassLines.push(` }`);
2648
2698
  doClassLines.push(` async __kuratchiLogActivity(payload) {`);
2649
2699
  doClassLines.push(` const now = new Date().toISOString();`);
2650
2700
  doClassLines.push(` try {`);
@@ -2680,42 +2730,18 @@ ${initLines.join('\n')}
2680
2730
  doClassLines.push(` if (Number.isFinite(limit) && limit > 0) return rows.slice(0, Math.floor(limit));`);
2681
2731
  doClassLines.push(` return rows;`);
2682
2732
  doClassLines.push(` }`);
2733
+ doClassLines.push(`}`);
2683
2734
  }
2684
- // Function-mode lifecycle dispatchers
2685
- if (alarmHandlers.length > 0) {
2686
- doClassLines.push(` async alarm(...args) {`);
2687
- doClassLines.push(` __setDoContext(this);`);
2688
- for (const h of alarmHandlers) {
2689
- const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2690
- doClassLines.push(` await ${handlerVar}.onAlarm.call(this, ...args);`);
2691
- }
2692
- doClassLines.push(` }`);
2735
+ else if (handlers.length > 0) {
2736
+ // No ORM - just re-export the user's class directly
2737
+ const handler = handlers[0];
2738
+ const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
2739
+ doClassLines.push(`export { ${handlerVar} as ${doEntry.className} };`);
2693
2740
  }
2694
- if (messageHandlers.length > 0) {
2695
- doClassLines.push(` webSocketMessage(...args) {`);
2696
- doClassLines.push(` __setDoContext(this);`);
2697
- for (const h of messageHandlers) {
2698
- const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2699
- doClassLines.push(` ${handlerVar}.onMessage.call(this, ...args);`);
2700
- }
2701
- doClassLines.push(` }`);
2702
- }
2703
- doClassLines.push(`}`);
2704
- // Apply handler methods to prototype (outside class body)
2741
+ // Register class binding for RPC
2705
2742
  for (const h of handlers) {
2706
2743
  const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
2707
- if (h.mode === 'class') {
2708
- doClassLines.push(`for (const __k of Object.getOwnPropertyNames(${handlerVar}.prototype)) { if (__k !== 'constructor') ${doEntry.className}.prototype[__k] = function(...__a){ __setDoContext(this); return ${handlerVar}.prototype[__k].apply(this, __a.map(__decodeDoArg)); }; }`);
2709
- doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
2710
- }
2711
- else {
2712
- const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
2713
- for (const fn of h.exportedFunctions) {
2714
- if (lifecycle.has(fn))
2715
- continue;
2716
- doClassLines.push(`${doEntry.className}.prototype[${JSON.stringify(fn)}] = function(...__a){ __setDoContext(this); return ${handlerVar}.${fn}.apply(this, __a.map(__decodeDoArg)); };`);
2717
- }
2718
- }
2744
+ doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
2719
2745
  }
2720
2746
  // Register stub resolver
2721
2747
  if (doEntry.stubId) {
@@ -2734,412 +2760,483 @@ ${initLines.join('\n')}
2734
2760
  }
2735
2761
  }
2736
2762
  doImports = doImportLines.join('\n');
2737
- doClassCode = `\n// �"��"� Durable Object Classes (generated) �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�\n\n` + doClassLines.join('\n') + '\n';
2763
+ doClassCode = `\n// ── Durable Object Classes (generated) ─────────────────────────\n\n` + doClassLines.join('\n') + '\n';
2738
2764
  doResolverInit = `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`;
2739
2765
  }
2740
- return `// Generated by KuratchiJS compiler �" do not edit.
2741
- ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
2742
- ${workerImport}
2743
- ${contextImport}
2744
- ${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
2745
-
2746
- // �"��"� Assets �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2747
-
2748
- const __assets = {
2749
- ${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')}
2750
- };
2751
-
2752
- // �"��"� Router �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2753
-
2754
- const __staticRoutes = new Map(); // exact path �' index (O(1) lookup)
2755
- const __dynamicRoutes = []; // regex-based routes (params/wildcards)
2756
-
2757
- function __addRoute(pattern, index) {
2758
- if (!pattern.includes(':') && !pattern.includes('*')) {
2759
- // Static route �" direct Map lookup, no regex needed
2760
- __staticRoutes.set(pattern, index);
2761
- } else {
2762
- // Dynamic route �" build regex for param extraction
2763
- const paramNames = [];
2764
- let regexStr = pattern
2765
- .replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
2766
- .replace(/:(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>[^/]+)'; });
2767
- __dynamicRoutes.push({ regex: new RegExp('^' + regexStr + '$'), paramNames, index });
2768
- }
2769
- }
2770
-
2771
- function __match(pathname) {
2772
- const normalized = pathname === '/' ? '/' : pathname.replace(/\\/$/, '');
2773
- // Fast path: static routes (most common)
2774
- const staticIdx = __staticRoutes.get(normalized);
2775
- if (staticIdx !== undefined) return { params: {}, index: staticIdx };
2776
- // Slow path: dynamic routes with params
2777
- for (const route of __dynamicRoutes) {
2778
- const m = normalized.match(route.regex);
2779
- if (m) {
2780
- const params = {};
2781
- for (const name of route.paramNames) params[name] = m.groups?.[name] ?? '';
2782
- return { params, index: route.index };
2783
- }
2784
- }
2785
- return null;
2786
- }
2787
-
2788
- // �"��"� Layout �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2789
-
2790
- ${layoutBlock}
2791
-
2792
- ${layoutActionsBlock}
2793
-
2794
- // �"��"� Error pages �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2795
-
2796
- const __errorMessages = {
2797
- 400: 'Bad Request',
2798
- 401: 'Unauthorized',
2799
- 403: 'Forbidden',
2800
- 404: 'Not Found',
2801
- 405: 'Method Not Allowed',
2802
- 408: 'Request Timeout',
2803
- 429: 'Too Many Requests',
2804
- 500: 'Internal Server Error',
2805
- 502: 'Bad Gateway',
2806
- 503: 'Service Unavailable',
2807
- };
2808
-
2809
- // Built-in default error page �" clean, dark, minimal, centered
2810
- function __errorPage(status, detail) {
2811
- const title = __errorMessages[status] || 'Error';
2812
- 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>' : '';
2813
- return '<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:2rem">'
2814
- + '<div>'
2815
- + '<p style="font-size:5rem;font-weight:700;margin:0;color:#333;line-height:1">' + status + '</p>'
2816
- + '<p style="font-size:1rem;color:#555;margin:0.5rem 0 0;letter-spacing:0.05em">' + __esc(title) + '</p>'
2817
- + detailHtml
2818
- + '</div>'
2819
- + '</div>';
2820
- }
2821
-
2822
- ${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}
2823
- // Dispatch: use custom override if it exists, otherwise built-in default
2824
- const __customErrors = {${Array.from(opts.compiledErrorPages.keys()).map(s => ` ${s}: __error_${s}`).join(',')} };
2825
-
2826
- function __error(status, detail) {
2827
- if (__customErrors[status]) return __customErrors[status](detail);
2828
- return __errorPage(status, detail);
2829
- }
2830
-
2831
- ${opts.compiledComponents.length > 0 ? '// �"��"� Components �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
2832
- // �"��"� Route definitions �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2833
-
2834
- const routes = [
2835
- ${opts.compiledRoutes.join(',\n')}
2836
- ];
2837
-
2838
- for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
2839
-
2840
- // �"��"� Response helpers �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2841
-
2842
- const __defaultSecHeaders = {
2843
- 'X-Content-Type-Options': 'nosniff',
2844
- 'X-Frame-Options': 'DENY',
2845
- 'Referrer-Policy': 'strict-origin-when-cross-origin',
2846
- };
2847
-
2848
- function __secHeaders(response) {
2849
- for (const [k, v] of Object.entries(__defaultSecHeaders)) {
2850
- if (!response.headers.has(k)) response.headers.set(k, v);
2851
- }
2852
- return response;
2853
- }
2854
-
2855
- function __attachCookies(response) {
2856
- const cookies = __getLocals().__setCookieHeaders;
2857
- if (cookies && cookies.length > 0) {
2858
- const newResponse = new Response(response.body, response);
2859
- for (const h of cookies) newResponse.headers.append('Set-Cookie', h);
2860
- return __secHeaders(newResponse);
2861
- }
2862
- return __secHeaders(response);
2863
- }
2864
-
2865
- function __isSameOrigin(request, url) {
2866
- const fetchSite = request.headers.get('sec-fetch-site');
2867
- if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
2868
- return false;
2869
- }
2870
- const origin = request.headers.get('origin');
2871
- if (!origin) return true;
2872
- try { return new URL(origin).origin === url.origin; } catch { return false; }
2873
- }
2874
-
2875
- ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
2876
- let html = route.render(data);
2877
- const headMatch = html.match(/<head>([\\s\\S]*?)<\\/head>/);
2878
- if (headMatch) {
2879
- html = html.replace(headMatch[0], '');
2880
- const layoutHtml = ${opts.isLayoutAsync ? 'await ' : ''}__layout(html);
2881
- return __attachCookies(new Response(layoutHtml.replace('</head>', headMatch[1] + '</head>'), {
2882
- headers: { 'content-type': 'text/html; charset=utf-8' }
2883
- }));
2884
- }
2885
- return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
2886
- }
2887
-
2888
- const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
2889
- const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
2890
-
2891
- async function __runRuntimeRequest(ctx, next) {
2892
- let idx = -1;
2893
- async function __dispatch(i) {
2894
- if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
2895
- idx = i;
2896
- const entry = __runtimeEntries[i];
2897
- if (!entry) return next();
2898
- const [, step] = entry;
2899
- if (typeof step.request !== 'function') return __dispatch(i + 1);
2900
- return await step.request(ctx, () => __dispatch(i + 1));
2901
- }
2902
- return __dispatch(0);
2903
- }
2904
-
2905
- async function __runRuntimeRoute(ctx, next) {
2906
- let idx = -1;
2907
- async function __dispatch(i) {
2908
- if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
2909
- idx = i;
2910
- const entry = __runtimeEntries[i];
2911
- if (!entry) return next();
2912
- const [, step] = entry;
2913
- if (typeof step.route !== 'function') return __dispatch(i + 1);
2914
- return await step.route(ctx, () => __dispatch(i + 1));
2915
- }
2916
- return __dispatch(0);
2917
- }
2918
-
2919
- async function __runRuntimeResponse(ctx, response) {
2920
- let out = response;
2921
- for (const [, step] of __runtimeEntries) {
2922
- if (typeof step.response !== 'function') continue;
2923
- out = await step.response(ctx, out);
2924
- if (!(out instanceof Response)) {
2925
- throw new Error('[kuratchi runtime] response handlers must return a Response');
2926
- }
2927
- }
2928
- return out;
2929
- }
2930
-
2931
- async function __runRuntimeError(ctx, error) {
2932
- for (const [name, step] of __runtimeEntries) {
2933
- if (typeof step.error !== 'function') continue;
2934
- try {
2935
- const handled = await step.error(ctx, error);
2936
- if (handled instanceof Response) return handled;
2937
- } catch (hookErr) {
2938
- console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
2939
- }
2940
- }
2941
- return null;
2942
- }
2943
-
2944
- // �"��"� Exported Worker entrypoint �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2945
-
2946
- export default class extends WorkerEntrypoint {
2947
- async fetch(request) {
2948
- __setRequestContext(this.ctx, request, __env);
2949
- ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
2950
- const __runtimeCtx = {
2951
- request,
2952
- env: __env,
2953
- ctx: this.ctx,
2954
- url: new URL(request.url),
2955
- params: {},
2956
- locals: __getLocals(),
2957
- };
2958
-
2959
- const __coreFetch = async () => {
2960
- const request = __runtimeCtx.request;
2961
- const url = __runtimeCtx.url;
2962
- ${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' : ''}
2963
-
2964
- // Serve static assets from src/assets/
2965
- if (url.pathname.startsWith('${opts.assetsPrefix}')) {
2966
- const name = url.pathname.slice('${opts.assetsPrefix}'.length);
2967
- const asset = __assets[name];
2968
- if (asset) {
2969
- if (request.headers.get('if-none-match') === asset.etag) {
2970
- return new Response(null, { status: 304 });
2971
- }
2972
- return new Response(asset.content, {
2973
- headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2974
- });
2975
- }
2976
- return __secHeaders(new Response('Not Found', { status: 404 }));
2977
- }
2978
-
2979
- const match = __match(url.pathname);
2980
-
2981
- if (!match) {
2982
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2983
- }
2984
-
2985
- __runtimeCtx.params = match.params;
2986
- const route = routes[match.index];
2987
- __setLocal('params', match.params);
2988
-
2989
- // API route: dispatch to method handler
2990
- if (route.__api) {
2991
- const method = request.method;
2992
- if (method === 'OPTIONS') {
2993
- const handler = route['OPTIONS'];
2994
- if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
2995
- const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
2996
- return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
2997
- }
2998
- const handler = route[method];
2999
- if (typeof handler !== 'function') {
3000
- const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
3001
- return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
3002
- }
3003
- return __secHeaders(await handler(__runtimeCtx));
3004
- }
3005
-
3006
- const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
3007
- const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
3008
- let __qArgs = [];
3009
- try {
3010
- const __parsed = JSON.parse(__qArgsRaw);
3011
- __qArgs = Array.isArray(__parsed) ? __parsed : [];
3012
- } catch {}
3013
- __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
3014
- if (!__getLocals().__breadcrumbs) {
3015
- __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
3016
- }
3017
-
3018
- // RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
3019
- const __rpcName = url.searchParams.get('_rpc');
3020
- if (request.method === 'GET' && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
3021
- if (request.headers.get('x-kuratchi-rpc') !== '1') {
3022
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
3023
- status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3024
- }));
3025
- }
3026
- try {
3027
- const __rpcArgsStr = url.searchParams.get('_args');
3028
- let __rpcArgs = [];
3029
- if (__rpcArgsStr) {
3030
- const __parsed = JSON.parse(__rpcArgsStr);
3031
- __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
3032
- }
3033
- const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
3034
- return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
3035
- headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3036
- }));
3037
- } catch (err) {
3038
- console.error('[kuratchi] RPC error:', err);
3039
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
3040
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
3041
- status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3042
- }));
3043
- }
3044
- }
3045
-
3046
- // Form action: POST with hidden _action field in form body
3047
- if (request.method === 'POST') {
3048
- if (!__isSameOrigin(request, url)) {
3049
- return __secHeaders(new Response('Forbidden', { status: 403 }));
3050
- }
3051
- const formData = await request.formData();
3052
- const actionName = formData.get('_action');
3053
- const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
3054
- || (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
3055
- if (actionName && __actionFn) {
3056
- // Check if this is a fetch-based action call (onclick) with JSON args
3057
- const argsStr = formData.get('_args');
3058
- const isFetchAction = argsStr !== null;
3059
- try {
3060
- if (isFetchAction) {
3061
- const __parsed = JSON.parse(argsStr);
3062
- const args = Array.isArray(__parsed) ? __parsed : [];
3063
- await __actionFn(...args);
3064
- } else {
3065
- await __actionFn(formData);
3066
- }
3067
- } catch (err) {
3068
- if (err && err.isRedirectError) {
3069
- const __redirectTo = err.location || url.pathname;
3070
- const __redirectStatus = Number(err.status) || 303;
3071
- if (isFetchAction) {
3072
- return __attachCookies(__secHeaders(new Response(JSON.stringify({ ok: true, redirectTo: __redirectTo, redirectStatus: __redirectStatus }), {
3073
- headers: { 'content-type': 'application/json' }
3074
- })));
3075
- }
3076
- return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
3077
- }
3078
- console.error('[kuratchi] Action error:', err);
3079
- if (isFetchAction) {
3080
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' && err && err.message ? err.message : 'Internal Server Error';
3081
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
3082
- status: 500, headers: { 'content-type': 'application/json' }
3083
- }));
3084
- }
3085
- const __loaded = route.load ? await route.load(match.params) : {};
3086
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
3087
- data.params = match.params;
3088
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
3089
- const __allActions = Object.assign({}, route.actions, __layoutActions || {});
3090
- Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
3091
- const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
3092
- data[actionName] = { error: __errMsg, loading: false, success: false };
3093
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
3094
- }
3095
- // Fetch-based actions return lightweight JSON (no page re-render)
3096
- if (isFetchAction) {
3097
- return __attachCookies(new Response(JSON.stringify({ ok: true }), {
3098
- headers: { 'content-type': 'application/json' }
3099
- }));
3100
- }
3101
- // POST-Redirect-GET: redirect to custom target or back to same URL
3102
- const __locals = __getLocals();
3103
- const redirectTo = __locals.__redirectTo || url.pathname;
3104
- const redirectStatus = Number(__locals.__redirectStatus) || 303;
3105
- return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
3106
- }
3107
- }
3108
-
3109
- // GET (or unmatched POST): load + render
3110
- try {
3111
- const __loaded = route.load ? await route.load(match.params) : {};
3112
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
3113
- data.params = match.params;
3114
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
3115
- const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
3116
- Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
3117
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
3118
- } catch (err) {
3119
- if (err && err.isRedirectError) {
3120
- const __redirectTo = err.location || url.pathname;
3121
- const __redirectStatus = Number(err.status) || 303;
3122
- return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
3123
- }
3124
- console.error('[kuratchi] Route load/render error:', err);
3125
- const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
3126
- const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
3127
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
3128
- }
3129
- };
3130
-
3131
- try {
3132
- const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
3133
- return __runRuntimeRoute(__runtimeCtx, __coreFetch);
3134
- });
3135
- return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
3136
- } catch (err) {
3137
- const __handled = await __runRuntimeError(__runtimeCtx, err);
3138
- if (__handled) return __secHeaders(__handled);
3139
- throw err;
3140
- }
3141
- }
3142
- }
2766
+ // Generate workflow status RPC handlers for auto-discovered workflows
2767
+ // Naming: migration.workflow.ts -> migrationWorkflowStatus(instanceId)
2768
+ let workflowStatusRpc = '';
2769
+ if (opts.workflowConfig.length > 0) {
2770
+ const rpcLines = [];
2771
+ rpcLines.push(`\n// ── Workflow Status RPCs (auto-generated) ─────────────────────`);
2772
+ rpcLines.push(`const __workflowStatusRpc = {`);
2773
+ for (const wf of opts.workflowConfig) {
2774
+ // file: src/server/migration.workflow.ts -> camelCase RPC name: migrationWorkflowStatus
2775
+ const baseName = wf.file.split('/').pop()?.replace(/\.workflow\.ts$/, '') || '';
2776
+ const camelName = baseName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2777
+ const rpcName = `${camelName}WorkflowStatus`;
2778
+ rpcLines.push(` '${rpcName}': async (instanceId) => {`);
2779
+ rpcLines.push(` if (!instanceId) return { status: 'unknown', error: { name: 'Error', message: 'Missing instanceId' } };`);
2780
+ rpcLines.push(` try {`);
2781
+ rpcLines.push(` const instance = await __env.${wf.binding}.get(instanceId);`);
2782
+ rpcLines.push(` return await instance.status();`);
2783
+ rpcLines.push(` } catch (err) {`);
2784
+ rpcLines.push(` return { status: 'errored', error: { name: err?.name || 'Error', message: err?.message || 'Unknown error' } };`);
2785
+ rpcLines.push(` }`);
2786
+ rpcLines.push(` },`);
2787
+ }
2788
+ rpcLines.push(`};`);
2789
+ workflowStatusRpc = rpcLines.join('\n');
2790
+ }
2791
+ return `// Generated by KuratchiJS compiler �" do not edit.
2792
+ ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
2793
+ ${workerImport}
2794
+ ${contextImport}
2795
+ ${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
2796
+ ${workflowStatusRpc}
2797
+
2798
+ // ── Assets ─────────────────────────────────────────────────────
2799
+
2800
+ const __assets = {
2801
+ ${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')}
2802
+ };
2803
+
2804
+ // �"��"� Router �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2805
+
2806
+ const __staticRoutes = new Map(); // exact path �' index (O(1) lookup)
2807
+ const __dynamicRoutes = []; // regex-based routes (params/wildcards)
2808
+
2809
+ function __addRoute(pattern, index) {
2810
+ if (!pattern.includes(':') && !pattern.includes('*')) {
2811
+ // Static route �" direct Map lookup, no regex needed
2812
+ __staticRoutes.set(pattern, index);
2813
+ } else {
2814
+ // Dynamic route �" build regex for param extraction
2815
+ const paramNames = [];
2816
+ let regexStr = pattern
2817
+ .replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
2818
+ .replace(/:(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>[^/]+)'; });
2819
+ __dynamicRoutes.push({ regex: new RegExp('^' + regexStr + '$'), paramNames, index });
2820
+ }
2821
+ }
2822
+
2823
+ function __match(pathname) {
2824
+ const normalized = pathname === '/' ? '/' : pathname.replace(/\\/$/, '');
2825
+ // Fast path: static routes (most common)
2826
+ const staticIdx = __staticRoutes.get(normalized);
2827
+ if (staticIdx !== undefined) return { params: {}, index: staticIdx };
2828
+ // Slow path: dynamic routes with params
2829
+ for (const route of __dynamicRoutes) {
2830
+ const m = normalized.match(route.regex);
2831
+ if (m) {
2832
+ const params = {};
2833
+ for (const name of route.paramNames) params[name] = m.groups?.[name] ?? '';
2834
+ return { params, index: route.index };
2835
+ }
2836
+ }
2837
+ return null;
2838
+ }
2839
+
2840
+ // �"��"� Layout �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2841
+
2842
+ ${layoutBlock}
2843
+
2844
+ ${layoutActionsBlock}
2845
+
2846
+ // �"��"� Error pages �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2847
+
2848
+ const __errorMessages = {
2849
+ 400: 'Bad Request',
2850
+ 401: 'Unauthorized',
2851
+ 403: 'Forbidden',
2852
+ 404: 'Not Found',
2853
+ 405: 'Method Not Allowed',
2854
+ 408: 'Request Timeout',
2855
+ 429: 'Too Many Requests',
2856
+ 500: 'Internal Server Error',
2857
+ 502: 'Bad Gateway',
2858
+ 503: 'Service Unavailable',
2859
+ };
2860
+
2861
+ // Built-in default error page �" clean, dark, minimal, centered
2862
+ function __errorPage(status, detail) {
2863
+ const title = __errorMessages[status] || 'Error';
2864
+ 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>' : '';
2865
+ return '<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:2rem">'
2866
+ + '<div>'
2867
+ + '<p style="font-size:5rem;font-weight:700;margin:0;color:#333;line-height:1">' + status + '</p>'
2868
+ + '<p style="font-size:1rem;color:#555;margin:0.5rem 0 0;letter-spacing:0.05em">' + __esc(title) + '</p>'
2869
+ + detailHtml
2870
+ + '</div>'
2871
+ + '</div>';
2872
+ }
2873
+
2874
+ ${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}
2875
+ // Dispatch: use custom override if it exists, otherwise built-in default
2876
+ const __customErrors = {${Array.from(opts.compiledErrorPages.keys()).map(s => ` ${s}: __error_${s}`).join(',')} };
2877
+
2878
+ function __error(status, detail) {
2879
+ if (__customErrors[status]) return __customErrors[status](detail);
2880
+ return __errorPage(status, detail);
2881
+ }
2882
+
2883
+ ${opts.compiledComponents.length > 0 ? '// �"��"� Components �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
2884
+ // �"��"� Route definitions �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2885
+
2886
+ const routes = [
2887
+ ${opts.compiledRoutes.join(',\n')}
2888
+ ];
2889
+
2890
+ for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
2891
+
2892
+ // �"��"� Response helpers �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
2893
+
2894
+ const __defaultSecHeaders = {
2895
+ 'X-Content-Type-Options': 'nosniff',
2896
+ 'X-Frame-Options': 'DENY',
2897
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
2898
+ };
2899
+
2900
+ function __secHeaders(response) {
2901
+ for (const [k, v] of Object.entries(__defaultSecHeaders)) {
2902
+ if (!response.headers.has(k)) response.headers.set(k, v);
2903
+ }
2904
+ return response;
2905
+ }
2906
+
2907
+ function __attachCookies(response) {
2908
+ const cookies = __getLocals().__setCookieHeaders;
2909
+ if (cookies && cookies.length > 0) {
2910
+ const newResponse = new Response(response.body, response);
2911
+ for (const h of cookies) newResponse.headers.append('Set-Cookie', h);
2912
+ return __secHeaders(newResponse);
2913
+ }
2914
+ return __secHeaders(response);
2915
+ }
2916
+
2917
+ function __isSameOrigin(request, url) {
2918
+ const fetchSite = request.headers.get('sec-fetch-site');
2919
+ if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
2920
+ return false;
2921
+ }
2922
+ const origin = request.headers.get('origin');
2923
+ if (!origin) return true;
2924
+ try { return new URL(origin).origin === url.origin; } catch { return false; }
2925
+ }
2926
+
2927
+ // Extract fragment content by ID from rendered HTML
2928
+ function __extractFragment(html, fragmentId) {
2929
+ // Find the element with data-poll-id="fragmentId" and extract its innerHTML
2930
+ const escaped = fragmentId.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
2931
+ const openTagRegex = new RegExp('<([a-z][a-z0-9]*)\\\\s[^>]*data-poll-id="' + escaped + '"[^>]*>', 'i');
2932
+ const match = html.match(openTagRegex);
2933
+ if (!match) return null;
2934
+ const tagName = match[1];
2935
+ const startIdx = match.index + match[0].length;
2936
+ // Find matching closing tag (handle nesting)
2937
+ let depth = 1;
2938
+ let i = startIdx;
2939
+ const closeTag = '</' + tagName + '>';
2940
+ const openTag = '<' + tagName;
2941
+ while (i < html.length && depth > 0) {
2942
+ const nextClose = html.indexOf(closeTag, i);
2943
+ const nextOpen = html.indexOf(openTag, i);
2944
+ if (nextClose === -1) break;
2945
+ if (nextOpen !== -1 && nextOpen < nextClose) {
2946
+ depth++;
2947
+ i = nextOpen + openTag.length;
2948
+ } else {
2949
+ depth--;
2950
+ if (depth === 0) return html.slice(startIdx, nextClose);
2951
+ i = nextClose + closeTag.length;
2952
+ }
2953
+ }
2954
+ return null;
2955
+ }
2956
+
2957
+ ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data, fragmentId) {
2958
+ let html = route.render(data);
2959
+
2960
+ // Fragment request: return only the fragment's innerHTML
2961
+ if (fragmentId) {
2962
+ const fragment = __extractFragment(html, fragmentId);
2963
+ if (fragment !== null) {
2964
+ return new Response(fragment, { headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } });
2965
+ }
2966
+ return new Response('Fragment not found', { status: 404 });
2967
+ }
2968
+
2969
+ // Full page render
2970
+ const headMatch = html.match(/<head>([\\s\\S]*?)<\\/head>/);
2971
+ if (headMatch) {
2972
+ html = html.replace(headMatch[0], '');
2973
+ const layoutHtml = ${opts.isLayoutAsync ? 'await ' : ''}__layout(html);
2974
+ return __attachCookies(new Response(layoutHtml.replace('</head>', headMatch[1] + '</head>'), {
2975
+ headers: { 'content-type': 'text/html; charset=utf-8' }
2976
+ }));
2977
+ }
2978
+ return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
2979
+ }
2980
+
2981
+ const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
2982
+ const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
2983
+
2984
+ async function __runRuntimeRequest(ctx, next) {
2985
+ let idx = -1;
2986
+ async function __dispatch(i) {
2987
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
2988
+ idx = i;
2989
+ const entry = __runtimeEntries[i];
2990
+ if (!entry) return next();
2991
+ const [, step] = entry;
2992
+ if (typeof step.request !== 'function') return __dispatch(i + 1);
2993
+ return await step.request(ctx, () => __dispatch(i + 1));
2994
+ }
2995
+ return __dispatch(0);
2996
+ }
2997
+
2998
+ async function __runRuntimeRoute(ctx, next) {
2999
+ let idx = -1;
3000
+ async function __dispatch(i) {
3001
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
3002
+ idx = i;
3003
+ const entry = __runtimeEntries[i];
3004
+ if (!entry) return next();
3005
+ const [, step] = entry;
3006
+ if (typeof step.route !== 'function') return __dispatch(i + 1);
3007
+ return await step.route(ctx, () => __dispatch(i + 1));
3008
+ }
3009
+ return __dispatch(0);
3010
+ }
3011
+
3012
+ async function __runRuntimeResponse(ctx, response) {
3013
+ let out = response;
3014
+ for (const [, step] of __runtimeEntries) {
3015
+ if (typeof step.response !== 'function') continue;
3016
+ out = await step.response(ctx, out);
3017
+ if (!(out instanceof Response)) {
3018
+ throw new Error('[kuratchi runtime] response handlers must return a Response');
3019
+ }
3020
+ }
3021
+ return out;
3022
+ }
3023
+
3024
+ async function __runRuntimeError(ctx, error) {
3025
+ for (const [name, step] of __runtimeEntries) {
3026
+ if (typeof step.error !== 'function') continue;
3027
+ try {
3028
+ const handled = await step.error(ctx, error);
3029
+ if (handled instanceof Response) return handled;
3030
+ } catch (hookErr) {
3031
+ console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
3032
+ }
3033
+ }
3034
+ return null;
3035
+ }
3036
+
3037
+ // �"��"� Exported Worker entrypoint �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
3038
+
3039
+ export default class extends WorkerEntrypoint {
3040
+ async fetch(request) {
3041
+ __setRequestContext(this.ctx, request, __env);
3042
+ ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
3043
+ const __runtimeCtx = {
3044
+ request,
3045
+ env: __env,
3046
+ ctx: this.ctx,
3047
+ url: new URL(request.url),
3048
+ params: {},
3049
+ locals: __getLocals(),
3050
+ };
3051
+
3052
+ const __coreFetch = async () => {
3053
+ const request = __runtimeCtx.request;
3054
+ const url = __runtimeCtx.url;
3055
+ const __fragmentId = request.headers.get('x-kuratchi-fragment');
3056
+ ${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' : ''}
3057
+
3058
+ // Serve static assets from src/assets/
3059
+ if (url.pathname.startsWith('${opts.assetsPrefix}')) {
3060
+ const name = url.pathname.slice('${opts.assetsPrefix}'.length);
3061
+ const asset = __assets[name];
3062
+ if (asset) {
3063
+ if (request.headers.get('if-none-match') === asset.etag) {
3064
+ return new Response(null, { status: 304 });
3065
+ }
3066
+ return new Response(asset.content, {
3067
+ headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
3068
+ });
3069
+ }
3070
+ return __secHeaders(new Response('Not Found', { status: 404 }));
3071
+ }
3072
+
3073
+ const match = __match(url.pathname);
3074
+
3075
+ if (!match) {
3076
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
3077
+ }
3078
+
3079
+ __runtimeCtx.params = match.params;
3080
+ const route = routes[match.index];
3081
+ __setLocal('params', match.params);
3082
+
3083
+ // API route: dispatch to method handler
3084
+ if (route.__api) {
3085
+ const method = request.method;
3086
+ if (method === 'OPTIONS') {
3087
+ const handler = route['OPTIONS'];
3088
+ if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
3089
+ const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
3090
+ return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
3091
+ }
3092
+ const handler = route[method];
3093
+ if (typeof handler !== 'function') {
3094
+ const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
3095
+ return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
3096
+ }
3097
+ return __secHeaders(await handler(__runtimeCtx));
3098
+ }
3099
+
3100
+ const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
3101
+ const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
3102
+ let __qArgs = [];
3103
+ try {
3104
+ const __parsed = JSON.parse(__qArgsRaw);
3105
+ __qArgs = Array.isArray(__parsed) ? __parsed : [];
3106
+ } catch {}
3107
+ __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
3108
+ if (!__getLocals().__breadcrumbs) {
3109
+ __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
3110
+ }
3111
+
3112
+ // RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
3113
+ const __rpcName = url.searchParams.get('_rpc');
3114
+ const __hasRouteRpc = __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName);
3115
+ const __hasWorkflowRpc = __rpcName && typeof __workflowStatusRpc !== 'undefined' && Object.hasOwn(__workflowStatusRpc, __rpcName);
3116
+ if (request.method === 'GET' && __rpcName && (__hasRouteRpc || __hasWorkflowRpc)) {
3117
+ if (request.headers.get('x-kuratchi-rpc') !== '1') {
3118
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
3119
+ status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3120
+ }));
3121
+ }
3122
+ try {
3123
+ const __rpcArgsStr = url.searchParams.get('_args');
3124
+ let __rpcArgs = [];
3125
+ if (__rpcArgsStr) {
3126
+ const __parsed = JSON.parse(__rpcArgsStr);
3127
+ __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
3128
+ }
3129
+ const __rpcFn = __hasRouteRpc ? route.rpc[__rpcName] : __workflowStatusRpc[__rpcName];
3130
+ const __rpcResult = await __rpcFn(...__rpcArgs);
3131
+ return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
3132
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3133
+ }));
3134
+ } catch (err) {
3135
+ console.error('[kuratchi] RPC error:', err);
3136
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
3137
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
3138
+ status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
3139
+ }));
3140
+ }
3141
+ }
3142
+
3143
+ // Form action: POST with hidden _action field in form body
3144
+ if (request.method === 'POST') {
3145
+ if (!__isSameOrigin(request, url)) {
3146
+ return __secHeaders(new Response('Forbidden', { status: 403 }));
3147
+ }
3148
+ const formData = await request.formData();
3149
+ const actionName = formData.get('_action');
3150
+ const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
3151
+ || (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
3152
+ if (actionName && __actionFn) {
3153
+ // Check if this is a fetch-based action call (onclick) with JSON args
3154
+ const argsStr = formData.get('_args');
3155
+ const isFetchAction = argsStr !== null;
3156
+ try {
3157
+ if (isFetchAction) {
3158
+ const __parsed = JSON.parse(argsStr);
3159
+ const args = Array.isArray(__parsed) ? __parsed : [];
3160
+ await __actionFn(...args);
3161
+ } else {
3162
+ await __actionFn(formData);
3163
+ }
3164
+ } catch (err) {
3165
+ if (err && err.isRedirectError) {
3166
+ const __redirectTo = err.location || url.pathname;
3167
+ const __redirectStatus = Number(err.status) || 303;
3168
+ if (isFetchAction) {
3169
+ return __attachCookies(__secHeaders(new Response(JSON.stringify({ ok: true, redirectTo: __redirectTo, redirectStatus: __redirectStatus }), {
3170
+ headers: { 'content-type': 'application/json' }
3171
+ })));
3172
+ }
3173
+ return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
3174
+ }
3175
+ console.error('[kuratchi] Action error:', err);
3176
+ if (isFetchAction) {
3177
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' && err && err.message ? err.message : 'Internal Server Error';
3178
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
3179
+ status: 500, headers: { 'content-type': 'application/json' }
3180
+ }));
3181
+ }
3182
+ const __loaded = route.load ? await route.load(match.params) : {};
3183
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
3184
+ data.params = match.params;
3185
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
3186
+ const __allActions = Object.assign({}, route.actions, __layoutActions || {});
3187
+ Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
3188
+ const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
3189
+ data[actionName] = { error: __errMsg, loading: false, success: false };
3190
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data, __fragmentId);
3191
+ }
3192
+ // Fetch-based actions return lightweight JSON (no page re-render)
3193
+ if (isFetchAction) {
3194
+ return __attachCookies(new Response(JSON.stringify({ ok: true }), {
3195
+ headers: { 'content-type': 'application/json' }
3196
+ }));
3197
+ }
3198
+ // POST-Redirect-GET: redirect to custom target or back to same URL
3199
+ const __locals = __getLocals();
3200
+ const redirectTo = __locals.__redirectTo || url.pathname;
3201
+ const redirectStatus = Number(__locals.__redirectStatus) || 303;
3202
+ return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
3203
+ }
3204
+ }
3205
+
3206
+ // GET (or unmatched POST): load + render
3207
+ try {
3208
+ const __loaded = route.load ? await route.load(match.params) : {};
3209
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
3210
+ data.params = match.params;
3211
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
3212
+ const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
3213
+ Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
3214
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data, __fragmentId);
3215
+ } catch (err) {
3216
+ if (err && err.isRedirectError) {
3217
+ const __redirectTo = err.location || url.pathname;
3218
+ const __redirectStatus = Number(err.status) || 303;
3219
+ return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
3220
+ }
3221
+ console.error('[kuratchi] Route load/render error:', err);
3222
+ const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
3223
+ const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
3224
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
3225
+ }
3226
+ };
3227
+
3228
+ try {
3229
+ const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
3230
+ return __runRuntimeRoute(__runtimeCtx, __coreFetch);
3231
+ });
3232
+ return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
3233
+ } catch (err) {
3234
+ const __handled = await __runRuntimeError(__runtimeCtx, err);
3235
+ if (__handled) return __secHeaders(__handled);
3236
+ throw err;
3237
+ }
3238
+ }
3239
+ }
3143
3240
  `;
3144
3241
  }
3145
3242
  function resolveRuntimeImportPath(projectDir) {
@@ -3160,3 +3257,216 @@ function toWorkerImportPath(projectDir, outDir, filePath) {
3160
3257
  rel = `./${rel}`;
3161
3258
  return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
3162
3259
  }
3260
+ /**
3261
+ * Auto-sync wrangler.jsonc with workflow/container/DO config from kuratchi.config.ts.
3262
+ * This eliminates the need to manually duplicate config between kuratchi.config.ts and wrangler.jsonc.
3263
+ *
3264
+ * The function:
3265
+ * 1. Reads existing wrangler.jsonc (or wrangler.json)
3266
+ * 2. Updates/adds workflow entries based on kuratchi.config.ts
3267
+ * 3. Preserves all other wrangler config (bindings, vars, etc.)
3268
+ * 4. Writes back only if changed
3269
+ */
3270
+ function syncWranglerConfig(projectDir, config) {
3271
+ // Find wrangler config file (prefer .jsonc, fall back to .json)
3272
+ const jsoncPath = path.join(projectDir, 'wrangler.jsonc');
3273
+ const jsonPath = path.join(projectDir, 'wrangler.json');
3274
+ const tomlPath = path.join(projectDir, 'wrangler.toml');
3275
+ let configPath;
3276
+ let isJsonc = false;
3277
+ if (fs.existsSync(jsoncPath)) {
3278
+ configPath = jsoncPath;
3279
+ isJsonc = true;
3280
+ }
3281
+ else if (fs.existsSync(jsonPath)) {
3282
+ configPath = jsonPath;
3283
+ }
3284
+ else if (fs.existsSync(tomlPath)) {
3285
+ // TOML is not supported for auto-sync — user must migrate to JSON/JSONC
3286
+ console.log('[kuratchi] wrangler.toml detected. Auto-sync requires wrangler.jsonc. Skipping wrangler sync.');
3287
+ return;
3288
+ }
3289
+ else {
3290
+ // No wrangler config exists — create a minimal wrangler.jsonc
3291
+ console.log('[kuratchi] Creating wrangler.jsonc with workflow config...');
3292
+ configPath = jsoncPath;
3293
+ isJsonc = true;
3294
+ }
3295
+ // Read existing config (or start fresh)
3296
+ let rawContent = '';
3297
+ let wranglerConfig = {};
3298
+ if (fs.existsSync(configPath)) {
3299
+ rawContent = fs.readFileSync(configPath, 'utf-8');
3300
+ try {
3301
+ // Strip JSONC comments for parsing
3302
+ const jsonContent = stripJsonComments(rawContent);
3303
+ wranglerConfig = JSON.parse(jsonContent);
3304
+ }
3305
+ catch (err) {
3306
+ console.error(`[kuratchi] Failed to parse ${path.basename(configPath)}: ${err.message}`);
3307
+ console.error('[kuratchi] Skipping wrangler sync. Please fix the JSON syntax.');
3308
+ return;
3309
+ }
3310
+ }
3311
+ let changed = false;
3312
+ // Sync workflows
3313
+ if (config.workflows.length > 0) {
3314
+ const existingWorkflows = wranglerConfig.workflows || [];
3315
+ const existingByBinding = new Map(existingWorkflows.map(w => [w.binding, w]));
3316
+ for (const wf of config.workflows) {
3317
+ // Convert SCREAMING_SNAKE binding to kebab-case name
3318
+ const name = wf.binding.toLowerCase().replace(/_/g, '-');
3319
+ const entry = {
3320
+ name,
3321
+ binding: wf.binding,
3322
+ class_name: wf.className,
3323
+ };
3324
+ const existing = existingByBinding.get(wf.binding);
3325
+ if (!existing) {
3326
+ existingWorkflows.push(entry);
3327
+ changed = true;
3328
+ console.log(`[kuratchi] Added workflow "${wf.binding}" to wrangler config`);
3329
+ }
3330
+ else if (existing.class_name !== wf.className) {
3331
+ existing.class_name = wf.className;
3332
+ changed = true;
3333
+ console.log(`[kuratchi] Updated workflow "${wf.binding}" class_name to "${wf.className}"`);
3334
+ }
3335
+ }
3336
+ // Remove workflows that are no longer in kuratchi.config.ts
3337
+ const configBindings = new Set(config.workflows.map(w => w.binding));
3338
+ const filtered = existingWorkflows.filter(w => {
3339
+ if (!configBindings.has(w.binding)) {
3340
+ // Check if this was a kuratchi-managed workflow (has matching naming convention)
3341
+ const expectedName = w.binding.toLowerCase().replace(/_/g, '-');
3342
+ if (w.name === expectedName) {
3343
+ console.log(`[kuratchi] Removed workflow "${w.binding}" from wrangler config`);
3344
+ changed = true;
3345
+ return false;
3346
+ }
3347
+ }
3348
+ return true;
3349
+ });
3350
+ if (filtered.length !== existingWorkflows.length) {
3351
+ wranglerConfig.workflows = filtered;
3352
+ }
3353
+ else {
3354
+ wranglerConfig.workflows = existingWorkflows;
3355
+ }
3356
+ if (wranglerConfig.workflows.length === 0) {
3357
+ delete wranglerConfig.workflows;
3358
+ }
3359
+ }
3360
+ // Sync containers (similar pattern)
3361
+ if (config.containers.length > 0) {
3362
+ const existingContainers = wranglerConfig.containers || [];
3363
+ const existingByBinding = new Map(existingContainers.map(c => [c.binding, c]));
3364
+ for (const ct of config.containers) {
3365
+ const name = ct.binding.toLowerCase().replace(/_/g, '-');
3366
+ const entry = {
3367
+ name,
3368
+ binding: ct.binding,
3369
+ class_name: ct.className,
3370
+ };
3371
+ const existing = existingByBinding.get(ct.binding);
3372
+ if (!existing) {
3373
+ existingContainers.push(entry);
3374
+ changed = true;
3375
+ console.log(`[kuratchi] Added container "${ct.binding}" to wrangler config`);
3376
+ }
3377
+ else if (existing.class_name !== ct.className) {
3378
+ existing.class_name = ct.className;
3379
+ changed = true;
3380
+ console.log(`[kuratchi] Updated container "${ct.binding}" class_name to "${ct.className}"`);
3381
+ }
3382
+ }
3383
+ wranglerConfig.containers = existingContainers;
3384
+ if (wranglerConfig.containers.length === 0) {
3385
+ delete wranglerConfig.containers;
3386
+ }
3387
+ }
3388
+ // Sync durable_objects
3389
+ if (config.durableObjects.length > 0) {
3390
+ if (!wranglerConfig.durable_objects) {
3391
+ wranglerConfig.durable_objects = { bindings: [] };
3392
+ }
3393
+ const existingBindings = wranglerConfig.durable_objects.bindings || [];
3394
+ const existingByName = new Map(existingBindings.map(b => [b.name, b]));
3395
+ for (const doEntry of config.durableObjects) {
3396
+ const entry = {
3397
+ name: doEntry.binding,
3398
+ class_name: doEntry.className,
3399
+ };
3400
+ const existing = existingByName.get(doEntry.binding);
3401
+ if (!existing) {
3402
+ existingBindings.push(entry);
3403
+ changed = true;
3404
+ console.log(`[kuratchi] Added durable_object "${doEntry.binding}" to wrangler config`);
3405
+ }
3406
+ else if (existing.class_name !== doEntry.className) {
3407
+ existing.class_name = doEntry.className;
3408
+ changed = true;
3409
+ console.log(`[kuratchi] Updated durable_object "${doEntry.binding}" class_name to "${doEntry.className}"`);
3410
+ }
3411
+ }
3412
+ wranglerConfig.durable_objects.bindings = existingBindings;
3413
+ }
3414
+ if (!changed)
3415
+ return;
3416
+ // Write back with pretty formatting
3417
+ const newContent = JSON.stringify(wranglerConfig, null, '\t');
3418
+ writeIfChanged(configPath, newContent + '\n');
3419
+ }
3420
+ /**
3421
+ * Strip JSON comments (// and /* *\/) for parsing JSONC files.
3422
+ */
3423
+ function stripJsonComments(content) {
3424
+ let result = '';
3425
+ let i = 0;
3426
+ let inString = false;
3427
+ let stringChar = '';
3428
+ while (i < content.length) {
3429
+ const ch = content[i];
3430
+ const next = content[i + 1];
3431
+ // Handle string literals
3432
+ if (inString) {
3433
+ result += ch;
3434
+ if (ch === '\\' && i + 1 < content.length) {
3435
+ result += next;
3436
+ i += 2;
3437
+ continue;
3438
+ }
3439
+ if (ch === stringChar) {
3440
+ inString = false;
3441
+ }
3442
+ i++;
3443
+ continue;
3444
+ }
3445
+ // Start of string
3446
+ if (ch === '"' || ch === "'") {
3447
+ inString = true;
3448
+ stringChar = ch;
3449
+ result += ch;
3450
+ i++;
3451
+ continue;
3452
+ }
3453
+ // Line comment
3454
+ if (ch === '/' && next === '/') {
3455
+ // Skip until end of line
3456
+ while (i < content.length && content[i] !== '\n')
3457
+ i++;
3458
+ continue;
3459
+ }
3460
+ // Block comment
3461
+ if (ch === '/' && next === '*') {
3462
+ i += 2;
3463
+ while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/'))
3464
+ i++;
3465
+ i += 2; // Skip */
3466
+ continue;
3467
+ }
3468
+ result += ch;
3469
+ i++;
3470
+ }
3471
+ return result;
3472
+ }