@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.
Files changed (46) hide show
  1. package/README.md +193 -7
  2. package/dist/cli.js +8 -0
  3. package/dist/compiler/client-module-pipeline.d.ts +8 -0
  4. package/dist/compiler/client-module-pipeline.js +181 -30
  5. package/dist/compiler/compiler-shared.d.ts +23 -0
  6. package/dist/compiler/component-pipeline.js +9 -1
  7. package/dist/compiler/config-reading.js +27 -1
  8. package/dist/compiler/convention-discovery.d.ts +2 -0
  9. package/dist/compiler/convention-discovery.js +16 -0
  10. package/dist/compiler/durable-object-pipeline.d.ts +1 -0
  11. package/dist/compiler/durable-object-pipeline.js +459 -119
  12. package/dist/compiler/index.js +40 -1
  13. package/dist/compiler/page-route-pipeline.js +31 -2
  14. package/dist/compiler/parser.d.ts +1 -0
  15. package/dist/compiler/parser.js +47 -4
  16. package/dist/compiler/root-layout-pipeline.js +18 -3
  17. package/dist/compiler/route-pipeline.d.ts +2 -0
  18. package/dist/compiler/route-pipeline.js +19 -3
  19. package/dist/compiler/routes-module-feature-blocks.js +143 -17
  20. package/dist/compiler/routes-module-types.d.ts +1 -0
  21. package/dist/compiler/server-module-pipeline.js +24 -0
  22. package/dist/compiler/template.d.ts +4 -0
  23. package/dist/compiler/template.js +50 -18
  24. package/dist/compiler/type-generator.d.ts +8 -0
  25. package/dist/compiler/type-generator.js +124 -0
  26. package/dist/compiler/worker-output-pipeline.js +2 -0
  27. package/dist/compiler/wrangler-sync.d.ts +3 -0
  28. package/dist/compiler/wrangler-sync.js +25 -11
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +1 -0
  31. package/dist/runtime/context.d.ts +9 -1
  32. package/dist/runtime/context.js +25 -2
  33. package/dist/runtime/generated-worker.d.ts +1 -0
  34. package/dist/runtime/generated-worker.js +11 -7
  35. package/dist/runtime/index.d.ts +2 -0
  36. package/dist/runtime/index.js +1 -0
  37. package/dist/runtime/navigation.d.ts +8 -0
  38. package/dist/runtime/navigation.js +8 -0
  39. package/dist/runtime/request.d.ts +28 -0
  40. package/dist/runtime/request.js +44 -0
  41. package/dist/runtime/schema.d.ts +49 -0
  42. package/dist/runtime/schema.js +148 -0
  43. package/dist/runtime/types.d.ts +2 -0
  44. package/dist/runtime/validation.d.ts +26 -0
  45. package/dist/runtime/validation.js +147 -0
  46. package/package.json +76 -68
@@ -29,6 +29,7 @@ export { compileTemplate, generateRenderFunction } from './template.js';
29
29
  const FRAMEWORK_PACKAGE_NAME = getFrameworkPackageName();
30
30
  const RUNTIME_CONTEXT_IMPORT = `${FRAMEWORK_PACKAGE_NAME}/runtime/context.js`;
31
31
  const RUNTIME_DO_IMPORT = `${FRAMEWORK_PACKAGE_NAME}/runtime/do.js`;
32
+ const RUNTIME_SCHEMA_IMPORT = `${FRAMEWORK_PACKAGE_NAME}/runtime/schema.js`;
32
33
  const RUNTIME_WORKER_IMPORT = `${FRAMEWORK_PACKAGE_NAME}/runtime/generated-worker.js`;
33
34
  function getFrameworkPackageName() {
34
35
  try {
@@ -151,6 +152,7 @@ export async function compile(options) {
151
152
  const proxyCode = generateHandlerProxy(handler, {
152
153
  projectDir,
153
154
  runtimeDoImport: RUNTIME_DO_IMPORT,
155
+ runtimeSchemaImport: RUNTIME_SCHEMA_IMPORT,
154
156
  });
155
157
  const proxyFile = path.join(doProxyDir, handler.fileName + '.ts');
156
158
  const proxyFileDir = path.dirname(proxyFile);
@@ -168,7 +170,7 @@ export async function compile(options) {
168
170
  if (handler.fileName.endsWith('.do')) {
169
171
  const aliasFileName = handler.fileName.slice(0, -3);
170
172
  const aliasProxyFile = path.join(doProxyDir, aliasFileName + '.ts');
171
- const aliasCode = `// Auto-generated alias for .do handler\nexport * from './${handler.fileName}.ts';\n`;
173
+ const aliasCode = `// Auto-generated alias for .do handler\nexport * from './${path.basename(handler.fileName)}.ts';\n`;
172
174
  const aliasProxyDir = path.dirname(aliasProxyFile);
173
175
  if (!fs.existsSync(aliasProxyDir))
174
176
  fs.mkdirSync(aliasProxyDir, { recursive: true });
@@ -285,6 +287,7 @@ export async function compile(options) {
285
287
  assetsPrefix,
286
288
  runtimeContextImport: RUNTIME_CONTEXT_IMPORT,
287
289
  runtimeDoImport: RUNTIME_DO_IMPORT,
290
+ runtimeSchemaImport: RUNTIME_SCHEMA_IMPORT,
288
291
  runtimeWorkerImport: RUNTIME_WORKER_IMPORT,
289
292
  });
290
293
  // Write to .kuratchi/routes.ts
@@ -308,12 +311,26 @@ export async function compile(options) {
308
311
  }));
309
312
  writeIfChanged(path.join(outDir, 'worker.js'), buildCompatEntrypointSource('./worker.ts'));
310
313
  // Auto-sync wrangler.jsonc with workflow/container/DO config from kuratchi.config.ts
314
+ // Also sync the static assets directory when src/assets/ exists, so Cloudflare Workers
315
+ // serves them natively without any manual wrangler.jsonc edits from the user.
316
+ const srcAssetsDir = path.join(srcDir, 'assets');
317
+ let syncedAssetsDirectory;
318
+ if (fs.existsSync(srcAssetsDir)) {
319
+ // Mirror src/assets/ into .kuratchi/public/<prefix>/ so Cloudflare serves them at the
320
+ // correct URL (e.g. /assets/app.css) — the directory passed to wrangler is the parent.
321
+ const prefixSegment = assetsPrefix.replace(/^\/|\/$/g, ''); // '/assets/' -> 'assets'
322
+ const publicDir = path.join(projectDir, '.kuratchi', 'public');
323
+ const publicAssetsDir = prefixSegment ? path.join(publicDir, prefixSegment) : publicDir;
324
+ copyDirIfChanged(srcAssetsDir, publicAssetsDir);
325
+ syncedAssetsDirectory = path.relative(projectDir, publicDir).replace(/\\/g, '/');
326
+ }
311
327
  syncWranglerConfigPipeline({
312
328
  projectDir,
313
329
  config: {
314
330
  workflows: workflowConfig,
315
331
  containers: containerConfig,
316
332
  durableObjects: doConfig,
333
+ assetsDirectory: syncedAssetsDirectory,
317
334
  },
318
335
  writeFile: writeIfChanged,
319
336
  });
@@ -332,3 +349,25 @@ function writeIfChanged(filePath, content) {
332
349
  }
333
350
  fs.writeFileSync(filePath, content, 'utf-8');
334
351
  }
352
+ /**
353
+ * Recursively copy files from src to dest, skipping files whose content is already identical.
354
+ * Used to mirror src/assets/ into .kuratchi/public/ for Cloudflare Workers Static Assets.
355
+ */
356
+ function copyDirIfChanged(src, dest) {
357
+ if (!fs.existsSync(dest))
358
+ fs.mkdirSync(dest, { recursive: true });
359
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
360
+ const srcPath = path.join(src, entry.name);
361
+ const destPath = path.join(dest, entry.name);
362
+ if (entry.isDirectory()) {
363
+ copyDirIfChanged(srcPath, destPath);
364
+ }
365
+ else {
366
+ const srcBuf = fs.readFileSync(srcPath);
367
+ const destBuf = fs.existsSync(destPath) ? fs.readFileSync(destPath) : null;
368
+ if (!destBuf || !srcBuf.equals(destBuf)) {
369
+ fs.writeFileSync(destPath, srcBuf);
370
+ }
371
+ }
372
+ }
373
+ }
@@ -42,10 +42,38 @@ export function compilePageRoute(opts) {
42
42
  query.rpcId = rpcNameMap.get(query.fnName);
43
43
  }
44
44
  const clientRouteRegistry = opts.clientModuleCompiler.createRouteRegistry(opts.routeIndex, opts.routeState.routeBrowserImportEntries);
45
+ const awaitQueryBindings = new Map(opts.routeState.mergedParsed.dataGetQueries
46
+ .filter((query) => !!query.awaitExpr)
47
+ .map((query) => [query.awaitExpr, { asName: query.asName, rpcId: query.rpcId || rpcNameMap.get(query.fnName) || query.fnName }]));
45
48
  const renderSections = splitTemplateRenderSections(opts.routeState.effectiveTemplate);
46
- const renderBody = compileTemplate(renderSections.bodyTemplate, routeComponentNames, actionNames, rpcNameMap, { emitCall: '__emit', enableFragmentManifest: true, clientRouteRegistry });
47
- const renderHeadBody = compileTemplate(renderSections.headTemplate, routeComponentNames, actionNames, rpcNameMap, { clientRouteRegistry });
49
+ const renderBody = compileTemplate(renderSections.bodyTemplate, routeComponentNames, actionNames, rpcNameMap, { emitCall: '__emit', enableFragmentManifest: true, clientRouteRegistry, awaitQueryBindings });
50
+ const renderHeadBody = compileTemplate(renderSections.headTemplate, routeComponentNames, actionNames, rpcNameMap, { clientRouteRegistry, awaitQueryBindings });
48
51
  const clientEntryAsset = clientRouteRegistry.buildEntryAsset();
52
+ const clientServerProxyBindings = clientRouteRegistry.getServerProxyBindings();
53
+ const clientServerProxyModules = new Map();
54
+ const extraRpcBindings = [];
55
+ const seenClientServerRpcIds = new Set();
56
+ for (const binding of clientServerProxyBindings) {
57
+ const moduleKey = `${binding.importerDir}::${binding.moduleSpecifier}`;
58
+ let moduleId = clientServerProxyModules.get(moduleKey);
59
+ if (!moduleId) {
60
+ moduleId = opts.allocateModuleId();
61
+ clientServerProxyModules.set(moduleKey, moduleId);
62
+ const importPath = opts.resolveCompiledImportPath(binding.moduleSpecifier, binding.importerDir, outFileDir);
63
+ opts.pushImport(`import * as ${moduleId} from '${importPath}';`);
64
+ }
65
+ if (!seenClientServerRpcIds.has(binding.rpcId)) {
66
+ seenClientServerRpcIds.add(binding.rpcId);
67
+ extraRpcBindings.push({
68
+ name: `${binding.moduleSpecifier}:${binding.importedName}`,
69
+ rpcId: binding.rpcId,
70
+ expression: binding.importedName === 'default' ? `${moduleId}.default` : `${moduleId}.${binding.importedName}`,
71
+ schemaExpression: binding.importedName === 'default'
72
+ ? 'undefined'
73
+ : `${moduleId}.schemas?.[${JSON.stringify(binding.importedName)}]`,
74
+ });
75
+ }
76
+ }
49
77
  const clientModuleHref = clientEntryAsset ? `${opts.assetsPrefix}${clientEntryAsset.assetName}` : null;
50
78
  const routePlan = analyzeRouteBuild({
51
79
  pattern: opts.pattern,
@@ -55,6 +83,7 @@ export function compilePageRoute(opts) {
55
83
  parsed: opts.routeState.mergedParsed,
56
84
  fnToModule,
57
85
  rpcNameMap,
86
+ extraRpcBindings,
58
87
  componentStyles: opts.componentCompiler.collectStyles(routeComponentNames),
59
88
  clientModuleHref,
60
89
  });
@@ -33,6 +33,7 @@ export interface ParsedFile {
33
33
  asName: string;
34
34
  key?: string;
35
35
  rpcId?: string;
36
+ awaitExpr?: string;
36
37
  }>;
37
38
  /** Imports found in a top-level client script block */
38
39
  clientImports: string[];
@@ -451,10 +451,37 @@ function extractCallExpression(value) {
451
451
  const match = expr.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
452
452
  if (!match)
453
453
  return null;
454
- return {
455
- fnName: match[1],
456
- argsExpr: (match[2] || '').trim(),
457
- };
454
+ return { fnName: match[1], argsExpr: (match[2] || '').trim() };
455
+ }
456
+ function extractAwaitCallExpression(expr) {
457
+ const match = expr.trim().match(/^await\s+([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
458
+ if (!match)
459
+ return null;
460
+ return { fnName: match[1], argsExpr: (match[2] || '').trim() };
461
+ }
462
+ function collectAwaitTemplateQueries(template) {
463
+ const source = template.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
464
+ const queries = [];
465
+ const seen = new Map();
466
+ for (let i = 0; i < source.length; i++) {
467
+ if (source[i] !== '{')
468
+ continue;
469
+ const closeIdx = findMatchingToken(source, i, '{', '}');
470
+ if (closeIdx === -1)
471
+ continue;
472
+ const inner = source.slice(i + 1, closeIdx).trim();
473
+ const call = extractAwaitCallExpression(inner);
474
+ if (call) {
475
+ const awaitExpr = `${call.fnName}(${call.argsExpr})`;
476
+ if (!seen.has(awaitExpr)) {
477
+ const asName = `__await_query_${queries.length}`;
478
+ seen.set(awaitExpr, asName);
479
+ queries.push({ fnName: call.fnName, argsExpr: call.argsExpr, asName, awaitExpr });
480
+ }
481
+ }
482
+ i = closeIdx;
483
+ }
484
+ return queries;
458
485
  }
459
486
  function extractTopLevelImportNames(source) {
460
487
  const sourceFile = ts.createSourceFile('kuratchi-inline-client.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
@@ -1144,6 +1171,22 @@ export function parseFile(source, options = {}) {
1144
1171
  if (!exists)
1145
1172
  dataGetQueries.push({ fnName: getCall.fnName, argsExpr: getCall.argsExpr, asName, key });
1146
1173
  }
1174
+ for (const awaitQuery of collectAwaitTemplateQueries(templateWithoutComments)) {
1175
+ if (!pollFunctions.includes(awaitQuery.fnName))
1176
+ pollFunctions.push(awaitQuery.fnName);
1177
+ if (!dataVars.includes(awaitQuery.asName))
1178
+ dataVars.push(awaitQuery.asName);
1179
+ const exists = dataGetQueries.some((query) => query.awaitExpr === awaitQuery.awaitExpr);
1180
+ if (!exists) {
1181
+ dataGetQueries.push({
1182
+ fnName: awaitQuery.fnName,
1183
+ argsExpr: awaitQuery.argsExpr,
1184
+ asName: awaitQuery.asName,
1185
+ key: awaitQuery.asName,
1186
+ awaitExpr: awaitQuery.awaitExpr,
1187
+ });
1188
+ }
1189
+ }
1147
1190
  for (const clientBinding of routeClientImportBindings) {
1148
1191
  const idx = actionFunctions.indexOf(clientBinding);
1149
1192
  if (idx !== -1)
@@ -206,6 +206,20 @@ const BRIDGE_SOURCE = `(function(){
206
206
  if(!keys.length){ location.reload(); return Promise.resolve(); }
207
207
  return Promise.all(keys.map(function(k){ return replaceBlocksWithKey('key:' + k); })).then(function(){});
208
208
  }
209
+ function refreshRemoteReads(){
210
+ var seen = Object.create(null);
211
+ var tasks = [];
212
+ by('[data-remote-read][data-get]').forEach(function(el){
213
+ var fn = el.getAttribute('data-get');
214
+ if(!fn) return;
215
+ var args = String(el.getAttribute('data-get-args') || '[]');
216
+ var key = String(fn) + '|' + args;
217
+ if(seen[key]) return;
218
+ seen[key] = true;
219
+ tasks.push(replaceBlocksByDescriptor(fn, args));
220
+ });
221
+ return Promise.all(tasks).then(function(){});
222
+ }
209
223
  function act(e){
210
224
  var clientSel = '[data-client-event="' + e.type + '"]';
211
225
  var clientEl = e.target && e.target.closest ? e.target.closest(clientSel) : null;
@@ -301,6 +315,9 @@ const BRIDGE_SOURCE = `(function(){
301
315
  } else {
302
316
  autoLoadQueries();
303
317
  }
318
+ window.addEventListener('kuratchi:invalidate-reads', function(){
319
+ refreshRemoteReads().catch(function(err){ console.error('[kuratchi] remote read refresh error:', err); });
320
+ });
304
321
  document.addEventListener('click', function(e){
305
322
  var b = e.target && e.target.closest ? e.target.closest('[command="fill-dialog"]') : null;
306
323
  if(!b) return;
@@ -356,12 +373,10 @@ const BRIDGE_SOURCE = `(function(){
356
373
  fetch(location.pathname + location.search, { headers: { 'x-kuratchi-fragment': pollId } })
357
374
  .then(function(r){
358
375
  if(r.status === 404){
359
- // Fragment no longer exists (e.g., item completed and removed from list)
360
- // Remove the element and stop polling
361
376
  el.remove();
362
377
  return null;
363
378
  }
364
- if(!r.ok) return null;
379
+ if(!r.ok) throw new Error('Poll fragment request failed: ' + r.status);
365
380
  return r.text();
366
381
  })
367
382
  .then(function(html){
@@ -15,6 +15,7 @@ export interface RouteRpcBinding {
15
15
  name: string;
16
16
  rpcId: string;
17
17
  expression: string;
18
+ schemaExpression: string;
18
19
  }
19
20
  export interface RouteLoadPlan {
20
21
  mode: 'none' | 'explicit' | 'generated';
@@ -49,6 +50,7 @@ interface AnalyzeRouteOptions {
49
50
  parsed: RoutePipelineParsedFile;
50
51
  fnToModule: Record<string, string>;
51
52
  rpcNameMap?: Map<string, string>;
53
+ extraRpcBindings?: RouteRpcBinding[];
52
54
  componentStyles: string[];
53
55
  clientModuleHref?: string | null;
54
56
  }
@@ -20,7 +20,7 @@ function buildLoadQueryStateCode(opts) {
20
20
  lines.push(`let ${asName} = { state: 'loading', loading: true, error: null, data: null, empty: false, success: false };`);
21
21
  lines.push(`const __qOverride_${asName} = __getLocals().__queryOverride;`);
22
22
  lines.push(`const __qArgs_${asName} = ${defaultArgs};`);
23
- lines.push(`const __qShouldRun_${asName} = !!(__qOverride_${asName} && __qOverride_${asName}.fn === '${rpcId}' && Array.isArray(__qOverride_${asName}.args) && JSON.stringify(__qOverride_${asName}.args) === JSON.stringify(__qArgs_${asName}));`);
23
+ lines.push(`const __qShouldRun_${asName} = !__qOverride_${asName} || (__qOverride_${asName}.fn === '${rpcId}' && Array.isArray(__qOverride_${asName}.args) && JSON.stringify(__qOverride_${asName}.args) === JSON.stringify(__qArgs_${asName}));`);
24
24
  lines.push(`if (__qShouldRun_${asName}) {`);
25
25
  lines.push(` try {`);
26
26
  lines.push(` const __qData_${asName} = await ${qualifiedFn}(...__qArgs_${asName});`);
@@ -83,7 +83,7 @@ function assertRoutePlanInvariants(opts) {
83
83
  }
84
84
  }
85
85
  export function analyzeRouteBuild(opts) {
86
- const { pattern, renderBody, renderHeadBody, isDev, parsed, fnToModule, rpcNameMap, componentStyles, clientModuleHref } = opts;
86
+ const { pattern, renderBody, renderHeadBody, isDev, parsed, fnToModule, rpcNameMap, extraRpcBindings, componentStyles, clientModuleHref } = opts;
87
87
  const hasFns = Object.keys(fnToModule).length > 0;
88
88
  const queryDefs = parsed.dataGetQueries ?? [];
89
89
  const queryVars = queryDefs.map((query) => query.asName);
@@ -199,9 +199,21 @@ export function analyzeRouteBuild(opts) {
199
199
  ? parsed.pollFunctions.map((name) => {
200
200
  const moduleId = fnToModule[name];
201
201
  const rpcId = rpcNameMap?.get(name) || name;
202
- return { name, rpcId, expression: moduleId ? `${moduleId}.${name}` : name };
202
+ const schemaExpression = moduleId
203
+ ? `${moduleId}.schemas?.[${JSON.stringify(name)}]`
204
+ : `(typeof schemas !== 'undefined' ? schemas?.[${JSON.stringify(name)}] : undefined)`;
205
+ return { name, rpcId, expression: moduleId ? `${moduleId}.${name}` : name, schemaExpression };
203
206
  })
204
207
  : [];
208
+ if (extraRpcBindings?.length) {
209
+ const seenRpcIds = new Set(rpc.map((binding) => binding.rpcId));
210
+ for (const binding of extraRpcBindings) {
211
+ if (seenRpcIds.has(binding.rpcId))
212
+ continue;
213
+ seenRpcIds.add(binding.rpcId);
214
+ rpc.push(binding);
215
+ }
216
+ }
205
217
  assertRoutePlanInvariants({
206
218
  pattern,
207
219
  loadReturnVars: loadPlan.returnVars,
@@ -250,6 +262,10 @@ export function emitRouteObject(plan) {
250
262
  .map((rpc) => `'${rpc.rpcId}': ${rpc.expression}`)
251
263
  .join(', ');
252
264
  parts.push(` rpc: { ${rpcEntries} }`);
265
+ const rpcSchemaEntries = plan.rpc
266
+ .map((rpc) => `'${rpc.rpcId}': ${rpc.schemaExpression}`)
267
+ .join(', ');
268
+ parts.push(` rpcSchemas: { ${rpcSchemaEntries} }`);
253
269
  // Also emit allowedQueries for query override validation
254
270
  const allowedQueryNames = plan.rpc.map((rpc) => `'${rpc.rpcId}'`).join(', ');
255
271
  parts.push(` allowedQueries: [${allowedQueryNames}]`);
@@ -2,7 +2,7 @@ import * as path from 'node:path';
2
2
  import { toSafeIdentifier } from './compiler-shared.js';
3
3
  export function buildRoutesModuleFeatureBlocks(opts) {
4
4
  const workerImport = `import { env as __env } from 'cloudflare:workers';`;
5
- const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, __getCsrfToken, __signFragment, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${opts.runtimeContextImport}';`;
5
+ const contextImport = `import { __setRequestContext, __pushRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, __getCsrfToken, __signFragment, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${opts.runtimeContextImport}';`;
6
6
  const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
7
7
  ? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
8
8
  : '';
@@ -182,7 +182,10 @@ function buildDurableObjectBlock(opts) {
182
182
  doImportLines.push(`import { DurableObject as __DO } from 'cloudflare:workers';`);
183
183
  doImportLines.push(`import { initDO as __initDO } from '@kuratchi/orm';`);
184
184
  doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding, __setDoContext } from '${opts.runtimeDoImport}';`);
185
+ doImportLines.push(`import { validateSchemaInput as __validateSchemaInput } from '${opts.runtimeSchemaImport}';`);
185
186
  doImportLines.push(`const __DO_FD_TAG = '__kuratchi_form_data__';`);
187
+ doImportLines.push(`const __DO_RPC_CTX_TAG = '__kuratchi_rpc_context__';`);
188
+ doImportLines.push(`const __DO_RPC_RESULT_TAG = '__kuratchi_rpc_result__';`);
186
189
  doImportLines.push(`function __isDoPlainObject(__v) {`);
187
190
  doImportLines.push(` if (!__v || typeof __v !== 'object') return false;`);
188
191
  doImportLines.push(` const __proto = Object.getPrototypeOf(__v);`);
@@ -203,6 +206,38 @@ function buildDurableObjectBlock(opts) {
203
206
  doImportLines.push(` }`);
204
207
  doImportLines.push(` return __v;`);
205
208
  doImportLines.push(`}`);
209
+ doImportLines.push(`function __extractDoRpcContext(__args) {`);
210
+ doImportLines.push(` if (!Array.isArray(__args) || __args.length === 0) return { args: __args, context: null };`);
211
+ doImportLines.push(` const __tail = __args[__args.length - 1];`);
212
+ doImportLines.push(` if (!__isDoPlainObject(__tail) || !(__DO_RPC_CTX_TAG in __tail)) return { args: __args, context: null };`);
213
+ doImportLines.push(` return { args: __args.slice(0, -1), context: __tail[__DO_RPC_CTX_TAG] || null };`);
214
+ doImportLines.push(`}`);
215
+ doImportLines.push(`function __wrapDoRpcResult(__value) {`);
216
+ doImportLines.push(` const __locals = __getLocals();`);
217
+ doImportLines.push(` return { [__DO_RPC_RESULT_TAG]: { value: __value, effects: { redirectTo: __locals.__redirectTo ?? null, redirectStatus: __locals.__redirectStatus ?? null, setCookieHeaders: Array.isArray(__locals.__setCookieHeaders) ? __locals.__setCookieHeaders : [] } } };`);
218
+ doImportLines.push(`}`);
219
+ doImportLines.push(`function __invokeDoRpc(__self, __methodName, __fn, __args) {`);
220
+ doImportLines.push(` __setDoContext(__self);`);
221
+ doImportLines.push(` const { args: __callArgs, context: __rpcContext } = __extractDoRpcContext(__args);`);
222
+ doImportLines.push(` const __decoded = (__callArgs ?? []).map(__decodeDoArg);`);
223
+ doImportLines.push(` const __schema = __self?.constructor?.schemas?.[__methodName];`);
224
+ doImportLines.push(` const __validated = __validateSchemaInput(__schema, __decoded);`);
225
+ doImportLines.push(` if (!__rpcContext) return __fn.apply(__self, __validated);`);
226
+ doImportLines.push(` const __restore = __pushRequestContext(__rpcContext, __self.ctx, __self.env);`);
227
+ doImportLines.push(` const __finish = (__value) => __wrapDoRpcResult(__value);`);
228
+ doImportLines.push(` const __fail = (__err) => { if (__err && __err.isRedirectError) return __wrapDoRpcResult(undefined); throw __err; };`);
229
+ doImportLines.push(` try {`);
230
+ doImportLines.push(` const __result = __fn.apply(__self, __validated);`);
231
+ doImportLines.push(` if (__result && typeof __result.then === 'function') {`);
232
+ doImportLines.push(` return __result.then(__finish, __fail).finally(__restore);`);
233
+ doImportLines.push(` }`);
234
+ doImportLines.push(` const __wrapped = __finish(__result);`);
235
+ doImportLines.push(` __restore();`);
236
+ doImportLines.push(` return __wrapped;`);
237
+ doImportLines.push(` } catch (__err) {`);
238
+ doImportLines.push(` try { return __fail(__err); } finally { __restore(); }`);
239
+ doImportLines.push(` }`);
240
+ doImportLines.push(`}`);
206
241
  doImportLines.push(`import { getCurrentUser as __getCU, getOrgStubByName as __getOSBN } from '@kuratchi/auth';`);
207
242
  const handlersByBinding = new Map();
208
243
  for (const handler of opts.doHandlers) {
@@ -213,6 +248,10 @@ function buildDurableObjectBlock(opts) {
213
248
  for (const doEntry of opts.doConfig) {
214
249
  const handlers = handlersByBinding.get(doEntry.binding) ?? [];
215
250
  const ormDb = opts.ormDatabases.find((db) => db.binding === doEntry.binding);
251
+ const fnHandlers = handlers.filter((h) => h.mode === 'function');
252
+ const initHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onInit'));
253
+ const alarmHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onAlarm'));
254
+ const messageHandlers = fnHandlers.filter((h) => h.exportedFunctions.includes('onMessage'));
216
255
  if (ormDb) {
217
256
  const schemaPath = ormDb.schemaImportPath.replace(/^\.\//, '../');
218
257
  doImportLines.push(`import { ${ormDb.schemaExportName} as __doSchema_${doEntry.binding} } from '${schemaPath}';`);
@@ -225,22 +264,114 @@ function buildDurableObjectBlock(opts) {
225
264
  if (!handlerImportPath.startsWith('.'))
226
265
  handlerImportPath = './' + handlerImportPath;
227
266
  const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
228
- doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
267
+ if (handler.mode === 'class') {
268
+ if (handler.exportKind === 'named' && handler.className) {
269
+ doImportLines.push(`import { ${handler.className} as ${handlerVar} } from '${handlerImportPath}';`);
270
+ }
271
+ else {
272
+ doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
273
+ }
274
+ for (const [index, contributor] of (handler.classContributors ?? []).entries()) {
275
+ let contributorImportPath = path
276
+ .relative(path.join(opts.projectDir, '.kuratchi'), contributor.absPath)
277
+ .replace(/\\/g, '/')
278
+ .replace(/\.ts$/, '.js');
279
+ if (!contributorImportPath.startsWith('.'))
280
+ contributorImportPath = './' + contributorImportPath;
281
+ const contributorVar = `__handler_${toSafeIdentifier(`${handler.fileName}__${contributor.className}_${index}`)}`;
282
+ if (contributor.exportKind === 'named') {
283
+ doImportLines.push(`import { ${contributor.className} as ${contributorVar} } from '${contributorImportPath}';`);
284
+ }
285
+ else {
286
+ doImportLines.push(`import ${contributorVar} from '${contributorImportPath}';`);
287
+ }
288
+ }
289
+ }
290
+ else {
291
+ doImportLines.push(`import * as ${handlerVar} from '${handlerImportPath}';`);
292
+ }
229
293
  }
294
+ // Generate the DO class
295
+ doClassLines.push(`export class ${doEntry.className} extends __DO {`);
296
+ doClassLines.push(` constructor(ctx, env) {`);
297
+ doClassLines.push(` super(ctx, env);`);
230
298
  if (ormDb) {
231
- const handler = handlers[0];
232
- const handlerVar = handler ? `__handler_${toSafeIdentifier(handler.fileName)}` : '__DO';
233
- const baseClass = handler ? handlerVar : '__DO';
234
- doClassLines.push(...buildOrmDurableObjectClassLines(doEntry.className, doEntry.binding, baseClass));
299
+ doClassLines.push(` this.db = __initDO(ctx.storage.sql, __doSchema_${doEntry.binding});`);
300
+ }
301
+ for (const handler of handlers.filter((h) => h.mode === 'class')) {
302
+ const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
303
+ const handlerInstanceVar = `__instance_${toSafeIdentifier(handler.fileName)}`;
304
+ doClassLines.push(` const ${handlerInstanceVar} = new ${handlerVar}(ctx, env);`);
305
+ doClassLines.push(` Object.assign(this, ${handlerInstanceVar});`);
235
306
  }
236
- else if (handlers.length > 0) {
237
- const handler = handlers[0];
307
+ for (const handler of initHandlers) {
238
308
  const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
239
- doClassLines.push(`export { ${handlerVar} as ${doEntry.className} };`);
309
+ doClassLines.push(` __setDoContext(this);`);
310
+ doClassLines.push(` Promise.resolve(${handlerVar}.onInit.call(this)).catch((err) => console.error('[KuratchiJS] DO onInit failed:', err?.message || err));`);
311
+ }
312
+ doClassLines.push(` }`);
313
+ if (ormDb) {
314
+ doClassLines.push(...buildOrmDoActivityLogLines(doEntry.binding));
240
315
  }
316
+ if (alarmHandlers.length > 0) {
317
+ doClassLines.push(` async alarm(...args) {`);
318
+ doClassLines.push(` __setDoContext(this);`);
319
+ for (const handler of alarmHandlers) {
320
+ const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
321
+ doClassLines.push(` await ${handlerVar}.onAlarm.call(this, ...args);`);
322
+ }
323
+ doClassLines.push(` }`);
324
+ }
325
+ if (messageHandlers.length > 0) {
326
+ doClassLines.push(` webSocketMessage(...args) {`);
327
+ doClassLines.push(` __setDoContext(this);`);
328
+ for (const handler of messageHandlers) {
329
+ const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
330
+ doClassLines.push(` ${handlerVar}.onMessage.call(this, ...args);`);
331
+ }
332
+ doClassLines.push(` }`);
333
+ }
334
+ doClassLines.push(` static schemas = {};`);
335
+ doClassLines.push(`}`);
336
+ // Apply prototype methods from each class handler (and its contributors) onto the generated class
241
337
  for (const handler of handlers) {
242
338
  const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
243
- doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
339
+ if (handler.mode === 'class') {
340
+ const classSourceVars = [
341
+ handlerVar,
342
+ ...(handler.classContributors ?? []).map((c, i) => `__handler_${toSafeIdentifier(`${handler.fileName}__${c.className}_${i}`)}`),
343
+ ];
344
+ doClassLines.push(`{`);
345
+ doClassLines.push(` for (const __source of [${classSourceVars.join(', ')}]) {`);
346
+ doClassLines.push(` const __seen = new Set();`);
347
+ doClassLines.push(` for (let __p = __source.prototype; __p && __p !== __DO.prototype && __p !== Object.prototype; __p = Object.getPrototypeOf(__p)) {`);
348
+ doClassLines.push(` for (const __k of Object.getOwnPropertyNames(__p)) {`);
349
+ doClassLines.push(` if (__k === 'constructor' || __seen.has(__k)) continue;`);
350
+ doClassLines.push(` const __desc = Object.getOwnPropertyDescriptor(__p, __k);`);
351
+ doClassLines.push(` const __fn = __desc?.value;`);
352
+ doClassLines.push(` if (typeof __fn !== 'function') continue;`);
353
+ doClassLines.push(` __seen.add(__k);`);
354
+ doClassLines.push(` ${doEntry.className}.prototype[__k] = function(...__a){ return __invokeDoRpc(this, __k, __fn, __a); };`);
355
+ doClassLines.push(` }`);
356
+ doClassLines.push(` }`);
357
+ doClassLines.push(` }`);
358
+ doClassLines.push(`}`);
359
+ doClassLines.push(`Object.assign(${doEntry.className}.schemas, ${handlerVar}.schemas || {});`);
360
+ for (const [index] of (handler.classContributors ?? []).entries()) {
361
+ const contributorVar = `__handler_${toSafeIdentifier(`${handler.fileName}__${handler.classContributors[index].className}_${index}`)}`;
362
+ doClassLines.push(`Object.assign(${doEntry.className}.schemas, ${contributorVar}.schemas || {});`);
363
+ }
364
+ doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
365
+ }
366
+ else {
367
+ const lifecycle = new Set(['onInit', 'onAlarm', 'onMessage']);
368
+ for (const fn of handler.exportedFunctions) {
369
+ if (lifecycle.has(fn))
370
+ continue;
371
+ doClassLines.push(`${doEntry.className}.prototype[${JSON.stringify(fn)}] = function(...__a){ return __invokeDoRpc(this, ${JSON.stringify(fn)}, ${handlerVar}.${fn}, __a); };`);
372
+ }
373
+ doClassLines.push(`Object.assign(${doEntry.className}.schemas, ${handlerVar}.schemas || {});`);
374
+ }
244
375
  }
245
376
  if (doEntry.stubId) {
246
377
  const fieldPath = doEntry.stubId.startsWith('user.') ? `__u.${doEntry.stubId.slice(5)}` : doEntry.stubId;
@@ -261,13 +392,8 @@ function buildDurableObjectBlock(opts) {
261
392
  doResolverInit: `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`,
262
393
  };
263
394
  }
264
- function buildOrmDurableObjectClassLines(className, binding, baseClass) {
395
+ function buildOrmDoActivityLogLines(binding) {
265
396
  return [
266
- `export class ${className} extends ${baseClass} {`,
267
- ` constructor(ctx, env) {`,
268
- ` super(ctx, env);`,
269
- ` this.db = __initDO(ctx.storage.sql, __doSchema_${binding});`,
270
- ` }`,
271
397
  ` async __kuratchiLogActivity(payload) {`,
272
398
  ` const now = new Date().toISOString();`,
273
399
  ` try {`,
@@ -302,8 +428,8 @@ function buildOrmDurableObjectClassLines(className, binding, baseClass) {
302
428
  ` if (Number.isFinite(limit) && limit > 0) return rows.slice(0, Math.floor(limit));`,
303
429
  ` return rows;`,
304
430
  ` }`,
305
- `}`,
306
431
  ];
432
+ void binding;
307
433
  }
308
434
  function buildWorkflowStatusRpc(opts) {
309
435
  if (opts.workflowConfig.length === 0)
@@ -27,6 +27,7 @@ export interface GenerateRoutesModuleOptions {
27
27
  assetsPrefix: string;
28
28
  runtimeContextImport: string;
29
29
  runtimeDoImport: string;
30
+ runtimeSchemaImport: string;
30
31
  runtimeWorkerImport: string;
31
32
  }
32
33
  export interface RoutesModuleFeatureBlocks {
@@ -37,6 +37,12 @@ export function createServerModuleCompiler(options) {
37
37
  return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.ts') ? proxyNoExt + '.ts' : null);
38
38
  }
39
39
  function resolveImportTarget(importerAbs, spec) {
40
+ // Handle kuratchi:* virtual modules
41
+ if (spec.startsWith('kuratchi:')) {
42
+ // These are resolved at bundle time to @kuratchi/js runtime modules
43
+ // Return null to keep the specifier as-is for later rewriting
44
+ return null;
45
+ }
40
46
  if (spec.startsWith('$')) {
41
47
  const slashIdx = spec.indexOf('/');
42
48
  const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
@@ -70,6 +76,15 @@ export function createServerModuleCompiler(options) {
70
76
  }
71
77
  const source = fs.readFileSync(resolved, 'utf-8');
72
78
  const rewriteSpecifier = (spec) => {
79
+ // Rewrite kuratchi:* virtual modules to @kuratchi/js runtime paths
80
+ if (spec.startsWith('kuratchi:')) {
81
+ const moduleName = spec.slice('kuratchi:'.length);
82
+ const moduleMap = {
83
+ 'request': '@kuratchi/js/request',
84
+ 'navigation': '@kuratchi/js/navigation',
85
+ };
86
+ return moduleMap[moduleName] ?? spec;
87
+ }
73
88
  const target = resolveImportTarget(resolved, spec);
74
89
  if (!target)
75
90
  return spec;
@@ -96,6 +111,15 @@ export function createServerModuleCompiler(options) {
96
111
  return outPath;
97
112
  }
98
113
  function resolveCompiledImportPath(origPath, importerDir, outFileDir) {
114
+ // Rewrite kuratchi:* virtual modules to @kuratchi/js runtime paths
115
+ if (origPath.startsWith('kuratchi:')) {
116
+ const moduleName = origPath.slice('kuratchi:'.length);
117
+ const moduleMap = {
118
+ 'request': '@kuratchi/js/request',
119
+ 'navigation': '@kuratchi/js/navigation',
120
+ };
121
+ return moduleMap[moduleName] ?? origPath;
122
+ }
99
123
  const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
100
124
  if (isBareModule)
101
125
  return origPath;
@@ -28,6 +28,10 @@ export interface CompileTemplateOptions {
28
28
  enableFragmentManifest?: boolean;
29
29
  appendNewline?: boolean;
30
30
  clientRouteRegistry?: ClientRouteRegistry;
31
+ awaitQueryBindings?: Map<string, {
32
+ asName: string;
33
+ rpcId: string;
34
+ }>;
31
35
  }
32
36
  export declare function splitTemplateRenderSections(template: string): TemplateRenderSections;
33
37
  /**