@kuratchi/js 0.0.3 → 0.0.5
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 +226 -50
- package/dist/cli.js +59 -17
- package/dist/compiler/index.d.ts +2 -2
- package/dist/compiler/index.js +658 -259
- package/dist/compiler/parser.d.ts +20 -4
- package/dist/compiler/parser.js +296 -12
- package/dist/compiler/template.d.ts +3 -1
- package/dist/compiler/template.js +217 -14
- package/dist/compiler/transpile.d.ts +1 -0
- package/dist/compiler/transpile.js +61 -0
- package/dist/create.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/runtime/action.d.ts +11 -0
- package/dist/runtime/action.js +14 -0
- package/dist/runtime/app.js +35 -8
- package/dist/runtime/context.d.ts +8 -2
- package/dist/runtime/context.js +31 -1
- package/dist/runtime/do.d.ts +6 -0
- package/dist/runtime/do.js +15 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/page-error.d.ts +16 -0
- package/dist/runtime/page-error.js +20 -0
- package/dist/runtime/types.d.ts +53 -33
- package/package.json +50 -46
package/dist/compiler/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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';
|
|
6
6
|
import { compileTemplate } from './template.js';
|
|
7
|
+
import { transpileTypeScript } from './transpile.js';
|
|
7
8
|
import { filePathToPattern } from '../runtime/router.js';
|
|
8
9
|
import * as fs from 'node:fs';
|
|
9
10
|
import * as path from 'node:path';
|
|
@@ -31,10 +32,56 @@ function compactInlineJs(source) {
|
|
|
31
32
|
.replace(/\s*([{}();,:])\s*/g, '$1')
|
|
32
33
|
.trim();
|
|
33
34
|
}
|
|
35
|
+
function rewriteImportedFunctionCalls(source, fnToModule) {
|
|
36
|
+
let out = source;
|
|
37
|
+
for (const [fnName, moduleId] of Object.entries(fnToModule)) {
|
|
38
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
|
|
39
|
+
continue;
|
|
40
|
+
const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
|
|
41
|
+
out = out.replace(callRegex, `${moduleId}.${fnName}(`);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function rewriteWorkerEnvAliases(source, aliases) {
|
|
46
|
+
let out = source;
|
|
47
|
+
for (const alias of aliases) {
|
|
48
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(alias))
|
|
49
|
+
continue;
|
|
50
|
+
const aliasRegex = new RegExp(`\\b${alias}\\b`, 'g');
|
|
51
|
+
out = out.replace(aliasRegex, '__env');
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function parseNamedImportBindings(line) {
|
|
56
|
+
const namesMatch = line.match(/import\s*\{([^}]+)\}/);
|
|
57
|
+
if (!namesMatch)
|
|
58
|
+
return [];
|
|
59
|
+
return namesMatch[1]
|
|
60
|
+
.split(',')
|
|
61
|
+
.map(n => n.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map(n => {
|
|
64
|
+
const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
|
|
65
|
+
return parts[1] || parts[0] || '';
|
|
66
|
+
})
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
function filterClientImportsForServer(imports, neededFns) {
|
|
70
|
+
const selected = [];
|
|
71
|
+
for (const line of imports) {
|
|
72
|
+
const bindings = parseNamedImportBindings(line);
|
|
73
|
+
if (bindings.length === 0)
|
|
74
|
+
continue;
|
|
75
|
+
if (bindings.some(name => neededFns.has(name))) {
|
|
76
|
+
selected.push(line);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return selected;
|
|
80
|
+
}
|
|
34
81
|
/**
|
|
35
82
|
* Compile a project's src/routes/ into .kuratchi/routes.js
|
|
36
83
|
*
|
|
37
|
-
* The generated module exports { app }
|
|
84
|
+
* The generated module exports { app } â€" an object with a fetch() method
|
|
38
85
|
* that handles routing, load functions, form actions, and rendering.
|
|
39
86
|
* Returns the path to .kuratchi/worker.js — the stable wrangler entry point that
|
|
40
87
|
* re-exports everything from routes.js (default fetch handler + named DO class exports).
|
|
@@ -49,21 +96,21 @@ export function compile(options) {
|
|
|
49
96
|
}
|
|
50
97
|
// Discover all .html route files
|
|
51
98
|
const routeFiles = discoverRoutes(routesDir);
|
|
52
|
-
// Component compilation cache
|
|
99
|
+
// Component compilation cache â€" only compile components that are actually imported
|
|
53
100
|
const libDir = path.join(srcDir, 'lib');
|
|
54
|
-
const compiledComponentCache = new Map(); // fileName
|
|
55
|
-
const componentStyleCache = new Map(); // fileName
|
|
101
|
+
const compiledComponentCache = new Map(); // fileName â†' compiled function code
|
|
102
|
+
const componentStyleCache = new Map(); // fileName â†' escaped CSS string (or empty)
|
|
56
103
|
// Tracks which prop names inside a component are used as action={propName}.
|
|
57
|
-
// e.g. db-studio uses action={runQueryAction}
|
|
104
|
+
// e.g. db-studio uses action={runQueryAction} â†' stores 'runQueryAction'.
|
|
58
105
|
// When the route passes runQueryAction={runAdminSqlQuery}, the compiler knows
|
|
59
106
|
// to add 'runAdminSqlQuery' to the route's actionFunctions.
|
|
60
|
-
const componentActionCache = new Map(); // fileName
|
|
107
|
+
const componentActionCache = new Map(); // fileName â†' Set of action prop names
|
|
61
108
|
function compileComponent(fileName) {
|
|
62
109
|
if (compiledComponentCache.has(fileName))
|
|
63
110
|
return compiledComponentCache.get(fileName);
|
|
64
111
|
let filePath;
|
|
65
112
|
let funcName;
|
|
66
|
-
// Package component: "@kuratchi/ui:badge"
|
|
113
|
+
// Package component: "@kuratchi/ui:badge" â†' resolve from package
|
|
67
114
|
const pkgMatch = fileName.match(/^(@[^:]+):(.+)$/);
|
|
68
115
|
if (pkgMatch) {
|
|
69
116
|
const pkgName = pkgMatch[1]; // e.g. "@kuratchi/ui"
|
|
@@ -87,16 +134,19 @@ export function compile(options) {
|
|
|
87
134
|
// Use parseFile to properly split the <script> block from the template, and to
|
|
88
135
|
// separate component imports (import X from '@kuratchi/ui/x.html') from regular code.
|
|
89
136
|
// This prevents import lines from being inlined verbatim in the compiled function body.
|
|
90
|
-
const compParsed = parseFile(rawSource);
|
|
137
|
+
const compParsed = parseFile(rawSource, { kind: 'component', filePath });
|
|
91
138
|
// propsCode = script body with all import lines stripped out
|
|
92
139
|
const propsCode = compParsed.script
|
|
93
140
|
? compParsed.script
|
|
94
141
|
.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '')
|
|
95
142
|
.trim()
|
|
96
143
|
: '';
|
|
144
|
+
const transpiledPropsCode = propsCode
|
|
145
|
+
? transpileTypeScript(propsCode, `component-script:${fileName}.ts`)
|
|
146
|
+
: '';
|
|
97
147
|
// template source (parseFile already removes the <script> block)
|
|
98
148
|
let source = compParsed.template;
|
|
99
|
-
// Extract optional <style> block
|
|
149
|
+
// Extract optional <style> block â€" CSS is scoped and injected once per route at compile time
|
|
100
150
|
let styleBlock = '';
|
|
101
151
|
const styleMatch = source.match(/<style[\s>][\s\S]*?<\/style>/i);
|
|
102
152
|
if (styleMatch) {
|
|
@@ -124,8 +174,8 @@ export function compile(options) {
|
|
|
124
174
|
: '';
|
|
125
175
|
componentStyleCache.set(fileName, escapedStyle);
|
|
126
176
|
// Replace <slot></slot> and <slot /> with children output
|
|
127
|
-
source = source.replace(/<slot\s*><\/slot>/g, '{
|
|
128
|
-
source = source.replace(/<slot\s*\/>/g, '{
|
|
177
|
+
source = source.replace(/<slot\s*><\/slot>/g, '{@raw props.children || ""}');
|
|
178
|
+
source = source.replace(/<slot\s*\/>/g, '{@raw props.children || ""}');
|
|
129
179
|
// Build a sub-component map from the component's own component imports so that
|
|
130
180
|
// <Alert>, <Badge>, <Dialog>, etc. get expanded instead of emitted as raw tags.
|
|
131
181
|
const subComponentNames = new Map();
|
|
@@ -142,7 +192,7 @@ export function compile(options) {
|
|
|
142
192
|
}
|
|
143
193
|
}
|
|
144
194
|
// Scan the component template for action={propName} uses.
|
|
145
|
-
// These prop names are "action props"
|
|
195
|
+
// These prop names are "action props" â€" when the route passes actionProp={routeFn},
|
|
146
196
|
// the compiler knows to add routeFn to the route's actionFunctions so it ends up
|
|
147
197
|
// in the route's actions map and can be dispatched at runtime.
|
|
148
198
|
const actionPropNames = new Set();
|
|
@@ -157,12 +207,12 @@ export function compile(options) {
|
|
|
157
207
|
// Insert scope open after 'let __html = "";' (first line of body) and scope close at end
|
|
158
208
|
const bodyLines = body.split('\n');
|
|
159
209
|
const scopedBody = [bodyLines[0], scopeOpen, ...bodyLines.slice(1), scopeClose].join('\n');
|
|
160
|
-
const fnBody =
|
|
210
|
+
const fnBody = transpiledPropsCode ? `${transpiledPropsCode}\n ${scopedBody}` : scopedBody;
|
|
161
211
|
const compiled = `function ${funcName}(props, __esc) {\n ${fnBody}\n return __html;\n}`;
|
|
162
212
|
compiledComponentCache.set(fileName, compiled);
|
|
163
213
|
return compiled;
|
|
164
214
|
}
|
|
165
|
-
// App layout: src/routes/layout.html (convention
|
|
215
|
+
// App layout: src/routes/layout.html (convention â€" wraps all routes automatically)
|
|
166
216
|
const layoutFile = path.join(routesDir, 'layout.html');
|
|
167
217
|
let compiledLayout = null;
|
|
168
218
|
const layoutComponentNames = new Map();
|
|
@@ -444,6 +494,17 @@ export function compile(options) {
|
|
|
444
494
|
if(!msg) return;
|
|
445
495
|
if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
|
|
446
496
|
}, true);
|
|
497
|
+
document.addEventListener('submit', function(e){
|
|
498
|
+
if(e.defaultPrevented) return;
|
|
499
|
+
var f = e.target;
|
|
500
|
+
if(!f || !f.querySelector) return;
|
|
501
|
+
var aInput = f.querySelector('input[name="_action"]');
|
|
502
|
+
if(!aInput) return;
|
|
503
|
+
var aName = aInput.value;
|
|
504
|
+
if(!aName) return;
|
|
505
|
+
f.setAttribute('data-action-loading', aName);
|
|
506
|
+
Array.prototype.slice.call(f.querySelectorAll('button[type="submit"],button:not([type="button"]):not([type="reset"])')).forEach(function(b){ b.disabled = true; });
|
|
507
|
+
}, true);
|
|
447
508
|
document.addEventListener('change', function(e){
|
|
448
509
|
var t = e.target;
|
|
449
510
|
if(!t || !t.getAttribute) return;
|
|
@@ -458,21 +519,118 @@ export function compile(options) {
|
|
|
458
519
|
}, true);
|
|
459
520
|
by('[data-select-all]').forEach(function(m){ var g = m.getAttribute('data-select-all'); if(g) syncGroup(g); });
|
|
460
521
|
})();`;
|
|
522
|
+
const reactiveRuntimeSource = `(function(g){
|
|
523
|
+
if(g.__kuratchiReactive) return;
|
|
524
|
+
const targetMap = new WeakMap();
|
|
525
|
+
const proxyMap = new WeakMap();
|
|
526
|
+
let active = null;
|
|
527
|
+
const queue = new Set();
|
|
528
|
+
let flushing = false;
|
|
529
|
+
function queueRun(fn){
|
|
530
|
+
queue.add(fn);
|
|
531
|
+
if(flushing) return;
|
|
532
|
+
flushing = true;
|
|
533
|
+
Promise.resolve().then(function(){
|
|
534
|
+
try {
|
|
535
|
+
const jobs = Array.from(queue);
|
|
536
|
+
queue.clear();
|
|
537
|
+
for (const job of jobs) job();
|
|
538
|
+
} finally {
|
|
539
|
+
flushing = false;
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function cleanup(effect){
|
|
544
|
+
const deps = effect.__deps || [];
|
|
545
|
+
for (const dep of deps) dep.delete(effect);
|
|
546
|
+
effect.__deps = [];
|
|
547
|
+
}
|
|
548
|
+
function track(target, key){
|
|
549
|
+
if(!active) return;
|
|
550
|
+
let depsMap = targetMap.get(target);
|
|
551
|
+
if(!depsMap){ depsMap = new Map(); targetMap.set(target, depsMap); }
|
|
552
|
+
let dep = depsMap.get(key);
|
|
553
|
+
if(!dep){ dep = new Set(); depsMap.set(key, dep); }
|
|
554
|
+
if(dep.has(active)) return;
|
|
555
|
+
dep.add(active);
|
|
556
|
+
if(!active.__deps) active.__deps = [];
|
|
557
|
+
active.__deps.push(dep);
|
|
558
|
+
}
|
|
559
|
+
function trigger(target, key){
|
|
560
|
+
const depsMap = targetMap.get(target);
|
|
561
|
+
if(!depsMap) return;
|
|
562
|
+
const effects = new Set();
|
|
563
|
+
const add = function(k){
|
|
564
|
+
const dep = depsMap.get(k);
|
|
565
|
+
if(dep) dep.forEach(function(e){ effects.add(e); });
|
|
566
|
+
};
|
|
567
|
+
add(key);
|
|
568
|
+
add('*');
|
|
569
|
+
effects.forEach(function(e){ queueRun(e); });
|
|
570
|
+
}
|
|
571
|
+
function isObject(value){ return value !== null && typeof value === 'object'; }
|
|
572
|
+
function proxify(value){
|
|
573
|
+
if(!isObject(value)) return value;
|
|
574
|
+
if(proxyMap.has(value)) return proxyMap.get(value);
|
|
575
|
+
const proxy = new Proxy(value, {
|
|
576
|
+
get(target, key, receiver){
|
|
577
|
+
track(target, key);
|
|
578
|
+
const out = Reflect.get(target, key, receiver);
|
|
579
|
+
return isObject(out) ? proxify(out) : out;
|
|
580
|
+
},
|
|
581
|
+
set(target, key, next, receiver){
|
|
582
|
+
const prev = target[key];
|
|
583
|
+
const result = Reflect.set(target, key, next, receiver);
|
|
584
|
+
if(prev !== next) trigger(target, key);
|
|
585
|
+
if(Array.isArray(target) && key !== 'length') trigger(target, 'length');
|
|
586
|
+
return result;
|
|
587
|
+
},
|
|
588
|
+
deleteProperty(target, key){
|
|
589
|
+
const had = Object.prototype.hasOwnProperty.call(target, key);
|
|
590
|
+
const result = Reflect.deleteProperty(target, key);
|
|
591
|
+
if(had) trigger(target, key);
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
proxyMap.set(value, proxy);
|
|
596
|
+
return proxy;
|
|
597
|
+
}
|
|
598
|
+
function effect(fn){
|
|
599
|
+
const run = function(){
|
|
600
|
+
cleanup(run);
|
|
601
|
+
active = run;
|
|
602
|
+
try { fn(); } finally { active = null; }
|
|
603
|
+
};
|
|
604
|
+
run.__deps = [];
|
|
605
|
+
run();
|
|
606
|
+
return function(){ cleanup(run); };
|
|
607
|
+
}
|
|
608
|
+
function state(initial){ return proxify(initial); }
|
|
609
|
+
function replace(_prev, next){ return proxify(next); }
|
|
610
|
+
g.__kuratchiReactive = { state, effect, replace };
|
|
611
|
+
})(window);`;
|
|
461
612
|
const actionScript = `<script>${options.isDev ? bridgeSource : compactInlineJs(bridgeSource)}</script>`;
|
|
613
|
+
const reactiveRuntimeScript = `<script>${options.isDev ? reactiveRuntimeSource : compactInlineJs(reactiveRuntimeSource)}</script>`;
|
|
614
|
+
if (source.includes('</head>')) {
|
|
615
|
+
source = source.replace('</head>', reactiveRuntimeScript + '\n</head>');
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
source = reactiveRuntimeScript + '\n' + source;
|
|
619
|
+
}
|
|
462
620
|
source = source.replace('</body>', actionScript + '\n</body>');
|
|
463
621
|
// Parse layout for <script> block (component imports + data vars)
|
|
464
|
-
const layoutParsed = parseFile(source);
|
|
622
|
+
const layoutParsed = parseFile(source, { kind: 'layout', filePath: layoutFile });
|
|
465
623
|
const hasLayoutScript = layoutParsed.script && (Object.keys(layoutParsed.componentImports).length > 0 || layoutParsed.hasLoad);
|
|
466
624
|
if (hasLayoutScript) {
|
|
467
|
-
// Dynamic layout
|
|
625
|
+
// Dynamic layout â€" has component imports and/or data declarations
|
|
468
626
|
// Compile component imports from layout
|
|
469
627
|
for (const [pascalName, fileName] of Object.entries(layoutParsed.componentImports)) {
|
|
470
628
|
compileComponent(fileName);
|
|
471
629
|
layoutComponentNames.set(pascalName, fileName);
|
|
472
630
|
}
|
|
473
631
|
// Replace <slot></slot> with content parameter injection
|
|
474
|
-
let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{
|
|
475
|
-
layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{
|
|
632
|
+
let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{@raw __content}');
|
|
633
|
+
layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{@raw __content}');
|
|
476
634
|
// Build layout action names so action={fn} works in layouts
|
|
477
635
|
const layoutActionNames = new Set(layoutParsed.actionFunctions);
|
|
478
636
|
// Compile the layout template with component + action support
|
|
@@ -500,7 +658,7 @@ export function compile(options) {
|
|
|
500
658
|
}`;
|
|
501
659
|
}
|
|
502
660
|
else {
|
|
503
|
-
// Static layout
|
|
661
|
+
// Static layout â€" no components, use fast string split (original behavior)
|
|
504
662
|
const slotMarker = '<slot></slot>';
|
|
505
663
|
const slotIdx = source.indexOf(slotMarker);
|
|
506
664
|
if (slotIdx === -1) {
|
|
@@ -513,7 +671,7 @@ export function compile(options) {
|
|
|
513
671
|
}
|
|
514
672
|
}
|
|
515
673
|
// 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
|
|
674
|
+
// Only compiled if the user explicitly creates them â€" otherwise the framework's built-in default is used
|
|
517
675
|
const compiledErrorPages = new Map();
|
|
518
676
|
for (const file of fs.readdirSync(routesDir)) {
|
|
519
677
|
const match = file.match(/^(\d{3})\.html$/);
|
|
@@ -573,16 +731,130 @@ export function compile(options) {
|
|
|
573
731
|
}
|
|
574
732
|
}
|
|
575
733
|
}
|
|
734
|
+
const resolveExistingModuleFile = (absBase) => {
|
|
735
|
+
const candidates = [
|
|
736
|
+
absBase,
|
|
737
|
+
absBase + '.ts',
|
|
738
|
+
absBase + '.js',
|
|
739
|
+
absBase + '.mjs',
|
|
740
|
+
absBase + '.cjs',
|
|
741
|
+
path.join(absBase, 'index.ts'),
|
|
742
|
+
path.join(absBase, 'index.js'),
|
|
743
|
+
path.join(absBase, 'index.mjs'),
|
|
744
|
+
path.join(absBase, 'index.cjs'),
|
|
745
|
+
];
|
|
746
|
+
for (const candidate of candidates) {
|
|
747
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
748
|
+
return candidate;
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
};
|
|
752
|
+
const toModuleSpecifier = (fromFileAbs, toFileAbs) => {
|
|
753
|
+
let rel = path.relative(path.dirname(fromFileAbs), toFileAbs).replace(/\\/g, '/');
|
|
754
|
+
if (!rel.startsWith('.'))
|
|
755
|
+
rel = './' + rel;
|
|
756
|
+
return rel;
|
|
757
|
+
};
|
|
758
|
+
const transformedServerModules = new Map();
|
|
759
|
+
const modulesOutDir = path.join(projectDir, '.kuratchi', 'modules');
|
|
760
|
+
const resolveDoProxyTarget = (absPath) => {
|
|
761
|
+
const normalizedNoExt = absPath.replace(/\\/g, '/').replace(/\.[^.\/]+$/, '');
|
|
762
|
+
const proxyNoExt = doHandlerProxyPaths.get(normalizedNoExt);
|
|
763
|
+
if (!proxyNoExt)
|
|
764
|
+
return null;
|
|
765
|
+
return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.js') ? proxyNoExt + '.js' : null);
|
|
766
|
+
};
|
|
767
|
+
const resolveImportTarget = (importerAbs, spec) => {
|
|
768
|
+
if (spec.startsWith('$')) {
|
|
769
|
+
const slashIdx = spec.indexOf('/');
|
|
770
|
+
const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
|
|
771
|
+
const rest = slashIdx === -1 ? '' : spec.slice(slashIdx + 1);
|
|
772
|
+
if (!folder)
|
|
773
|
+
return null;
|
|
774
|
+
const abs = path.join(srcDir, folder, rest);
|
|
775
|
+
return resolveExistingModuleFile(abs) ?? abs;
|
|
776
|
+
}
|
|
777
|
+
if (spec.startsWith('.')) {
|
|
778
|
+
const abs = path.resolve(path.dirname(importerAbs), spec);
|
|
779
|
+
return resolveExistingModuleFile(abs) ?? abs;
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
};
|
|
783
|
+
const transformServerModule = (entryAbsPath) => {
|
|
784
|
+
const resolved = resolveExistingModuleFile(entryAbsPath) ?? entryAbsPath;
|
|
785
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
786
|
+
const cached = transformedServerModules.get(normalized);
|
|
787
|
+
if (cached)
|
|
788
|
+
return cached;
|
|
789
|
+
const relFromProject = path.relative(projectDir, resolved);
|
|
790
|
+
const outPath = path.join(modulesOutDir, relFromProject);
|
|
791
|
+
transformedServerModules.set(normalized, outPath);
|
|
792
|
+
const outDir = path.dirname(outPath);
|
|
793
|
+
if (!fs.existsSync(outDir))
|
|
794
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
795
|
+
if (!/\.(ts|js|mjs|cjs)$/i.test(resolved) || !fs.existsSync(resolved)) {
|
|
796
|
+
const passthrough = resolved;
|
|
797
|
+
transformedServerModules.set(normalized, passthrough);
|
|
798
|
+
return passthrough;
|
|
799
|
+
}
|
|
800
|
+
const source = fs.readFileSync(resolved, 'utf-8');
|
|
801
|
+
const rewriteSpecifier = (spec) => {
|
|
802
|
+
const target = resolveImportTarget(resolved, spec);
|
|
803
|
+
if (!target)
|
|
804
|
+
return spec;
|
|
805
|
+
const doProxyTarget = resolveDoProxyTarget(target);
|
|
806
|
+
if (doProxyTarget)
|
|
807
|
+
return toModuleSpecifier(outPath, doProxyTarget);
|
|
808
|
+
const normalizedTarget = target.replace(/\\/g, '/');
|
|
809
|
+
const inProject = normalizedTarget.startsWith(projectDir.replace(/\\/g, '/') + '/');
|
|
810
|
+
if (!inProject)
|
|
811
|
+
return spec;
|
|
812
|
+
const targetResolved = resolveExistingModuleFile(target) ?? target;
|
|
813
|
+
if (!/\.(ts|js|mjs|cjs)$/i.test(targetResolved))
|
|
814
|
+
return spec;
|
|
815
|
+
const rewrittenTarget = transformServerModule(targetResolved);
|
|
816
|
+
return toModuleSpecifier(outPath, rewrittenTarget);
|
|
817
|
+
};
|
|
818
|
+
let rewritten = source.replace(/(from\s+)(['"])([^'"]+)\2/g, (_m, p1, q, spec) => {
|
|
819
|
+
return `${p1}${q}${rewriteSpecifier(spec)}${q}`;
|
|
820
|
+
});
|
|
821
|
+
rewritten = rewritten.replace(/(import\s*\(\s*)(['"])([^'"]+)\2(\s*\))/g, (_m, p1, q, spec, p4) => {
|
|
822
|
+
return `${p1}${q}${rewriteSpecifier(spec)}${q}${p4}`;
|
|
823
|
+
});
|
|
824
|
+
writeIfChanged(outPath, rewritten);
|
|
825
|
+
return outPath;
|
|
826
|
+
};
|
|
827
|
+
const resolveCompiledImportPath = (origPath, importerDir, outFileDir) => {
|
|
828
|
+
const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
|
|
829
|
+
if (isBareModule)
|
|
830
|
+
return origPath;
|
|
831
|
+
let absImport;
|
|
832
|
+
if (origPath.startsWith('$')) {
|
|
833
|
+
const slashIdx = origPath.indexOf('/');
|
|
834
|
+
const folder = slashIdx === -1 ? origPath.slice(1) : origPath.slice(1, slashIdx);
|
|
835
|
+
const rest = slashIdx === -1 ? '' : origPath.slice(slashIdx + 1);
|
|
836
|
+
absImport = path.join(srcDir, folder, rest);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
absImport = path.resolve(importerDir, origPath);
|
|
840
|
+
}
|
|
841
|
+
const doProxyTarget = resolveDoProxyTarget(absImport);
|
|
842
|
+
const target = doProxyTarget ?? transformServerModule(absImport);
|
|
843
|
+
let relPath = path.relative(outFileDir, target).replace(/\\/g, '/');
|
|
844
|
+
if (!relPath.startsWith('.'))
|
|
845
|
+
relPath = './' + relPath;
|
|
846
|
+
return relPath;
|
|
847
|
+
};
|
|
576
848
|
// Parse and compile each route
|
|
577
849
|
const compiledRoutes = [];
|
|
578
850
|
const allImports = [];
|
|
579
851
|
let moduleCounter = 0;
|
|
580
|
-
// Layout server import resolution
|
|
852
|
+
// Layout server import resolution â€" resolve non-component imports to module IDs
|
|
581
853
|
let isLayoutAsync = false;
|
|
582
854
|
let compiledLayoutActions = null;
|
|
583
855
|
if (compiledLayout && fs.existsSync(path.join(routesDir, 'layout.html'))) {
|
|
584
856
|
const layoutSource = fs.readFileSync(path.join(routesDir, 'layout.html'), 'utf-8');
|
|
585
|
-
const layoutParsedForImports = parseFile(layoutSource);
|
|
857
|
+
const layoutParsedForImports = parseFile(layoutSource, { kind: 'layout', filePath: layoutFile });
|
|
586
858
|
if (layoutParsedForImports.serverImports.length > 0) {
|
|
587
859
|
const layoutFileDir = routesDir;
|
|
588
860
|
const outFileDir = path.join(projectDir, '.kuratchi');
|
|
@@ -592,39 +864,7 @@ export function compile(options) {
|
|
|
592
864
|
if (!pathMatch)
|
|
593
865
|
continue;
|
|
594
866
|
const origPath = pathMatch[1];
|
|
595
|
-
const
|
|
596
|
-
let importPath;
|
|
597
|
-
if (isBareModule) {
|
|
598
|
-
importPath = origPath;
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
let absImport;
|
|
602
|
-
if (origPath.startsWith('$')) {
|
|
603
|
-
const slashIdx = origPath.indexOf('/');
|
|
604
|
-
const folder = origPath.slice(1, slashIdx);
|
|
605
|
-
const rest = origPath.slice(slashIdx + 1);
|
|
606
|
-
absImport = path.join(srcDir, folder, rest);
|
|
607
|
-
// Redirect DO handler imports to generated proxy modules
|
|
608
|
-
const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
|
|
609
|
-
if (doProxyPath) {
|
|
610
|
-
absImport = doProxyPath;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
absImport = path.resolve(layoutFileDir, origPath);
|
|
615
|
-
}
|
|
616
|
-
let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
|
|
617
|
-
if (!relPath.startsWith('.'))
|
|
618
|
-
relPath = './' + relPath;
|
|
619
|
-
let resolvedExt = '';
|
|
620
|
-
for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
|
|
621
|
-
if (fs.existsSync(absImport + ext)) {
|
|
622
|
-
resolvedExt = ext;
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
importPath = relPath + resolvedExt;
|
|
627
|
-
}
|
|
867
|
+
const importPath = resolveCompiledImportPath(origPath, layoutFileDir, outFileDir);
|
|
628
868
|
const moduleId = `__m${moduleCounter++}`;
|
|
629
869
|
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
630
870
|
const namesMatch = imp.match(/import\s*\{([^}]+)\}/);
|
|
@@ -665,7 +905,7 @@ export function compile(options) {
|
|
|
665
905
|
}
|
|
666
906
|
}
|
|
667
907
|
}
|
|
668
|
-
// Detect if the compiled layout uses await
|
|
908
|
+
// Detect if the compiled layout uses await â†' make it async
|
|
669
909
|
isLayoutAsync = /\bawait\b/.test(compiledLayout);
|
|
670
910
|
if (isLayoutAsync) {
|
|
671
911
|
compiledLayout = compiledLayout.replace(/^function __layout\(/, 'async function __layout(');
|
|
@@ -674,72 +914,62 @@ export function compile(options) {
|
|
|
674
914
|
for (let i = 0; i < routeFiles.length; i++) {
|
|
675
915
|
const rf = routeFiles[i];
|
|
676
916
|
const fullPath = path.join(routesDir, rf.file);
|
|
677
|
-
const source = fs.readFileSync(fullPath, 'utf-8');
|
|
678
|
-
const parsed = parseFile(source);
|
|
679
917
|
const pattern = filePathToPattern(rf.name);
|
|
680
|
-
//
|
|
918
|
+
// -- API route (index.ts / index.js) --
|
|
919
|
+
if (rf.type === 'api') {
|
|
920
|
+
const outFileDir = path.join(projectDir, '.kuratchi');
|
|
921
|
+
// Resolve the route file's absolute path through transformServerModule
|
|
922
|
+
// so that $-prefixed imports inside it get rewritten correctly
|
|
923
|
+
const absRoutePath = transformServerModule(fullPath);
|
|
924
|
+
let importPath = path.relative(outFileDir, absRoutePath).replace(/\\/g, '/');
|
|
925
|
+
if (!importPath.startsWith('.'))
|
|
926
|
+
importPath = './' + importPath;
|
|
927
|
+
const moduleId = `__m${moduleCounter++}`;
|
|
928
|
+
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
929
|
+
// Scan the source for exported method handlers (only include what exists)
|
|
930
|
+
const apiSource = fs.readFileSync(fullPath, 'utf-8');
|
|
931
|
+
const allMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
932
|
+
const exportedMethods = allMethods.filter(m => {
|
|
933
|
+
// Match: export function GET, export async function GET, export { ... as GET }
|
|
934
|
+
const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`);
|
|
935
|
+
const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${m}\\b`);
|
|
936
|
+
const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${m}\\b`);
|
|
937
|
+
return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
|
|
938
|
+
});
|
|
939
|
+
const methodEntries = exportedMethods
|
|
940
|
+
.map(m => `${m}: ${moduleId}.${m}`)
|
|
941
|
+
.join(', ');
|
|
942
|
+
compiledRoutes.push(`{ pattern: '${pattern}', __api: true, ${methodEntries} }`);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
// ── Page route (page.html) ──
|
|
946
|
+
const source = fs.readFileSync(fullPath, 'utf-8');
|
|
947
|
+
const parsed = parseFile(source, { kind: 'route', filePath: fullPath });
|
|
948
|
+
// Build a mapping: functionName â†' moduleId for all imports in this route
|
|
681
949
|
const fnToModule = {};
|
|
682
950
|
const outFileDir = path.join(projectDir, '.kuratchi');
|
|
683
|
-
|
|
951
|
+
const neededServerFns = new Set([
|
|
952
|
+
...parsed.actionFunctions,
|
|
953
|
+
...parsed.pollFunctions,
|
|
954
|
+
...parsed.dataGetQueries.map((q) => q.fnName),
|
|
955
|
+
]);
|
|
956
|
+
const routeServerImports = parsed.serverImports.length > 0
|
|
957
|
+
? parsed.serverImports
|
|
958
|
+
: filterClientImportsForServer(parsed.clientImports, neededServerFns);
|
|
959
|
+
if (routeServerImports.length > 0) {
|
|
684
960
|
const routeFileDir = path.dirname(fullPath);
|
|
685
|
-
for (const imp of
|
|
961
|
+
for (const imp of routeServerImports) {
|
|
686
962
|
const pathMatch = imp.match(/from\s+['"]([^'"]+)['"]/);
|
|
687
963
|
if (!pathMatch)
|
|
688
964
|
continue;
|
|
689
965
|
const origPath = pathMatch[1];
|
|
690
|
-
// Bare module specifiers (packages)
|
|
691
|
-
const
|
|
692
|
-
let importPath;
|
|
693
|
-
if (isBareModule) {
|
|
694
|
-
// Package import: @kuratchi/auth, KuratchiJS, cloudflare:workers, etc.
|
|
695
|
-
importPath = origPath;
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
let absImport;
|
|
699
|
-
if (origPath.startsWith('$')) {
|
|
700
|
-
// Dynamic $folder/ alias → src/folder/
|
|
701
|
-
const slashIdx = origPath.indexOf('/');
|
|
702
|
-
const folder = origPath.slice(1, slashIdx);
|
|
703
|
-
const rest = origPath.slice(slashIdx + 1);
|
|
704
|
-
absImport = path.join(srcDir, folder, rest);
|
|
705
|
-
// Redirect DO handler imports to generated proxy modules
|
|
706
|
-
const doProxyPath = doHandlerProxyPaths.get(absImport.replace(/\\/g, '/'));
|
|
707
|
-
if (doProxyPath) {
|
|
708
|
-
absImport = doProxyPath;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
// Resolve the import relative to the route file
|
|
713
|
-
absImport = path.resolve(routeFileDir, origPath);
|
|
714
|
-
}
|
|
715
|
-
// Make it relative to the output directory
|
|
716
|
-
let relPath = path.relative(outFileDir, absImport).replace(/\\/g, '/');
|
|
717
|
-
if (!relPath.startsWith('.'))
|
|
718
|
-
relPath = './' + relPath;
|
|
719
|
-
// Check if the resolved file exists (try .ts, .js extensions)
|
|
720
|
-
let resolvedExt = '';
|
|
721
|
-
for (const ext of ['.ts', '.js', '/index.ts', '/index.js']) {
|
|
722
|
-
if (fs.existsSync(absImport + ext)) {
|
|
723
|
-
resolvedExt = ext;
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
importPath = relPath + resolvedExt;
|
|
728
|
-
}
|
|
966
|
+
// Bare module specifiers (packages) â€" pass through as-is
|
|
967
|
+
const importPath = resolveCompiledImportPath(origPath, routeFileDir, outFileDir);
|
|
729
968
|
const moduleId = `__m${moduleCounter++}`;
|
|
730
969
|
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
731
970
|
// Extract named imports and map them to this module
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
734
|
-
const names = namesMatch[1]
|
|
735
|
-
.split(',')
|
|
736
|
-
.map(n => n.trim())
|
|
737
|
-
.filter(Boolean)
|
|
738
|
-
.map(n => {
|
|
739
|
-
const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
|
|
740
|
-
return parts[1] || parts[0] || '';
|
|
741
|
-
})
|
|
742
|
-
.filter(Boolean);
|
|
971
|
+
const names = parseNamedImportBindings(imp);
|
|
972
|
+
if (names.length > 0) {
|
|
743
973
|
for (const name of names) {
|
|
744
974
|
fnToModule[name] = moduleId;
|
|
745
975
|
}
|
|
@@ -752,7 +982,7 @@ export function compile(options) {
|
|
|
752
982
|
}
|
|
753
983
|
}
|
|
754
984
|
// Build per-route component names from explicit imports
|
|
755
|
-
// componentImports: { StatCard: 'stat-card' }
|
|
985
|
+
// componentImports: { StatCard: 'stat-card' } â†' componentNames maps PascalCase â†' fileName
|
|
756
986
|
const routeComponentNames = new Map();
|
|
757
987
|
for (const [pascalName, fileName] of Object.entries(parsed.componentImports)) {
|
|
758
988
|
// Compile the component on first use
|
|
@@ -767,22 +997,22 @@ export function compile(options) {
|
|
|
767
997
|
// We then scan the route template for that component's usage and extract the bound values.
|
|
768
998
|
for (const [pascalName, compFileName] of routeComponentNames.entries()) {
|
|
769
999
|
const actionPropNames = componentActionCache.get(compFileName);
|
|
770
|
-
if (!actionPropNames || actionPropNames.size === 0)
|
|
771
|
-
continue;
|
|
772
1000
|
// Find all usages of <PascalName ...> in the route template and extract prop bindings.
|
|
773
1001
|
// Match <ComponentName ... propName={value} ... > across multiple lines.
|
|
774
1002
|
const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)/>`, 'g');
|
|
775
1003
|
for (const tagMatch of parsed.template.matchAll(compTagRegex)) {
|
|
776
1004
|
const attrs = tagMatch[1];
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
parsed.actionFunctions.
|
|
1005
|
+
if (actionPropNames && actionPropNames.size > 0) {
|
|
1006
|
+
for (const propName of actionPropNames) {
|
|
1007
|
+
// Find propName={identifier} binding
|
|
1008
|
+
const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
|
|
1009
|
+
const propMatch = attrs.match(propRegex);
|
|
1010
|
+
if (propMatch) {
|
|
1011
|
+
const routeFnName = propMatch[1];
|
|
1012
|
+
// Only add if this function is actually imported by the route
|
|
1013
|
+
if (routeFnName in fnToModule && !parsed.actionFunctions.includes(routeFnName)) {
|
|
1014
|
+
parsed.actionFunctions.push(routeFnName);
|
|
1015
|
+
}
|
|
786
1016
|
}
|
|
787
1017
|
}
|
|
788
1018
|
}
|
|
@@ -791,7 +1021,7 @@ export function compile(options) {
|
|
|
791
1021
|
// Compile template to render function body (pass component names and action names)
|
|
792
1022
|
// An identifier is a valid server action if it is either:
|
|
793
1023
|
// 1. Directly imported (present in fnToModule), or
|
|
794
|
-
// 2. A top-level script declaration (present in dataVars)
|
|
1024
|
+
// 2. A top-level script declaration (present in dataVars) â€" covers cases like
|
|
795
1025
|
// `const fn = importedFn` or `async function fn() {}` where the binding
|
|
796
1026
|
// is locally declared but delegates to an imported function.
|
|
797
1027
|
const dataVarsSet = new Set(parsed.dataVars);
|
|
@@ -869,7 +1099,16 @@ export function compile(options) {
|
|
|
869
1099
|
// Collect only the components that were actually imported by routes
|
|
870
1100
|
const compiledComponents = Array.from(compiledComponentCache.values());
|
|
871
1101
|
// Generate the routes module
|
|
872
|
-
const
|
|
1102
|
+
const rawRuntimeImportPath = resolveRuntimeImportPath(projectDir);
|
|
1103
|
+
let runtimeImportPath;
|
|
1104
|
+
if (rawRuntimeImportPath) {
|
|
1105
|
+
// Resolve the runtime file's absolute path and pass it through transformServerModule
|
|
1106
|
+
// so that $durable-objects/* and other project imports get rewritten to their proxies.
|
|
1107
|
+
const runtimeAbs = path.resolve(path.join(projectDir, '.kuratchi'), rawRuntimeImportPath);
|
|
1108
|
+
const transformedRuntimePath = transformServerModule(runtimeAbs);
|
|
1109
|
+
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
|
|
1110
|
+
runtimeImportPath = toModuleSpecifier(outFile, transformedRuntimePath);
|
|
1111
|
+
}
|
|
873
1112
|
const hasRuntime = !!runtimeImportPath;
|
|
874
1113
|
const output = generateRoutesModule({
|
|
875
1114
|
projectDir,
|
|
@@ -887,7 +1126,7 @@ export function compile(options) {
|
|
|
887
1126
|
isLayoutAsync,
|
|
888
1127
|
compiledLayoutActions,
|
|
889
1128
|
hasRuntime,
|
|
890
|
-
runtimeImportPath
|
|
1129
|
+
runtimeImportPath,
|
|
891
1130
|
});
|
|
892
1131
|
// Write to .kuratchi/routes.js
|
|
893
1132
|
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
|
|
@@ -919,7 +1158,7 @@ export function compile(options) {
|
|
|
919
1158
|
writeIfChanged(workerFile, workerLines.join('\n'));
|
|
920
1159
|
return workerFile;
|
|
921
1160
|
}
|
|
922
|
-
//
|
|
1161
|
+
// â"€â"€ Helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
923
1162
|
/**
|
|
924
1163
|
* Write a file only if its content has changed.
|
|
925
1164
|
* Prevents unnecessary filesystem events that would retrigger wrangler's file watcher.
|
|
@@ -1013,7 +1252,7 @@ function readUiTheme(projectDir) {
|
|
|
1013
1252
|
console.warn('[kuratchi] ui.theme: "default" configured but @kuratchi/ui theme.css not found');
|
|
1014
1253
|
return null;
|
|
1015
1254
|
}
|
|
1016
|
-
// Custom path
|
|
1255
|
+
// Custom path â€" resolve relative to project root
|
|
1017
1256
|
const customPath = path.resolve(projectDir, themeValue);
|
|
1018
1257
|
if (fs.existsSync(customPath)) {
|
|
1019
1258
|
return fs.readFileSync(customPath, 'utf-8');
|
|
@@ -1031,7 +1270,7 @@ function resolvePackageComponent(projectDir, pkgName, componentFile) {
|
|
|
1031
1270
|
if (fs.existsSync(nmPath))
|
|
1032
1271
|
return nmPath;
|
|
1033
1272
|
// 2. Try workspace layout: project is in apps/X or packages/X, sibling packages in packages/
|
|
1034
|
-
// @kuratchi/ui
|
|
1273
|
+
// @kuratchi/ui â†' kuratchi-ui (convention: scope stripped, slash â†' dash)
|
|
1035
1274
|
const pkgDirName = pkgName.replace(/^@/, '').replace(/\//g, '-');
|
|
1036
1275
|
const workspaceRoot = path.resolve(projectDir, '../..');
|
|
1037
1276
|
const wsPath = path.join(workspaceRoot, 'packages', pkgDirName, 'src', 'lib', componentFile + '.html');
|
|
@@ -1069,32 +1308,49 @@ function discoverRoutes(routesDir) {
|
|
|
1069
1308
|
for (const entry of entries) {
|
|
1070
1309
|
if (entry.isDirectory()) {
|
|
1071
1310
|
const childPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1072
|
-
// Folder-based route: routes/db/page.html
|
|
1311
|
+
// Folder-based page route: routes/db/page.html → /db
|
|
1073
1312
|
const pageFile = path.join(dir, entry.name, 'page.html');
|
|
1074
1313
|
if (fs.existsSync(pageFile)) {
|
|
1075
1314
|
const routeFile = `${childPrefix}/page.html`;
|
|
1076
1315
|
if (!registered.has(routeFile)) {
|
|
1077
1316
|
registered.add(routeFile);
|
|
1078
|
-
results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix) });
|
|
1317
|
+
results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix), type: 'page' });
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// Folder-based API route: routes/api/v1/health/index.ts -> /api/v1/health
|
|
1321
|
+
const apiFile = ['index.ts', 'index.js'].find(f => fs.existsSync(path.join(dir, entry.name, f)));
|
|
1322
|
+
if (apiFile && !fs.existsSync(pageFile)) {
|
|
1323
|
+
const routeFile = `${childPrefix}/${apiFile}`;
|
|
1324
|
+
if (!registered.has(routeFile)) {
|
|
1325
|
+
registered.add(routeFile);
|
|
1326
|
+
results.push({ file: routeFile, name: childPrefix, layouts: [], type: 'api' });
|
|
1079
1327
|
}
|
|
1080
1328
|
}
|
|
1081
1329
|
// Always recurse into subdirectory (for nested routes like /admin/roles)
|
|
1082
1330
|
walk(path.join(dir, entry.name), childPrefix);
|
|
1083
1331
|
}
|
|
1084
1332
|
else if (entry.name === 'layout.html' || entry.name === '404.html' || entry.name === '500.html') {
|
|
1085
|
-
// Skip
|
|
1333
|
+
// Skip — layout.html is the app layout, 404/500 are error pages, not routes
|
|
1086
1334
|
continue;
|
|
1087
1335
|
}
|
|
1336
|
+
else if (entry.name === 'index.ts' || entry.name === 'index.js') {
|
|
1337
|
+
// API route file in current directory -> index API route for this prefix
|
|
1338
|
+
const routeFile = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1339
|
+
if (!registered.has(routeFile)) {
|
|
1340
|
+
registered.add(routeFile);
|
|
1341
|
+
results.push({ file: routeFile, name: prefix || 'index', layouts: [], type: 'api' });
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1088
1344
|
else if (entry.name === 'page.html') {
|
|
1089
|
-
// page.html in current directory
|
|
1345
|
+
// page.html in current directory → index route for this prefix
|
|
1090
1346
|
const routeFile = prefix ? `${prefix}/page.html` : 'page.html';
|
|
1091
1347
|
if (!registered.has(routeFile)) {
|
|
1092
1348
|
registered.add(routeFile);
|
|
1093
|
-
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix) });
|
|
1349
|
+
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix), type: 'page' });
|
|
1094
1350
|
}
|
|
1095
1351
|
}
|
|
1096
1352
|
else if (entry.name.endsWith('.html') && entry.name !== 'page.html') {
|
|
1097
|
-
// File-based route: routes/about.html
|
|
1353
|
+
// File-based route: routes/about.html → /about (fallback)
|
|
1098
1354
|
const name = prefix
|
|
1099
1355
|
? `${prefix}/${entry.name.replace('.html', '')}`
|
|
1100
1356
|
: entry.name.replace('.html', '');
|
|
@@ -1102,6 +1358,7 @@ function discoverRoutes(routesDir) {
|
|
|
1102
1358
|
file: prefix ? `${prefix}/${entry.name}` : entry.name,
|
|
1103
1359
|
name,
|
|
1104
1360
|
layouts: getLayoutsForPrefix(prefix),
|
|
1361
|
+
type: 'page',
|
|
1105
1362
|
});
|
|
1106
1363
|
}
|
|
1107
1364
|
}
|
|
@@ -1120,18 +1377,48 @@ function buildRouteObject(opts) {
|
|
|
1120
1377
|
const hasFns = Object.keys(fnToModule).length > 0;
|
|
1121
1378
|
const parts = [];
|
|
1122
1379
|
parts.push(` pattern: '${pattern}'`);
|
|
1123
|
-
|
|
1380
|
+
const queryVars = parsed.dataGetQueries?.map((q) => q.asName) ?? [];
|
|
1381
|
+
let scriptBody = parsed.script
|
|
1382
|
+
? parsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim()
|
|
1383
|
+
: '';
|
|
1384
|
+
scriptBody = rewriteImportedFunctionCalls(scriptBody, fnToModule);
|
|
1385
|
+
scriptBody = rewriteWorkerEnvAliases(scriptBody, parsed.workerEnvAliases);
|
|
1386
|
+
let explicitLoadFunction = parsed.loadFunction
|
|
1387
|
+
? parsed.loadFunction.replace(/^export\s+/, '').trim()
|
|
1388
|
+
: '';
|
|
1389
|
+
if (explicitLoadFunction) {
|
|
1390
|
+
explicitLoadFunction = rewriteImportedFunctionCalls(explicitLoadFunction, fnToModule);
|
|
1391
|
+
explicitLoadFunction = rewriteWorkerEnvAliases(explicitLoadFunction, parsed.workerEnvAliases);
|
|
1392
|
+
}
|
|
1393
|
+
if (explicitLoadFunction && /\bawait\b/.test(scriptBody)) {
|
|
1394
|
+
throw new Error(`[kuratchi compiler] ${pattern}\nTop-level await cannot be mixed with export async function load(). Move async server work into load().`);
|
|
1395
|
+
}
|
|
1396
|
+
if (scriptBody) {
|
|
1397
|
+
scriptBody = transpileTypeScript(scriptBody, `route-script:${pattern}.ts`);
|
|
1398
|
+
}
|
|
1399
|
+
if (explicitLoadFunction) {
|
|
1400
|
+
explicitLoadFunction = transpileTypeScript(explicitLoadFunction, `route-load:${pattern}.ts`);
|
|
1401
|
+
}
|
|
1402
|
+
const scriptUsesAwait = /\bawait\b/.test(scriptBody);
|
|
1403
|
+
const scriptReturnVars = parsed.script
|
|
1404
|
+
? parsed.dataVars.filter((v) => !queryVars.includes(v))
|
|
1405
|
+
: [];
|
|
1406
|
+
// Load function â€" internal server prepass for async route script bodies
|
|
1407
|
+
// and data-get query state hydration.
|
|
1124
1408
|
const hasDataGetQueries = Array.isArray(parsed.dataGetQueries) && parsed.dataGetQueries.length > 0;
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1409
|
+
if (explicitLoadFunction) {
|
|
1410
|
+
parts.push(` load: ${explicitLoadFunction}`);
|
|
1411
|
+
}
|
|
1412
|
+
else if ((scriptBody && scriptUsesAwait) || hasDataGetQueries) {
|
|
1413
|
+
let loadBody = '';
|
|
1414
|
+
if (scriptBody && scriptUsesAwait) {
|
|
1415
|
+
loadBody = scriptBody;
|
|
1416
|
+
}
|
|
1130
1417
|
// Inject data-get query state blocks into load scope.
|
|
1131
1418
|
// Each query exposes:
|
|
1132
1419
|
// { state, loading, error, data, empty, success }
|
|
1420
|
+
const queries = parsed.dataGetQueries;
|
|
1133
1421
|
if (hasDataGetQueries) {
|
|
1134
|
-
const queries = parsed.dataGetQueries;
|
|
1135
1422
|
const queryLines = [];
|
|
1136
1423
|
for (const q of queries) {
|
|
1137
1424
|
const fnName = q.fnName;
|
|
@@ -1154,25 +1441,13 @@ function buildRouteObject(opts) {
|
|
|
1154
1441
|
queryLines.push(` }`);
|
|
1155
1442
|
queryLines.push(`}`);
|
|
1156
1443
|
}
|
|
1157
|
-
|
|
1444
|
+
loadBody = [loadBody, queryLines.join('\n')].filter(Boolean).join('\n');
|
|
1158
1445
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
|
|
1163
|
-
continue;
|
|
1164
|
-
const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
|
|
1165
|
-
body = body.replace(callRegex, `${moduleId}.${fnName}(`);
|
|
1166
|
-
}
|
|
1167
|
-
// Determine if body uses await
|
|
1168
|
-
const isAsync = /\bawait\b/.test(body) || hasDataGetQueries;
|
|
1169
|
-
// Return an object with all declared data variables
|
|
1170
|
-
const returnObj = parsed.dataVars.length > 0
|
|
1171
|
-
? `\n return { ${parsed.dataVars.join(', ')} };`
|
|
1172
|
-
: '';
|
|
1173
|
-
parts.push(` ${isAsync ? 'async ' : ''}load(params = {}) {\n ${body}${returnObj}\n }`);
|
|
1446
|
+
const loadReturnVars = [...scriptReturnVars, ...queryVars];
|
|
1447
|
+
const returnObj = loadReturnVars.length > 0 ? `\n return { ${loadReturnVars.join(', ')} };` : '';
|
|
1448
|
+
parts.push(` async load(params = {}) {\n ${loadBody}${returnObj}\n }`);
|
|
1174
1449
|
}
|
|
1175
|
-
// Actions
|
|
1450
|
+
// Actions â€" functions referenced via action={fn} in the template
|
|
1176
1451
|
if (hasFns && parsed.actionFunctions.length > 0) {
|
|
1177
1452
|
const actionEntries = parsed.actionFunctions
|
|
1178
1453
|
.map(fn => {
|
|
@@ -1182,7 +1457,7 @@ function buildRouteObject(opts) {
|
|
|
1182
1457
|
.join(', ');
|
|
1183
1458
|
parts.push(` actions: { ${actionEntries} }`);
|
|
1184
1459
|
}
|
|
1185
|
-
// RPC
|
|
1460
|
+
// RPC â€" functions referenced via data-poll={fn(args)} in the template
|
|
1186
1461
|
if (hasFns && parsed.pollFunctions.length > 0) {
|
|
1187
1462
|
const rpcEntries = parsed.pollFunctions
|
|
1188
1463
|
.map(fn => {
|
|
@@ -1193,12 +1468,21 @@ function buildRouteObject(opts) {
|
|
|
1193
1468
|
.join(', ');
|
|
1194
1469
|
parts.push(` rpc: { ${rpcEntries} }`);
|
|
1195
1470
|
}
|
|
1196
|
-
// Render function
|
|
1471
|
+
// Render function â€" template compiled to JS with native flow control
|
|
1197
1472
|
// Destructure data vars so templates reference them directly (e.g., {todos} not {data.todos})
|
|
1198
|
-
//
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1473
|
+
// Auto-inject action state objects so templates can reference signIn.error, signIn.loading, etc.
|
|
1474
|
+
const renderPrelude = (scriptBody && !scriptUsesAwait) ? scriptBody : '';
|
|
1475
|
+
const allVars = [...queryVars];
|
|
1476
|
+
if (scriptUsesAwait) {
|
|
1477
|
+
for (const v of scriptReturnVars) {
|
|
1478
|
+
if (!allVars.includes(v))
|
|
1479
|
+
allVars.push(v);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
for (const fn of parsed.actionFunctions) {
|
|
1483
|
+
if (!allVars.includes(fn))
|
|
1484
|
+
allVars.push(fn);
|
|
1485
|
+
}
|
|
1202
1486
|
if (!allVars.includes('params'))
|
|
1203
1487
|
allVars.push('params');
|
|
1204
1488
|
if (!allVars.includes('breadcrumbs'))
|
|
@@ -1213,7 +1497,7 @@ function buildRouteObject(opts) {
|
|
|
1213
1497
|
finalRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
|
|
1214
1498
|
}
|
|
1215
1499
|
parts.push(` render(data) {
|
|
1216
|
-
${destructure}${finalRenderBody}
|
|
1500
|
+
${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
|
|
1217
1501
|
return __html;
|
|
1218
1502
|
}`);
|
|
1219
1503
|
return ` {\n${parts.join(',\n')}\n }`;
|
|
@@ -1227,7 +1511,7 @@ function readOrmConfig(projectDir) {
|
|
|
1227
1511
|
if (!ormBlock)
|
|
1228
1512
|
return [];
|
|
1229
1513
|
// Extract schema imports: import { todoSchema } from './src/schemas/todo';
|
|
1230
|
-
const importMap = new Map(); // exportName
|
|
1514
|
+
const importMap = new Map(); // exportName â†' importPath
|
|
1231
1515
|
const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
1232
1516
|
let m;
|
|
1233
1517
|
while ((m = importRegex.exec(source)) !== null) {
|
|
@@ -1364,7 +1648,7 @@ function readDoConfig(projectDir) {
|
|
|
1364
1648
|
if (list.length > 0)
|
|
1365
1649
|
entry.files = list;
|
|
1366
1650
|
}
|
|
1367
|
-
// (inject config removed
|
|
1651
|
+
// (inject config removed â€" DO methods are org-scoped, no auto-injection needed)
|
|
1368
1652
|
entries.push(entry);
|
|
1369
1653
|
}
|
|
1370
1654
|
// Match string shorthand: BINDING: 'ClassName' (skip bindings already found)
|
|
@@ -1482,9 +1766,9 @@ function discoverFilesWithSuffix(dir, suffix) {
|
|
|
1482
1766
|
return out;
|
|
1483
1767
|
}
|
|
1484
1768
|
/**
|
|
1485
|
-
* Scan
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1769
|
+
* Scan DO handler files.
|
|
1770
|
+
* - Class mode: default class extends kuratchiDO
|
|
1771
|
+
* - Function mode: exported functions in *.do.ts files (compiler wraps into DO class)
|
|
1488
1772
|
*/
|
|
1489
1773
|
function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
1490
1774
|
const serverDir = path.join(srcDir, 'server');
|
|
@@ -1512,14 +1796,15 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1512
1796
|
for (const absPath of discoveredFiles) {
|
|
1513
1797
|
const file = path.basename(absPath);
|
|
1514
1798
|
const source = fs.readFileSync(absPath, 'utf-8');
|
|
1515
|
-
|
|
1516
|
-
|
|
1799
|
+
const exportedFunctions = extractExportedFunctions(source);
|
|
1800
|
+
const hasClass = /extends\s+kuratchiDO\b/.test(source);
|
|
1801
|
+
if (!hasClass && exportedFunctions.length === 0)
|
|
1517
1802
|
continue;
|
|
1518
|
-
// Extract class name
|
|
1803
|
+
// Extract class name when class mode is used.
|
|
1519
1804
|
const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+kuratchiDO/);
|
|
1520
|
-
|
|
1805
|
+
const className = classMatch?.[1] ?? null;
|
|
1806
|
+
if (hasClass && !className)
|
|
1521
1807
|
continue;
|
|
1522
|
-
const className = classMatch[1];
|
|
1523
1808
|
// Binding resolution:
|
|
1524
1809
|
// 1) explicit static binding in class
|
|
1525
1810
|
// 2) config-mapped file name (supports .do.ts convention)
|
|
@@ -1544,10 +1829,8 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1544
1829
|
continue;
|
|
1545
1830
|
if (!bindings.has(binding))
|
|
1546
1831
|
continue;
|
|
1547
|
-
// Extract class methods
|
|
1548
|
-
const classMethods = extractClassMethods(source, className);
|
|
1549
|
-
// Extract named exports (custom worker-side helpers)
|
|
1550
|
-
const namedExports = extractNamedExports(source);
|
|
1832
|
+
// Extract class methods in class mode
|
|
1833
|
+
const classMethods = className ? extractClassMethods(source, className) : [];
|
|
1551
1834
|
const fileName = file.replace(/\.ts$/, '');
|
|
1552
1835
|
const existing = fileNameToAbsPath.get(fileName);
|
|
1553
1836
|
if (existing && existing !== absPath) {
|
|
@@ -1558,9 +1841,10 @@ function discoverDoHandlers(srcDir, doConfig, ormDatabases) {
|
|
|
1558
1841
|
fileName,
|
|
1559
1842
|
absPath,
|
|
1560
1843
|
binding,
|
|
1561
|
-
|
|
1844
|
+
mode: hasClass ? 'class' : 'function',
|
|
1845
|
+
className: className ?? undefined,
|
|
1562
1846
|
classMethods,
|
|
1563
|
-
|
|
1847
|
+
exportedFunctions,
|
|
1564
1848
|
});
|
|
1565
1849
|
}
|
|
1566
1850
|
return handlers;
|
|
@@ -1652,40 +1936,33 @@ function extractClassMethods(source, className) {
|
|
|
1652
1936
|
}
|
|
1653
1937
|
return methods;
|
|
1654
1938
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
*/
|
|
1658
|
-
function extractNamedExports(source) {
|
|
1659
|
-
const exports = [];
|
|
1660
|
-
// export async function name / export function name
|
|
1939
|
+
function extractExportedFunctions(source) {
|
|
1940
|
+
const out = [];
|
|
1661
1941
|
const fnRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
|
|
1662
1942
|
let m;
|
|
1663
1943
|
while ((m = fnRegex.exec(source)) !== null)
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const constRegex = /export\s+const\s+(\w+)/g;
|
|
1667
|
-
while ((m = constRegex.exec(source)) !== null)
|
|
1668
|
-
exports.push(m[1]);
|
|
1669
|
-
return exports;
|
|
1944
|
+
out.push(m[1]);
|
|
1945
|
+
return out;
|
|
1670
1946
|
}
|
|
1671
1947
|
/**
|
|
1672
1948
|
* Generate a proxy module for a DO handler file.
|
|
1673
1949
|
*
|
|
1674
|
-
* The proxy provides
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
* Methods that collide with custom exports are skipped (user's export wins).
|
|
1950
|
+
* The proxy provides auto-RPC function exports.
|
|
1951
|
+
* - Class mode: public class methods become RPC exports.
|
|
1952
|
+
* - Function mode: exported functions become RPC exports, excluding lifecycle hooks.
|
|
1679
1953
|
*/
|
|
1680
1954
|
function generateHandlerProxy(handler, projectDir) {
|
|
1681
1955
|
const doDir = path.join(projectDir, '.kuratchi', 'do');
|
|
1682
1956
|
const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
|
|
1683
1957
|
const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
|
|
1684
|
-
const
|
|
1958
|
+
const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
|
|
1959
|
+
const rpcFunctions = handler.mode === 'function'
|
|
1960
|
+
? handler.exportedFunctions.filter((n) => !lifecycle.has(n))
|
|
1961
|
+
: handler.classMethods.filter((m) => m.visibility === 'public').map((m) => m.name);
|
|
1685
1962
|
const methods = handler.classMethods.map((m) => ({ ...m }));
|
|
1686
1963
|
const methodMap = new Map(methods.map((m) => [m.name, m]));
|
|
1687
1964
|
let changed = true;
|
|
1688
|
-
while (changed) {
|
|
1965
|
+
while (changed && handler.mode === 'class') {
|
|
1689
1966
|
changed = false;
|
|
1690
1967
|
for (const m of methods) {
|
|
1691
1968
|
if (m.hasWorkerContextCalls)
|
|
@@ -1700,15 +1977,56 @@ function generateHandlerProxy(handler, projectDir) {
|
|
|
1700
1977
|
}
|
|
1701
1978
|
}
|
|
1702
1979
|
}
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1980
|
+
const workerContextMethods = handler.mode === 'class'
|
|
1981
|
+
? methods.filter((m) => m.visibility === 'public' && m.hasWorkerContextCalls).map((m) => m.name)
|
|
1982
|
+
: [];
|
|
1983
|
+
const asyncMethods = handler.mode === 'class'
|
|
1984
|
+
? methods.filter((m) => m.isAsync).map((m) => m.name)
|
|
1985
|
+
: [];
|
|
1708
1986
|
const lines = [
|
|
1709
|
-
`// Auto-generated by KuratchiJS compiler
|
|
1987
|
+
`// Auto-generated by KuratchiJS compiler â€" do not edit.`,
|
|
1710
1988
|
`import { __getDoStub } from '${RUNTIME_DO_IMPORT}';`,
|
|
1711
|
-
`import ${handlerLocal} from '${origRelPath}'
|
|
1989
|
+
...(handler.mode === 'class' ? [`import ${handlerLocal} from '${origRelPath}';`] : []),
|
|
1990
|
+
``,
|
|
1991
|
+
`const __FD_TAG = '__kuratchi_form_data__';`,
|
|
1992
|
+
`function __isPlainObject(__v) {`,
|
|
1993
|
+
` if (!__v || typeof __v !== 'object') return false;`,
|
|
1994
|
+
` const __proto = Object.getPrototypeOf(__v);`,
|
|
1995
|
+
` return __proto === Object.prototype || __proto === null;`,
|
|
1996
|
+
`}`,
|
|
1997
|
+
`function __encodeArg(__v, __seen = new WeakSet()) {`,
|
|
1998
|
+
` if (typeof FormData !== 'undefined' && __v instanceof FormData) {`,
|
|
1999
|
+
` return { [__FD_TAG]: Array.from(__v.entries()) };`,
|
|
2000
|
+
` }`,
|
|
2001
|
+
` if (Array.isArray(__v)) return __v.map((__x) => __encodeArg(__x, __seen));`,
|
|
2002
|
+
` if (__isPlainObject(__v)) {`,
|
|
2003
|
+
` if (__seen.has(__v)) throw new Error('[KuratchiJS] Circular object passed to DO RPC');`,
|
|
2004
|
+
` __seen.add(__v);`,
|
|
2005
|
+
` const __out = {};`,
|
|
2006
|
+
` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __encodeArg(__val, __seen);`,
|
|
2007
|
+
` __seen.delete(__v);`,
|
|
2008
|
+
` return __out;`,
|
|
2009
|
+
` }`,
|
|
2010
|
+
` return __v;`,
|
|
2011
|
+
`}`,
|
|
2012
|
+
`function __decodeArg(__v) {`,
|
|
2013
|
+
` if (Array.isArray(__v)) return __v.map(__decodeArg);`,
|
|
2014
|
+
` if (__isPlainObject(__v)) {`,
|
|
2015
|
+
` const __obj = __v;`,
|
|
2016
|
+
` if (__FD_TAG in __obj) {`,
|
|
2017
|
+
` const __fd = new FormData();`,
|
|
2018
|
+
` const __entries = Array.isArray(__obj[__FD_TAG]) ? __obj[__FD_TAG] : [];`,
|
|
2019
|
+
` for (const __pair of __entries) {`,
|
|
2020
|
+
` if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]);`,
|
|
2021
|
+
` }`,
|
|
2022
|
+
` return __fd;`,
|
|
2023
|
+
` }`,
|
|
2024
|
+
` const __out = {};`,
|
|
2025
|
+
` for (const [__k, __val] of Object.entries(__obj)) __out[__k] = __decodeArg(__val);`,
|
|
2026
|
+
` return __out;`,
|
|
2027
|
+
` }`,
|
|
2028
|
+
` return __v;`,
|
|
2029
|
+
`}`,
|
|
1712
2030
|
``,
|
|
1713
2031
|
];
|
|
1714
2032
|
if (workerContextMethods.length > 0) {
|
|
@@ -1728,29 +2046,22 @@ function generateHandlerProxy(handler, projectDir) {
|
|
|
1728
2046
|
lines.push(` if (typeof __local === 'function' && !__asyncMethods.has(__k)) {`);
|
|
1729
2047
|
lines.push(` return (...__a) => __local.apply(__self, __a);`);
|
|
1730
2048
|
lines.push(` }`);
|
|
1731
|
-
lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a); };`);
|
|
2049
|
+
lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a.map((__x) => __encodeArg(__x))); };`);
|
|
1732
2050
|
lines.push(` },`);
|
|
1733
2051
|
lines.push(` });`);
|
|
1734
|
-
lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args);`);
|
|
2052
|
+
lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args.map(__decodeArg));`);
|
|
1735
2053
|
lines.push(`}`);
|
|
1736
2054
|
lines.push(``);
|
|
1737
2055
|
}
|
|
1738
|
-
// Export
|
|
1739
|
-
for (const method of
|
|
1740
|
-
if (customSet.has(method))
|
|
1741
|
-
continue; // user's export wins
|
|
2056
|
+
// Export RPC methods
|
|
2057
|
+
for (const method of rpcFunctions) {
|
|
1742
2058
|
if (workerContextMethods.includes(method)) {
|
|
1743
2059
|
lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', a); }`);
|
|
1744
2060
|
}
|
|
1745
2061
|
else {
|
|
1746
|
-
lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a); }`);
|
|
2062
|
+
lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a.map((__x) => __encodeArg(__x))); }`);
|
|
1747
2063
|
}
|
|
1748
2064
|
}
|
|
1749
|
-
// Re-export custom named exports from the original file
|
|
1750
|
-
if (handler.namedExports.length > 0) {
|
|
1751
|
-
lines.push(``);
|
|
1752
|
-
lines.push(`export { ${handler.namedExports.join(', ')} } from '${origRelPath}';`);
|
|
1753
|
-
}
|
|
1754
2065
|
return lines.join('\n') + '\n';
|
|
1755
2066
|
}
|
|
1756
2067
|
function generateRoutesModule(opts) {
|
|
@@ -1763,16 +2074,16 @@ function generateRoutesModule(opts) {
|
|
|
1763
2074
|
.map(([status, fn]) => fn)
|
|
1764
2075
|
.join('\n\n');
|
|
1765
2076
|
// Resolve path to the framework's context module from the output directory
|
|
1766
|
-
const contextImport = `import { __setRequestContext, __esc, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
2077
|
+
const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
1767
2078
|
const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
|
|
1768
2079
|
? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
|
|
1769
2080
|
: '';
|
|
1770
|
-
// Auth session init
|
|
2081
|
+
// Auth session init â€" thin cookie parsing injected into Worker entry
|
|
1771
2082
|
let authInit = '';
|
|
1772
2083
|
if (opts.authConfig && opts.authConfig.sessionEnabled) {
|
|
1773
2084
|
const cookieName = opts.authConfig.cookieName;
|
|
1774
2085
|
authInit = `
|
|
1775
|
-
//
|
|
2086
|
+
// â"€â"€ Auth Session Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1776
2087
|
|
|
1777
2088
|
function __parseCookies(header) {
|
|
1778
2089
|
const map = {};
|
|
@@ -1823,7 +2134,7 @@ function __initAuth(request) {
|
|
|
1823
2134
|
...schemaImports,
|
|
1824
2135
|
].join('\n');
|
|
1825
2136
|
migrationInit = `
|
|
1826
|
-
//
|
|
2137
|
+
// â"€â"€ ORM Auto-Migration â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1827
2138
|
|
|
1828
2139
|
let __migrated = false;
|
|
1829
2140
|
const __ormDatabases = [
|
|
@@ -1857,7 +2168,7 @@ async function __runMigrations() {
|
|
|
1857
2168
|
`;
|
|
1858
2169
|
}
|
|
1859
2170
|
}
|
|
1860
|
-
// Auth plugin init
|
|
2171
|
+
// Auth plugin init â€" import config + call @kuratchi/auth setup functions
|
|
1861
2172
|
let authPluginImports = '';
|
|
1862
2173
|
let authPluginInit = '';
|
|
1863
2174
|
const ac = opts.authConfig;
|
|
@@ -1909,14 +2220,14 @@ async function __runMigrations() {
|
|
|
1909
2220
|
}
|
|
1910
2221
|
authPluginImports = imports.join('\n');
|
|
1911
2222
|
authPluginInit = `
|
|
1912
|
-
//
|
|
2223
|
+
// â"€â"€ Auth Plugin Init â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1913
2224
|
|
|
1914
2225
|
function __initAuthPlugins() {
|
|
1915
2226
|
${initLines.join('\n')}
|
|
1916
2227
|
}
|
|
1917
2228
|
`;
|
|
1918
2229
|
}
|
|
1919
|
-
//
|
|
2230
|
+
// â"€â"€ Durable Object class generation â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
1920
2231
|
let doImports = '';
|
|
1921
2232
|
let doClassCode = '';
|
|
1922
2233
|
let doResolverInit = '';
|
|
@@ -1926,7 +2237,28 @@ ${initLines.join('\n')}
|
|
|
1926
2237
|
const doResolverLines = [];
|
|
1927
2238
|
doImportLines.push(`import { DurableObject as __DO } from 'cloudflare:workers';`);
|
|
1928
2239
|
doImportLines.push(`import { initDO as __initDO } from '@kuratchi/orm';`);
|
|
1929
|
-
doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding } from '${RUNTIME_DO_IMPORT}';`);
|
|
2240
|
+
doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding, __setDoContext } from '${RUNTIME_DO_IMPORT}';`);
|
|
2241
|
+
doImportLines.push(`const __DO_FD_TAG = '__kuratchi_form_data__';`);
|
|
2242
|
+
doImportLines.push(`function __isDoPlainObject(__v) {`);
|
|
2243
|
+
doImportLines.push(` if (!__v || typeof __v !== 'object') return false;`);
|
|
2244
|
+
doImportLines.push(` const __proto = Object.getPrototypeOf(__v);`);
|
|
2245
|
+
doImportLines.push(` return __proto === Object.prototype || __proto === null;`);
|
|
2246
|
+
doImportLines.push(`}`);
|
|
2247
|
+
doImportLines.push(`function __decodeDoArg(__v) {`);
|
|
2248
|
+
doImportLines.push(` if (Array.isArray(__v)) return __v.map(__decodeDoArg);`);
|
|
2249
|
+
doImportLines.push(` if (__isDoPlainObject(__v)) {`);
|
|
2250
|
+
doImportLines.push(` if (__DO_FD_TAG in __v) {`);
|
|
2251
|
+
doImportLines.push(` const __fd = new FormData();`);
|
|
2252
|
+
doImportLines.push(` const __entries = Array.isArray(__v[__DO_FD_TAG]) ? __v[__DO_FD_TAG] : [];`);
|
|
2253
|
+
doImportLines.push(` for (const __pair of __entries) { if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]); }`);
|
|
2254
|
+
doImportLines.push(` return __fd;`);
|
|
2255
|
+
doImportLines.push(` }`);
|
|
2256
|
+
doImportLines.push(` const __out = {};`);
|
|
2257
|
+
doImportLines.push(` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __decodeDoArg(__val);`);
|
|
2258
|
+
doImportLines.push(` return __out;`);
|
|
2259
|
+
doImportLines.push(` }`);
|
|
2260
|
+
doImportLines.push(` return __v;`);
|
|
2261
|
+
doImportLines.push(`}`);
|
|
1930
2262
|
// We need getCurrentUser and getOrgStubByName for stub resolvers
|
|
1931
2263
|
doImportLines.push(`import { getCurrentUser as __getCU, getOrgStubByName as __getOSBN } from '@kuratchi/auth';`);
|
|
1932
2264
|
// Group handlers by binding
|
|
@@ -1940,6 +2272,10 @@ ${initLines.join('\n')}
|
|
|
1940
2272
|
for (const doEntry of opts.doConfig) {
|
|
1941
2273
|
const handlers = handlersByBinding.get(doEntry.binding) ?? [];
|
|
1942
2274
|
const ormDb = opts.ormDatabases.find(d => d.binding === doEntry.binding);
|
|
2275
|
+
const fnHandlers = handlers.filter((h) => h.mode === 'function');
|
|
2276
|
+
const initHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onInit'));
|
|
2277
|
+
const alarmHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onAlarm'));
|
|
2278
|
+
const messageHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onMessage'));
|
|
1943
2279
|
// Import schema (paths are relative to project root; prefix ../ since we're in .kuratchi/)
|
|
1944
2280
|
if (ormDb) {
|
|
1945
2281
|
const schemaPath = ormDb.schemaImportPath.replace(/^\.\//, '../');
|
|
@@ -1954,7 +2290,12 @@ ${initLines.join('\n')}
|
|
|
1954
2290
|
if (!handlerImportPath.startsWith('.'))
|
|
1955
2291
|
handlerImportPath = './' + handlerImportPath;
|
|
1956
2292
|
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
1957
|
-
|
|
2293
|
+
if (h.mode === 'class') {
|
|
2294
|
+
doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
|
|
2295
|
+
}
|
|
2296
|
+
else {
|
|
2297
|
+
doImportLines.push(`import * as ${handlerVar} from '${handlerImportPath}';`);
|
|
2298
|
+
}
|
|
1958
2299
|
}
|
|
1959
2300
|
// Generate DO class
|
|
1960
2301
|
doClassLines.push(`export class ${doEntry.className} extends __DO {`);
|
|
@@ -1963,6 +2304,11 @@ ${initLines.join('\n')}
|
|
|
1963
2304
|
if (ormDb) {
|
|
1964
2305
|
doClassLines.push(` this.db = __initDO(ctx.storage.sql, __doSchema_${doEntry.binding});`);
|
|
1965
2306
|
}
|
|
2307
|
+
for (const h of initHandlers) {
|
|
2308
|
+
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2309
|
+
doClassLines.push(` __setDoContext(this);`);
|
|
2310
|
+
doClassLines.push(` Promise.resolve(${handlerVar}.onInit.call(this)).catch((err) => console.error('[kuratchi] DO onInit failed:', err?.message || err));`);
|
|
2311
|
+
}
|
|
1966
2312
|
doClassLines.push(` }`);
|
|
1967
2313
|
if (ormDb) {
|
|
1968
2314
|
doClassLines.push(` async __kuratchiLogActivity(payload) {`);
|
|
@@ -2001,16 +2347,45 @@ ${initLines.join('\n')}
|
|
|
2001
2347
|
doClassLines.push(` return rows;`);
|
|
2002
2348
|
doClassLines.push(` }`);
|
|
2003
2349
|
}
|
|
2350
|
+
// Function-mode lifecycle dispatchers
|
|
2351
|
+
if (alarmHandlers.length > 0) {
|
|
2352
|
+
doClassLines.push(` async alarm(...args) {`);
|
|
2353
|
+
doClassLines.push(` __setDoContext(this);`);
|
|
2354
|
+
for (const h of alarmHandlers) {
|
|
2355
|
+
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2356
|
+
doClassLines.push(` await ${handlerVar}.onAlarm.call(this, ...args);`);
|
|
2357
|
+
}
|
|
2358
|
+
doClassLines.push(` }`);
|
|
2359
|
+
}
|
|
2360
|
+
if (messageHandlers.length > 0) {
|
|
2361
|
+
doClassLines.push(` webSocketMessage(...args) {`);
|
|
2362
|
+
doClassLines.push(` __setDoContext(this);`);
|
|
2363
|
+
for (const h of messageHandlers) {
|
|
2364
|
+
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2365
|
+
doClassLines.push(` ${handlerVar}.onMessage.call(this, ...args);`);
|
|
2366
|
+
}
|
|
2367
|
+
doClassLines.push(` }`);
|
|
2368
|
+
}
|
|
2004
2369
|
doClassLines.push(`}`);
|
|
2005
|
-
// Apply handler methods to prototype
|
|
2370
|
+
// Apply handler methods to prototype (outside class body)
|
|
2006
2371
|
for (const h of handlers) {
|
|
2007
2372
|
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2008
|
-
|
|
2009
|
-
|
|
2373
|
+
if (h.mode === 'class') {
|
|
2374
|
+
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)); }; }`);
|
|
2375
|
+
doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
|
|
2376
|
+
}
|
|
2377
|
+
else {
|
|
2378
|
+
const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
|
|
2379
|
+
for (const fn of h.exportedFunctions) {
|
|
2380
|
+
if (lifecycle.has(fn))
|
|
2381
|
+
continue;
|
|
2382
|
+
doClassLines.push(`${doEntry.className}.prototype[${JSON.stringify(fn)}] = function(...__a){ __setDoContext(this); return ${handlerVar}.${fn}.apply(this, __a.map(__decodeDoArg)); };`);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2010
2385
|
}
|
|
2011
2386
|
// Register stub resolver
|
|
2012
2387
|
if (doEntry.stubId) {
|
|
2013
|
-
// Config-driven: e.g. stubId: 'user.orgId'
|
|
2388
|
+
// Config-driven: e.g. stubId: 'user.orgId' â†' __u.orgId
|
|
2014
2389
|
const fieldPath = doEntry.stubId.startsWith('user.') ? `__u.${doEntry.stubId.slice(5)}` : doEntry.stubId;
|
|
2015
2390
|
const checkField = doEntry.stubId.startsWith('user.') ? doEntry.stubId.slice(5) : doEntry.stubId;
|
|
2016
2391
|
doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
|
|
@@ -2020,37 +2395,37 @@ ${initLines.join('\n')}
|
|
|
2020
2395
|
doResolverLines.push(` });`);
|
|
2021
2396
|
}
|
|
2022
2397
|
else {
|
|
2023
|
-
// No stubId config
|
|
2024
|
-
doResolverLines.push(` // No 'stubId' config for ${doEntry.binding}
|
|
2398
|
+
// No stubId config â€" stub must be obtained manually
|
|
2399
|
+
doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} â€" stub must be obtained manually`);
|
|
2025
2400
|
}
|
|
2026
2401
|
}
|
|
2027
2402
|
doImports = doImportLines.join('\n');
|
|
2028
|
-
doClassCode = `\n//
|
|
2403
|
+
doClassCode = `\n// â"€â"€ Durable Object Classes (generated) â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n` + doClassLines.join('\n') + '\n';
|
|
2029
2404
|
doResolverInit = `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`;
|
|
2030
2405
|
}
|
|
2031
|
-
return `// Generated by KuratchiJS compiler
|
|
2406
|
+
return `// Generated by KuratchiJS compiler â€" do not edit.
|
|
2032
2407
|
${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
|
|
2033
2408
|
${workerImport}
|
|
2034
2409
|
${contextImport}
|
|
2035
|
-
${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
|
|
2410
|
+
${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
|
|
2036
2411
|
|
|
2037
|
-
//
|
|
2412
|
+
// â"€â"€ Assets â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2038
2413
|
|
|
2039
2414
|
const __assets = {
|
|
2040
2415
|
${opts.compiledAssets.map(a => ` ${JSON.stringify(a.name)}: { content: ${JSON.stringify(a.content)}, mime: ${JSON.stringify(a.mime)}, etag: ${JSON.stringify(a.etag)} }`).join(',\n')}
|
|
2041
2416
|
};
|
|
2042
2417
|
|
|
2043
|
-
//
|
|
2418
|
+
// â"€â"€ Router â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2044
2419
|
|
|
2045
|
-
const __staticRoutes = new Map(); // exact path
|
|
2420
|
+
const __staticRoutes = new Map(); // exact path â†' index (O(1) lookup)
|
|
2046
2421
|
const __dynamicRoutes = []; // regex-based routes (params/wildcards)
|
|
2047
2422
|
|
|
2048
2423
|
function __addRoute(pattern, index) {
|
|
2049
2424
|
if (!pattern.includes(':') && !pattern.includes('*')) {
|
|
2050
|
-
// Static route
|
|
2425
|
+
// Static route â€" direct Map lookup, no regex needed
|
|
2051
2426
|
__staticRoutes.set(pattern, index);
|
|
2052
2427
|
} else {
|
|
2053
|
-
// Dynamic route
|
|
2428
|
+
// Dynamic route â€" build regex for param extraction
|
|
2054
2429
|
const paramNames = [];
|
|
2055
2430
|
let regexStr = pattern
|
|
2056
2431
|
.replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
|
|
@@ -2076,13 +2451,13 @@ function __match(pathname) {
|
|
|
2076
2451
|
return null;
|
|
2077
2452
|
}
|
|
2078
2453
|
|
|
2079
|
-
//
|
|
2454
|
+
// â"€â"€ Layout â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2080
2455
|
|
|
2081
2456
|
${layoutBlock}
|
|
2082
2457
|
|
|
2083
2458
|
${layoutActionsBlock}
|
|
2084
2459
|
|
|
2085
|
-
//
|
|
2460
|
+
// â"€â"€ Error pages â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2086
2461
|
|
|
2087
2462
|
const __errorMessages = {
|
|
2088
2463
|
400: 'Bad Request',
|
|
@@ -2097,7 +2472,7 @@ const __errorMessages = {
|
|
|
2097
2472
|
503: 'Service Unavailable',
|
|
2098
2473
|
};
|
|
2099
2474
|
|
|
2100
|
-
// Built-in default error page
|
|
2475
|
+
// Built-in default error page â€" clean, dark, minimal, centered
|
|
2101
2476
|
function __errorPage(status, detail) {
|
|
2102
2477
|
const title = __errorMessages[status] || 'Error';
|
|
2103
2478
|
const detailHtml = detail ? '<p style="font-family:ui-monospace,monospace;font-size:0.8rem;color:#555;background:#111;padding:0.5rem 1rem;border-radius:6px;max-width:480px;margin:1rem auto 0;word-break:break-word">' + __esc(detail) + '</p>' : '';
|
|
@@ -2119,8 +2494,8 @@ function __error(status, detail) {
|
|
|
2119
2494
|
return __errorPage(status, detail);
|
|
2120
2495
|
}
|
|
2121
2496
|
|
|
2122
|
-
${opts.compiledComponents.length > 0 ? '//
|
|
2123
|
-
//
|
|
2497
|
+
${opts.compiledComponents.length > 0 ? '// â"€â"€ Components â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
|
|
2498
|
+
// â"€â"€ Route definitions â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2124
2499
|
|
|
2125
2500
|
const routes = [
|
|
2126
2501
|
${opts.compiledRoutes.join(',\n')}
|
|
@@ -2128,7 +2503,7 @@ ${opts.compiledRoutes.join(',\n')}
|
|
|
2128
2503
|
|
|
2129
2504
|
for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
|
|
2130
2505
|
|
|
2131
|
-
//
|
|
2506
|
+
// â"€â"€ Response helpers â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2132
2507
|
|
|
2133
2508
|
const __defaultSecHeaders = {
|
|
2134
2509
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -2232,13 +2607,13 @@ async function __runRuntimeError(ctx, error) {
|
|
|
2232
2607
|
return null;
|
|
2233
2608
|
}
|
|
2234
2609
|
|
|
2235
|
-
//
|
|
2610
|
+
// â"€â"€ Exported Worker entrypoint â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€
|
|
2236
2611
|
|
|
2237
|
-
export default class extends WorkerEntrypoint {
|
|
2238
|
-
async fetch(request) {
|
|
2239
|
-
__setRequestContext(this.ctx, request);
|
|
2240
|
-
globalThis.__cloudflare_env__ = __env;
|
|
2241
|
-
${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
|
|
2612
|
+
export default class extends WorkerEntrypoint {
|
|
2613
|
+
async fetch(request) {
|
|
2614
|
+
__setRequestContext(this.ctx, request);
|
|
2615
|
+
globalThis.__cloudflare_env__ = __env;
|
|
2616
|
+
${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
|
|
2242
2617
|
const __runtimeCtx = {
|
|
2243
2618
|
request,
|
|
2244
2619
|
env: __env,
|
|
@@ -2277,6 +2652,24 @@ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n
|
|
|
2277
2652
|
__runtimeCtx.params = match.params;
|
|
2278
2653
|
const route = routes[match.index];
|
|
2279
2654
|
__setLocal('params', match.params);
|
|
2655
|
+
|
|
2656
|
+
// API route: dispatch to method handler
|
|
2657
|
+
if (route.__api) {
|
|
2658
|
+
const method = request.method;
|
|
2659
|
+
if (method === 'OPTIONS') {
|
|
2660
|
+
const handler = route['OPTIONS'];
|
|
2661
|
+
if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
|
|
2662
|
+
const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
|
|
2663
|
+
return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
|
|
2664
|
+
}
|
|
2665
|
+
const handler = route[method];
|
|
2666
|
+
if (typeof handler !== 'function') {
|
|
2667
|
+
const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
|
|
2668
|
+
return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
|
|
2669
|
+
}
|
|
2670
|
+
return __secHeaders(await handler(__runtimeCtx));
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2280
2673
|
const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
|
|
2281
2674
|
const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
|
|
2282
2675
|
let __qArgs = [];
|
|
@@ -2350,7 +2743,10 @@ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n
|
|
|
2350
2743
|
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
2351
2744
|
data.params = match.params;
|
|
2352
2745
|
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
2353
|
-
|
|
2746
|
+
const __allActions = Object.assign({}, route.actions, __layoutActions || {});
|
|
2747
|
+
Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
|
|
2748
|
+
const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
|
|
2749
|
+
data[actionName] = { error: __errMsg, loading: false, success: false };
|
|
2354
2750
|
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2355
2751
|
}
|
|
2356
2752
|
// Fetch-based actions return lightweight JSON (no page re-render)
|
|
@@ -2373,11 +2769,14 @@ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n
|
|
|
2373
2769
|
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
2374
2770
|
data.params = match.params;
|
|
2375
2771
|
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
2772
|
+
const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
|
|
2773
|
+
Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
|
|
2376
2774
|
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
|
|
2377
2775
|
} catch (err) {
|
|
2378
2776
|
console.error('[kuratchi] Route load/render error:', err);
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2777
|
+
const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
|
|
2778
|
+
const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
|
|
2779
|
+
return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
|
|
2381
2780
|
}
|
|
2382
2781
|
};
|
|
2383
2782
|
|