@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
|
@@ -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,159 @@ 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
|
+
const transpiled = transpileTypeScript(body, 'client-script.ts');
|
|
179
|
+
return `${openTag}${transpiled}${closeTag}`;
|
|
180
|
+
}
|
|
181
|
+
const out = [];
|
|
182
|
+
const lines = body.split('\n');
|
|
183
|
+
const reactiveVars = new Set();
|
|
184
|
+
const rewritten = lines.map((line) => {
|
|
185
|
+
const m = line.match(/^(\s*)let\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);\s*$/);
|
|
186
|
+
if (!m)
|
|
187
|
+
return line;
|
|
188
|
+
const indent = m[1] ?? '';
|
|
189
|
+
const name = m[2];
|
|
190
|
+
const expr = (m[3] ?? '').trim();
|
|
191
|
+
if (!expr || (!expr.startsWith('[') && !expr.startsWith('{')))
|
|
192
|
+
return line;
|
|
193
|
+
reactiveVars.add(name);
|
|
194
|
+
return `${indent}let ${name} = __k$.state(${expr});`;
|
|
195
|
+
});
|
|
196
|
+
const assignRegexes = Array.from(reactiveVars).map((name) => ({
|
|
197
|
+
name,
|
|
198
|
+
re: new RegExp(`^(\\s*)${name}\\s*=\\s*([^;]+);\\s*$`),
|
|
199
|
+
}));
|
|
200
|
+
let inReactiveBlock = false;
|
|
201
|
+
let blockIndent = '';
|
|
202
|
+
let blockDepth = 0;
|
|
203
|
+
for (const line of rewritten) {
|
|
204
|
+
let current = line;
|
|
205
|
+
for (const { name, re } of assignRegexes) {
|
|
206
|
+
const am = current.match(re);
|
|
207
|
+
if (!am)
|
|
208
|
+
continue;
|
|
209
|
+
const indent = am[1] ?? '';
|
|
210
|
+
const expr = (am[2] ?? '').trim();
|
|
211
|
+
current = `${indent}${name} = __k$.replace(${name}, ${expr});`;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (!inReactiveBlock) {
|
|
215
|
+
const rm = current.match(/^(\s*)\$:\s*(.*)$/);
|
|
216
|
+
if (!rm) {
|
|
217
|
+
out.push(current);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const indent = rm[1] ?? '';
|
|
221
|
+
const expr = (rm[2] ?? '').trim();
|
|
222
|
+
if (!expr)
|
|
223
|
+
continue;
|
|
224
|
+
if (expr.startsWith('{')) {
|
|
225
|
+
const tail = expr.slice(1);
|
|
226
|
+
out.push(`${indent}__k$.effect(() => {`);
|
|
227
|
+
inReactiveBlock = true;
|
|
228
|
+
blockIndent = indent;
|
|
229
|
+
blockDepth = 1 + braceDelta(tail);
|
|
230
|
+
if (tail.trim())
|
|
231
|
+
out.push(`${indent}${tail}`);
|
|
232
|
+
if (blockDepth <= 0) {
|
|
233
|
+
out.push(`${indent}});`);
|
|
234
|
+
inReactiveBlock = false;
|
|
235
|
+
blockIndent = '';
|
|
236
|
+
blockDepth = 0;
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const normalized = expr.endsWith(';') ? expr : `${expr};`;
|
|
241
|
+
out.push(`${indent}__k$.effect(() => { ${normalized} });`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const nextDepth = blockDepth + braceDelta(current);
|
|
245
|
+
if (nextDepth <= 0 && current.trim() === '}') {
|
|
246
|
+
out.push(`${blockIndent}});`);
|
|
247
|
+
inReactiveBlock = false;
|
|
248
|
+
blockIndent = '';
|
|
249
|
+
blockDepth = 0;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
out.push(current);
|
|
253
|
+
blockDepth = nextDepth;
|
|
254
|
+
}
|
|
255
|
+
if (inReactiveBlock)
|
|
256
|
+
out.push(`${blockIndent}});`);
|
|
257
|
+
let insertAt = 0;
|
|
258
|
+
while (insertAt < out.length) {
|
|
259
|
+
const t = out[insertAt].trim();
|
|
260
|
+
if (!t || t.startsWith('//')) {
|
|
261
|
+
insertAt++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (/^\/\*/.test(t)) {
|
|
265
|
+
insertAt++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (/^import\s/.test(t)) {
|
|
269
|
+
insertAt++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
out.splice(insertAt, 0, 'const __k$ = window.__kuratchiReactive;');
|
|
275
|
+
const transpiled = transpileTypeScript(out.join('\n'), 'client-script.ts');
|
|
276
|
+
return `${openTag}${transpiled}${closeTag}`;
|
|
277
|
+
}
|
|
278
|
+
function braceDelta(line) {
|
|
279
|
+
let delta = 0;
|
|
280
|
+
let inSingle = false;
|
|
281
|
+
let inDouble = false;
|
|
282
|
+
let inTemplate = false;
|
|
283
|
+
let escaped = false;
|
|
284
|
+
for (let i = 0; i < line.length; i++) {
|
|
285
|
+
const ch = line[i];
|
|
286
|
+
if (escaped) {
|
|
287
|
+
escaped = false;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (ch === '\\') {
|
|
291
|
+
escaped = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (!inDouble && !inTemplate && ch === "'" && !inSingle) {
|
|
295
|
+
inSingle = true;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (inSingle && ch === "'") {
|
|
299
|
+
inSingle = false;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!inSingle && !inTemplate && ch === '"' && !inDouble) {
|
|
303
|
+
inDouble = true;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (inDouble && ch === '"') {
|
|
307
|
+
inDouble = false;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!inSingle && !inDouble && ch === '`') {
|
|
311
|
+
inTemplate = !inTemplate;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (inSingle || inDouble || inTemplate)
|
|
315
|
+
continue;
|
|
316
|
+
if (ch === '{')
|
|
317
|
+
delta++;
|
|
318
|
+
else if (ch === '}')
|
|
319
|
+
delta--;
|
|
320
|
+
}
|
|
321
|
+
return delta;
|
|
322
|
+
}
|
|
150
323
|
function findMatching(src, openPos, openChar, closeChar) {
|
|
151
324
|
let depth = 0;
|
|
152
325
|
let quote = null;
|
|
@@ -382,8 +555,9 @@ function expandShorthands(line) {
|
|
|
382
555
|
return line;
|
|
383
556
|
}
|
|
384
557
|
/**
|
|
385
|
-
* Compile a single HTML line, replacing {expr} with escaped output
|
|
386
|
-
*
|
|
558
|
+
* Compile a single HTML line, replacing {expr} with escaped output,
|
|
559
|
+
* {@html expr} with sanitized HTML, and {@raw expr} with raw output.
|
|
560
|
+
* Handles attribute values like value={x}.
|
|
387
561
|
*/
|
|
388
562
|
function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
389
563
|
// Expand shorthand syntax before main compilation
|
|
@@ -405,10 +579,22 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
405
579
|
// Find matching closing brace
|
|
406
580
|
const closeIdx = findClosingBrace(line, braceIdx);
|
|
407
581
|
const inner = line.slice(braceIdx + 1, closeIdx).trim();
|
|
408
|
-
//
|
|
409
|
-
if (inner.startsWith('
|
|
582
|
+
// Sanitized HTML: {@html expr}
|
|
583
|
+
if (inner.startsWith('@html ')) {
|
|
584
|
+
const expr = inner.slice(6).trim();
|
|
585
|
+
result += `\${__sanitizeHtml(${expr})}`;
|
|
586
|
+
hasExpr = true;
|
|
587
|
+
}
|
|
588
|
+
else if (inner.startsWith('@raw ')) {
|
|
589
|
+
// Unsafe raw HTML: {@raw expr}
|
|
590
|
+
const expr = inner.slice(5).trim();
|
|
591
|
+
result += `\${__rawHtml(${expr})}`;
|
|
592
|
+
hasExpr = true;
|
|
593
|
+
}
|
|
594
|
+
else if (inner.startsWith('=html ')) {
|
|
595
|
+
// Legacy alias for raw HTML: {=html expr}
|
|
410
596
|
const expr = inner.slice(6).trim();
|
|
411
|
-
result += `\${${expr}}`;
|
|
597
|
+
result += `\${__rawHtml(${expr})}`;
|
|
412
598
|
hasExpr = true;
|
|
413
599
|
}
|
|
414
600
|
else {
|
|
@@ -610,10 +796,26 @@ function findClosingBrace(src, openPos) {
|
|
|
610
796
|
*/
|
|
611
797
|
export function generateRenderFunction(template) {
|
|
612
798
|
const body = compileTemplate(template);
|
|
613
|
-
return `function render(data) {
|
|
614
|
-
const
|
|
615
|
-
if (v == null) return '';
|
|
616
|
-
return String(v)
|
|
799
|
+
return `function render(data) {
|
|
800
|
+
const __rawHtml = (v) => {
|
|
801
|
+
if (v == null) return '';
|
|
802
|
+
return String(v);
|
|
803
|
+
};
|
|
804
|
+
const __sanitizeHtml = (v) => {
|
|
805
|
+
let html = __rawHtml(v);
|
|
806
|
+
html = html.replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, '');
|
|
807
|
+
html = html.replace(/<iframe\\b[^>]*>[\\s\\S]*?<\\/iframe>/gi, '');
|
|
808
|
+
html = html.replace(/<object\\b[^>]*>[\\s\\S]*?<\\/object>/gi, '');
|
|
809
|
+
html = html.replace(/<embed\\b[^>]*>/gi, '');
|
|
810
|
+
html = html.replace(/\\son[a-z]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
|
|
811
|
+
html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*([\"'])\\s*javascript:[\\s\\S]*?\\2/gi, ' $1="#"');
|
|
812
|
+
html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*javascript:[^\\s>]+/gi, ' $1="#"');
|
|
813
|
+
html = html.replace(/\\ssrcdoc\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
|
|
814
|
+
return html;
|
|
815
|
+
};
|
|
816
|
+
const __esc = (v) => {
|
|
817
|
+
if (v == null) return '';
|
|
818
|
+
return String(v)
|
|
617
819
|
.replace(/&/g, '&')
|
|
618
820
|
.replace(/</g, '<')
|
|
619
821
|
.replace(/>/g, '>')
|
|
@@ -623,3 +825,4 @@ export function generateRenderFunction(template) {
|
|
|
623
825
|
${body}
|
|
624
826
|
}`;
|
|
625
827
|
}
|
|
828
|
+
import { transpileTypeScript } from './transpile.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function transpileTypeScript(source: string, contextLabel: string, compilerOptions?: Record<string, unknown>): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
let tsImpl;
|
|
6
|
+
let bunTranspiler;
|
|
7
|
+
function transpileWithBun(source) {
|
|
8
|
+
const BunRuntime = globalThis.Bun;
|
|
9
|
+
if (!BunRuntime?.Transpiler)
|
|
10
|
+
return null;
|
|
11
|
+
if (!bunTranspiler) {
|
|
12
|
+
bunTranspiler = new BunRuntime.Transpiler({ loader: 'ts' });
|
|
13
|
+
}
|
|
14
|
+
const output = bunTranspiler.transformSync(source);
|
|
15
|
+
return typeof output === 'string' ? output.trim() : String(output).trim();
|
|
16
|
+
}
|
|
17
|
+
function getTypeScript() {
|
|
18
|
+
if (!tsImpl) {
|
|
19
|
+
const localTsPath = fileURLToPath(new URL('../../node_modules/typescript/lib/typescript.js', import.meta.url));
|
|
20
|
+
if (fs.existsSync(localTsPath)) {
|
|
21
|
+
tsImpl = require(localTsPath);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
tsImpl = require(require.resolve('typescript', { paths: [process.cwd()] }));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return tsImpl;
|
|
28
|
+
}
|
|
29
|
+
function formatDiagnostic(diag) {
|
|
30
|
+
if (!diag.file || typeof diag.start !== 'number') {
|
|
31
|
+
return getTypeScript().flattenDiagnosticMessageText(diag.messageText, '\n');
|
|
32
|
+
}
|
|
33
|
+
const pos = diag.file.getLineAndCharacterOfPosition(diag.start);
|
|
34
|
+
const message = getTypeScript().flattenDiagnosticMessageText(diag.messageText, '\n');
|
|
35
|
+
return `${diag.file.fileName}:${pos.line + 1}:${pos.character + 1} ${message}`;
|
|
36
|
+
}
|
|
37
|
+
export function transpileTypeScript(source, contextLabel, compilerOptions = {}) {
|
|
38
|
+
if (!source.trim())
|
|
39
|
+
return source;
|
|
40
|
+
const bunOutput = transpileWithBun(source);
|
|
41
|
+
if (bunOutput !== null)
|
|
42
|
+
return bunOutput;
|
|
43
|
+
const ts = getTypeScript();
|
|
44
|
+
const result = ts.transpileModule(source, {
|
|
45
|
+
compilerOptions: {
|
|
46
|
+
target: ts.ScriptTarget.ES2022,
|
|
47
|
+
module: ts.ModuleKind.ESNext,
|
|
48
|
+
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
|
|
49
|
+
verbatimModuleSyntax: false,
|
|
50
|
+
...compilerOptions,
|
|
51
|
+
},
|
|
52
|
+
reportDiagnostics: true,
|
|
53
|
+
fileName: contextLabel,
|
|
54
|
+
});
|
|
55
|
+
const diagnostics = (result.diagnostics || []).filter((diag) => diag.category === ts.DiagnosticCategory.Error);
|
|
56
|
+
if (diagnostics.length > 0) {
|
|
57
|
+
const rendered = diagnostics.map(formatDiagnostic).join('\n');
|
|
58
|
+
throw new Error(`[kuratchi compiler] TypeScript transpile failed for ${contextLabel}\n${rendered}`);
|
|
59
|
+
}
|
|
60
|
+
return result.outputText.trim();
|
|
61
|
+
}
|
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 */
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export { Router, filePathToPattern } from './router.js';
|
|
|
5
5
|
export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
|
|
6
6
|
export { kuratchiDO, doRpc } from './do.js';
|
|
7
7
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './containers.js';
|
|
8
|
-
export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, LayoutModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './types.js';
|
|
8
|
+
export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, ApiRouteModule, HttpMethod, LayoutModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './types.js';
|
|
9
9
|
export type { RpcOf } from './do.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageError — throw from a route's load scope to return a specific HTTP status page.
|
|
3
|
+
*
|
|
4
|
+
* Without PageError, any thrown error becomes a 500. PageError lets you return
|
|
5
|
+
* the correct HTTP status (404, 403, 401, etc.) and an optional message.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const post = await db.posts.findOne({ id: params.id });
|
|
9
|
+
* if (!post) throw new PageError(404);
|
|
10
|
+
* if (!post.isPublished) throw new PageError(403, 'This post is not published');
|
|
11
|
+
*/
|
|
12
|
+
export declare class PageError extends Error {
|
|
13
|
+
readonly isPageError = true;
|
|
14
|
+
readonly status: number;
|
|
15
|
+
constructor(status: number, message?: string);
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageError — throw from a route's load scope to return a specific HTTP status page.
|
|
3
|
+
*
|
|
4
|
+
* Without PageError, any thrown error becomes a 500. PageError lets you return
|
|
5
|
+
* the correct HTTP status (404, 403, 401, etc.) and an optional message.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const post = await db.posts.findOne({ id: params.id });
|
|
9
|
+
* if (!post) throw new PageError(404);
|
|
10
|
+
* if (!post.isPublished) throw new PageError(403, 'This post is not published');
|
|
11
|
+
*/
|
|
12
|
+
export class PageError extends Error {
|
|
13
|
+
isPageError = true;
|
|
14
|
+
status;
|
|
15
|
+
constructor(status, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'PageError';
|
|
18
|
+
this.status = status;
|
|
19
|
+
}
|
|
20
|
+
}
|