export-runtime 0.0.8 → 0.0.9

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.
@@ -9,7 +9,7 @@ import { fileURLToPath } from "url";
9
9
 
10
10
  const cwd = process.cwd();
11
11
 
12
- // Read wrangler.toml to find user module path
12
+ // Read wrangler.toml to find source root
13
13
  const wranglerPath = path.join(cwd, "wrangler.toml");
14
14
  if (!fs.existsSync(wranglerPath)) {
15
15
  console.error("wrangler.toml not found in", cwd);
@@ -22,17 +22,41 @@ if (!aliasMatch) {
22
22
  process.exit(1);
23
23
  }
24
24
 
25
- const userModulePath = path.resolve(cwd, aliasMatch[1]);
26
- if (!fs.existsSync(userModulePath)) {
27
- console.error("User module not found:", userModulePath);
28
- process.exit(1);
25
+ // Derive source root directory from the alias (e.g., "./src/index.ts" → "./src")
26
+ const aliasTarget = aliasMatch[1];
27
+ // If alias points to the module map, resolve src dir from it; otherwise derive from file
28
+ const srcDir = aliasTarget.includes(".export-module-map")
29
+ ? path.resolve(cwd, "src")
30
+ : path.resolve(cwd, path.dirname(aliasTarget.replace(/^\.\//, "")));
31
+
32
+ // --- Discover all source files under srcDir ---
33
+
34
+ const EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
35
+
36
+ function discoverModules(dir, base = "") {
37
+ const modules = [];
38
+ if (!fs.existsSync(dir)) return modules;
39
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
40
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
41
+ const fullPath = path.join(dir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ modules.push(...discoverModules(fullPath, base ? `${base}/${entry.name}` : entry.name));
44
+ } else if (EXTENSIONS.includes(path.extname(entry.name))) {
45
+ const nameWithoutExt = entry.name.replace(/\.(ts|tsx|js|jsx)$/, "");
46
+ const routePath = nameWithoutExt === "index"
47
+ ? base // index.ts → directory path ("" for root)
48
+ : (base ? `${base}/${nameWithoutExt}` : nameWithoutExt);
49
+ modules.push({ routePath, filePath: fullPath });
50
+ }
51
+ }
52
+ return modules;
29
53
  }
30
54
 
31
- const source = fs.readFileSync(userModulePath, "utf8");
32
- const isTS = userModulePath.endsWith(".ts") || userModulePath.endsWith(".tsx");
33
- const fileName = path.basename(userModulePath);
34
- const result = parseSync(fileName, source, { sourceType: "module" });
35
- const program = result.program;
55
+ const modules = discoverModules(srcDir);
56
+ if (modules.length === 0) {
57
+ console.error("No source files found in", srcDir);
58
+ process.exit(1);
59
+ }
36
60
 
37
61
  // --- Type extraction helpers ---
38
62
 
@@ -52,14 +76,10 @@ function extractType(node) {
52
76
  case "TSBigIntKeyword": return "bigint";
53
77
  case "TSSymbolKeyword": return "symbol";
54
78
  case "TSObjectKeyword": return "object";
55
- case "TSArrayType":
56
- return `${extractType(ta.elementType)}[]`;
57
- case "TSTupleType":
58
- return `[${(ta.elementTypes || []).map(e => extractType(e)).join(", ")}]`;
59
- case "TSUnionType":
60
- return ta.types.map(t => extractType(t)).join(" | ");
61
- case "TSIntersectionType":
62
- return ta.types.map(t => extractType(t)).join(" & ");
79
+ case "TSArrayType": return `${extractType(ta.elementType)}[]`;
80
+ case "TSTupleType": return `[${(ta.elementTypes || []).map(e => extractType(e)).join(", ")}]`;
81
+ case "TSUnionType": return ta.types.map(t => extractType(t)).join(" | ");
82
+ case "TSIntersectionType": return ta.types.map(t => extractType(t)).join(" & ");
63
83
  case "TSLiteralType": {
64
84
  const lit = ta.literal;
65
85
  if (lit.type === "StringLiteral") return JSON.stringify(lit.value);
@@ -94,10 +114,8 @@ function extractType(node) {
94
114
  }).filter(Boolean);
95
115
  return `{ ${members.join("; ")} }`;
96
116
  }
97
- case "TSTypeAnnotation":
98
- return extractType(ta.typeAnnotation);
99
- default:
100
- return "any";
117
+ case "TSTypeAnnotation": return extractType(ta.typeAnnotation);
118
+ default: return "any";
101
119
  }
102
120
  }
103
121
 
@@ -116,105 +134,110 @@ function extractParams(params) {
116
134
  });
117
135
  }
118
136
 
119
- // Wrap return type: all functions become async over the network
120
137
  function wrapReturnType(returnType, isAsync, isGenerator) {
121
138
  if (isGenerator) {
122
- // async generator → AsyncIterable<YieldType>
123
- // extract inner type from AsyncGenerator<T> if present
124
139
  if (returnType.startsWith("AsyncGenerator")) {
125
140
  const inner = returnType.match(/^AsyncGenerator<(.+?)(?:,.*)?>/);
126
141
  return `Promise<AsyncIterable<${inner ? inner[1] : "any"}>>`;
127
142
  }
128
143
  return `Promise<AsyncIterable<${returnType === "any" ? "any" : returnType}>>`;
129
144
  }
130
- // Already Promise<T> → keep as-is
131
145
  if (returnType.startsWith("Promise<")) return returnType;
132
- // ReadableStream<T> → Promise<ReadableStream<T>>
133
146
  if (returnType.startsWith("ReadableStream")) return `Promise<${returnType}>`;
134
- // Wrap in Promise
135
147
  return `Promise<${returnType}>`;
136
148
  }
137
149
 
138
- // --- Generate .d.ts ---
150
+ // --- Extract types and export names from a single file ---
139
151
 
140
- const lines = [
141
- "// Auto-generated type definitions (oxc-parser)",
142
- "// All functions are async over the network",
143
- "",
144
- ];
152
+ function extractFileTypes(filePath) {
153
+ const source = fs.readFileSync(filePath, "utf8");
154
+ const fileName = path.basename(filePath);
155
+ const result = parseSync(fileName, source, { sourceType: "module" });
156
+ const program = result.program;
145
157
 
146
- for (const node of program.body) {
147
- if (node.type !== "ExportNamedDeclaration" || !node.declaration) continue;
148
- const decl = node.declaration;
149
-
150
- if (decl.type === "FunctionDeclaration") {
151
- const name = decl.id.name;
152
- const params = extractParams(decl.params);
153
- const rawRet = decl.returnType ? extractType(decl.returnType) : "any";
154
- const ret = wrapReturnType(rawRet, decl.async, decl.generator);
155
- lines.push(`export declare function ${name}(${params.join(", ")}): ${ret};`);
156
- lines.push("");
157
-
158
- } else if (decl.type === "ClassDeclaration") {
159
- const name = decl.id.name;
160
- lines.push(`export declare class ${name} {`);
161
- for (const member of decl.body.body) {
162
- if (member.type === "MethodDefinition") {
163
- const mName = member.key.name || member.key.value;
164
- if (member.kind === "constructor") {
165
- const params = extractParams(member.value.params);
166
- lines.push(` constructor(${params.join(", ")});`);
167
- } else {
168
- const params = extractParams(member.value.params);
169
- const rawRet = member.value.returnType ? extractType(member.value.returnType) : "any";
170
- const ret = wrapReturnType(rawRet, member.value.async, member.value.generator);
171
- lines.push(` ${mName}(${params.join(", ")}): ${ret};`);
158
+ const lines = [];
159
+ const exportNames = [];
160
+
161
+ for (const node of program.body) {
162
+ if (node.type !== "ExportNamedDeclaration" || !node.declaration) continue;
163
+ const decl = node.declaration;
164
+
165
+ if (decl.type === "FunctionDeclaration") {
166
+ const name = decl.id.name;
167
+ exportNames.push(name);
168
+ const params = extractParams(decl.params);
169
+ const rawRet = decl.returnType ? extractType(decl.returnType) : "any";
170
+ const ret = wrapReturnType(rawRet, decl.async, decl.generator);
171
+ lines.push(`export declare function ${name}(${params.join(", ")}): ${ret};`);
172
+ lines.push("");
173
+ } else if (decl.type === "ClassDeclaration") {
174
+ const name = decl.id.name;
175
+ exportNames.push(name);
176
+ lines.push(`export declare class ${name} {`);
177
+ for (const member of decl.body.body) {
178
+ if (member.type === "MethodDefinition") {
179
+ const mName = member.key.name || member.key.value;
180
+ if (member.kind === "constructor") {
181
+ lines.push(` constructor(${extractParams(member.value.params).join(", ")});`);
182
+ } else {
183
+ const params = extractParams(member.value.params);
184
+ const rawRet = member.value.returnType ? extractType(member.value.returnType) : "any";
185
+ const ret = wrapReturnType(rawRet, member.value.async, member.value.generator);
186
+ lines.push(` ${mName}(${params.join(", ")}): ${ret};`);
187
+ }
188
+ } else if (member.type === "PropertyDefinition") {
189
+ if (member.accessibility === "private") continue;
190
+ const mName = member.key.name;
191
+ const type = member.typeAnnotation ? extractType(member.typeAnnotation) : "any";
192
+ lines.push(` ${mName}: ${type};`);
172
193
  }
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
194
  }
180
- }
181
- lines.push(` [Symbol.dispose](): Promise<void>;`);
182
- lines.push(` "[release]"(): Promise<void>;`);
183
- lines.push(`}`);
184
- lines.push("");
185
-
186
- } else if (decl.type === "VariableDeclaration") {
187
- for (const d of decl.declarations) {
188
- const name = d.id.name;
189
- if (d.init?.type === "ObjectExpression") {
190
- lines.push(`export declare const ${name}: {`);
191
- for (const prop of d.init.properties) {
192
- if (prop.type === "SpreadElement") continue;
193
- const key = prop.key?.name || prop.key?.value;
194
- if (prop.value?.type === "FunctionExpression" || prop.value?.type === "ArrowFunctionExpression") {
195
- const params = extractParams(prop.value.params);
196
- const rawRet = prop.value.returnType ? extractType(prop.value.returnType) : "any";
197
- const ret = wrapReturnType(rawRet, prop.value.async, prop.value.generator);
198
- lines.push(` ${key}(${params.join(", ")}): ${ret};`);
199
- } else {
200
- const type = d.id.typeAnnotation ? "any" : "any";
201
- lines.push(` ${key}: any;`);
195
+ lines.push(` [Symbol.dispose](): Promise<void>;`);
196
+ lines.push(` "[release]"(): Promise<void>;`);
197
+ lines.push(`}`);
198
+ lines.push("");
199
+ } else if (decl.type === "VariableDeclaration") {
200
+ for (const d of decl.declarations) {
201
+ const name = d.id.name;
202
+ exportNames.push(name);
203
+ if (d.init?.type === "ObjectExpression") {
204
+ lines.push(`export declare const ${name}: {`);
205
+ for (const prop of d.init.properties) {
206
+ if (prop.type === "SpreadElement") continue;
207
+ const key = prop.key?.name || prop.key?.value;
208
+ if (prop.value?.type === "FunctionExpression" || prop.value?.type === "ArrowFunctionExpression") {
209
+ const params = extractParams(prop.value.params);
210
+ const rawRet = prop.value.returnType ? extractType(prop.value.returnType) : "any";
211
+ const ret = wrapReturnType(rawRet, prop.value.async, prop.value.generator);
212
+ lines.push(` ${key}(${params.join(", ")}): ${ret};`);
213
+ } else {
214
+ lines.push(` ${key}: any;`);
215
+ }
202
216
  }
217
+ lines.push(`};`);
218
+ lines.push("");
219
+ } else {
220
+ const type = d.id.typeAnnotation ? extractType(d.id.typeAnnotation) : "any";
221
+ lines.push(`export declare const ${name}: ${type};`);
222
+ lines.push("");
203
223
  }
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
224
  }
211
225
  }
212
226
  }
227
+
228
+ return { types: lines.join("\n"), exportNames };
213
229
  }
214
230
 
231
+ // --- Process all modules ---
215
232
 
233
+ const typesMap = {}; // routePath → type definition string
234
+ const exportsMap = {}; // routePath → export names array
216
235
 
217
- const typeDefinitions = lines.join("\n");
236
+ for (const mod of modules) {
237
+ const { types, exportNames } = extractFileTypes(mod.filePath);
238
+ typesMap[mod.routePath] = types;
239
+ exportsMap[mod.routePath] = exportNames;
240
+ }
218
241
 
219
242
  // --- Minify core modules ---
220
243
 
@@ -222,42 +245,39 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
222
245
  const { CORE_CODE, SHARED_CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
223
246
 
224
247
  const minified = minifySync("_core.js", CORE_CODE);
225
- if (minified.errors?.length) {
226
- console.error("Minification errors (core):", minified.errors);
227
- }
248
+ if (minified.errors?.length) console.error("Minification errors (core):", minified.errors);
228
249
 
229
250
  const minifiedShared = minifySync("_core-shared.js", SHARED_CORE_CODE);
230
- if (minifiedShared.errors?.length) {
231
- console.error("Minification errors (shared core):", minifiedShared.errors);
232
- }
251
+ if (minifiedShared.errors?.length) console.error("Minification errors (shared core):", minifiedShared.errors);
233
252
 
234
- // Generate a unique ID per build for cache-busting the core module path
235
253
  const coreId = crypto.randomUUID();
236
254
 
237
- // Write as a JS module
255
+ // --- Write .export-types.js ---
256
+
238
257
  const outPath = path.join(cwd, ".export-types.js");
239
258
  fs.writeFileSync(outPath, [
240
- `export default ${JSON.stringify(typeDefinitions)};`,
259
+ `export default ${JSON.stringify(typesMap)};`,
241
260
  `export const minifiedCore = ${JSON.stringify(minified.code)};`,
242
261
  `export const minifiedSharedCore = ${JSON.stringify(minifiedShared.code)};`,
243
262
  `export const coreId = ${JSON.stringify(coreId)};`,
244
263
  ].join("\n") + "\n");
245
264
 
246
- // Generate Worker-side shared import module (.export-shared.js)
247
- const exportNames = [];
248
- for (const node of program.body) {
249
- if (node.type !== "ExportNamedDeclaration" || !node.declaration) continue;
250
- const decl = node.declaration;
251
- if (decl.id?.name) exportNames.push(decl.id.name);
252
- else if (decl.declarations) {
253
- for (const d of decl.declarations) {
254
- if (d.id?.name) exportNames.push(d.id.name);
255
- }
256
- }
257
- }
265
+ // --- Write .export-module-map.js ---
266
+
267
+ const moduleMapPath = path.join(cwd, ".export-module-map.js");
268
+ const relSrcDir = path.relative(cwd, srcDir);
269
+ const mapLines = [];
270
+ modules.forEach((mod, i) => {
271
+ const relFile = "./" + path.relative(cwd, mod.filePath).replace(/\\/g, "/");
272
+ mapLines.push(`import * as _${i} from ${JSON.stringify(relFile)};`);
273
+ });
274
+ mapLines.push(`export default { ${modules.map((mod, i) => `${JSON.stringify(mod.routePath)}: _${i}`).join(", ")} };`);
275
+ fs.writeFileSync(moduleMapPath, mapLines.join("\n") + "\n");
276
+
277
+ // --- Write .export-shared.js ---
258
278
 
259
279
  const sharedModulePath = path.join(cwd, ".export-shared.js");
260
- const sharedModuleLines = [
280
+ const sharedLines = [
261
281
  `import { env } from "cloudflare:workers";`,
262
282
  ``,
263
283
  `const getStub = (room = "default") =>`,
@@ -294,10 +314,20 @@ const sharedModuleLines = [
294
314
  ` });`,
295
315
  ``,
296
316
  `const _stub = getStub();`,
297
- ...exportNames.map(n => `export const ${n} = createSharedProxy(_stub, [${JSON.stringify(n)}]);`),
298
- `export { getStub };`,
299
317
  ];
300
- fs.writeFileSync(sharedModulePath, sharedModuleLines.join("\n") + "\n");
318
+ // Generate proxies for all modules' exports, prefixed with route
319
+ for (const mod of modules) {
320
+ const names = exportsMap[mod.routePath] || [];
321
+ for (const n of names) {
322
+ const proxyPath = mod.routePath ? `[${JSON.stringify(mod.routePath)}, ${JSON.stringify(n)}]` : `[${JSON.stringify("")}, ${JSON.stringify(n)}]`;
323
+ const exportAlias = mod.routePath ? `${mod.routePath.replace(/\//g, "_")}_${n}` : n;
324
+ sharedLines.push(`export const ${exportAlias} = createSharedProxy(_stub, ${proxyPath});`);
325
+ }
326
+ }
327
+ sharedLines.push(`export { getStub };`);
328
+ fs.writeFileSync(sharedModulePath, sharedLines.join("\n") + "\n");
301
329
 
330
+ console.log(`Discovered ${modules.length} module(s): ${modules.map(m => m.routePath || "/").join(", ")}`);
302
331
  console.log("Generated type definitions + minified core →", outPath);
332
+ console.log("Generated module map →", moduleMapPath);
303
333
  console.log("Generated shared import module →", sharedModulePath);
package/entry.js CHANGED
@@ -1,6 +1,6 @@
1
- import * as userExports from "__USER_MODULE__";
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(userExports, generatedTypes, minifiedCore, coreId, minifiedSharedCore);
6
+ export default createHandler(moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore);
package/handler.js CHANGED
@@ -13,26 +13,59 @@ 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 = (exports, generatedTypes, minifiedCore, coreId, minifiedSharedCore) => {
17
- const exportKeys = Object.keys(exports);
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
+ moduleExportKeys[route] = Object.keys(mod);
22
+ }
18
23
 
19
24
  const coreModuleCode = minifiedCore || CORE_CODE;
20
25
  const sharedCoreModuleCode = minifiedSharedCore || SHARED_CORE_CODE;
21
26
  const corePath = `/${coreId || crypto.randomUUID()}.js`;
22
27
  const sharedCorePath = corePath.replace(".js", "-shared.js");
23
28
 
24
- // Pre-generate the named exports string (same for shared and normal, only import source differs)
25
- const namedExportsCode = exportKeys
26
- .map((key) => `export const ${key} = createProxy([${JSON.stringify(key)}]);`)
27
- .join("\n");
29
+ // Resolve a URL pathname to { route, exportName } or null
30
+ const resolveRoute = (pathname) => {
31
+ const p = pathname === "/" ? "" : pathname.slice(1);
28
32
 
29
- const buildIndexModule = (cpath) =>
30
- `import { createProxy } from ".${cpath}";\n${namedExportsCode}`;
33
+ // Exact module match: /greet → route "greet", /utils/math → route "utils/math"
34
+ if (moduleRoutes.includes(p)) {
35
+ return { route: p, exportName: null };
36
+ }
31
37
 
32
- const buildExportModule = (cpath, name) =>
33
- `import { createProxy } from ".${cpath}";\nconst _export = createProxy([${JSON.stringify(name)}]);\nexport default _export;\nexport { _export as ${name} };`;
38
+ // Try parent as module, last segment as export: /greet/foo → route "greet", export "foo"
39
+ const lastSlash = p.lastIndexOf("/");
40
+ if (lastSlash > 0) {
41
+ const parentRoute = p.slice(0, lastSlash);
42
+ const name = p.slice(lastSlash + 1);
43
+ if (moduleRoutes.includes(parentRoute) && moduleExportKeys[parentRoute]?.includes(name)) {
44
+ return { route: parentRoute, exportName: name };
45
+ }
46
+ }
47
+
48
+ // Root module export: /greet → route "", export "greet" (only if no module named "greet")
49
+ if (moduleExportKeys[""]?.includes(p) && !p.includes("/")) {
50
+ return { route: "", exportName: p };
51
+ }
52
+
53
+ return null;
54
+ };
55
+
56
+ const buildIndexModule = (cpath, route) => {
57
+ const keys = moduleExportKeys[route] || [];
58
+ const namedExports = keys
59
+ .map((key) => `export const ${key} = createProxy([${JSON.stringify(route)}, ${JSON.stringify(key)}]);`)
60
+ .join("\n");
61
+ return `import { createProxy } from ".${cpath}";\n${namedExports}`;
62
+ };
63
+
64
+ const buildExportModule = (cpath, route, name) =>
65
+ `import { createProxy } from ".${cpath}";\n` +
66
+ `const _export = createProxy([${JSON.stringify(route)}, ${JSON.stringify(name)}]);\n` +
67
+ `export default _export;\nexport { _export as ${name} };`;
34
68
 
35
- // Dispatch a parsed devalue message to an RPC dispatcher
36
69
  const dispatchMessage = async (dispatcher, msg) => {
37
70
  const { type, path = [], args = [], instanceId, iteratorId, streamId } = msg;
38
71
  switch (type) {
@@ -83,7 +116,7 @@ export const createHandler = (exports, generatedTypes, minifiedCore, coreId, min
83
116
  const stub = env.SHARED_EXPORT.get(env.SHARED_EXPORT.idFromName(room));
84
117
  wireWebSocket(server, stub);
85
118
  } else {
86
- const dispatcher = createRpcDispatcher(exports);
119
+ const dispatcher = createRpcDispatcher(moduleMap);
87
120
  wireWebSocket(server, dispatcher, () => dispatcher.clearAll());
88
121
  }
89
122
 
@@ -93,41 +126,52 @@ export const createHandler = (exports, generatedTypes, minifiedCore, coreId, min
93
126
  // --- HTTP routing ---
94
127
  const pathname = url.pathname;
95
128
 
96
- // Core modules (cached immutably)
129
+ // Core modules
97
130
  if (pathname === corePath) return jsResponse(coreModuleCode, { "Cache-Control": IMMUTABLE });
98
131
  if (pathname === sharedCorePath) return jsResponse(sharedCoreModuleCode, { "Cache-Control": IMMUTABLE });
99
132
 
100
133
  // Type definitions
101
134
  if (url.searchParams.has("types")) {
102
- if (pathname === "/") return tsResponse(generatedTypes || "");
103
- const name = pathname.slice(1);
104
- return exportKeys.includes(name)
105
- ? tsResponse(`export { ${name} as default, ${name} } from "./?types";`)
106
- : tsResponse("// Export not found", 404);
135
+ const p = pathname === "/" ? "" : pathname.slice(1);
136
+ // Module types
137
+ if (generatedTypes?.[p] !== undefined) {
138
+ return tsResponse(generatedTypes[p]);
139
+ }
140
+ // Per-export re-export
141
+ const resolved = resolveRoute(pathname);
142
+ if (resolved?.exportName) {
143
+ const routeTypesPath = resolved.route ? `./${resolved.route}?types` : "./?types";
144
+ const code = `export { ${resolved.exportName} as default, ${resolved.exportName} } from "${routeTypesPath}";`;
145
+ return tsResponse(code);
146
+ }
147
+ return tsResponse("// Not found", 404);
148
+ }
149
+ if (pathname.endsWith(".d.ts")) {
150
+ return tsResponse(generatedTypes?.[""] || "");
107
151
  }
108
- if (pathname.endsWith(".d.ts")) return tsResponse(generatedTypes || "");
109
152
 
110
153
  const baseUrl = `${url.protocol}//${url.host}`;
111
154
  const cpath = isShared ? sharedCorePath : corePath;
112
155
 
113
- // Root module
114
- if (pathname === "/") {
115
- return jsResponse(buildIndexModule(cpath), {
116
- "Cache-Control": "no-cache",
117
- "X-TypeScript-Types": `${baseUrl}/?types`,
118
- });
119
- }
156
+ const resolved = resolveRoute(pathname);
157
+ if (!resolved) return new Response("Not found", { status: 404 });
158
+
159
+ const { route, exportName } = resolved;
120
160
 
121
- // Per-export module
122
- const exportName = pathname.slice(1);
123
- if (exportKeys.includes(exportName)) {
124
- return jsResponse(buildExportModule(cpath, exportName), {
161
+ if (exportName) {
162
+ // Per-export module
163
+ return jsResponse(buildExportModule(cpath, route, exportName), {
125
164
  "Cache-Control": "no-cache",
126
- "X-TypeScript-Types": `${baseUrl}/${exportName}?types`,
165
+ "X-TypeScript-Types": `${baseUrl}${pathname}?types`,
127
166
  });
128
167
  }
129
168
 
130
- return new Response("Not found", { status: 404 });
169
+ // Module index
170
+ const typesPath = route ? `${baseUrl}/${route}?types` : `${baseUrl}/?types`;
171
+ return jsResponse(buildIndexModule(cpath, route), {
172
+ "Cache-Control": "no-cache",
173
+ "X-TypeScript-Types": typesPath,
174
+ });
131
175
  },
132
176
  };
133
177
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "export-runtime",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Cloudflare Workers ESM Export Framework Runtime",
5
5
  "keywords": [
6
6
  "cloudflare",
package/rpc.js CHANGED
@@ -21,7 +21,19 @@ export const RPC_METHODS = [
21
21
  "rpcIterateNext", "rpcIterateReturn", "rpcStreamRead", "rpcStreamCancel",
22
22
  ];
23
23
 
24
- export function createRpcDispatcher(exports) {
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,29 @@ export function createRpcDispatcher(exports) {
50
62
  return { type: "result", value: result };
51
63
  };
52
64
 
53
- const callTarget = async (obj, path, args) => {
65
+ // path = [route, ...exportPath] — route selects the module, exportPath walks its exports
66
+ const splitPath = (path) => {
67
+ const [route, ...rest] = path;
68
+ return { exports: resolveModule(route), exportPath: rest };
69
+ };
70
+
71
+ const callTarget = async (obj, path, args, isRoot = false) => {
54
72
  const target = getByPath(obj, path);
55
- const thisArg = path.length > 1 ? getByPath(obj, path.slice(0, -1)) : (obj === exports ? undefined : obj);
73
+ const thisArg = path.length > 1 ? getByPath(obj, path.slice(0, -1)) : (isRoot ? undefined : obj);
56
74
  if (typeof target !== "function") throw new Error(`${path.join(".")} is not a function`);
57
75
  return wrapResult(await target.apply(thisArg, args), path);
58
76
  };
59
77
 
60
78
  return {
61
- rpcCall: (path, args = []) => callTarget(exports, path, args),
79
+ rpcCall(path, args = []) {
80
+ const { exports, exportPath } = splitPath(path);
81
+ return callTarget(exports, exportPath, args, true);
82
+ },
62
83
 
63
84
  async rpcConstruct(path, args = []) {
64
- const Ctor = getByPath(exports, path);
65
- if (!isClass(Ctor)) throw new Error(`${path.join(".")} is not a class`);
85
+ const { exports, exportPath } = splitPath(path);
86
+ const Ctor = getByPath(exports, exportPath);
87
+ if (!isClass(Ctor)) throw new Error(`${exportPath.join(".")} is not a class`);
66
88
  const id = nextId++;
67
89
  instances.set(id, new Ctor(...args));
68
90
  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 * as userExports from "__USER_MODULE__";
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(userExports);
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); }