@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
package/README.md CHANGED
@@ -16,9 +16,9 @@ cd my-app
16
16
  bun run dev
17
17
  ```
18
18
 
19
- ## How it works
20
-
21
- `kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
19
+ ## How it works
20
+
21
+ `kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
22
22
 
23
23
  | File | Purpose |
24
24
  |---|---|
@@ -26,13 +26,13 @@ bun run dev
26
26
  | `.kuratchi/worker.js` | Stable wrangler entry - re-exports the fetch handler plus all Durable Object and Agent classes |
27
27
  | `.kuratchi/do/*.js` | Generated Durable Object RPC proxy modules for `$durable-objects/*` imports |
28
28
 
29
- Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
30
-
31
- For the framework's internal compiler/runtime orchestration and tracked implementation roadmap, see [ARCHITECTURE.md](./ARCHITECTURE.md).
32
-
33
- ```jsonc
34
- // wrangler.jsonc
35
- {
29
+ Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
30
+
31
+ For the framework's internal compiler/runtime orchestration and tracked implementation roadmap, see [ARCHITECTURE.md](./ARCHITECTURE.md).
32
+
33
+ ```jsonc
34
+ // wrangler.jsonc
35
+ {
36
36
  "main": ".kuratchi/worker.js"
37
37
  }
38
38
  ```
@@ -766,9 +766,166 @@ Kuratchi also exposes a framework build-mode flag:
766
766
  - `dev` is compile-time framework state, not a generic process env var
767
767
  - `@kuratchi/js/environment` is intended for server route code, not client `$:` scripts
768
768
 
769
+ ## Security
770
+
771
+ Kuratchi includes built-in security features that are enabled by default or configurable via `kuratchi.config.ts`.
772
+
773
+ ### Default Security Headers
774
+
775
+ All responses include these headers automatically:
776
+
777
+ - `X-Content-Type-Options: nosniff`
778
+ - `X-Frame-Options: DENY`
779
+ - `Referrer-Policy: strict-origin-when-cross-origin`
780
+
781
+ ### CSRF Protection
782
+
783
+ CSRF protection is **enabled by default** for all form actions and RPC calls.
784
+
785
+ **How it works:**
786
+ 1. A cryptographically random token is generated per session and stored in a cookie
787
+ 2. The compiler auto-injects a hidden `_csrf` field into forms with `action={fn}`
788
+ 3. The client bridge includes the CSRF token header in fetch action requests
789
+ 4. Server validates the token using timing-safe comparison
790
+
791
+ No configuration required — it just works.
792
+
793
+ ```html
794
+ <!-- CSRF token is auto-injected -->
795
+ <form action={submitForm}>
796
+ <input type="text" name="email" />
797
+ <button type="submit">Submit</button>
798
+ </form>
799
+ ```
800
+
801
+ ### Authentication Enforcement
802
+
803
+ Optionally require authentication for all RPC calls or form actions:
804
+
805
+ ```ts
806
+ // kuratchi.config.ts
807
+ export default defineConfig({
808
+ security: {
809
+ rpcRequireAuth: true, // Require auth for all RPC calls (default: false)
810
+ actionRequireAuth: true, // Require auth for all form actions (default: false)
811
+ },
812
+ });
813
+ ```
814
+
815
+ When enabled, unauthenticated requests return `401 Authentication required`. The check looks for `locals.user` or `locals.session.user`, which is populated by `@kuratchi/auth`.
816
+
817
+ For per-function control, use guards in individual functions instead:
818
+
819
+ ```ts
820
+ import { requireAuth } from '@kuratchi/auth';
821
+
822
+ export async function deleteItem(formData: FormData) {
823
+ await requireAuth(); // Throws 401 if not authenticated
824
+ // ... action logic
825
+ }
826
+ ```
827
+
828
+ ### Configurable Security Headers
829
+
830
+ Add CSP, HSTS, and Permissions-Policy headers:
831
+
832
+ ```ts
833
+ // kuratchi.config.ts
834
+ export default defineConfig({
835
+ security: {
836
+ contentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
837
+ strictTransportSecurity: "max-age=31536000; includeSubDomains",
838
+ permissionsPolicy: "camera=(), microphone=(), geolocation=()",
839
+ },
840
+ });
841
+ ```
842
+
843
+ ### HTML Sanitization
844
+
845
+ The `{@html}` directive automatically sanitizes output to prevent XSS:
846
+
847
+ - Removes dangerous elements: `<script>`, `<iframe>`, `<object>`, `<embed>`, `<style>`, `<template>`, etc.
848
+ - Strips all `on*` event handlers
849
+ - Neutralizes `javascript:` and `vbscript:` URLs
850
+ - Removes `data:` URLs from `src` attributes
851
+
852
+ For user-generated HTML, we recommend using DOMPurify on the client side for maximum security.
853
+
854
+ ### Fragment Refresh Security
855
+
856
+ Fragment IDs used for `data-poll` are automatically signed to prevent attackers from probing for data:
857
+
858
+ - Fragment IDs are signed at render time with the session's CSRF token
859
+ - Server validates signatures before returning fragment content
860
+ - Invalid or unsigned fragments return 403 when CSRF is enabled
861
+
862
+ This is automatic — no configuration required.
863
+
864
+ ### Query Override Protection
865
+
866
+ Query function calls via `x-kuratchi-query-fn` headers are validated against a whitelist:
867
+
868
+ - Only query functions registered for the current route can be called
869
+ - Prevents attackers from invoking arbitrary RPC functions
870
+ - Returns 403 for unauthorized query function calls
871
+
872
+ This is automatic — no configuration required.
873
+
874
+ ### Client Bridge Security
875
+
876
+ Client-side handler invocation is protected against injection attacks:
877
+
878
+ - Route and handler IDs are validated against safe patterns
879
+ - Prototype pollution attempts are blocked (`__proto__`, `constructor`, `prototype`)
880
+ - Uses `hasOwnProperty` checks to prevent prototype chain traversal
881
+
882
+ This is automatic — no configuration required.
883
+
884
+ ### Error Information Protection
885
+
886
+ Error messages are sanitized to prevent information leakage in production:
887
+
888
+ - Generic errors show full details in dev mode only
889
+ - Production uses safe fallback messages ("Internal Server Error", "Action failed")
890
+ - `ActionError` and `PageError` messages are always shown (developer-controlled)
891
+
892
+ ```ts
893
+ // Safe to show - developer-controlled message
894
+ throw new ActionError('Invalid email format');
895
+
896
+ // In production: "Internal Server Error" (details hidden)
897
+ // In dev mode: Full error message for debugging
898
+ throw new Error('Database connection failed at line 42');
899
+ ```
900
+
901
+ ### Full Security Configuration
902
+
903
+ ```ts
904
+ // kuratchi.config.ts
905
+ export default defineConfig({
906
+ security: {
907
+ // CSRF Protection (enabled by default)
908
+ csrfEnabled: true,
909
+ csrfCookieName: '__kuratchi_csrf',
910
+ csrfHeaderName: 'x-kuratchi-csrf',
911
+
912
+ // Authentication Enforcement
913
+ rpcRequireAuth: false, // Require auth for RPC calls
914
+ actionRequireAuth: false, // Require auth for form actions
915
+
916
+ // Security Headers
917
+ contentSecurityPolicy: "default-src 'self'",
918
+ strictTransportSecurity: "max-age=31536000; includeSubDomains",
919
+ permissionsPolicy: "camera=(), microphone=()",
920
+ },
921
+ });
922
+ ```
923
+
924
+ For a comprehensive security analysis and roadmap, see [SECURITY.md](./SECURITY.md).
925
+
769
926
  ## `kuratchi.config.ts`
770
927
 
771
- Optional. Required only when using framework integrations (ORM, auth, UI).
928
+ Optional. Required only when using framework integrations (ORM, auth, UI, security).
772
929
 
773
930
  **Durable Objects are auto-discovered** — no config needed unless you need `stubId` for auth integration.
774
931
 
package/dist/cli.js CHANGED
@@ -30,14 +30,14 @@ async function main() {
30
30
  await runCreate();
31
31
  return;
32
32
  default:
33
- console.log(`
34
- KuratchiJS CLI
35
-
36
- Usage:
37
- kuratchi create [name] Scaffold a new KuratchiJS project
38
- kuratchi build Compile routes once
39
- kuratchi dev Compile, watch for changes, and start wrangler dev server
40
- kuratchi watch Compile + watch only (no wrangler — for custom setups)
33
+ console.log(`
34
+ KuratchiJS CLI
35
+
36
+ Usage:
37
+ kuratchi create [name] Scaffold a new KuratchiJS project
38
+ kuratchi build Compile routes once
39
+ kuratchi dev Compile, watch for changes, and start wrangler dev server
40
+ kuratchi watch Compile + watch only (no wrangler — for custom setups)
41
41
  `);
42
42
  process.exit(1);
43
43
  }
@@ -49,10 +49,10 @@ async function runCreate() {
49
49
  const positional = remaining.filter(a => !a.startsWith('-'));
50
50
  await create(positional[0], flags);
51
51
  }
52
- function runBuild(isDev = false) {
52
+ async function runBuild(isDev = false) {
53
53
  console.log('[kuratchi] Compiling...');
54
54
  try {
55
- const outFile = compile({ projectDir, isDev });
55
+ const outFile = await compile({ projectDir, isDev });
56
56
  console.log(`[kuratchi] Built → ${path.relative(projectDir, outFile)}`);
57
57
  }
58
58
  catch (err) {
@@ -61,7 +61,7 @@ function runBuild(isDev = false) {
61
61
  }
62
62
  }
63
63
  async function runWatch(withWrangler = false) {
64
- runBuild(true);
64
+ await runBuild(true);
65
65
  const routesDir = path.join(projectDir, 'src', 'routes');
66
66
  const serverDir = path.join(projectDir, 'src', 'server');
67
67
  const watchDirs = [routesDir, serverDir].filter(d => fs.existsSync(d));
@@ -69,10 +69,10 @@ async function runWatch(withWrangler = false) {
69
69
  const triggerRebuild = () => {
70
70
  if (rebuildTimeout)
71
71
  clearTimeout(rebuildTimeout);
72
- rebuildTimeout = setTimeout(() => {
72
+ rebuildTimeout = setTimeout(async () => {
73
73
  console.log('[kuratchi] File changed, rebuilding...');
74
74
  try {
75
- compile({ projectDir, isDev: true });
75
+ await compile({ projectDir, isDev: true });
76
76
  console.log('[kuratchi] Rebuilt.');
77
77
  }
78
78
  catch (err) {
@@ -2,7 +2,6 @@ import * as crypto from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { collectReferencedIdentifiers, parseImportStatement } from './import-linking.js';
5
- import { transpileTypeScript } from './transpile.js';
6
5
  function resolveExistingModuleFile(absBase) {
7
6
  const candidates = [
8
7
  absBase,
@@ -179,12 +178,13 @@ class CompilerBackedClientRouteRegistry {
179
178
  const registrationEntries = Array.from(this.handlerByKey.values()).map((record) => {
180
179
  return `${JSON.stringify(record.id)}: (args, event, element) => ${record.calleeExpr}(...args, event, element)`;
181
180
  });
182
- const source = transpileTypeScript([
181
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
182
+ const source = [
183
183
  ...importLines,
184
184
  `window.__kuratchiClient?.register(${JSON.stringify(this.routeId)}, {`,
185
185
  registrationEntries.map((entry) => ` ${entry},`).join('\n'),
186
186
  `});`,
187
- ].join('\n'), `client-route:${this.routeId}.ts`);
187
+ ].join('\n');
188
188
  const asset = buildAsset(assetName, source);
189
189
  this.compiler.registerAsset(asset);
190
190
  return { assetName, asset };
@@ -241,9 +241,9 @@ class CompilerBackedClientModuleCompiler {
241
241
  if (!fs.existsSync(resolved)) {
242
242
  throw new Error(`[kuratchi compiler] Browser module not found: ${resolved}`);
243
243
  }
244
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
244
245
  const source = fs.readFileSync(resolved, 'utf-8');
245
- let rewritten = transpileTypeScript(source, `client-module:${relFromSrc}`);
246
- rewritten = rewriteImportSpecifiers(rewritten, (spec) => {
246
+ let rewritten = rewriteImportSpecifiers(source, (spec) => {
247
247
  const targetAbs = resolveClientImportTarget(this.srcDir, resolved, spec);
248
248
  const targetAssetName = this.transformClientModule(targetAbs);
249
249
  return toRelativeSpecifier(assetName, targetAssetName);
@@ -18,6 +18,24 @@ export interface AuthConfigEntry {
18
18
  hasTurnstile: boolean;
19
19
  hasOrganization: boolean;
20
20
  }
21
+ export interface SecurityConfigEntry {
22
+ /** Enable CSRF protection for actions and RPC (default: true) */
23
+ csrfEnabled: boolean;
24
+ /** CSRF cookie name (default: '__kuratchi_csrf') */
25
+ csrfCookieName: string;
26
+ /** CSRF header name for fetch requests (default: 'x-kuratchi-csrf') */
27
+ csrfHeaderName: string;
28
+ /** Require authentication for RPC calls (default: false) */
29
+ rpcRequireAuth: boolean;
30
+ /** Require authentication for form actions (default: false) */
31
+ actionRequireAuth: boolean;
32
+ /** Content Security Policy directive string (default: null - no CSP) */
33
+ contentSecurityPolicy: string | null;
34
+ /** Strict-Transport-Security header (default: null - no HSTS) */
35
+ strictTransportSecurity: string | null;
36
+ /** Permissions-Policy header (default: null) */
37
+ permissionsPolicy: string | null;
38
+ }
21
39
  export interface DoConfigEntry {
22
40
  binding: string;
23
41
  className: string;
@@ -3,7 +3,6 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { parseFile, stripTopLevelImports } from './parser.js';
5
5
  import { compileTemplate } from './template.js';
6
- import { transpileTypeScript } from './transpile.js';
7
6
  import { buildDevAliasDeclarations } from './script-transform.js';
8
7
  function resolvePackageComponent(projectDir, pkgName, componentFile) {
9
8
  const nmPath = path.join(projectDir, 'node_modules', pkgName, 'src', 'lib', componentFile + '.html');
@@ -62,12 +61,8 @@ export function createComponentCompiler(options) {
62
61
  const parsed = parseFile(rawSource, { kind: 'component', filePath });
63
62
  const propsCode = parsed.script ? stripTopLevelImports(parsed.script) : '';
64
63
  const devDecls = buildDevAliasDeclarations(parsed.devAliases, isDev);
64
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
65
65
  const effectivePropsCode = [devDecls, propsCode].filter(Boolean).join('\n');
66
- const transpiledPropsCode = propsCode
67
- ? transpileTypeScript(effectivePropsCode, `component-script:${fileName}.ts`)
68
- : devDecls
69
- ? transpileTypeScript(devDecls, `component-script:${fileName}.ts`)
70
- : '';
71
66
  let source = parsed.template;
72
67
  let styleBlock = '';
73
68
  const styleMatch = source.match(/<style[\s>][\s\S]*?<\/style>/i);
@@ -99,11 +94,11 @@ export function createComponentCompiler(options) {
99
94
  }
100
95
  componentActionCache.set(fileName, actionPropNames);
101
96
  const body = compileTemplate(source, subComponentNames, undefined, undefined);
102
- const scopeOpen = `__html += '<div class="${scopeHash}">';`;
103
- const scopeClose = `__html += '</div>';`;
97
+ const scopeOpen = `__parts.push('<div class="${scopeHash}">');`;
98
+ const scopeClose = `__parts.push('</div>');`;
104
99
  const bodyLines = body.split('\n');
105
100
  const scopedBody = [bodyLines[0], scopeOpen, ...bodyLines.slice(1), scopeClose].join('\n');
106
- const fnBody = transpiledPropsCode ? `${transpiledPropsCode}\n ${scopedBody}` : scopedBody;
101
+ const fnBody = effectivePropsCode ? `${effectivePropsCode}\n ${scopedBody}` : scopedBody;
107
102
  const compiled = `function ${funcName}(props, __esc) {\n ${fnBody}\n return __html;\n}`;
108
103
  compiledComponentCache.set(fileName, compiled);
109
104
  return compiled;
@@ -1,4 +1,4 @@
1
- import { type AuthConfigEntry, type DoConfigEntry, type OrmDatabaseEntry, type WorkerClassConfigEntry } from './compiler-shared.js';
1
+ import { type AuthConfigEntry, type DoConfigEntry, type OrmDatabaseEntry, type SecurityConfigEntry, type WorkerClassConfigEntry } from './compiler-shared.js';
2
2
  export declare function readUiTheme(projectDir: string): string | null;
3
3
  export declare function readUiConfigValues(projectDir: string): {
4
4
  theme: string;
@@ -9,3 +9,4 @@ export declare function readAuthConfig(projectDir: string): AuthConfigEntry | nu
9
9
  export declare function readDoConfig(projectDir: string): DoConfigEntry[];
10
10
  export declare function readWorkerClassConfig(projectDir: string, key: 'containers' | 'workflows'): WorkerClassConfigEntry[];
11
11
  export declare function readAssetsPrefix(projectDir: string): string;
12
+ export declare function readSecurityConfig(projectDir: string): SecurityConfigEntry;
@@ -321,3 +321,60 @@ export function readAssetsPrefix(projectDir) {
321
321
  prefix += '/';
322
322
  return prefix;
323
323
  }
324
+ export function readSecurityConfig(projectDir) {
325
+ const defaults = {
326
+ csrfEnabled: true,
327
+ csrfCookieName: '__kuratchi_csrf',
328
+ csrfHeaderName: 'x-kuratchi-csrf',
329
+ rpcRequireAuth: false,
330
+ actionRequireAuth: false,
331
+ contentSecurityPolicy: null,
332
+ strictTransportSecurity: null,
333
+ permissionsPolicy: null,
334
+ };
335
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
336
+ if (!fs.existsSync(configPath))
337
+ return defaults;
338
+ const source = fs.readFileSync(configPath, 'utf-8');
339
+ const securityBlock = readConfigBlock(source, 'security');
340
+ if (!securityBlock)
341
+ return defaults;
342
+ const body = securityBlock.body;
343
+ // Parse CSRF settings
344
+ const csrfEnabledMatch = body.match(/csrfEnabled\s*:\s*(true|false)/);
345
+ if (csrfEnabledMatch) {
346
+ defaults.csrfEnabled = csrfEnabledMatch[1] === 'true';
347
+ }
348
+ const csrfCookieMatch = body.match(/csrfCookieName\s*:\s*['"]([^'"]+)['"]/);
349
+ if (csrfCookieMatch) {
350
+ defaults.csrfCookieName = csrfCookieMatch[1];
351
+ }
352
+ const csrfHeaderMatch = body.match(/csrfHeaderName\s*:\s*['"]([^'"]+)['"]/);
353
+ if (csrfHeaderMatch) {
354
+ defaults.csrfHeaderName = csrfHeaderMatch[1];
355
+ }
356
+ // Parse RPC settings
357
+ const rpcAuthMatch = body.match(/rpcRequireAuth\s*:\s*(true|false)/);
358
+ if (rpcAuthMatch) {
359
+ defaults.rpcRequireAuth = rpcAuthMatch[1] === 'true';
360
+ }
361
+ // Parse action settings
362
+ const actionAuthMatch = body.match(/actionRequireAuth\s*:\s*(true|false)/);
363
+ if (actionAuthMatch) {
364
+ defaults.actionRequireAuth = actionAuthMatch[1] === 'true';
365
+ }
366
+ // Parse security headers
367
+ const cspMatch = body.match(/contentSecurityPolicy\s*:\s*['"`]([^'"`]+)['"`]/);
368
+ if (cspMatch) {
369
+ defaults.contentSecurityPolicy = cspMatch[1];
370
+ }
371
+ const hstsMatch = body.match(/strictTransportSecurity\s*:\s*['"`]([^'"`]+)['"`]/);
372
+ if (hstsMatch) {
373
+ defaults.strictTransportSecurity = hstsMatch[1];
374
+ }
375
+ const permMatch = body.match(/permissionsPolicy\s*:\s*['"`]([^'"`]+)['"`]/);
376
+ if (permMatch) {
377
+ defaults.permissionsPolicy = permMatch[1];
378
+ }
379
+ return defaults;
380
+ }
@@ -65,7 +65,7 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
65
65
  }
66
66
  export function generateHandlerProxy(handler, opts) {
67
67
  const doDir = path.join(opts.projectDir, '.kuratchi', 'do');
68
- const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
68
+ const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/');
69
69
  const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
70
70
  const lifecycle = new Set(['constructor', 'fetch', 'alarm', 'webSocketMessage', 'webSocketClose', 'webSocketError']);
71
71
  const rpcFunctions = handler.classMethods
@@ -93,6 +93,7 @@ export function filterImportsByNeededBindings(imports, neededBindings) {
93
93
  }
94
94
  return selected;
95
95
  }
96
+ const RESERVED_RENDER_VARS = new Set(['params', 'breadcrumbs']);
96
97
  export function linkRouteServerImports(opts) {
97
98
  const fnToModule = {};
98
99
  const routeImportDeclMap = new Map();
@@ -119,7 +120,7 @@ export function linkRouteServerImports(opts) {
119
120
  continue;
120
121
  }
121
122
  fnToModule[binding.local] = moduleId;
122
- if (!routeImportDeclMap.has(binding.local)) {
123
+ if (!routeImportDeclMap.has(binding.local) && !RESERVED_RENDER_VARS.has(binding.local)) {
123
124
  const accessExpr = binding.imported === 'default' ? `${moduleId}.default` : `${moduleId}.${binding.imported}`;
124
125
  routeImportDeclMap.set(binding.local, `const ${binding.local} = ${accessExpr};`);
125
126
  }
@@ -7,7 +7,7 @@ export { compileTemplate, generateRenderFunction } from './template.js';
7
7
  export interface CompileOptions {
8
8
  /** Absolute path to the project root */
9
9
  projectDir: string;
10
- /** Override path for routes.js (default: .kuratchi/routes.js). worker.js is always co-located. */
10
+ /** Override path for routes.ts (default: .kuratchi/routes.ts). worker.ts is always co-located. */
11
11
  outFile?: string;
12
12
  /** Whether this is a dev build (sets __kuratchi_DEV__ global) */
13
13
  isDev?: boolean;
@@ -25,12 +25,12 @@ export interface CompiledRoute {
25
25
  hasRpc: boolean;
26
26
  }
27
27
  /**
28
- * Compile a project's src/routes/ into .kuratchi/routes.js
28
+ * Compile a project's src/routes/ into .kuratchi/routes.ts
29
29
  *
30
- * The generated module exports { app } ?" an object with a fetch() method
30
+ * The generated module exports { app } an object with a fetch() method
31
31
  * that handles routing, load functions, form actions, and rendering.
32
- * Returns the path to .kuratchi/worker.js ? the stable wrangler entry point that
33
- * re-exports everything from routes.js (default fetch handler + named DO class exports).
32
+ * Returns the path to .kuratchi/worker.ts the stable wrangler entry point that
33
+ * re-exports everything from routes.ts (default fetch handler + named DO class exports).
34
34
  * No src/index.ts is needed in user projects.
35
35
  */
36
- export declare function compile(options: CompileOptions): string;
36
+ export declare function compile(options: CompileOptions): Promise<string>;