@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.
- package/README.md +835 -691
- package/dist/compiler/index.js +1297 -987
- package/dist/compiler/template.js +82 -30
- package/dist/runtime/types.d.ts +0 -14
- package/package.json +1 -1
package/dist/compiler/index.js
CHANGED
|
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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
|
-
|
|
473
|
-
function
|
|
474
|
-
if(!
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
var
|
|
523
|
-
if(!
|
|
524
|
-
var
|
|
525
|
-
if(!
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
var
|
|
533
|
-
if(!
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
var
|
|
541
|
-
if(
|
|
542
|
-
|
|
543
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); };
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); };
|
|
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
|
-
//
|
|
735
|
-
const
|
|
736
|
-
const
|
|
737
|
-
|
|
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
|
-
*
|
|
2104
|
-
*
|
|
2105
|
-
*
|
|
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
|
|
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
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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
|
-
|
|
2134
|
-
const hasClass = /extends\s+
|
|
2135
|
-
if (!hasClass
|
|
2200
|
+
// Must extend DurableObject
|
|
2201
|
+
const hasClass = /extends\s+DurableObject\b/.test(source);
|
|
2202
|
+
if (!hasClass)
|
|
2136
2203
|
continue;
|
|
2137
|
-
// Extract class name
|
|
2138
|
-
const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+
|
|
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 (
|
|
2207
|
+
if (!className)
|
|
2141
2208
|
continue;
|
|
2142
|
-
//
|
|
2143
|
-
//
|
|
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
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
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:
|
|
2179
|
-
className
|
|
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
|
|
2191
|
-
const classIdx = source.search(new RegExp(`class\\s+${className}\\s+extends\\s+
|
|
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
|
-
*
|
|
2286
|
-
*
|
|
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
|
-
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
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
|
|
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 =
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
const asyncMethods =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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//
|
|
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
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
// �"
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
return
|
|
2929
|
-
}
|
|
2930
|
-
|
|
2931
|
-
async function
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
return
|
|
3041
|
-
|
|
3042
|
-
})
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
const
|
|
3104
|
-
const
|
|
3105
|
-
return
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
if (
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
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
|
+
}
|