@kuratchi/js 0.0.15 → 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 +10 -8
  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 +133 -3305
  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 +315 -58
  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
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,11 +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
- ```jsonc
32
- // wrangler.jsonc
33
- {
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
+ {
34
36
  "main": ".kuratchi/worker.js"
35
37
  }
36
38
  ```
package/dist/cli.js CHANGED
@@ -6,34 +6,41 @@ import { compile } from './compiler/index.js';
6
6
  import * as path from 'node:path';
7
7
  import * as fs from 'node:fs';
8
8
  import * as net from 'node:net';
9
+ import { createRequire } from 'node:module';
9
10
  import { spawn } from 'node:child_process';
10
11
  const args = process.argv.slice(2);
11
12
  const command = args[0];
12
13
  const projectDir = process.cwd();
13
- switch (command) {
14
- case 'build':
15
- runBuild();
16
- break;
17
- case 'watch':
18
- runWatch(false);
19
- break;
20
- case 'dev':
21
- runWatch(true);
22
- break;
23
- case 'create':
24
- runCreate();
25
- break;
26
- default:
27
- console.log(`
28
- KuratchiJS CLI
29
-
30
- Usage:
31
- kuratchi create [name] Scaffold a new KuratchiJS project
32
- kuratchi build Compile routes once
33
- kuratchi dev Compile, watch for changes, and start wrangler dev server
34
- kuratchi watch Compile + watch only (no wrangler — for custom setups)
14
+ void main().catch((err) => {
15
+ console.error(`[kuratchi] ${err?.message ?? err}`);
16
+ process.exit(1);
17
+ });
18
+ async function main() {
19
+ switch (command) {
20
+ case 'build':
21
+ runBuild();
22
+ return;
23
+ case 'watch':
24
+ await runWatch(false);
25
+ return;
26
+ case 'dev':
27
+ await runWatch(true);
28
+ return;
29
+ case 'create':
30
+ await runCreate();
31
+ return;
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)
35
41
  `);
36
- process.exit(1);
42
+ process.exit(1);
43
+ }
37
44
  }
38
45
  async function runCreate() {
39
46
  const { create } = await import('./create.js');
@@ -53,7 +60,7 @@ function runBuild(isDev = false) {
53
60
  process.exit(1);
54
61
  }
55
62
  }
56
- function runWatch(withWrangler = false) {
63
+ async function runWatch(withWrangler = false) {
57
64
  runBuild(true);
58
65
  const routesDir = path.join(projectDir, 'src', 'routes');
59
66
  const serverDir = path.join(projectDir, 'src', 'server');
@@ -80,11 +87,10 @@ function runWatch(withWrangler = false) {
80
87
  // `kuratchi dev` also starts the wrangler dev server.
81
88
  // `kuratchi watch` is the compiler-only mode for custom setups.
82
89
  if (withWrangler) {
83
- startWranglerDev().catch((err) => {
84
- console.error(`[kuratchi] Failed to start wrangler dev: ${err?.message ?? err}`);
85
- process.exit(1);
86
- });
90
+ await startWranglerDev();
91
+ return;
87
92
  }
93
+ await new Promise(() => { });
88
94
  }
89
95
  function hasPortFlag(inputArgs) {
90
96
  for (let i = 0; i < inputArgs.length; i++) {
@@ -117,25 +123,13 @@ async function findOpenPort(start = 8787, end = 8899) {
117
123
  }
118
124
  async function startWranglerDev() {
119
125
  const passthroughArgs = args.slice(1);
120
- const wranglerArgs = ['wrangler', 'dev', ...passthroughArgs];
126
+ const wranglerArgs = ['dev', ...passthroughArgs];
121
127
  if (!hasPortFlag(passthroughArgs)) {
122
128
  const port = await findOpenPort();
123
129
  wranglerArgs.push('--port', String(port));
124
130
  console.log(`[kuratchi] Starting wrangler dev on port ${port}`);
125
131
  }
126
- // Use 'pipe' for stdin so wrangler doesn't detect stdin EOF and exit
127
- // prematurely when launched via a script runner (e.g. `bun run dev`).
128
- const isWin = process.platform === 'win32';
129
- const wrangler = isWin
130
- ? spawn('npx ' + wranglerArgs.join(' '), {
131
- cwd: projectDir,
132
- stdio: ['pipe', 'inherit', 'inherit'],
133
- shell: true,
134
- })
135
- : spawn('npx', wranglerArgs, {
136
- cwd: projectDir,
137
- stdio: ['pipe', 'inherit', 'inherit'],
138
- });
132
+ const wrangler = spawnWranglerProcess(wranglerArgs);
139
133
  const cleanup = () => {
140
134
  if (!wrangler.killed)
141
135
  wrangler.kill();
@@ -143,10 +137,49 @@ async function startWranglerDev() {
143
137
  process.on('exit', cleanup);
144
138
  process.on('SIGINT', () => { cleanup(); process.exit(0); });
145
139
  process.on('SIGTERM', () => { cleanup(); process.exit(0); });
146
- wrangler.on('exit', (code) => {
147
- if (code !== 0 && code !== null) {
148
- console.error(`[kuratchi] wrangler exited with code ${code}`);
149
- }
150
- process.exit(code ?? 0);
140
+ await new Promise((resolve, reject) => {
141
+ wrangler.on('exit', (code) => {
142
+ if (code !== 0 && code !== null) {
143
+ reject(new Error(`wrangler exited with code ${code}`));
144
+ return;
145
+ }
146
+ resolve();
147
+ });
148
+ wrangler.on('error', (err) => {
149
+ reject(err);
150
+ });
151
+ }).catch((err) => {
152
+ console.error(`[kuratchi] Failed to start wrangler dev: ${err?.message ?? err}`);
153
+ process.exit(1);
154
+ });
155
+ }
156
+ function resolveWranglerBin() {
157
+ try {
158
+ const projectPackageJson = path.join(projectDir, 'package.json');
159
+ const projectRequire = createRequire(projectPackageJson);
160
+ return projectRequire.resolve('wrangler/bin/wrangler.js');
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ function getNodeExecutable() {
167
+ if (!process.versions.bun)
168
+ return process.execPath;
169
+ return 'node';
170
+ }
171
+ function spawnWranglerProcess(wranglerArgs) {
172
+ const localWranglerBin = resolveWranglerBin();
173
+ const stdio = ['pipe', 'inherit', 'inherit'];
174
+ if (localWranglerBin) {
175
+ return spawn(getNodeExecutable(), [localWranglerBin, ...wranglerArgs], {
176
+ cwd: projectDir,
177
+ stdio,
178
+ });
179
+ }
180
+ const fallbackCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
181
+ return spawn(fallbackCommand, ['wrangler', ...wranglerArgs], {
182
+ cwd: projectDir,
183
+ stdio,
151
184
  });
152
185
  }
@@ -0,0 +1,8 @@
1
+ export declare function compileApiRoute(opts: {
2
+ pattern: string;
3
+ fullPath: string;
4
+ projectDir: string;
5
+ transformModule: (entryAbsPath: string) => string;
6
+ allocateModuleId: () => string;
7
+ pushImport: (statement: string) => void;
8
+ }): string;
@@ -0,0 +1,23 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ const ALL_API_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
4
+ export function compileApiRoute(opts) {
5
+ const outFileDir = path.join(opts.projectDir, '.kuratchi');
6
+ const absRoutePath = opts.transformModule(opts.fullPath);
7
+ let importPath = path.relative(outFileDir, absRoutePath).replace(/\\/g, '/');
8
+ if (!importPath.startsWith('.'))
9
+ importPath = './' + importPath;
10
+ const moduleId = opts.allocateModuleId();
11
+ opts.pushImport(`import * as ${moduleId} from '${importPath}';`);
12
+ const apiSource = fs.readFileSync(opts.fullPath, 'utf-8');
13
+ const exportedMethods = ALL_API_METHODS.filter((method) => {
14
+ const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${method}\\b`);
15
+ const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${method}\\b`);
16
+ const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${method}\\b`);
17
+ return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
18
+ });
19
+ const methodEntries = exportedMethods
20
+ .map((method) => `${method}: ${moduleId}.${method}`)
21
+ .join(', ');
22
+ return `{ pattern: '${opts.pattern}', __api: true, ${methodEntries} }`;
23
+ }
@@ -0,0 +1,7 @@
1
+ export interface CompiledAsset {
2
+ name: string;
3
+ content: string;
4
+ mime: string;
5
+ etag: string;
6
+ }
7
+ export declare function compileAssets(assetsDir: string): CompiledAsset[];
@@ -0,0 +1,33 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ const MIME_TYPES = {
5
+ '.css': 'text/css; charset=utf-8',
6
+ '.js': 'text/javascript; charset=utf-8',
7
+ '.json': 'application/json; charset=utf-8',
8
+ '.svg': 'image/svg+xml',
9
+ '.txt': 'text/plain; charset=utf-8',
10
+ };
11
+ export function compileAssets(assetsDir) {
12
+ const compiledAssets = [];
13
+ if (!fs.existsSync(assetsDir))
14
+ return compiledAssets;
15
+ const scanAssets = (dir, prefix) => {
16
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
17
+ if (entry.isDirectory()) {
18
+ scanAssets(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
19
+ continue;
20
+ }
21
+ const ext = path.extname(entry.name).toLowerCase();
22
+ const mime = MIME_TYPES[ext];
23
+ if (!mime)
24
+ continue;
25
+ const content = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
26
+ const etag = '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"';
27
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
28
+ compiledAssets.push({ name, content, mime, etag });
29
+ }
30
+ };
31
+ scanAssets(assetsDir, '');
32
+ return compiledAssets;
33
+ }
@@ -0,0 +1,25 @@
1
+ import type { CompiledAsset } from './asset-pipeline.js';
2
+ import { type RouteImportEntry } from './import-linking.js';
3
+ export interface ClientEventRegistration {
4
+ routeId: string;
5
+ handlerId: string;
6
+ argsExpr: string | null;
7
+ }
8
+ export interface ClientModuleCompiler {
9
+ createRegistry(scopeId: string, importEntries: RouteImportEntry[]): ClientRouteRegistry;
10
+ createRouteRegistry(routeIndex: number, importEntries: RouteImportEntry[]): ClientRouteRegistry;
11
+ getCompiledAssets(): CompiledAsset[];
12
+ }
13
+ export interface ClientRouteRegistry {
14
+ hasBindings(): boolean;
15
+ hasBindingReference(expression: string): boolean;
16
+ registerEventHandler(eventName: string, expression: string): ClientEventRegistration | null;
17
+ buildEntryAsset(): {
18
+ assetName: string;
19
+ asset: CompiledAsset;
20
+ } | null;
21
+ }
22
+ export declare function createClientModuleCompiler(opts: {
23
+ projectDir: string;
24
+ srcDir: string;
25
+ }): ClientModuleCompiler;
@@ -0,0 +1,257 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { collectReferencedIdentifiers, parseImportStatement } from './import-linking.js';
5
+ import { transpileTypeScript } from './transpile.js';
6
+ function resolveExistingModuleFile(absBase) {
7
+ const candidates = [
8
+ absBase,
9
+ absBase + '.ts',
10
+ absBase + '.js',
11
+ absBase + '.mjs',
12
+ absBase + '.cjs',
13
+ path.join(absBase, 'index.ts'),
14
+ path.join(absBase, 'index.js'),
15
+ path.join(absBase, 'index.mjs'),
16
+ path.join(absBase, 'index.cjs'),
17
+ ];
18
+ for (const candidate of candidates) {
19
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
20
+ return candidate;
21
+ }
22
+ return null;
23
+ }
24
+ function buildAsset(name, content) {
25
+ return {
26
+ name,
27
+ content,
28
+ mime: 'text/javascript; charset=utf-8',
29
+ etag: '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"',
30
+ };
31
+ }
32
+ function toRelativeSpecifier(fromAssetName, toAssetName) {
33
+ let rel = path.posix.relative(path.posix.dirname(fromAssetName), toAssetName);
34
+ if (!rel.startsWith('.'))
35
+ rel = './' + rel;
36
+ return rel;
37
+ }
38
+ function rewriteImportSpecifiers(source, rewriteSpecifier) {
39
+ let rewritten = source.replace(/(from\s+)(['"])([^'"]+)\2/g, (_match, prefix, quote, spec) => {
40
+ return `${prefix}${quote}${rewriteSpecifier(spec)}${quote}`;
41
+ });
42
+ rewritten = rewritten.replace(/(import\s*\(\s*)(['"])([^'"]+)\2(\s*\))/g, (_match, prefix, quote, spec, suffix) => {
43
+ return `${prefix}${quote}${rewriteSpecifier(spec)}${quote}${suffix}`;
44
+ });
45
+ return rewritten;
46
+ }
47
+ function resolveClientImportTarget(srcDir, importerAbs, spec) {
48
+ if (spec.startsWith('$')) {
49
+ const slashIdx = spec.indexOf('/');
50
+ const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
51
+ const rest = slashIdx === -1 ? '' : spec.slice(slashIdx + 1);
52
+ if (folder !== 'client' && folder !== 'shared') {
53
+ throw new Error(`[kuratchi compiler] Unsupported browser import realm "${spec}". Only $client/* and $shared/* may be loaded in the browser.`);
54
+ }
55
+ const abs = path.join(srcDir, folder, rest);
56
+ const resolved = resolveExistingModuleFile(abs);
57
+ if (!resolved) {
58
+ throw new Error(`[kuratchi compiler] Browser import not found: ${spec}`);
59
+ }
60
+ return resolved;
61
+ }
62
+ if (spec.startsWith('.')) {
63
+ const abs = path.resolve(path.dirname(importerAbs), spec);
64
+ const resolved = resolveExistingModuleFile(abs);
65
+ if (!resolved) {
66
+ throw new Error(`[kuratchi compiler] Browser import not found: ${spec}`);
67
+ }
68
+ return resolved;
69
+ }
70
+ throw new Error(`[kuratchi compiler] Browser modules currently only support project-local imports ($client, $shared, or relative). Unsupported import: ${spec}`);
71
+ }
72
+ class CompilerBackedClientRouteRegistry {
73
+ compiler;
74
+ bindingMap = new Map();
75
+ clientOnlyBindings = new Set();
76
+ handlerByKey = new Map();
77
+ routeId;
78
+ constructor(compiler, scopeId, importEntries) {
79
+ this.compiler = compiler;
80
+ this.routeId = scopeId;
81
+ for (const entry of importEntries) {
82
+ const parsed = parseImportStatement(entry.line);
83
+ if (!parsed.moduleSpecifier)
84
+ continue;
85
+ const isClient = parsed.moduleSpecifier.startsWith('$client/');
86
+ const isShared = parsed.moduleSpecifier.startsWith('$shared/');
87
+ if (!isClient && !isShared)
88
+ continue;
89
+ for (const binding of parsed.bindings) {
90
+ this.bindingMap.set(binding.local, {
91
+ importLine: entry.line,
92
+ importerDir: entry.importerDir,
93
+ localName: binding.local,
94
+ moduleSpecifier: parsed.moduleSpecifier,
95
+ });
96
+ if (isClient)
97
+ this.clientOnlyBindings.add(binding.local);
98
+ }
99
+ if (parsed.namespaceImport) {
100
+ this.bindingMap.set(parsed.namespaceImport, {
101
+ importLine: entry.line,
102
+ importerDir: entry.importerDir,
103
+ localName: parsed.namespaceImport,
104
+ moduleSpecifier: parsed.moduleSpecifier,
105
+ });
106
+ if (isClient)
107
+ this.clientOnlyBindings.add(parsed.namespaceImport);
108
+ }
109
+ }
110
+ }
111
+ hasBindings() {
112
+ return this.bindingMap.size > 0;
113
+ }
114
+ hasBindingReference(expression) {
115
+ const refs = collectReferencedIdentifiers(expression);
116
+ for (const ref of refs) {
117
+ if (this.bindingMap.has(ref))
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+ registerEventHandler(_eventName, expression) {
123
+ const parsed = this.parseClientExpression(expression);
124
+ if (!parsed)
125
+ return null;
126
+ const binding = this.bindingMap.get(parsed.rootBinding);
127
+ if (!binding)
128
+ return null;
129
+ if (parsed.argsExpr) {
130
+ const argRefs = collectReferencedIdentifiers(parsed.argsExpr);
131
+ const leakedClientRefs = Array.from(argRefs).filter((ref) => this.clientOnlyBindings.has(ref));
132
+ if (leakedClientRefs.length > 0) {
133
+ throw new Error(`[kuratchi compiler] Client event arguments cannot depend on $client bindings: ${leakedClientRefs.join(', ')}.\n` +
134
+ `Only server/shared values can be serialized into event handler arguments.`);
135
+ }
136
+ }
137
+ const key = `${parsed.calleeExpr}::${parsed.argsExpr || ''}`;
138
+ let existing = this.handlerByKey.get(key);
139
+ if (!existing) {
140
+ existing = {
141
+ id: `h${this.handlerByKey.size}`,
142
+ calleeExpr: parsed.calleeExpr,
143
+ rootBinding: parsed.rootBinding,
144
+ };
145
+ this.handlerByKey.set(key, existing);
146
+ }
147
+ return {
148
+ routeId: this.routeId,
149
+ handlerId: existing.id,
150
+ argsExpr: parsed.argsExpr,
151
+ };
152
+ }
153
+ buildEntryAsset() {
154
+ if (this.handlerByKey.size === 0)
155
+ return null;
156
+ const usedImportLines = new Map();
157
+ for (const record of this.handlerByKey.values()) {
158
+ const binding = this.bindingMap.get(record.rootBinding);
159
+ if (!binding)
160
+ continue;
161
+ if (!usedImportLines.has(binding.importLine)) {
162
+ usedImportLines.set(binding.importLine, {
163
+ line: binding.importLine,
164
+ importerDir: binding.importerDir,
165
+ });
166
+ }
167
+ }
168
+ const assetName = `__kuratchi/client/routes/${this.routeId}.js`;
169
+ const importLines = [];
170
+ for (const entry of usedImportLines.values()) {
171
+ const parsed = parseImportStatement(entry.line);
172
+ if (!parsed.moduleSpecifier)
173
+ continue;
174
+ const targetAbs = resolveClientImportTarget(this.compiler.srcDir, path.join(entry.importerDir, '__route__.ts'), parsed.moduleSpecifier);
175
+ const targetAssetName = this.compiler.transformClientModule(targetAbs);
176
+ const relSpecifier = toRelativeSpecifier(assetName, targetAssetName);
177
+ importLines.push(entry.line.replace(parsed.moduleSpecifier, relSpecifier));
178
+ }
179
+ const registrationEntries = Array.from(this.handlerByKey.values()).map((record) => {
180
+ return `${JSON.stringify(record.id)}: (args, event, element) => ${record.calleeExpr}(...args, event, element)`;
181
+ });
182
+ const source = transpileTypeScript([
183
+ ...importLines,
184
+ `window.__kuratchiClient?.register(${JSON.stringify(this.routeId)}, {`,
185
+ registrationEntries.map((entry) => ` ${entry},`).join('\n'),
186
+ `});`,
187
+ ].join('\n'), `client-route:${this.routeId}.ts`);
188
+ const asset = buildAsset(assetName, source);
189
+ this.compiler.registerAsset(asset);
190
+ return { assetName, asset };
191
+ }
192
+ parseClientExpression(expression) {
193
+ const trimmed = expression.trim();
194
+ if (!trimmed)
195
+ return null;
196
+ const callMatch = trimmed.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\(([\s\S]*)\)$/);
197
+ if (callMatch) {
198
+ const calleeExpr = callMatch[1];
199
+ const rootBinding = calleeExpr.split('.')[0];
200
+ const argsExpr = (callMatch[2] || '').trim();
201
+ return { calleeExpr, rootBinding, argsExpr: argsExpr || null };
202
+ }
203
+ const refMatch = trimmed.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)$/);
204
+ if (!refMatch)
205
+ return null;
206
+ const calleeExpr = refMatch[1];
207
+ const rootBinding = calleeExpr.split('.')[0];
208
+ return { calleeExpr, rootBinding, argsExpr: null };
209
+ }
210
+ }
211
+ class CompilerBackedClientModuleCompiler {
212
+ projectDir;
213
+ srcDir;
214
+ compiledAssets = new Map();
215
+ transformedModules = new Map();
216
+ constructor(projectDir, srcDir) {
217
+ this.projectDir = projectDir;
218
+ this.srcDir = srcDir;
219
+ }
220
+ createRegistry(scopeId, importEntries) {
221
+ return new CompilerBackedClientRouteRegistry(this, scopeId, importEntries);
222
+ }
223
+ createRouteRegistry(routeIndex, importEntries) {
224
+ return this.createRegistry(`route_${routeIndex}`, importEntries);
225
+ }
226
+ getCompiledAssets() {
227
+ return Array.from(this.compiledAssets.values());
228
+ }
229
+ registerAsset(asset) {
230
+ this.compiledAssets.set(asset.name, asset);
231
+ }
232
+ transformClientModule(entryAbsPath) {
233
+ const resolved = resolveExistingModuleFile(entryAbsPath) ?? entryAbsPath;
234
+ const normalized = resolved.replace(/\\/g, '/');
235
+ const cached = this.transformedModules.get(normalized);
236
+ if (cached)
237
+ return cached;
238
+ const relFromSrc = path.relative(this.srcDir, resolved).replace(/\\/g, '/');
239
+ const assetName = `__kuratchi/client/modules/${relFromSrc.replace(/\.(ts|js|mjs|cjs)$/i, '.js')}`;
240
+ this.transformedModules.set(normalized, assetName);
241
+ if (!fs.existsSync(resolved)) {
242
+ throw new Error(`[kuratchi compiler] Browser module not found: ${resolved}`);
243
+ }
244
+ const source = fs.readFileSync(resolved, 'utf-8');
245
+ let rewritten = transpileTypeScript(source, `client-module:${relFromSrc}`);
246
+ rewritten = rewriteImportSpecifiers(rewritten, (spec) => {
247
+ const targetAbs = resolveClientImportTarget(this.srcDir, resolved, spec);
248
+ const targetAssetName = this.transformClientModule(targetAbs);
249
+ return toRelativeSpecifier(assetName, targetAssetName);
250
+ });
251
+ this.registerAsset(buildAsset(assetName, rewritten));
252
+ return assetName;
253
+ }
254
+ }
255
+ export function createClientModuleCompiler(opts) {
256
+ return new CompilerBackedClientModuleCompiler(opts.projectDir, opts.srcDir);
257
+ }
@@ -0,0 +1,55 @@
1
+ export interface OrmDatabaseEntry {
2
+ binding: string;
3
+ schemaImportPath: string;
4
+ schemaExportName: string;
5
+ skipMigrations: boolean;
6
+ type: 'd1' | 'do';
7
+ }
8
+ export interface AuthConfigEntry {
9
+ cookieName: string;
10
+ secretEnvKey: string;
11
+ sessionEnabled: boolean;
12
+ hasCredentials: boolean;
13
+ hasActivity: boolean;
14
+ hasRoles: boolean;
15
+ hasOAuth: boolean;
16
+ hasGuards: boolean;
17
+ hasRateLimit: boolean;
18
+ hasTurnstile: boolean;
19
+ hasOrganization: boolean;
20
+ }
21
+ export interface DoConfigEntry {
22
+ binding: string;
23
+ className: string;
24
+ stubId?: string;
25
+ files?: string[];
26
+ }
27
+ export interface WorkerClassConfigEntry {
28
+ binding: string;
29
+ className: string;
30
+ file: string;
31
+ exportKind: 'named' | 'default';
32
+ }
33
+ export interface ConventionClassEntry {
34
+ className: string;
35
+ file: string;
36
+ exportKind: 'named' | 'default';
37
+ }
38
+ export interface DoClassMethodEntry {
39
+ name: string;
40
+ visibility: 'public' | 'private' | 'protected';
41
+ isStatic: boolean;
42
+ isAsync: boolean;
43
+ hasWorkerContextCalls: boolean;
44
+ callsThisMethods: string[];
45
+ }
46
+ export interface DoHandlerEntry {
47
+ fileName: string;
48
+ absPath: string;
49
+ binding: string;
50
+ mode: 'class' | 'function';
51
+ className?: string;
52
+ classMethods: DoClassMethodEntry[];
53
+ exportedFunctions: string[];
54
+ }
55
+ export declare function toSafeIdentifier(input: string): string;
@@ -0,0 +1,4 @@
1
+ export function toSafeIdentifier(input) {
2
+ const normalized = input.replace(/[^A-Za-z0-9_$]/g, '_');
3
+ return /^[A-Za-z_$]/.test(normalized) ? normalized : `_${normalized}`;
4
+ }
@@ -0,0 +1,15 @@
1
+ export interface ComponentCompiler {
2
+ ensureCompiled(fileName: string): string | null;
3
+ collectComponentMap(componentImports: Record<string, string>): Map<string, string>;
4
+ getActionPropNames(fileName: string): Set<string>;
5
+ collectStyles(componentNames: Map<string, string>): string[];
6
+ resolveActionProps(template: string, componentNames: Map<string, string>, shouldInclude?: (fnName: string) => boolean): Set<string>;
7
+ getCompiledComponents(): string[];
8
+ }
9
+ interface CreateComponentCompilerOptions {
10
+ projectDir: string;
11
+ srcDir: string;
12
+ isDev: boolean;
13
+ }
14
+ export declare function createComponentCompiler(options: CreateComponentCompilerOptions): ComponentCompiler;
15
+ export {};