@kuratchi/js 0.0.16 → 0.0.18

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 (38) hide show
  1. package/README.md +168 -11
  2. package/dist/cli.js +13 -13
  3. package/dist/compiler/client-module-pipeline.js +5 -5
  4. package/dist/compiler/compiler-shared.d.ts +18 -0
  5. package/dist/compiler/component-pipeline.js +4 -9
  6. package/dist/compiler/config-reading.d.ts +2 -1
  7. package/dist/compiler/config-reading.js +57 -0
  8. package/dist/compiler/durable-object-pipeline.js +1 -1
  9. package/dist/compiler/import-linking.js +2 -1
  10. package/dist/compiler/index.d.ts +6 -6
  11. package/dist/compiler/index.js +57 -23
  12. package/dist/compiler/layout-pipeline.js +6 -6
  13. package/dist/compiler/parser.js +10 -11
  14. package/dist/compiler/root-layout-pipeline.js +444 -429
  15. package/dist/compiler/route-pipeline.js +36 -41
  16. package/dist/compiler/route-state-pipeline.d.ts +1 -0
  17. package/dist/compiler/route-state-pipeline.js +3 -3
  18. package/dist/compiler/routes-module-feature-blocks.js +63 -63
  19. package/dist/compiler/routes-module-runtime-shell.js +65 -55
  20. package/dist/compiler/routes-module-types.d.ts +2 -1
  21. package/dist/compiler/server-module-pipeline.js +1 -1
  22. package/dist/compiler/template.js +24 -15
  23. package/dist/compiler/worker-output-pipeline.d.ts +1 -0
  24. package/dist/compiler/worker-output-pipeline.js +10 -2
  25. package/dist/create.js +1 -1
  26. package/dist/runtime/context.d.ts +4 -0
  27. package/dist/runtime/context.js +40 -2
  28. package/dist/runtime/do.js +21 -6
  29. package/dist/runtime/generated-worker.d.ts +22 -0
  30. package/dist/runtime/generated-worker.js +154 -23
  31. package/dist/runtime/index.d.ts +3 -1
  32. package/dist/runtime/index.js +1 -0
  33. package/dist/runtime/router.d.ts +5 -1
  34. package/dist/runtime/router.js +116 -31
  35. package/dist/runtime/security.d.ts +101 -0
  36. package/dist/runtime/security.js +312 -0
  37. package/dist/runtime/types.d.ts +21 -0
  38. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  import { stripTopLevelImports } from './parser.js';
2
- import { transpileTypeScript } from './transpile.js';
3
2
  import { buildDevAliasDeclarations, buildSegmentedScriptBody, rewriteImportedFunctionCalls, rewriteWorkerEnvAliases, } from './script-transform.js';
4
3
  function dedupe(items) {
5
4
  return Array.from(new Set(items));
@@ -151,15 +150,7 @@ export function analyzeRouteBuild(opts) {
151
150
  if (explicitLoadFunction && scriptUsesAwait) {
152
151
  throw new Error(`[kuratchi compiler] ${pattern}\nTop-level await cannot be mixed with export async function load(). Move async server work into load().`);
153
152
  }
154
- if (scriptBody) {
155
- scriptBody = transpileTypeScript(scriptBody, `route-script:${pattern}.ts`);
156
- }
157
- if (renderPreludeSource) {
158
- renderPreludeSource = transpileTypeScript(renderPreludeSource, `route-render:${pattern}.ts`);
159
- }
160
- if (explicitLoadFunction) {
161
- explicitLoadFunction = transpileTypeScript(explicitLoadFunction, `route-load:${pattern}.ts`);
162
- }
153
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
163
154
  const scriptReturnVars = parsed.script
164
155
  ? parsed.dataVars.filter((name) => !queryVars.includes(name) &&
165
156
  !importedBindingNames.has(name) &&
@@ -251,46 +242,50 @@ export function emitRouteObject(plan) {
251
242
  .map((rpc) => `'${rpc.rpcId}': ${rpc.expression}`)
252
243
  .join(', ');
253
244
  parts.push(` rpc: { ${rpcEntries} }`);
245
+ // Also emit allowedQueries for query override validation
246
+ const allowedQueryNames = plan.rpc.map((rpc) => `'${rpc.rpcId}'`).join(', ');
247
+ parts.push(` allowedQueries: [${allowedQueryNames}]`);
254
248
  }
255
249
  const destructure = `const { ${plan.render.dataVars.join(', ')} } = data;\n `;
256
250
  let finalHeadRenderBody = plan.render.headBody;
257
251
  if (plan.render.componentStyles.length > 0) {
258
252
  const lines = plan.render.headBody.split('\n');
259
- const styleLines = plan.render.componentStyles.map((css) => `__html += \`${css}\\n\`;`);
253
+ const styleLines = plan.render.componentStyles.map((css) => `__parts.push(\`${css}\\n\`);`);
260
254
  finalHeadRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
261
255
  }
262
256
  if (plan.render.clientModuleHref) {
263
- finalHeadRenderBody += `\n__html += \`<script type="module" src="${plan.render.clientModuleHref}"></script>\\n\`;`;
257
+ finalHeadRenderBody += `\n__parts.push(\`<script type="module" src="${plan.render.clientModuleHref}"></script>\\n\`);`;
264
258
  }
265
- parts.push(` render(data) {
266
- ${destructure}${plan.render.prelude ? plan.render.prelude + '\n ' : ''}const __head = (() => {
267
- ${finalHeadRenderBody}
268
- return __html;
269
- })();
270
- const __rendered = (() => {
271
- const __fragments = Object.create(null);
272
- const __fragmentStack = [];
273
- const __emit = (chunk) => {
274
- const __value = chunk == null ? '' : String(chunk);
275
- __html += __value;
276
- for (const __fragmentId of __fragmentStack) {
277
- __fragments[__fragmentId] = (__fragments[__fragmentId] || '') + __value;
278
- }
279
- };
280
- const __pushFragment = (id) => {
281
- const __key = String(id);
282
- if (!Object.prototype.hasOwnProperty.call(__fragments, __key)) {
283
- __fragments[__key] = '';
284
- }
285
- __fragmentStack.push(__key);
286
- };
287
- const __popFragment = () => {
288
- __fragmentStack.pop();
289
- };
290
- ${plan.render.body}
291
- return { html: __html, fragments: __fragments };
292
- })();
293
- return { html: __rendered.html, head: __head, fragments: __rendered.fragments };
259
+ parts.push(` render(data) {
260
+ ${destructure}${plan.render.prelude ? plan.render.prelude + '\n ' : ''}const __head = (() => {
261
+ ${finalHeadRenderBody}
262
+ return __html;
263
+ })();
264
+ const __rendered = (() => {
265
+ const __fragments = Object.create(null);
266
+ const __fragmentStack = [];
267
+ const __parts = [];
268
+ const __emit = (chunk) => {
269
+ const __value = chunk == null ? '' : String(chunk);
270
+ __parts.push(__value);
271
+ for (const __fragmentId of __fragmentStack) {
272
+ __fragments[__fragmentId] = (__fragments[__fragmentId] || '') + __value;
273
+ }
274
+ };
275
+ const __pushFragment = (id) => {
276
+ const __key = String(id);
277
+ if (!Object.prototype.hasOwnProperty.call(__fragments, __key)) {
278
+ __fragments[__key] = '';
279
+ }
280
+ __fragmentStack.push(__key);
281
+ };
282
+ const __popFragment = () => {
283
+ __fragmentStack.pop();
284
+ };
285
+ ${plan.render.body}
286
+ return { html: __parts.join(''), fragments: __fragments };
287
+ })();
288
+ return { html: __rendered.html, head: __head, fragments: __rendered.fragments };
294
289
  }`);
295
290
  return ` {\n${parts.join(',\n')}\n }`;
296
291
  }
@@ -22,4 +22,5 @@ export declare function assembleRouteState(opts: {
22
22
  fullPath: string;
23
23
  routesDir: string;
24
24
  layoutRelativePaths: string[];
25
+ fileContents?: Map<string, string>;
25
26
  }): RouteStatePlan;
@@ -9,7 +9,7 @@ function isSharedImport(line) {
9
9
  return /\bfrom\s+['"]\$shared\//.test(line);
10
10
  }
11
11
  export function assembleRouteState(opts) {
12
- const { parsed, fullPath, routesDir, layoutRelativePaths } = opts;
12
+ const { parsed, fullPath, routesDir, layoutRelativePaths, fileContents } = opts;
13
13
  let effectiveTemplate = parsed.template;
14
14
  const routeScriptParts = [];
15
15
  const routeScriptSegments = [];
@@ -44,9 +44,9 @@ export function assembleRouteState(opts) {
44
44
  if (layoutRelPath === 'layout.html')
45
45
  continue;
46
46
  const layoutPath = path.join(routesDir, layoutRelPath);
47
- if (!fs.existsSync(layoutPath))
47
+ const layoutSource = fileContents?.get(layoutPath) ?? (fs.existsSync(layoutPath) ? fs.readFileSync(layoutPath, 'utf-8') : null);
48
+ if (!layoutSource)
48
49
  continue;
49
- const layoutSource = fs.readFileSync(layoutPath, 'utf-8');
50
50
  const layoutParsed = parseFile(layoutSource, { kind: 'layout', filePath: layoutPath });
51
51
  if (layoutParsed.loadFunction) {
52
52
  throw new Error(`${layoutRelPath} cannot export load(); nested layouts currently share the child route load lifecycle.`);
@@ -2,7 +2,7 @@ import * as path from 'node:path';
2
2
  import { toSafeIdentifier } from './compiler-shared.js';
3
3
  export function buildRoutesModuleFeatureBlocks(opts) {
4
4
  const workerImport = `import { env as __env } from 'cloudflare:workers';`;
5
- const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${opts.runtimeContextImport}';`;
5
+ const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, __getCsrfToken, __signFragment, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${opts.runtimeContextImport}';`;
6
6
  const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
7
7
  ? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
8
8
  : '';
@@ -30,30 +30,30 @@ function buildAuthSessionInit(opts) {
30
30
  if (!opts.authConfig?.sessionEnabled)
31
31
  return '';
32
32
  const cookieName = opts.authConfig.cookieName;
33
- return `
34
- // Auth Session Init
35
-
36
- function __parseCookies(header) {
37
- const map = {};
38
- if (!header) return map;
39
- for (const pair of header.split(';')) {
40
- const eq = pair.indexOf('=');
41
- if (eq === -1) continue;
42
- map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
43
- }
44
- return map;
45
- }
46
-
47
- function __initAuth(request) {
48
- const cookies = __parseCookies(request.headers.get('cookie'));
49
- __setLocal('session', null);
50
- __setLocal('user', null);
51
- __setLocal('auth', {
52
- cookies,
53
- sessionCookie: cookies['${cookieName}'] || null,
54
- cookieName: '${cookieName}',
55
- });
56
- }
33
+ return `
34
+ // Auth Session Init
35
+
36
+ function __parseCookies(header) {
37
+ const map = {};
38
+ if (!header) return map;
39
+ for (const pair of header.split(';')) {
40
+ const eq = pair.indexOf('=');
41
+ if (eq === -1) continue;
42
+ map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
43
+ }
44
+ return map;
45
+ }
46
+
47
+ function __initAuth(request) {
48
+ const cookies = __parseCookies(request.headers.get('cookie'));
49
+ __setLocal('session', null);
50
+ __setLocal('user', null);
51
+ __setLocal('auth', {
52
+ cookies,
53
+ sessionCookie: cookies['${cookieName}'] || null,
54
+ cookieName: '${cookieName}',
55
+ });
56
+ }
57
57
  `;
58
58
  }
59
59
  function buildOrmMigrationBlock(opts) {
@@ -78,38 +78,38 @@ function buildOrmMigrationBlock(opts) {
78
78
  `import { kuratchiORM } from '@kuratchi/orm';`,
79
79
  ...schemaImports,
80
80
  ].join('\n'),
81
- migrationInit: `
82
- // ORM Auto-Migration
83
-
84
- let __migrated = false;
85
- const __ormDatabases = [
86
- ${migrateEntries.join(',\n')}
87
- ];
88
-
89
- async function __runMigrations() {
90
- if (__migrated) return;
91
- __migrated = true;
92
- for (const db of __ormDatabases) {
93
- const binding = __env[db.binding];
94
- if (!binding) continue;
95
- try {
96
- const executor = (sql, params) => {
97
- let stmt = binding.prepare(sql);
98
- if (params?.length) stmt = stmt.bind(...params);
99
- return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
100
- };
101
- const result = await runMigrations({ execute: executor, schema: db.schema });
102
- if (result.applied) {
103
- console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
104
- }
105
- if (result.warnings.length) {
106
- result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
107
- }
108
- } catch (err) {
109
- console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
110
- }
111
- }
112
- }
81
+ migrationInit: `
82
+ // ORM Auto-Migration
83
+
84
+ let __migrated = false;
85
+ const __ormDatabases = [
86
+ ${migrateEntries.join(',\n')}
87
+ ];
88
+
89
+ async function __runMigrations() {
90
+ if (__migrated) return;
91
+ __migrated = true;
92
+ for (const db of __ormDatabases) {
93
+ const binding = __env[db.binding];
94
+ if (!binding) continue;
95
+ try {
96
+ const executor = (sql, params) => {
97
+ let stmt = binding.prepare(sql);
98
+ if (params?.length) stmt = stmt.bind(...params);
99
+ return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
100
+ };
101
+ const result = await runMigrations({ execute: executor, schema: db.schema });
102
+ if (result.applied) {
103
+ console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
104
+ }
105
+ if (result.warnings.length) {
106
+ result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
107
+ }
108
+ } catch (err) {
109
+ console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
110
+ }
111
+ }
112
+ }
113
113
  `,
114
114
  };
115
115
  }
@@ -163,12 +163,12 @@ function buildAuthPluginBlock(opts) {
163
163
  }
164
164
  return {
165
165
  authPluginImports: imports.join('\n'),
166
- authPluginInit: `
167
- // Auth Plugin Init
168
-
169
- function __initAuthPlugins() {
170
- ${initLines.join('\n')}
171
- }
166
+ authPluginInit: `
167
+ // Auth Plugin Init
168
+
169
+ function __initAuthPlugins() {
170
+ ${initLines.join('\n')}
171
+ }
172
172
  `,
173
173
  };
174
174
  }
@@ -22,60 +22,70 @@ export function buildRoutesModuleRuntimeShell(opts, blocks) {
22
22
  opts.authConfig?.hasGuards ? ` { const __gRes = __checkGuard(); if (__gRes) return __gRes; }` : '',
23
23
  ' return null;',
24
24
  ].filter(Boolean).join('\n');
25
- return `// Generated by KuratchiJS compiler - do not edit.
26
- ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
27
- ${blocks.workerImport}
28
- import { createGeneratedWorker } from '${opts.runtimeWorkerImport}';
29
- ${blocks.contextImport}
30
- ${blocks.runtimeImport ? blocks.runtimeImport + '\n' : ''}${blocks.migrationImports ? blocks.migrationImports + '\n' : ''}${blocks.authPluginImports ? blocks.authPluginImports + '\n' : ''}${blocks.doImports ? blocks.doImports + '\n' : ''}${opts.serverImports.join('\n')}
31
- ${blocks.workflowStatusRpc}
32
-
33
- // Assets
34
-
35
- const __assets = {
36
- ${opts.compiledAssets.map((asset) => ` ${JSON.stringify(asset.name)}: { content: ${JSON.stringify(asset.content)}, mime: ${JSON.stringify(asset.mime)}, etag: ${JSON.stringify(asset.etag)} }`).join(',\n')}
37
- };
38
-
39
- // Layout
40
-
41
- ${layoutBlock}
42
-
43
- ${layoutActionsBlock}
44
-
45
- // Error pages
46
-
47
- ${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}const __customErrors = {
48
- ${customErrorEntries}
49
- };
50
-
51
- ${componentBlock}${blocks.migrationInit}${blocks.authInit}${blocks.authPluginInit}${blocks.doResolverInit}${blocks.doClassCode}
52
- // Route definitions
53
-
54
- const routes = [
55
- ${opts.compiledRoutes.join(',\n')}
56
- ];
57
-
58
- const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
59
-
60
- async function __initializeRequest(ctx) {
61
- ${initializeRequestBody || ' return;'}
62
- }
63
-
64
- async function __preRouteChecks() {
65
- ${preRouteChecksBody}
66
- }
67
-
68
- export default createGeneratedWorker({
69
- routes,
70
- layout: __layout,
71
- layoutActions: __layoutActions,
72
- assetsPrefix: ${JSON.stringify(opts.assetsPrefix)},
73
- assets: __assets,
74
- errorPages: __customErrors,
75
- runtimeDefinition: __runtimeDef,
76
- workflowStatusRpc: typeof __workflowStatusRpc !== 'undefined' ? __workflowStatusRpc : {},
77
- initializeRequest: __initializeRequest,
78
- preRouteChecks: __preRouteChecks,
79
- });
25
+ return `// Generated by KuratchiJS compiler - do not edit.
26
+ ${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
27
+ ${blocks.workerImport}
28
+ import { createGeneratedWorker } from '${opts.runtimeWorkerImport}';
29
+ ${blocks.contextImport}
30
+ ${blocks.runtimeImport ? blocks.runtimeImport + '\n' : ''}${blocks.migrationImports ? blocks.migrationImports + '\n' : ''}${blocks.authPluginImports ? blocks.authPluginImports + '\n' : ''}${blocks.doImports ? blocks.doImports + '\n' : ''}${opts.serverImports.join('\n')}
31
+ ${blocks.workflowStatusRpc}
32
+
33
+ // Assets
34
+
35
+ const __assets = {
36
+ ${opts.compiledAssets.map((asset) => ` ${JSON.stringify(asset.name)}: { content: ${JSON.stringify(asset.content)}, mime: ${JSON.stringify(asset.mime)}, etag: ${JSON.stringify(asset.etag)} }`).join(',\n')}
37
+ };
38
+
39
+ // Layout
40
+
41
+ ${layoutBlock}
42
+
43
+ ${layoutActionsBlock}
44
+
45
+ // Error pages
46
+
47
+ ${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}const __customErrors = {
48
+ ${customErrorEntries}
49
+ };
50
+
51
+ ${componentBlock}${blocks.migrationInit}${blocks.authInit}${blocks.authPluginInit}${blocks.doResolverInit}${blocks.doClassCode}
52
+ // Route definitions
53
+
54
+ const routes = [
55
+ ${opts.compiledRoutes.join(',\n')}
56
+ ];
57
+
58
+ const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
59
+
60
+ async function __initializeRequest(ctx) {
61
+ ${initializeRequestBody || ' return;'}
62
+ }
63
+
64
+ async function __preRouteChecks() {
65
+ ${preRouteChecksBody}
66
+ }
67
+
68
+ export default createGeneratedWorker({
69
+ routes,
70
+ layout: __layout,
71
+ layoutActions: __layoutActions,
72
+ assetsPrefix: ${JSON.stringify(opts.assetsPrefix)},
73
+ assets: __assets,
74
+ errorPages: __customErrors,
75
+ runtimeDefinition: __runtimeDef,
76
+ workflowStatusRpc: typeof __workflowStatusRpc !== 'undefined' ? __workflowStatusRpc : {},
77
+ initializeRequest: __initializeRequest,
78
+ preRouteChecks: __preRouteChecks,
79
+ security: {
80
+ csrfEnabled: ${opts.securityConfig.csrfEnabled},
81
+ csrfCookieName: ${JSON.stringify(opts.securityConfig.csrfCookieName)},
82
+ csrfHeaderName: ${JSON.stringify(opts.securityConfig.csrfHeaderName)},
83
+ rpcRequireAuth: ${opts.securityConfig.rpcRequireAuth},
84
+ actionRequireAuth: ${opts.securityConfig.actionRequireAuth},
85
+ contentSecurityPolicy: ${JSON.stringify(opts.securityConfig.contentSecurityPolicy)},
86
+ strictTransportSecurity: ${JSON.stringify(opts.securityConfig.strictTransportSecurity)},
87
+ permissionsPolicy: ${JSON.stringify(opts.securityConfig.permissionsPolicy)},
88
+ },
89
+ });
80
90
  `;
81
91
  }
@@ -1,4 +1,4 @@
1
- import type { AuthConfigEntry, DoConfigEntry, DoHandlerEntry, OrmDatabaseEntry, WorkerClassConfigEntry } from './compiler-shared.js';
1
+ import type { AuthConfigEntry, DoConfigEntry, DoHandlerEntry, OrmDatabaseEntry, SecurityConfigEntry, WorkerClassConfigEntry } from './compiler-shared.js';
2
2
  export interface CompiledAssetEntry {
3
3
  name: string;
4
4
  content: string;
@@ -16,6 +16,7 @@ export interface GenerateRoutesModuleOptions {
16
16
  compiledErrorPages: Map<number, string>;
17
17
  ormDatabases: OrmDatabaseEntry[];
18
18
  authConfig: AuthConfigEntry | null;
19
+ securityConfig: SecurityConfigEntry;
19
20
  doConfig: DoConfigEntry[];
20
21
  doHandlers: DoHandlerEntry[];
21
22
  workflowConfig: WorkerClassConfigEntry[];
@@ -34,7 +34,7 @@ export function createServerModuleCompiler(options) {
34
34
  const proxyNoExt = doHandlerProxyPaths.get(normalizedNoExt);
35
35
  if (!proxyNoExt)
36
36
  return null;
37
- return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.js') ? proxyNoExt + '.js' : null);
37
+ return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.ts') ? proxyNoExt + '.ts' : null);
38
38
  }
39
39
  function resolveImportTarget(importerAbs, spec) {
40
40
  if (spec.startsWith('$')) {
@@ -148,7 +148,9 @@ export function splitTemplateRenderSections(template) {
148
148
  };
149
149
  }
150
150
  function buildAppendStatement(expression, emitCall) {
151
- return emitCall ? `${emitCall}(${expression});` : `__html += ${expression};`;
151
+ // When emitCall is provided, use it (e.g., __emit(expr))
152
+ // Otherwise, use array push for O(n) performance
153
+ return emitCall ? `${emitCall}(${expression});` : `__parts.push(${expression});`;
152
154
  }
153
155
  function extractPollFragmentExpr(tagText, rpcNameMap) {
154
156
  const pollMatch = tagText.match(/\bdata-poll=\{([\s\S]*?)\}/);
@@ -275,7 +277,9 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
275
277
  if (options.enableFragmentManifest) {
276
278
  template = instrumentPollFragments(template, rpcNameMap);
277
279
  }
278
- const out = ['let __html = "";'];
280
+ // Use array accumulation for O(n) performance instead of O(n²) string concatenation
281
+ const useArrayAccum = !emitCall;
282
+ const out = useArrayAccum ? ['const __parts = [];'] : ['let __html = "";'];
279
283
  const lines = template.split('\n');
280
284
  let inStyle = false;
281
285
  let inScript = false;
@@ -422,6 +426,10 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
422
426
  }
423
427
  out.push(...compileHtmlLineStatements(htmlLine, actionNames, rpcNameMap, options));
424
428
  }
429
+ // For non-emit mode, add final join to produce __html
430
+ if (!emitCall) {
431
+ out.push('let __html = __parts.join(\'\');');
432
+ }
425
433
  return out.join('\n');
426
434
  }
427
435
  /**
@@ -495,9 +503,9 @@ function transformClientScriptBlock(block) {
495
503
  const openTag = match[1];
496
504
  const body = match[2];
497
505
  const closeTag = match[3];
506
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
498
507
  if (!/\$\s*:/.test(body)) {
499
- const transpiled = transpileTypeScript(body, 'client-script.ts');
500
- return `${openTag}${transpiled}${closeTag}`;
508
+ return `${openTag}${body}${closeTag}`;
501
509
  }
502
510
  const out = [];
503
511
  const lines = body.split('\n');
@@ -593,8 +601,8 @@ function transformClientScriptBlock(block) {
593
601
  break;
594
602
  }
595
603
  out.splice(insertAt, 0, 'const __k$ = window.__kuratchiReactive;');
596
- const transpiled = transpileTypeScript(out.join('\n'), 'client-script.ts');
597
- return `${openTag}${transpiled}${closeTag}`;
604
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
605
+ return `${openTag}${out.join('\n')}${closeTag}`;
598
606
  }
599
607
  function braceDelta(line) {
600
608
  let delta = 0;
@@ -1021,7 +1029,7 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
1021
1029
  continue;
1022
1030
  }
1023
1031
  else if (attrName === 'data-poll') {
1024
- // data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]" data-poll-id="stable-id"
1032
+ // data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]" data-poll-id="signed-id"
1025
1033
  const pollCallMatch = inner.match(/^(\w+)\((.*)\)$/);
1026
1034
  if (pollCallMatch) {
1027
1035
  const fnName = pollCallMatch[1];
@@ -1029,16 +1037,16 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
1029
1037
  const argsExpr = pollCallMatch[2].trim();
1030
1038
  // Remove the trailing "data-poll=" we already appended
1031
1039
  result = result.replace(/\s*data-poll=$/, '');
1032
- // Emit data-poll, data-poll-args, and stable data-poll-id (based on fn + args expression)
1040
+ // Emit data-poll, data-poll-args, and signed data-poll-id (signed at runtime for security)
1033
1041
  result += ` data-poll="${rpcName}"`;
1034
1042
  if (argsExpr) {
1035
1043
  result += ` data-poll-args="\${__esc(JSON.stringify([${argsExpr}]))}"`;
1036
- // Stable ID based on args so same data produces same ID across renders
1037
- result += ` data-poll-id="\${__esc('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
1044
+ // Sign the fragment ID at runtime with __signFragment(fragmentId, routePath)
1045
+ result += ` data-poll-id="\${__signFragment('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
1038
1046
  }
1039
1047
  else {
1040
- // No args - use function name as ID
1041
- result += ` data-poll-id="__poll_${rpcName}"`;
1048
+ // No args - sign with function name as base ID
1049
+ result += ` data-poll-id="\${__signFragment('__poll_${rpcName}')}"`;
1042
1050
  }
1043
1051
  }
1044
1052
  pos = closeIdx + 1;
@@ -1054,12 +1062,13 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
1054
1062
  if (!isServerAction) {
1055
1063
  throw new Error(`Invalid action expression: "${inner}". Use action={myActionFn} for server actions.`);
1056
1064
  }
1057
- // Remove trailing `action=` from output and inject _action hidden field.
1065
+ // Remove trailing `action=` from output and inject _action hidden field + CSRF token.
1058
1066
  result = result.replace(/\s*action=$/, '');
1059
1067
  const actionValue = actionNames === undefined
1060
1068
  ? `\${__esc(${inner})}`
1061
1069
  : inner;
1062
- pendingActionHiddenInput = `\\n<input type="hidden" name="_action" value="${actionValue}">`;
1070
+ // Inject both _action and _csrf hidden fields for server action forms
1071
+ pendingActionHiddenInput = `\\n<input type="hidden" name="_action" value="${actionValue}">\\n<input type="hidden" name="_csrf" value="\${__getCsrfToken()}">`;
1063
1072
  pos = closeIdx + 1;
1064
1073
  continue;
1065
1074
  }
@@ -1199,4 +1208,4 @@ export function generateRenderFunction(template) {
1199
1208
  ${body}
1200
1209
  }`;
1201
1210
  }
1202
- import { transpileTypeScript } from './transpile.js';
1211
+ // TypeScript transpilation removed wrangler's esbuild handles it
@@ -11,3 +11,4 @@ export declare function buildWorkerEntrypointSource(opts: {
11
11
  doClassNames: string[];
12
12
  workerClassEntries: WorkerClassExportEntry[];
13
13
  }): string;
14
+ export declare function buildCompatEntrypointSource(targetFile: string): string;
@@ -29,9 +29,17 @@ export function buildWorkerEntrypointSource(opts) {
29
29
  });
30
30
  return [
31
31
  '// Auto-generated by kuratchi — do not edit.',
32
- "export { default } from './routes.js';",
33
- ...opts.doClassNames.map((className) => `export { ${className} } from './routes.js';`),
32
+ "export { default } from './routes.ts';",
33
+ ...opts.doClassNames.map((className) => `export { ${className} } from './routes.ts';`),
34
34
  ...workerClassExports,
35
35
  '',
36
36
  ].join('\n');
37
37
  }
38
+ export function buildCompatEntrypointSource(targetFile) {
39
+ return [
40
+ '// Auto-generated by kuratchi — do not edit.',
41
+ `export { default } from '${targetFile}';`,
42
+ `export * from '${targetFile}';`,
43
+ '',
44
+ ].join('\n');
45
+ }
package/dist/create.js CHANGED
@@ -278,7 +278,7 @@ function genPackageJson(opts) {
278
278
  function genWrangler(opts) {
279
279
  const config = {
280
280
  name: opts.name,
281
- main: '.kuratchi/worker.js',
281
+ main: '.kuratchi/worker.ts',
282
282
  compatibility_date: new Date().toISOString().split('T')[0],
283
283
  compatibility_flags: ['nodejs_compat'],
284
284
  };
@@ -57,3 +57,7 @@ export declare function __esc(v: any): string;
57
57
  export declare function __rawHtml(v: any): string;
58
58
  /** Best-effort HTML sanitizer for {@html ...} template output. */
59
59
  export declare function __sanitizeHtml(v: any): string;
60
+ /** Get CSRF token for form injection (used by template compiler) */
61
+ export declare function __getCsrfToken(): string;
62
+ /** Sign a fragment ID for secure polling (used by template compiler) */
63
+ export declare function __signFragment(fragmentId: string): string;
@@ -161,13 +161,51 @@ export function __rawHtml(v) {
161
161
  /** Best-effort HTML sanitizer for {@html ...} template output. */
162
162
  export function __sanitizeHtml(v) {
163
163
  let html = __rawHtml(v);
164
+ // Remove dangerous elements entirely
164
165
  html = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
165
166
  html = html.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, '');
166
167
  html = html.replace(/<object\b[^>]*>[\s\S]*?<\/object>/gi, '');
167
168
  html = html.replace(/<embed\b[^>]*>/gi, '');
169
+ html = html.replace(/<base\b[^>]*>/gi, '');
170
+ html = html.replace(/<meta\b[^>]*>/gi, '');
171
+ html = html.replace(/<link\b[^>]*>/gi, '');
172
+ html = html.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
173
+ html = html.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, '');
174
+ html = html.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, '');
175
+ // Remove all event handlers (on*)
168
176
  html = html.replace(/\son[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
169
- html = html.replace(/\s(href|src|xlink:href)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi, ' $1="#"');
170
- html = html.replace(/\s(href|src|xlink:href)\s*=\s*javascript:[^\s>]+/gi, ' $1="#"');
177
+ // Remove javascript: URLs in href, src, xlink:href, action, formaction, data
178
+ html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi, ' $1="#"');
179
+ html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*javascript:[^\s>]+/gi, ' $1="#"');
180
+ // Remove vbscript: URLs
181
+ html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*(["'])\s*vbscript:[\s\S]*?\2/gi, ' $1="#"');
182
+ html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*vbscript:[^\s>]+/gi, ' $1="#"');
183
+ // Remove data: URLs in src (can contain scripts)
184
+ html = html.replace(/\ssrc\s*=\s*(["'])\s*data:[\s\S]*?\1/gi, ' src="#"');
185
+ html = html.replace(/\ssrc\s*=\s*data:[^\s>]+/gi, ' src="#"');
186
+ // Remove srcdoc (can contain arbitrary HTML)
171
187
  html = html.replace(/\ssrcdoc\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
188
+ // Remove form-related dangerous attributes
189
+ html = html.replace(/\sformaction\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
190
+ // Remove SVG-specific dangerous elements
191
+ html = html.replace(/<foreignObject\b[^>]*>[\s\S]*?<\/foreignObject>/gi, '');
192
+ html = html.replace(/<use\b[^>]*>/gi, '');
172
193
  return html;
173
194
  }
195
+ /** Get CSRF token for form injection (used by template compiler) */
196
+ export function __getCsrfToken() {
197
+ return __locals.__csrfToken || '';
198
+ }
199
+ /** Sign a fragment ID for secure polling (used by template compiler) */
200
+ export function __signFragment(fragmentId) {
201
+ const token = __locals.__csrfToken || '';
202
+ const routePath = __locals.__currentRoutePath || '/';
203
+ const payload = `${fragmentId}:${routePath}:${token}`;
204
+ // FNV-1a hash for fast, consistent signing
205
+ let hash = 2166136261;
206
+ for (let i = 0; i < payload.length; i++) {
207
+ hash ^= payload.charCodeAt(i);
208
+ hash = (hash * 16777619) >>> 0;
209
+ }
210
+ return `${fragmentId}:${hash.toString(36)}`;
211
+ }