@kuratchi/js 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -47
- package/dist/cli.js +59 -17
- package/dist/compiler/index.d.ts +2 -2
- package/dist/compiler/index.js +614 -254
- package/dist/compiler/parser.d.ts +8 -3
- package/dist/compiler/parser.js +29 -7
- package/dist/compiler/template.d.ts +3 -1
- package/dist/compiler/template.js +213 -14
- package/dist/create.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/runtime/action.d.ts +11 -0
- package/dist/runtime/action.js +14 -0
- package/dist/runtime/app.js +35 -8
- package/dist/runtime/context.d.ts +8 -2
- package/dist/runtime/context.js +31 -1
- package/dist/runtime/do.d.ts +6 -0
- package/dist/runtime/do.js +15 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/page-error.d.ts +16 -0
- package/dist/runtime/page-error.js +20 -0
- package/dist/runtime/types.d.ts +53 -33
- package/package.json +47 -45
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML file parser.
|
|
3
3
|
*
|
|
4
|
-
* Extracts the top-level <script> block (before the HTML document).
|
|
4
|
+
* Extracts the top-level compile-time <script> block (before the HTML document).
|
|
5
|
+
* A top-level block containing reactive `$:` labels is preserved in template output
|
|
6
|
+
* as client script.
|
|
5
7
|
* Everything else is the template — full HTML with native JS flow control.
|
|
6
8
|
* <style> inside the HTML is NOT extracted; it's part of the template.
|
|
7
9
|
*/
|
|
8
10
|
export interface ParsedFile {
|
|
9
|
-
/** Script content (
|
|
11
|
+
/** Script content (compile-time code, minus imports) */
|
|
10
12
|
script: string | null;
|
|
11
13
|
/** Template — the full HTML document with inline JS flow control */
|
|
12
14
|
template: string;
|
|
@@ -30,11 +32,14 @@ export interface ParsedFile {
|
|
|
30
32
|
key?: string;
|
|
31
33
|
rpcId?: string;
|
|
32
34
|
}>;
|
|
35
|
+
/** Imports found in a top-level client script block */
|
|
36
|
+
clientImports: string[];
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
35
39
|
* Parse a .html route file.
|
|
36
40
|
*
|
|
37
|
-
* The <script> block
|
|
41
|
+
* The top-level compile-time <script> block is extracted for the compiler.
|
|
42
|
+
* If it includes reactive `$:` labels, it is preserved in template output.
|
|
38
43
|
* Everything else (the HTML document) becomes the template.
|
|
39
44
|
*/
|
|
40
45
|
export declare function parseFile(source: string): ParsedFile;
|
package/dist/compiler/parser.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML file parser.
|
|
3
3
|
*
|
|
4
|
-
* Extracts the top-level <script> block (before the HTML document).
|
|
4
|
+
* Extracts the top-level compile-time <script> block (before the HTML document).
|
|
5
|
+
* A top-level block containing reactive `$:` labels is preserved in template output
|
|
6
|
+
* as client script.
|
|
5
7
|
* Everything else is the template — full HTML with native JS flow control.
|
|
6
8
|
* <style> inside the HTML is NOT extracted; it's part of the template.
|
|
7
9
|
*/
|
|
10
|
+
function hasReactiveLabel(scriptBody) {
|
|
11
|
+
return /\$\s*:/.test(scriptBody);
|
|
12
|
+
}
|
|
8
13
|
function splitTopLevel(input, delimiter) {
|
|
9
14
|
const parts = [];
|
|
10
15
|
let start = 0;
|
|
@@ -401,21 +406,31 @@ function extractTopLevelFunctionNames(scriptBody) {
|
|
|
401
406
|
/**
|
|
402
407
|
* Parse a .html route file.
|
|
403
408
|
*
|
|
404
|
-
* The <script> block
|
|
409
|
+
* The top-level compile-time <script> block is extracted for the compiler.
|
|
410
|
+
* If it includes reactive `$:` labels, it is preserved in template output.
|
|
405
411
|
* Everything else (the HTML document) becomes the template.
|
|
406
412
|
*/
|
|
407
413
|
export function parseFile(source) {
|
|
408
414
|
let script = null;
|
|
415
|
+
let clientScript = null;
|
|
409
416
|
let template = source;
|
|
410
|
-
// Extract the first <script>...</script> block
|
|
411
|
-
//
|
|
417
|
+
// Extract the first top-level compile-time <script>...</script> block.
|
|
418
|
+
// Client blocks (reactive `$:` labels) stay in the template
|
|
419
|
+
// and are emitted for browser execution.
|
|
412
420
|
const scriptMatch = template.match(/^(\s*)<script(\s[^>]*)?\s*>([\s\S]*?)<\/script>/);
|
|
413
421
|
if (scriptMatch) {
|
|
414
|
-
|
|
415
|
-
|
|
422
|
+
const body = scriptMatch[3].trim();
|
|
423
|
+
if (!hasReactiveLabel(body)) {
|
|
424
|
+
script = body;
|
|
425
|
+
template = template.slice(scriptMatch[0].length).trim();
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
clientScript = body;
|
|
429
|
+
}
|
|
416
430
|
}
|
|
417
431
|
// Extract all imports from script
|
|
418
432
|
const serverImports = [];
|
|
433
|
+
const clientImports = [];
|
|
419
434
|
const componentImports = {};
|
|
420
435
|
if (script) {
|
|
421
436
|
// Support both single-line and multiline static imports.
|
|
@@ -442,6 +457,13 @@ export function parseFile(source) {
|
|
|
442
457
|
}
|
|
443
458
|
}
|
|
444
459
|
}
|
|
460
|
+
if (clientScript) {
|
|
461
|
+
const importRegex = /^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm;
|
|
462
|
+
let m;
|
|
463
|
+
while ((m = importRegex.exec(clientScript)) !== null) {
|
|
464
|
+
clientImports.push(m[0].trim());
|
|
465
|
+
}
|
|
466
|
+
}
|
|
445
467
|
// Extract top-level variable declarations from script body (after removing imports)
|
|
446
468
|
const dataVars = [];
|
|
447
469
|
let scriptBody = '';
|
|
@@ -530,5 +552,5 @@ export function parseFile(source) {
|
|
|
530
552
|
if (!exists)
|
|
531
553
|
dataGetQueries.push({ fnName, argsExpr, asName, key });
|
|
532
554
|
}
|
|
533
|
-
return { script, template, serverImports, hasLoad, actionFunctions, dataVars, componentImports, pollFunctions, dataGetQueries };
|
|
555
|
+
return { script, template, serverImports, hasLoad, actionFunctions, dataVars, componentImports, pollFunctions, dataGetQueries, clientImports };
|
|
534
556
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Syntax:
|
|
5
5
|
* {expression} → escaped output
|
|
6
|
-
* {
|
|
6
|
+
* {@html expression} → sanitized HTML output
|
|
7
|
+
* {@raw expression} → raw HTML output (unescaped)
|
|
8
|
+
* {=html expression} → legacy alias for {@raw expression}
|
|
7
9
|
* for (const x of arr) { → JS for loop (inline in HTML)
|
|
8
10
|
* <li>{x.name}</li>
|
|
9
11
|
* }
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Syntax:
|
|
5
5
|
* {expression} → escaped output
|
|
6
|
-
* {
|
|
6
|
+
* {@html expression} → sanitized HTML output
|
|
7
|
+
* {@raw expression} → raw HTML output (unescaped)
|
|
8
|
+
* {=html expression} → legacy alias for {@raw expression}
|
|
7
9
|
* for (const x of arr) { → JS for loop (inline in HTML)
|
|
8
10
|
* <li>{x.name}</li>
|
|
9
11
|
* }
|
|
@@ -46,6 +48,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
46
48
|
const lines = template.split('\n');
|
|
47
49
|
let inStyle = false;
|
|
48
50
|
let inScript = false;
|
|
51
|
+
let scriptBuffer = [];
|
|
49
52
|
for (let i = 0; i < lines.length; i++) {
|
|
50
53
|
const line = lines[i];
|
|
51
54
|
const trimmed = line.trim();
|
|
@@ -58,13 +61,30 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
58
61
|
inStyle = false;
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
61
|
-
// Track <script> blocks —
|
|
62
|
-
if (trimmed.match(/<script[\s>]/i))
|
|
64
|
+
// Track <script> blocks — transform reactive ($:) client syntax first.
|
|
65
|
+
if (!inScript && trimmed.match(/<script[\s>]/i)) {
|
|
63
66
|
inScript = true;
|
|
67
|
+
scriptBuffer = [line];
|
|
68
|
+
if (trimmed.match(/<\/script>/i)) {
|
|
69
|
+
const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
|
|
70
|
+
for (const scriptLine of transformed.split('\n')) {
|
|
71
|
+
out.push(`__html += \`${escapeLiteral(scriptLine)}\\n\`;`);
|
|
72
|
+
}
|
|
73
|
+
scriptBuffer = [];
|
|
74
|
+
inScript = false;
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
64
78
|
if (inScript) {
|
|
65
|
-
|
|
66
|
-
if (trimmed.match(/<\/script>/i))
|
|
79
|
+
scriptBuffer.push(line);
|
|
80
|
+
if (trimmed.match(/<\/script>/i)) {
|
|
81
|
+
const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
|
|
82
|
+
for (const scriptLine of transformed.split('\n')) {
|
|
83
|
+
out.push(`__html += \`${escapeLiteral(scriptLine)}\\n\`;`);
|
|
84
|
+
}
|
|
85
|
+
scriptBuffer = [];
|
|
67
86
|
inScript = false;
|
|
87
|
+
}
|
|
68
88
|
continue;
|
|
69
89
|
}
|
|
70
90
|
// Skip empty lines
|
|
@@ -147,6 +167,156 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
147
167
|
}
|
|
148
168
|
return out.join('\n');
|
|
149
169
|
}
|
|
170
|
+
function transformClientScriptBlock(block) {
|
|
171
|
+
const match = block.match(/^([\s\S]*?<script\b[^>]*>)([\s\S]*?)(<\/script>\s*)$/i);
|
|
172
|
+
if (!match)
|
|
173
|
+
return block;
|
|
174
|
+
const openTag = match[1];
|
|
175
|
+
const body = match[2];
|
|
176
|
+
const closeTag = match[3];
|
|
177
|
+
if (!/\$\s*:/.test(body))
|
|
178
|
+
return block;
|
|
179
|
+
const out = [];
|
|
180
|
+
const lines = body.split('\n');
|
|
181
|
+
const reactiveVars = new Set();
|
|
182
|
+
const rewritten = lines.map((line) => {
|
|
183
|
+
const m = line.match(/^(\s*)let\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);\s*$/);
|
|
184
|
+
if (!m)
|
|
185
|
+
return line;
|
|
186
|
+
const indent = m[1] ?? '';
|
|
187
|
+
const name = m[2];
|
|
188
|
+
const expr = (m[3] ?? '').trim();
|
|
189
|
+
if (!expr || (!expr.startsWith('[') && !expr.startsWith('{')))
|
|
190
|
+
return line;
|
|
191
|
+
reactiveVars.add(name);
|
|
192
|
+
return `${indent}let ${name} = __k$.state(${expr});`;
|
|
193
|
+
});
|
|
194
|
+
const assignRegexes = Array.from(reactiveVars).map((name) => ({
|
|
195
|
+
name,
|
|
196
|
+
re: new RegExp(`^(\\s*)${name}\\s*=\\s*([^;]+);\\s*$`),
|
|
197
|
+
}));
|
|
198
|
+
let inReactiveBlock = false;
|
|
199
|
+
let blockIndent = '';
|
|
200
|
+
let blockDepth = 0;
|
|
201
|
+
for (const line of rewritten) {
|
|
202
|
+
let current = line;
|
|
203
|
+
for (const { name, re } of assignRegexes) {
|
|
204
|
+
const am = current.match(re);
|
|
205
|
+
if (!am)
|
|
206
|
+
continue;
|
|
207
|
+
const indent = am[1] ?? '';
|
|
208
|
+
const expr = (am[2] ?? '').trim();
|
|
209
|
+
current = `${indent}${name} = __k$.replace(${name}, ${expr});`;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (!inReactiveBlock) {
|
|
213
|
+
const rm = current.match(/^(\s*)\$:\s*(.*)$/);
|
|
214
|
+
if (!rm) {
|
|
215
|
+
out.push(current);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const indent = rm[1] ?? '';
|
|
219
|
+
const expr = (rm[2] ?? '').trim();
|
|
220
|
+
if (!expr)
|
|
221
|
+
continue;
|
|
222
|
+
if (expr.startsWith('{')) {
|
|
223
|
+
const tail = expr.slice(1);
|
|
224
|
+
out.push(`${indent}__k$.effect(() => {`);
|
|
225
|
+
inReactiveBlock = true;
|
|
226
|
+
blockIndent = indent;
|
|
227
|
+
blockDepth = 1 + braceDelta(tail);
|
|
228
|
+
if (tail.trim())
|
|
229
|
+
out.push(`${indent}${tail}`);
|
|
230
|
+
if (blockDepth <= 0) {
|
|
231
|
+
out.push(`${indent}});`);
|
|
232
|
+
inReactiveBlock = false;
|
|
233
|
+
blockIndent = '';
|
|
234
|
+
blockDepth = 0;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const normalized = expr.endsWith(';') ? expr : `${expr};`;
|
|
239
|
+
out.push(`${indent}__k$.effect(() => { ${normalized} });`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const nextDepth = blockDepth + braceDelta(current);
|
|
243
|
+
if (nextDepth <= 0 && current.trim() === '}') {
|
|
244
|
+
out.push(`${blockIndent}});`);
|
|
245
|
+
inReactiveBlock = false;
|
|
246
|
+
blockIndent = '';
|
|
247
|
+
blockDepth = 0;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
out.push(current);
|
|
251
|
+
blockDepth = nextDepth;
|
|
252
|
+
}
|
|
253
|
+
if (inReactiveBlock)
|
|
254
|
+
out.push(`${blockIndent}});`);
|
|
255
|
+
let insertAt = 0;
|
|
256
|
+
while (insertAt < out.length) {
|
|
257
|
+
const t = out[insertAt].trim();
|
|
258
|
+
if (!t || t.startsWith('//')) {
|
|
259
|
+
insertAt++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (/^\/\*/.test(t)) {
|
|
263
|
+
insertAt++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (/^import\s/.test(t)) {
|
|
267
|
+
insertAt++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
out.splice(insertAt, 0, 'const __k$ = window.__kuratchiReactive;');
|
|
273
|
+
return `${openTag}${out.join('\n')}${closeTag}`;
|
|
274
|
+
}
|
|
275
|
+
function braceDelta(line) {
|
|
276
|
+
let delta = 0;
|
|
277
|
+
let inSingle = false;
|
|
278
|
+
let inDouble = false;
|
|
279
|
+
let inTemplate = false;
|
|
280
|
+
let escaped = false;
|
|
281
|
+
for (let i = 0; i < line.length; i++) {
|
|
282
|
+
const ch = line[i];
|
|
283
|
+
if (escaped) {
|
|
284
|
+
escaped = false;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (ch === '\\') {
|
|
288
|
+
escaped = true;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!inDouble && !inTemplate && ch === "'" && !inSingle) {
|
|
292
|
+
inSingle = true;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (inSingle && ch === "'") {
|
|
296
|
+
inSingle = false;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!inSingle && !inTemplate && ch === '"' && !inDouble) {
|
|
300
|
+
inDouble = true;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (inDouble && ch === '"') {
|
|
304
|
+
inDouble = false;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (!inSingle && !inDouble && ch === '`') {
|
|
308
|
+
inTemplate = !inTemplate;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (inSingle || inDouble || inTemplate)
|
|
312
|
+
continue;
|
|
313
|
+
if (ch === '{')
|
|
314
|
+
delta++;
|
|
315
|
+
else if (ch === '}')
|
|
316
|
+
delta--;
|
|
317
|
+
}
|
|
318
|
+
return delta;
|
|
319
|
+
}
|
|
150
320
|
function findMatching(src, openPos, openChar, closeChar) {
|
|
151
321
|
let depth = 0;
|
|
152
322
|
let quote = null;
|
|
@@ -382,8 +552,9 @@ function expandShorthands(line) {
|
|
|
382
552
|
return line;
|
|
383
553
|
}
|
|
384
554
|
/**
|
|
385
|
-
* Compile a single HTML line, replacing {expr} with escaped output
|
|
386
|
-
*
|
|
555
|
+
* Compile a single HTML line, replacing {expr} with escaped output,
|
|
556
|
+
* {@html expr} with sanitized HTML, and {@raw expr} with raw output.
|
|
557
|
+
* Handles attribute values like value={x}.
|
|
387
558
|
*/
|
|
388
559
|
function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
389
560
|
// Expand shorthand syntax before main compilation
|
|
@@ -405,10 +576,22 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
405
576
|
// Find matching closing brace
|
|
406
577
|
const closeIdx = findClosingBrace(line, braceIdx);
|
|
407
578
|
const inner = line.slice(braceIdx + 1, closeIdx).trim();
|
|
408
|
-
//
|
|
409
|
-
if (inner.startsWith('
|
|
579
|
+
// Sanitized HTML: {@html expr}
|
|
580
|
+
if (inner.startsWith('@html ')) {
|
|
581
|
+
const expr = inner.slice(6).trim();
|
|
582
|
+
result += `\${__sanitizeHtml(${expr})}`;
|
|
583
|
+
hasExpr = true;
|
|
584
|
+
}
|
|
585
|
+
else if (inner.startsWith('@raw ')) {
|
|
586
|
+
// Unsafe raw HTML: {@raw expr}
|
|
587
|
+
const expr = inner.slice(5).trim();
|
|
588
|
+
result += `\${__rawHtml(${expr})}`;
|
|
589
|
+
hasExpr = true;
|
|
590
|
+
}
|
|
591
|
+
else if (inner.startsWith('=html ')) {
|
|
592
|
+
// Legacy alias for raw HTML: {=html expr}
|
|
410
593
|
const expr = inner.slice(6).trim();
|
|
411
|
-
result += `\${${expr}}`;
|
|
594
|
+
result += `\${__rawHtml(${expr})}`;
|
|
412
595
|
hasExpr = true;
|
|
413
596
|
}
|
|
414
597
|
else {
|
|
@@ -610,10 +793,26 @@ function findClosingBrace(src, openPos) {
|
|
|
610
793
|
*/
|
|
611
794
|
export function generateRenderFunction(template) {
|
|
612
795
|
const body = compileTemplate(template);
|
|
613
|
-
return `function render(data) {
|
|
614
|
-
const
|
|
615
|
-
if (v == null) return '';
|
|
616
|
-
return String(v)
|
|
796
|
+
return `function render(data) {
|
|
797
|
+
const __rawHtml = (v) => {
|
|
798
|
+
if (v == null) return '';
|
|
799
|
+
return String(v);
|
|
800
|
+
};
|
|
801
|
+
const __sanitizeHtml = (v) => {
|
|
802
|
+
let html = __rawHtml(v);
|
|
803
|
+
html = html.replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, '');
|
|
804
|
+
html = html.replace(/<iframe\\b[^>]*>[\\s\\S]*?<\\/iframe>/gi, '');
|
|
805
|
+
html = html.replace(/<object\\b[^>]*>[\\s\\S]*?<\\/object>/gi, '');
|
|
806
|
+
html = html.replace(/<embed\\b[^>]*>/gi, '');
|
|
807
|
+
html = html.replace(/\\son[a-z]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
|
|
808
|
+
html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*([\"'])\\s*javascript:[\\s\\S]*?\\2/gi, ' $1="#"');
|
|
809
|
+
html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*javascript:[^\\s>]+/gi, ' $1="#"');
|
|
810
|
+
html = html.replace(/\\ssrcdoc\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
|
|
811
|
+
return html;
|
|
812
|
+
};
|
|
813
|
+
const __esc = (v) => {
|
|
814
|
+
if (v == null) return '';
|
|
815
|
+
return String(v)
|
|
617
816
|
.replace(/&/g, '&')
|
|
618
817
|
.replace(/</g, '<')
|
|
619
818
|
.replace(/>/g, '>')
|
package/dist/create.js
CHANGED
|
@@ -915,7 +915,7 @@ function genLoginPage() {
|
|
|
915
915
|
footerText="Don't have an account?"
|
|
916
916
|
footerLink="Sign up"
|
|
917
917
|
footerHref="/auth/signup"
|
|
918
|
-
error={
|
|
918
|
+
error={signIn.error}
|
|
919
919
|
>
|
|
920
920
|
<form action={signIn} method="POST" class="kui-auth-form">
|
|
921
921
|
<div class="kui-field">
|
|
@@ -943,7 +943,7 @@ function genSignupPage() {
|
|
|
943
943
|
footerText="Already have an account?"
|
|
944
944
|
footerLink="Sign in"
|
|
945
945
|
footerHref="/auth/login"
|
|
946
|
-
error={
|
|
946
|
+
error={signUp.error}
|
|
947
947
|
>
|
|
948
948
|
<form action={signUp} method="POST" class="kui-auth-form">
|
|
949
949
|
<div class="kui-field">
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
export { createApp } from './runtime/app.js';
|
|
7
7
|
export { defineConfig } from './runtime/config.js';
|
|
8
8
|
export { defineRuntime } from './runtime/runtime.js';
|
|
9
|
-
export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
10
|
-
export { kuratchiDO, doRpc } from './runtime/do.js';
|
|
9
|
+
export { getCtx, getEnv, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
10
|
+
export { kuratchiDO, doRpc, getDb } from './runtime/do.js';
|
|
11
|
+
export { ActionError } from './runtime/action.js';
|
|
12
|
+
export { PageError } from './runtime/page-error.js';
|
|
11
13
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
12
14
|
export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './runtime/types.js';
|
|
13
15
|
export type { RpcOf } from './runtime/do.js';
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
export { createApp } from './runtime/app.js';
|
|
8
8
|
export { defineConfig } from './runtime/config.js';
|
|
9
9
|
export { defineRuntime } from './runtime/runtime.js';
|
|
10
|
-
export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
11
|
-
export { kuratchiDO, doRpc } from './runtime/do.js';
|
|
10
|
+
export { getCtx, getEnv, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
11
|
+
export { kuratchiDO, doRpc, getDb } from './runtime/do.js';
|
|
12
|
+
export { ActionError } from './runtime/action.js';
|
|
13
|
+
export { PageError } from './runtime/page-error.js';
|
|
12
14
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
|
|
13
15
|
// Compatibility aliases
|
|
14
16
|
matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionError — throw from a form action to surface a user-facing error.
|
|
3
|
+
*
|
|
4
|
+
* Throwing an ActionError makes the error message available in the template
|
|
5
|
+
* as `actionName.error` (e.g. `signIn.error`). Throwing a plain Error in
|
|
6
|
+
* production shows a generic "Action failed" message instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ActionError extends Error {
|
|
9
|
+
readonly isActionError = true;
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionError — throw from a form action to surface a user-facing error.
|
|
3
|
+
*
|
|
4
|
+
* Throwing an ActionError makes the error message available in the template
|
|
5
|
+
* as `actionName.error` (e.g. `signIn.error`). Throwing a plain Error in
|
|
6
|
+
* production shows a generic "Action failed" message instead.
|
|
7
|
+
*/
|
|
8
|
+
export class ActionError extends Error {
|
|
9
|
+
isActionError = true;
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'ActionError';
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/runtime/app.js
CHANGED
|
@@ -39,10 +39,37 @@ export function createApp(config) {
|
|
|
39
39
|
}
|
|
40
40
|
const route = routes[match.index];
|
|
41
41
|
context.params = match.params;
|
|
42
|
+
// --- API routes: dispatch to method handler ---
|
|
43
|
+
if ('__api' in route && route.__api) {
|
|
44
|
+
const method = request.method;
|
|
45
|
+
if (method === 'OPTIONS') {
|
|
46
|
+
const handler = route['OPTIONS'];
|
|
47
|
+
if (typeof handler === 'function')
|
|
48
|
+
return handler(context);
|
|
49
|
+
const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
50
|
+
.filter(m => typeof route[m] === 'function').join(', ');
|
|
51
|
+
return new Response(null, {
|
|
52
|
+
status: 204,
|
|
53
|
+
headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const handler = route[method];
|
|
57
|
+
if (typeof handler !== 'function') {
|
|
58
|
+
const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
59
|
+
.filter(m => typeof route[m] === 'function').join(', ');
|
|
60
|
+
return new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
|
|
61
|
+
status: 405,
|
|
62
|
+
headers: { 'content-type': 'application/json', 'Allow': allowed },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return handler(context);
|
|
66
|
+
}
|
|
67
|
+
// From here, route is a page route (RouteModule)
|
|
68
|
+
const pageRoute = route;
|
|
42
69
|
// --- RPC calls: POST ?/_rpc/functionName ---
|
|
43
70
|
if (request.method === 'POST' && url.searchParams.has('_rpc')) {
|
|
44
71
|
const fnName = url.searchParams.get('_rpc');
|
|
45
|
-
const rpcFn =
|
|
72
|
+
const rpcFn = pageRoute.rpc?.[fnName];
|
|
46
73
|
if (!rpcFn) {
|
|
47
74
|
return new Response(JSON.stringify({ error: `RPC function '${fnName}' not found` }), {
|
|
48
75
|
status: 404,
|
|
@@ -68,7 +95,7 @@ export function createApp(config) {
|
|
|
68
95
|
const actionParam = [...url.searchParams.keys()].find(k => k.startsWith('/'));
|
|
69
96
|
if (actionParam) {
|
|
70
97
|
const actionName = actionParam.slice(1); // remove leading /
|
|
71
|
-
const actionFn =
|
|
98
|
+
const actionFn = pageRoute.actions?.[actionName];
|
|
72
99
|
if (!actionFn) {
|
|
73
100
|
return new Response(`Action '${actionName}' not found`, { status: 404 });
|
|
74
101
|
}
|
|
@@ -76,22 +103,22 @@ export function createApp(config) {
|
|
|
76
103
|
const formData = await request.formData();
|
|
77
104
|
const actionResult = await actionFn(formData, env, context);
|
|
78
105
|
// After action, re-run load and re-render with action result
|
|
79
|
-
const loadData =
|
|
106
|
+
const loadData = pageRoute.load ? await pageRoute.load(context) : {};
|
|
80
107
|
const data = { ...loadData, actionResult, actionName };
|
|
81
|
-
return renderPage(
|
|
108
|
+
return renderPage(pageRoute, data, layouts);
|
|
82
109
|
}
|
|
83
110
|
catch (err) {
|
|
84
111
|
// Re-render with error
|
|
85
|
-
const loadData =
|
|
112
|
+
const loadData = pageRoute.load ? await pageRoute.load(context) : {};
|
|
86
113
|
const data = { ...loadData, actionError: err.message, actionName };
|
|
87
|
-
return renderPage(
|
|
114
|
+
return renderPage(pageRoute, data, layouts);
|
|
88
115
|
}
|
|
89
116
|
}
|
|
90
117
|
}
|
|
91
118
|
// --- GET: load + render ---
|
|
92
119
|
try {
|
|
93
|
-
const data =
|
|
94
|
-
return renderPage(
|
|
120
|
+
const data = pageRoute.load ? await pageRoute.load(context) : {};
|
|
121
|
+
return renderPage(pageRoute, data, layouts);
|
|
95
122
|
}
|
|
96
123
|
catch (err) {
|
|
97
124
|
return new Response(`Server Error: ${err.message}`, {
|
|
@@ -14,8 +14,10 @@ export interface BreadcrumbItem {
|
|
|
14
14
|
}
|
|
15
15
|
/** Called by the framework at the start of each request */
|
|
16
16
|
export declare function __setRequestContext(ctx: any, request: Request): void;
|
|
17
|
-
/** Get the execution context (
|
|
18
|
-
export declare function getCtx():
|
|
17
|
+
/** Get the execution context (Worker: ExecutionContext, DO: DurableObjectState) */
|
|
18
|
+
export declare function getCtx(): any;
|
|
19
|
+
/** Get the current environment bindings */
|
|
20
|
+
export declare function getEnv<T = Record<string, any>>(): T;
|
|
19
21
|
/** Get the current request */
|
|
20
22
|
export declare function getRequest(): Request;
|
|
21
23
|
/** Get request-scoped locals (session, auth, custom data) */
|
|
@@ -45,3 +47,7 @@ export declare function __setLocal(key: string, value: any): void;
|
|
|
45
47
|
export declare function __getLocals(): Record<string, any>;
|
|
46
48
|
/** HTML-escape a value for safe output in templates */
|
|
47
49
|
export declare function __esc(v: any): string;
|
|
50
|
+
/** Convert a value to a raw HTML string (unsafe, no escaping). */
|
|
51
|
+
export declare function __rawHtml(v: any): string;
|
|
52
|
+
/** Best-effort HTML sanitizer for {@html ...} template output. */
|
|
53
|
+
export declare function __sanitizeHtml(v: any): string;
|
package/dist/runtime/context.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Workers are single-threaded per request — module-scoped
|
|
8
8
|
* variables are safe and require no Node.js compat flags.
|
|
9
9
|
*/
|
|
10
|
+
import { __getDoSelf } from './do.js';
|
|
10
11
|
let __ctx = null;
|
|
11
12
|
let __request = null;
|
|
12
13
|
let __locals = {};
|
|
@@ -22,12 +23,22 @@ export function __setRequestContext(ctx, request) {
|
|
|
22
23
|
get locals() { return __locals; },
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
|
-
/** Get the execution context (
|
|
26
|
+
/** Get the execution context (Worker: ExecutionContext, DO: DurableObjectState) */
|
|
26
27
|
export function getCtx() {
|
|
28
|
+
const doSelf = __getDoSelf();
|
|
29
|
+
if (doSelf)
|
|
30
|
+
return doSelf.ctx;
|
|
27
31
|
if (!__ctx)
|
|
28
32
|
throw new Error('getCtx() called outside of a request context');
|
|
29
33
|
return __ctx;
|
|
30
34
|
}
|
|
35
|
+
/** Get the current environment bindings */
|
|
36
|
+
export function getEnv() {
|
|
37
|
+
const doSelf = __getDoSelf();
|
|
38
|
+
if (doSelf)
|
|
39
|
+
return doSelf.env;
|
|
40
|
+
return globalThis.__cloudflare_env__;
|
|
41
|
+
}
|
|
31
42
|
/** Get the current request */
|
|
32
43
|
export function getRequest() {
|
|
33
44
|
if (!__request)
|
|
@@ -121,3 +132,22 @@ export function __esc(v) {
|
|
|
121
132
|
.replace(/"/g, '"')
|
|
122
133
|
.replace(/'/g, ''');
|
|
123
134
|
}
|
|
135
|
+
/** Convert a value to a raw HTML string (unsafe, no escaping). */
|
|
136
|
+
export function __rawHtml(v) {
|
|
137
|
+
if (v == null)
|
|
138
|
+
return '';
|
|
139
|
+
return String(v);
|
|
140
|
+
}
|
|
141
|
+
/** Best-effort HTML sanitizer for {@html ...} template output. */
|
|
142
|
+
export function __sanitizeHtml(v) {
|
|
143
|
+
let html = __rawHtml(v);
|
|
144
|
+
html = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
145
|
+
html = html.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, '');
|
|
146
|
+
html = html.replace(/<object\b[^>]*>[\s\S]*?<\/object>/gi, '');
|
|
147
|
+
html = html.replace(/<embed\b[^>]*>/gi, '');
|
|
148
|
+
html = html.replace(/\son[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
149
|
+
html = html.replace(/\s(href|src|xlink:href)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi, ' $1="#"');
|
|
150
|
+
html = html.replace(/\s(href|src|xlink:href)\s*=\s*javascript:[^\s>]+/gi, ' $1="#"');
|
|
151
|
+
html = html.replace(/\ssrcdoc\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
152
|
+
return html;
|
|
153
|
+
}
|
package/dist/runtime/do.d.ts
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* The compiler uses __registerDoResolver / __getDoStub internally.
|
|
13
13
|
* User code never touches these — they're wired up from kuratchi.config.ts.
|
|
14
14
|
*/
|
|
15
|
+
/** @internal — called by compiler-generated method wrappers */
|
|
16
|
+
export declare function __setDoContext(self: any): void;
|
|
17
|
+
/** Get the DO's ORM database instance */
|
|
18
|
+
export declare function getDb<T = Record<string, any>>(): T;
|
|
19
|
+
/** @internal — read by context.ts getCtx()/getEnv() */
|
|
20
|
+
export declare function __getDoSelf(): any;
|
|
15
21
|
/** @internal — called by compiler-generated init code */
|
|
16
22
|
export declare function __registerDoResolver(binding: string, resolver: () => Promise<any>): void;
|
|
17
23
|
/** @internal — called by compiler-generated init code */
|
package/dist/runtime/do.js
CHANGED
|
@@ -13,6 +13,21 @@
|
|
|
13
13
|
* User code never touches these — they're wired up from kuratchi.config.ts.
|
|
14
14
|
*/
|
|
15
15
|
// ── Internal: stub resolver registry ────────────────────────
|
|
16
|
+
let __doSelf = null;
|
|
17
|
+
/** @internal — called by compiler-generated method wrappers */
|
|
18
|
+
export function __setDoContext(self) {
|
|
19
|
+
__doSelf = self;
|
|
20
|
+
}
|
|
21
|
+
/** Get the DO's ORM database instance */
|
|
22
|
+
export function getDb() {
|
|
23
|
+
if (!__doSelf)
|
|
24
|
+
throw new Error('getDb() called outside of a DO context');
|
|
25
|
+
return __doSelf.db;
|
|
26
|
+
}
|
|
27
|
+
/** @internal — read by context.ts getCtx()/getEnv() */
|
|
28
|
+
export function __getDoSelf() {
|
|
29
|
+
return __doSelf;
|
|
30
|
+
}
|
|
16
31
|
const _resolvers = new Map();
|
|
17
32
|
const _classBindings = new WeakMap();
|
|
18
33
|
/** @internal — called by compiler-generated init code */
|