@kuratchi/js 0.0.2 → 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 +914 -349
- 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 +8 -4
- package/dist/index.js +8 -3
- 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 +2 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/page-error.d.ts +16 -0
- package/dist/runtime/page-error.js +20 -0
- package/dist/runtime/runtime.d.ts +5 -0
- package/dist/runtime/runtime.js +6 -0
- package/dist/runtime/types.d.ts +85 -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$/);
|
|
@@ -531,6 +665,8 @@ export function compile(options) {
|
|
|
531
665
|
const authConfig = readAuthConfig(projectDir);
|
|
532
666
|
// Read Durable Object config and discover handler files
|
|
533
667
|
const doConfig = readDoConfig(projectDir);
|
|
668
|
+
const containerConfig = readWorkerClassConfig(projectDir, 'containers');
|
|
669
|
+
const workflowConfig = readWorkerClassConfig(projectDir, 'workflows');
|
|
534
670
|
const doHandlers = doConfig.length > 0
|
|
535
671
|
? discoverDoHandlers(srcDir, doConfig, ormDatabases)
|
|
536
672
|
: [];
|
|
@@ -571,11 +707,125 @@ export function compile(options) {
|
|
|
571
707
|
}
|
|
572
708
|
}
|
|
573
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
|
+
};
|
|
574
824
|
// Parse and compile each route
|
|
575
825
|
const compiledRoutes = [];
|
|
576
826
|
const allImports = [];
|
|
577
827
|
let moduleCounter = 0;
|
|
578
|
-
// Layout server import resolution
|
|
828
|
+
// Layout server import resolution â€" resolve non-component imports to module IDs
|
|
579
829
|
let isLayoutAsync = false;
|
|
580
830
|
let compiledLayoutActions = null;
|
|
581
831
|
if (compiledLayout && fs.existsSync(path.join(routesDir, 'layout.html'))) {
|
|
@@ -590,39 +840,7 @@ export function compile(options) {
|
|
|
590
840
|
if (!pathMatch)
|
|
591
841
|
continue;
|
|
592
842
|
const origPath = pathMatch[1];
|
|
593
|
-
const
|
|
594
|
-
let importPath;
|
|
595
|
-
if (isBareModule) {
|
|
596
|
-
importPath = origPath;
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
let absImport;
|
|
600
|
-
if (origPath.startsWith('$')) {
|
|
601
|
-
const slashIdx = origPath.indexOf('/');
|
|
602
|
-
const folder = origPath.slice(1, slashIdx);
|
|
603
|
-
const rest = origPath.slice(slashIdx + 1);
|
|
604
|
-
absImport = path.join(srcDir, folder, rest);
|
|
605
|
-
// Redirect DO handler imports to generated proxy modules
|
|
606
|
-
const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
|
|
607
|
-
if (doProxyPath) {
|
|
608
|
-
absImport = doProxyPath;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
absImport = path.resolve(layoutFileDir, origPath);
|
|
613
|
-
}
|
|
614
|
-
let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
|
|
615
|
-
if (!relPath.startsWith('.'))
|
|
616
|
-
relPath = './' + relPath;
|
|
617
|
-
let resolvedExt = '';
|
|
618
|
-
for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
|
|
619
|
-
if (fs.existsSync(absImport + ext)) {
|
|
620
|
-
resolvedExt = ext;
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
importPath = relPath + resolvedExt;
|
|
625
|
-
}
|
|
843
|
+
const importPath = resolveCompiledImportPath(origPath, layoutFileDir, outFileDir);
|
|
626
844
|
const moduleId = `__m${moduleCounter++}`;
|
|
627
845
|
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
628
846
|
const namesMatch = imp.match(/import\s*\{([^}]+)\}/);
|
|
@@ -663,7 +881,7 @@ export function compile(options) {
|
|
|
663
881
|
}
|
|
664
882
|
}
|
|
665
883
|
}
|
|
666
|
-
// Detect if the compiled layout uses await
|
|
884
|
+
// Detect if the compiled layout uses await â†' make it async
|
|
667
885
|
isLayoutAsync = /\bawait\b/.test(compiledLayout);
|
|
668
886
|
if (isLayoutAsync) {
|
|
669
887
|
compiledLayout = compiledLayout.replace(/^function __layout\(/, 'async function __layout(');
|
|
@@ -672,72 +890,62 @@ export function compile(options) {
|
|
|
672
890
|
for (let i = 0; i < routeFiles.length; i++) {
|
|
673
891
|
const rf = routeFiles[i];
|
|
674
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) ──
|
|
675
922
|
const source = fs.readFileSync(fullPath, 'utf-8');
|
|
676
923
|
const parsed = parseFile(source);
|
|
677
|
-
|
|
678
|
-
// Build a mapping: functionName → moduleId for all imports in this route
|
|
924
|
+
// Build a mapping: functionName â†' moduleId for all imports in this route
|
|
679
925
|
const fnToModule = {};
|
|
680
926
|
const outFileDir = path.join(projectDir, '.kuratchi');
|
|
681
|
-
|
|
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) {
|
|
682
936
|
const routeFileDir = path.dirname(fullPath);
|
|
683
|
-
for (const imp of
|
|
937
|
+
for (const imp of routeServerImports) {
|
|
684
938
|
const pathMatch = imp.match(/from\s+['"]([^'"]+)['"]/);
|
|
685
939
|
if (!pathMatch)
|
|
686
940
|
continue;
|
|
687
941
|
const origPath = pathMatch[1];
|
|
688
|
-
// Bare module specifiers (packages)
|
|
689
|
-
const
|
|
690
|
-
let importPath;
|
|
691
|
-
if (isBareModule) {
|
|
692
|
-
// Package import: @kuratchi/auth, KuratchiJS, cloudflare:workers, etc.
|
|
693
|
-
importPath = origPath;
|
|
694
|
-
}
|
|
695
|
-
else {
|
|
696
|
-
let absImport;
|
|
697
|
-
if (origPath.startsWith('$')) {
|
|
698
|
-
// Dynamic $folder/ alias → src/folder/
|
|
699
|
-
const slashIdx = origPath.indexOf('/');
|
|
700
|
-
const folder = origPath.slice(1, slashIdx);
|
|
701
|
-
const rest = origPath.slice(slashIdx + 1);
|
|
702
|
-
absImport = path.join(srcDir, folder, rest);
|
|
703
|
-
// Redirect DO handler imports to generated proxy modules
|
|
704
|
-
const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
|
|
705
|
-
if (doProxyPath) {
|
|
706
|
-
absImport = doProxyPath;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
// Resolve the import relative to the route file
|
|
711
|
-
absImport = path.resolve(routeFileDir, origPath);
|
|
712
|
-
}
|
|
713
|
-
// Make it relative to the output directory
|
|
714
|
-
let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
|
|
715
|
-
if (!relPath.startsWith('.'))
|
|
716
|
-
relPath = './' + relPath;
|
|
717
|
-
// Check if the resolved file exists (try .ts, .js extensions)
|
|
718
|
-
let resolvedExt = '';
|
|
719
|
-
for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
|
|
720
|
-
if (fs.existsSync(absImport + ext)) {
|
|
721
|
-
resolvedExt = ext;
|
|
722
|
-
break;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
importPath = relPath + resolvedExt;
|
|
726
|
-
}
|
|
942
|
+
// Bare module specifiers (packages) â€" pass through as-is
|
|
943
|
+
const importPath = resolveCompiledImportPath(origPath, routeFileDir, outFileDir);
|
|
727
944
|
const moduleId = `__m${moduleCounter++}`;
|
|
728
945
|
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
729
946
|
// Extract named imports and map them to this module
|
|
730
|
-
const
|
|
731
|
-
if (
|
|
732
|
-
const names = namesMatch[1]
|
|
733
|
-
.split(',')
|
|
734
|
-
.map(n => n.trim())
|
|
735
|
-
.filter(Boolean)
|
|
736
|
-
.map(n => {
|
|
737
|
-
const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
|
|
738
|
-
return parts[1] || parts[0] || '';
|
|
739
|
-
})
|
|
740
|
-
.filter(Boolean);
|
|
947
|
+
const names = parseNamedImportBindings(imp);
|
|
948
|
+
if (names.length > 0) {
|
|
741
949
|
for (const name of names) {
|
|
742
950
|
fnToModule[name] = moduleId;
|
|
743
951
|
}
|
|
@@ -750,7 +958,7 @@ export function compile(options) {
|
|
|
750
958
|
}
|
|
751
959
|
}
|
|
752
960
|
// Build per-route component names from explicit imports
|
|
753
|
-
// componentImports: { StatCard: 'stat-card' }
|
|
961
|
+
// componentImports: { StatCard: 'stat-card' } â†' componentNames maps PascalCase â†' fileName
|
|
754
962
|
const routeComponentNames = new Map();
|
|
755
963
|
for (const [pascalName, fileName] of Object.entries(parsed.componentImports)) {
|
|
756
964
|
// Compile the component on first use
|
|
@@ -765,22 +973,22 @@ export function compile(options) {
|
|
|
765
973
|
// We then scan the route template for that component's usage and extract the bound values.
|
|
766
974
|
for (const [pascalName, compFileName] of routeComponentNames.entries()) {
|
|
767
975
|
const actionPropNames = componentActionCache.get(compFileName);
|
|
768
|
-
if (!actionPropNames || actionPropNames.size === 0)
|
|
769
|
-
continue;
|
|
770
976
|
// Find all usages of <PascalName ...> in the route template and extract prop bindings.
|
|
771
977
|
// Match <ComponentName ... propName={value} ... > across multiple lines.
|
|
772
978
|
const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)/>`, 'g');
|
|
773
979
|
for (const tagMatch of parsed.template.matchAll(compTagRegex)) {
|
|
774
980
|
const attrs = tagMatch[1];
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
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
|
+
}
|
|
784
992
|
}
|
|
785
993
|
}
|
|
786
994
|
}
|
|
@@ -789,7 +997,7 @@ export function compile(options) {
|
|
|
789
997
|
// Compile template to render function body (pass component names and action names)
|
|
790
998
|
// An identifier is a valid server action if it is either:
|
|
791
999
|
// 1. Directly imported (present in fnToModule), or
|
|
792
|
-
// 2. A top-level script declaration (present in dataVars)
|
|
1000
|
+
// 2. A top-level script declaration (present in dataVars) â€" covers cases like
|
|
793
1001
|
// `const fn = importedFn` or `async function fn() {}` where the binding
|
|
794
1002
|
// is locally declared but delegates to an imported function.
|
|
795
1003
|
const dataVarsSet = new Set(parsed.dataVars);
|
|
@@ -867,6 +1075,17 @@ export function compile(options) {
|
|
|
867
1075
|
// Collect only the components that were actually imported by routes
|
|
868
1076
|
const compiledComponents = Array.from(compiledComponentCache.values());
|
|
869
1077
|
// Generate the routes module
|
|
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
|
+
}
|
|
1088
|
+
const hasRuntime = !!runtimeImportPath;
|
|
870
1089
|
const output = generateRoutesModule({
|
|
871
1090
|
projectDir,
|
|
872
1091
|
serverImports: allImports,
|
|
@@ -882,6 +1101,8 @@ export function compile(options) {
|
|
|
882
1101
|
isDev: options.isDev ?? false,
|
|
883
1102
|
isLayoutAsync,
|
|
884
1103
|
compiledLayoutActions,
|
|
1104
|
+
hasRuntime,
|
|
1105
|
+
runtimeImportPath,
|
|
885
1106
|
});
|
|
886
1107
|
// Write to .kuratchi/routes.js
|
|
887
1108
|
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
|
|
@@ -895,16 +1116,25 @@ export function compile(options) {
|
|
|
895
1116
|
// worker.js explicitly re-exports them so wrangler.jsonc can reference a
|
|
896
1117
|
// stable filename while routes.js is freely regenerated.
|
|
897
1118
|
const workerFile = path.join(outDir, 'worker.js');
|
|
1119
|
+
const workerClassExports = [...containerConfig, ...workflowConfig]
|
|
1120
|
+
.map((entry) => {
|
|
1121
|
+
const importPath = toWorkerImportPath(projectDir, outDir, entry.file);
|
|
1122
|
+
if (entry.exportKind === 'default') {
|
|
1123
|
+
return `export { default as ${entry.className} } from '${importPath}';`;
|
|
1124
|
+
}
|
|
1125
|
+
return `export { ${entry.className} } from '${importPath}';`;
|
|
1126
|
+
});
|
|
898
1127
|
const workerLines = [
|
|
899
1128
|
'// Auto-generated by kuratchi \u2014 do not edit.',
|
|
900
1129
|
"export { default } from './routes.js';",
|
|
901
1130
|
...doConfig.map(c => `export { ${c.className} } from './routes.js';`),
|
|
1131
|
+
...workerClassExports,
|
|
902
1132
|
'',
|
|
903
1133
|
];
|
|
904
1134
|
writeIfChanged(workerFile, workerLines.join('\n'));
|
|
905
1135
|
return workerFile;
|
|
906
1136
|
}
|
|
907
|
-
//
|
|
1137
|
+
// â"€â"€ Helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
908
1138
|
/**
|
|
909
1139
|
* Write a file only if its content has changed.
|
|
910
1140
|
* Prevents unnecessary filesystem events that would retrigger wrangler's file watcher.
|
|
@@ -998,7 +1228,7 @@ function readUiTheme(projectDir) {
|
|
|
998
1228
|
console.warn('[kuratchi] ui.theme: "default" configured but @kuratchi/ui theme.css not found');
|
|
999
1229
|
return null;
|
|
1000
1230
|
}
|
|
1001
|
-
// Custom path
|
|
1231
|
+
// Custom path â€" resolve relative to project root
|
|
1002
1232
|
const customPath = path.resolve(projectDir, themeValue);
|
|
1003
1233
|
if (fs.existsSync(customPath)) {
|
|
1004
1234
|
return fs.readFileSync(customPath, 'utf-8');
|
|
@@ -1016,7 +1246,7 @@ function resolvePackageComponent(projectDir, pkgName, componentFile) {
|
|
|
1016
1246
|
if (fs.existsSync(nmPath))
|
|
1017
1247
|
return nmPath;
|
|
1018
1248
|
// 2. Try workspace layout: project is in apps/X or packages/X, sibling packages in packages/
|
|
1019
|
-
// @kuratchi/ui
|
|
1249
|
+
// @kuratchi/ui â†' kuratchi-ui (convention: scope stripped, slash â†' dash)
|
|
1020
1250
|
const pkgDirName = pkgName.replace(/^@/, '').replace(/\//g, '-');
|
|
1021
1251
|
const workspaceRoot = path.resolve(projectDir, '../..');
|
|
1022
1252
|
const wsPath = path.join(workspaceRoot, 'packages', pkgDirName, 'src', 'lib', componentFile + '.html');
|
|
@@ -1054,32 +1284,49 @@ function discoverRoutes(routesDir) {
|
|
|
1054
1284
|
for (const entry of entries) {
|
|
1055
1285
|
if (entry.isDirectory()) {
|
|
1056
1286
|
const childPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1057
|
-
// Folder-based route: routes/db/page.html
|
|
1287
|
+
// Folder-based page route: routes/db/page.html → /db
|
|
1058
1288
|
const pageFile = path.join(dir, entry.name, 'page.html');
|
|
1059
1289
|
if (fs.existsSync(pageFile)) {
|
|
1060
1290
|
const routeFile = `${childPrefix}/page.html`;
|
|
1061
1291
|
if (!registered.has(routeFile)) {
|
|
1062
1292
|
registered.add(routeFile);
|
|
1063
|
-
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' });
|
|
1064
1303
|
}
|
|
1065
1304
|
}
|
|
1066
1305
|
// Always recurse into subdirectory (for nested routes like /admin/roles)
|
|
1067
1306
|
walk(path.join(dir, entry.name), childPrefix);
|
|
1068
1307
|
}
|
|
1069
1308
|
else if (entry.name === 'layout.html' || entry.name === '404.html' || entry.name === '500.html') {
|
|
1070
|
-
// Skip
|
|
1309
|
+
// Skip — layout.html is the app layout, 404/500 are error pages, not routes
|
|
1071
1310
|
continue;
|
|
1072
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
|
+
}
|
|
1073
1320
|
else if (entry.name === 'page.html') {
|
|
1074
|
-
// page.html in current directory
|
|
1321
|
+
// page.html in current directory → index route for this prefix
|
|
1075
1322
|
const routeFile = prefix ? `${prefix}/page.html` : 'page.html';
|
|
1076
1323
|
if (!registered.has(routeFile)) {
|
|
1077
1324
|
registered.add(routeFile);
|
|
1078
|
-
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix) });
|
|
1325
|
+
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix), type: 'page' });
|
|
1079
1326
|
}
|
|
1080
1327
|
}
|
|
1081
1328
|
else if (entry.name.endsWith('.html') && entry.name !== 'page.html') {
|
|
1082
|
-
// File-based route: routes/about.html
|
|
1329
|
+
// File-based route: routes/about.html → /about (fallback)
|
|
1083
1330
|
const name = prefix
|
|
1084
1331
|
? `${prefix}/${entry.name.replace('.html', '')}`
|
|
1085
1332
|
: entry.name.replace('.html', '');
|
|
@@ -1087,6 +1334,7 @@ function discoverRoutes(routesDir) {
|
|
|
1087
1334
|
file: prefix ? `${prefix}/${entry.name}` : entry.name,
|
|
1088
1335
|
name,
|
|
1089
1336
|
layouts: getLayoutsForPrefix(prefix),
|
|
1337
|
+
type: 'page',
|
|
1090
1338
|
});
|
|
1091
1339
|
}
|
|
1092
1340
|
}
|
|
@@ -1105,18 +1353,33 @@ function buildRouteObject(opts) {
|
|
|
1105
1353
|
const hasFns = Object.keys(fnToModule).length > 0;
|
|
1106
1354
|
const parts = [];
|
|
1107
1355
|
parts.push(` pattern: '${pattern}'`);
|
|
1108
|
-
|
|
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.
|
|
1109
1372
|
const hasDataGetQueries = Array.isArray(parsed.dataGetQueries) && parsed.dataGetQueries.length > 0;
|
|
1110
|
-
if ((
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1373
|
+
if ((scriptBody && scriptUsesAwait) || hasDataGetQueries) {
|
|
1374
|
+
let loadBody = '';
|
|
1375
|
+
if (scriptBody && scriptUsesAwait) {
|
|
1376
|
+
loadBody = scriptBody;
|
|
1377
|
+
}
|
|
1115
1378
|
// Inject data-get query state blocks into load scope.
|
|
1116
1379
|
// Each query exposes:
|
|
1117
1380
|
// { state, loading, error, data, empty, success }
|
|
1381
|
+
const queries = parsed.dataGetQueries;
|
|
1118
1382
|
if (hasDataGetQueries) {
|
|
1119
|
-
const queries = parsed.dataGetQueries;
|
|
1120
1383
|
const queryLines = [];
|
|
1121
1384
|
for (const q of queries) {
|
|
1122
1385
|
const fnName = q.fnName;
|
|
@@ -1139,25 +1402,13 @@ function buildRouteObject(opts) {
|
|
|
1139
1402
|
queryLines.push(` }`);
|
|
1140
1403
|
queryLines.push(`}`);
|
|
1141
1404
|
}
|
|
1142
|
-
|
|
1405
|
+
loadBody = [loadBody, queryLines.join('\n')].filter(Boolean).join('\n');
|
|
1143
1406
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
|
|
1148
|
-
continue;
|
|
1149
|
-
const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
|
|
1150
|
-
body = body.replace(callRegex, `${moduleId}.${fnName}(`);
|
|
1151
|
-
}
|
|
1152
|
-
// Determine if body uses await
|
|
1153
|
-
const isAsync = /\bawait\b/.test(body) || hasDataGetQueries;
|
|
1154
|
-
// Return an object with all declared data variables
|
|
1155
|
-
const returnObj = parsed.dataVars.length > 0
|
|
1156
|
-
? `\n return { ${parsed.dataVars.join(', ')} };`
|
|
1157
|
-
: '';
|
|
1158
|
-
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 }`);
|
|
1159
1410
|
}
|
|
1160
|
-
// Actions
|
|
1411
|
+
// Actions â€" functions referenced via action={fn} in the template
|
|
1161
1412
|
if (hasFns && parsed.actionFunctions.length > 0) {
|
|
1162
1413
|
const actionEntries = parsed.actionFunctions
|
|
1163
1414
|
.map(fn => {
|
|
@@ -1167,7 +1418,7 @@ function buildRouteObject(opts) {
|
|
|
1167
1418
|
.join(', ');
|
|
1168
1419
|
parts.push(` actions: { ${actionEntries} }`);
|
|
1169
1420
|
}
|
|
1170
|
-
// RPC
|
|
1421
|
+
// RPC â€" functions referenced via data-poll={fn(args)} in the template
|
|
1171
1422
|
if (hasFns && parsed.pollFunctions.length > 0) {
|
|
1172
1423
|
const rpcEntries = parsed.pollFunctions
|
|
1173
1424
|
.map(fn => {
|
|
@@ -1178,12 +1429,21 @@ function buildRouteObject(opts) {
|
|
|
1178
1429
|
.join(', ');
|
|
1179
1430
|
parts.push(` rpc: { ${rpcEntries} }`);
|
|
1180
1431
|
}
|
|
1181
|
-
// Render function
|
|
1432
|
+
// Render function â€" template compiled to JS with native flow control
|
|
1182
1433
|
// Destructure data vars so templates reference them directly (e.g., {todos} not {data.todos})
|
|
1183
|
-
//
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
+
}
|
|
1187
1447
|
if (!allVars.includes('params'))
|
|
1188
1448
|
allVars.push('params');
|
|
1189
1449
|
if (!allVars.includes('breadcrumbs'))
|
|
@@ -1198,7 +1458,7 @@ function buildRouteObject(opts) {
|
|
|
1198
1458
|
finalRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
|
|
1199
1459
|
}
|
|
1200
1460
|
parts.push(` render(data) {
|
|
1201
|
-
${destructure}${finalRenderBody}
|
|
1461
|
+
${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
|
|
1202
1462
|
return __html;
|
|
1203
1463
|
}`);
|
|
1204
1464
|
return ` {\n${parts.join(',\n')}\n }`;
|
|
@@ -1212,7 +1472,7 @@ function readOrmConfig(projectDir) {
|
|
|
1212
1472
|
if (!ormBlock)
|
|
1213
1473
|
return [];
|
|
1214
1474
|
// Extract schema imports: import { todoSchema } from './src/schemas/todo';
|
|
1215
|
-
const importMap = new Map(); // exportName
|
|
1475
|
+
const importMap = new Map(); // exportName â†' importPath
|
|
1216
1476
|
const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
1217
1477
|
let m;
|
|
1218
1478
|
while ((m = importRegex.exec(source)) !== null) {
|
|
@@ -1349,7 +1609,7 @@ function readDoConfig(projectDir) {
|
|
|
1349
1609
|
if (list.length > 0)
|
|
1350
1610
|
entry.files = list;
|
|
1351
1611
|
}
|
|
1352
|
-
// (inject config removed
|
|
1612
|
+
// (inject config removed â€" DO methods are org-scoped, no auto-injection needed)
|
|
1353
1613
|
entries.push(entry);
|
|
1354
1614
|
}
|
|
1355
1615
|
// Match string shorthand: BINDING: 'ClassName' (skip bindings already found)
|
|
@@ -1365,6 +1625,89 @@ function readDoConfig(projectDir) {
|
|
|
1365
1625
|
}
|
|
1366
1626
|
return entries;
|
|
1367
1627
|
}
|
|
1628
|
+
function readWorkerClassConfig(projectDir, key) {
|
|
1629
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
1630
|
+
if (!fs.existsSync(configPath))
|
|
1631
|
+
return [];
|
|
1632
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
1633
|
+
const keyIdx = source.search(new RegExp(`\\b${key}\\s*:\\s*\\{`));
|
|
1634
|
+
if (keyIdx === -1)
|
|
1635
|
+
return [];
|
|
1636
|
+
const braceStart = source.indexOf('{', keyIdx);
|
|
1637
|
+
if (braceStart === -1)
|
|
1638
|
+
return [];
|
|
1639
|
+
const body = extractBalancedBody(source, braceStart, '{', '}');
|
|
1640
|
+
if (body == null)
|
|
1641
|
+
return [];
|
|
1642
|
+
const entries = [];
|
|
1643
|
+
const expectedSuffix = key === 'containers' ? '.container' : '.workflow';
|
|
1644
|
+
const allowedExt = /\.(ts|js|mjs|cjs)$/i;
|
|
1645
|
+
const requiredFilePattern = new RegExp(`\\${expectedSuffix}\\.(ts|js|mjs|cjs)$`, 'i');
|
|
1646
|
+
const resolveClassFromFile = (binding, filePath) => {
|
|
1647
|
+
if (!requiredFilePattern.test(filePath)) {
|
|
1648
|
+
throw new Error(`[kuratchi] ${key}.${binding} must reference a file ending in "${expectedSuffix}.ts|js|mjs|cjs". Received: ${filePath}`);
|
|
1649
|
+
}
|
|
1650
|
+
if (!allowedExt.test(filePath)) {
|
|
1651
|
+
throw new Error(`[kuratchi] ${key}.${binding} file must be a TypeScript or JavaScript module. Received: ${filePath}`);
|
|
1652
|
+
}
|
|
1653
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
1654
|
+
if (!fs.existsSync(absPath)) {
|
|
1655
|
+
throw new Error(`[kuratchi] ${key}.${binding} file not found: ${filePath}`);
|
|
1656
|
+
}
|
|
1657
|
+
const fileSource = fs.readFileSync(absPath, 'utf-8');
|
|
1658
|
+
const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
|
|
1659
|
+
if (defaultClass) {
|
|
1660
|
+
return { className: defaultClass[1], exportKind: 'default' };
|
|
1661
|
+
}
|
|
1662
|
+
const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
|
|
1663
|
+
if (namedClass) {
|
|
1664
|
+
return { className: namedClass[1], exportKind: 'named' };
|
|
1665
|
+
}
|
|
1666
|
+
throw new Error(`[kuratchi] ${key}.${binding} must export a class via "export class X" or "export default class X". File: ${filePath}`);
|
|
1667
|
+
};
|
|
1668
|
+
// Object form:
|
|
1669
|
+
// containers: { WORDPRESS_CONTAINER: { file: 'src/server/containers/wordpress.container.ts', className?: 'WordPressContainer' } }
|
|
1670
|
+
// workflows: { NEW_SITE_WORKFLOW: { file: 'src/server/workflows/new-site.workflow.ts', className?: 'NewSiteWorkflow' } }
|
|
1671
|
+
const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
1672
|
+
let m;
|
|
1673
|
+
while ((m = objRegex.exec(body)) !== null) {
|
|
1674
|
+
const binding = m[1];
|
|
1675
|
+
const entryBody = m[2];
|
|
1676
|
+
const fileMatch = entryBody.match(/file\s*:\s*['"]([^'"]+)['"]/);
|
|
1677
|
+
if (!fileMatch)
|
|
1678
|
+
continue;
|
|
1679
|
+
const inferred = resolveClassFromFile(binding, fileMatch[1]);
|
|
1680
|
+
const classMatch = entryBody.match(/className\s*:\s*['"](\w+)['"]/);
|
|
1681
|
+
const className = classMatch?.[1] ?? inferred.className;
|
|
1682
|
+
entries.push({
|
|
1683
|
+
binding,
|
|
1684
|
+
className,
|
|
1685
|
+
file: fileMatch[1],
|
|
1686
|
+
exportKind: inferred.exportKind,
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
// String shorthand:
|
|
1690
|
+
// containers: { WORDPRESS_CONTAINER: 'src/server/containers/wordpress.container.ts' }
|
|
1691
|
+
// workflows: { NEW_SITE_WORKFLOW: 'src/server/workflows/new-site.workflow.ts' }
|
|
1692
|
+
const foundBindings = new Set(entries.map((e) => e.binding));
|
|
1693
|
+
const pairRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]\s*[,}\n]/g;
|
|
1694
|
+
while ((m = pairRegex.exec(body)) !== null) {
|
|
1695
|
+
const binding = m[1];
|
|
1696
|
+
const file = m[2];
|
|
1697
|
+
if (foundBindings.has(binding))
|
|
1698
|
+
continue;
|
|
1699
|
+
if (binding === 'file' || binding === 'className')
|
|
1700
|
+
continue;
|
|
1701
|
+
const inferred = resolveClassFromFile(binding, file);
|
|
1702
|
+
entries.push({
|
|
1703
|
+
binding,
|
|
1704
|
+
className: inferred.className,
|
|
1705
|
+
file,
|
|
1706
|
+
exportKind: inferred.exportKind,
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
return entries;
|
|
1710
|
+
}
|
|
1368
1711
|
function discoverFilesWithSuffix(dir, suffix) {
|
|
1369
1712
|
if (!fs.existsSync(dir))
|
|
1370
1713
|
return [];
|
|
@@ -1384,9 +1727,9 @@ function discoverFilesWithSuffix(dir, suffix) {
|
|
|
1384
1727
|
return out;
|
|
1385
1728
|
}
|
|
1386
1729
|
/**
|
|
1387
|
-
* Scan
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
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)
|
|
1390
1733
|
*/
|
|
1391
1734
|
function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
1392
1735
|
const serverDir = path.join(srcDir, 'server');
|
|
@@ -1414,14 +1757,15 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1414
1757
|
for (const absPath of discoveredFiles) {
|
|
1415
1758
|
const file = path.basename(absPath);
|
|
1416
1759
|
const source = fs.readFileSync(absPath, 'utf-8');
|
|
1417
|
-
|
|
1418
|
-
|
|
1760
|
+
const exportedFunctions = extractExportedFunctions(source);
|
|
1761
|
+
const hasClass = /extends\s+kuratchiDO\b/.test(source);
|
|
1762
|
+
if (!hasClass && exportedFunctions.length === 0)
|
|
1419
1763
|
continue;
|
|
1420
|
-
// Extract class name
|
|
1764
|
+
// Extract class name when class mode is used.
|
|
1421
1765
|
const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+kuratchiDO/);
|
|
1422
|
-
|
|
1766
|
+
const className = classMatch?.[1] ?? null;
|
|
1767
|
+
if (hasClass && !className)
|
|
1423
1768
|
continue;
|
|
1424
|
-
const className = classMatch[1];
|
|
1425
1769
|
// Binding resolution:
|
|
1426
1770
|
// 1) explicit static binding in class
|
|
1427
1771
|
// 2) config-mapped file name (supports .do.ts convention)
|
|
@@ -1446,10 +1790,8 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1446
1790
|
continue;
|
|
1447
1791
|
if (!bindings.has(binding))
|
|
1448
1792
|
continue;
|
|
1449
|
-
// Extract class methods
|
|
1450
|
-
const classMethods = extractClassMethods(source, className);
|
|
1451
|
-
// Extract named exports (custom worker-side helpers)
|
|
1452
|
-
const namedExports = extractNamedExports(source);
|
|
1793
|
+
// Extract class methods in class mode
|
|
1794
|
+
const classMethods = className ? extractClassMethods(source, className) : [];
|
|
1453
1795
|
const fileName = file.replace(/\.ts$/, '');
|
|
1454
1796
|
const existing = fileNameToAbsPath.get(fileName);
|
|
1455
1797
|
if (existing && existing !== absPath) {
|
|
@@ -1460,9 +1802,10 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1460
1802
|
fileName,
|
|
1461
1803
|
absPath,
|
|
1462
1804
|
binding,
|
|
1463
|
-
|
|
1805
|
+
mode: hasClass ? 'class' : 'function',
|
|
1806
|
+
className: className ?? undefined,
|
|
1464
1807
|
classMethods,
|
|
1465
|
-
|
|
1808
|
+
exportedFunctions,
|
|
1466
1809
|
});
|
|
1467
1810
|
}
|
|
1468
1811
|
return handlers;
|
|
@@ -1554,40 +1897,33 @@ function extractClassMethods(source, className) {
|
|
|
1554
1897
|
}
|
|
1555
1898
|
return methods;
|
|
1556
1899
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
*/
|
|
1560
|
-
function extractNamedExports(source) {
|
|
1561
|
-
const exports = [];
|
|
1562
|
-
// export async function name / export function name
|
|
1900
|
+
function extractExportedFunctions(source) {
|
|
1901
|
+
const out = [];
|
|
1563
1902
|
const fnRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
|
|
1564
1903
|
let m;
|
|
1565
1904
|
while ((m = fnRegex.exec(source)) !== null)
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
const constRegex = /export\s+const\s+(\w+)/g;
|
|
1569
|
-
while ((m = constRegex.exec(source)) !== null)
|
|
1570
|
-
exports.push(m[1]);
|
|
1571
|
-
return exports;
|
|
1905
|
+
out.push(m[1]);
|
|
1906
|
+
return out;
|
|
1572
1907
|
}
|
|
1573
1908
|
/**
|
|
1574
1909
|
* Generate a proxy module for a DO handler file.
|
|
1575
1910
|
*
|
|
1576
|
-
* The proxy provides
|
|
1577
|
-
*
|
|
1578
|
-
*
|
|
1579
|
-
*
|
|
1580
|
-
* 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.
|
|
1581
1914
|
*/
|
|
1582
1915
|
function generateHandlerProxy(handler, projectDir) {
|
|
1583
1916
|
const doDir = path.join(projectDir, '.kuratchi', 'do');
|
|
1584
1917
|
const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
|
|
1585
1918
|
const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
|
|
1586
|
-
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);
|
|
1587
1923
|
const methods = handler.classMethods.map((m) => ({ ...m }));
|
|
1588
1924
|
const methodMap = new Map(methods.map((m) => [m.name, m]));
|
|
1589
1925
|
let changed = true;
|
|
1590
|
-
while (changed) {
|
|
1926
|
+
while (changed && handler.mode === 'class') {
|
|
1591
1927
|
changed = false;
|
|
1592
1928
|
for (const m of methods) {
|
|
1593
1929
|
if (m.hasWorkerContextCalls)
|
|
@@ -1602,15 +1938,56 @@ function generateHandlerProxy(handler, projectDir) {
|
|
|
1602
1938
|
}
|
|
1603
1939
|
}
|
|
1604
1940
|
}
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
+
: [];
|
|
1610
1947
|
const lines = [
|
|
1611
|
-
`// Auto-generated by KuratchiJS compiler
|
|
1948
|
+
`// Auto-generated by KuratchiJS compiler â€" do not edit.`,
|
|
1612
1949
|
`import { __getDoStub } from '${RUNTIME_DO_IMPORT}';`,
|
|
1613
|
-
`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
|
+
`}`,
|
|
1614
1991
|
``,
|
|
1615
1992
|
];
|
|
1616
1993
|
if (workerContextMethods.length > 0) {
|
|
@@ -1630,29 +2007,22 @@ function generateHandlerProxy(handler, projectDir) {
|
|
|
1630
2007
|
lines.push(` if (typeof __local === 'function' && !__asyncMethods.has(__k)) {`);
|
|
1631
2008
|
lines.push(` return (...__a) => __local.apply(__self, __a);`);
|
|
1632
2009
|
lines.push(` }`);
|
|
1633
|
-
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))); };`);
|
|
1634
2011
|
lines.push(` },`);
|
|
1635
2012
|
lines.push(` });`);
|
|
1636
|
-
lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args);`);
|
|
2013
|
+
lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args.map(__decodeArg));`);
|
|
1637
2014
|
lines.push(`}`);
|
|
1638
2015
|
lines.push(``);
|
|
1639
2016
|
}
|
|
1640
|
-
// Export
|
|
1641
|
-
for (const method of
|
|
1642
|
-
if (customSet.has(method))
|
|
1643
|
-
continue; // user's export wins
|
|
2017
|
+
// Export RPC methods
|
|
2018
|
+
for (const method of rpcFunctions) {
|
|
1644
2019
|
if (workerContextMethods.includes(method)) {
|
|
1645
2020
|
lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', a); }`);
|
|
1646
2021
|
}
|
|
1647
2022
|
else {
|
|
1648
|
-
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))); }`);
|
|
1649
2024
|
}
|
|
1650
2025
|
}
|
|
1651
|
-
// Re-export custom named exports from the original file
|
|
1652
|
-
if (handler.namedExports.length > 0) {
|
|
1653
|
-
lines.push(``);
|
|
1654
|
-
lines.push(`export { ${handler.namedExports.join(', ')} } from '${origRelPath}';`);
|
|
1655
|
-
}
|
|
1656
2026
|
return lines.join('\n') + '\n';
|
|
1657
2027
|
}
|
|
1658
2028
|
function generateRoutesModule(opts) {
|
|
@@ -1665,13 +2035,16 @@ function generateRoutesModule(opts) {
|
|
|
1665
2035
|
.map(([status, fn]) => fn)
|
|
1666
2036
|
.join('\n\n');
|
|
1667
2037
|
// Resolve path to the framework's context module from the output directory
|
|
1668
|
-
const contextImport = `import { __setRequestContext, __esc, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
1669
|
-
|
|
2038
|
+
const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
2039
|
+
const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
|
|
2040
|
+
? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
|
|
2041
|
+
: '';
|
|
2042
|
+
// Auth session init â€" thin cookie parsing injected into Worker entry
|
|
1670
2043
|
let authInit = '';
|
|
1671
2044
|
if (opts.authConfig && opts.authConfig.sessionEnabled) {
|
|
1672
2045
|
const cookieName = opts.authConfig.cookieName;
|
|
1673
2046
|
authInit = `
|
|
1674
|
-
//
|
|
2047
|
+
// â"€â"€ Auth Session Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1675
2048
|
|
|
1676
2049
|
function __parseCookies(header) {
|
|
1677
2050
|
const map = {};
|
|
@@ -1722,7 +2095,7 @@ function __initAuth(request) {
|
|
|
1722
2095
|
...schemaImports,
|
|
1723
2096
|
].join('\n');
|
|
1724
2097
|
migrationInit = `
|
|
1725
|
-
//
|
|
2098
|
+
// â"€â"€ ORM Auto-Migration â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1726
2099
|
|
|
1727
2100
|
let __migrated = false;
|
|
1728
2101
|
const __ormDatabases = [
|
|
@@ -1756,7 +2129,7 @@ async function __runMigrations() {
|
|
|
1756
2129
|
`;
|
|
1757
2130
|
}
|
|
1758
2131
|
}
|
|
1759
|
-
// Auth plugin init
|
|
2132
|
+
// Auth plugin init â€" import config + call @kuratchi/auth setup functions
|
|
1760
2133
|
let authPluginImports = '';
|
|
1761
2134
|
let authPluginInit = '';
|
|
1762
2135
|
const ac = opts.authConfig;
|
|
@@ -1808,14 +2181,14 @@ async function __runMigrations() {
|
|
|
1808
2181
|
}
|
|
1809
2182
|
authPluginImports = imports.join('\n');
|
|
1810
2183
|
authPluginInit = `
|
|
1811
|
-
//
|
|
2184
|
+
// â"€â"€ Auth Plugin Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1812
2185
|
|
|
1813
2186
|
function __initAuthPlugins() {
|
|
1814
2187
|
${initLines.join('\n')}
|
|
1815
2188
|
}
|
|
1816
2189
|
`;
|
|
1817
2190
|
}
|
|
1818
|
-
//
|
|
2191
|
+
// â"€â"€ Durable Object class generation â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1819
2192
|
let doImports = '';
|
|
1820
2193
|
let doClassCode = '';
|
|
1821
2194
|
let doResolverInit = '';
|
|
@@ -1825,7 +2198,28 @@ ${initLines.join('\n')}
|
|
|
1825
2198
|
const doResolverLines = [];
|
|
1826
2199
|
doImportLines.push(`import { DurableObject as __DO } from 'cloudflare:workers';`);
|
|
1827
2200
|
doImportLines.push(`import { initDO as __initDO } from '@kuratchi/orm';`);
|
|
1828
|
-
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(`}`);
|
|
1829
2223
|
// We need getCurrentUser and getOrgStubByName for stub resolvers
|
|
1830
2224
|
doImportLines.push(`import { getCurrentUser as __getCU, getOrgStubByName as __getOSBN } from '@kuratchi/auth';`);
|
|
1831
2225
|
// Group handlers by binding
|
|
@@ -1839,6 +2233,10 @@ ${initLines.join('\n')}
|
|
|
1839
2233
|
for (const doEntry of opts.doConfig) {
|
|
1840
2234
|
const handlers = handlersByBinding.get(doEntry.binding) ?? [];
|
|
1841
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'));
|
|
1842
2240
|
// Import schema (paths are relative to project root; prefix ../ since we're in .kuratchi/)
|
|
1843
2241
|
if (ormDb) {
|
|
1844
2242
|
const schemaPath = ormDb.schemaImportPath.replace(/^\.\//, '../');
|
|
@@ -1853,7 +2251,12 @@ ${initLines.join('\n')}
|
|
|
1853
2251
|
if (!handlerImportPath.startsWith('.'))
|
|
1854
2252
|
handlerImportPath = './' + handlerImportPath;
|
|
1855
2253
|
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
1856
|
-
|
|
2254
|
+
if (h.mode === 'class') {
|
|
2255
|
+
doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
|
|
2256
|
+
}
|
|
2257
|
+
else {
|
|
2258
|
+
doImportLines.push(`import * as ${handlerVar} from '${handlerImportPath}';`);
|
|
2259
|
+
}
|
|
1857
2260
|
}
|
|
1858
2261
|
// Generate DO class
|
|
1859
2262
|
doClassLines.push(`export class ${doEntry.className} extends __DO {`);
|
|
@@ -1862,6 +2265,11 @@ ${initLines.join('\n')}
|
|
|
1862
2265
|
if (ormDb) {
|
|
1863
2266
|
doClassLines.push(` this.db = __initDO(ctx.storage.sql, __doSchema_${doEntry.binding});`);
|
|
1864
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
|
+
}
|
|
1865
2273
|
doClassLines.push(` }`);
|
|
1866
2274
|
if (ormDb) {
|
|
1867
2275
|
doClassLines.push(` async __kuratchiLogActivity(payload) {`);
|
|
@@ -1900,16 +2308,45 @@ ${initLines.join('\n')}
|
|
|
1900
2308
|
doClassLines.push(` return rows;`);
|
|
1901
2309
|
doClassLines.push(` }`);
|
|
1902
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
|
+
}
|
|
1903
2330
|
doClassLines.push(`}`);
|
|
1904
|
-
// Apply handler methods to prototype
|
|
2331
|
+
// Apply handler methods to prototype (outside class body)
|
|
1905
2332
|
for (const h of handlers) {
|
|
1906
2333
|
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
+
}
|
|
1909
2346
|
}
|
|
1910
2347
|
// Register stub resolver
|
|
1911
2348
|
if (doEntry.stubId) {
|
|
1912
|
-
// Config-driven: e.g. stubId: 'user.orgId'
|
|
2349
|
+
// Config-driven: e.g. stubId: 'user.orgId' â†' __u.orgId
|
|
1913
2350
|
const fieldPath = doEntry.stubId.startsWith('user.') ? `__u.${doEntry.stubId.slice(5)}` : doEntry.stubId;
|
|
1914
2351
|
const checkField = doEntry.stubId.startsWith('user.') ? doEntry.stubId.slice(5) : doEntry.stubId;
|
|
1915
2352
|
doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
|
|
@@ -1919,37 +2356,37 @@ ${initLines.join('\n')}
|
|
|
1919
2356
|
doResolverLines.push(` });`);
|
|
1920
2357
|
}
|
|
1921
2358
|
else {
|
|
1922
|
-
// No stubId config
|
|
1923
|
-
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`);
|
|
1924
2361
|
}
|
|
1925
2362
|
}
|
|
1926
2363
|
doImports = doImportLines.join('\n');
|
|
1927
|
-
doClassCode = `\n//
|
|
2364
|
+
doClassCode = `\n// â"€â"€ Durable Object Classes (generated) â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n` + doClassLines.join('\n') + '\n';
|
|
1928
2365
|
doResolverInit = `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`;
|
|
1929
2366
|
}
|
|
1930
|
-
return `// Generated by KuratchiJS compiler
|
|
2367
|
+
return `// Generated by KuratchiJS compiler â€" do not edit.
|
|
1931
2368
|
${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
|
|
1932
2369
|
${workerImport}
|
|
1933
2370
|
${contextImport}
|
|
1934
|
-
${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')}
|
|
1935
2372
|
|
|
1936
|
-
//
|
|
2373
|
+
// â"€â"€ Assets â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1937
2374
|
|
|
1938
2375
|
const __assets = {
|
|
1939
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')}
|
|
1940
2377
|
};
|
|
1941
2378
|
|
|
1942
|
-
//
|
|
2379
|
+
// â"€â"€ Router â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1943
2380
|
|
|
1944
|
-
const __staticRoutes = new Map(); // exact path
|
|
2381
|
+
const __staticRoutes = new Map(); // exact path â†' index (O(1) lookup)
|
|
1945
2382
|
const __dynamicRoutes = []; // regex-based routes (params/wildcards)
|
|
1946
2383
|
|
|
1947
2384
|
function __addRoute(pattern, index) {
|
|
1948
2385
|
if (!pattern.includes(':') && !pattern.includes('*')) {
|
|
1949
|
-
// Static route
|
|
2386
|
+
// Static route â€" direct Map lookup, no regex needed
|
|
1950
2387
|
__staticRoutes.set(pattern, index);
|
|
1951
2388
|
} else {
|
|
1952
|
-
// Dynamic route
|
|
2389
|
+
// Dynamic route â€" build regex for param extraction
|
|
1953
2390
|
const paramNames = [];
|
|
1954
2391
|
let regexStr = pattern
|
|
1955
2392
|
.replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
|
|
@@ -1975,13 +2412,13 @@ function __match(pathname) {
|
|
|
1975
2412
|
return null;
|
|
1976
2413
|
}
|
|
1977
2414
|
|
|
1978
|
-
//
|
|
2415
|
+
// â"€â"€ Layout â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1979
2416
|
|
|
1980
2417
|
${layoutBlock}
|
|
1981
2418
|
|
|
1982
2419
|
${layoutActionsBlock}
|
|
1983
2420
|
|
|
1984
|
-
//
|
|
2421
|
+
// â"€â"€ Error pages â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1985
2422
|
|
|
1986
2423
|
const __errorMessages = {
|
|
1987
2424
|
400: 'Bad Request',
|
|
@@ -1996,7 +2433,7 @@ const __errorMessages = {
|
|
|
1996
2433
|
503: 'Service Unavailable',
|
|
1997
2434
|
};
|
|
1998
2435
|
|
|
1999
|
-
// Built-in default error page
|
|
2436
|
+
// Built-in default error page â€" clean, dark, minimal, centered
|
|
2000
2437
|
function __errorPage(status, detail) {
|
|
2001
2438
|
const title = __errorMessages[status] || 'Error';
|
|
2002
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>' : '';
|
|
@@ -2018,8 +2455,8 @@ function __error(status, detail) {
|
|
|
2018
2455
|
return __errorPage(status, detail);
|
|
2019
2456
|
}
|
|
2020
2457
|
|
|
2021
|
-
${opts.compiledComponents.length > 0 ? '//
|
|
2022
|
-
//
|
|
2458
|
+
${opts.compiledComponents.length > 0 ? '// â"€â"€ Components â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
|
|
2459
|
+
// â"€â"€ Route definitions â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2023
2460
|
|
|
2024
2461
|
const routes = [
|
|
2025
2462
|
${opts.compiledRoutes.join(',\n')}
|
|
@@ -2027,7 +2464,7 @@ ${opts.compiledRoutes.join(',\n')}
|
|
|
2027
2464
|
|
|
2028
2465
|
for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
|
|
2029
2466
|
|
|
2030
|
-
//
|
|
2467
|
+
// â"€â"€ Response helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2031
2468
|
|
|
2032
2469
|
const __defaultSecHeaders = {
|
|
2033
2470
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -2075,141 +2512,269 @@ ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
|
|
|
2075
2512
|
return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
|
|
2076
2513
|
}
|
|
2077
2514
|
|
|
2078
|
-
|
|
2515
|
+
const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
|
|
2516
|
+
const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
|
|
2517
|
+
|
|
2518
|
+
async function __runRuntimeRequest(ctx, next) {
|
|
2519
|
+
let idx = -1;
|
|
2520
|
+
async function __dispatch(i) {
|
|
2521
|
+
if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
|
|
2522
|
+
idx = i;
|
|
2523
|
+
const entry = __runtimeEntries[i];
|
|
2524
|
+
if (!entry) return next();
|
|
2525
|
+
const [, step] = entry;
|
|
2526
|
+
if (typeof step.request !== 'function') return __dispatch(i + 1);
|
|
2527
|
+
return await step.request(ctx, () => __dispatch(i + 1));
|
|
2528
|
+
}
|
|
2529
|
+
return __dispatch(0);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
async function __runRuntimeRoute(ctx, next) {
|
|
2533
|
+
let idx = -1;
|
|
2534
|
+
async function __dispatch(i) {
|
|
2535
|
+
if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
|
|
2536
|
+
idx = i;
|
|
2537
|
+
const entry = __runtimeEntries[i];
|
|
2538
|
+
if (!entry) return next();
|
|
2539
|
+
const [, step] = entry;
|
|
2540
|
+
if (typeof step.route !== 'function') return __dispatch(i + 1);
|
|
2541
|
+
return await step.route(ctx, () => __dispatch(i + 1));
|
|
2542
|
+
}
|
|
2543
|
+
return __dispatch(0);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
async function __runRuntimeResponse(ctx, response) {
|
|
2547
|
+
let out = response;
|
|
2548
|
+
for (const [, step] of __runtimeEntries) {
|
|
2549
|
+
if (typeof step.response !== 'function') continue;
|
|
2550
|
+
out = await step.response(ctx, out);
|
|
2551
|
+
if (!(out instanceof Response)) {
|
|
2552
|
+
throw new Error('[kuratchi runtime] response handlers must return a Response');
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
return out;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
async function __runRuntimeError(ctx, error) {
|
|
2559
|
+
for (const [name, step] of __runtimeEntries) {
|
|
2560
|
+
if (typeof step.error !== 'function') continue;
|
|
2561
|
+
try {
|
|
2562
|
+
const handled = await step.error(ctx, error);
|
|
2563
|
+
if (handled instanceof Response) return handled;
|
|
2564
|
+
} catch (hookErr) {
|
|
2565
|
+
console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return null;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// â"€â"€ Exported Worker entrypoint â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2079
2572
|
|
|
2080
2573
|
export default class extends WorkerEntrypoint {
|
|
2081
2574
|
async fetch(request) {
|
|
2082
2575
|
__setRequestContext(this.ctx, request);
|
|
2576
|
+
globalThis.__cloudflare_env__ = __env;
|
|
2083
2577
|
${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2578
|
+
const __runtimeCtx = {
|
|
2579
|
+
request,
|
|
2580
|
+
env: __env,
|
|
2581
|
+
ctx: this.ctx,
|
|
2582
|
+
url: new URL(request.url),
|
|
2583
|
+
params: {},
|
|
2584
|
+
locals: __getLocals(),
|
|
2585
|
+
};
|
|
2086
2586
|
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2587
|
+
const __coreFetch = async () => {
|
|
2588
|
+
const request = __runtimeCtx.request;
|
|
2589
|
+
const url = __runtimeCtx.url;
|
|
2590
|
+
${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' : ''}
|
|
2591
|
+
|
|
2592
|
+
// Serve static assets from src/assets/ at /_assets/*
|
|
2593
|
+
if (url.pathname.startsWith('/_assets/')) {
|
|
2594
|
+
const name = url.pathname.slice('/_assets/'.length);
|
|
2595
|
+
const asset = __assets[name];
|
|
2596
|
+
if (asset) {
|
|
2597
|
+
if (request.headers.get('if-none-match') === asset.etag) {
|
|
2598
|
+
return new Response(null, { status: 304 });
|
|
2599
|
+
}
|
|
2600
|
+
return new Response(asset.content, {
|
|
2601
|
+
headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
|
|
2602
|
+
});
|
|
2094
2603
|
}
|
|
2095
|
-
return new Response(
|
|
2096
|
-
headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
|
|
2097
|
-
});
|
|
2604
|
+
return __secHeaders(new Response('Not Found', { status: 404 }));
|
|
2098
2605
|
}
|
|
2099
|
-
return __secHeaders(new Response('Not Found', { status: 404 }));
|
|
2100
|
-
}
|
|
2101
2606
|
|
|
2102
|
-
|
|
2607
|
+
const match = __match(url.pathname);
|
|
2103
2608
|
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2609
|
+
if (!match) {
|
|
2610
|
+
return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
|
|
2611
|
+
}
|
|
2107
2612
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
|
|
2112
|
-
let __qArgs = [];
|
|
2113
|
-
try {
|
|
2114
|
-
const __parsed = JSON.parse(__qArgsRaw);
|
|
2115
|
-
__qArgs = Array.isArray(__parsed) ? __parsed : [];
|
|
2116
|
-
} catch {}
|
|
2117
|
-
__setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
|
|
2118
|
-
if (!__getLocals().__breadcrumbs) {
|
|
2119
|
-
__setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
|
|
2120
|
-
}
|
|
2613
|
+
__runtimeCtx.params = match.params;
|
|
2614
|
+
const route = routes[match.index];
|
|
2615
|
+
__setLocal('params', match.params);
|
|
2121
2616
|
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
try {
|
|
2131
|
-
const __rpcArgsStr = url.searchParams.get('_args');
|
|
2132
|
-
let __rpcArgs = [];
|
|
2133
|
-
if (__rpcArgsStr) {
|
|
2134
|
-
const __parsed = JSON.parse(__rpcArgsStr);
|
|
2135
|
-
__rpcArgs = Array.isArray(__parsed) ? __parsed : [];
|
|
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 } }));
|
|
2136
2625
|
}
|
|
2137
|
-
const
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
|
|
2144
|
-
return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
|
|
2145
|
-
status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
2146
|
-
}));
|
|
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));
|
|
2147
2632
|
}
|
|
2148
|
-
}
|
|
2149
2633
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2634
|
+
const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
|
|
2635
|
+
const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
|
|
2636
|
+
let __qArgs = [];
|
|
2637
|
+
try {
|
|
2638
|
+
const __parsed = JSON.parse(__qArgsRaw);
|
|
2639
|
+
__qArgs = Array.isArray(__parsed) ? __parsed : [];
|
|
2640
|
+
} catch {}
|
|
2641
|
+
__setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
|
|
2642
|
+
if (!__getLocals().__breadcrumbs) {
|
|
2643
|
+
__setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
|
|
2154
2644
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2645
|
+
|
|
2646
|
+
// RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
|
|
2647
|
+
const __rpcName = url.searchParams.get('_rpc');
|
|
2648
|
+
if (request.method === 'GET' && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
|
|
2649
|
+
if (request.headers.get('x-kuratchi-rpc') !== '1') {
|
|
2650
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
|
|
2651
|
+
status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
2652
|
+
}));
|
|
2653
|
+
}
|
|
2163
2654
|
try {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
await __actionFn(formData);
|
|
2655
|
+
const __rpcArgsStr = url.searchParams.get('_args');
|
|
2656
|
+
let __rpcArgs = [];
|
|
2657
|
+
if (__rpcArgsStr) {
|
|
2658
|
+
const __parsed = JSON.parse(__rpcArgsStr);
|
|
2659
|
+
__rpcArgs = Array.isArray(__parsed) ? __parsed : [];
|
|
2170
2660
|
}
|
|
2661
|
+
const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
|
|
2662
|
+
return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
|
|
2663
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
2664
|
+
}));
|
|
2171
2665
|
} catch (err) {
|
|
2172
|
-
console.error('[kuratchi]
|
|
2666
|
+
console.error('[kuratchi] RPC error:', err);
|
|
2667
|
+
const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
|
|
2668
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
|
|
2669
|
+
status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
2670
|
+
}));
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Form action: POST with hidden _action field in form body
|
|
2675
|
+
if (request.method === 'POST') {
|
|
2676
|
+
if (!__isSameOrigin(request, url)) {
|
|
2677
|
+
return __secHeaders(new Response('Forbidden', { status: 403 }));
|
|
2678
|
+
}
|
|
2679
|
+
const formData = await request.formData();
|
|
2680
|
+
const actionName = formData.get('_action');
|
|
2681
|
+
const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
|
|
2682
|
+
|| (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
|
|
2683
|
+
if (actionName && __actionFn) {
|
|
2684
|
+
// Check if this is a fetch-based action call (onclick) with JSON args
|
|
2685
|
+
const argsStr = formData.get('_args');
|
|
2686
|
+
const isFetchAction = argsStr !== null;
|
|
2687
|
+
try {
|
|
2688
|
+
if (isFetchAction) {
|
|
2689
|
+
const __parsed = JSON.parse(argsStr);
|
|
2690
|
+
const args = Array.isArray(__parsed) ? __parsed : [];
|
|
2691
|
+
await __actionFn(...args);
|
|
2692
|
+
} else {
|
|
2693
|
+
await __actionFn(formData);
|
|
2694
|
+
}
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
console.error('[kuratchi] Action error:', err);
|
|
2697
|
+
if (isFetchAction) {
|
|
2698
|
+
const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
|
|
2699
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
|
|
2700
|
+
status: 500, headers: { 'content-type': 'application/json' }
|
|
2701
|
+
}));
|
|
2702
|
+
}
|
|
2703
|
+
const __loaded = route.load ? await route.load(match.params) : {};
|
|
2704
|
+
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
2705
|
+
data.params = match.params;
|
|
2706
|
+
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
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 };
|
|
2711
|
+
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2712
|
+
}
|
|
2713
|
+
// Fetch-based actions return lightweight JSON (no page re-render)
|
|
2173
2714
|
if (isFetchAction) {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
status: 500, headers: { 'content-type': 'application/json' }
|
|
2715
|
+
return __attachCookies(new Response(JSON.stringify({ ok: true }), {
|
|
2716
|
+
headers: { 'content-type': 'application/json' }
|
|
2177
2717
|
}));
|
|
2178
2718
|
}
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2185
|
-
}
|
|
2186
|
-
// Fetch-based actions return lightweight JSON (no page re-render)
|
|
2187
|
-
if (isFetchAction) {
|
|
2188
|
-
return __attachCookies(new Response(JSON.stringify({ ok: true }), {
|
|
2189
|
-
headers: { 'content-type': 'application/json' }
|
|
2190
|
-
}));
|
|
2719
|
+
// POST-Redirect-GET: redirect to custom target or back to same URL
|
|
2720
|
+
const __locals = __getLocals();
|
|
2721
|
+
const redirectTo = __locals.__redirectTo || url.pathname;
|
|
2722
|
+
const redirectStatus = Number(__locals.__redirectStatus) || 303;
|
|
2723
|
+
return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
|
|
2191
2724
|
}
|
|
2192
|
-
// POST-Redirect-GET: redirect to custom target or back to same URL
|
|
2193
|
-
const __locals = __getLocals();
|
|
2194
|
-
const redirectTo = __locals.__redirectTo || url.pathname;
|
|
2195
|
-
const redirectStatus = Number(__locals.__redirectStatus) || 303;
|
|
2196
|
-
return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
|
|
2197
2725
|
}
|
|
2198
|
-
}
|
|
2199
2726
|
|
|
2200
|
-
|
|
2727
|
+
// GET (or unmatched POST): load + render
|
|
2728
|
+
try {
|
|
2729
|
+
const __loaded = route.load ? await route.load(match.params) : {};
|
|
2730
|
+
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
2731
|
+
data.params = match.params;
|
|
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 }; });
|
|
2735
|
+
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2736
|
+
} catch (err) {
|
|
2737
|
+
console.error('[kuratchi] Route load/render error:', err);
|
|
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' } }));
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
|
|
2201
2744
|
try {
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2745
|
+
const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
|
|
2746
|
+
return __runRuntimeRoute(__runtimeCtx, __coreFetch);
|
|
2747
|
+
});
|
|
2748
|
+
return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
|
|
2207
2749
|
} catch (err) {
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2750
|
+
const __handled = await __runRuntimeError(__runtimeCtx, err);
|
|
2751
|
+
if (__handled) return __secHeaders(__handled);
|
|
2752
|
+
throw err;
|
|
2211
2753
|
}
|
|
2212
2754
|
}
|
|
2213
2755
|
}
|
|
2214
2756
|
`;
|
|
2215
2757
|
}
|
|
2758
|
+
function resolveRuntimeImportPath(projectDir) {
|
|
2759
|
+
const candidates = [
|
|
2760
|
+
{ file: 'src/kuratchi.runtime.ts', importPath: '../src/kuratchi.runtime' },
|
|
2761
|
+
{ file: 'src/kuratchi.runtime.js', importPath: '../src/kuratchi.runtime' },
|
|
2762
|
+
{ file: 'src/kuratchi.runtime.mjs', importPath: '../src/kuratchi.runtime' },
|
|
2763
|
+
{ file: 'kuratchi.runtime.ts', importPath: '../kuratchi.runtime' },
|
|
2764
|
+
{ file: 'kuratchi.runtime.js', importPath: '../kuratchi.runtime' },
|
|
2765
|
+
{ file: 'kuratchi.runtime.mjs', importPath: '../kuratchi.runtime' },
|
|
2766
|
+
];
|
|
2767
|
+
for (const candidate of candidates) {
|
|
2768
|
+
if (fs.existsSync(path.join(projectDir, candidate.file))) {
|
|
2769
|
+
return candidate.importPath;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return null;
|
|
2773
|
+
}
|
|
2774
|
+
function toWorkerImportPath(projectDir, outDir, filePath) {
|
|
2775
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
2776
|
+
let rel = path.relative(outDir, absPath).replace(/\\/g, '/');
|
|
2777
|
+
if (!rel.startsWith('.'))
|
|
2778
|
+
rel = `./${rel}`;
|
|
2779
|
+
return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
|
|
2780
|
+
}
|