@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,323 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ function skipWhitespace(source, start) {
4
+ let i = start;
5
+ while (i < source.length && /\s/.test(source[i]))
6
+ i++;
7
+ return i;
8
+ }
9
+ function extractBalancedBody(source, start, openChar, closeChar) {
10
+ if (source[start] !== openChar)
11
+ return null;
12
+ let depth = 0;
13
+ for (let i = start; i < source.length; i++) {
14
+ if (source[i] === openChar)
15
+ depth++;
16
+ else if (source[i] === closeChar) {
17
+ depth--;
18
+ if (depth === 0)
19
+ return source.slice(start + 1, i);
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ function readConfigBlock(source, key) {
25
+ const keyRegex = new RegExp(`\\b${key}\\s*:`);
26
+ const keyMatch = keyRegex.exec(source);
27
+ if (!keyMatch)
28
+ return null;
29
+ const colonIdx = source.indexOf(':', keyMatch.index);
30
+ if (colonIdx === -1)
31
+ return null;
32
+ const valueIdx = skipWhitespace(source, colonIdx + 1);
33
+ if (valueIdx >= source.length)
34
+ return null;
35
+ if (source[valueIdx] === '{') {
36
+ throw new Error(`[kuratchi] "${key}" config must use an adapter call (e.g. ${key}: kuratchi${key[0].toUpperCase()}${key.slice(1)}Config({...})).`);
37
+ }
38
+ const callOpen = source.indexOf('(', valueIdx);
39
+ if (callOpen === -1)
40
+ return null;
41
+ const argIdx = skipWhitespace(source, callOpen + 1);
42
+ if (argIdx >= source.length)
43
+ return null;
44
+ if (source[argIdx] === ')')
45
+ return { kind: 'call-empty', body: '' };
46
+ if (source[argIdx] === '{') {
47
+ const body = extractBalancedBody(source, argIdx, '{', '}');
48
+ if (body == null)
49
+ return null;
50
+ return { kind: 'call-object', body };
51
+ }
52
+ return { kind: 'call-empty', body: '' };
53
+ }
54
+ export function readUiTheme(projectDir) {
55
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
56
+ if (!fs.existsSync(configPath))
57
+ return null;
58
+ const source = fs.readFileSync(configPath, 'utf-8');
59
+ const uiBlock = readConfigBlock(source, 'ui');
60
+ if (!uiBlock)
61
+ return null;
62
+ const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
63
+ const themeValue = themeMatch?.[1] ?? 'default';
64
+ if (themeValue === 'default' || themeValue === 'dark' || themeValue === 'light' || themeValue === 'system') {
65
+ const candidates = [
66
+ path.join(projectDir, 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
67
+ path.join(path.resolve(projectDir, '../..'), 'packages', 'kuratchi-ui', 'src', 'styles', 'theme.css'),
68
+ path.join(path.resolve(projectDir, '../..'), 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
69
+ ];
70
+ for (const candidate of candidates) {
71
+ if (fs.existsSync(candidate)) {
72
+ return fs.readFileSync(candidate, 'utf-8');
73
+ }
74
+ }
75
+ console.warn(`[kuratchi] ui.theme: "${themeValue}" configured but @kuratchi/ui theme.css not found`);
76
+ return null;
77
+ }
78
+ const customPath = path.resolve(projectDir, themeValue);
79
+ if (fs.existsSync(customPath)) {
80
+ return fs.readFileSync(customPath, 'utf-8');
81
+ }
82
+ console.warn(`[kuratchi] ui.theme: "${themeValue}" not found at ${customPath}`);
83
+ return null;
84
+ }
85
+ export function readUiConfigValues(projectDir) {
86
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
87
+ if (!fs.existsSync(configPath))
88
+ return null;
89
+ const source = fs.readFileSync(configPath, 'utf-8');
90
+ const uiBlock = readConfigBlock(source, 'ui');
91
+ if (!uiBlock)
92
+ return null;
93
+ const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
94
+ const radiusMatch = uiBlock.body.match(/radius\s*:\s*['"]([^'"]+)['"]/);
95
+ return {
96
+ theme: themeMatch?.[1] ?? 'dark',
97
+ radius: radiusMatch?.[1] ?? 'default',
98
+ };
99
+ }
100
+ export function readOrmConfig(projectDir) {
101
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
102
+ if (!fs.existsSync(configPath))
103
+ return [];
104
+ const source = fs.readFileSync(configPath, 'utf-8');
105
+ const ormBlock = readConfigBlock(source, 'orm');
106
+ if (!ormBlock)
107
+ return [];
108
+ const importMap = new Map();
109
+ const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
110
+ let match;
111
+ while ((match = importRegex.exec(source)) !== null) {
112
+ const names = match[1].split(',').map((name) => name.trim()).filter(Boolean);
113
+ const importPath = match[2];
114
+ for (const name of names) {
115
+ importMap.set(name, importPath);
116
+ }
117
+ }
118
+ const databasesIdx = ormBlock.body.search(/databases\s*:\s*\{/);
119
+ if (databasesIdx === -1)
120
+ return [];
121
+ const dbBraceStart = ormBlock.body.indexOf('{', databasesIdx);
122
+ if (dbBraceStart === -1)
123
+ return [];
124
+ const databasesBody = extractBalancedBody(ormBlock.body, dbBraceStart, '{', '}');
125
+ if (databasesBody == null)
126
+ return [];
127
+ const entries = [];
128
+ const entryRegex = /(\w+)\s*:\s*\{\s*schema\s*:\s*(\w+)([^}]*)\}/g;
129
+ while ((match = entryRegex.exec(databasesBody)) !== null) {
130
+ const binding = match[1];
131
+ const schemaExportName = match[2];
132
+ const rest = match[3] || '';
133
+ const skipMatch = rest.match(/skipMigrations\s*:\s*(true|false)/);
134
+ const skipMigrations = skipMatch?.[1] === 'true';
135
+ const typeMatch = rest.match(/type\s*:\s*['"]?(d1|do)['"]?/);
136
+ const type = typeMatch?.[1] ?? 'd1';
137
+ const schemaImportPath = importMap.get(schemaExportName);
138
+ if (!schemaImportPath)
139
+ continue;
140
+ entries.push({ binding, schemaImportPath, schemaExportName, skipMigrations, type });
141
+ }
142
+ return entries;
143
+ }
144
+ export function readAuthConfig(projectDir) {
145
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
146
+ if (!fs.existsSync(configPath))
147
+ return null;
148
+ const source = fs.readFileSync(configPath, 'utf-8');
149
+ const authBlockMatch = readConfigBlock(source, 'auth');
150
+ if (!authBlockMatch)
151
+ return null;
152
+ const authBlock = authBlockMatch.body;
153
+ const cookieMatch = authBlock.match(/cookieName\s*:\s*['"]([^'"]+)['"]/);
154
+ const secretMatch = authBlock.match(/secretEnvKey\s*:\s*['"]([^'"]+)['"]/);
155
+ const sessionMatch = authBlock.match(/sessionEnabled\s*:\s*(true|false)/);
156
+ return {
157
+ cookieName: cookieMatch?.[1] ?? 'kuratchi_session',
158
+ secretEnvKey: secretMatch?.[1] ?? 'AUTH_SECRET',
159
+ sessionEnabled: sessionMatch?.[1] !== 'false',
160
+ hasCredentials: /credentials\s*:/.test(authBlock),
161
+ hasActivity: /activity\s*:/.test(authBlock),
162
+ hasRoles: /roles\s*:/.test(authBlock),
163
+ hasOAuth: /oauth\s*:/.test(authBlock),
164
+ hasGuards: /guards\s*:/.test(authBlock),
165
+ hasRateLimit: /rateLimit\s*:/.test(authBlock),
166
+ hasTurnstile: /turnstile\s*:/.test(authBlock),
167
+ hasOrganization: /organizations\s*:/.test(authBlock),
168
+ };
169
+ }
170
+ export function readDoConfig(projectDir) {
171
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
172
+ if (!fs.existsSync(configPath))
173
+ return [];
174
+ const source = fs.readFileSync(configPath, 'utf-8');
175
+ const doIdx = source.search(/durableObjects\s*:\s*\{/);
176
+ if (doIdx === -1)
177
+ return [];
178
+ const braceStart = source.indexOf('{', doIdx);
179
+ if (braceStart === -1)
180
+ return [];
181
+ let depth = 0;
182
+ let braceEnd = braceStart;
183
+ for (let i = braceStart; i < source.length; i++) {
184
+ if (source[i] === '{')
185
+ depth++;
186
+ else if (source[i] === '}') {
187
+ depth--;
188
+ if (depth === 0) {
189
+ braceEnd = i;
190
+ break;
191
+ }
192
+ }
193
+ }
194
+ const doBlock = source.slice(braceStart + 1, braceEnd);
195
+ const entries = [];
196
+ const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
197
+ let match;
198
+ while ((match = objRegex.exec(doBlock)) !== null) {
199
+ const binding = match[1];
200
+ const body = match[2];
201
+ const cnMatch = body.match(/className\s*:\s*['"](\w+)['"]/);
202
+ if (!cnMatch)
203
+ continue;
204
+ const entry = { binding, className: cnMatch[1] };
205
+ const stubIdMatch = body.match(/stubId\s*:\s*['"]([^'"]+)['"]/);
206
+ if (stubIdMatch)
207
+ entry.stubId = stubIdMatch[1];
208
+ const filesMatch = body.match(/files\s*:\s*\[([\s\S]*?)\]/);
209
+ if (filesMatch) {
210
+ const list = [];
211
+ const itemRegex = /['"]([^'"]+)['"]/g;
212
+ let fileMatch;
213
+ while ((fileMatch = itemRegex.exec(filesMatch[1])) !== null) {
214
+ list.push(fileMatch[1]);
215
+ }
216
+ if (list.length > 0)
217
+ entry.files = list;
218
+ }
219
+ entries.push(entry);
220
+ }
221
+ const foundBindings = new Set(entries.map((entry) => entry.binding));
222
+ const pairRegex = /(\w+)\s*:\s*['"](\w+)['"]\s*[,}\n]/g;
223
+ while ((match = pairRegex.exec(doBlock)) !== null) {
224
+ if (foundBindings.has(match[1]))
225
+ continue;
226
+ if (['className', 'stubId'].includes(match[1]))
227
+ continue;
228
+ entries.push({ binding: match[1], className: match[2] });
229
+ }
230
+ return entries;
231
+ }
232
+ export function readWorkerClassConfig(projectDir, key) {
233
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
234
+ if (!fs.existsSync(configPath))
235
+ return [];
236
+ const source = fs.readFileSync(configPath, 'utf-8');
237
+ const keyIdx = source.search(new RegExp(`\\b${key}\\s*:\\s*\\{`));
238
+ if (keyIdx === -1)
239
+ return [];
240
+ const braceStart = source.indexOf('{', keyIdx);
241
+ if (braceStart === -1)
242
+ return [];
243
+ const body = extractBalancedBody(source, braceStart, '{', '}');
244
+ if (body == null)
245
+ return [];
246
+ const entries = [];
247
+ const expectedSuffix = key === 'containers' ? '.container' : '.workflow';
248
+ const allowedExt = /\.(ts|js|mjs|cjs)$/i;
249
+ const requiredFilePattern = new RegExp(`\\${expectedSuffix}\\.(ts|js|mjs|cjs)$`, 'i');
250
+ const resolveClassFromFile = (binding, filePath) => {
251
+ if (!requiredFilePattern.test(filePath)) {
252
+ throw new Error(`[kuratchi] ${key}.${binding} must reference a file ending in "${expectedSuffix}.ts|js|mjs|cjs". Received: ${filePath}`);
253
+ }
254
+ if (!allowedExt.test(filePath)) {
255
+ throw new Error(`[kuratchi] ${key}.${binding} file must be a TypeScript or JavaScript module. Received: ${filePath}`);
256
+ }
257
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
258
+ if (!fs.existsSync(absPath)) {
259
+ throw new Error(`[kuratchi] ${key}.${binding} file not found: ${filePath}`);
260
+ }
261
+ const fileSource = fs.readFileSync(absPath, 'utf-8');
262
+ const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
263
+ if (defaultClass) {
264
+ return { className: defaultClass[1], exportKind: 'default' };
265
+ }
266
+ const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
267
+ if (namedClass) {
268
+ return { className: namedClass[1], exportKind: 'named' };
269
+ }
270
+ throw new Error(`[kuratchi] ${key}.${binding} must export a class via "export class X" or "export default class X". File: ${filePath}`);
271
+ };
272
+ const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
273
+ let match;
274
+ while ((match = objRegex.exec(body)) !== null) {
275
+ const binding = match[1];
276
+ const entryBody = match[2];
277
+ const fileMatch = entryBody.match(/file\s*:\s*['"]([^'"]+)['"]/);
278
+ if (!fileMatch)
279
+ continue;
280
+ const inferred = resolveClassFromFile(binding, fileMatch[1]);
281
+ const classMatch = entryBody.match(/className\s*:\s*['"](\w+)['"]/);
282
+ const className = classMatch?.[1] ?? inferred.className;
283
+ entries.push({
284
+ binding,
285
+ className,
286
+ file: fileMatch[1],
287
+ exportKind: inferred.exportKind,
288
+ });
289
+ }
290
+ const foundBindings = new Set(entries.map((entry) => entry.binding));
291
+ const pairRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]\s*[,}\n]/g;
292
+ while ((match = pairRegex.exec(body)) !== null) {
293
+ const binding = match[1];
294
+ const file = match[2];
295
+ if (foundBindings.has(binding))
296
+ continue;
297
+ if (binding === 'file' || binding === 'className')
298
+ continue;
299
+ const inferred = resolveClassFromFile(binding, file);
300
+ entries.push({
301
+ binding,
302
+ className: inferred.className,
303
+ file,
304
+ exportKind: inferred.exportKind,
305
+ });
306
+ }
307
+ return entries;
308
+ }
309
+ export function readAssetsPrefix(projectDir) {
310
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
311
+ if (!fs.existsSync(configPath))
312
+ return '/assets/';
313
+ const source = fs.readFileSync(configPath, 'utf-8');
314
+ const match = source.match(/assetsPrefix\s*:\s*['"]([^'"]+)['"]/);
315
+ if (!match)
316
+ return '/assets/';
317
+ let prefix = match[1];
318
+ if (!prefix.startsWith('/'))
319
+ prefix = '/' + prefix;
320
+ if (!prefix.endsWith('/'))
321
+ prefix += '/';
322
+ return prefix;
323
+ }
@@ -0,0 +1,9 @@
1
+ import { type ConventionClassEntry, type WorkerClassConfigEntry } from './compiler-shared.js';
2
+ export declare function resolveClassExportFromFile(absPath: string, errorLabel: string): {
3
+ className: string;
4
+ exportKind: 'named' | 'default';
5
+ };
6
+ export declare function discoverConventionClassFiles(projectDir: string, dir: string, suffix: string, errorLabel: string): ConventionClassEntry[];
7
+ export declare function discoverFilesWithSuffix(dir: string, suffix: string): string[];
8
+ export declare function discoverWorkflowFiles(projectDir: string): WorkerClassConfigEntry[];
9
+ export declare function discoverContainerFiles(projectDir: string): WorkerClassConfigEntry[];
@@ -0,0 +1,83 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ export function resolveClassExportFromFile(absPath, errorLabel) {
4
+ if (!fs.existsSync(absPath)) {
5
+ throw new Error(`[kuratchi] ${errorLabel} file not found: ${absPath}`);
6
+ }
7
+ const fileSource = fs.readFileSync(absPath, 'utf-8');
8
+ const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
9
+ if (defaultClass) {
10
+ return { className: defaultClass[1], exportKind: 'default' };
11
+ }
12
+ const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
13
+ if (namedClass) {
14
+ return { className: namedClass[1], exportKind: 'named' };
15
+ }
16
+ throw new Error(`[kuratchi] ${errorLabel} must export a class via "export class X" or "export default class X". File: ${absPath}`);
17
+ }
18
+ export function discoverConventionClassFiles(projectDir, dir, suffix, errorLabel) {
19
+ const absDir = path.join(projectDir, dir);
20
+ const files = discoverFilesWithSuffix(absDir, suffix);
21
+ if (files.length === 0)
22
+ return [];
23
+ return files.map((absPath) => {
24
+ const resolved = resolveClassExportFromFile(absPath, errorLabel);
25
+ return {
26
+ className: resolved.className,
27
+ file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
28
+ exportKind: resolved.exportKind,
29
+ };
30
+ });
31
+ }
32
+ export function discoverFilesWithSuffix(dir, suffix) {
33
+ if (!fs.existsSync(dir))
34
+ return [];
35
+ const out = [];
36
+ const walk = (absDir) => {
37
+ for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
38
+ const abs = path.join(absDir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ walk(abs);
41
+ }
42
+ else if (entry.isFile() && abs.endsWith(suffix)) {
43
+ out.push(abs);
44
+ }
45
+ }
46
+ };
47
+ walk(dir);
48
+ return out;
49
+ }
50
+ export function discoverWorkflowFiles(projectDir) {
51
+ const serverDir = path.join(projectDir, 'src', 'server');
52
+ const files = discoverFilesWithSuffix(serverDir, '.workflow.ts');
53
+ if (files.length === 0)
54
+ return [];
55
+ return files.map((absPath) => {
56
+ const fileName = path.basename(absPath, '.workflow.ts');
57
+ const binding = fileName.toUpperCase().replace(/-/g, '_') + '_WORKFLOW';
58
+ const resolved = resolveClassExportFromFile(absPath, '.workflow');
59
+ return {
60
+ binding,
61
+ className: resolved.className,
62
+ file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
63
+ exportKind: resolved.exportKind,
64
+ };
65
+ });
66
+ }
67
+ export function discoverContainerFiles(projectDir) {
68
+ const serverDir = path.join(projectDir, 'src', 'server');
69
+ const files = discoverFilesWithSuffix(serverDir, '.container.ts');
70
+ if (files.length === 0)
71
+ return [];
72
+ return files.map((absPath) => {
73
+ const fileName = path.basename(absPath, '.container.ts');
74
+ const binding = fileName.toUpperCase().replace(/-/g, '_') + '_CONTAINER';
75
+ const resolved = resolveClassExportFromFile(absPath, '.container');
76
+ return {
77
+ binding,
78
+ className: resolved.className,
79
+ file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
80
+ exportKind: resolved.exportKind,
81
+ };
82
+ });
83
+ }
@@ -0,0 +1,9 @@
1
+ import { type DoConfigEntry, type DoHandlerEntry, type OrmDatabaseEntry } from './compiler-shared.js';
2
+ export declare function discoverDurableObjects(srcDir: string, configDoEntries: DoConfigEntry[], ormDatabases: OrmDatabaseEntry[]): {
3
+ config: DoConfigEntry[];
4
+ handlers: DoHandlerEntry[];
5
+ };
6
+ export declare function generateHandlerProxy(handler: DoHandlerEntry, opts: {
7
+ projectDir: string;
8
+ runtimeDoImport: string;
9
+ }): string;
@@ -0,0 +1,255 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { toSafeIdentifier, } from './compiler-shared.js';
4
+ import { discoverFilesWithSuffix } from './convention-discovery.js';
5
+ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
6
+ const serverDir = path.join(srcDir, 'server');
7
+ const legacyDir = path.join(srcDir, 'durable-objects');
8
+ const serverDoFiles = discoverFilesWithSuffix(serverDir, '.do.ts');
9
+ const legacyDoFiles = discoverFilesWithSuffix(legacyDir, '.ts');
10
+ const discoveredFiles = Array.from(new Set([...serverDoFiles, ...legacyDoFiles]));
11
+ if (discoveredFiles.length === 0) {
12
+ return { config: configDoEntries, handlers: [] };
13
+ }
14
+ const configByBinding = new Map();
15
+ for (const entry of configDoEntries) {
16
+ configByBinding.set(entry.binding, entry);
17
+ }
18
+ const handlers = [];
19
+ const discoveredConfig = [];
20
+ const fileNameToAbsPath = new Map();
21
+ const seenBindings = new Set();
22
+ for (const absPath of discoveredFiles) {
23
+ const file = path.basename(absPath);
24
+ const source = fs.readFileSync(absPath, 'utf-8');
25
+ if (!/extends\s+DurableObject\b/.test(source))
26
+ continue;
27
+ const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+DurableObject/);
28
+ const className = classMatch?.[1] ?? null;
29
+ if (!className)
30
+ continue;
31
+ const bindingMatch = source.match(/static\s+binding\s*=\s*['"](\w+)['"]/);
32
+ const baseName = file.replace(/\.do\.ts$/, '').replace(/\.ts$/, '');
33
+ const derivedBinding = baseName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase() + '_DO';
34
+ const binding = bindingMatch?.[1] ?? derivedBinding;
35
+ if (seenBindings.has(binding)) {
36
+ throw new Error(`[KuratchiJS] Duplicate DO binding '${binding}' detected. Use 'static binding = "UNIQUE_NAME"' in one of the classes.`);
37
+ }
38
+ seenBindings.add(binding);
39
+ const classMethods = extractClassMethods(source, className);
40
+ const fileName = file.replace(/\.ts$/, '');
41
+ const existing = fileNameToAbsPath.get(fileName);
42
+ if (existing && existing !== absPath) {
43
+ throw new Error(`[KuratchiJS] Duplicate DO handler file name '${fileName}.ts' detected:\n- ${existing}\n- ${absPath}\nRename one file or move it to avoid proxy name collision.`);
44
+ }
45
+ fileNameToAbsPath.set(fileName, absPath);
46
+ const configEntry = configByBinding.get(binding);
47
+ void ormDatabases;
48
+ discoveredConfig.push({
49
+ binding,
50
+ className,
51
+ stubId: configEntry?.stubId,
52
+ files: [file],
53
+ });
54
+ handlers.push({
55
+ fileName,
56
+ absPath,
57
+ binding,
58
+ mode: 'class',
59
+ className,
60
+ classMethods,
61
+ exportedFunctions: [],
62
+ });
63
+ }
64
+ return { config: discoveredConfig, handlers };
65
+ }
66
+ export function generateHandlerProxy(handler, opts) {
67
+ const doDir = path.join(opts.projectDir, '.kuratchi', 'do');
68
+ const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
69
+ const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
70
+ const lifecycle = new Set(['constructor', 'fetch', 'alarm', 'webSocketMessage', 'webSocketClose', 'webSocketError']);
71
+ const rpcFunctions = handler.classMethods
72
+ .filter((method) => method.visibility === 'public' && !method.name.startsWith('_') && !lifecycle.has(method.name))
73
+ .map((method) => method.name);
74
+ const methods = handler.classMethods.map((method) => ({ ...method }));
75
+ const methodMap = new Map(methods.map((method) => [method.name, method]));
76
+ let changed = true;
77
+ while (changed) {
78
+ changed = false;
79
+ for (const method of methods) {
80
+ if (method.hasWorkerContextCalls)
81
+ continue;
82
+ for (const called of method.callsThisMethods) {
83
+ const target = methodMap.get(called);
84
+ if (target?.hasWorkerContextCalls) {
85
+ method.hasWorkerContextCalls = true;
86
+ changed = true;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ const workerContextMethods = methods
93
+ .filter((method) => method.visibility === 'public' && method.hasWorkerContextCalls)
94
+ .map((method) => method.name);
95
+ const asyncMethods = methods.filter((method) => method.isAsync).map((method) => method.name);
96
+ const lines = [
97
+ `// Auto-generated by KuratchiJS compiler �" do not edit.`,
98
+ `import { __getDoStub } from '${opts.runtimeDoImport}';`,
99
+ `import ${handlerLocal} from '${origRelPath}';`,
100
+ ``,
101
+ `const __FD_TAG = '__kuratchi_form_data__';`,
102
+ `function __isPlainObject(__v) {`,
103
+ ` if (!__v || typeof __v !== 'object') return false;`,
104
+ ` const __proto = Object.getPrototypeOf(__v);`,
105
+ ` return __proto === Object.prototype || __proto === null;`,
106
+ `}`,
107
+ `function __encodeArg(__v, __seen = new WeakSet()) {`,
108
+ ` if (typeof FormData !== 'undefined' && __v instanceof FormData) {`,
109
+ ` return { [__FD_TAG]: Array.from(__v.entries()) };`,
110
+ ` }`,
111
+ ` if (Array.isArray(__v)) return __v.map((__x) => __encodeArg(__x, __seen));`,
112
+ ` if (__isPlainObject(__v)) {`,
113
+ ` if (__seen.has(__v)) throw new Error('[KuratchiJS] Circular object passed to DO RPC');`,
114
+ ` __seen.add(__v);`,
115
+ ` const __out = {};`,
116
+ ` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __encodeArg(__val, __seen);`,
117
+ ` __seen.delete(__v);`,
118
+ ` return __out;`,
119
+ ` }`,
120
+ ` return __v;`,
121
+ `}`,
122
+ `function __decodeArg(__v) {`,
123
+ ` if (Array.isArray(__v)) return __v.map(__decodeArg);`,
124
+ ` if (__isPlainObject(__v)) {`,
125
+ ` const __obj = __v;`,
126
+ ` if (__FD_TAG in __obj) {`,
127
+ ` const __fd = new FormData();`,
128
+ ` const __entries = Array.isArray(__obj[__FD_TAG]) ? __obj[__FD_TAG] : [];`,
129
+ ` for (const __pair of __entries) {`,
130
+ ` if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]);`,
131
+ ` }`,
132
+ ` return __fd;`,
133
+ ` }`,
134
+ ` const __out = {};`,
135
+ ` for (const [__k, __val] of Object.entries(__obj)) __out[__k] = __decodeArg(__val);`,
136
+ ` return __out;`,
137
+ ` }`,
138
+ ` return __v;`,
139
+ `}`,
140
+ ``,
141
+ ];
142
+ if (workerContextMethods.length > 0) {
143
+ lines.push(`const __workerMethods = new Set(${JSON.stringify(workerContextMethods)});`);
144
+ lines.push(`const __asyncMethods = new Set(${JSON.stringify(asyncMethods)});`);
145
+ lines.push(`function __callWorkerMethod(__name, __args) {`);
146
+ lines.push(` const __self = new Proxy({}, {`);
147
+ lines.push(` get(_, __k) {`);
148
+ lines.push(` if (typeof __k !== 'string') return undefined;`);
149
+ lines.push(` if (__k === 'db') {`);
150
+ lines.push(` throw new Error("[KuratchiJS] Worker-executed DO method cannot use this.db directly. Move DB access into a non-public method and call it via this.<method>().");`);
151
+ lines.push(` }`);
152
+ lines.push(` if (__workerMethods.has(__k)) {`);
153
+ lines.push(` return (...__a) => ${handlerLocal}.prototype[__k].apply(__self, __a);`);
154
+ lines.push(` }`);
155
+ lines.push(` const __local = ${handlerLocal}.prototype[__k];`);
156
+ lines.push(` if (typeof __local === 'function' && !__asyncMethods.has(__k)) {`);
157
+ lines.push(` return (...__a) => __local.apply(__self, __a);`);
158
+ lines.push(` }`);
159
+ lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a.map((__x) => __encodeArg(__x))); };`);
160
+ lines.push(` },`);
161
+ lines.push(` });`);
162
+ lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args.map(__decodeArg));`);
163
+ lines.push(`}`);
164
+ lines.push(``);
165
+ }
166
+ for (const method of rpcFunctions) {
167
+ if (workerContextMethods.includes(method)) {
168
+ lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', a); }`);
169
+ }
170
+ else {
171
+ lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a.map((__x) => __encodeArg(__x))); }`);
172
+ }
173
+ }
174
+ return lines.join('\n') + '\n';
175
+ }
176
+ function extractClassMethods(source, className) {
177
+ const classIdx = source.search(new RegExp(`class\\s+${className}\\s+extends\\s+DurableObject`));
178
+ if (classIdx === -1)
179
+ return [];
180
+ const braceStart = source.indexOf('{', classIdx);
181
+ if (braceStart === -1)
182
+ return [];
183
+ let depth = 0;
184
+ let braceEnd = braceStart;
185
+ for (let i = braceStart; i < source.length; i++) {
186
+ if (source[i] === '{')
187
+ depth++;
188
+ else if (source[i] === '}') {
189
+ depth--;
190
+ if (depth === 0) {
191
+ braceEnd = i;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ const classBody = source.slice(braceStart + 1, braceEnd);
197
+ const methods = [];
198
+ const methodRegex = /^\s+(?:(public|private|protected)\s+)?(?:(static)\s+)?(?:(async)\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::[^{]+)?\{/gm;
199
+ const reserved = new Set([
200
+ 'constructor', 'static', 'get', 'set',
201
+ 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case',
202
+ 'throw', 'try', 'catch', 'finally', 'new', 'delete', 'typeof',
203
+ 'void', 'instanceof', 'in', 'of', 'await', 'yield', 'const',
204
+ 'let', 'var', 'function', 'class', 'import', 'export', 'default',
205
+ 'break', 'continue', 'with', 'super', 'this',
206
+ ]);
207
+ let match;
208
+ while ((match = methodRegex.exec(classBody)) !== null) {
209
+ const visibility = match[1] ?? 'public';
210
+ const isStatic = !!match[2];
211
+ const isAsync = !!match[3];
212
+ const name = match[4];
213
+ if (!name || isStatic || reserved.has(name))
214
+ continue;
215
+ const matchText = match[0] ?? '';
216
+ const openRel = matchText.lastIndexOf('{');
217
+ const openAbs = openRel >= 0 ? match.index + openRel : -1;
218
+ let hasWorkerContextCalls = false;
219
+ const callsThisMethods = [];
220
+ if (openAbs >= 0) {
221
+ let innerDepth = 0;
222
+ let endAbs = openAbs;
223
+ for (let i = openAbs; i < classBody.length; i++) {
224
+ const ch = classBody[i];
225
+ if (ch === '{')
226
+ innerDepth++;
227
+ else if (ch === '}') {
228
+ innerDepth--;
229
+ if (innerDepth === 0) {
230
+ endAbs = i;
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ const bodySource = classBody.slice(openAbs + 1, endAbs);
236
+ hasWorkerContextCalls = /\b(getCurrentUser|redirect|goto|getRequest|getLocals)\s*\(/.test(bodySource);
237
+ const called = new Set();
238
+ const callRegex = /\bthis\.([A-Za-z_$][\w$]*)\s*\(/g;
239
+ let callMatch;
240
+ while ((callMatch = callRegex.exec(bodySource)) !== null) {
241
+ called.add(callMatch[1]);
242
+ }
243
+ callsThisMethods.push(...called);
244
+ }
245
+ methods.push({
246
+ name,
247
+ visibility: visibility,
248
+ isStatic,
249
+ isAsync,
250
+ hasWorkerContextCalls,
251
+ callsThisMethods,
252
+ });
253
+ }
254
+ return methods;
255
+ }
@@ -0,0 +1 @@
1
+ export declare function compileErrorPages(routesDir: string): Map<number, string>;