export-runtime 0.0.8 → 0.0.10
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/bin/generate-types.mjs +220 -126
- package/entry.js +2 -2
- package/handler.js +86 -31
- package/package.json +1 -1
- package/rpc.js +30 -6
- package/shared-do.js +2 -2
package/bin/generate-types.mjs
CHANGED
|
@@ -9,30 +9,72 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
|
|
10
10
|
const cwd = process.cwd();
|
|
11
11
|
|
|
12
|
-
// Read
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// --- Read package.json for configuration ---
|
|
13
|
+
|
|
14
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
15
|
+
if (!fs.existsSync(pkgPath)) {
|
|
16
|
+
console.error("package.json not found in", cwd);
|
|
16
17
|
process.exit(1);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
21
|
+
|
|
22
|
+
// Required fields
|
|
23
|
+
const workerName = pkg.name;
|
|
24
|
+
if (!workerName) {
|
|
25
|
+
console.error("package.json must have a 'name' field for the Worker name");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const exportsEntry = pkg.exports;
|
|
30
|
+
if (!exportsEntry) {
|
|
31
|
+
console.error("package.json must have an 'exports' field pointing to the source entry (e.g., './src' or './src/index.ts')");
|
|
22
32
|
process.exit(1);
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
// Optional: static assets directory
|
|
36
|
+
const assetsDir = pkg.main || null;
|
|
37
|
+
|
|
38
|
+
// --- Resolve source directory from exports field ---
|
|
39
|
+
|
|
40
|
+
const exportsPath = path.resolve(cwd, exportsEntry.replace(/^\.\//, ""));
|
|
41
|
+
const srcDir = fs.existsSync(exportsPath) && fs.statSync(exportsPath).isDirectory()
|
|
42
|
+
? exportsPath
|
|
43
|
+
: path.dirname(exportsPath);
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(srcDir)) {
|
|
46
|
+
console.error(`Source directory not found: ${srcDir}`);
|
|
28
47
|
process.exit(1);
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
// --- Discover all source files under srcDir ---
|
|
51
|
+
|
|
52
|
+
const EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
53
|
+
|
|
54
|
+
function discoverModules(dir, base = "") {
|
|
55
|
+
const modules = [];
|
|
56
|
+
if (!fs.existsSync(dir)) return modules;
|
|
57
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
58
|
+
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
59
|
+
const fullPath = path.join(dir, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
modules.push(...discoverModules(fullPath, base ? `${base}/${entry.name}` : entry.name));
|
|
62
|
+
} else if (EXTENSIONS.includes(path.extname(entry.name))) {
|
|
63
|
+
const nameWithoutExt = entry.name.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
64
|
+
const routePath = nameWithoutExt === "index"
|
|
65
|
+
? base // index.ts → directory path ("" for root)
|
|
66
|
+
: (base ? `${base}/${nameWithoutExt}` : nameWithoutExt);
|
|
67
|
+
modules.push({ routePath, filePath: fullPath });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return modules;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const modules = discoverModules(srcDir);
|
|
74
|
+
if (modules.length === 0) {
|
|
75
|
+
console.error("No source files found in", srcDir);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
36
78
|
|
|
37
79
|
// --- Type extraction helpers ---
|
|
38
80
|
|
|
@@ -52,14 +94,10 @@ function extractType(node) {
|
|
|
52
94
|
case "TSBigIntKeyword": return "bigint";
|
|
53
95
|
case "TSSymbolKeyword": return "symbol";
|
|
54
96
|
case "TSObjectKeyword": return "object";
|
|
55
|
-
case "TSArrayType":
|
|
56
|
-
|
|
57
|
-
case "
|
|
58
|
-
|
|
59
|
-
case "TSUnionType":
|
|
60
|
-
return ta.types.map(t => extractType(t)).join(" | ");
|
|
61
|
-
case "TSIntersectionType":
|
|
62
|
-
return ta.types.map(t => extractType(t)).join(" & ");
|
|
97
|
+
case "TSArrayType": return `${extractType(ta.elementType)}[]`;
|
|
98
|
+
case "TSTupleType": return `[${(ta.elementTypes || []).map(e => extractType(e)).join(", ")}]`;
|
|
99
|
+
case "TSUnionType": return ta.types.map(t => extractType(t)).join(" | ");
|
|
100
|
+
case "TSIntersectionType": return ta.types.map(t => extractType(t)).join(" & ");
|
|
63
101
|
case "TSLiteralType": {
|
|
64
102
|
const lit = ta.literal;
|
|
65
103
|
if (lit.type === "StringLiteral") return JSON.stringify(lit.value);
|
|
@@ -94,10 +132,8 @@ function extractType(node) {
|
|
|
94
132
|
}).filter(Boolean);
|
|
95
133
|
return `{ ${members.join("; ")} }`;
|
|
96
134
|
}
|
|
97
|
-
case "TSTypeAnnotation":
|
|
98
|
-
|
|
99
|
-
default:
|
|
100
|
-
return "any";
|
|
135
|
+
case "TSTypeAnnotation": return extractType(ta.typeAnnotation);
|
|
136
|
+
default: return "any";
|
|
101
137
|
}
|
|
102
138
|
}
|
|
103
139
|
|
|
@@ -116,105 +152,110 @@ function extractParams(params) {
|
|
|
116
152
|
});
|
|
117
153
|
}
|
|
118
154
|
|
|
119
|
-
// Wrap return type: all functions become async over the network
|
|
120
155
|
function wrapReturnType(returnType, isAsync, isGenerator) {
|
|
121
156
|
if (isGenerator) {
|
|
122
|
-
// async generator → AsyncIterable<YieldType>
|
|
123
|
-
// extract inner type from AsyncGenerator<T> if present
|
|
124
157
|
if (returnType.startsWith("AsyncGenerator")) {
|
|
125
158
|
const inner = returnType.match(/^AsyncGenerator<(.+?)(?:,.*)?>/);
|
|
126
159
|
return `Promise<AsyncIterable<${inner ? inner[1] : "any"}>>`;
|
|
127
160
|
}
|
|
128
161
|
return `Promise<AsyncIterable<${returnType === "any" ? "any" : returnType}>>`;
|
|
129
162
|
}
|
|
130
|
-
// Already Promise<T> → keep as-is
|
|
131
163
|
if (returnType.startsWith("Promise<")) return returnType;
|
|
132
|
-
// ReadableStream<T> → Promise<ReadableStream<T>>
|
|
133
164
|
if (returnType.startsWith("ReadableStream")) return `Promise<${returnType}>`;
|
|
134
|
-
// Wrap in Promise
|
|
135
165
|
return `Promise<${returnType}>`;
|
|
136
166
|
}
|
|
137
167
|
|
|
138
|
-
// ---
|
|
168
|
+
// --- Extract types and export names from a single file ---
|
|
139
169
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
""
|
|
144
|
-
|
|
170
|
+
function extractFileTypes(filePath) {
|
|
171
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
172
|
+
const fileName = path.basename(filePath);
|
|
173
|
+
const result = parseSync(fileName, source, { sourceType: "module" });
|
|
174
|
+
const program = result.program;
|
|
145
175
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
const lines = [];
|
|
177
|
+
const exportNames = [];
|
|
178
|
+
|
|
179
|
+
for (const node of program.body) {
|
|
180
|
+
if (node.type !== "ExportNamedDeclaration" || !node.declaration) continue;
|
|
181
|
+
const decl = node.declaration;
|
|
182
|
+
|
|
183
|
+
if (decl.type === "FunctionDeclaration") {
|
|
184
|
+
const name = decl.id.name;
|
|
185
|
+
exportNames.push(name);
|
|
186
|
+
const params = extractParams(decl.params);
|
|
187
|
+
const rawRet = decl.returnType ? extractType(decl.returnType) : "any";
|
|
188
|
+
const ret = wrapReturnType(rawRet, decl.async, decl.generator);
|
|
189
|
+
lines.push(`export declare function ${name}(${params.join(", ")}): ${ret};`);
|
|
190
|
+
lines.push("");
|
|
191
|
+
} else if (decl.type === "ClassDeclaration") {
|
|
192
|
+
const name = decl.id.name;
|
|
193
|
+
exportNames.push(name);
|
|
194
|
+
lines.push(`export declare class ${name} {`);
|
|
195
|
+
for (const member of decl.body.body) {
|
|
196
|
+
if (member.type === "MethodDefinition") {
|
|
197
|
+
const mName = member.key.name || member.key.value;
|
|
198
|
+
if (member.kind === "constructor") {
|
|
199
|
+
lines.push(` constructor(${extractParams(member.value.params).join(", ")});`);
|
|
200
|
+
} else {
|
|
201
|
+
const params = extractParams(member.value.params);
|
|
202
|
+
const rawRet = member.value.returnType ? extractType(member.value.returnType) : "any";
|
|
203
|
+
const ret = wrapReturnType(rawRet, member.value.async, member.value.generator);
|
|
204
|
+
lines.push(` ${mName}(${params.join(", ")}): ${ret};`);
|
|
205
|
+
}
|
|
206
|
+
} else if (member.type === "PropertyDefinition") {
|
|
207
|
+
if (member.accessibility === "private") continue;
|
|
208
|
+
const mName = member.key.name;
|
|
209
|
+
const type = member.typeAnnotation ? extractType(member.typeAnnotation) : "any";
|
|
210
|
+
lines.push(` ${mName}: ${type};`);
|
|
172
211
|
}
|
|
173
|
-
} else if (member.type === "PropertyDefinition") {
|
|
174
|
-
// Skip private members
|
|
175
|
-
if (member.accessibility === "private") continue;
|
|
176
|
-
const mName = member.key.name;
|
|
177
|
-
const type = member.typeAnnotation ? extractType(member.typeAnnotation) : "any";
|
|
178
|
-
lines.push(` ${mName}: ${type};`);
|
|
179
212
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
lines.push(` ${key}: any;`);
|
|
213
|
+
lines.push(` [Symbol.dispose](): Promise<void>;`);
|
|
214
|
+
lines.push(` "[release]"(): Promise<void>;`);
|
|
215
|
+
lines.push(`}`);
|
|
216
|
+
lines.push("");
|
|
217
|
+
} else if (decl.type === "VariableDeclaration") {
|
|
218
|
+
for (const d of decl.declarations) {
|
|
219
|
+
const name = d.id.name;
|
|
220
|
+
exportNames.push(name);
|
|
221
|
+
if (d.init?.type === "ObjectExpression") {
|
|
222
|
+
lines.push(`export declare const ${name}: {`);
|
|
223
|
+
for (const prop of d.init.properties) {
|
|
224
|
+
if (prop.type === "SpreadElement") continue;
|
|
225
|
+
const key = prop.key?.name || prop.key?.value;
|
|
226
|
+
if (prop.value?.type === "FunctionExpression" || prop.value?.type === "ArrowFunctionExpression") {
|
|
227
|
+
const params = extractParams(prop.value.params);
|
|
228
|
+
const rawRet = prop.value.returnType ? extractType(prop.value.returnType) : "any";
|
|
229
|
+
const ret = wrapReturnType(rawRet, prop.value.async, prop.value.generator);
|
|
230
|
+
lines.push(` ${key}(${params.join(", ")}): ${ret};`);
|
|
231
|
+
} else {
|
|
232
|
+
lines.push(` ${key}: any;`);
|
|
233
|
+
}
|
|
202
234
|
}
|
|
235
|
+
lines.push(`};`);
|
|
236
|
+
lines.push("");
|
|
237
|
+
} else {
|
|
238
|
+
const type = d.id.typeAnnotation ? extractType(d.id.typeAnnotation) : "any";
|
|
239
|
+
lines.push(`export declare const ${name}: ${type};`);
|
|
240
|
+
lines.push("");
|
|
203
241
|
}
|
|
204
|
-
lines.push(`};`);
|
|
205
|
-
lines.push("");
|
|
206
|
-
} else {
|
|
207
|
-
const type = d.id.typeAnnotation ? extractType(d.id.typeAnnotation) : "any";
|
|
208
|
-
lines.push(`export declare const ${name}: ${type};`);
|
|
209
|
-
lines.push("");
|
|
210
242
|
}
|
|
211
243
|
}
|
|
212
244
|
}
|
|
245
|
+
|
|
246
|
+
return { types: lines.join("\n"), exportNames };
|
|
213
247
|
}
|
|
214
248
|
|
|
249
|
+
// --- Process all modules ---
|
|
215
250
|
|
|
251
|
+
const typesMap = {}; // routePath → type definition string
|
|
252
|
+
const exportsMap = {}; // routePath → export names array
|
|
216
253
|
|
|
217
|
-
const
|
|
254
|
+
for (const mod of modules) {
|
|
255
|
+
const { types, exportNames } = extractFileTypes(mod.filePath);
|
|
256
|
+
typesMap[mod.routePath] = types;
|
|
257
|
+
exportsMap[mod.routePath] = exportNames;
|
|
258
|
+
}
|
|
218
259
|
|
|
219
260
|
// --- Minify core modules ---
|
|
220
261
|
|
|
@@ -222,42 +263,39 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
222
263
|
const { CORE_CODE, SHARED_CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
|
|
223
264
|
|
|
224
265
|
const minified = minifySync("_core.js", CORE_CODE);
|
|
225
|
-
if (minified.errors?.length)
|
|
226
|
-
console.error("Minification errors (core):", minified.errors);
|
|
227
|
-
}
|
|
266
|
+
if (minified.errors?.length) console.error("Minification errors (core):", minified.errors);
|
|
228
267
|
|
|
229
268
|
const minifiedShared = minifySync("_core-shared.js", SHARED_CORE_CODE);
|
|
230
|
-
if (minifiedShared.errors?.length)
|
|
231
|
-
console.error("Minification errors (shared core):", minifiedShared.errors);
|
|
232
|
-
}
|
|
269
|
+
if (minifiedShared.errors?.length) console.error("Minification errors (shared core):", minifiedShared.errors);
|
|
233
270
|
|
|
234
|
-
// Generate a unique ID per build for cache-busting the core module path
|
|
235
271
|
const coreId = crypto.randomUUID();
|
|
236
272
|
|
|
237
|
-
// Write
|
|
273
|
+
// --- Write .export-types.js ---
|
|
274
|
+
|
|
238
275
|
const outPath = path.join(cwd, ".export-types.js");
|
|
239
276
|
fs.writeFileSync(outPath, [
|
|
240
|
-
`export default ${JSON.stringify(
|
|
277
|
+
`export default ${JSON.stringify(typesMap)};`,
|
|
241
278
|
`export const minifiedCore = ${JSON.stringify(minified.code)};`,
|
|
242
279
|
`export const minifiedSharedCore = ${JSON.stringify(minifiedShared.code)};`,
|
|
243
280
|
`export const coreId = ${JSON.stringify(coreId)};`,
|
|
244
281
|
].join("\n") + "\n");
|
|
245
282
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
// --- Write .export-module-map.js ---
|
|
284
|
+
|
|
285
|
+
const moduleMapPath = path.join(cwd, ".export-module-map.js");
|
|
286
|
+
const relSrcDir = path.relative(cwd, srcDir);
|
|
287
|
+
const mapLines = [];
|
|
288
|
+
modules.forEach((mod, i) => {
|
|
289
|
+
const relFile = "./" + path.relative(cwd, mod.filePath).replace(/\\/g, "/");
|
|
290
|
+
mapLines.push(`import * as _${i} from ${JSON.stringify(relFile)};`);
|
|
291
|
+
});
|
|
292
|
+
mapLines.push(`export default { ${modules.map((mod, i) => `${JSON.stringify(mod.routePath)}: _${i}`).join(", ")} };`);
|
|
293
|
+
fs.writeFileSync(moduleMapPath, mapLines.join("\n") + "\n");
|
|
294
|
+
|
|
295
|
+
// --- Write .export-shared.js ---
|
|
258
296
|
|
|
259
297
|
const sharedModulePath = path.join(cwd, ".export-shared.js");
|
|
260
|
-
const
|
|
298
|
+
const sharedLines = [
|
|
261
299
|
`import { env } from "cloudflare:workers";`,
|
|
262
300
|
``,
|
|
263
301
|
`const getStub = (room = "default") =>`,
|
|
@@ -294,10 +332,66 @@ const sharedModuleLines = [
|
|
|
294
332
|
` });`,
|
|
295
333
|
``,
|
|
296
334
|
`const _stub = getStub();`,
|
|
297
|
-
...exportNames.map(n => `export const ${n} = createSharedProxy(_stub, [${JSON.stringify(n)}]);`),
|
|
298
|
-
`export { getStub };`,
|
|
299
335
|
];
|
|
300
|
-
|
|
336
|
+
// Generate proxies for all modules' exports, prefixed with route
|
|
337
|
+
for (const mod of modules) {
|
|
338
|
+
const names = exportsMap[mod.routePath] || [];
|
|
339
|
+
for (const n of names) {
|
|
340
|
+
const proxyPath = mod.routePath ? `[${JSON.stringify(mod.routePath)}, ${JSON.stringify(n)}]` : `[${JSON.stringify("")}, ${JSON.stringify(n)}]`;
|
|
341
|
+
const exportAlias = mod.routePath ? `${mod.routePath.replace(/\//g, "_")}_${n}` : n;
|
|
342
|
+
sharedLines.push(`export const ${exportAlias} = createSharedProxy(_stub, ${proxyPath});`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
sharedLines.push(`export { getStub };`);
|
|
346
|
+
fs.writeFileSync(sharedModulePath, sharedLines.join("\n") + "\n");
|
|
347
|
+
|
|
348
|
+
// --- Generate wrangler.toml ---
|
|
349
|
+
|
|
350
|
+
const wranglerLines = [
|
|
351
|
+
`# Auto-generated by export-runtime. Do not edit manually.`,
|
|
352
|
+
`name = "${workerName}"`,
|
|
353
|
+
`main = "node_modules/export-runtime/entry.js"`,
|
|
354
|
+
`compatibility_date = "2024-11-01"`,
|
|
355
|
+
``,
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
// Add static assets configuration if main is specified
|
|
359
|
+
if (assetsDir) {
|
|
360
|
+
const normalizedAssetsDir = assetsDir.startsWith("./") ? assetsDir : `./${assetsDir}`;
|
|
361
|
+
wranglerLines.push(
|
|
362
|
+
`[assets]`,
|
|
363
|
+
`directory = "${normalizedAssetsDir}"`,
|
|
364
|
+
`binding = "ASSETS"`,
|
|
365
|
+
`run_worker_first = true`,
|
|
366
|
+
``,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Add Durable Objects for shared state
|
|
371
|
+
wranglerLines.push(
|
|
372
|
+
`[durable_objects]`,
|
|
373
|
+
`bindings = [`,
|
|
374
|
+
` { name = "SHARED_EXPORT", class_name = "SharedExportDO" }`,
|
|
375
|
+
`]`,
|
|
376
|
+
``,
|
|
377
|
+
`[[migrations]]`,
|
|
378
|
+
`tag = "v1"`,
|
|
379
|
+
`new_classes = ["SharedExportDO"]`,
|
|
380
|
+
``,
|
|
381
|
+
`[alias]`,
|
|
382
|
+
`"__USER_MODULE__" = "./.export-module-map.js"`,
|
|
383
|
+
`"__GENERATED_TYPES__" = "./.export-types.js"`,
|
|
384
|
+
`"__SHARED_MODULE__" = "./.export-shared.js"`,
|
|
385
|
+
``,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const wranglerPath = path.join(cwd, "wrangler.toml");
|
|
389
|
+
fs.writeFileSync(wranglerPath, wranglerLines.join("\n"));
|
|
390
|
+
|
|
391
|
+
// --- Output summary ---
|
|
301
392
|
|
|
393
|
+
console.log(`Discovered ${modules.length} module(s): ${modules.map(m => m.routePath || "/").join(", ")}`);
|
|
302
394
|
console.log("Generated type definitions + minified core →", outPath);
|
|
395
|
+
console.log("Generated module map →", moduleMapPath);
|
|
303
396
|
console.log("Generated shared import module →", sharedModulePath);
|
|
397
|
+
console.log("Generated wrangler.toml →", wranglerPath);
|
package/entry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import moduleMap from "__USER_MODULE__";
|
|
2
2
|
import generatedTypes, { minifiedCore, minifiedSharedCore, coreId } from "__GENERATED_TYPES__";
|
|
3
3
|
import { createHandler } from "./handler.js";
|
|
4
4
|
export { SharedExportDO } from "./shared-do.js";
|
|
5
5
|
|
|
6
|
-
export default createHandler(
|
|
6
|
+
export default createHandler(moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore);
|
package/handler.js
CHANGED
|
@@ -13,26 +13,64 @@ const jsResponse = (body, extra = {}) =>
|
|
|
13
13
|
const tsResponse = (body, status = 200) =>
|
|
14
14
|
new Response(body, { status, headers: { "Content-Type": TS, ...CORS, "Cache-Control": "no-cache" } });
|
|
15
15
|
|
|
16
|
-
export const createHandler = (
|
|
17
|
-
|
|
16
|
+
export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore) => {
|
|
17
|
+
// moduleMap: { routePath: moduleNamespace, ... }
|
|
18
|
+
const moduleRoutes = Object.keys(moduleMap); // e.g. ["", "greet", "utils/math"]
|
|
19
|
+
const moduleExportKeys = {};
|
|
20
|
+
for (const [route, mod] of Object.entries(moduleMap)) {
|
|
21
|
+
const keys = Object.keys(mod);
|
|
22
|
+
if (keys.includes("default")) {
|
|
23
|
+
const modulePath = route || "(root)";
|
|
24
|
+
console.warn(`[export-runtime] WARN: default export in "${modulePath}" is ignored. Use named exports instead.`);
|
|
25
|
+
}
|
|
26
|
+
moduleExportKeys[route] = keys.filter(k => k !== "default");
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
const coreModuleCode = minifiedCore || CORE_CODE;
|
|
20
30
|
const sharedCoreModuleCode = minifiedSharedCore || SHARED_CORE_CODE;
|
|
21
31
|
const corePath = `/${coreId || crypto.randomUUID()}.js`;
|
|
22
32
|
const sharedCorePath = corePath.replace(".js", "-shared.js");
|
|
23
33
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
// Resolve a URL pathname to { route, exportName } or null
|
|
35
|
+
const resolveRoute = (pathname) => {
|
|
36
|
+
const p = pathname === "/" ? "" : pathname.slice(1);
|
|
37
|
+
|
|
38
|
+
// Exact module match: /greet → route "greet", /utils/math → route "utils/math"
|
|
39
|
+
if (moduleRoutes.includes(p)) {
|
|
40
|
+
return { route: p, exportName: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try parent as module, last segment as export: /greet/foo → route "greet", export "foo"
|
|
44
|
+
const lastSlash = p.lastIndexOf("/");
|
|
45
|
+
if (lastSlash > 0) {
|
|
46
|
+
const parentRoute = p.slice(0, lastSlash);
|
|
47
|
+
const name = p.slice(lastSlash + 1);
|
|
48
|
+
if (moduleRoutes.includes(parentRoute) && moduleExportKeys[parentRoute]?.includes(name)) {
|
|
49
|
+
return { route: parentRoute, exportName: name };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Root module export: /greet → route "", export "greet" (only if no module named "greet")
|
|
54
|
+
if (moduleExportKeys[""]?.includes(p) && !p.includes("/")) {
|
|
55
|
+
return { route: "", exportName: p };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
28
60
|
|
|
29
|
-
const buildIndexModule = (cpath) =>
|
|
30
|
-
|
|
61
|
+
const buildIndexModule = (cpath, route) => {
|
|
62
|
+
const keys = moduleExportKeys[route] || [];
|
|
63
|
+
const namedExports = keys
|
|
64
|
+
.map((key) => `export const ${key} = createProxy([${JSON.stringify(route)}, ${JSON.stringify(key)}]);`)
|
|
65
|
+
.join("\n");
|
|
66
|
+
return `import { createProxy } from ".${cpath}";\n${namedExports}`;
|
|
67
|
+
};
|
|
31
68
|
|
|
32
|
-
const buildExportModule = (cpath, name) =>
|
|
33
|
-
`import { createProxy } from ".${cpath}";\
|
|
69
|
+
const buildExportModule = (cpath, route, name) =>
|
|
70
|
+
`import { createProxy } from ".${cpath}";\n` +
|
|
71
|
+
`const _export = createProxy([${JSON.stringify(route)}, ${JSON.stringify(name)}]);\n` +
|
|
72
|
+
`export default _export;\nexport { _export as ${name} };`;
|
|
34
73
|
|
|
35
|
-
// Dispatch a parsed devalue message to an RPC dispatcher
|
|
36
74
|
const dispatchMessage = async (dispatcher, msg) => {
|
|
37
75
|
const { type, path = [], args = [], instanceId, iteratorId, streamId } = msg;
|
|
38
76
|
switch (type) {
|
|
@@ -83,7 +121,7 @@ export const createHandler = (exports, generatedTypes, minifiedCore, coreId, min
|
|
|
83
121
|
const stub = env.SHARED_EXPORT.get(env.SHARED_EXPORT.idFromName(room));
|
|
84
122
|
wireWebSocket(server, stub);
|
|
85
123
|
} else {
|
|
86
|
-
const dispatcher = createRpcDispatcher(
|
|
124
|
+
const dispatcher = createRpcDispatcher(moduleMap);
|
|
87
125
|
wireWebSocket(server, dispatcher, () => dispatcher.clearAll());
|
|
88
126
|
}
|
|
89
127
|
|
|
@@ -93,41 +131,58 @@ export const createHandler = (exports, generatedTypes, minifiedCore, coreId, min
|
|
|
93
131
|
// --- HTTP routing ---
|
|
94
132
|
const pathname = url.pathname;
|
|
95
133
|
|
|
96
|
-
// Core modules
|
|
134
|
+
// Core modules
|
|
97
135
|
if (pathname === corePath) return jsResponse(coreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
98
136
|
if (pathname === sharedCorePath) return jsResponse(sharedCoreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
99
137
|
|
|
100
138
|
// Type definitions
|
|
101
139
|
if (url.searchParams.has("types")) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
const p = pathname === "/" ? "" : pathname.slice(1);
|
|
141
|
+
// Module types
|
|
142
|
+
if (generatedTypes?.[p] !== undefined) {
|
|
143
|
+
return tsResponse(generatedTypes[p]);
|
|
144
|
+
}
|
|
145
|
+
// Per-export re-export
|
|
146
|
+
const resolved = resolveRoute(pathname);
|
|
147
|
+
if (resolved?.exportName) {
|
|
148
|
+
const routeTypesPath = resolved.route ? `./${resolved.route}?types` : "./?types";
|
|
149
|
+
const code = `export { ${resolved.exportName} as default, ${resolved.exportName} } from "${routeTypesPath}";`;
|
|
150
|
+
return tsResponse(code);
|
|
151
|
+
}
|
|
152
|
+
return tsResponse("// Not found", 404);
|
|
153
|
+
}
|
|
154
|
+
if (pathname.endsWith(".d.ts")) {
|
|
155
|
+
return tsResponse(generatedTypes?.[""] || "");
|
|
107
156
|
}
|
|
108
|
-
if (pathname.endsWith(".d.ts")) return tsResponse(generatedTypes || "");
|
|
109
157
|
|
|
110
158
|
const baseUrl = `${url.protocol}//${url.host}`;
|
|
111
159
|
const cpath = isShared ? sharedCorePath : corePath;
|
|
112
160
|
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
161
|
+
const resolved = resolveRoute(pathname);
|
|
162
|
+
if (!resolved) {
|
|
163
|
+
// Fallback to static assets if ASSETS binding is available
|
|
164
|
+
if (env?.ASSETS) {
|
|
165
|
+
return env.ASSETS.fetch(request);
|
|
166
|
+
}
|
|
167
|
+
return new Response("Not found", { status: 404 });
|
|
119
168
|
}
|
|
120
169
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
170
|
+
const { route, exportName } = resolved;
|
|
171
|
+
|
|
172
|
+
if (exportName) {
|
|
173
|
+
// Per-export module
|
|
174
|
+
return jsResponse(buildExportModule(cpath, route, exportName), {
|
|
125
175
|
"Cache-Control": "no-cache",
|
|
126
|
-
"X-TypeScript-Types": `${baseUrl}
|
|
176
|
+
"X-TypeScript-Types": `${baseUrl}${pathname}?types`,
|
|
127
177
|
});
|
|
128
178
|
}
|
|
129
179
|
|
|
130
|
-
|
|
180
|
+
// Module index
|
|
181
|
+
const typesPath = route ? `${baseUrl}/${route}?types` : `${baseUrl}/?types`;
|
|
182
|
+
return jsResponse(buildIndexModule(cpath, route), {
|
|
183
|
+
"Cache-Control": "no-cache",
|
|
184
|
+
"X-TypeScript-Types": typesPath,
|
|
185
|
+
});
|
|
131
186
|
},
|
|
132
187
|
};
|
|
133
188
|
};
|
package/package.json
CHANGED
package/rpc.js
CHANGED
|
@@ -21,7 +21,19 @@ export const RPC_METHODS = [
|
|
|
21
21
|
"rpcIterateNext", "rpcIterateReturn", "rpcStreamRead", "rpcStreamCancel",
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// moduleMap: { routePath: moduleNamespace, ... } or a single namespace (backward compat)
|
|
25
|
+
export function createRpcDispatcher(moduleMap) {
|
|
26
|
+
// Normalize: if moduleMap has no "" key and looks like a namespace, wrap it
|
|
27
|
+
const isMap = typeof moduleMap === "object" && !moduleMap.__esModule && "" in moduleMap;
|
|
28
|
+
const resolveModule = (route) => {
|
|
29
|
+
if (isMap) {
|
|
30
|
+
const mod = moduleMap[route];
|
|
31
|
+
if (!mod) throw new Error(`Module not found: ${route}`);
|
|
32
|
+
return mod;
|
|
33
|
+
}
|
|
34
|
+
return moduleMap; // single namespace fallback
|
|
35
|
+
};
|
|
36
|
+
|
|
25
37
|
const instances = new Map();
|
|
26
38
|
const iterators = new Map();
|
|
27
39
|
const streams = new Map();
|
|
@@ -50,19 +62,31 @@ export function createRpcDispatcher(exports) {
|
|
|
50
62
|
return { type: "result", value: result };
|
|
51
63
|
};
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
// path = [route, ...exportPath] — route selects the module, exportPath walks its exports
|
|
66
|
+
const splitPath = (path) => {
|
|
67
|
+
const [route, ...rest] = path;
|
|
68
|
+
// Reject default export access
|
|
69
|
+
if (rest[0] === "default") throw new Error("Export not found: default");
|
|
70
|
+
return { exports: resolveModule(route), exportPath: rest };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const callTarget = async (obj, path, args, isRoot = false) => {
|
|
54
74
|
const target = getByPath(obj, path);
|
|
55
|
-
const thisArg = path.length > 1 ? getByPath(obj, path.slice(0, -1)) : (
|
|
75
|
+
const thisArg = path.length > 1 ? getByPath(obj, path.slice(0, -1)) : (isRoot ? undefined : obj);
|
|
56
76
|
if (typeof target !== "function") throw new Error(`${path.join(".")} is not a function`);
|
|
57
77
|
return wrapResult(await target.apply(thisArg, args), path);
|
|
58
78
|
};
|
|
59
79
|
|
|
60
80
|
return {
|
|
61
|
-
rpcCall
|
|
81
|
+
rpcCall(path, args = []) {
|
|
82
|
+
const { exports, exportPath } = splitPath(path);
|
|
83
|
+
return callTarget(exports, exportPath, args, true);
|
|
84
|
+
},
|
|
62
85
|
|
|
63
86
|
async rpcConstruct(path, args = []) {
|
|
64
|
-
const
|
|
65
|
-
|
|
87
|
+
const { exports, exportPath } = splitPath(path);
|
|
88
|
+
const Ctor = getByPath(exports, exportPath);
|
|
89
|
+
if (!isClass(Ctor)) throw new Error(`${exportPath.join(".")} is not a class`);
|
|
66
90
|
const id = nextId++;
|
|
67
91
|
instances.set(id, new Ctor(...args));
|
|
68
92
|
return { type: "result", instanceId: id, valueType: "instance" };
|
package/shared-do.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { DurableObject } from "cloudflare:workers";
|
|
2
|
-
import
|
|
2
|
+
import moduleMap from "__USER_MODULE__";
|
|
3
3
|
import { createRpcDispatcher } from "./rpc.js";
|
|
4
4
|
|
|
5
5
|
export class SharedExportDO extends DurableObject {
|
|
6
6
|
#d;
|
|
7
7
|
constructor(ctx, env) {
|
|
8
8
|
super(ctx, env);
|
|
9
|
-
this.#d = createRpcDispatcher(
|
|
9
|
+
this.#d = createRpcDispatcher(moduleMap);
|
|
10
10
|
}
|
|
11
11
|
rpcCall(p, a) { return this.#d.rpcCall(p, a); }
|
|
12
12
|
rpcConstruct(p, a) { return this.#d.rpcConstruct(p, a); }
|