@kuratchi/js 0.0.19 → 0.0.21
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 +193 -7
- package/dist/cli.js +8 -0
- package/dist/compiler/client-module-pipeline.d.ts +8 -0
- package/dist/compiler/client-module-pipeline.js +181 -30
- package/dist/compiler/compiler-shared.d.ts +23 -0
- package/dist/compiler/component-pipeline.js +9 -1
- package/dist/compiler/config-reading.js +27 -1
- package/dist/compiler/convention-discovery.d.ts +2 -0
- package/dist/compiler/convention-discovery.js +16 -0
- package/dist/compiler/durable-object-pipeline.d.ts +1 -0
- package/dist/compiler/durable-object-pipeline.js +459 -119
- package/dist/compiler/index.js +40 -1
- package/dist/compiler/page-route-pipeline.js +31 -2
- package/dist/compiler/parser.d.ts +1 -0
- package/dist/compiler/parser.js +47 -4
- package/dist/compiler/root-layout-pipeline.js +18 -3
- package/dist/compiler/route-pipeline.d.ts +2 -0
- package/dist/compiler/route-pipeline.js +19 -3
- package/dist/compiler/routes-module-feature-blocks.js +143 -17
- package/dist/compiler/routes-module-types.d.ts +1 -0
- package/dist/compiler/server-module-pipeline.js +24 -0
- package/dist/compiler/template.d.ts +4 -0
- package/dist/compiler/template.js +50 -18
- package/dist/compiler/type-generator.d.ts +8 -0
- package/dist/compiler/type-generator.js +124 -0
- package/dist/compiler/worker-output-pipeline.js +2 -0
- package/dist/compiler/wrangler-sync.d.ts +3 -0
- package/dist/compiler/wrangler-sync.js +25 -11
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/runtime/context.d.ts +9 -1
- package/dist/runtime/context.js +25 -2
- package/dist/runtime/generated-worker.d.ts +1 -0
- package/dist/runtime/generated-worker.js +11 -7
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/navigation.d.ts +8 -0
- package/dist/runtime/navigation.js +8 -0
- package/dist/runtime/request.d.ts +28 -0
- package/dist/runtime/request.js +44 -0
- package/dist/runtime/schema.d.ts +49 -0
- package/dist/runtime/schema.js +148 -0
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/validation.d.ts +26 -0
- package/dist/runtime/validation.js +147 -0
- package/package.json +76 -68
|
@@ -152,20 +152,24 @@ function buildAppendStatement(expression, emitCall) {
|
|
|
152
152
|
// Otherwise, use array push for O(n) performance
|
|
153
153
|
return emitCall ? `${emitCall}(${expression});` : `__parts.push(${expression});`;
|
|
154
154
|
}
|
|
155
|
-
function
|
|
156
|
-
const pollMatch = tagText.match(/\bdata-poll=\{([\s\S]*?)\}/);
|
|
157
|
-
if (!pollMatch)
|
|
158
|
-
return null;
|
|
159
|
-
const inner = pollMatch[1].trim();
|
|
155
|
+
function buildPollFragmentExpr(inner, rpcNameMap, pollIndex = 0) {
|
|
160
156
|
const callMatch = inner.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
|
|
161
157
|
if (!callMatch)
|
|
162
158
|
return null;
|
|
163
159
|
const fnName = callMatch[1];
|
|
164
160
|
const rpcName = rpcNameMap?.get(fnName) || fnName;
|
|
165
161
|
const argsExpr = (callMatch[2] || '').trim();
|
|
162
|
+
const fragmentPrefix = `__poll_${pollIndex}`;
|
|
166
163
|
if (!argsExpr)
|
|
167
|
-
return JSON.stringify(
|
|
168
|
-
return
|
|
164
|
+
return JSON.stringify(`${fragmentPrefix}_${rpcName}`);
|
|
165
|
+
return `${JSON.stringify(fragmentPrefix + '_')} + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_')`;
|
|
166
|
+
}
|
|
167
|
+
function extractPollFragmentExpr(tagText, rpcNameMap, pollIndex = 0) {
|
|
168
|
+
const pollMatch = tagText.match(/\bdata-poll=\{([\s\S]*?)\}/);
|
|
169
|
+
if (!pollMatch)
|
|
170
|
+
return null;
|
|
171
|
+
const inner = pollMatch[1].trim();
|
|
172
|
+
return buildPollFragmentExpr(inner, rpcNameMap, pollIndex);
|
|
169
173
|
}
|
|
170
174
|
function findTagEnd(template, start) {
|
|
171
175
|
let quote = null;
|
|
@@ -208,6 +212,7 @@ function instrumentPollFragments(template, rpcNameMap) {
|
|
|
208
212
|
const out = [];
|
|
209
213
|
const stack = [];
|
|
210
214
|
let cursor = 0;
|
|
215
|
+
let pollIndex = 0;
|
|
211
216
|
while (cursor < template.length) {
|
|
212
217
|
const lt = template.indexOf('<', cursor);
|
|
213
218
|
if (lt === -1) {
|
|
@@ -245,15 +250,20 @@ function instrumentPollFragments(template, rpcNameMap) {
|
|
|
245
250
|
continue;
|
|
246
251
|
}
|
|
247
252
|
const openMatch = tagText.match(/^<\s*([A-Za-z][\w:-]*)\b/);
|
|
248
|
-
out.push(tagText);
|
|
249
253
|
if (!openMatch) {
|
|
254
|
+
out.push(tagText);
|
|
250
255
|
cursor = tagEnd + 1;
|
|
251
256
|
continue;
|
|
252
257
|
}
|
|
253
258
|
const tagName = openMatch[1].toLowerCase();
|
|
254
259
|
const isVoidLike = /\/\s*>$/.test(tagText) || /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(tagName);
|
|
255
|
-
const fragmentExpr = extractPollFragmentExpr(tagText, rpcNameMap);
|
|
260
|
+
const fragmentExpr = extractPollFragmentExpr(tagText, rpcNameMap, pollIndex);
|
|
261
|
+
const instrumentedTagText = fragmentExpr
|
|
262
|
+
? tagText.replace(/\bdata-poll=\{([\s\S]*?)\}/, (match) => `${match} data-kuratchi-poll-fragment={${fragmentExpr}}`)
|
|
263
|
+
: tagText;
|
|
264
|
+
out.push(instrumentedTagText);
|
|
256
265
|
if (fragmentExpr) {
|
|
266
|
+
pollIndex += 1;
|
|
257
267
|
out.push(`${FRAGMENT_OPEN_MARKER}${encodeURIComponent(fragmentExpr)}-->`);
|
|
258
268
|
if (isVoidLike) {
|
|
259
269
|
out.push(FRAGMENT_CLOSE_MARKER);
|
|
@@ -404,6 +414,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
404
414
|
const childTemplate = childLines.join('\n');
|
|
405
415
|
const childBody = compileTemplate(childTemplate, componentNames, actionNames, rpcNameMap, {
|
|
406
416
|
clientRouteRegistry: options.clientRouteRegistry,
|
|
417
|
+
awaitQueryBindings: options.awaitQueryBindings,
|
|
407
418
|
});
|
|
408
419
|
// Wrap in an IIFE that returns the children HTML
|
|
409
420
|
const childrenExpr = `(function() { ${childBody}; return __html; })()`;
|
|
@@ -604,6 +615,14 @@ function transformClientScriptBlock(block) {
|
|
|
604
615
|
// TypeScript is preserved — wrangler's esbuild handles transpilation
|
|
605
616
|
return `${openTag}${out.join('\n')}${closeTag}`;
|
|
606
617
|
}
|
|
618
|
+
function extractAwaitTemplateCall(expr) {
|
|
619
|
+
const match = expr.trim().match(/^await\s+([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
|
|
620
|
+
if (!match)
|
|
621
|
+
return null;
|
|
622
|
+
const fnName = match[1];
|
|
623
|
+
const argsExpr = (match[2] || '').trim();
|
|
624
|
+
return { fnName, argsExpr, awaitExpr: `${fnName}(${argsExpr})` };
|
|
625
|
+
}
|
|
607
626
|
function braceDelta(line) {
|
|
608
627
|
let delta = 0;
|
|
609
628
|
let inSingle = false;
|
|
@@ -843,6 +862,7 @@ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap, o
|
|
|
843
862
|
// Compile the inline content as a mini-template to handle {expr} interpolation
|
|
844
863
|
const childBody = compileTemplate(innerContent, componentNames, actionNames, rpcNameMap, {
|
|
845
864
|
clientRouteRegistry: options.clientRouteRegistry,
|
|
865
|
+
awaitQueryBindings: options.awaitQueryBindings,
|
|
846
866
|
});
|
|
847
867
|
const childrenExpr = `(function() { ${childBody}; return __html; })()`;
|
|
848
868
|
return buildAppendStatement(`${funcName}({ ${propsStr}${propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc)`, options.emitCall);
|
|
@@ -1028,8 +1048,14 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
|
1028
1048
|
pos = closeIdx + 1;
|
|
1029
1049
|
continue;
|
|
1030
1050
|
}
|
|
1051
|
+
else if (attrName === 'data-kuratchi-poll-fragment') {
|
|
1052
|
+
result = result.replace(/\s*data-kuratchi-poll-fragment=$/, '');
|
|
1053
|
+
result += ` data-poll-id="\${__signFragment(${inner})}"`;
|
|
1054
|
+
pos = closeIdx + 1;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1031
1057
|
else if (attrName === 'data-poll') {
|
|
1032
|
-
// data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]"
|
|
1058
|
+
// data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]"
|
|
1033
1059
|
const pollCallMatch = inner.match(/^(\w+)\((.*)\)$/);
|
|
1034
1060
|
if (pollCallMatch) {
|
|
1035
1061
|
const fnName = pollCallMatch[1];
|
|
@@ -1037,16 +1063,11 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
|
1037
1063
|
const argsExpr = pollCallMatch[2].trim();
|
|
1038
1064
|
// Remove the trailing "data-poll=" we already appended
|
|
1039
1065
|
result = result.replace(/\s*data-poll=$/, '');
|
|
1040
|
-
// Emit data-poll
|
|
1066
|
+
// Emit data-poll and data-poll-args. The signed poll fragment ID is emitted
|
|
1067
|
+
// from the synthetic data-kuratchi-poll-fragment attribute injected earlier.
|
|
1041
1068
|
result += ` data-poll="${rpcName}"`;
|
|
1042
1069
|
if (argsExpr) {
|
|
1043
1070
|
result += ` data-poll-args="\${__esc(JSON.stringify([${argsExpr}]))}"`;
|
|
1044
|
-
// Sign the fragment ID at runtime with __signFragment(fragmentId, routePath)
|
|
1045
|
-
result += ` data-poll-id="\${__signFragment('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
|
|
1046
|
-
}
|
|
1047
|
-
else {
|
|
1048
|
-
// No args - sign with function name as base ID
|
|
1049
|
-
result += ` data-poll-id="\${__signFragment('__poll_${rpcName}')}"`;
|
|
1050
1071
|
}
|
|
1051
1072
|
}
|
|
1052
1073
|
pos = closeIdx + 1;
|
|
@@ -1119,7 +1140,18 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
|
1119
1140
|
}
|
|
1120
1141
|
}
|
|
1121
1142
|
else {
|
|
1122
|
-
|
|
1143
|
+
const awaitCall = extractAwaitTemplateCall(inner);
|
|
1144
|
+
const awaitBinding = awaitCall ? options.awaitQueryBindings?.get(awaitCall.awaitExpr) : null;
|
|
1145
|
+
if (awaitCall && awaitBinding) {
|
|
1146
|
+
result += `<span data-remote-read="${awaitBinding.rpcId}" data-get="${awaitBinding.rpcId}"`;
|
|
1147
|
+
if (awaitCall.argsExpr) {
|
|
1148
|
+
result += ` data-get-args="\${__esc(JSON.stringify([${awaitCall.argsExpr}]))}"`;
|
|
1149
|
+
}
|
|
1150
|
+
result += `>\${__esc(((${awaitBinding.asName} && ${awaitBinding.asName}.data) ?? ''))}</span>`;
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
result += `\${__esc(${inner})}`;
|
|
1154
|
+
}
|
|
1123
1155
|
}
|
|
1124
1156
|
}
|
|
1125
1157
|
pos = closeIdx + 1;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface GenerateTypesOptions {
|
|
2
|
+
projectDir: string;
|
|
3
|
+
schemaPath?: string;
|
|
4
|
+
outputPath?: string;
|
|
5
|
+
localsInterface?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function generateAppTypes(options: GenerateTypesOptions): string;
|
|
8
|
+
export declare function writeAppTypes(options: GenerateTypesOptions): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
function sqliteTypeToTs(sqlType) {
|
|
4
|
+
const lower = sqlType.toLowerCase();
|
|
5
|
+
if (lower.includes('integer') || lower.includes('int') || lower.includes('real') || lower.includes('numeric')) {
|
|
6
|
+
return 'number';
|
|
7
|
+
}
|
|
8
|
+
if (lower.includes('text') || lower.includes('varchar') || lower.includes('char')) {
|
|
9
|
+
return 'string';
|
|
10
|
+
}
|
|
11
|
+
if (lower.includes('blob')) {
|
|
12
|
+
return 'Uint8Array';
|
|
13
|
+
}
|
|
14
|
+
if (lower.includes('json')) {
|
|
15
|
+
return 'Record<string, unknown>';
|
|
16
|
+
}
|
|
17
|
+
if (lower.includes('boolean') || lower.includes('bool')) {
|
|
18
|
+
return 'boolean';
|
|
19
|
+
}
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
function parseSchemaColumn(name, definition) {
|
|
23
|
+
const lower = definition.toLowerCase();
|
|
24
|
+
const nullable = !lower.includes('not null');
|
|
25
|
+
const hasDefault = lower.includes('default');
|
|
26
|
+
const type = sqliteTypeToTs(definition);
|
|
27
|
+
return { name, type, nullable, hasDefault };
|
|
28
|
+
}
|
|
29
|
+
function parseSchemaFromSource(source) {
|
|
30
|
+
const tables = [];
|
|
31
|
+
// Match tables: { tableName: { col: 'def', ... }, ... }
|
|
32
|
+
const tablesMatch = source.match(/tables\s*:\s*\{([\s\S]*?)\n\t?\}/);
|
|
33
|
+
if (!tablesMatch)
|
|
34
|
+
return tables;
|
|
35
|
+
const tablesBlock = tablesMatch[1];
|
|
36
|
+
// Match each table definition
|
|
37
|
+
const tableRegex = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = tableRegex.exec(tablesBlock)) !== null) {
|
|
40
|
+
const tableName = match[1];
|
|
41
|
+
const columnsBlock = match[2];
|
|
42
|
+
const columns = [];
|
|
43
|
+
// Match each column: name: 'definition'
|
|
44
|
+
const colRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]/g;
|
|
45
|
+
let colMatch;
|
|
46
|
+
while ((colMatch = colRegex.exec(columnsBlock)) !== null) {
|
|
47
|
+
columns.push(parseSchemaColumn(colMatch[1], colMatch[2]));
|
|
48
|
+
}
|
|
49
|
+
tables.push({ name: tableName, columns });
|
|
50
|
+
}
|
|
51
|
+
return tables;
|
|
52
|
+
}
|
|
53
|
+
function generateTableTypes(tables) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
for (const table of tables) {
|
|
56
|
+
const pascalName = table.name
|
|
57
|
+
.split('_')
|
|
58
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
59
|
+
.join('');
|
|
60
|
+
lines.push(` /** Row type for ${table.name} table */`);
|
|
61
|
+
lines.push(` interface ${pascalName}Row {`);
|
|
62
|
+
for (const col of table.columns) {
|
|
63
|
+
const optional = col.nullable || col.hasDefault ? '?' : '';
|
|
64
|
+
lines.push(` ${col.name}${optional}: ${col.type};`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(` }`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
export function generateAppTypes(options) {
|
|
72
|
+
const { projectDir, schemaPath = 'src/server/schema.ts', outputPath = 'src/app.d.ts', localsInterface, } = options;
|
|
73
|
+
const schemaFullPath = path.join(projectDir, schemaPath);
|
|
74
|
+
let tables = [];
|
|
75
|
+
if (fs.existsSync(schemaFullPath)) {
|
|
76
|
+
const schemaSource = fs.readFileSync(schemaFullPath, 'utf-8');
|
|
77
|
+
tables = parseSchemaFromSource(schemaSource);
|
|
78
|
+
}
|
|
79
|
+
const tableTypes = tables.length > 0 ? generateTableTypes(tables) : '';
|
|
80
|
+
// Check if user has existing Locals definition to preserve
|
|
81
|
+
const outputFullPath = path.join(projectDir, outputPath);
|
|
82
|
+
let existingLocals = null;
|
|
83
|
+
if (fs.existsSync(outputFullPath)) {
|
|
84
|
+
const existing = fs.readFileSync(outputFullPath, 'utf-8');
|
|
85
|
+
// Extract user-defined Locals interface (between USER LOCALS START/END markers)
|
|
86
|
+
const localsMatch = existing.match(/\/\/ USER LOCALS START\n([\s\S]*?)\/\/ USER LOCALS END/);
|
|
87
|
+
if (localsMatch) {
|
|
88
|
+
existingLocals = localsMatch[1];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const localsBlock = existingLocals || localsInterface || ` interface Locals {
|
|
92
|
+
userId: number;
|
|
93
|
+
userEmail: string;
|
|
94
|
+
}`;
|
|
95
|
+
const output = `/**
|
|
96
|
+
* Type declarations for kuratchi app.
|
|
97
|
+
*
|
|
98
|
+
* DB types are auto-generated from schema.ts - regenerate with: kuratchi types
|
|
99
|
+
* Edit the Locals interface below to match your runtime.hook.ts
|
|
100
|
+
*/
|
|
101
|
+
declare global {
|
|
102
|
+
namespace App {
|
|
103
|
+
/** Request-scoped locals set by runtime hooks */
|
|
104
|
+
// USER LOCALS START
|
|
105
|
+
${localsBlock}
|
|
106
|
+
// USER LOCALS END
|
|
107
|
+
|
|
108
|
+
${tableTypes ? ` // Database table row types (auto-generated from schema.ts)\n${tableTypes}` : ''} }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export {};
|
|
112
|
+
`;
|
|
113
|
+
return output;
|
|
114
|
+
}
|
|
115
|
+
export function writeAppTypes(options) {
|
|
116
|
+
const output = generateAppTypes(options);
|
|
117
|
+
const outputPath = path.join(options.projectDir, options.outputPath || 'src/app.d.ts');
|
|
118
|
+
const dir = path.dirname(outputPath);
|
|
119
|
+
if (!fs.existsSync(dir)) {
|
|
120
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
fs.writeFileSync(outputPath, output, 'utf-8');
|
|
123
|
+
console.log(`[kuratchi] Generated types → ${path.relative(options.projectDir, outputPath)}`);
|
|
124
|
+
}
|
|
@@ -19,7 +19,9 @@ export function toWorkerImportPath(projectDir, outDir, filePath) {
|
|
|
19
19
|
return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
|
|
20
20
|
}
|
|
21
21
|
export function buildWorkerEntrypointSource(opts) {
|
|
22
|
+
const doClassSet = new Set(opts.doClassNames);
|
|
22
23
|
const workerClassExports = opts.workerClassEntries
|
|
24
|
+
.filter((entry) => !doClassSet.has(entry.className))
|
|
23
25
|
.map((entry) => {
|
|
24
26
|
const importPath = toWorkerImportPath(opts.projectDir, opts.outDir, entry.file);
|
|
25
27
|
if (entry.exportKind === 'default') {
|
|
@@ -6,6 +6,9 @@ export interface WranglerSyncConfig {
|
|
|
6
6
|
workflows: WranglerSyncEntry[];
|
|
7
7
|
containers: WranglerSyncEntry[];
|
|
8
8
|
durableObjects: WranglerSyncEntry[];
|
|
9
|
+
/** Relative path to the public assets directory (e.g. '.kuratchi/public/'). When set, the
|
|
10
|
+
* `assets.directory` and `assets.binding` keys in wrangler.jsonc are kept in sync automatically. */
|
|
11
|
+
assetsDirectory?: string;
|
|
9
12
|
}
|
|
10
13
|
export declare function syncWranglerConfig(opts: {
|
|
11
14
|
projectDir: string;
|
|
@@ -128,24 +128,23 @@ export function syncWranglerConfig(opts) {
|
|
|
128
128
|
}
|
|
129
129
|
if (opts.config.containers.length > 0) {
|
|
130
130
|
const existingContainers = wranglerConfig.containers || [];
|
|
131
|
-
const
|
|
131
|
+
const existingByClassName = new Map(existingContainers.map((container) => [container.class_name, container]));
|
|
132
132
|
for (const container of opts.config.containers) {
|
|
133
133
|
const name = container.binding.toLowerCase().replace(/_/g, '-');
|
|
134
134
|
const entry = {
|
|
135
135
|
name,
|
|
136
|
-
binding: container.binding,
|
|
137
136
|
class_name: container.className,
|
|
138
137
|
};
|
|
139
|
-
const existing =
|
|
138
|
+
const existing = existingByClassName.get(container.className);
|
|
140
139
|
if (!existing) {
|
|
141
140
|
existingContainers.push(entry);
|
|
142
141
|
changed = true;
|
|
143
|
-
console.log(`[kuratchi] Added container "${container.
|
|
142
|
+
console.log(`[kuratchi] Added container "${container.className}" to wrangler config`);
|
|
144
143
|
}
|
|
145
|
-
else if (existing.
|
|
146
|
-
existing.
|
|
144
|
+
else if (existing.name !== name) {
|
|
145
|
+
existing.name = name;
|
|
147
146
|
changed = true;
|
|
148
|
-
console.log(`[kuratchi] Updated container "${container.
|
|
147
|
+
console.log(`[kuratchi] Updated container "${container.className}" name to "${name}"`);
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
150
|
wranglerConfig.containers = existingContainers;
|
|
@@ -170,13 +169,28 @@ export function syncWranglerConfig(opts) {
|
|
|
170
169
|
changed = true;
|
|
171
170
|
console.log(`[kuratchi] Added durable_object "${durableObject.binding}" to wrangler config`);
|
|
172
171
|
}
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
}
|
|
173
|
+
wranglerConfig.durable_objects.bindings = existingBindings;
|
|
174
|
+
}
|
|
175
|
+
if (opts.config.assetsDirectory !== undefined) {
|
|
176
|
+
const existing = wranglerConfig.assets;
|
|
177
|
+
if (!existing) {
|
|
178
|
+
wranglerConfig.assets = { directory: opts.config.assetsDirectory, binding: 'ASSETS' };
|
|
179
|
+
changed = true;
|
|
180
|
+
console.log(`[kuratchi] Added static assets directory "${opts.config.assetsDirectory}" to wrangler config`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
if (existing.directory !== opts.config.assetsDirectory) {
|
|
184
|
+
existing.directory = opts.config.assetsDirectory;
|
|
185
|
+
changed = true;
|
|
186
|
+
console.log(`[kuratchi] Updated static assets directory to "${opts.config.assetsDirectory}" in wrangler config`);
|
|
187
|
+
}
|
|
188
|
+
if (!existing.binding) {
|
|
189
|
+
existing.binding = 'ASSETS';
|
|
175
190
|
changed = true;
|
|
176
|
-
console.log(`[kuratchi]
|
|
191
|
+
console.log(`[kuratchi] Added ASSETS binding to static assets config in wrangler config`);
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
|
-
wranglerConfig.durable_objects.bindings = existingBindings;
|
|
180
194
|
}
|
|
181
195
|
if (!changed)
|
|
182
196
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,11 @@ export { defineConfig } from './runtime/config.js';
|
|
|
8
8
|
export { defineRuntime } from './runtime/runtime.js';
|
|
9
9
|
export { getCtx, getEnv, getRequest, getLocals, getParams, getParam, RedirectError, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
10
10
|
export { kuratchiDO, doRpc, getDb } from './runtime/do.js';
|
|
11
|
+
export { SchemaValidationError, schema, } from './runtime/schema.js';
|
|
11
12
|
export { ActionError } from './runtime/action.js';
|
|
12
13
|
export { PageError } from './runtime/page-error.js';
|
|
13
14
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
14
15
|
export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './runtime/types.js';
|
|
15
16
|
export type { RpcOf } from './runtime/do.js';
|
|
17
|
+
export type { SchemaType, InferSchema } from './runtime/schema.js';
|
|
16
18
|
export { url, pathname, searchParams, headers, method, params, slug } from './runtime/request.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { defineConfig } from './runtime/config.js';
|
|
|
9
9
|
export { defineRuntime } from './runtime/runtime.js';
|
|
10
10
|
export { getCtx, getEnv, getRequest, getLocals, getParams, getParam, RedirectError, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
11
11
|
export { kuratchiDO, doRpc, getDb } from './runtime/do.js';
|
|
12
|
+
export { SchemaValidationError, schema, } from './runtime/schema.js';
|
|
12
13
|
export { ActionError } from './runtime/action.js';
|
|
13
14
|
export { PageError } from './runtime/page-error.js';
|
|
14
15
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
|
|
@@ -20,6 +20,12 @@ export declare class RedirectError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
/** Called by the framework at the start of each request */
|
|
22
22
|
export declare function __setRequestContext(ctx: any, request: Request, env?: Record<string, any>): void;
|
|
23
|
+
/**
|
|
24
|
+
* Push a new request context for the duration of a DO RPC call.
|
|
25
|
+
* Saves current state and returns a restore function.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function __pushRequestContext(rpcContext: any, ctx: any, env: any): () => void;
|
|
23
29
|
/** Get the execution context (Worker: ExecutionContext, DO: DurableObjectState) */
|
|
24
30
|
export declare function getCtx(): any;
|
|
25
31
|
/** Get the current environment bindings */
|
|
@@ -37,7 +43,9 @@ export declare function getParam(name: string): string | undefined;
|
|
|
37
43
|
* Throws a redirect signal consumed by the framework's PRG flow.
|
|
38
44
|
*/
|
|
39
45
|
export declare function redirect(path: string, status?: number): never;
|
|
40
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* @deprecated Use redirect() instead. This alias will be removed in a future version.
|
|
48
|
+
*/
|
|
41
49
|
export declare function goto(path: string, status?: number): never;
|
|
42
50
|
export declare function setBreadcrumbs(items: BreadcrumbItem[]): void;
|
|
43
51
|
export declare function getBreadcrumbs(): BreadcrumbItem[];
|
package/dist/runtime/context.js
CHANGED
|
@@ -32,12 +32,33 @@ export function __setRequestContext(ctx, request, env) {
|
|
|
32
32
|
__locals = {};
|
|
33
33
|
__setRequestState(request);
|
|
34
34
|
// Expose context on globalThis for @kuratchi/auth and other packages
|
|
35
|
-
// Workers are single-threaded per request
|
|
35
|
+
// Workers are single-threaded per request — this is safe
|
|
36
36
|
globalThis.__kuratchi_context__ = {
|
|
37
37
|
get request() { return __request; },
|
|
38
38
|
get locals() { return __locals; },
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Push a new request context for the duration of a DO RPC call.
|
|
43
|
+
* Saves current state and returns a restore function.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export function __pushRequestContext(rpcContext, ctx, env) {
|
|
47
|
+
const prevCtx = __ctx;
|
|
48
|
+
const prevRequest = __request;
|
|
49
|
+
const prevEnv = __env;
|
|
50
|
+
const prevLocals = __locals;
|
|
51
|
+
__ctx = ctx;
|
|
52
|
+
__request = rpcContext?.request ?? __request;
|
|
53
|
+
__env = env ?? __env;
|
|
54
|
+
__locals = rpcContext?.locals ? { ...rpcContext.locals } : {};
|
|
55
|
+
return () => {
|
|
56
|
+
__ctx = prevCtx;
|
|
57
|
+
__request = prevRequest;
|
|
58
|
+
__env = prevEnv;
|
|
59
|
+
__locals = prevLocals;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
41
62
|
/** Get the execution context (Worker: ExecutionContext, DO: DurableObjectState) */
|
|
42
63
|
export function getCtx() {
|
|
43
64
|
const doSelf = __getDoSelf();
|
|
@@ -83,7 +104,9 @@ export function redirect(path, status = 303) {
|
|
|
83
104
|
__locals.__redirectStatus = status;
|
|
84
105
|
throw new RedirectError(path, status);
|
|
85
106
|
}
|
|
86
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* @deprecated Use redirect() instead. This alias will be removed in a future version.
|
|
109
|
+
*/
|
|
87
110
|
export function goto(path, status = 303) {
|
|
88
111
|
redirect(path, status);
|
|
89
112
|
}
|
|
@@ -14,6 +14,7 @@ export interface GeneratedPageRoute {
|
|
|
14
14
|
load?: (params: Record<string, string>) => Promise<unknown> | unknown;
|
|
15
15
|
actions?: Record<string, (...args: any[]) => Promise<unknown> | unknown>;
|
|
16
16
|
rpc?: Record<string, (...args: any[]) => Promise<unknown> | unknown>;
|
|
17
|
+
rpcSchemas?: Record<string, any>;
|
|
17
18
|
/** Allowed query function names for this route (for query override validation) */
|
|
18
19
|
allowedQueries?: string[];
|
|
19
20
|
render: (data: Record<string, any>) => PageRenderOutput;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { __esc, __getLocals, __setLocal, __setRequestContext, buildDefaultBreadcrumbs } from './context.js';
|
|
2
2
|
import { Router } from './router.js';
|
|
3
3
|
import { initCsrf, getCsrfCookieHeader, validateRpcRequest, validateActionRequest, validateSignedFragment, validateQueryOverride, parseQueryArgs, CSRF_DEFAULTS, } from './security.js';
|
|
4
|
+
import { SchemaValidationError, parseRpcArgsPayload, validateSchemaInput, } from './schema.js';
|
|
4
5
|
export function createGeneratedWorker(opts) {
|
|
5
6
|
const router = new Router();
|
|
6
7
|
const runtimeEntries = __getRuntimeEntries(opts.runtimeDefinition);
|
|
@@ -225,19 +226,22 @@ async function __handleRpc(route, workflowStatusRpc, runtimeCtx, securityConfig)
|
|
|
225
226
|
}));
|
|
226
227
|
}
|
|
227
228
|
try {
|
|
228
|
-
const
|
|
229
|
-
let rpcArgs = [];
|
|
230
|
-
if (rpcArgsStr) {
|
|
231
|
-
const parsed = JSON.parse(rpcArgsStr);
|
|
232
|
-
rpcArgs = Array.isArray(parsed) ? parsed : [];
|
|
233
|
-
}
|
|
229
|
+
const rpcArgs = parseRpcArgsPayload(url.searchParams.get('_args'));
|
|
234
230
|
const rpcFn = hasRouteRpc ? route.rpc[rpcName] : workflowStatusRpc[rpcName];
|
|
235
|
-
const
|
|
231
|
+
const rpcSchema = hasRouteRpc ? route.rpcSchemas?.[rpcName] : undefined;
|
|
232
|
+
const validatedArgs = hasRouteRpc ? validateSchemaInput(rpcSchema, rpcArgs) : rpcArgs;
|
|
233
|
+
const rpcResult = await rpcFn(...validatedArgs);
|
|
236
234
|
return __attachCookies(new Response(JSON.stringify({ ok: true, data: rpcResult }), {
|
|
237
235
|
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
238
236
|
}));
|
|
239
237
|
}
|
|
240
238
|
catch (err) {
|
|
239
|
+
if (err instanceof SchemaValidationError || err?.isSchemaValidationError) {
|
|
240
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: err.message }), {
|
|
241
|
+
status: 400,
|
|
242
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
241
245
|
console.error('[kuratchi] RPC error:', err);
|
|
242
246
|
const errMsg = __sanitizeErrorMessage(err);
|
|
243
247
|
return __secHeaders(new Response(JSON.stringify({ ok: false, error: errMsg }), {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { defineRuntime } from './runtime.js';
|
|
|
5
5
|
export { Router, filePathToPattern } from './router.js';
|
|
6
6
|
export { getCtx, getRequest, getLocals, getParams, getParam, RedirectError, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
|
|
7
7
|
export { kuratchiDO, doRpc } from './do.js';
|
|
8
|
+
export { SchemaValidationError, schema, validateSchemaInput, parseRpcArgsPayload, } from './schema.js';
|
|
9
|
+
export type { SchemaType, InferSchema, } from './schema.js';
|
|
8
10
|
export { initCsrf, getCsrfToken, validateCsrf, getCsrfCookieHeader, validateRpcRequest, validateActionRequest, applySecurityHeaders, signFragmentId, validateSignedFragment, validateQueryOverride, parseQueryArgs, CSRF_DEFAULTS, } from './security.js';
|
|
9
11
|
export type { RpcSecurityConfig, ActionSecurityConfig, SecurityHeadersConfig, } from './security.js';
|
|
10
12
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './containers.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { defineRuntime } from './runtime.js';
|
|
|
5
5
|
export { Router, filePathToPattern } from './router.js';
|
|
6
6
|
export { getCtx, getRequest, getLocals, getParams, getParam, RedirectError, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
|
|
7
7
|
export { kuratchiDO, doRpc } from './do.js';
|
|
8
|
+
export { SchemaValidationError, schema, validateSchemaInput, parseRpcArgsPayload, } from './schema.js';
|
|
8
9
|
export { initCsrf, getCsrfToken, validateCsrf, getCsrfCookieHeader, validateRpcRequest, validateActionRequest, applySecurityHeaders, signFragmentId, validateSignedFragment, validateQueryOverride, parseQueryArgs, CSRF_DEFAULTS, } from './security.js';
|
|
9
10
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
|
|
10
11
|
// Compatibility aliases
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation helpers for kuratchi routes.
|
|
3
|
+
* Import via: import { redirect } from 'kuratchi:navigation';
|
|
4
|
+
*
|
|
5
|
+
* redirect() is server-side only — works in route scripts, server modules, and form actions.
|
|
6
|
+
* It throws a RedirectError that the framework catches and converts to a 303 redirect response.
|
|
7
|
+
*/
|
|
8
|
+
export { redirect } from './context.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation helpers for kuratchi routes.
|
|
3
|
+
* Import via: import { redirect } from 'kuratchi:navigation';
|
|
4
|
+
*
|
|
5
|
+
* redirect() is server-side only — works in route scripts, server modules, and form actions.
|
|
6
|
+
* It throws a RedirectError that the framework catches and converts to a 303 redirect response.
|
|
7
|
+
*/
|
|
8
|
+
export { redirect } from './context.js';
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
namespace App {
|
|
3
|
+
/** Request-scoped locals set by runtime hooks. Extend in your app.d.ts */
|
|
4
|
+
interface Locals {
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
1
9
|
export declare let url: URL;
|
|
2
10
|
export declare let pathname: string;
|
|
3
11
|
export declare let searchParams: URLSearchParams;
|
|
@@ -5,5 +13,25 @@ export declare let headers: Headers;
|
|
|
5
13
|
export declare let method: string;
|
|
6
14
|
export declare let params: Record<string, string>;
|
|
7
15
|
export declare let slug: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Get request-scoped locals with full type safety.
|
|
18
|
+
* Define your Locals type in src/app.d.ts:
|
|
19
|
+
* ```
|
|
20
|
+
* declare global {
|
|
21
|
+
* namespace App {
|
|
22
|
+
* interface Locals {
|
|
23
|
+
* userId: number;
|
|
24
|
+
* userEmail: string;
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function getLocals(): App.Locals;
|
|
31
|
+
/**
|
|
32
|
+
* Direct access to request-scoped locals.
|
|
33
|
+
* Type is inferred from App.Locals declared in your app.d.ts.
|
|
34
|
+
*/
|
|
35
|
+
export declare const locals: App.Locals;
|
|
8
36
|
export declare function __setRequestState(request: Request): void;
|
|
9
37
|
export declare function __setRequestParams(nextParams: Record<string, string> | null | undefined): void;
|
package/dist/runtime/request.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { __getLocals } from './context.js';
|
|
1
2
|
export let url = new URL('http://localhost/');
|
|
2
3
|
export let pathname = '/';
|
|
3
4
|
export let searchParams = url.searchParams;
|
|
@@ -5,6 +6,49 @@ export let headers = new Headers();
|
|
|
5
6
|
export let method = 'GET';
|
|
6
7
|
export let params = {};
|
|
7
8
|
export let slug = undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Get request-scoped locals with full type safety.
|
|
11
|
+
* Define your Locals type in src/app.d.ts:
|
|
12
|
+
* ```
|
|
13
|
+
* declare global {
|
|
14
|
+
* namespace App {
|
|
15
|
+
* interface Locals {
|
|
16
|
+
* userId: number;
|
|
17
|
+
* userEmail: string;
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function getLocals() {
|
|
24
|
+
return __getLocals();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Direct access to request-scoped locals.
|
|
28
|
+
* Type is inferred from App.Locals declared in your app.d.ts.
|
|
29
|
+
*/
|
|
30
|
+
export const locals = new Proxy({}, {
|
|
31
|
+
get(_target, prop) {
|
|
32
|
+
return __getLocals()[prop];
|
|
33
|
+
},
|
|
34
|
+
set(_target, prop, value) {
|
|
35
|
+
__getLocals()[prop] = value;
|
|
36
|
+
return true;
|
|
37
|
+
},
|
|
38
|
+
has(_target, prop) {
|
|
39
|
+
return prop in __getLocals();
|
|
40
|
+
},
|
|
41
|
+
ownKeys() {
|
|
42
|
+
return Reflect.ownKeys(__getLocals());
|
|
43
|
+
},
|
|
44
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
45
|
+
const locals = __getLocals();
|
|
46
|
+
if (prop in locals) {
|
|
47
|
+
return { configurable: true, enumerable: true, value: locals[prop] };
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
8
52
|
function __syncDerivedState() {
|
|
9
53
|
pathname = url.pathname;
|
|
10
54
|
searchParams = url.searchParams;
|