@kuratchi/js 0.0.19 → 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.
- package/README.md +168 -5
- package/dist/compiler/client-module-pipeline.d.ts +8 -0
- package/dist/compiler/client-module-pipeline.js +181 -30
- package/dist/compiler/compiler-shared.d.ts +23 -0
- package/dist/compiler/config-reading.js +27 -1
- package/dist/compiler/convention-discovery.d.ts +2 -0
- package/dist/compiler/convention-discovery.js +16 -0
- package/dist/compiler/durable-object-pipeline.d.ts +1 -0
- package/dist/compiler/durable-object-pipeline.js +459 -119
- package/dist/compiler/index.js +40 -1
- package/dist/compiler/page-route-pipeline.js +31 -2
- package/dist/compiler/parser.d.ts +1 -0
- package/dist/compiler/parser.js +47 -4
- package/dist/compiler/root-layout-pipeline.js +18 -3
- package/dist/compiler/route-pipeline.d.ts +2 -0
- package/dist/compiler/route-pipeline.js +19 -3
- package/dist/compiler/routes-module-feature-blocks.js +143 -17
- package/dist/compiler/routes-module-types.d.ts +1 -0
- package/dist/compiler/template.d.ts +4 -0
- package/dist/compiler/template.js +50 -18
- package/dist/compiler/worker-output-pipeline.js +2 -0
- package/dist/compiler/wrangler-sync.d.ts +3 -0
- package/dist/compiler/wrangler-sync.js +25 -11
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/runtime/context.d.ts +6 -0
- package/dist/runtime/context.js +22 -1
- package/dist/runtime/generated-worker.d.ts +1 -0
- package/dist/runtime/generated-worker.js +11 -7
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/schema.d.ts +49 -0
- package/dist/runtime/schema.js +148 -0
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/validation.d.ts +26 -0
- package/dist/runtime/validation.js +147 -0
- 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
|
|
15
|
+
const bindings = new Set(configDoEntries.map((d) => d.binding));
|
|
16
|
+
const fileToBinding = new Map();
|
|
15
17
|
for (const entry of configDoEntries) {
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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}(...
|
|
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
|
-
}
|