@kuratchi/js 0.0.18 → 0.0.20

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 (40) hide show
  1. package/README.md +172 -9
  2. package/dist/compiler/client-module-pipeline.d.ts +8 -0
  3. package/dist/compiler/client-module-pipeline.js +181 -30
  4. package/dist/compiler/compiler-shared.d.ts +23 -0
  5. package/dist/compiler/config-reading.js +27 -1
  6. package/dist/compiler/convention-discovery.d.ts +2 -0
  7. package/dist/compiler/convention-discovery.js +16 -0
  8. package/dist/compiler/durable-object-pipeline.d.ts +1 -0
  9. package/dist/compiler/durable-object-pipeline.js +459 -119
  10. package/dist/compiler/import-linking.js +1 -1
  11. package/dist/compiler/index.js +41 -2
  12. package/dist/compiler/page-route-pipeline.js +31 -2
  13. package/dist/compiler/parser.d.ts +1 -0
  14. package/dist/compiler/parser.js +47 -4
  15. package/dist/compiler/root-layout-pipeline.js +26 -1
  16. package/dist/compiler/route-discovery.js +5 -5
  17. package/dist/compiler/route-pipeline.d.ts +2 -0
  18. package/dist/compiler/route-pipeline.js +28 -4
  19. package/dist/compiler/routes-module-feature-blocks.js +149 -17
  20. package/dist/compiler/routes-module-types.d.ts +1 -0
  21. package/dist/compiler/template.d.ts +4 -0
  22. package/dist/compiler/template.js +50 -18
  23. package/dist/compiler/worker-output-pipeline.js +2 -0
  24. package/dist/compiler/wrangler-sync.d.ts +3 -0
  25. package/dist/compiler/wrangler-sync.js +25 -11
  26. package/dist/create.js +6 -6
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +1 -0
  29. package/dist/runtime/context.d.ts +6 -0
  30. package/dist/runtime/context.js +22 -1
  31. package/dist/runtime/generated-worker.d.ts +1 -0
  32. package/dist/runtime/generated-worker.js +11 -7
  33. package/dist/runtime/index.d.ts +2 -0
  34. package/dist/runtime/index.js +1 -0
  35. package/dist/runtime/schema.d.ts +49 -0
  36. package/dist/runtime/schema.js +148 -0
  37. package/dist/runtime/types.d.ts +2 -0
  38. package/dist/runtime/validation.d.ts +26 -0
  39. package/dist/runtime/validation.js +147 -0
  40. package/package.json +5 -1
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import * as ts from 'typescript';
3
4
  import { toSafeIdentifier, } from './compiler-shared.js';
4
- import { discoverFilesWithSuffix } from './convention-discovery.js';
5
+ import { discoverFilesWithExtensions, discoverFilesWithSuffix } from './convention-discovery.js';
5
6
  export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
6
7
  const serverDir = path.join(srcDir, 'server');
7
8
  const legacyDir = path.join(srcDir, 'durable-objects');
@@ -11,61 +12,475 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
11
12
  if (discoveredFiles.length === 0) {
12
13
  return { config: configDoEntries, handlers: [] };
13
14
  }
14
- const configByBinding = new Map();
15
+ const bindings = new Set(configDoEntries.map((d) => d.binding));
16
+ const fileToBinding = new Map();
15
17
  for (const entry of configDoEntries) {
16
- configByBinding.set(entry.binding, entry);
18
+ for (const rawFile of entry.files ?? []) {
19
+ const normalized = rawFile.trim().replace(/^\.?[\\/]/, '').replace(/\\/g, '/').toLowerCase();
20
+ if (!normalized)
21
+ continue;
22
+ fileToBinding.set(normalized, entry.binding);
23
+ const base = path.basename(normalized);
24
+ if (!fileToBinding.has(base))
25
+ fileToBinding.set(base, entry.binding);
26
+ }
17
27
  }
18
28
  const handlers = [];
19
- const discoveredConfig = [];
20
- const fileNameToAbsPath = new Map();
21
- const seenBindings = new Set();
29
+ const handlerIdToAbsPath = new Map();
22
30
  for (const absPath of discoveredFiles) {
23
31
  const file = path.basename(absPath);
24
32
  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)
33
+ const exportedFunctions = extractExportedFunctions(source);
34
+ const defaultMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+([A-Za-z_$][\w$]*)/);
35
+ const namedMatch = source.match(/export\s+class\s+(\w+)\s+extends\s+([A-Za-z_$][\w$]*)/);
36
+ let className = null;
37
+ let exportKind;
38
+ if (defaultMatch && isDurableObjectSubclass(absPath, source, defaultMatch[1])) {
39
+ className = defaultMatch[1] ?? null;
40
+ exportKind = 'default';
41
+ }
42
+ else if (namedMatch && isDurableObjectSubclass(absPath, source, namedMatch[1])) {
43
+ className = namedMatch[1] ?? null;
44
+ exportKind = 'named';
45
+ }
46
+ const hasClass = !!className;
47
+ if (!hasClass && exportedFunctions.length === 0)
30
48
  continue;
49
+ // Binding resolution:
50
+ // 1) explicit static binding declared in the class
51
+ // 2) config-mapped file name
52
+ // 3) if exactly one binding exists, infer it
53
+ let binding = null;
31
54
  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);
55
+ if (bindingMatch) {
56
+ binding = bindingMatch[1];
57
+ }
58
+ else {
59
+ const normalizedFile = file.replace(/\\/g, '/').toLowerCase();
60
+ const normalizedRelFromSrc = path.relative(srcDir, absPath).replace(/\\/g, '/').toLowerCase();
61
+ binding = className ? (configDoEntries.find((entry) => entry.className === className)?.binding ?? null) : null;
62
+ if (!binding) {
63
+ binding = fileToBinding.get(normalizedRelFromSrc) ?? fileToBinding.get(normalizedFile) ?? null;
64
+ }
65
+ if (!binding && configDoEntries.length === 1) {
66
+ binding = configDoEntries[0].binding;
67
+ }
68
+ }
69
+ if (!binding)
70
+ continue;
71
+ if (!bindings.has(binding))
72
+ continue;
73
+ const classMethods = className ? extractClassMethods(absPath, source, className) : [];
74
+ const fileName = path
75
+ .relative(absPath.startsWith(serverDir) ? serverDir : legacyDir, absPath)
76
+ .replace(/\\/g, '/')
77
+ .replace(/\.ts$/, '');
78
+ const existing = handlerIdToAbsPath.get(fileName);
42
79
  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.`);
80
+ throw new Error(`[KuratchiJS] Duplicate DO handler id '${fileName}.ts' detected:\n- ${existing}\n- ${absPath}\nRename one file or move it to avoid proxy name collision.`);
44
81
  }
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
- });
82
+ handlerIdToAbsPath.set(fileName, absPath);
54
83
  handlers.push({
55
84
  fileName,
56
85
  absPath,
57
86
  binding,
58
- mode: 'class',
59
- className,
87
+ mode: hasClass ? 'class' : 'function',
88
+ className: className ?? undefined,
89
+ exportKind,
60
90
  classMethods,
61
- exportedFunctions: [],
91
+ classContributors: [],
92
+ exportedFunctions,
93
+ });
94
+ }
95
+ // Discover contributor classes and merge their methods into each base handler.
96
+ for (const handler of handlers) {
97
+ if (handler.mode !== 'class' || !handler.className)
98
+ continue;
99
+ const contributors = discoverDoClassContributors(handler);
100
+ handler.classContributors = contributors;
101
+ if (contributors.length > 0) {
102
+ handler.classMethods = mergeDoClassMethods(handler.classMethods, contributors);
103
+ }
104
+ }
105
+ // Build config entries from discovered handlers (de-duped by binding).
106
+ // Prefer class name from the original config entry (e.g. from wrangler.jsonc).
107
+ const discoveredConfigByBinding = new Map();
108
+ for (const handler of handlers) {
109
+ const configEntry = configDoEntries.find((e) => e.binding === handler.binding);
110
+ const existing = discoveredConfigByBinding.get(handler.binding);
111
+ if (!existing) {
112
+ discoveredConfigByBinding.set(handler.binding, {
113
+ binding: handler.binding,
114
+ // Use config class name when available (authoritative, e.g. from wrangler.jsonc).
115
+ className: configEntry?.className ?? handler.className ?? handler.binding,
116
+ stubId: configEntry?.stubId,
117
+ files: [path.basename(handler.absPath)],
118
+ });
119
+ }
120
+ else {
121
+ existing.files?.push(path.basename(handler.absPath));
122
+ }
123
+ }
124
+ void ormDatabases;
125
+ return { config: [...discoveredConfigByBinding.values()], handlers };
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // TypeScript AST helpers
129
+ // ---------------------------------------------------------------------------
130
+ function extractExportedFunctions(source) {
131
+ const out = [];
132
+ const fnRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
133
+ let m;
134
+ while ((m = fnRegex.exec(source)) !== null)
135
+ out.push(m[1]);
136
+ return out;
137
+ }
138
+ function extractExportedClasses(source) {
139
+ const sourceFile = ts.createSourceFile('classes.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
140
+ const out = [];
141
+ const hasModifier = (node, kind) => (node.modifiers ?? ts.factory.createNodeArray()).some((m) => m.kind === kind);
142
+ const visit = (node) => {
143
+ if (ts.isClassDeclaration(node) && node.name?.text && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
144
+ out.push({
145
+ className: node.name.text,
146
+ exportKind: hasModifier(node, ts.SyntaxKind.DefaultKeyword) ? 'default' : 'named',
147
+ });
148
+ }
149
+ ts.forEachChild(node, visit);
150
+ };
151
+ ts.forEachChild(sourceFile, visit);
152
+ return out;
153
+ }
154
+ function extractOwnClassMethods(source, className) {
155
+ const sourceFile = ts.createSourceFile(`${className}.ts`, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
156
+ let classDecl = null;
157
+ const visit = (node) => {
158
+ if (classDecl)
159
+ return;
160
+ if (ts.isClassDeclaration(node) && node.name?.text === className) {
161
+ classDecl = node;
162
+ return;
163
+ }
164
+ ts.forEachChild(node, visit);
165
+ };
166
+ ts.forEachChild(sourceFile, visit);
167
+ if (!classDecl)
168
+ return null;
169
+ const targetClass = classDecl;
170
+ let extendsName = null;
171
+ for (const clause of targetClass.heritageClauses ?? []) {
172
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword)
173
+ continue;
174
+ const heritage = clause.types[0];
175
+ if (!heritage)
176
+ continue;
177
+ const expr = heritage.expression;
178
+ if (ts.isIdentifier(expr)) {
179
+ extendsName = expr.text;
180
+ }
181
+ else if (ts.isPropertyAccessExpression(expr)) {
182
+ extendsName = expr.name.text;
183
+ }
184
+ else {
185
+ extendsName = expr.getText(sourceFile).trim() || null;
186
+ }
187
+ break;
188
+ }
189
+ const methods = [];
190
+ for (const member of targetClass.members) {
191
+ if (!ts.isMethodDeclaration(member))
192
+ continue;
193
+ if (!member.body)
194
+ continue;
195
+ if (!member.name || !ts.isIdentifier(member.name))
196
+ continue;
197
+ const name = member.name.text;
198
+ const modifiers = member.modifiers ?? ts.factory.createNodeArray();
199
+ const visibility = modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword)
200
+ ? 'private'
201
+ : modifiers.some((m) => m.kind === ts.SyntaxKind.ProtectedKeyword)
202
+ ? 'protected'
203
+ : 'public';
204
+ const isStatic = modifiers.some((m) => m.kind === ts.SyntaxKind.StaticKeyword);
205
+ const isAsync = modifiers.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
206
+ if (isStatic)
207
+ continue;
208
+ const bodySource = member.body.getText(sourceFile);
209
+ const hasWorkerContextCalls = /\b(getCurrentUser|redirect|goto|getRequest|getLocals)\s*\(/.test(bodySource);
210
+ const called = new Set();
211
+ const visitBody = (node) => {
212
+ if (ts.isCallExpression(node) &&
213
+ ts.isPropertyAccessExpression(node.expression) &&
214
+ node.expression.expression.kind === ts.SyntaxKind.ThisKeyword) {
215
+ called.add(node.expression.name.text);
216
+ }
217
+ ts.forEachChild(node, visitBody);
218
+ };
219
+ ts.forEachChild(member.body, visitBody);
220
+ methods.push({
221
+ name,
222
+ visibility,
223
+ isStatic,
224
+ isAsync,
225
+ hasWorkerContextCalls,
226
+ callsThisMethods: [...called],
62
227
  });
63
228
  }
64
- return { config: discoveredConfig, handlers };
229
+ return { methods, extendsName };
230
+ }
231
+ function resolveBaseClassReference(absPath, source, baseName) {
232
+ if (new RegExp(`class\\s+${baseName}\\b`).test(source)) {
233
+ return { absPath, className: baseName };
234
+ }
235
+ const imports = extractRelativeClassImports(source);
236
+ const ref = imports.get(baseName);
237
+ if (!ref)
238
+ return null;
239
+ const targetAbsPath = resolveRelativeModulePath(absPath, ref.source);
240
+ if (!targetAbsPath || !fs.existsSync(targetAbsPath))
241
+ return null;
242
+ if (ref.importedName !== 'default') {
243
+ return { absPath: targetAbsPath, className: ref.importedName };
244
+ }
245
+ const targetSource = fs.readFileSync(targetAbsPath, 'utf-8');
246
+ const namedDefaultClass = targetSource.match(/export\s+default\s+class\s+([A-Za-z_$][\w$]*)\b/);
247
+ if (namedDefaultClass) {
248
+ return { absPath: targetAbsPath, className: namedDefaultClass[1] };
249
+ }
250
+ const defaultAlias = targetSource.match(/export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/);
251
+ if (defaultAlias) {
252
+ return { absPath: targetAbsPath, className: defaultAlias[1] };
253
+ }
254
+ return null;
255
+ }
256
+ function extractRelativeClassImports(source) {
257
+ const imports = new Map();
258
+ const importRegex = /import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"]/g;
259
+ let match;
260
+ while ((match = importRegex.exec(source)) !== null) {
261
+ const clause = String(match[1] ?? '').trim();
262
+ const specifier = String(match[2] ?? '').trim();
263
+ if (!specifier.startsWith('.'))
264
+ continue;
265
+ if (!clause || clause.startsWith('*'))
266
+ continue;
267
+ let defaultPart = '';
268
+ let namedPart = '';
269
+ if (clause.startsWith('{')) {
270
+ namedPart = clause;
271
+ }
272
+ else if (clause.includes('{')) {
273
+ const splitIdx = clause.indexOf('{');
274
+ defaultPart = clause.slice(0, splitIdx).replace(/,$/, '').trim();
275
+ namedPart = clause.slice(splitIdx).trim();
276
+ }
277
+ else {
278
+ defaultPart = clause.trim();
279
+ }
280
+ if (defaultPart) {
281
+ imports.set(defaultPart, { source: specifier, importedName: 'default' });
282
+ }
283
+ if (namedPart) {
284
+ const namedBody = namedPart.replace(/^\{/, '').replace(/\}$/, '');
285
+ for (const entry of namedBody.split(',')) {
286
+ const trimmed = entry.trim();
287
+ if (!trimmed)
288
+ continue;
289
+ const namedMatch = trimmed.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
290
+ if (!namedMatch)
291
+ continue;
292
+ const importedName = namedMatch[1];
293
+ const localName = namedMatch[2] ?? importedName;
294
+ imports.set(localName, { source: specifier, importedName });
295
+ }
296
+ }
297
+ }
298
+ return imports;
299
+ }
300
+ function resolveRelativeModulePath(importerAbsPath, specifier) {
301
+ const basePath = path.resolve(path.dirname(importerAbsPath), specifier);
302
+ const moduleExt = path.extname(basePath).toLowerCase();
303
+ const hasSourceExtension = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs']).has(moduleExt);
304
+ const candidates = hasSourceExtension
305
+ ? [basePath]
306
+ : [
307
+ `${basePath}.ts`,
308
+ `${basePath}.tsx`,
309
+ `${basePath}.js`,
310
+ `${basePath}.mjs`,
311
+ `${basePath}.cjs`,
312
+ path.join(basePath, 'index.ts'),
313
+ path.join(basePath, 'index.tsx'),
314
+ path.join(basePath, 'index.js'),
315
+ path.join(basePath, 'index.mjs'),
316
+ path.join(basePath, 'index.cjs'),
317
+ ];
318
+ for (const candidate of candidates) {
319
+ if (fs.existsSync(candidate))
320
+ return candidate;
321
+ }
322
+ return null;
323
+ }
324
+ function getClassExtensionDistance(absPath, source, className, targetAbsPath, targetClassName, cache = new Map(), stack = new Set()) {
325
+ const cacheKey = `${absPath}::${className}=>${targetAbsPath}::${targetClassName}`;
326
+ if (cache.has(cacheKey))
327
+ return cache.get(cacheKey) ?? null;
328
+ if (stack.has(cacheKey))
329
+ return null;
330
+ stack.add(cacheKey);
331
+ const own = extractOwnClassMethods(source, className);
332
+ if (!own?.extendsName) {
333
+ cache.set(cacheKey, null);
334
+ stack.delete(cacheKey);
335
+ return null;
336
+ }
337
+ const baseRef = resolveBaseClassReference(absPath, source, own.extendsName);
338
+ if (!baseRef) {
339
+ cache.set(cacheKey, null);
340
+ stack.delete(cacheKey);
341
+ return null;
342
+ }
343
+ if (baseRef.absPath === targetAbsPath && baseRef.className === targetClassName) {
344
+ cache.set(cacheKey, 1);
345
+ stack.delete(cacheKey);
346
+ return 1;
347
+ }
348
+ const baseSource = fs.readFileSync(baseRef.absPath, 'utf-8');
349
+ const parentDistance = getClassExtensionDistance(baseRef.absPath, baseSource, baseRef.className, targetAbsPath, targetClassName, cache, stack);
350
+ const result = parentDistance == null ? null : parentDistance + 1;
351
+ cache.set(cacheKey, result);
352
+ stack.delete(cacheKey);
353
+ return result;
354
+ }
355
+ function isDurableObjectSubclass(absPath, source, className, cache = new Map(), stack = new Set()) {
356
+ const cacheKey = `${absPath}::${className}`;
357
+ const cached = cache.get(cacheKey);
358
+ if (cached != null)
359
+ return cached;
360
+ if (stack.has(cacheKey))
361
+ return false;
362
+ stack.add(cacheKey);
363
+ const own = extractOwnClassMethods(source, className);
364
+ if (!own?.extendsName) {
365
+ cache.set(cacheKey, false);
366
+ stack.delete(cacheKey);
367
+ return false;
368
+ }
369
+ if (own.extendsName === 'DurableObject' || own.extendsName === 'kuratchiDO') {
370
+ cache.set(cacheKey, true);
371
+ stack.delete(cacheKey);
372
+ return true;
373
+ }
374
+ const baseRef = resolveBaseClassReference(absPath, source, own.extendsName);
375
+ if (!baseRef) {
376
+ cache.set(cacheKey, false);
377
+ stack.delete(cacheKey);
378
+ return false;
379
+ }
380
+ const baseSource = fs.readFileSync(baseRef.absPath, 'utf-8');
381
+ const result = isDurableObjectSubclass(baseRef.absPath, baseSource, baseRef.className, cache, stack);
382
+ cache.set(cacheKey, result);
383
+ stack.delete(cacheKey);
384
+ return result;
385
+ }
386
+ function extractClassMethods(absPath, source, className) {
387
+ const cache = new Map();
388
+ return resolveClassMethods(absPath, source, className, cache, new Set());
389
+ }
390
+ function resolveClassMethods(absPath, source, className, cache, stack) {
391
+ const cacheKey = `${absPath}::${className}`;
392
+ const cached = cache.get(cacheKey);
393
+ if (cached)
394
+ return cached.map((e) => ({ ...e, callsThisMethods: [...e.callsThisMethods] }));
395
+ if (stack.has(cacheKey))
396
+ return [];
397
+ stack.add(cacheKey);
398
+ const own = extractOwnClassMethods(source, className);
399
+ if (!own) {
400
+ stack.delete(cacheKey);
401
+ cache.set(cacheKey, []);
402
+ return [];
403
+ }
404
+ let inherited = [];
405
+ const extendsName = own.extendsName;
406
+ if (extendsName && extendsName !== 'DurableObject' && extendsName !== 'kuratchiDO') {
407
+ const baseRef = resolveBaseClassReference(absPath, source, extendsName);
408
+ if (baseRef) {
409
+ const baseSource = fs.readFileSync(baseRef.absPath, 'utf-8');
410
+ inherited = resolveClassMethods(baseRef.absPath, baseSource, baseRef.className, cache, stack);
411
+ }
412
+ }
413
+ const merged = new Map();
414
+ for (const method of inherited) {
415
+ merged.set(method.name, { ...method, callsThisMethods: [...method.callsThisMethods] });
416
+ }
417
+ for (const method of own.methods) {
418
+ merged.set(method.name, { ...method, callsThisMethods: [...method.callsThisMethods] });
419
+ }
420
+ const result = [...merged.values()];
421
+ cache.set(cacheKey, result.map((e) => ({ ...e, callsThisMethods: [...e.callsThisMethods] })));
422
+ stack.delete(cacheKey);
423
+ return result;
424
+ }
425
+ function discoverDoClassContributors(handler) {
426
+ if (handler.mode !== 'class' || !handler.className)
427
+ return [];
428
+ const folder = path.dirname(handler.absPath);
429
+ const files = discoverFilesWithExtensions(folder, ['.ts', '.tsx', '.js', '.mjs', '.cjs']);
430
+ const contributors = [];
431
+ const seen = new Set();
432
+ for (const absPath of files) {
433
+ const source = fs.readFileSync(absPath, 'utf-8');
434
+ const exportedClasses = extractExportedClasses(source);
435
+ for (const exportedClass of exportedClasses) {
436
+ if (absPath === handler.absPath && exportedClass.className === handler.className)
437
+ continue;
438
+ const depth = getClassExtensionDistance(absPath, source, exportedClass.className, handler.absPath, handler.className);
439
+ if (depth == null || depth < 1)
440
+ continue;
441
+ const own = extractOwnClassMethods(source, exportedClass.className);
442
+ if (!own)
443
+ continue;
444
+ const key = `${absPath}::${exportedClass.className}`;
445
+ if (seen.has(key))
446
+ continue;
447
+ seen.add(key);
448
+ contributors.push({
449
+ absPath,
450
+ className: exportedClass.className,
451
+ exportKind: exportedClass.exportKind,
452
+ classMethods: own.methods.map((m) => ({ ...m, callsThisMethods: [...m.callsThisMethods] })),
453
+ depth,
454
+ });
455
+ }
456
+ }
457
+ contributors.sort((a, b) => {
458
+ if (a.depth !== b.depth)
459
+ return a.depth - b.depth;
460
+ const fc = a.absPath.localeCompare(b.absPath);
461
+ if (fc !== 0)
462
+ return fc;
463
+ return a.className.localeCompare(b.className);
464
+ });
465
+ return contributors;
466
+ }
467
+ function mergeDoClassMethods(baseMethods, contributors) {
468
+ const merged = new Map();
469
+ for (const method of baseMethods) {
470
+ merged.set(method.name, { ...method, callsThisMethods: [...method.callsThisMethods] });
471
+ }
472
+ for (const contributor of contributors) {
473
+ for (const method of contributor.classMethods) {
474
+ merged.set(method.name, { ...method, callsThisMethods: [...method.callsThisMethods] });
475
+ }
476
+ }
477
+ return [...merged.values()];
65
478
  }
66
479
  export function generateHandlerProxy(handler, opts) {
67
480
  const doDir = path.join(opts.projectDir, '.kuratchi', 'do');
68
- const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/');
481
+ const proxyFile = path.join(doDir, handler.fileName + '.ts');
482
+ const proxyFileDir = path.dirname(proxyFile);
483
+ const origRelPath = path.relative(proxyFileDir, handler.absPath).replace(/\\/g, '/');
69
484
  const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
70
485
  const lifecycle = new Set(['constructor', 'fetch', 'alarm', 'webSocketMessage', 'webSocketClose', 'webSocketError']);
71
486
  const rpcFunctions = handler.classMethods
@@ -93,10 +508,14 @@ export function generateHandlerProxy(handler, opts) {
93
508
  .filter((method) => method.visibility === 'public' && method.hasWorkerContextCalls)
94
509
  .map((method) => method.name);
95
510
  const asyncMethods = methods.filter((method) => method.isAsync).map((method) => method.name);
511
+ const handlerImport = handler.exportKind === 'named' && handler.className
512
+ ? `import { ${handler.className} as ${handlerLocal} } from '${origRelPath}';`
513
+ : `import ${handlerLocal} from '${origRelPath}';`;
96
514
  const lines = [
97
515
  `// Auto-generated by KuratchiJS compiler �" do not edit.`,
98
516
  `import { __getDoStub } from '${opts.runtimeDoImport}';`,
99
- `import ${handlerLocal} from '${origRelPath}';`,
517
+ `import { validateSchemaInput as __validateSchemaInput } from '${opts.runtimeSchemaImport}';`,
518
+ handlerImport,
100
519
  ``,
101
520
  `const __FD_TAG = '__kuratchi_form_data__';`,
102
521
  `function __isPlainObject(__v) {`,
@@ -164,92 +583,13 @@ export function generateHandlerProxy(handler, opts) {
164
583
  lines.push(``);
165
584
  }
166
585
  for (const method of rpcFunctions) {
586
+ lines.push(`const __schema_${toSafeIdentifier(method)} = ${handlerLocal}.schemas?.[${JSON.stringify(method)}];`);
167
587
  if (workerContextMethods.includes(method)) {
168
- lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', a); }`);
588
+ lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', __validateSchemaInput(__schema_${toSafeIdentifier(method)}, a)); }`);
169
589
  }
170
590
  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))); }`);
591
+ lines.push(`export async function ${method}(...a) { const __validated = __validateSchemaInput(__schema_${toSafeIdentifier(method)}, a); const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...__validated.map((__x) => __encodeArg(__x))); }`);
172
592
  }
173
593
  }
174
594
  return lines.join('\n') + '\n';
175
595
  }
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
- }
@@ -120,7 +120,7 @@ export function linkRouteServerImports(opts) {
120
120
  continue;
121
121
  }
122
122
  fnToModule[binding.local] = moduleId;
123
- if (!routeImportDeclMap.has(binding.local) && !RESERVED_RENDER_VARS.has(binding.local)) {
123
+ if (!routeImportDeclMap.has(binding.local)) {
124
124
  const accessExpr = binding.imported === 'default' ? `${moduleId}.default` : `${moduleId}.${binding.imported}`;
125
125
  routeImportDeclMap.set(binding.local, `const ${binding.local} = ${accessExpr};`);
126
126
  }