@kuratchi/js 0.0.2 → 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.
@@ -531,6 +531,8 @@ export function compile(options) {
531
531
  const authConfig = readAuthConfig(projectDir);
532
532
  // Read Durable Object config and discover handler files
533
533
  const doConfig = readDoConfig(projectDir);
534
+ const containerConfig = readWorkerClassConfig(projectDir, 'containers');
535
+ const workflowConfig = readWorkerClassConfig(projectDir, 'workflows');
534
536
  const doHandlers = doConfig.length > 0
535
537
  ? discoverDoHandlers(srcDir, doConfig, ormDatabases)
536
538
  : [];
@@ -867,6 +869,8 @@ export function compile(options) {
867
869
  // Collect only the components that were actually imported by routes
868
870
  const compiledComponents = Array.from(compiledComponentCache.values());
869
871
  // Generate the routes module
872
+ const runtimeImportPath = resolveRuntimeImportPath(projectDir);
873
+ const hasRuntime = !!runtimeImportPath;
870
874
  const output = generateRoutesModule({
871
875
  projectDir,
872
876
  serverImports: allImports,
@@ -882,6 +886,8 @@ export function compile(options) {
882
886
  isDev: options.isDev ?? false,
883
887
  isLayoutAsync,
884
888
  compiledLayoutActions,
889
+ hasRuntime,
890
+ runtimeImportPath: runtimeImportPath ?? undefined,
885
891
  });
886
892
  // Write to .kuratchi/routes.js
887
893
  const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
@@ -895,10 +901,19 @@ export function compile(options) {
895
901
  // worker.js explicitly re-exports them so wrangler.jsonc can reference a
896
902
  // stable filename while routes.js is freely regenerated.
897
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
+ });
898
912
  const workerLines = [
899
913
  '// Auto-generated by kuratchi \u2014 do not edit.',
900
914
  "export { default } from './routes.js';",
901
915
  ...doConfig.map(c => `export { ${c.className} } from './routes.js';`),
916
+ ...workerClassExports,
902
917
  '',
903
918
  ];
904
919
  writeIfChanged(workerFile, workerLines.join('\n'));
@@ -1365,6 +1380,89 @@ function readDoConfig(projectDir) {
1365
1380
  }
1366
1381
  return entries;
1367
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
+ }
1368
1466
  function discoverFilesWithSuffix(dir, suffix) {
1369
1467
  if (!fs.existsSync(dir))
1370
1468
  return [];
@@ -1666,6 +1764,9 @@ function generateRoutesModule(opts) {
1666
1764
  .join('\n\n');
1667
1765
  // Resolve path to the framework's context module from the output directory
1668
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
+ : '';
1669
1770
  // Auth session init — thin cookie parsing injected into Worker entry
1670
1771
  let authInit = '';
1671
1772
  if (opts.authConfig && opts.authConfig.sessionEnabled) {
@@ -1931,7 +2032,7 @@ ${initLines.join('\n')}
1931
2032
  ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
1932
2033
  ${workerImport}
1933
2034
  ${contextImport}
1934
- ${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')}
1935
2036
 
1936
2037
  // ── Assets ──────────────────────────────────────────────────────
1937
2038
 
@@ -2075,141 +2176,245 @@ ${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data) {
2075
2176
  return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
2076
2177
  }
2077
2178
 
2078
- // ── Exported Worker entrypoint ──────────────────────────────────
2079
-
2080
- export default class extends WorkerEntrypoint {
2081
- async fetch(request) {
2082
- __setRequestContext(this.ctx, request);
2083
- ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
2084
- const url = new URL(request.url);
2085
- ${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');
2086
2181
 
2087
- // Serve static assets from src/assets/ at /_assets/*
2088
- if (url.pathname.startsWith('/_assets/')) {
2089
- const name = url.pathname.slice('/_assets/'.length);
2090
- const asset = __assets[name];
2091
- if (asset) {
2092
- if (request.headers.get('if-none-match') === asset.etag) {
2093
- return new Response(null, { status: 304 });
2094
- }
2095
- return new Response(asset.content, {
2096
- headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
2097
- });
2098
- }
2099
- return __secHeaders(new Response('Not Found', { status: 404 }));
2100
- }
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
+ }
2101
2195
 
2102
- 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
+ }
2103
2209
 
2104
- if (!match) {
2105
- 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');
2106
2217
  }
2218
+ }
2219
+ return out;
2220
+ }
2107
2221
 
2108
- const route = routes[match.index];
2109
- __setLocal('params', match.params);
2110
- const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
2111
- const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
2112
- let __qArgs = [];
2222
+ async function __runRuntimeError(ctx, error) {
2223
+ for (const [name, step] of __runtimeEntries) {
2224
+ if (typeof step.error !== 'function') continue;
2113
2225
  try {
2114
- const __parsed = JSON.parse(__qArgsRaw);
2115
- __qArgs = Array.isArray(__parsed) ? __parsed : [];
2116
- } catch {}
2117
- __setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
2118
- if (!__getLocals().__breadcrumbs) {
2119
- __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);
2120
2230
  }
2231
+ }
2232
+ return null;
2233
+ }
2121
2234
 
2122
- // RPC call: GET ?_rpc=fnName&_args=[...] → JSON response
2123
- const __rpcName = url.searchParams.get(‘_rpc’);
2124
- if (request.method === ‘GET’ && __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName)) {
2125
- if (request.headers.get('x-kuratchi-rpc') !== '1') {
2126
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
2127
- status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2128
- }));
2129
- }
2130
- try {
2131
- const __rpcArgsStr = url.searchParams.get('_args');
2132
- let __rpcArgs = [];
2133
- if (__rpcArgsStr) {
2134
- const __parsed = JSON.parse(__rpcArgsStr);
2135
- __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
+ });
2136
2267
  }
2137
- const __rpcResult = await route.rpc[__rpcName](...__rpcArgs);
2138
- return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
2139
- headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2140
- }));
2141
- } catch (err) {
2142
- console.error('[kuratchi] RPC error:', err);
2143
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2144
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2145
- status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
2146
- }));
2268
+ return __secHeaders(new Response('Not Found', { status: 404 }));
2147
2269
  }
2148
- }
2149
2270
 
2150
- // Form action: POST with hidden _action field in form body
2151
- if (request.method === 'POST') {
2152
- if (!__isSameOrigin(request, url)) {
2153
- 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' } }));
2154
2275
  }
2155
- const formData = await request.formData();
2156
- const actionName = formData.get('_action');
2157
- const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
2158
- || (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
2159
- if (actionName && __actionFn) {
2160
- // Check if this is a fetch-based action call (onclick) with JSON args
2161
- const argsStr = formData.get('_args');
2162
- 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
+ }
2163
2300
  try {
2164
- if (isFetchAction) {
2165
- const __parsed = JSON.parse(argsStr);
2166
- const args = Array.isArray(__parsed) ? __parsed : [];
2167
- await __actionFn(...args);
2168
- } else {
2169
- 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 : [];
2170
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
+ }));
2171
2311
  } catch (err) {
2172
- 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)
2173
2357
  if (isFetchAction) {
2174
- const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
2175
- return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
2176
- status: 500, headers: { 'content-type': 'application/json' }
2358
+ return __attachCookies(new Response(JSON.stringify({ ok: true }), {
2359
+ headers: { 'content-type': 'application/json' }
2177
2360
  }));
2178
2361
  }
2179
- const __loaded = route.load ? await route.load(match.params) : {};
2180
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2181
- data.params = match.params;
2182
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2183
- data.__error = (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
2184
- return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data);
2185
- }
2186
- // Fetch-based actions return lightweight JSON (no page re-render)
2187
- if (isFetchAction) {
2188
- return __attachCookies(new Response(JSON.stringify({ ok: true }), {
2189
- headers: { 'content-type': 'application/json' }
2190
- }));
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 } }));
2191
2367
  }
2192
- // POST-Redirect-GET: redirect to custom target or back to same URL
2193
- const __locals = __getLocals();
2194
- const redirectTo = __locals.__redirectTo || url.pathname;
2195
- const redirectStatus = Number(__locals.__redirectStatus) || 303;
2196
- return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
2197
2368
  }
2198
- }
2199
2369
 
2200
- // 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
+
2201
2384
  try {
2202
- const __loaded = route.load ? await route.load(match.params) : {};
2203
- const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
2204
- data.params = match.params;
2205
- data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
2206
- 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);
2207
2389
  } catch (err) {
2208
- console.error('[kuratchi] Route load/render error:', err);
2209
- const __errDetail = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : undefined;
2210
- 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;
2211
2393
  }
2212
2394
  }
2213
2395
  }
2214
2396
  `;
2215
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
+ }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
- * KuratchiJS — Public API
2
+ * KuratchiJS - Public API
3
3
  *
4
4
  * A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax.
5
5
  */
6
6
  export { createApp } from './runtime/app.js';
7
7
  export { defineConfig } from './runtime/config.js';
8
+ export { defineRuntime } from './runtime/runtime.js';
8
9
  export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
9
10
  export { kuratchiDO, doRpc } from './runtime/do.js';
10
11
  export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
11
- export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule } from './runtime/types.js';
12
+ export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './runtime/types.js';
12
13
  export type { RpcOf } from './runtime/do.js';
14
+ export { compile } from './compiler/index.js';
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  /**
2
- * KuratchiJS — Public API
2
+ * KuratchiJS - Public API
3
3
  *
4
4
  * A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax.
5
5
  */
6
6
  // Runtime
7
7
  export { createApp } from './runtime/app.js';
8
8
  export { defineConfig } from './runtime/config.js';
9
+ export { defineRuntime } from './runtime/runtime.js';
9
10
  export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
10
11
  export { kuratchiDO, doRpc } from './runtime/do.js';
11
12
  export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
12
13
  // Compatibility aliases
13
14
  matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
15
+ // Compiler (for build tooling)
16
+ export { compile } from './compiler/index.js';
@@ -1,8 +1,9 @@
1
1
  export { createApp } from './app.js';
2
2
  export { defineConfig } from './config.js';
3
+ export { defineRuntime } from './runtime.js';
3
4
  export { Router, filePathToPattern } from './router.js';
4
5
  export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
5
6
  export { kuratchiDO, doRpc } from './do.js';
6
7
  export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './containers.js';
7
- export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, LayoutModule } from './types.js';
8
+ export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, LayoutModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './types.js';
8
9
  export type { RpcOf } from './do.js';
@@ -1,5 +1,6 @@
1
1
  export { createApp } from './app.js';
2
2
  export { defineConfig } from './config.js';
3
+ export { defineRuntime } from './runtime.js';
3
4
  export { Router, filePathToPattern } from './router.js';
4
5
  export { getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
5
6
  export { kuratchiDO, doRpc } from './do.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Runtime extension system
3
+ */
4
+ import type { Env, RuntimeDefinition } from './types.js';
5
+ export declare function defineRuntime<E extends Env = Env>(runtime: RuntimeDefinition<E>): RuntimeDefinition<E>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Runtime extension system
3
+ */
4
+ export function defineRuntime(runtime) {
5
+ return runtime;
6
+ }
@@ -115,6 +115,20 @@ export interface kuratchiConfig<E extends Env = Env> {
115
115
  /** The user field path that identifies the DO stub (e.g. 'user.orgId') */
116
116
  stubId?: string;
117
117
  }>;
118
+ /** Container classes exported into the generated worker entry. */
119
+ containers?: Record<string, string | {
120
+ /** Relative path from project root. Must end in `.container.ts|js|mjs|cjs`. */
121
+ file: string;
122
+ /** Optional override; inferred from exported class in `file` when omitted. */
123
+ className?: string;
124
+ }>;
125
+ /** Workflow classes exported into the generated worker entry. */
126
+ workflows?: Record<string, string | {
127
+ /** Relative path from project root. Must end in `.workflow.ts|js|mjs|cjs`. */
128
+ file: string;
129
+ /** Optional override; inferred from exported class in `file` when omitted. */
130
+ className?: string;
131
+ }>;
118
132
  }
119
133
  /** Auth configuration for kuratchi.config.ts */
120
134
  export interface AuthConfig {
@@ -205,3 +219,21 @@ export interface AuthConfig {
205
219
  binding: string;
206
220
  };
207
221
  }
222
+ /** Runtime pipeline context - shared across runtime step handlers */
223
+ export interface RuntimeContext<E extends Env = Env> {
224
+ request: Request;
225
+ env: E;
226
+ ctx: ExecutionContext;
227
+ url: URL;
228
+ params: Record<string, string>;
229
+ locals: Record<string, any>;
230
+ }
231
+ export type RuntimeNext = () => Promise<Response>;
232
+ export type RuntimeErrorResult = Response | null | undefined | void;
233
+ export interface RuntimeStep<E extends Env = Env> {
234
+ request?: (ctx: RuntimeContext<E>, next: RuntimeNext) => Promise<Response> | Response;
235
+ route?: (ctx: RuntimeContext<E>, next: RuntimeNext) => Promise<Response> | Response;
236
+ response?: (ctx: RuntimeContext<E>, response: Response) => Promise<Response> | Response;
237
+ error?: (ctx: RuntimeContext<E>, error: unknown) => Promise<RuntimeErrorResult> | RuntimeErrorResult;
238
+ }
239
+ export type RuntimeDefinition<E extends Env = Env> = Record<string, RuntimeStep<E>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuratchi/js",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",