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.
@@ -9,30 +9,72 @@ import { fileURLToPath } from "url";
9
9
 
10
10
  const cwd = process.cwd();
11
11
 
12
- // Read wrangler.toml to find user module path
13
- const wranglerPath = path.join(cwd, "wrangler.toml");
14
- if (!fs.existsSync(wranglerPath)) {
15
- console.error("wrangler.toml not found in", cwd);
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
- const wranglerContent = fs.readFileSync(wranglerPath, "utf8");
19
- const aliasMatch = wranglerContent.match(/"__USER_MODULE__"\s*=\s*"([^"]+)"/);
20
- if (!aliasMatch) {
21
- console.error('Could not find __USER_MODULE__ alias in wrangler.toml');
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
- const userModulePath = path.resolve(cwd, aliasMatch[1]);
26
- if (!fs.existsSync(userModulePath)) {
27
- console.error("User module not found:", userModulePath);
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
- 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;
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
- 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(" & ");
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
- return extractType(ta.typeAnnotation);
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
- // --- Generate .d.ts ---
168
+ // --- Extract types and export names from a single file ---
139
169
 
140
- const lines = [
141
- "// Auto-generated type definitions (oxc-parser)",
142
- "// All functions are async over the network",
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
- 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};`);
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
- 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;`);
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 typeDefinitions = lines.join("\n");
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 as a JS module
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(typeDefinitions)};`,
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
- // 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
- }
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 sharedModuleLines = [
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
- fs.writeFileSync(sharedModulePath, sharedModuleLines.join("\n") + "\n");
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 * 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,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 = (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
+ 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
- // 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");
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
- `import { createProxy } from ".${cpath}";\n${namedExportsCode}`;
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}";\nconst _export = createProxy([${JSON.stringify(name)}]);\nexport default _export;\nexport { _export as ${name} };`;
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(exports);
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 (cached immutably)
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
- 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);
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
- // Root module
114
- if (pathname === "/") {
115
- return jsResponse(buildIndexModule(cpath), {
116
- "Cache-Control": "no-cache",
117
- "X-TypeScript-Types": `${baseUrl}/?types`,
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
- // Per-export module
122
- const exportName = pathname.slice(1);
123
- if (exportKeys.includes(exportName)) {
124
- return jsResponse(buildExportModule(cpath, exportName), {
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}/${exportName}?types`,
176
+ "X-TypeScript-Types": `${baseUrl}${pathname}?types`,
127
177
  });
128
178
  }
129
179
 
130
- return new Response("Not found", { status: 404 });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "export-runtime",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
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,31 @@ 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
+ // 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)) : (obj === exports ? undefined : obj);
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: (path, args = []) => callTarget(exports, path, args),
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 Ctor = getByPath(exports, path);
65
- if (!isClass(Ctor)) throw new Error(`${path.join(".")} is not a class`);
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 * 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); }