@kuratchi/js 0.0.3 → 0.0.4
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 +206 -47
- package/dist/cli.js +59 -17
- package/dist/compiler/index.d.ts +2 -2
- package/dist/compiler/index.js +614 -254
- package/dist/compiler/parser.d.ts +8 -3
- package/dist/compiler/parser.js +29 -7
- package/dist/compiler/template.d.ts +3 -1
- package/dist/compiler/template.js +213 -14
- package/dist/create.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/runtime/action.d.ts +11 -0
- package/dist/runtime/action.js +14 -0
- package/dist/runtime/app.js +35 -8
- package/dist/runtime/context.d.ts +8 -2
- package/dist/runtime/context.js +31 -1
- package/dist/runtime/do.d.ts +6 -0
- package/dist/runtime/do.js +15 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/page-error.d.ts +16 -0
- package/dist/runtime/page-error.js +20 -0
- package/dist/runtime/types.d.ts +53 -33
- package/package.json +47 -45
package/dist/compiler/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import { parseFile } from './parser.js';
|
|
@@ -31,10 +31,36 @@ function compactInlineJs(source) {
|
|
|
31
31
|
.replace(/\s*([{}();,:])\s*/g, '$1')
|
|
32
32
|
.trim();
|
|
33
33
|
}
|
|
34
|
+
function parseNamedImportBindings(line) {
|
|
35
|
+
const namesMatch = line.match(/import\s*\{([^}]+)\}/);
|
|
36
|
+
if (!namesMatch)
|
|
37
|
+
return [];
|
|
38
|
+
return namesMatch[1]
|
|
39
|
+
.split(',')
|
|
40
|
+
.map(n => n.trim())
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map(n => {
|
|
43
|
+
const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
|
|
44
|
+
return parts[1] || parts[0] || '';
|
|
45
|
+
})
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
function filterClientImportsForServer(imports, neededFns) {
|
|
49
|
+
const selected = [];
|
|
50
|
+
for (const line of imports) {
|
|
51
|
+
const bindings = parseNamedImportBindings(line);
|
|
52
|
+
if (bindings.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
if (bindings.some(name => neededFns.has(name))) {
|
|
55
|
+
selected.push(line);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return selected;
|
|
59
|
+
}
|
|
34
60
|
/**
|
|
35
61
|
* Compile a project's src/routes/ into .kuratchi/routes.js
|
|
36
62
|
*
|
|
37
|
-
* The generated module exports { app }
|
|
63
|
+
* The generated module exports { app } â€" an object with a fetch() method
|
|
38
64
|
* that handles routing, load functions, form actions, and rendering.
|
|
39
65
|
* Returns the path to .kuratchi/worker.js — the stable wrangler entry point that
|
|
40
66
|
* re-exports everything from routes.js (default fetch handler + named DO class exports).
|
|
@@ -49,21 +75,21 @@ export function compile(options) {
|
|
|
49
75
|
}
|
|
50
76
|
// Discover all .html route files
|
|
51
77
|
const routeFiles = discoverRoutes(routesDir);
|
|
52
|
-
// Component compilation cache
|
|
78
|
+
// Component compilation cache â€" only compile components that are actually imported
|
|
53
79
|
const libDir = path.join(srcDir, 'lib');
|
|
54
|
-
const compiledComponentCache = new Map(); // fileName
|
|
55
|
-
const componentStyleCache = new Map(); // fileName
|
|
80
|
+
const compiledComponentCache = new Map(); // fileName â†' compiled function code
|
|
81
|
+
const componentStyleCache = new Map(); // fileName â†' escaped CSS string (or empty)
|
|
56
82
|
// Tracks which prop names inside a component are used as action={propName}.
|
|
57
|
-
// e.g. db-studio uses action={runQueryAction}
|
|
83
|
+
// e.g. db-studio uses action={runQueryAction} â†' stores 'runQueryAction'.
|
|
58
84
|
// When the route passes runQueryAction={runAdminSqlQuery}, the compiler knows
|
|
59
85
|
// to add 'runAdminSqlQuery' to the route's actionFunctions.
|
|
60
|
-
const componentActionCache = new Map(); // fileName
|
|
86
|
+
const componentActionCache = new Map(); // fileName â†' Set of action prop names
|
|
61
87
|
function compileComponent(fileName) {
|
|
62
88
|
if (compiledComponentCache.has(fileName))
|
|
63
89
|
return compiledComponentCache.get(fileName);
|
|
64
90
|
let filePath;
|
|
65
91
|
let funcName;
|
|
66
|
-
// Package component: "@kuratchi/ui:badge"
|
|
92
|
+
// Package component: "@kuratchi/ui:badge" â†' resolve from package
|
|
67
93
|
const pkgMatch = fileName.match(/^(@[^:]+):(.+)$/);
|
|
68
94
|
if (pkgMatch) {
|
|
69
95
|
const pkgName = pkgMatch[1]; // e.g. "@kuratchi/ui"
|
|
@@ -96,7 +122,7 @@ export function compile(options) {
|
|
|
96
122
|
: '';
|
|
97
123
|
// template source (parseFile already removes the <script> block)
|
|
98
124
|
let source = compParsed.template;
|
|
99
|
-
// Extract optional <style> block
|
|
125
|
+
// Extract optional <style> block â€" CSS is scoped and injected once per route at compile time
|
|
100
126
|
let styleBlock = '';
|
|
101
127
|
const styleMatch = source.match(/<style[\s>][\s\S]*?<\/style>/i);
|
|
102
128
|
if (styleMatch) {
|
|
@@ -124,8 +150,8 @@ export function compile(options) {
|
|
|
124
150
|
: '';
|
|
125
151
|
componentStyleCache.set(fileName, escapedStyle);
|
|
126
152
|
// Replace <slot></slot> and <slot /> with children output
|
|
127
|
-
source = source.replace(/<slot\s*><\/slot>/g, '{
|
|
128
|
-
source = source.replace(/<slot\s*\/>/g, '{
|
|
153
|
+
source = source.replace(/<slot\s*><\/slot>/g, '{@raw props.children || ""}');
|
|
154
|
+
source = source.replace(/<slot\s*\/>/g, '{@raw props.children || ""}');
|
|
129
155
|
// Build a sub-component map from the component's own component imports so that
|
|
130
156
|
// <Alert>, <Badge>, <Dialog>, etc. get expanded instead of emitted as raw tags.
|
|
131
157
|
const subComponentNames = new Map();
|
|
@@ -142,7 +168,7 @@ export function compile(options) {
|
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
// Scan the component template for action={propName} uses.
|
|
145
|
-
// These prop names are "action props"
|
|
171
|
+
// These prop names are "action props" â€" when the route passes actionProp={routeFn},
|
|
146
172
|
// the compiler knows to add routeFn to the route's actionFunctions so it ends up
|
|
147
173
|
// in the route's actions map and can be dispatched at runtime.
|
|
148
174
|
const actionPropNames = new Set();
|
|
@@ -162,7 +188,7 @@ export function compile(options) {
|
|
|
162
188
|
compiledComponentCache.set(fileName, compiled);
|
|
163
189
|
return compiled;
|
|
164
190
|
}
|
|
165
|
-
// App layout: src/routes/layout.html (convention
|
|
191
|
+
// App layout: src/routes/layout.html (convention â€" wraps all routes automatically)
|
|
166
192
|
const layoutFile = path.join(routesDir, 'layout.html');
|
|
167
193
|
let compiledLayout = null;
|
|
168
194
|
const layoutComponentNames = new Map();
|
|
@@ -444,6 +470,17 @@ export function compile(options) {
|
|
|
444
470
|
if(!msg) return;
|
|
445
471
|
if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
|
|
446
472
|
}, true);
|
|
473
|
+
document.addEventListener('submit', function(e){
|
|
474
|
+
if(e.defaultPrevented) return;
|
|
475
|
+
var f = e.target;
|
|
476
|
+
if(!f || !f.querySelector) return;
|
|
477
|
+
var aInput = f.querySelector('input[name="_action"]');
|
|
478
|
+
if(!aInput) return;
|
|
479
|
+
var aName = aInput.value;
|
|
480
|
+
if(!aName) return;
|
|
481
|
+
f.setAttribute('data-action-loading', aName);
|
|
482
|
+
Array.prototype.slice.call(f.querySelectorAll('button[type="submit"],button:not([type="button"]):not([type="reset"])')).forEach(function(b){ b.disabled = true; });
|
|
483
|
+
}, true);
|
|
447
484
|
document.addEventListener('change', function(e){
|
|
448
485
|
var t = e.target;
|
|
449
486
|
if(!t || !t.getAttribute) return;
|
|
@@ -458,21 +495,118 @@ export function compile(options) {
|
|
|
458
495
|
}, true);
|
|
459
496
|
by('[data-select-all]').forEach(function(m){ var g = m.getAttribute('data-select-all'); if(g) syncGroup(g); });
|
|
460
497
|
})();`;
|
|
498
|
+
const reactiveRuntimeSource = `(function(g){
|
|
499
|
+
if(g.__kuratchiReactive) return;
|
|
500
|
+
const targetMap = new WeakMap();
|
|
501
|
+
const proxyMap = new WeakMap();
|
|
502
|
+
let active = null;
|
|
503
|
+
const queue = new Set();
|
|
504
|
+
let flushing = false;
|
|
505
|
+
function queueRun(fn){
|
|
506
|
+
queue.add(fn);
|
|
507
|
+
if(flushing) return;
|
|
508
|
+
flushing = true;
|
|
509
|
+
Promise.resolve().then(function(){
|
|
510
|
+
try {
|
|
511
|
+
const jobs = Array.from(queue);
|
|
512
|
+
queue.clear();
|
|
513
|
+
for (const job of jobs) job();
|
|
514
|
+
} finally {
|
|
515
|
+
flushing = false;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function cleanup(effect){
|
|
520
|
+
const deps = effect.__deps || [];
|
|
521
|
+
for (const dep of deps) dep.delete(effect);
|
|
522
|
+
effect.__deps = [];
|
|
523
|
+
}
|
|
524
|
+
function track(target, key){
|
|
525
|
+
if(!active) return;
|
|
526
|
+
let depsMap = targetMap.get(target);
|
|
527
|
+
if(!depsMap){ depsMap = new Map(); targetMap.set(target, depsMap); }
|
|
528
|
+
let dep = depsMap.get(key);
|
|
529
|
+
if(!dep){ dep = new Set(); depsMap.set(key, dep); }
|
|
530
|
+
if(dep.has(active)) return;
|
|
531
|
+
dep.add(active);
|
|
532
|
+
if(!active.__deps) active.__deps = [];
|
|
533
|
+
active.__deps.push(dep);
|
|
534
|
+
}
|
|
535
|
+
function trigger(target, key){
|
|
536
|
+
const depsMap = targetMap.get(target);
|
|
537
|
+
if(!depsMap) return;
|
|
538
|
+
const effects = new Set();
|
|
539
|
+
const add = function(k){
|
|
540
|
+
const dep = depsMap.get(k);
|
|
541
|
+
if(dep) dep.forEach(function(e){ effects.add(e); });
|
|
542
|
+
};
|
|
543
|
+
add(key);
|
|
544
|
+
add('*');
|
|
545
|
+
effects.forEach(function(e){ queueRun(e); });
|
|
546
|
+
}
|
|
547
|
+
function isObject(value){ return value !== null && typeof value === 'object'; }
|
|
548
|
+
function proxify(value){
|
|
549
|
+
if(!isObject(value)) return value;
|
|
550
|
+
if(proxyMap.has(value)) return proxyMap.get(value);
|
|
551
|
+
const proxy = new Proxy(value, {
|
|
552
|
+
get(target, key, receiver){
|
|
553
|
+
track(target, key);
|
|
554
|
+
const out = Reflect.get(target, key, receiver);
|
|
555
|
+
return isObject(out) ? proxify(out) : out;
|
|
556
|
+
},
|
|
557
|
+
set(target, key, next, receiver){
|
|
558
|
+
const prev = target[key];
|
|
559
|
+
const result = Reflect.set(target, key, next, receiver);
|
|
560
|
+
if(prev !== next) trigger(target, key);
|
|
561
|
+
if(Array.isArray(target) && key !== 'length') trigger(target, 'length');
|
|
562
|
+
return result;
|
|
563
|
+
},
|
|
564
|
+
deleteProperty(target, key){
|
|
565
|
+
const had = Object.prototype.hasOwnProperty.call(target, key);
|
|
566
|
+
const result = Reflect.deleteProperty(target, key);
|
|
567
|
+
if(had) trigger(target, key);
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
proxyMap.set(value, proxy);
|
|
572
|
+
return proxy;
|
|
573
|
+
}
|
|
574
|
+
function effect(fn){
|
|
575
|
+
const run = function(){
|
|
576
|
+
cleanup(run);
|
|
577
|
+
active = run;
|
|
578
|
+
try { fn(); } finally { active = null; }
|
|
579
|
+
};
|
|
580
|
+
run.__deps = [];
|
|
581
|
+
run();
|
|
582
|
+
return function(){ cleanup(run); };
|
|
583
|
+
}
|
|
584
|
+
function state(initial){ return proxify(initial); }
|
|
585
|
+
function replace(_prev, next){ return proxify(next); }
|
|
586
|
+
g.__kuratchiReactive = { state, effect, replace };
|
|
587
|
+
})(window);`;
|
|
461
588
|
const actionScript = `<script>${options.isDev ? bridgeSource : compactInlineJs(bridgeSource)}</script>`;
|
|
589
|
+
const reactiveRuntimeScript = `<script>${options.isDev ? reactiveRuntimeSource : compactInlineJs(reactiveRuntimeSource)}</script>`;
|
|
590
|
+
if (source.includes('</head>')) {
|
|
591
|
+
source = source.replace('</head>', reactiveRuntimeScript + '\n</head>');
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
source = reactiveRuntimeScript + '\n' + source;
|
|
595
|
+
}
|
|
462
596
|
source = source.replace('</body>', actionScript + '\n</body>');
|
|
463
597
|
// Parse layout for <script> block (component imports + data vars)
|
|
464
598
|
const layoutParsed = parseFile(source);
|
|
465
599
|
const hasLayoutScript = layoutParsed.script && (Object.keys(layoutParsed.componentImports).length > 0 || layoutParsed.hasLoad);
|
|
466
600
|
if (hasLayoutScript) {
|
|
467
|
-
// Dynamic layout
|
|
601
|
+
// Dynamic layout â€" has component imports and/or data declarations
|
|
468
602
|
// Compile component imports from layout
|
|
469
603
|
for (const [pascalName, fileName] of Object.entries(layoutParsed.componentImports)) {
|
|
470
604
|
compileComponent(fileName);
|
|
471
605
|
layoutComponentNames.set(pascalName, fileName);
|
|
472
606
|
}
|
|
473
607
|
// Replace <slot></slot> with content parameter injection
|
|
474
|
-
let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{
|
|
475
|
-
layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{
|
|
608
|
+
let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{@raw __content}');
|
|
609
|
+
layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{@raw __content}');
|
|
476
610
|
// Build layout action names so action={fn} works in layouts
|
|
477
611
|
const layoutActionNames = new Set(layoutParsed.actionFunctions);
|
|
478
612
|
// Compile the layout template with component + action support
|
|
@@ -500,7 +634,7 @@ export function compile(options) {
|
|
|
500
634
|
}`;
|
|
501
635
|
}
|
|
502
636
|
else {
|
|
503
|
-
// Static layout
|
|
637
|
+
// Static layout â€" no components, use fast string split (original behavior)
|
|
504
638
|
const slotMarker = '<slot></slot>';
|
|
505
639
|
const slotIdx = source.indexOf(slotMarker);
|
|
506
640
|
if (slotIdx === -1) {
|
|
@@ -513,7 +647,7 @@ export function compile(options) {
|
|
|
513
647
|
}
|
|
514
648
|
}
|
|
515
649
|
// Custom error pages: src/routes/NNN.html (e.g. 404.html, 500.html, 401.html, 403.html)
|
|
516
|
-
// Only compiled if the user explicitly creates them
|
|
650
|
+
// Only compiled if the user explicitly creates them â€" otherwise the framework's built-in default is used
|
|
517
651
|
const compiledErrorPages = new Map();
|
|
518
652
|
for (const file of fs.readdirSync(routesDir)) {
|
|
519
653
|
const match = file.match(/^(\d{3})\.html$/);
|
|
@@ -573,11 +707,125 @@ export function compile(options) {
|
|
|
573
707
|
}
|
|
574
708
|
}
|
|
575
709
|
}
|
|
710
|
+
const resolveExistingModuleFile = (absBase) => {
|
|
711
|
+
const candidates = [
|
|
712
|
+
absBase,
|
|
713
|
+
absBase + '.ts',
|
|
714
|
+
absBase + '.js',
|
|
715
|
+
absBase + '.mjs',
|
|
716
|
+
absBase + '.cjs',
|
|
717
|
+
path.join(absBase, 'index.ts'),
|
|
718
|
+
path.join(absBase, 'index.js'),
|
|
719
|
+
path.join(absBase, 'index.mjs'),
|
|
720
|
+
path.join(absBase, 'index.cjs'),
|
|
721
|
+
];
|
|
722
|
+
for (const candidate of candidates) {
|
|
723
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
724
|
+
return candidate;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
};
|
|
728
|
+
const toModuleSpecifier = (fromFileAbs, toFileAbs) => {
|
|
729
|
+
let rel = path.relative(path.dirname(fromFileAbs), toFileAbs).replace(/\\/g, '/');
|
|
730
|
+
if (!rel.startsWith('.'))
|
|
731
|
+
rel = './' + rel;
|
|
732
|
+
return rel;
|
|
733
|
+
};
|
|
734
|
+
const transformedServerModules = new Map();
|
|
735
|
+
const modulesOutDir = path.join(projectDir, '.kuratchi', 'modules');
|
|
736
|
+
const resolveDoProxyTarget = (absPath) => {
|
|
737
|
+
const normalizedNoExt = absPath.replace(/\\/g, '/').replace(/\.[^.\/]+$/, '');
|
|
738
|
+
const proxyNoExt = doHandlerProxyPaths.get(normalizedNoExt);
|
|
739
|
+
if (!proxyNoExt)
|
|
740
|
+
return null;
|
|
741
|
+
return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.js') ? proxyNoExt + '.js' : null);
|
|
742
|
+
};
|
|
743
|
+
const resolveImportTarget = (importerAbs, spec) => {
|
|
744
|
+
if (spec.startsWith('$')) {
|
|
745
|
+
const slashIdx = spec.indexOf('/');
|
|
746
|
+
const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
|
|
747
|
+
const rest = slashIdx === -1 ? '' : spec.slice(slashIdx + 1);
|
|
748
|
+
if (!folder)
|
|
749
|
+
return null;
|
|
750
|
+
const abs = path.join(srcDir, folder, rest);
|
|
751
|
+
return resolveExistingModuleFile(abs) ?? abs;
|
|
752
|
+
}
|
|
753
|
+
if (spec.startsWith('.')) {
|
|
754
|
+
const abs = path.resolve(path.dirname(importerAbs), spec);
|
|
755
|
+
return resolveExistingModuleFile(abs) ?? abs;
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
};
|
|
759
|
+
const transformServerModule = (entryAbsPath) => {
|
|
760
|
+
const resolved = resolveExistingModuleFile(entryAbsPath) ?? entryAbsPath;
|
|
761
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
762
|
+
const cached = transformedServerModules.get(normalized);
|
|
763
|
+
if (cached)
|
|
764
|
+
return cached;
|
|
765
|
+
const relFromProject = path.relative(projectDir, resolved);
|
|
766
|
+
const outPath = path.join(modulesOutDir, relFromProject);
|
|
767
|
+
transformedServerModules.set(normalized, outPath);
|
|
768
|
+
const outDir = path.dirname(outPath);
|
|
769
|
+
if (!fs.existsSync(outDir))
|
|
770
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
771
|
+
if (!/\.(ts|js|mjs|cjs)$/i.test(resolved) || !fs.existsSync(resolved)) {
|
|
772
|
+
const passthrough = resolved;
|
|
773
|
+
transformedServerModules.set(normalized, passthrough);
|
|
774
|
+
return passthrough;
|
|
775
|
+
}
|
|
776
|
+
const source = fs.readFileSync(resolved, 'utf-8');
|
|
777
|
+
const rewriteSpecifier = (spec) => {
|
|
778
|
+
const target = resolveImportTarget(resolved, spec);
|
|
779
|
+
if (!target)
|
|
780
|
+
return spec;
|
|
781
|
+
const doProxyTarget = resolveDoProxyTarget(target);
|
|
782
|
+
if (doProxyTarget)
|
|
783
|
+
return toModuleSpecifier(outPath, doProxyTarget);
|
|
784
|
+
const normalizedTarget = target.replace(/\\/g, '/');
|
|
785
|
+
const inProject = normalizedTarget.startsWith(projectDir.replace(/\\/g, '/') + '/');
|
|
786
|
+
if (!inProject)
|
|
787
|
+
return spec;
|
|
788
|
+
const targetResolved = resolveExistingModuleFile(target) ?? target;
|
|
789
|
+
if (!/\.(ts|js|mjs|cjs)$/i.test(targetResolved))
|
|
790
|
+
return spec;
|
|
791
|
+
const rewrittenTarget = transformServerModule(targetResolved);
|
|
792
|
+
return toModuleSpecifier(outPath, rewrittenTarget);
|
|
793
|
+
};
|
|
794
|
+
let rewritten = source.replace(/(from\s+)(['"])([^'"]+)\2/g, (_m, p1, q, spec) => {
|
|
795
|
+
return `${p1}${q}${rewriteSpecifier(spec)}${q}`;
|
|
796
|
+
});
|
|
797
|
+
rewritten = rewritten.replace(/(import\s*\(\s*)(['"])([^'"]+)\2(\s*\))/g, (_m, p1, q, spec, p4) => {
|
|
798
|
+
return `${p1}${q}${rewriteSpecifier(spec)}${q}${p4}`;
|
|
799
|
+
});
|
|
800
|
+
writeIfChanged(outPath, rewritten);
|
|
801
|
+
return outPath;
|
|
802
|
+
};
|
|
803
|
+
const resolveCompiledImportPath = (origPath, importerDir, outFileDir) => {
|
|
804
|
+
const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
|
|
805
|
+
if (isBareModule)
|
|
806
|
+
return origPath;
|
|
807
|
+
let absImport;
|
|
808
|
+
if (origPath.startsWith('$')) {
|
|
809
|
+
const slashIdx = origPath.indexOf('/');
|
|
810
|
+
const folder = slashIdx === -1 ? origPath.slice(1) : origPath.slice(1, slashIdx);
|
|
811
|
+
const rest = slashIdx === -1 ? '' : origPath.slice(slashIdx + 1);
|
|
812
|
+
absImport = path.join(srcDir, folder, rest);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
absImport = path.resolve(importerDir, origPath);
|
|
816
|
+
}
|
|
817
|
+
const doProxyTarget = resolveDoProxyTarget(absImport);
|
|
818
|
+
const target = doProxyTarget ?? transformServerModule(absImport);
|
|
819
|
+
let relPath = path.relative(outFileDir, target).replace(/\\/g, '/');
|
|
820
|
+
if (!relPath.startsWith('.'))
|
|
821
|
+
relPath = './' + relPath;
|
|
822
|
+
return relPath;
|
|
823
|
+
};
|
|
576
824
|
// Parse and compile each route
|
|
577
825
|
const compiledRoutes = [];
|
|
578
826
|
const allImports = [];
|
|
579
827
|
let moduleCounter = 0;
|
|
580
|
-
// Layout server import resolution
|
|
828
|
+
// Layout server import resolution â€" resolve non-component imports to module IDs
|
|
581
829
|
let isLayoutAsync = false;
|
|
582
830
|
let compiledLayoutActions = null;
|
|
583
831
|
if (compiledLayout && fs.existsSync(path.join(routesDir, 'layout.html'))) {
|
|
@@ -592,39 +840,7 @@ export function compile(options) {
|
|
|
592
840
|
if (!pathMatch)
|
|
593
841
|
continue;
|
|
594
842
|
const origPath = pathMatch[1];
|
|
595
|
-
const
|
|
596
|
-
let importPath;
|
|
597
|
-
if (isBareModule) {
|
|
598
|
-
importPath = origPath;
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
let absImport;
|
|
602
|
-
if (origPath.startsWith('$')) {
|
|
603
|
-
const slashIdx = origPath.indexOf('/');
|
|
604
|
-
const folder = origPath.slice(1, slashIdx);
|
|
605
|
-
const rest = origPath.slice(slashIdx + 1);
|
|
606
|
-
absImport = path.join(srcDir, folder, rest);
|
|
607
|
-
// Redirect DO handler imports to generated proxy modules
|
|
608
|
-
const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
|
|
609
|
-
if (doProxyPath) {
|
|
610
|
-
absImport = doProxyPath;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
absImport = path.resolve(layoutFileDir, origPath);
|
|
615
|
-
}
|
|
616
|
-
let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
|
|
617
|
-
if (!relPath.startsWith('.'))
|
|
618
|
-
relPath = './' + relPath;
|
|
619
|
-
let resolvedExt = '';
|
|
620
|
-
for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
|
|
621
|
-
if (fs.existsSync(absImport + ext)) {
|
|
622
|
-
resolvedExt = ext;
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
importPath = relPath + resolvedExt;
|
|
627
|
-
}
|
|
843
|
+
const importPath = resolveCompiledImportPath(origPath, layoutFileDir, outFileDir);
|
|
628
844
|
const moduleId = `__m${moduleCounter++}`;
|
|
629
845
|
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
630
846
|
const namesMatch = imp.match(/import\s*\{([^}]+)\}/);
|
|
@@ -665,7 +881,7 @@ export function compile(options) {
|
|
|
665
881
|
}
|
|
666
882
|
}
|
|
667
883
|
}
|
|
668
|
-
// Detect if the compiled layout uses await
|
|
884
|
+
// Detect if the compiled layout uses await â†' make it async
|
|
669
885
|
isLayoutAsync = /\bawait\b/.test(compiledLayout);
|
|
670
886
|
if (isLayoutAsync) {
|
|
671
887
|
compiledLayout = compiledLayout.replace(/^function __layout\(/, 'async function __layout(');
|
|
@@ -674,72 +890,62 @@ export function compile(options) {
|
|
|
674
890
|
for (let i = 0; i < routeFiles.length; i++) {
|
|
675
891
|
const rf = routeFiles[i];
|
|
676
892
|
const fullPath = path.join(routesDir, rf.file);
|
|
893
|
+
const pattern = filePathToPattern(rf.name);
|
|
894
|
+
// -- API route (index.ts / index.js) --
|
|
895
|
+
if (rf.type === 'api') {
|
|
896
|
+
const outFileDir = path.join(projectDir, '.kuratchi');
|
|
897
|
+
// Resolve the route file's absolute path through transformServerModule
|
|
898
|
+
// so that $-prefixed imports inside it get rewritten correctly
|
|
899
|
+
const absRoutePath = transformServerModule(fullPath);
|
|
900
|
+
let importPath = path.relative(outFileDir, absRoutePath).replace(/\\/g, '/');
|
|
901
|
+
if (!importPath.startsWith('.'))
|
|
902
|
+
importPath = './' + importPath;
|
|
903
|
+
const moduleId = `__m${moduleCounter++}`;
|
|
904
|
+
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
905
|
+
// Scan the source for exported method handlers (only include what exists)
|
|
906
|
+
const apiSource = fs.readFileSync(fullPath, 'utf-8');
|
|
907
|
+
const allMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
908
|
+
const exportedMethods = allMethods.filter(m => {
|
|
909
|
+
// Match: export function GET, export async function GET, export { ... as GET }
|
|
910
|
+
const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`);
|
|
911
|
+
const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${m}\\b`);
|
|
912
|
+
const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${m}\\b`);
|
|
913
|
+
return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
|
|
914
|
+
});
|
|
915
|
+
const methodEntries = exportedMethods
|
|
916
|
+
.map(m => `${m}: ${moduleId}.${m}`)
|
|
917
|
+
.join(', ');
|
|
918
|
+
compiledRoutes.push(`{ pattern: '${pattern}', __api: true, ${methodEntries} }`);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
// ── Page route (page.html) ──
|
|
677
922
|
const source = fs.readFileSync(fullPath, 'utf-8');
|
|
678
923
|
const parsed = parseFile(source);
|
|
679
|
-
|
|
680
|
-
// Build a mapping: functionName → moduleId for all imports in this route
|
|
924
|
+
// Build a mapping: functionName â†' moduleId for all imports in this route
|
|
681
925
|
const fnToModule = {};
|
|
682
926
|
const outFileDir = path.join(projectDir, '.kuratchi');
|
|
683
|
-
|
|
927
|
+
const neededServerFns = new Set([
|
|
928
|
+
...parsed.actionFunctions,
|
|
929
|
+
...parsed.pollFunctions,
|
|
930
|
+
...parsed.dataGetQueries.map((q) => q.fnName),
|
|
931
|
+
]);
|
|
932
|
+
const routeServerImports = parsed.serverImports.length > 0
|
|
933
|
+
? parsed.serverImports
|
|
934
|
+
: filterClientImportsForServer(parsed.clientImports, neededServerFns);
|
|
935
|
+
if (routeServerImports.length > 0) {
|
|
684
936
|
const routeFileDir = path.dirname(fullPath);
|
|
685
|
-
for (const imp of
|
|
937
|
+
for (const imp of routeServerImports) {
|
|
686
938
|
const pathMatch = imp.match(/from\s+['"]([^'"]+)['"]/);
|
|
687
939
|
if (!pathMatch)
|
|
688
940
|
continue;
|
|
689
941
|
const origPath = pathMatch[1];
|
|
690
|
-
// Bare module specifiers (packages)
|
|
691
|
-
const
|
|
692
|
-
let importPath;
|
|
693
|
-
if (isBareModule) {
|
|
694
|
-
// Package import: @kuratchi/auth, KuratchiJS, cloudflare:workers, etc.
|
|
695
|
-
importPath = origPath;
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
let absImport;
|
|
699
|
-
if (origPath.startsWith('$')) {
|
|
700
|
-
// Dynamic $folder/ alias → src/folder/
|
|
701
|
-
const slashIdx = origPath.indexOf('/');
|
|
702
|
-
const folder = origPath.slice(1, slashIdx);
|
|
703
|
-
const rest = origPath.slice(slashIdx + 1);
|
|
704
|
-
absImport = path.join(srcDir, folder, rest);
|
|
705
|
-
// Redirect DO handler imports to generated proxy modules
|
|
706
|
-
const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
|
|
707
|
-
if (doProxyPath) {
|
|
708
|
-
absImport = doProxyPath;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
// Resolve the import relative to the route file
|
|
713
|
-
absImport = path.resolve(routeFileDir, origPath);
|
|
714
|
-
}
|
|
715
|
-
// Make it relative to the output directory
|
|
716
|
-
let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
|
|
717
|
-
if (!relPath.startsWith('.'))
|
|
718
|
-
relPath = './' + relPath;
|
|
719
|
-
// Check if the resolved file exists (try .ts, .js extensions)
|
|
720
|
-
let resolvedExt = '';
|
|
721
|
-
for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
|
|
722
|
-
if (fs.existsSync(absImport + ext)) {
|
|
723
|
-
resolvedExt = ext;
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
importPath = relPath + resolvedExt;
|
|
728
|
-
}
|
|
942
|
+
// Bare module specifiers (packages) â€" pass through as-is
|
|
943
|
+
const importPath = resolveCompiledImportPath(origPath, routeFileDir, outFileDir);
|
|
729
944
|
const moduleId = `__m${moduleCounter++}`;
|
|
730
945
|
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
731
946
|
// Extract named imports and map them to this module
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
734
|
-
const names = namesMatch[1]
|
|
735
|
-
.split(',')
|
|
736
|
-
.map(n => n.trim())
|
|
737
|
-
.filter(Boolean)
|
|
738
|
-
.map(n => {
|
|
739
|
-
const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
|
|
740
|
-
return parts[1] || parts[0] || '';
|
|
741
|
-
})
|
|
742
|
-
.filter(Boolean);
|
|
947
|
+
const names = parseNamedImportBindings(imp);
|
|
948
|
+
if (names.length > 0) {
|
|
743
949
|
for (const name of names) {
|
|
744
950
|
fnToModule[name] = moduleId;
|
|
745
951
|
}
|
|
@@ -752,7 +958,7 @@ export function compile(options) {
|
|
|
752
958
|
}
|
|
753
959
|
}
|
|
754
960
|
// Build per-route component names from explicit imports
|
|
755
|
-
// componentImports: { StatCard: 'stat-card' }
|
|
961
|
+
// componentImports: { StatCard: 'stat-card' } â†' componentNames maps PascalCase â†' fileName
|
|
756
962
|
const routeComponentNames = new Map();
|
|
757
963
|
for (const [pascalName, fileName] of Object.entries(parsed.componentImports)) {
|
|
758
964
|
// Compile the component on first use
|
|
@@ -767,22 +973,22 @@ export function compile(options) {
|
|
|
767
973
|
// We then scan the route template for that component's usage and extract the bound values.
|
|
768
974
|
for (const [pascalName, compFileName] of routeComponentNames.entries()) {
|
|
769
975
|
const actionPropNames = componentActionCache.get(compFileName);
|
|
770
|
-
if (!actionPropNames || actionPropNames.size === 0)
|
|
771
|
-
continue;
|
|
772
976
|
// Find all usages of <PascalName ...> in the route template and extract prop bindings.
|
|
773
977
|
// Match <ComponentName ... propName={value} ... > across multiple lines.
|
|
774
978
|
const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)/>`, 'g');
|
|
775
979
|
for (const tagMatch of parsed.template.matchAll(compTagRegex)) {
|
|
776
980
|
const attrs = tagMatch[1];
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
parsed.actionFunctions.
|
|
981
|
+
if (actionPropNames && actionPropNames.size > 0) {
|
|
982
|
+
for (const propName of actionPropNames) {
|
|
983
|
+
// Find propName={identifier} binding
|
|
984
|
+
const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
|
|
985
|
+
const propMatch = attrs.match(propRegex);
|
|
986
|
+
if (propMatch) {
|
|
987
|
+
const routeFnName = propMatch[1];
|
|
988
|
+
// Only add if this function is actually imported by the route
|
|
989
|
+
if (routeFnName in fnToModule && !parsed.actionFunctions.includes(routeFnName)) {
|
|
990
|
+
parsed.actionFunctions.push(routeFnName);
|
|
991
|
+
}
|
|
786
992
|
}
|
|
787
993
|
}
|
|
788
994
|
}
|
|
@@ -791,7 +997,7 @@ export function compile(options) {
|
|
|
791
997
|
// Compile template to render function body (pass component names and action names)
|
|
792
998
|
// An identifier is a valid server action if it is either:
|
|
793
999
|
// 1. Directly imported (present in fnToModule), or
|
|
794
|
-
// 2. A top-level script declaration (present in dataVars)
|
|
1000
|
+
// 2. A top-level script declaration (present in dataVars) â€" covers cases like
|
|
795
1001
|
// `const fn = importedFn` or `async function fn() {}` where the binding
|
|
796
1002
|
// is locally declared but delegates to an imported function.
|
|
797
1003
|
const dataVarsSet = new Set(parsed.dataVars);
|
|
@@ -869,7 +1075,16 @@ export function compile(options) {
|
|
|
869
1075
|
// Collect only the components that were actually imported by routes
|
|
870
1076
|
const compiledComponents = Array.from(compiledComponentCache.values());
|
|
871
1077
|
// Generate the routes module
|
|
872
|
-
const
|
|
1078
|
+
const rawRuntimeImportPath = resolveRuntimeImportPath(projectDir);
|
|
1079
|
+
let runtimeImportPath;
|
|
1080
|
+
if (rawRuntimeImportPath) {
|
|
1081
|
+
// Resolve the runtime file's absolute path and pass it through transformServerModule
|
|
1082
|
+
// so that $durable-objects/* and other project imports get rewritten to their proxies.
|
|
1083
|
+
const runtimeAbs = path.resolve(path.join(projectDir, '.kuratchi'), rawRuntimeImportPath);
|
|
1084
|
+
const transformedRuntimePath = transformServerModule(runtimeAbs);
|
|
1085
|
+
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
|
|
1086
|
+
runtimeImportPath = toModuleSpecifier(outFile, transformedRuntimePath);
|
|
1087
|
+
}
|
|
873
1088
|
const hasRuntime = !!runtimeImportPath;
|
|
874
1089
|
const output = generateRoutesModule({
|
|
875
1090
|
projectDir,
|
|
@@ -887,7 +1102,7 @@ export function compile(options) {
|
|
|
887
1102
|
isLayoutAsync,
|
|
888
1103
|
compiledLayoutActions,
|
|
889
1104
|
hasRuntime,
|
|
890
|
-
runtimeImportPath
|
|
1105
|
+
runtimeImportPath,
|
|
891
1106
|
});
|
|
892
1107
|
// Write to .kuratchi/routes.js
|
|
893
1108
|
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
|
|
@@ -919,7 +1134,7 @@ export function compile(options) {
|
|
|
919
1134
|
writeIfChanged(workerFile, workerLines.join('\n'));
|
|
920
1135
|
return workerFile;
|
|
921
1136
|
}
|
|
922
|
-
//
|
|
1137
|
+
// â"€â"€ Helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
923
1138
|
/**
|
|
924
1139
|
* Write a file only if its content has changed.
|
|
925
1140
|
* Prevents unnecessary filesystem events that would retrigger wrangler's file watcher.
|
|
@@ -1013,7 +1228,7 @@ function readUiTheme(projectDir) {
|
|
|
1013
1228
|
console.warn('[kuratchi] ui.theme: "default" configured but @kuratchi/ui theme.css not found');
|
|
1014
1229
|
return null;
|
|
1015
1230
|
}
|
|
1016
|
-
// Custom path
|
|
1231
|
+
// Custom path â€" resolve relative to project root
|
|
1017
1232
|
const customPath = path.resolve(projectDir, themeValue);
|
|
1018
1233
|
if (fs.existsSync(customPath)) {
|
|
1019
1234
|
return fs.readFileSync(customPath, 'utf-8');
|
|
@@ -1031,7 +1246,7 @@ function resolvePackageComponent(projectDir, pkgName, componentFile) {
|
|
|
1031
1246
|
if (fs.existsSync(nmPath))
|
|
1032
1247
|
return nmPath;
|
|
1033
1248
|
// 2. Try workspace layout: project is in apps/X or packages/X, sibling packages in packages/
|
|
1034
|
-
// @kuratchi/ui
|
|
1249
|
+
// @kuratchi/ui â†' kuratchi-ui (convention: scope stripped, slash â†' dash)
|
|
1035
1250
|
const pkgDirName = pkgName.replace(/^@/, '').replace(/\//g, '-');
|
|
1036
1251
|
const workspaceRoot = path.resolve(projectDir, '../..');
|
|
1037
1252
|
const wsPath = path.join(workspaceRoot, 'packages', pkgDirName, 'src', 'lib', componentFile + '.html');
|
|
@@ -1069,32 +1284,49 @@ function discoverRoutes(routesDir) {
|
|
|
1069
1284
|
for (const entry of entries) {
|
|
1070
1285
|
if (entry.isDirectory()) {
|
|
1071
1286
|
const childPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1072
|
-
// Folder-based route: routes/db/page.html
|
|
1287
|
+
// Folder-based page route: routes/db/page.html → /db
|
|
1073
1288
|
const pageFile = path.join(dir, entry.name, 'page.html');
|
|
1074
1289
|
if (fs.existsSync(pageFile)) {
|
|
1075
1290
|
const routeFile = `${childPrefix}/page.html`;
|
|
1076
1291
|
if (!registered.has(routeFile)) {
|
|
1077
1292
|
registered.add(routeFile);
|
|
1078
|
-
results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix) });
|
|
1293
|
+
results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix), type: 'page' });
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
// Folder-based API route: routes/api/v1/health/index.ts -> /api/v1/health
|
|
1297
|
+
const apiFile = ['index.ts', 'index.js'].find(f => fs.existsSync(path.join(dir, entry.name, f)));
|
|
1298
|
+
if (apiFile && !fs.existsSync(pageFile)) {
|
|
1299
|
+
const routeFile = `${childPrefix}/${apiFile}`;
|
|
1300
|
+
if (!registered.has(routeFile)) {
|
|
1301
|
+
registered.add(routeFile);
|
|
1302
|
+
results.push({ file: routeFile, name: childPrefix, layouts: [], type: 'api' });
|
|
1079
1303
|
}
|
|
1080
1304
|
}
|
|
1081
1305
|
// Always recurse into subdirectory (for nested routes like /admin/roles)
|
|
1082
1306
|
walk(path.join(dir, entry.name), childPrefix);
|
|
1083
1307
|
}
|
|
1084
1308
|
else if (entry.name === 'layout.html' || entry.name === '404.html' || entry.name === '500.html') {
|
|
1085
|
-
// Skip
|
|
1309
|
+
// Skip — layout.html is the app layout, 404/500 are error pages, not routes
|
|
1086
1310
|
continue;
|
|
1087
1311
|
}
|
|
1312
|
+
else if (entry.name === 'index.ts' || entry.name === 'index.js') {
|
|
1313
|
+
// API route file in current directory -> index API route for this prefix
|
|
1314
|
+
const routeFile = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1315
|
+
if (!registered.has(routeFile)) {
|
|
1316
|
+
registered.add(routeFile);
|
|
1317
|
+
results.push({ file: routeFile, name: prefix || 'index', layouts: [], type: 'api' });
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1088
1320
|
else if (entry.name === 'page.html') {
|
|
1089
|
-
// page.html in current directory
|
|
1321
|
+
// page.html in current directory → index route for this prefix
|
|
1090
1322
|
const routeFile = prefix ? `${prefix}/page.html` : 'page.html';
|
|
1091
1323
|
if (!registered.has(routeFile)) {
|
|
1092
1324
|
registered.add(routeFile);
|
|
1093
|
-
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix) });
|
|
1325
|
+
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix), type: 'page' });
|
|
1094
1326
|
}
|
|
1095
1327
|
}
|
|
1096
1328
|
else if (entry.name.endsWith('.html') && entry.name !== 'page.html') {
|
|
1097
|
-
// File-based route: routes/about.html
|
|
1329
|
+
// File-based route: routes/about.html → /about (fallback)
|
|
1098
1330
|
const name = prefix
|
|
1099
1331
|
? `${prefix}/${entry.name.replace('.html', '')}`
|
|
1100
1332
|
: entry.name.replace('.html', '');
|
|
@@ -1102,6 +1334,7 @@ function discoverRoutes(routesDir) {
|
|
|
1102
1334
|
file: prefix ? `${prefix}/${entry.name}` : entry.name,
|
|
1103
1335
|
name,
|
|
1104
1336
|
layouts: getLayoutsForPrefix(prefix),
|
|
1337
|
+
type: 'page',
|
|
1105
1338
|
});
|
|
1106
1339
|
}
|
|
1107
1340
|
}
|
|
@@ -1120,18 +1353,33 @@ function buildRouteObject(opts) {
|
|
|
1120
1353
|
const hasFns = Object.keys(fnToModule).length > 0;
|
|
1121
1354
|
const parts = [];
|
|
1122
1355
|
parts.push(` pattern: '${pattern}'`);
|
|
1123
|
-
|
|
1356
|
+
const queryVars = parsed.dataGetQueries?.map((q) => q.asName) ?? [];
|
|
1357
|
+
let scriptBody = parsed.script
|
|
1358
|
+
? parsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim()
|
|
1359
|
+
: '';
|
|
1360
|
+
for (const [fnName, moduleId] of Object.entries(fnToModule)) {
|
|
1361
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
|
|
1362
|
+
continue;
|
|
1363
|
+
const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
|
|
1364
|
+
scriptBody = scriptBody.replace(callRegex, `${moduleId}.${fnName}(`);
|
|
1365
|
+
}
|
|
1366
|
+
const scriptUsesAwait = /\bawait\b/.test(scriptBody);
|
|
1367
|
+
const scriptReturnVars = parsed.script
|
|
1368
|
+
? parsed.dataVars.filter((v) => !queryVars.includes(v))
|
|
1369
|
+
: [];
|
|
1370
|
+
// Load function â€" internal server prepass for async route script bodies
|
|
1371
|
+
// and data-get query state hydration.
|
|
1124
1372
|
const hasDataGetQueries = Array.isArray(parsed.dataGetQueries) && parsed.dataGetQueries.length > 0;
|
|
1125
|
-
if ((
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1373
|
+
if ((scriptBody && scriptUsesAwait) || hasDataGetQueries) {
|
|
1374
|
+
let loadBody = '';
|
|
1375
|
+
if (scriptBody && scriptUsesAwait) {
|
|
1376
|
+
loadBody = scriptBody;
|
|
1377
|
+
}
|
|
1130
1378
|
// Inject data-get query state blocks into load scope.
|
|
1131
1379
|
// Each query exposes:
|
|
1132
1380
|
// { state, loading, error, data, empty, success }
|
|
1381
|
+
const queries = parsed.dataGetQueries;
|
|
1133
1382
|
if (hasDataGetQueries) {
|
|
1134
|
-
const queries = parsed.dataGetQueries;
|
|
1135
1383
|
const queryLines = [];
|
|
1136
1384
|
for (const q of queries) {
|
|
1137
1385
|
const fnName = q.fnName;
|
|
@@ -1154,25 +1402,13 @@ function buildRouteObject(opts) {
|
|
|
1154
1402
|
queryLines.push(` }`);
|
|
1155
1403
|
queryLines.push(`}`);
|
|
1156
1404
|
}
|
|
1157
|
-
|
|
1405
|
+
loadBody = [loadBody, queryLines.join('\n')].filter(Boolean).join('\n');
|
|
1158
1406
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
|
|
1163
|
-
continue;
|
|
1164
|
-
const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
|
|
1165
|
-
body = body.replace(callRegex, `${moduleId}.${fnName}(`);
|
|
1166
|
-
}
|
|
1167
|
-
// Determine if body uses await
|
|
1168
|
-
const isAsync = /\bawait\b/.test(body) || hasDataGetQueries;
|
|
1169
|
-
// Return an object with all declared data variables
|
|
1170
|
-
const returnObj = parsed.dataVars.length > 0
|
|
1171
|
-
? `\n return { ${parsed.dataVars.join(', ')} };`
|
|
1172
|
-
: '';
|
|
1173
|
-
parts.push(` ${isAsync ? 'async ' : ''}load(params = {}) {\n ${body}${returnObj}\n }`);
|
|
1407
|
+
const loadReturnVars = [...scriptReturnVars, ...queryVars];
|
|
1408
|
+
const returnObj = loadReturnVars.length > 0 ? `\n return { ${loadReturnVars.join(', ')} };` : '';
|
|
1409
|
+
parts.push(` async load(params = {}) {\n ${loadBody}${returnObj}\n }`);
|
|
1174
1410
|
}
|
|
1175
|
-
// Actions
|
|
1411
|
+
// Actions â€" functions referenced via action={fn} in the template
|
|
1176
1412
|
if (hasFns && parsed.actionFunctions.length > 0) {
|
|
1177
1413
|
const actionEntries = parsed.actionFunctions
|
|
1178
1414
|
.map(fn => {
|
|
@@ -1182,7 +1418,7 @@ function buildRouteObject(opts) {
|
|
|
1182
1418
|
.join(', ');
|
|
1183
1419
|
parts.push(` actions: { ${actionEntries} }`);
|
|
1184
1420
|
}
|
|
1185
|
-
// RPC
|
|
1421
|
+
// RPC â€" functions referenced via data-poll={fn(args)} in the template
|
|
1186
1422
|
if (hasFns && parsed.pollFunctions.length > 0) {
|
|
1187
1423
|
const rpcEntries = parsed.pollFunctions
|
|
1188
1424
|
.map(fn => {
|
|
@@ -1193,12 +1429,21 @@ function buildRouteObject(opts) {
|
|
|
1193
1429
|
.join(', ');
|
|
1194
1430
|
parts.push(` rpc: { ${rpcEntries} }`);
|
|
1195
1431
|
}
|
|
1196
|
-
// Render function
|
|
1432
|
+
// Render function â€" template compiled to JS with native flow control
|
|
1197
1433
|
// Destructure data vars so templates reference them directly (e.g., {todos} not {data.todos})
|
|
1198
|
-
//
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1434
|
+
// Auto-inject action state objects so templates can reference signIn.error, signIn.loading, etc.
|
|
1435
|
+
const renderPrelude = (scriptBody && !scriptUsesAwait) ? scriptBody : '';
|
|
1436
|
+
const allVars = [...queryVars];
|
|
1437
|
+
if (scriptUsesAwait) {
|
|
1438
|
+
for (const v of scriptReturnVars) {
|
|
1439
|
+
if (!allVars.includes(v))
|
|
1440
|
+
allVars.push(v);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
for (const fn of parsed.actionFunctions) {
|
|
1444
|
+
if (!allVars.includes(fn))
|
|
1445
|
+
allVars.push(fn);
|
|
1446
|
+
}
|
|
1202
1447
|
if (!allVars.includes('params'))
|
|
1203
1448
|
allVars.push('params');
|
|
1204
1449
|
if (!allVars.includes('breadcrumbs'))
|
|
@@ -1213,7 +1458,7 @@ function buildRouteObject(opts) {
|
|
|
1213
1458
|
finalRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
|
|
1214
1459
|
}
|
|
1215
1460
|
parts.push(` render(data) {
|
|
1216
|
-
${destructure}${finalRenderBody}
|
|
1461
|
+
${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
|
|
1217
1462
|
return __html;
|
|
1218
1463
|
}`);
|
|
1219
1464
|
return ` {\n${parts.join(',\n')}\n }`;
|
|
@@ -1227,7 +1472,7 @@ function readOrmConfig(projectDir) {
|
|
|
1227
1472
|
if (!ormBlock)
|
|
1228
1473
|
return [];
|
|
1229
1474
|
// Extract schema imports: import { todoSchema } from './src/schemas/todo';
|
|
1230
|
-
const importMap = new Map(); // exportName
|
|
1475
|
+
const importMap = new Map(); // exportName â†' importPath
|
|
1231
1476
|
const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
1232
1477
|
let m;
|
|
1233
1478
|
while ((m = importRegex.exec(source)) !== null) {
|
|
@@ -1364,7 +1609,7 @@ function readDoConfig(projectDir) {
|
|
|
1364
1609
|
if (list.length > 0)
|
|
1365
1610
|
entry.files = list;
|
|
1366
1611
|
}
|
|
1367
|
-
// (inject config removed
|
|
1612
|
+
// (inject config removed â€" DO methods are org-scoped, no auto-injection needed)
|
|
1368
1613
|
entries.push(entry);
|
|
1369
1614
|
}
|
|
1370
1615
|
// Match string shorthand: BINDING: 'ClassName' (skip bindings already found)
|
|
@@ -1482,9 +1727,9 @@ function discoverFilesWithSuffix(dir, suffix) {
|
|
|
1482
1727
|
return out;
|
|
1483
1728
|
}
|
|
1484
1729
|
/**
|
|
1485
|
-
* Scan
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1730
|
+
* Scan DO handler files.
|
|
1731
|
+
* - Class mode: default class extends kuratchiDO
|
|
1732
|
+
* - Function mode: exported functions in *.do.ts files (compiler wraps into DO class)
|
|
1488
1733
|
*/
|
|
1489
1734
|
function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
1490
1735
|
const serverDir = path.join(srcDir, 'server');
|
|
@@ -1512,14 +1757,15 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1512
1757
|
for (const absPath of discoveredFiles) {
|
|
1513
1758
|
const file = path.basename(absPath);
|
|
1514
1759
|
const source = fs.readFileSync(absPath, 'utf-8');
|
|
1515
|
-
|
|
1516
|
-
|
|
1760
|
+
const exportedFunctions = extractExportedFunctions(source);
|
|
1761
|
+
const hasClass = /extends\s+kuratchiDO\b/.test(source);
|
|
1762
|
+
if (!hasClass && exportedFunctions.length === 0)
|
|
1517
1763
|
continue;
|
|
1518
|
-
// Extract class name
|
|
1764
|
+
// Extract class name when class mode is used.
|
|
1519
1765
|
const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+kuratchiDO/);
|
|
1520
|
-
|
|
1766
|
+
const className = classMatch?.[1] ?? null;
|
|
1767
|
+
if (hasClass && !className)
|
|
1521
1768
|
continue;
|
|
1522
|
-
const className = classMatch[1];
|
|
1523
1769
|
// Binding resolution:
|
|
1524
1770
|
// 1) explicit static binding in class
|
|
1525
1771
|
// 2) config-mapped file name (supports .do.ts convention)
|
|
@@ -1544,10 +1790,8 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1544
1790
|
continue;
|
|
1545
1791
|
if (!bindings.has(binding))
|
|
1546
1792
|
continue;
|
|
1547
|
-
// Extract class methods
|
|
1548
|
-
const classMethods = extractClassMethods(source, className);
|
|
1549
|
-
// Extract named exports (custom worker-side helpers)
|
|
1550
|
-
const namedExports = extractNamedExports(source);
|
|
1793
|
+
// Extract class methods in class mode
|
|
1794
|
+
const classMethods = className ? extractClassMethods(source, className) : [];
|
|
1551
1795
|
const fileName = file.replace(/\.ts$/, '');
|
|
1552
1796
|
const existing = fileNameToAbsPath.get(fileName);
|
|
1553
1797
|
if (existing && existing !== absPath) {
|
|
@@ -1558,9 +1802,10 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1558
1802
|
fileName,
|
|
1559
1803
|
absPath,
|
|
1560
1804
|
binding,
|
|
1561
|
-
|
|
1805
|
+
mode: hasClass ? 'class' : 'function',
|
|
1806
|
+
className: className ?? undefined,
|
|
1562
1807
|
classMethods,
|
|
1563
|
-
|
|
1808
|
+
exportedFunctions,
|
|
1564
1809
|
});
|
|
1565
1810
|
}
|
|
1566
1811
|
return handlers;
|
|
@@ -1652,40 +1897,33 @@ function extractClassMethods(source, className) {
|
|
|
1652
1897
|
}
|
|
1653
1898
|
return methods;
|
|
1654
1899
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
*/
|
|
1658
|
-
function extractNamedExports(source) {
|
|
1659
|
-
const exports = [];
|
|
1660
|
-
// export async function name / export function name
|
|
1900
|
+
function extractExportedFunctions(source) {
|
|
1901
|
+
const out = [];
|
|
1661
1902
|
const fnRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
|
|
1662
1903
|
let m;
|
|
1663
1904
|
while ((m = fnRegex.exec(source)) !== null)
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const constRegex = /export\s+const\s+(\w+)/g;
|
|
1667
|
-
while ((m = constRegex.exec(source)) !== null)
|
|
1668
|
-
exports.push(m[1]);
|
|
1669
|
-
return exports;
|
|
1905
|
+
out.push(m[1]);
|
|
1906
|
+
return out;
|
|
1670
1907
|
}
|
|
1671
1908
|
/**
|
|
1672
1909
|
* Generate a proxy module for a DO handler file.
|
|
1673
1910
|
*
|
|
1674
|
-
* The proxy provides
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
* Methods that collide with custom exports are skipped (user's export wins).
|
|
1911
|
+
* The proxy provides auto-RPC function exports.
|
|
1912
|
+
* - Class mode: public class methods become RPC exports.
|
|
1913
|
+
* - Function mode: exported functions become RPC exports, excluding lifecycle hooks.
|
|
1679
1914
|
*/
|
|
1680
1915
|
function generateHandlerProxy(handler, projectDir) {
|
|
1681
1916
|
const doDir = path.join(projectDir, '.kuratchi', 'do');
|
|
1682
1917
|
const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
|
|
1683
1918
|
const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
|
|
1684
|
-
const
|
|
1919
|
+
const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
|
|
1920
|
+
const rpcFunctions = handler.mode === 'function'
|
|
1921
|
+
? handler.exportedFunctions.filter((n) => !lifecycle.has(n))
|
|
1922
|
+
: handler.classMethods.filter((m) => m.visibility === 'public').map((m) => m.name);
|
|
1685
1923
|
const methods = handler.classMethods.map((m) => ({ ...m }));
|
|
1686
1924
|
const methodMap = new Map(methods.map((m) => [m.name, m]));
|
|
1687
1925
|
let changed = true;
|
|
1688
|
-
while (changed) {
|
|
1926
|
+
while (changed && handler.mode === 'class') {
|
|
1689
1927
|
changed = false;
|
|
1690
1928
|
for (const m of methods) {
|
|
1691
1929
|
if (m.hasWorkerContextCalls)
|
|
@@ -1700,15 +1938,56 @@ function generateHandlerProxy(handler, projectDir) {
|
|
|
1700
1938
|
}
|
|
1701
1939
|
}
|
|
1702
1940
|
}
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1941
|
+
const workerContextMethods = handler.mode === 'class'
|
|
1942
|
+
? methods.filter((m) => m.visibility === 'public' && m.hasWorkerContextCalls).map((m) => m.name)
|
|
1943
|
+
: [];
|
|
1944
|
+
const asyncMethods = handler.mode === 'class'
|
|
1945
|
+
? methods.filter((m) => m.isAsync).map((m) => m.name)
|
|
1946
|
+
: [];
|
|
1708
1947
|
const lines = [
|
|
1709
|
-
`// Auto-generated by KuratchiJS compiler
|
|
1948
|
+
`// Auto-generated by KuratchiJS compiler â€" do not edit.`,
|
|
1710
1949
|
`import { __getDoStub } from '${RUNTIME_DO_IMPORT}';`,
|
|
1711
|
-
`import ${handlerLocal} from '${origRelPath}'
|
|
1950
|
+
...(handler.mode === 'class' ? [`import ${handlerLocal} from '${origRelPath}';`] : []),
|
|
1951
|
+
``,
|
|
1952
|
+
`const __FD_TAG = '__kuratchi_form_data__';`,
|
|
1953
|
+
`function __isPlainObject(__v) {`,
|
|
1954
|
+
` if (!__v || typeof __v !== 'object') return false;`,
|
|
1955
|
+
` const __proto = Object.getPrototypeOf(__v);`,
|
|
1956
|
+
` return __proto === Object.prototype || __proto === null;`,
|
|
1957
|
+
`}`,
|
|
1958
|
+
`function __encodeArg(__v, __seen = new WeakSet()) {`,
|
|
1959
|
+
` if (typeof FormData !== 'undefined' && __v instanceof FormData) {`,
|
|
1960
|
+
` return { [__FD_TAG]: Array.from(__v.entries()) };`,
|
|
1961
|
+
` }`,
|
|
1962
|
+
` if (Array.isArray(__v)) return __v.map((__x) => __encodeArg(__x, __seen));`,
|
|
1963
|
+
` if (__isPlainObject(__v)) {`,
|
|
1964
|
+
` if (__seen.has(__v)) throw new Error('[KuratchiJS] Circular object passed to DO RPC');`,
|
|
1965
|
+
` __seen.add(__v);`,
|
|
1966
|
+
` const __out = {};`,
|
|
1967
|
+
` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __encodeArg(__val, __seen);`,
|
|
1968
|
+
` __seen.delete(__v);`,
|
|
1969
|
+
` return __out;`,
|
|
1970
|
+
` }`,
|
|
1971
|
+
` return __v;`,
|
|
1972
|
+
`}`,
|
|
1973
|
+
`function __decodeArg(__v) {`,
|
|
1974
|
+
` if (Array.isArray(__v)) return __v.map(__decodeArg);`,
|
|
1975
|
+
` if (__isPlainObject(__v)) {`,
|
|
1976
|
+
` const __obj = __v;`,
|
|
1977
|
+
` if (__FD_TAG in __obj) {`,
|
|
1978
|
+
` const __fd = new FormData();`,
|
|
1979
|
+
` const __entries = Array.isArray(__obj[__FD_TAG]) ? __obj[__FD_TAG] : [];`,
|
|
1980
|
+
` for (const __pair of __entries) {`,
|
|
1981
|
+
` if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]);`,
|
|
1982
|
+
` }`,
|
|
1983
|
+
` return __fd;`,
|
|
1984
|
+
` }`,
|
|
1985
|
+
` const __out = {};`,
|
|
1986
|
+
` for (const [__k, __val] of Object.entries(__obj)) __out[__k] = __decodeArg(__val);`,
|
|
1987
|
+
` return __out;`,
|
|
1988
|
+
` }`,
|
|
1989
|
+
` return __v;`,
|
|
1990
|
+
`}`,
|
|
1712
1991
|
``,
|
|
1713
1992
|
];
|
|
1714
1993
|
if (workerContextMethods.length > 0) {
|
|
@@ -1728,29 +2007,22 @@ function generateHandlerProxy(handler, projectDir) {
|
|
|
1728
2007
|
lines.push(` if (typeof __local === 'function' && !__asyncMethods.has(__k)) {`);
|
|
1729
2008
|
lines.push(` return (...__a) => __local.apply(__self, __a);`);
|
|
1730
2009
|
lines.push(` }`);
|
|
1731
|
-
lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a); };`);
|
|
2010
|
+
lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a.map((__x) => __encodeArg(__x))); };`);
|
|
1732
2011
|
lines.push(` },`);
|
|
1733
2012
|
lines.push(` });`);
|
|
1734
|
-
lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args);`);
|
|
2013
|
+
lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args.map(__decodeArg));`);
|
|
1735
2014
|
lines.push(`}`);
|
|
1736
2015
|
lines.push(``);
|
|
1737
2016
|
}
|
|
1738
|
-
// Export
|
|
1739
|
-
for (const method of
|
|
1740
|
-
if (customSet.has(method))
|
|
1741
|
-
continue; // user's export wins
|
|
2017
|
+
// Export RPC methods
|
|
2018
|
+
for (const method of rpcFunctions) {
|
|
1742
2019
|
if (workerContextMethods.includes(method)) {
|
|
1743
2020
|
lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', a); }`);
|
|
1744
2021
|
}
|
|
1745
2022
|
else {
|
|
1746
|
-
lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a); }`);
|
|
2023
|
+
lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a.map((__x) => __encodeArg(__x))); }`);
|
|
1747
2024
|
}
|
|
1748
2025
|
}
|
|
1749
|
-
// Re-export custom named exports from the original file
|
|
1750
|
-
if (handler.namedExports.length > 0) {
|
|
1751
|
-
lines.push(``);
|
|
1752
|
-
lines.push(`export { ${handler.namedExports.join(', ')} } from '${origRelPath}';`);
|
|
1753
|
-
}
|
|
1754
2026
|
return lines.join('\n') + '\n';
|
|
1755
2027
|
}
|
|
1756
2028
|
function generateRoutesModule(opts) {
|
|
@@ -1763,16 +2035,16 @@ function generateRoutesModule(opts) {
|
|
|
1763
2035
|
.map(([status, fn]) => fn)
|
|
1764
2036
|
.join('\n\n');
|
|
1765
2037
|
// Resolve path to the framework's context module from the output directory
|
|
1766
|
-
const contextImport = `import { __setRequestContext, __esc, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
2038
|
+
const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
1767
2039
|
const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
|
|
1768
2040
|
? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
|
|
1769
2041
|
: '';
|
|
1770
|
-
// Auth session init
|
|
2042
|
+
// Auth session init â€" thin cookie parsing injected into Worker entry
|
|
1771
2043
|
let authInit = '';
|
|
1772
2044
|
if (opts.authConfig && opts.authConfig.sessionEnabled) {
|
|
1773
2045
|
const cookieName = opts.authConfig.cookieName;
|
|
1774
2046
|
authInit = `
|
|
1775
|
-
//
|
|
2047
|
+
// â"€â"€ Auth Session Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1776
2048
|
|
|
1777
2049
|
function __parseCookies(header) {
|
|
1778
2050
|
const map = {};
|
|
@@ -1823,7 +2095,7 @@ function __initAuth(request) {
|
|
|
1823
2095
|
...schemaImports,
|
|
1824
2096
|
].join('\n');
|
|
1825
2097
|
migrationInit = `
|
|
1826
|
-
//
|
|
2098
|
+
// â"€â"€ ORM Auto-Migration â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1827
2099
|
|
|
1828
2100
|
let __migrated = false;
|
|
1829
2101
|
const __ormDatabases = [
|
|
@@ -1857,7 +2129,7 @@ async function __runMigrations() {
|
|
|
1857
2129
|
`;
|
|
1858
2130
|
}
|
|
1859
2131
|
}
|
|
1860
|
-
// Auth plugin init
|
|
2132
|
+
// Auth plugin init â€" import config + call @kuratchi/auth setup functions
|
|
1861
2133
|
let authPluginImports = '';
|
|
1862
2134
|
let authPluginInit = '';
|
|
1863
2135
|
const ac = opts.authConfig;
|
|
@@ -1909,14 +2181,14 @@ async function __runMigrations() {
|
|
|
1909
2181
|
}
|
|
1910
2182
|
authPluginImports = imports.join('\n');
|
|
1911
2183
|
authPluginInit = `
|
|
1912
|
-
//
|
|
2184
|
+
// â"€â"€ Auth Plugin Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1913
2185
|
|
|
1914
2186
|
function __initAuthPlugins() {
|
|
1915
2187
|
${initLines.join('\n')}
|
|
1916
2188
|
}
|
|
1917
2189
|
`;
|
|
1918
2190
|
}
|
|
1919
|
-
//
|
|
2191
|
+
// â"€â"€ Durable Object class generation â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1920
2192
|
let doImports = '';
|
|
1921
2193
|
let doClassCode = '';
|
|
1922
2194
|
let doResolverInit = '';
|
|
@@ -1926,7 +2198,28 @@ ${initLines.join('\n')}
|
|
|
1926
2198
|
const doResolverLines = [];
|
|
1927
2199
|
doImportLines.push(`import { DurableObject as __DO } from 'cloudflare:workers';`);
|
|
1928
2200
|
doImportLines.push(`import { initDO as __initDO } from '@kuratchi/orm';`);
|
|
1929
|
-
doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding } from '${RUNTIME_DO_IMPORT}';`);
|
|
2201
|
+
doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding, __setDoContext } from '${RUNTIME_DO_IMPORT}';`);
|
|
2202
|
+
doImportLines.push(`const __DO_FD_TAG = '__kuratchi_form_data__';`);
|
|
2203
|
+
doImportLines.push(`function __isDoPlainObject(__v) {`);
|
|
2204
|
+
doImportLines.push(` if (!__v || typeof __v !== 'object') return false;`);
|
|
2205
|
+
doImportLines.push(` const __proto = Object.getPrototypeOf(__v);`);
|
|
2206
|
+
doImportLines.push(` return __proto === Object.prototype || __proto === null;`);
|
|
2207
|
+
doImportLines.push(`}`);
|
|
2208
|
+
doImportLines.push(`function __decodeDoArg(__v) {`);
|
|
2209
|
+
doImportLines.push(` if (Array.isArray(__v)) return __v.map(__decodeDoArg);`);
|
|
2210
|
+
doImportLines.push(` if (__isDoPlainObject(__v)) {`);
|
|
2211
|
+
doImportLines.push(` if (__DO_FD_TAG in __v) {`);
|
|
2212
|
+
doImportLines.push(` const __fd = new FormData();`);
|
|
2213
|
+
doImportLines.push(` const __entries = Array.isArray(__v[__DO_FD_TAG]) ? __v[__DO_FD_TAG] : [];`);
|
|
2214
|
+
doImportLines.push(` for (const __pair of __entries) { if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]); }`);
|
|
2215
|
+
doImportLines.push(` return __fd;`);
|
|
2216
|
+
doImportLines.push(` }`);
|
|
2217
|
+
doImportLines.push(` const __out = {};`);
|
|
2218
|
+
doImportLines.push(` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __decodeDoArg(__val);`);
|
|
2219
|
+
doImportLines.push(` return __out;`);
|
|
2220
|
+
doImportLines.push(` }`);
|
|
2221
|
+
doImportLines.push(` return __v;`);
|
|
2222
|
+
doImportLines.push(`}`);
|
|
1930
2223
|
// We need getCurrentUser and getOrgStubByName for stub resolvers
|
|
1931
2224
|
doImportLines.push(`import { getCurrentUser as __getCU, getOrgStubByName as __getOSBN } from '@kuratchi/auth';`);
|
|
1932
2225
|
// Group handlers by binding
|
|
@@ -1940,6 +2233,10 @@ ${initLines.join('\n')}
|
|
|
1940
2233
|
for (const doEntry of opts.doConfig) {
|
|
1941
2234
|
const handlers = handlersByBinding.get(doEntry.binding) ?? [];
|
|
1942
2235
|
const ormDb = opts.ormDatabases.find(d => d.binding === doEntry.binding);
|
|
2236
|
+
const fnHandlers = handlers.filter((h) => h.mode === 'function');
|
|
2237
|
+
const initHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onInit'));
|
|
2238
|
+
const alarmHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onAlarm'));
|
|
2239
|
+
const messageHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onMessage'));
|
|
1943
2240
|
// Import schema (paths are relative to project root; prefix ../ since we're in .kuratchi/)
|
|
1944
2241
|
if (ormDb) {
|
|
1945
2242
|
const schemaPath = ormDb.schemaImportPath.replace(/^\.\//, '../');
|
|
@@ -1954,7 +2251,12 @@ ${initLines.join('\n')}
|
|
|
1954
2251
|
if (!handlerImportPath.startsWith('.'))
|
|
1955
2252
|
handlerImportPath = './' + handlerImportPath;
|
|
1956
2253
|
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
1957
|
-
|
|
2254
|
+
if (h.mode === 'class') {
|
|
2255
|
+
doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
|
|
2256
|
+
}
|
|
2257
|
+
else {
|
|
2258
|
+
doImportLines.push(`import * as ${handlerVar} from '${handlerImportPath}';`);
|
|
2259
|
+
}
|
|
1958
2260
|
}
|
|
1959
2261
|
// Generate DO class
|
|
1960
2262
|
doClassLines.push(`export class ${doEntry.className} extends __DO {`);
|
|
@@ -1963,6 +2265,11 @@ ${initLines.join('\n')}
|
|
|
1963
2265
|
if (ormDb) {
|
|
1964
2266
|
doClassLines.push(` this.db = __initDO(ctx.storage.sql, __doSchema_${doEntry.binding});`);
|
|
1965
2267
|
}
|
|
2268
|
+
for (const h of initHandlers) {
|
|
2269
|
+
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2270
|
+
doClassLines.push(` __setDoContext(this);`);
|
|
2271
|
+
doClassLines.push(` Promise.resolve(${handlerVar}.onInit.call(this)).catch((err) => console.error('[kuratchi] DO onInit failed:', err?.message || err));`);
|
|
2272
|
+
}
|
|
1966
2273
|
doClassLines.push(` }`);
|
|
1967
2274
|
if (ormDb) {
|
|
1968
2275
|
doClassLines.push(` async __kuratchiLogActivity(payload) {`);
|
|
@@ -2001,16 +2308,45 @@ ${initLines.join('\n')}
|
|
|
2001
2308
|
doClassLines.push(` return rows;`);
|
|
2002
2309
|
doClassLines.push(` }`);
|
|
2003
2310
|
}
|
|
2311
|
+
// Function-mode lifecycle dispatchers
|
|
2312
|
+
if (alarmHandlers.length > 0) {
|
|
2313
|
+
doClassLines.push(` async alarm(...args) {`);
|
|
2314
|
+
doClassLines.push(` __setDoContext(this);`);
|
|
2315
|
+
for (const h of alarmHandlers) {
|
|
2316
|
+
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2317
|
+
doClassLines.push(` await ${handlerVar}.onAlarm.call(this, ...args);`);
|
|
2318
|
+
}
|
|
2319
|
+
doClassLines.push(` }`);
|
|
2320
|
+
}
|
|
2321
|
+
if (messageHandlers.length > 0) {
|
|
2322
|
+
doClassLines.push(` webSocketMessage(...args) {`);
|
|
2323
|
+
doClassLines.push(` __setDoContext(this);`);
|
|
2324
|
+
for (const h of messageHandlers) {
|
|
2325
|
+
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2326
|
+
doClassLines.push(` ${handlerVar}.onMessage.call(this, ...args);`);
|
|
2327
|
+
}
|
|
2328
|
+
doClassLines.push(` }`);
|
|
2329
|
+
}
|
|
2004
2330
|
doClassLines.push(`}`);
|
|
2005
|
-
// Apply handler methods to prototype
|
|
2331
|
+
// Apply handler methods to prototype (outside class body)
|
|
2006
2332
|
for (const h of handlers) {
|
|
2007
2333
|
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2008
|
-
|
|
2009
|
-
|
|
2334
|
+
if (h.mode === 'class') {
|
|
2335
|
+
doClassLines.push(`for (const __k of Object.getOwnPropertyNames(${handlerVar}.prototype)) { if (__k !== 'constructor') ${doEntry.className}.prototype[__k] = function(...__a){ __setDoContext(this); return ${handlerVar}.prototype[__k].apply(this, __a.map(__decodeDoArg)); }; }`);
|
|
2336
|
+
doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
|
|
2337
|
+
}
|
|
2338
|
+
else {
|
|
2339
|
+
const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
|
|
2340
|
+
for (const fn of h.exportedFunctions) {
|
|
2341
|
+
if (lifecycle.has(fn))
|
|
2342
|
+
continue;
|
|
2343
|
+
doClassLines.push(`${doEntry.className}.prototype[${JSON.stringify(fn)}] = function(...__a){ __setDoContext(this); return ${handlerVar}.${fn}.apply(this, __a.map(__decodeDoArg)); };`);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2010
2346
|
}
|
|
2011
2347
|
// Register stub resolver
|
|
2012
2348
|
if (doEntry.stubId) {
|
|
2013
|
-
// Config-driven: e.g. stubId: 'user.orgId'
|
|
2349
|
+
// Config-driven: e.g. stubId: 'user.orgId' â†' __u.orgId
|
|
2014
2350
|
const fieldPath = doEntry.stubId.startsWith('user.') ? `__u.${doEntry.stubId.slice(5)}` : doEntry.stubId;
|
|
2015
2351
|
const checkField = doEntry.stubId.startsWith('user.') ? doEntry.stubId.slice(5) : doEntry.stubId;
|
|
2016
2352
|
doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
|
|
@@ -2020,37 +2356,37 @@ ${initLines.join('\n')}
|
|
|
2020
2356
|
doResolverLines.push(` });`);
|
|
2021
2357
|
}
|
|
2022
2358
|
else {
|
|
2023
|
-
// No stubId config
|
|
2024
|
-
doResolverLines.push(` // No 'stubId' config for ${doEntry.binding}
|
|
2359
|
+
// No stubId config â€" stub must be obtained manually
|
|
2360
|
+
doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} â€" stub must be obtained manually`);
|
|
2025
2361
|
}
|
|
2026
2362
|
}
|
|
2027
2363
|
doImports = doImportLines.join('\n');
|
|
2028
|
-
doClassCode = `\n//
|
|
2364
|
+
doClassCode = `\n// â"€â"€ Durable Object Classes (generated) â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n` + doClassLines.join('\n') + '\n';
|
|
2029
2365
|
doResolverInit = `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`;
|
|
2030
2366
|
}
|
|
2031
|
-
return `// Generated by KuratchiJS compiler
|
|
2367
|
+
return `// Generated by KuratchiJS compiler â€" do not edit.
|
|
2032
2368
|
${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
|
|
2033
2369
|
${workerImport}
|
|
2034
2370
|
${contextImport}
|
|
2035
|
-
${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
|
|
2371
|
+
${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
|
|
2036
2372
|
|
|
2037
|
-
//
|
|
2373
|
+
// â"€â"€ Assets â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2038
2374
|
|
|
2039
2375
|
const __assets = {
|
|
2040
2376
|
${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')}
|
|
2041
2377
|
};
|
|
2042
2378
|
|
|
2043
|
-
//
|
|
2379
|
+
// â"€â"€ Router â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2044
2380
|
|
|
2045
|
-
const __staticRoutes = new Map(); // exact path
|
|
2381
|
+
const __staticRoutes = new Map(); // exact path â†' index (O(1) lookup)
|
|
2046
2382
|
const __dynamicRoutes = []; // regex-based routes (params/wildcards)
|
|
2047
2383
|
|
|
2048
2384
|
function __addRoute(pattern, index) {
|
|
2049
2385
|
if (!pattern.includes(':') && !pattern.includes('*')) {
|
|
2050
|
-
// Static route
|
|
2386
|
+
// Static route â€" direct Map lookup, no regex needed
|
|
2051
2387
|
__staticRoutes.set(pattern, index);
|
|
2052
2388
|
} else {
|
|
2053
|
-
// Dynamic route
|
|
2389
|
+
// Dynamic route â€" build regex for param extraction
|
|
2054
2390
|
const paramNames = [];
|
|
2055
2391
|
let regexStr = pattern
|
|
2056
2392
|
.replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
|
|
@@ -2076,13 +2412,13 @@ function __match(pathname) {
|
|
|
2076
2412
|
return null;
|
|
2077
2413
|
}
|
|
2078
2414
|
|
|
2079
|
-
//
|
|
2415
|
+
// â"€â"€ Layout â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2080
2416
|
|
|
2081
2417
|
${layoutBlock}
|
|
2082
2418
|
|
|
2083
2419
|
${layoutActionsBlock}
|
|
2084
2420
|
|
|
2085
|
-
//
|
|
2421
|
+
// â"€â"€ Error pages â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2086
2422
|
|
|
2087
2423
|
const __errorMessages = {
|
|
2088
2424
|
400: 'Bad Request',
|
|
@@ -2097,7 +2433,7 @@ const __errorMessages = {
|
|
|
2097
2433
|
503: 'Service Unavailable',
|
|
2098
2434
|
};
|
|
2099
2435
|
|
|
2100
|
-
// Built-in default error page
|
|
2436
|
+
// Built-in default error page â€" clean, dark, minimal, centered
|
|
2101
2437
|
function __errorPage(status, detail) {
|
|
2102
2438
|
const title = __errorMessages[status] || 'Error';
|
|
2103
2439
|
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>' : '';
|
|
@@ -2119,8 +2455,8 @@ function __error(status, detail) {
|
|
|
2119
2455
|
return __errorPage(status, detail);
|
|
2120
2456
|
}
|
|
2121
2457
|
|
|
2122
|
-
${opts.compiledComponents.length > 0 ? '//
|
|
2123
|
-
//
|
|
2458
|
+
${opts.compiledComponents.length > 0 ? '// â"€â"€ Components â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
|
|
2459
|
+
// â"€â"€ Route definitions â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2124
2460
|
|
|
2125
2461
|
const routes = [
|
|
2126
2462
|
${opts.compiledRoutes.join(',\n')}
|
|
@@ -2128,7 +2464,7 @@ ${opts.compiledRoutes.join(',\n')}
|
|
|
2128
2464
|
|
|
2129
2465
|
for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
|
|
2130
2466
|
|
|
2131
|
-
//
|
|
2467
|
+
// â"€â"€ Response helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2132
2468
|
|
|
2133
2469
|
const __defaultSecHeaders = {
|
|
2134
2470
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -2232,13 +2568,13 @@ async function __runRuntimeError(ctx, error) {
|
|
|
2232
2568
|
return null;
|
|
2233
2569
|
}
|
|
2234
2570
|
|
|
2235
|
-
//
|
|
2571
|
+
// â"€â"€ Exported Worker entrypoint â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2236
2572
|
|
|
2237
|
-
export default class extends WorkerEntrypoint {
|
|
2238
|
-
async fetch(request) {
|
|
2239
|
-
__setRequestContext(this.ctx, request);
|
|
2240
|
-
globalThis.__cloudflare_env__ = __env;
|
|
2241
|
-
${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
|
|
2573
|
+
export default class extends WorkerEntrypoint {
|
|
2574
|
+
async fetch(request) {
|
|
2575
|
+
__setRequestContext(this.ctx, request);
|
|
2576
|
+
globalThis.__cloudflare_env__ = __env;
|
|
2577
|
+
${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
|
|
2242
2578
|
const __runtimeCtx = {
|
|
2243
2579
|
request,
|
|
2244
2580
|
env: __env,
|
|
@@ -2277,6 +2613,24 @@ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n
|
|
|
2277
2613
|
__runtimeCtx.params = match.params;
|
|
2278
2614
|
const route = routes[match.index];
|
|
2279
2615
|
__setLocal('params', match.params);
|
|
2616
|
+
|
|
2617
|
+
// API route: dispatch to method handler
|
|
2618
|
+
if (route.__api) {
|
|
2619
|
+
const method = request.method;
|
|
2620
|
+
if (method === 'OPTIONS') {
|
|
2621
|
+
const handler = route['OPTIONS'];
|
|
2622
|
+
if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
|
|
2623
|
+
const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
|
|
2624
|
+
return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
|
|
2625
|
+
}
|
|
2626
|
+
const handler = route[method];
|
|
2627
|
+
if (typeof handler !== 'function') {
|
|
2628
|
+
const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
|
|
2629
|
+
return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
|
|
2630
|
+
}
|
|
2631
|
+
return __secHeaders(await handler(__runtimeCtx));
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2280
2634
|
const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
|
|
2281
2635
|
const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
|
|
2282
2636
|
let __qArgs = [];
|
|
@@ -2350,7 +2704,10 @@ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n
|
|
|
2350
2704
|
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
2351
2705
|
data.params = match.params;
|
|
2352
2706
|
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
2353
|
-
|
|
2707
|
+
const __allActions = Object.assign({}, route.actions, __layoutActions || {});
|
|
2708
|
+
Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
|
|
2709
|
+
const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
|
|
2710
|
+
data[actionName] = { error: __errMsg, loading: false, success: false };
|
|
2354
2711
|
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2355
2712
|
}
|
|
2356
2713
|
// Fetch-based actions return lightweight JSON (no page re-render)
|
|
@@ -2373,11 +2730,14 @@ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n
|
|
|
2373
2730
|
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
2374
2731
|
data.params = match.params;
|
|
2375
2732
|
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
2733
|
+
const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
|
|
2734
|
+
Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
|
|
2376
2735
|
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2377
2736
|
} catch (err) {
|
|
2378
2737
|
console.error('[kuratchi] Route load/render error:', err);
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2738
|
+
const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
|
|
2739
|
+
const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
|
|
2740
|
+
return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
|
|
2381
2741
|
}
|
|
2382
2742
|
};
|
|
2383
2743
|
|