@kuratchi/js 0.0.1 → 0.0.3

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.
@@ -17,10 +17,10 @@ function getFrameworkPackageName() {
17
17
  try {
18
18
  const raw = fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf-8');
19
19
  const parsed = JSON.parse(raw);
20
- return parsed.name || 'KuratchiJS';
20
+ return parsed.name || '@kuratchi/js';
21
21
  }
22
22
  catch {
23
- return 'KuratchiJS';
23
+ return '@kuratchi/js';
24
24
  }
25
25
  }
26
26
  function compactInlineJs(source) {
@@ -36,7 +36,9 @@ function compactInlineJs(source) {
36
36
  *
37
37
  * The generated module exports { app } — an object with a fetch() method
38
38
  * that handles routing, load functions, form actions, and rendering.
39
- * The project's src/index.ts imports this and re-exports it as the Worker default.
39
+ * Returns the path to .kuratchi/worker.js the stable wrangler entry point that
40
+ * re-exports everything from routes.js (default fetch handler + named DO class exports).
41
+ * No src/index.ts is needed in user projects.
40
42
  */
41
43
  export function compile(options) {
42
44
  const { projectDir } = options;
@@ -182,8 +184,8 @@ export function compile(options) {
182
184
  function by(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
183
185
  var __refreshSeq = Object.create(null);
184
186
  function syncGroup(group){
185
- var items = by('[data-select-item="' + group + '"]');
186
- var masters = by('[data-select-all="' + group + '"]');
187
+ var items = by('[data-select-item]').filter(function(el){ return el.getAttribute('data-select-item') === group; });
188
+ var masters = by('[data-select-all]').filter(function(el){ return el.getAttribute('data-select-all') === group; });
187
189
  if(!items.length || !masters.length) return;
188
190
  var all = items.every(function(i){ return !!i.checked; });
189
191
  var any = items.some(function(i){ return !!i.checked; });
@@ -306,6 +308,7 @@ export function compile(options) {
306
308
  if(g && !g.hasAttribute('data-as') && !g.hasAttribute('data-action')){
307
309
  var getUrl = g.getAttribute('data-get');
308
310
  if(getUrl){
311
+ if(/^[a-z][a-z0-9+\-.]*:/i.test(getUrl) && !/^https?:/i.test(getUrl)) return;
309
312
  e.preventDefault();
310
313
  location.assign(getUrl);
311
314
  return;
@@ -446,7 +449,7 @@ export function compile(options) {
446
449
  if(!t || !t.getAttribute) return;
447
450
  var gAll = t.getAttribute('data-select-all');
448
451
  if(gAll){
449
- by('[data-select-item="' + gAll + '"]').forEach(function(i){ i.checked = !!t.checked; });
452
+ by('[data-select-item]').filter(function(i){ return i.getAttribute('data-select-item') === gAll; }).forEach(function(i){ i.checked = !!t.checked; });
450
453
  syncGroup(gAll);
451
454
  return;
452
455
  }
@@ -528,6 +531,8 @@ export function compile(options) {
528
531
  const authConfig = readAuthConfig(projectDir);
529
532
  // Read Durable Object config and discover handler files
530
533
  const doConfig = readDoConfig(projectDir);
534
+ const containerConfig = readWorkerClassConfig(projectDir, 'containers');
535
+ const workflowConfig = readWorkerClassConfig(projectDir, 'workflows');
531
536
  const doHandlers = doConfig.length > 0
532
537
  ? discoverDoHandlers(srcDir, doConfig, ormDatabases)
533
538
  : [];
@@ -864,6 +869,8 @@ export function compile(options) {
864
869
  // Collect only the components that were actually imported by routes
865
870
  const compiledComponents = Array.from(compiledComponentCache.values());
866
871
  // Generate the routes module
872
+ const runtimeImportPath = resolveRuntimeImportPath(projectDir);
873
+ const hasRuntime = !!runtimeImportPath;
867
874
  const output = generateRoutesModule({
868
875
  projectDir,
869
876
  serverImports: allImports,
@@ -879,6 +886,8 @@ export function compile(options) {
879
886
  isDev: options.isDev ?? false,
880
887
  isLayoutAsync,
881
888
  compiledLayoutActions,
889
+ hasRuntime,
890
+ runtimeImportPath: runtimeImportPath ?? undefined,
882
891
  });
883
892
  // Write to .kuratchi/routes.js
884
893
  const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
@@ -887,7 +896,28 @@ export function compile(options) {
887
896
  fs.mkdirSync(outDir, { recursive: true });
888
897
  }
889
898
  writeIfChanged(outFile, output);
890
- return outFile;
899
+ // Generate .kuratchi/worker.js — the stable wrangler entry point.
900
+ // routes.js already exports the default fetch handler and all named DO classes;
901
+ // worker.js explicitly re-exports them so wrangler.jsonc can reference a
902
+ // stable filename while routes.js is freely regenerated.
903
+ const workerFile = path.join(outDir, 'worker.js');
904
+ const workerClassExports = [...containerConfig, ...workflowConfig]
905
+ .map((entry) => {
906
+ const importPath = toWorkerImportPath(projectDir, outDir, entry.file);
907
+ if (entry.exportKind === 'default') {
908
+ return `export { default as ${entry.className} } from '${importPath}';`;
909
+ }
910
+ return `export { ${entry.className} } from '${importPath}';`;
911
+ });
912
+ const workerLines = [
913
+ '// Auto-generated by kuratchi \u2014 do not edit.',
914
+ "export { default } from './routes.js';",
915
+ ...doConfig.map(c => `export { ${c.className} } from './routes.js';`),
916
+ ...workerClassExports,
917
+ '',
918
+ ];
919
+ writeIfChanged(workerFile, workerLines.join('\n'));
920
+ return workerFile;
891
921
  }
892
922
  // ── Helpers ─────────────────────────────────────────────────────────────
893
923
  /**
@@ -1127,7 +1157,7 @@ function buildRouteObject(opts) {
1127
1157
  body = [body, queryLines.join('\n')].filter(Boolean).join('\n');
1128
1158
  }
1129
1159
  // Rewrite imported function calls: fnName( → __mN.fnName(
1130
- // No env injection — server functions use getEnv() from the framework context
1160
+ // Rewrite imported function calls to use the module namespace
1131
1161
  for (const [fnName, moduleId] of Object.entries(fnToModule)) {
1132
1162
  if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
1133
1163
  continue;
@@ -1350,6 +1380,89 @@ function readDoConfig(projectDir) {
1350
1380
  }
1351
1381
  return entries;
1352
1382
  }
1383
+ function readWorkerClassConfig(projectDir, key) {
1384
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
1385
+ if (!fs.existsSync(configPath))
1386
+ return [];
1387
+ const source = fs.readFileSync(configPath, 'utf-8');
1388
+ const keyIdx = source.search(new RegExp(`\\b${key}\\s*:\\s*\\{`));
1389
+ if (keyIdx === -1)
1390
+ return [];
1391
+ const braceStart = source.indexOf('{', keyIdx);
1392
+ if (braceStart === -1)
1393
+ return [];
1394
+ const body = extractBalancedBody(source, braceStart, '{', '}');
1395
+ if (body == null)
1396
+ return [];
1397
+ const entries = [];
1398
+ const expectedSuffix = key === 'containers' ? '.container' : '.workflow';
1399
+ const allowedExt = /\.(ts|js|mjs|cjs)$/i;
1400
+ const requiredFilePattern = new RegExp(`\\${expectedSuffix}\\.(ts|js|mjs|cjs)$`, 'i');
1401
+ const resolveClassFromFile = (binding, filePath) => {
1402
+ if (!requiredFilePattern.test(filePath)) {
1403
+ throw new Error(`[kuratchi] ${key}.${binding} must reference a file ending in "${expectedSuffix}.ts|js|mjs|cjs". Received: ${filePath}`);
1404
+ }
1405
+ if (!allowedExt.test(filePath)) {
1406
+ throw new Error(`[kuratchi] ${key}.${binding} file must be a TypeScript or JavaScript module. Received: ${filePath}`);
1407
+ }
1408
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
1409
+ if (!fs.existsSync(absPath)) {
1410
+ throw new Error(`[kuratchi] ${key}.${binding} file not found: ${filePath}`);
1411
+ }
1412
+ const fileSource = fs.readFileSync(absPath, 'utf-8');
1413
+ const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
1414
+ if (defaultClass) {
1415
+ return { className: defaultClass[1], exportKind: 'default' };
1416
+ }
1417
+ const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
1418
+ if (namedClass) {
1419
+ return { className: namedClass[1], exportKind: 'named' };
1420
+ }
1421
+ throw new Error(`[kuratchi] ${key}.${binding} must export a class via "export class X" or "export default class X". File: ${filePath}`);
1422
+ };
1423
+ // Object form:
1424
+ // containers: { WORDPRESS_CONTAINER: { file: 'src/server/containers/wordpress.container.ts', className?: 'WordPressContainer' } }
1425
+ // workflows: { NEW_SITE_WORKFLOW: { file: 'src/server/workflows/new-site.workflow.ts', className?: 'NewSiteWorkflow' } }
1426
+ const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
1427
+ let m;
1428
+ while ((m = objRegex.exec(body)) !== null) {
1429
+ const binding = m[1];
1430
+ const entryBody = m[2];
1431
+ const fileMatch = entryBody.match(/file\s*:\s*['"]([^'"]+)['"]/);
1432
+ if (!fileMatch)
1433
+ continue;
1434
+ const inferred = resolveClassFromFile(binding, fileMatch[1]);
1435
+ const classMatch = entryBody.match(/className\s*:\s*['"](\w+)['"]/);
1436
+ const className = classMatch?.[1] ?? inferred.className;
1437
+ entries.push({
1438
+ binding,
1439
+ className,
1440
+ file: fileMatch[1],
1441
+ exportKind: inferred.exportKind,
1442
+ });
1443
+ }
1444
+ // String shorthand:
1445
+ // containers: { WORDPRESS_CONTAINER: 'src/server/containers/wordpress.container.ts' }
1446
+ // workflows: { NEW_SITE_WORKFLOW: 'src/server/workflows/new-site.workflow.ts' }
1447
+ const foundBindings = new Set(entries.map((e) => e.binding));
1448
+ const pairRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]\s*[,}\n]/g;
1449
+ while ((m = pairRegex.exec(body)) !== null) {
1450
+ const binding = m[1];
1451
+ const file = m[2];
1452
+ if (foundBindings.has(binding))
1453
+ continue;
1454
+ if (binding === 'file' || binding === 'className')
1455
+ continue;
1456
+ const inferred = resolveClassFromFile(binding, file);
1457
+ entries.push({
1458
+ binding,
1459
+ className: inferred.className,
1460
+ file,
1461
+ exportKind: inferred.exportKind,
1462
+ });
1463
+ }
1464
+ return entries;
1465
+ }
1353
1466
  function discoverFilesWithSuffix(dir, suffix) {
1354
1467
  if (!fs.existsSync(dir))
1355
1468
  return [];
@@ -1650,7 +1763,10 @@ function generateRoutesModule(opts) {
1650
1763
  .map(([status, fn]) => fn)
1651
1764
  .join('\n\n');
1652
1765
  // Resolve path to the framework's context module from the output directory
1653
- const contextImport = `import { __setRequestContext, __setEnvCompat, __esc, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
1766
+ const contextImport = `import { __setRequestContext, __esc, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
1767
+ const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
1768
+ ? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
1769
+ : '';
1654
1770
  // Auth session init — thin cookie parsing injected into Worker entry
1655
1771
  let authInit = '';
1656
1772
  if (opts.authConfig && opts.authConfig.sessionEnabled) {
@@ -1916,7 +2032,7 @@ ${initLines.join('\n')}
1916
2032
  ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
1917
2033
  ${workerImport}
1918
2034
  ${contextImport}
1919
- ${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
2035
+ ${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
1920
2036
 
1921
2037
  // ── Assets ──────────────────────────────────────────────────────
1922
2038
 
@@ -2060,141 +2176,245 @@ ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
2060
2176
  return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
2061
2177
  }
2062
2178
 
2063
- // ── Exported Worker entrypoint ──────────────────────────────────
2064
-
2065
- export default class extends WorkerEntrypoint {
2066
- async fetch(request) {
2067
- __setRequestContext(this.ctx, request);
2068
- __setEnvCompat(this.env);
2069
- ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
2070
- const url = new URL(request.url);
2071
- ${ac?.hasRateLimit ? '\n // Rate limiting — check before route handlers\n { const __rlRes = await __checkRL(); if (__rlRes) return __secHeaders(__rlRes); }\n' : ''}${ac?.hasTurnstile ? ' // Turnstile bot protection\n { const __tsRes = await __checkTS(); if (__tsRes) return __secHeaders(__tsRes); }\n' : ''}${ac?.hasGuards ? ' // Route guards — redirect if not authenticated\n { const __gRes = __checkGuard(); if (__gRes) return __secHeaders(__gRes); }\n' : ''}
2179
+ const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
2180
+ const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
2072
2181
 
2073
- // Serve static assets from src/assets/ at /_assets/*
2074
- if (url.pathname.startsWith('/_assets/')) {
2075
- const name = url.pathname.slice('/_assets/'.length);
2076
- const asset = __assets[name];
2077
- if (asset) {
2078
- if (request.headers.get('if-none-match') === asset.etag) {
2079
- return new Response(null, { status: 304 });
2080
- }
2081
- return new Response(asset.content, {
2082
- headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2083
- });
2084
- }
2085
- return __secHeaders(new Response('Not Found', { status: 404 }));
2086
- }
2182
+ async function __runRuntimeRequest(ctx, next) {
2183
+ let idx = -1;
2184
+ async function __dispatch(i) {
2185
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
2186
+ idx = i;
2187
+ const entry = __runtimeEntries[i];
2188
+ if (!entry) return next();
2189
+ const [, step] = entry;
2190
+ if (typeof step.request !== 'function') return __dispatch(i + 1);
2191
+ return await step.request(ctx, () => __dispatch(i + 1));
2192
+ }
2193
+ return __dispatch(0);
2194
+ }
2087
2195
 
2088
- const match = __match(url.pathname);
2196
+ async function __runRuntimeRoute(ctx, next) {
2197
+ let idx = -1;
2198
+ async function __dispatch(i) {
2199
+ if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
2200
+ idx = i;
2201
+ const entry = __runtimeEntries[i];
2202
+ if (!entry) return next();
2203
+ const [, step] = entry;
2204
+ if (typeof step.route !== 'function') return __dispatch(i + 1);
2205
+ return await step.route(ctx, () => __dispatch(i + 1));
2206
+ }
2207
+ return __dispatch(0);
2208
+ }
2089
2209
 
2090
- if (!match) {
2091
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2210
+ async function __runRuntimeResponse(ctx, response) {
2211
+ let out = response;
2212
+ for (const [, step] of __runtimeEntries) {
2213
+ if (typeof step.response !== 'function') continue;
2214
+ out = await step.response(ctx, out);
2215
+ if (!(out instanceof Response)) {
2216
+ throw new Error('[kuratchi runtime] response handlers must return a Response');
2092
2217
  }
2218
+ }
2219
+ return out;
2220
+ }
2093
2221
 
2094
- const route = routes[match.index];
2095
- __setLocal('params', match.params);
2096
- const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
2097
- const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
2098
- let __qArgs = [];
2222
+ async function __runRuntimeError(ctx, error) {
2223
+ for (const [name, step] of __runtimeEntries) {
2224
+ if (typeof step.error !== 'function') continue;
2099
2225
  try {
2100
- const __parsed = JSON.parse(__qArgsRaw);
2101
- __qArgs = Array.isArray(__parsed) ? __parsed : [];
2102
- } catch {}
2103
- __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
2104
- if (!__getLocals().__breadcrumbs) {
2105
- __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
2226
+ const handled = await step.error(ctx, error);
2227
+ if (handled instanceof Response) return handled;
2228
+ } catch (hookErr) {
2229
+ console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
2106
2230
  }
2231
+ }
2232
+ return null;
2233
+ }
2107
2234
 
2108
- // RPC call: GET ?_rpc=fnName&_args=[...] → JSON response
2109
- const __rpcName = url.searchParams.get('_rpc');
2110
- if (request.method === 'GET' && __rpcName && route.rpc?.[__rpcName]) {
2111
- if (request.headers.get('x-kuratchi-rpc') !== '1') {
2112
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
2113
- status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2114
- }));
2115
- }
2116
- try {
2117
- const __rpcArgsStr = url.searchParams.get('_args');
2118
- let __rpcArgs = [];
2119
- if (__rpcArgsStr) {
2120
- const __parsed = JSON.parse(__rpcArgsStr);
2121
- __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
2235
+ // ── Exported Worker entrypoint ──────────────────────────────────
2236
+
2237
+ export default class extends WorkerEntrypoint {
2238
+ async fetch(request) {
2239
+ __setRequestContext(this.ctx, request);
2240
+ globalThis.__cloudflare_env__ = __env;
2241
+ ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
2242
+ const __runtimeCtx = {
2243
+ request,
2244
+ env: __env,
2245
+ ctx: this.ctx,
2246
+ url: new URL(request.url),
2247
+ params: {},
2248
+ locals: __getLocals(),
2249
+ };
2250
+
2251
+ const __coreFetch = async () => {
2252
+ const request = __runtimeCtx.request;
2253
+ const url = __runtimeCtx.url;
2254
+ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n { const __rlRes = await __checkRL(); if (__rlRes) return __secHeaders(__rlRes); }\n' : ''}${ac?.hasTurnstile ? ' // Turnstile bot protection\n { const __tsRes = await __checkTS(); if (__tsRes) return __secHeaders(__tsRes); }\n' : ''}${ac?.hasGuards ? ' // Route guards - redirect if not authenticated\n { const __gRes = __checkGuard(); if (__gRes) return __secHeaders(__gRes); }\n' : ''}
2255
+
2256
+ // Serve static assets from src/assets/ at /_assets/*
2257
+ if (url.pathname.startsWith('/_assets/')) {
2258
+ const name = url.pathname.slice('/_assets/'.length);
2259
+ const asset = __assets[name];
2260
+ if (asset) {
2261
+ if (request.headers.get('if-none-match') === asset.etag) {
2262
+ return new Response(null, { status: 304 });
2263
+ }
2264
+ return new Response(asset.content, {
2265
+ headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2266
+ });
2122
2267
  }
2123
- const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
2124
- return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
2125
- headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2126
- }));
2127
- } catch (err) {
2128
- console.error('[kuratchi] RPC error:', err);
2129
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2130
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2131
- status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2132
- }));
2268
+ return __secHeaders(new Response('Not Found', { status: 404 }));
2133
2269
  }
2134
- }
2135
2270
 
2136
- // Form action: POST with hidden _action field in form body
2137
- if (request.method === 'POST') {
2138
- if (!__isSameOrigin(request, url)) {
2139
- return __secHeaders(new Response('Forbidden', { status: 403 }));
2271
+ const match = __match(url.pathname);
2272
+
2273
+ if (!match) {
2274
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2140
2275
  }
2141
- const formData = await request.formData();
2142
- const actionName = formData.get('_action');
2143
- const __actionFn = route.actions?.[actionName] || __layoutActions[actionName];
2144
- if (actionName && __actionFn) {
2145
- // Check if this is a fetch-based action call (onclick) with JSON args
2146
- const argsStr = formData.get('_args');
2147
- const isFetchAction = argsStr !== null;
2276
+
2277
+ __runtimeCtx.params = match.params;
2278
+ const route = routes[match.index];
2279
+ __setLocal('params', match.params);
2280
+ const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
2281
+ const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
2282
+ let __qArgs = [];
2283
+ try {
2284
+ const __parsed = JSON.parse(__qArgsRaw);
2285
+ __qArgs = Array.isArray(__parsed) ? __parsed : [];
2286
+ } catch {}
2287
+ __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
2288
+ if (!__getLocals().__breadcrumbs) {
2289
+ __setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
2290
+ }
2291
+
2292
+ // RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
2293
+ const __rpcName = url.searchParams.get('_rpc');
2294
+ if (request.method === 'GET' && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
2295
+ if (request.headers.get('x-kuratchi-rpc') !== '1') {
2296
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
2297
+ status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2298
+ }));
2299
+ }
2148
2300
  try {
2149
- if (isFetchAction) {
2150
- const __parsed = JSON.parse(argsStr);
2151
- const args = Array.isArray(__parsed) ? __parsed : [];
2152
- await __actionFn(...args);
2153
- } else {
2154
- await __actionFn(formData);
2301
+ const __rpcArgsStr = url.searchParams.get('_args');
2302
+ let __rpcArgs = [];
2303
+ if (__rpcArgsStr) {
2304
+ const __parsed = JSON.parse(__rpcArgsStr);
2305
+ __rpcArgs = Array.isArray(__parsed) ? __parsed : [];
2155
2306
  }
2307
+ const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
2308
+ return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
2309
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2310
+ }));
2156
2311
  } catch (err) {
2157
- console.error('[kuratchi] Action error:', err);
2312
+ console.error('[kuratchi] RPC error:', err);
2313
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2314
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2315
+ status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2316
+ }));
2317
+ }
2318
+ }
2319
+
2320
+ // Form action: POST with hidden _action field in form body
2321
+ if (request.method === 'POST') {
2322
+ if (!__isSameOrigin(request, url)) {
2323
+ return __secHeaders(new Response('Forbidden', { status: 403 }));
2324
+ }
2325
+ const formData = await request.formData();
2326
+ const actionName = formData.get('_action');
2327
+ const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
2328
+ || (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
2329
+ if (actionName && __actionFn) {
2330
+ // Check if this is a fetch-based action call (onclick) with JSON args
2331
+ const argsStr = formData.get('_args');
2332
+ const isFetchAction = argsStr !== null;
2333
+ try {
2334
+ if (isFetchAction) {
2335
+ const __parsed = JSON.parse(argsStr);
2336
+ const args = Array.isArray(__parsed) ? __parsed : [];
2337
+ await __actionFn(...args);
2338
+ } else {
2339
+ await __actionFn(formData);
2340
+ }
2341
+ } catch (err) {
2342
+ console.error('[kuratchi] Action error:', err);
2343
+ if (isFetchAction) {
2344
+ const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2345
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2346
+ status: 500, headers: { 'content-type': 'application/json' }
2347
+ }));
2348
+ }
2349
+ const __loaded = route.load ? await route.load(match.params) : {};
2350
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2351
+ data.params = match.params;
2352
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2353
+ data.__error = (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
2354
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2355
+ }
2356
+ // Fetch-based actions return lightweight JSON (no page re-render)
2158
2357
  if (isFetchAction) {
2159
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2160
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2161
- status: 500, headers: { 'content-type': 'application/json' }
2358
+ return __attachCookies(new Response(JSON.stringify({ ok: true }), {
2359
+ headers: { 'content-type': 'application/json' }
2162
2360
  }));
2163
2361
  }
2164
- const __loaded = route.load ? await route.load(match.params) : {};
2165
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2166
- data.params = match.params;
2167
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2168
- data.__error = (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
2169
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2170
- }
2171
- // Fetch-based actions return lightweight JSON (no page re-render)
2172
- if (isFetchAction) {
2173
- return new Response(JSON.stringify({ ok: true }), {
2174
- headers: { 'content-type': 'application/json' }
2175
- });
2362
+ // POST-Redirect-GET: redirect to custom target or back to same URL
2363
+ const __locals = __getLocals();
2364
+ const redirectTo = __locals.__redirectTo || url.pathname;
2365
+ const redirectStatus = Number(__locals.__redirectStatus) || 303;
2366
+ return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
2176
2367
  }
2177
- // POST-Redirect-GET: redirect to custom target or back to same URL
2178
- const __locals = __getLocals();
2179
- const redirectTo = __locals.__redirectTo || url.pathname;
2180
- const redirectStatus = Number(__locals.__redirectStatus) || 303;
2181
- return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
2182
2368
  }
2183
- }
2184
2369
 
2185
- // GET (or unmatched POST): load + render
2370
+ // GET (or unmatched POST): load + render
2371
+ try {
2372
+ const __loaded = route.load ? await route.load(match.params) : {};
2373
+ const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2374
+ data.params = match.params;
2375
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2376
+ return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2377
+ } catch (err) {
2378
+ console.error('[kuratchi] Route load/render error:', err);
2379
+ const __errDetail = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : undefined;
2380
+ return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(500, __errDetail)), { status: 500, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2381
+ }
2382
+ };
2383
+
2186
2384
  try {
2187
- const __loaded = route.load ? await route.load(match.params) : {};
2188
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2189
- data.params = match.params;
2190
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2191
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2385
+ const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
2386
+ return __runRuntimeRoute(__runtimeCtx, __coreFetch);
2387
+ });
2388
+ return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
2192
2389
  } catch (err) {
2193
- console.error('[kuratchi] Route load/render error:', err);
2194
- const __errDetail = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : undefined;
2195
- return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(500, __errDetail)), { status: 500, headers: { 'content-type': 'text/html; charset=utf-8' } }));
2390
+ const __handled = await __runRuntimeError(__runtimeCtx, err);
2391
+ if (__handled) return __secHeaders(__handled);
2392
+ throw err;
2196
2393
  }
2197
2394
  }
2198
2395
  }
2199
2396
  `;
2200
2397
  }
2398
+ function resolveRuntimeImportPath(projectDir) {
2399
+ const candidates = [
2400
+ { file: 'src/kuratchi.runtime.ts', importPath: '../src/kuratchi.runtime' },
2401
+ { file: 'src/kuratchi.runtime.js', importPath: '../src/kuratchi.runtime' },
2402
+ { file: 'src/kuratchi.runtime.mjs', importPath: '../src/kuratchi.runtime' },
2403
+ { file: 'kuratchi.runtime.ts', importPath: '../kuratchi.runtime' },
2404
+ { file: 'kuratchi.runtime.js', importPath: '../kuratchi.runtime' },
2405
+ { file: 'kuratchi.runtime.mjs', importPath: '../kuratchi.runtime' },
2406
+ ];
2407
+ for (const candidate of candidates) {
2408
+ if (fs.existsSync(path.join(projectDir, candidate.file))) {
2409
+ return candidate.importPath;
2410
+ }
2411
+ }
2412
+ return null;
2413
+ }
2414
+ function toWorkerImportPath(projectDir, outDir, filePath) {
2415
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
2416
+ let rel = path.relative(outDir, absPath).replace(/\\/g, '/');
2417
+ if (!rel.startsWith('.'))
2418
+ rel = `./${rel}`;
2419
+ return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
2420
+ }