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