@kuratchi/js 0.0.14 → 0.0.16

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 (65) hide show
  1. package/README.md +135 -68
  2. package/dist/cli.js +80 -47
  3. package/dist/compiler/api-route-pipeline.d.ts +8 -0
  4. package/dist/compiler/api-route-pipeline.js +23 -0
  5. package/dist/compiler/asset-pipeline.d.ts +7 -0
  6. package/dist/compiler/asset-pipeline.js +33 -0
  7. package/dist/compiler/client-module-pipeline.d.ts +25 -0
  8. package/dist/compiler/client-module-pipeline.js +257 -0
  9. package/dist/compiler/compiler-shared.d.ts +55 -0
  10. package/dist/compiler/compiler-shared.js +4 -0
  11. package/dist/compiler/component-pipeline.d.ts +15 -0
  12. package/dist/compiler/component-pipeline.js +163 -0
  13. package/dist/compiler/config-reading.d.ts +11 -0
  14. package/dist/compiler/config-reading.js +323 -0
  15. package/dist/compiler/convention-discovery.d.ts +9 -0
  16. package/dist/compiler/convention-discovery.js +83 -0
  17. package/dist/compiler/durable-object-pipeline.d.ts +9 -0
  18. package/dist/compiler/durable-object-pipeline.js +255 -0
  19. package/dist/compiler/error-page-pipeline.d.ts +1 -0
  20. package/dist/compiler/error-page-pipeline.js +16 -0
  21. package/dist/compiler/import-linking.d.ts +36 -0
  22. package/dist/compiler/import-linking.js +139 -0
  23. package/dist/compiler/index.d.ts +3 -3
  24. package/dist/compiler/index.js +137 -3265
  25. package/dist/compiler/layout-pipeline.d.ts +31 -0
  26. package/dist/compiler/layout-pipeline.js +155 -0
  27. package/dist/compiler/page-route-pipeline.d.ts +16 -0
  28. package/dist/compiler/page-route-pipeline.js +62 -0
  29. package/dist/compiler/parser.d.ts +4 -0
  30. package/dist/compiler/parser.js +433 -51
  31. package/dist/compiler/root-layout-pipeline.d.ts +10 -0
  32. package/dist/compiler/root-layout-pipeline.js +517 -0
  33. package/dist/compiler/route-discovery.d.ts +7 -0
  34. package/dist/compiler/route-discovery.js +87 -0
  35. package/dist/compiler/route-pipeline.d.ts +57 -0
  36. package/dist/compiler/route-pipeline.js +296 -0
  37. package/dist/compiler/route-state-pipeline.d.ts +25 -0
  38. package/dist/compiler/route-state-pipeline.js +139 -0
  39. package/dist/compiler/routes-module-feature-blocks.d.ts +2 -0
  40. package/dist/compiler/routes-module-feature-blocks.js +330 -0
  41. package/dist/compiler/routes-module-pipeline.d.ts +2 -0
  42. package/dist/compiler/routes-module-pipeline.js +6 -0
  43. package/dist/compiler/routes-module-runtime-shell.d.ts +2 -0
  44. package/dist/compiler/routes-module-runtime-shell.js +81 -0
  45. package/dist/compiler/routes-module-types.d.ts +44 -0
  46. package/dist/compiler/routes-module-types.js +1 -0
  47. package/dist/compiler/script-transform.d.ts +16 -0
  48. package/dist/compiler/script-transform.js +218 -0
  49. package/dist/compiler/server-module-pipeline.d.ts +13 -0
  50. package/dist/compiler/server-module-pipeline.js +124 -0
  51. package/dist/compiler/template.d.ts +13 -1
  52. package/dist/compiler/template.js +323 -60
  53. package/dist/compiler/worker-output-pipeline.d.ts +13 -0
  54. package/dist/compiler/worker-output-pipeline.js +37 -0
  55. package/dist/compiler/wrangler-sync.d.ts +14 -0
  56. package/dist/compiler/wrangler-sync.js +185 -0
  57. package/dist/runtime/app.js +15 -3
  58. package/dist/runtime/generated-worker.d.ts +33 -0
  59. package/dist/runtime/generated-worker.js +412 -0
  60. package/dist/runtime/index.d.ts +2 -1
  61. package/dist/runtime/index.js +1 -0
  62. package/dist/runtime/router.d.ts +2 -1
  63. package/dist/runtime/router.js +12 -3
  64. package/dist/runtime/types.d.ts +8 -2
  65. package/package.json +5 -1
@@ -0,0 +1,185 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ function stripJsonComments(content) {
4
+ let result = '';
5
+ let i = 0;
6
+ let inString = false;
7
+ let stringChar = '';
8
+ while (i < content.length) {
9
+ const ch = content[i];
10
+ const next = content[i + 1];
11
+ if (inString) {
12
+ result += ch;
13
+ if (ch === '\\' && i + 1 < content.length) {
14
+ result += next;
15
+ i += 2;
16
+ continue;
17
+ }
18
+ if (ch === stringChar) {
19
+ inString = false;
20
+ }
21
+ i++;
22
+ continue;
23
+ }
24
+ if (ch === '"' || ch === "'") {
25
+ inString = true;
26
+ stringChar = ch;
27
+ result += ch;
28
+ i++;
29
+ continue;
30
+ }
31
+ if (ch === '/' && next === '/') {
32
+ while (i < content.length && content[i] !== '\n')
33
+ i++;
34
+ continue;
35
+ }
36
+ if (ch === '/' && next === '*') {
37
+ i += 2;
38
+ while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/'))
39
+ i++;
40
+ i += 2;
41
+ continue;
42
+ }
43
+ result += ch;
44
+ i++;
45
+ }
46
+ return result;
47
+ }
48
+ export function syncWranglerConfig(opts) {
49
+ const jsoncPath = path.join(opts.projectDir, 'wrangler.jsonc');
50
+ const jsonPath = path.join(opts.projectDir, 'wrangler.json');
51
+ const tomlPath = path.join(opts.projectDir, 'wrangler.toml');
52
+ let configPath;
53
+ let isJsonc = false;
54
+ if (fs.existsSync(jsoncPath)) {
55
+ configPath = jsoncPath;
56
+ isJsonc = true;
57
+ }
58
+ else if (fs.existsSync(jsonPath)) {
59
+ configPath = jsonPath;
60
+ }
61
+ else if (fs.existsSync(tomlPath)) {
62
+ console.log('[kuratchi] wrangler.toml detected. Auto-sync requires wrangler.jsonc. Skipping wrangler sync.');
63
+ return;
64
+ }
65
+ else {
66
+ console.log('[kuratchi] Creating wrangler.jsonc with workflow config...');
67
+ configPath = jsoncPath;
68
+ isJsonc = true;
69
+ }
70
+ let rawContent = '';
71
+ let wranglerConfig = {};
72
+ if (fs.existsSync(configPath)) {
73
+ rawContent = fs.readFileSync(configPath, 'utf-8');
74
+ try {
75
+ const jsonContent = stripJsonComments(rawContent);
76
+ wranglerConfig = JSON.parse(jsonContent);
77
+ }
78
+ catch (err) {
79
+ console.error(`[kuratchi] Failed to parse ${path.basename(configPath)}: ${err.message}`);
80
+ console.error('[kuratchi] Skipping wrangler sync. Please fix the JSON syntax.');
81
+ return;
82
+ }
83
+ }
84
+ let changed = false;
85
+ if (opts.config.workflows.length > 0) {
86
+ const existingWorkflows = wranglerConfig.workflows || [];
87
+ const existingByBinding = new Map(existingWorkflows.map((workflow) => [workflow.binding, workflow]));
88
+ for (const workflow of opts.config.workflows) {
89
+ const name = workflow.binding.toLowerCase().replace(/_/g, '-');
90
+ const entry = {
91
+ name,
92
+ binding: workflow.binding,
93
+ class_name: workflow.className,
94
+ };
95
+ const existing = existingByBinding.get(workflow.binding);
96
+ if (!existing) {
97
+ existingWorkflows.push(entry);
98
+ changed = true;
99
+ console.log(`[kuratchi] Added workflow "${workflow.binding}" to wrangler config`);
100
+ }
101
+ else if (existing.class_name !== workflow.className) {
102
+ existing.class_name = workflow.className;
103
+ changed = true;
104
+ console.log(`[kuratchi] Updated workflow "${workflow.binding}" class_name to "${workflow.className}"`);
105
+ }
106
+ }
107
+ const configBindings = new Set(opts.config.workflows.map((workflow) => workflow.binding));
108
+ const filtered = existingWorkflows.filter((workflow) => {
109
+ if (!configBindings.has(workflow.binding)) {
110
+ const expectedName = workflow.binding.toLowerCase().replace(/_/g, '-');
111
+ if (workflow.name === expectedName) {
112
+ console.log(`[kuratchi] Removed workflow "${workflow.binding}" from wrangler config`);
113
+ changed = true;
114
+ return false;
115
+ }
116
+ }
117
+ return true;
118
+ });
119
+ if (filtered.length !== existingWorkflows.length) {
120
+ wranglerConfig.workflows = filtered;
121
+ }
122
+ else {
123
+ wranglerConfig.workflows = existingWorkflows;
124
+ }
125
+ if (wranglerConfig.workflows.length === 0) {
126
+ delete wranglerConfig.workflows;
127
+ }
128
+ }
129
+ if (opts.config.containers.length > 0) {
130
+ const existingContainers = wranglerConfig.containers || [];
131
+ const existingByBinding = new Map(existingContainers.map((container) => [container.binding, container]));
132
+ for (const container of opts.config.containers) {
133
+ const name = container.binding.toLowerCase().replace(/_/g, '-');
134
+ const entry = {
135
+ name,
136
+ binding: container.binding,
137
+ class_name: container.className,
138
+ };
139
+ const existing = existingByBinding.get(container.binding);
140
+ if (!existing) {
141
+ existingContainers.push(entry);
142
+ changed = true;
143
+ console.log(`[kuratchi] Added container "${container.binding}" to wrangler config`);
144
+ }
145
+ else if (existing.class_name !== container.className) {
146
+ existing.class_name = container.className;
147
+ changed = true;
148
+ console.log(`[kuratchi] Updated container "${container.binding}" class_name to "${container.className}"`);
149
+ }
150
+ }
151
+ wranglerConfig.containers = existingContainers;
152
+ if (wranglerConfig.containers.length === 0) {
153
+ delete wranglerConfig.containers;
154
+ }
155
+ }
156
+ if (opts.config.durableObjects.length > 0) {
157
+ if (!wranglerConfig.durable_objects) {
158
+ wranglerConfig.durable_objects = { bindings: [] };
159
+ }
160
+ const existingBindings = wranglerConfig.durable_objects.bindings || [];
161
+ const existingByName = new Map(existingBindings.map((binding) => [binding.name, binding]));
162
+ for (const durableObject of opts.config.durableObjects) {
163
+ const entry = {
164
+ name: durableObject.binding,
165
+ class_name: durableObject.className,
166
+ };
167
+ const existing = existingByName.get(durableObject.binding);
168
+ if (!existing) {
169
+ existingBindings.push(entry);
170
+ changed = true;
171
+ console.log(`[kuratchi] Added durable_object "${durableObject.binding}" to wrangler config`);
172
+ }
173
+ else if (existing.class_name !== durableObject.className) {
174
+ existing.class_name = durableObject.className;
175
+ changed = true;
176
+ console.log(`[kuratchi] Updated durable_object "${durableObject.binding}" class_name to "${durableObject.className}"`);
177
+ }
178
+ }
179
+ wranglerConfig.durable_objects.bindings = existingBindings;
180
+ }
181
+ if (!changed)
182
+ return;
183
+ const newContent = JSON.stringify(wranglerConfig, null, isJsonc ? '\t' : '\t');
184
+ opts.writeFile(configPath, newContent + '\n');
185
+ }
@@ -133,13 +133,25 @@ export function createApp(config) {
133
133
  }
134
134
  /** Render a page through its layout */
135
135
  function renderPage(route, data, layouts) {
136
- const content = route.render(data);
136
+ const rendered = normalizeRenderOutput(route.render(data));
137
137
  const layoutName = route.layout ?? 'default';
138
138
  const layout = layouts[layoutName];
139
139
  const html = layout
140
- ? layout.render({ content, data })
141
- : content;
140
+ ? layout.render({ content: rendered.html, data, head: rendered.head })
141
+ : rendered.html;
142
142
  return new Response(html, {
143
143
  headers: { 'content-type': 'text/html; charset=utf-8' },
144
144
  });
145
145
  }
146
+ function normalizeRenderOutput(output) {
147
+ if (typeof output === 'string') {
148
+ return { html: output, head: '' };
149
+ }
150
+ return {
151
+ html: typeof output?.html === 'string' ? output.html : '',
152
+ head: typeof output?.head === 'string' ? output.head : '',
153
+ fragments: output && typeof output === 'object' && !Array.isArray(output) && typeof output.fragments === 'object'
154
+ ? (output.fragments ?? {})
155
+ : {},
156
+ };
157
+ }
@@ -0,0 +1,33 @@
1
+ import type { PageRenderOutput, RuntimeContext, RuntimeDefinition } from './types.js';
2
+ export interface GeneratedAssetEntry {
3
+ content: string;
4
+ mime: string;
5
+ etag: string;
6
+ }
7
+ export interface GeneratedApiRoute {
8
+ pattern: string;
9
+ __api: true;
10
+ [method: string]: unknown;
11
+ }
12
+ export interface GeneratedPageRoute {
13
+ pattern: string;
14
+ load?: (params: Record<string, string>) => Promise<unknown> | unknown;
15
+ actions?: Record<string, (...args: any[]) => Promise<unknown> | unknown>;
16
+ rpc?: Record<string, (...args: any[]) => Promise<unknown> | unknown>;
17
+ render: (data: Record<string, any>) => PageRenderOutput;
18
+ }
19
+ export interface GeneratedWorkerOptions {
20
+ routes: Array<GeneratedPageRoute | GeneratedApiRoute>;
21
+ layout: (content: string, head?: string) => Promise<string> | string;
22
+ layoutActions: Record<string, (...args: any[]) => Promise<unknown> | unknown>;
23
+ assetsPrefix: string;
24
+ assets: Record<string, GeneratedAssetEntry>;
25
+ errorPages: Record<number, (detail?: string) => string>;
26
+ runtimeDefinition?: RuntimeDefinition;
27
+ workflowStatusRpc?: Record<string, (instanceId: string) => Promise<unknown>>;
28
+ initializeRequest?: (ctx: RuntimeContext) => Promise<void> | void;
29
+ preRouteChecks?: (ctx: RuntimeContext) => Promise<Response | null | undefined> | Response | null | undefined;
30
+ }
31
+ export declare function createGeneratedWorker(opts: GeneratedWorkerOptions): {
32
+ fetch(request: Request, env: Record<string, any>, ctx: ExecutionContext): Promise<Response>;
33
+ };
@@ -0,0 +1,412 @@
1
+ import { __esc, __getLocals, __setLocal, __setRequestContext, buildDefaultBreadcrumbs } from './context.js';
2
+ import { Router } from './router.js';
3
+ export function createGeneratedWorker(opts) {
4
+ const router = new Router();
5
+ const runtimeEntries = __getRuntimeEntries(opts.runtimeDefinition);
6
+ for (let i = 0; i < opts.routes.length; i++) {
7
+ router.add(opts.routes[i].pattern, i);
8
+ }
9
+ return {
10
+ async fetch(request, env, ctx) {
11
+ __setRequestContext(ctx, request, env);
12
+ const runtimeCtx = {
13
+ request,
14
+ env,
15
+ ctx,
16
+ url: new URL(request.url),
17
+ params: {},
18
+ locals: __getLocals(),
19
+ };
20
+ if (opts.initializeRequest) {
21
+ await opts.initializeRequest(runtimeCtx);
22
+ }
23
+ const coreFetch = async () => {
24
+ const { url } = runtimeCtx;
25
+ const fragmentId = request.headers.get('x-kuratchi-fragment');
26
+ const preRoute = opts.preRouteChecks ? await opts.preRouteChecks(runtimeCtx) : null;
27
+ if (preRoute instanceof Response) {
28
+ return __secHeaders(preRoute);
29
+ }
30
+ if (url.pathname.startsWith(opts.assetsPrefix)) {
31
+ const name = url.pathname.slice(opts.assetsPrefix.length);
32
+ const asset = opts.assets[name];
33
+ if (asset) {
34
+ if (request.headers.get('if-none-match') === asset.etag) {
35
+ return new Response(null, { status: 304 });
36
+ }
37
+ return new Response(asset.content, {
38
+ headers: {
39
+ 'content-type': asset.mime,
40
+ 'cache-control': 'public, max-age=31536000, immutable',
41
+ 'etag': asset.etag,
42
+ },
43
+ });
44
+ }
45
+ return __secHeaders(new Response('Not Found', { status: 404 }));
46
+ }
47
+ const match = router.match(url.pathname);
48
+ if (!match) {
49
+ return __secHeaders(new Response(await opts.layout(__renderError(opts.errorPages, 404)), {
50
+ status: 404,
51
+ headers: { 'content-type': 'text/html; charset=utf-8' },
52
+ }));
53
+ }
54
+ runtimeCtx.params = match.params;
55
+ __setLocal('params', match.params);
56
+ const route = opts.routes[match.index];
57
+ if ('__api' in route && route.__api) {
58
+ return __dispatchApiRoute(route, runtimeCtx);
59
+ }
60
+ const pageRoute = route;
61
+ const queryFn = request.headers.get('x-kuratchi-query-fn') || '';
62
+ const queryArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
63
+ let queryArgs = [];
64
+ try {
65
+ const parsed = JSON.parse(queryArgsRaw);
66
+ queryArgs = Array.isArray(parsed) ? parsed : [];
67
+ }
68
+ catch { }
69
+ __setLocal('__queryOverride', queryFn ? { fn: queryFn, args: queryArgs } : null);
70
+ if (!__getLocals().__breadcrumbs) {
71
+ __setLocal('breadcrumbs', buildDefaultBreadcrumbs(url.pathname, match.params));
72
+ }
73
+ const rpcResponse = await __handleRpc(pageRoute, opts.workflowStatusRpc ?? {}, runtimeCtx);
74
+ if (rpcResponse)
75
+ return rpcResponse;
76
+ if (request.method === 'POST') {
77
+ const actionResponse = await __handleAction(pageRoute, opts.layoutActions, opts.layout, runtimeCtx, fragmentId);
78
+ if (actionResponse)
79
+ return actionResponse;
80
+ }
81
+ try {
82
+ const loaded = pageRoute.load ? await pageRoute.load(match.params) : {};
83
+ const data = (__isObject(loaded) ? loaded : { value: loaded });
84
+ data.params = match.params;
85
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
86
+ const allActions = Object.assign({}, pageRoute.actions, opts.layoutActions || {});
87
+ Object.keys(allActions).forEach((key) => {
88
+ if (!(key in data))
89
+ data[key] = { error: undefined, loading: false, success: false };
90
+ });
91
+ return await __renderPage(opts.layout, pageRoute, data, fragmentId);
92
+ }
93
+ catch (err) {
94
+ if (err?.isRedirectError) {
95
+ const redirectTo = err.location || url.pathname;
96
+ const redirectStatus = Number(err.status) || 303;
97
+ return __attachCookies(new Response(null, { status: redirectStatus, headers: { location: redirectTo } }));
98
+ }
99
+ const handled = await __runRuntimeError(runtimeEntries, runtimeCtx, err);
100
+ if (handled)
101
+ return __secHeaders(handled);
102
+ console.error('[kuratchi] Route load/render error:', err);
103
+ const pageErrStatus = err?.isPageError && err.status ? err.status : 500;
104
+ const errDetail = err?.isPageError ? err.message : (__isDevMode() && err?.message) ? err.message : undefined;
105
+ return __secHeaders(new Response(await opts.layout(__renderError(opts.errorPages, pageErrStatus, errDetail)), {
106
+ status: pageErrStatus,
107
+ headers: { 'content-type': 'text/html; charset=utf-8' },
108
+ }));
109
+ }
110
+ };
111
+ try {
112
+ const requestResponse = await __runRuntimeRequest(runtimeEntries, runtimeCtx, async () => {
113
+ return __runRuntimeRoute(runtimeEntries, runtimeCtx, coreFetch);
114
+ });
115
+ return await __runRuntimeResponse(runtimeEntries, runtimeCtx, requestResponse);
116
+ }
117
+ catch (err) {
118
+ const handled = await __runRuntimeError(runtimeEntries, runtimeCtx, err);
119
+ if (handled)
120
+ return __secHeaders(handled);
121
+ throw err;
122
+ }
123
+ },
124
+ };
125
+ }
126
+ async function __dispatchApiRoute(route, runtimeCtx) {
127
+ const { request } = runtimeCtx;
128
+ const method = request.method;
129
+ if (method === 'OPTIONS') {
130
+ const handler = route.OPTIONS;
131
+ if (typeof handler === 'function')
132
+ return __secHeaders(await handler(runtimeCtx));
133
+ const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
134
+ .filter((name) => typeof route[name] === 'function')
135
+ .join(', ');
136
+ return __secHeaders(new Response(null, { status: 204, headers: { Allow: allowed, 'Access-Control-Allow-Methods': allowed } }));
137
+ }
138
+ const handler = route[method];
139
+ if (typeof handler !== 'function') {
140
+ const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
141
+ .filter((name) => typeof route[name] === 'function')
142
+ .join(', ');
143
+ return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
144
+ status: 405,
145
+ headers: { 'content-type': 'application/json', Allow: allowed },
146
+ }));
147
+ }
148
+ return __secHeaders(await handler(runtimeCtx));
149
+ }
150
+ async function __handleRpc(route, workflowStatusRpc, runtimeCtx) {
151
+ const { request, url } = runtimeCtx;
152
+ const rpcName = url.searchParams.get('_rpc');
153
+ const hasRouteRpc = rpcName && route.rpc && Object.hasOwn(route.rpc, rpcName);
154
+ const hasWorkflowRpc = rpcName && Object.hasOwn(workflowStatusRpc, rpcName);
155
+ if (!(request.method === 'GET' && rpcName && (hasRouteRpc || hasWorkflowRpc))) {
156
+ return null;
157
+ }
158
+ if (request.headers.get('x-kuratchi-rpc') !== '1') {
159
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
160
+ status: 403,
161
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
162
+ }));
163
+ }
164
+ try {
165
+ const rpcArgsStr = url.searchParams.get('_args');
166
+ let rpcArgs = [];
167
+ if (rpcArgsStr) {
168
+ const parsed = JSON.parse(rpcArgsStr);
169
+ rpcArgs = Array.isArray(parsed) ? parsed : [];
170
+ }
171
+ const rpcFn = hasRouteRpc ? route.rpc[rpcName] : workflowStatusRpc[rpcName];
172
+ const rpcResult = await rpcFn(...rpcArgs);
173
+ return __secHeaders(new Response(JSON.stringify({ ok: true, data: rpcResult }), {
174
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
175
+ }));
176
+ }
177
+ catch (err) {
178
+ console.error('[kuratchi] RPC error:', err);
179
+ const errMsg = __isDevMode() ? err?.message : 'Internal Server Error';
180
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: errMsg }), {
181
+ status: 500,
182
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
183
+ }));
184
+ }
185
+ }
186
+ async function __handleAction(route, layoutActions, layout, runtimeCtx, fragmentId) {
187
+ const { request, url, params } = runtimeCtx;
188
+ if (request.method !== 'POST')
189
+ return null;
190
+ if (!__isSameOrigin(request, url)) {
191
+ return __secHeaders(new Response('Forbidden', { status: 403 }));
192
+ }
193
+ const formData = await request.formData();
194
+ const actionName = formData.get('_action');
195
+ const actionKey = typeof actionName === 'string' ? actionName : null;
196
+ const actionFn = (actionKey && route.actions && Object.hasOwn(route.actions, actionKey) ? route.actions[actionKey] : null)
197
+ || (actionKey && layoutActions && Object.hasOwn(layoutActions, actionKey) ? layoutActions[actionKey] : null);
198
+ if (!(actionKey && actionFn)) {
199
+ return null;
200
+ }
201
+ const argsStr = formData.get('_args');
202
+ const isFetchAction = argsStr !== null;
203
+ try {
204
+ if (isFetchAction) {
205
+ const parsed = JSON.parse(String(argsStr));
206
+ const args = Array.isArray(parsed) ? parsed : [];
207
+ await actionFn(...args);
208
+ }
209
+ else {
210
+ await actionFn(formData);
211
+ }
212
+ }
213
+ catch (err) {
214
+ if (err?.isRedirectError) {
215
+ const redirectTo = err.location || url.pathname;
216
+ const redirectStatus = Number(err.status) || 303;
217
+ if (isFetchAction) {
218
+ return __attachCookies(__secHeaders(new Response(JSON.stringify({ ok: true, redirectTo, redirectStatus }), {
219
+ headers: { 'content-type': 'application/json' },
220
+ })));
221
+ }
222
+ return __attachCookies(new Response(null, { status: redirectStatus, headers: { location: redirectTo } }));
223
+ }
224
+ console.error('[kuratchi] Action error:', err);
225
+ if (isFetchAction) {
226
+ const errMsg = __isDevMode() && err?.message ? err.message : 'Internal Server Error';
227
+ return __secHeaders(new Response(JSON.stringify({ ok: false, error: errMsg }), {
228
+ status: 500,
229
+ headers: { 'content-type': 'application/json' },
230
+ }));
231
+ }
232
+ const loaded = route.load ? await route.load(params) : {};
233
+ const data = (__isObject(loaded) ? loaded : { value: loaded });
234
+ data.params = params;
235
+ data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
236
+ const allActions = Object.assign({}, route.actions, layoutActions || {});
237
+ Object.keys(allActions).forEach((key) => {
238
+ if (!(key in data))
239
+ data[key] = { error: undefined, loading: false, success: false };
240
+ });
241
+ const errMsg = err?.isActionError ? err.message : (__isDevMode() && err?.message) ? err.message : 'Action failed';
242
+ data[actionKey] = { error: errMsg, loading: false, success: false };
243
+ return await __renderPage(layout, route, data, fragmentId);
244
+ }
245
+ if (isFetchAction) {
246
+ return __attachCookies(new Response(JSON.stringify({ ok: true }), {
247
+ headers: { 'content-type': 'application/json' },
248
+ }));
249
+ }
250
+ const locals = __getLocals();
251
+ const redirectTo = locals.__redirectTo || url.pathname;
252
+ const redirectStatus = Number(locals.__redirectStatus) || 303;
253
+ return __attachCookies(new Response(null, { status: redirectStatus, headers: { location: redirectTo } }));
254
+ }
255
+ async function __renderPage(layout, route, data, fragmentId) {
256
+ const rendered = __normalizeRenderOutput(route.render(data));
257
+ if (fragmentId) {
258
+ const fragment = rendered.fragments?.[fragmentId];
259
+ if (typeof fragment !== 'string') {
260
+ return __secHeaders(new Response('Fragment not found', { status: 404 }));
261
+ }
262
+ return __attachCookies(new Response(fragment, {
263
+ headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
264
+ }));
265
+ }
266
+ return __attachCookies(new Response(await layout(rendered.html, rendered.head || ''), {
267
+ headers: { 'content-type': 'text/html; charset=utf-8' },
268
+ }));
269
+ }
270
+ function __renderError(errorPages, status, detail) {
271
+ const custom = errorPages[status];
272
+ if (custom)
273
+ return custom(detail);
274
+ const title = __errorMessages[status] || 'Error';
275
+ const detailHtml = detail
276
+ ? '<p style="font-family:ui-monospace,monospace;font-size:0.8rem;color:#555;background:#111;padding:0.5rem 1rem;border-radius:6px;max-width:480px;margin:1rem auto 0;word-break:break-word">' + __esc(detail) + '</p>'
277
+ : '';
278
+ return '<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:2rem">'
279
+ + '<div>'
280
+ + '<p style="font-size:5rem;font-weight:700;margin:0;color:#333;line-height:1">' + status + '</p>'
281
+ + '<p style="font-size:1rem;color:#555;margin:0.5rem 0 0;letter-spacing:0.05em">' + __esc(title) + '</p>'
282
+ + detailHtml
283
+ + '</div>'
284
+ + '</div>';
285
+ }
286
+ function __isSameOrigin(request, url) {
287
+ const fetchSite = request.headers.get('sec-fetch-site');
288
+ if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
289
+ return false;
290
+ }
291
+ const origin = request.headers.get('origin');
292
+ if (!origin)
293
+ return true;
294
+ try {
295
+ return new URL(origin).origin === url.origin;
296
+ }
297
+ catch {
298
+ return false;
299
+ }
300
+ }
301
+ function __secHeaders(response) {
302
+ for (const [key, value] of Object.entries(__defaultSecHeaders)) {
303
+ if (!response.headers.has(key))
304
+ response.headers.set(key, value);
305
+ }
306
+ return response;
307
+ }
308
+ function __attachCookies(response) {
309
+ const cookies = __getLocals().__setCookieHeaders;
310
+ if (cookies && cookies.length > 0) {
311
+ const newResponse = new Response(response.body, response);
312
+ for (const header of cookies)
313
+ newResponse.headers.append('Set-Cookie', header);
314
+ return __secHeaders(newResponse);
315
+ }
316
+ return __secHeaders(response);
317
+ }
318
+ async function __runRuntimeRequest(runtimeEntries, ctx, next) {
319
+ let idx = -1;
320
+ async function dispatch(i) {
321
+ if (i <= idx)
322
+ throw new Error('[kuratchi runtime] next() called multiple times in request phase');
323
+ idx = i;
324
+ const entry = runtimeEntries[i];
325
+ if (!entry)
326
+ return next();
327
+ const [, step] = entry;
328
+ if (typeof step.request !== 'function')
329
+ return dispatch(i + 1);
330
+ return await step.request(ctx, () => dispatch(i + 1));
331
+ }
332
+ return dispatch(0);
333
+ }
334
+ async function __runRuntimeRoute(runtimeEntries, ctx, next) {
335
+ let idx = -1;
336
+ async function dispatch(i) {
337
+ if (i <= idx)
338
+ throw new Error('[kuratchi runtime] next() called multiple times in route phase');
339
+ idx = i;
340
+ const entry = runtimeEntries[i];
341
+ if (!entry)
342
+ return next();
343
+ const [, step] = entry;
344
+ if (typeof step.route !== 'function')
345
+ return dispatch(i + 1);
346
+ return await step.route(ctx, () => dispatch(i + 1));
347
+ }
348
+ return dispatch(0);
349
+ }
350
+ async function __runRuntimeResponse(runtimeEntries, ctx, response) {
351
+ let out = response;
352
+ for (const [, step] of runtimeEntries) {
353
+ if (typeof step.response !== 'function')
354
+ continue;
355
+ out = await step.response(ctx, out);
356
+ if (!(out instanceof Response)) {
357
+ throw new Error('[kuratchi runtime] response handlers must return a Response');
358
+ }
359
+ }
360
+ return out;
361
+ }
362
+ async function __runRuntimeError(runtimeEntries, ctx, error) {
363
+ for (const [name, step] of runtimeEntries) {
364
+ if (typeof step.error !== 'function')
365
+ continue;
366
+ try {
367
+ const handled = await step.error(ctx, error);
368
+ if (handled instanceof Response)
369
+ return handled;
370
+ }
371
+ catch (hookErr) {
372
+ console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
373
+ }
374
+ }
375
+ return null;
376
+ }
377
+ function __isObject(value) {
378
+ return !!value && typeof value === 'object' && !Array.isArray(value);
379
+ }
380
+ function __normalizeRenderOutput(output) {
381
+ if (typeof output === 'string') {
382
+ return { html: output, head: '' };
383
+ }
384
+ return {
385
+ html: typeof output?.html === 'string' ? output.html : '',
386
+ head: typeof output?.head === 'string' ? output.head : '',
387
+ fragments: __isObject(output?.fragments) ? output.fragments : {},
388
+ };
389
+ }
390
+ function __getRuntimeEntries(runtimeDefinition) {
391
+ return Object.entries(runtimeDefinition ?? {}).filter((entry) => !!entry[1] && typeof entry[1] === 'object');
392
+ }
393
+ function __isDevMode() {
394
+ return !!globalThis.__kuratchi_DEV__;
395
+ }
396
+ const __defaultSecHeaders = {
397
+ 'X-Content-Type-Options': 'nosniff',
398
+ 'X-Frame-Options': 'DENY',
399
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
400
+ };
401
+ const __errorMessages = {
402
+ 400: 'Bad Request',
403
+ 401: 'Unauthorized',
404
+ 403: 'Forbidden',
405
+ 404: 'Not Found',
406
+ 405: 'Method Not Allowed',
407
+ 408: 'Request Timeout',
408
+ 429: 'Too Many Requests',
409
+ 500: 'Internal Server Error',
410
+ 502: 'Bad Gateway',
411
+ 503: 'Service Unavailable',
412
+ };
@@ -1,10 +1,11 @@
1
1
  export { createApp } from './app.js';
2
+ export { createGeneratedWorker } from './generated-worker.js';
2
3
  export { defineConfig } from './config.js';
3
4
  export { defineRuntime } from './runtime.js';
4
5
  export { Router, filePathToPattern } from './router.js';
5
6
  export { getCtx, getRequest, getLocals, getParams, getParam, RedirectError, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
6
7
  export { kuratchiDO, doRpc } from './do.js';
7
8
  export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './containers.js';
8
- export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, ApiRouteModule, HttpMethod, LayoutModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './types.js';
9
+ export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, ApiRouteModule, HttpMethod, LayoutModule, PageRenderOutput, PageRenderResult, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './types.js';
9
10
  export type { RpcOf } from './do.js';
10
11
  export { url, pathname, searchParams, headers, method, params, slug } from './request.js';
@@ -1,4 +1,5 @@
1
1
  export { createApp } from './app.js';
2
+ export { createGeneratedWorker } from './generated-worker.js';
2
3
  export { defineConfig } from './config.js';
3
4
  export { defineRuntime } from './runtime.js';
4
5
  export { Router, filePathToPattern } from './router.js';