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