env-runner 0.1.1 → 0.1.3

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.
@@ -1,25 +1,256 @@
1
1
  import { t as BaseEnvRunner } from "./base-runner.mjs";
2
+ import { createRequire } from "node:module";
3
+ import { proxyUpgrade } from "httpxy";
2
4
  import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, resolve } from "node:path";
7
+ //#region src/runners/miniflare/wrapper.ts
8
+ const IPC_PATH$1 = "/__env_runner_ipc";
9
+ /** Service binding name used for cross-request IPC (worker → runner). */
10
+ const IPC_BINDING = "__ENV_RUNNER_IPC";
11
+ /**
12
+ * Generates a wrapper module that imports the user entry and adds IPC glue.
13
+ *
14
+ * The user module is expected to export `fetch` and optionally `ipc`.
15
+ * The wrapper uses a persistent WebSocket pair for bidirectional IPC:
16
+ * - Init: `fetch` with `upgrade: websocket` creates a WebSocketPair
17
+ * - Messages: JSON over the WebSocket (no per-message `dispatchFetch`)
18
+ * - Reload: `{ type: "reload" }` triggers cache-busted re-import
19
+ * - Shutdown: `{ type: "shutdown" }` calls `ipc.onClose()`
20
+ *
21
+ * For outgoing messages during fetch request handling, uses a service binding
22
+ * (`__ENV_RUNNER_IPC`) to avoid workerd's cross-request I/O restriction on
23
+ * the WebSocket object.
24
+ *
25
+ * Passed as an in-memory `script` to Miniflare (no temp files needed).
26
+ */
27
+ function generateWrapper(entryPath, opts) {
28
+ const staticReExport = opts?.dynamicOnly ? "" : `export * from ${JSON.stringify(entryPath)};`;
29
+ const explicitExports = opts?.dynamicOnly && opts.exports?.length ? opts.exports.map((name) => `export { ${name} } from ${JSON.stringify(entryPath)};`).join("\n") : "";
30
+ const fetchBody = opts?.captureErrors ?? true ? `try {
31
+ return await entryFetch(request, env, ctx);
32
+ } catch (e) {
33
+ const error = e instanceof Error ? e : new Error(String(e));
34
+ const body = JSON.stringify({
35
+ error: error.message,
36
+ stack: error.stack,
37
+ name: error.constructor?.name || "Error",
38
+ });
39
+ return new Response(body, {
40
+ status: 500,
41
+ headers: { "Content-Type": "application/json", "X-Env-Runner-Error": "1" },
42
+ });
43
+ }` : `return entryFetch(request, env, ctx);`;
44
+ return `import __process from "node:process";
45
+ if (!globalThis.process) { globalThis.process = __process; }
46
+ ${staticReExport}
47
+ ${explicitExports}
48
+
49
+ const __IPC_PATH = "${IPC_PATH$1}";
50
+ const __IPC_BINDING = "${IPC_BINDING}";
51
+ const __entryPath = ${JSON.stringify(entryPath)};
52
+ let __userEntry;
53
+ let __ipcInitialized = false;
54
+ let __serverWs;
55
+ let __currentEnv;
56
+
57
+ async function __loadEntry(env, path) {
58
+ globalThis.__ENV_RUNNER_UNSAFE_EVAL__ = env.__ENV_RUNNER_UNSAFE_EVAL__;
59
+ const importFn = env.__ENV_RUNNER_UNSAFE_EVAL__.newAsyncFunction(
60
+ "return await import(path)",
61
+ "loadEntry",
62
+ "path"
63
+ );
64
+ const mod = await importFn(path);
65
+ return mod.default || mod;
66
+ }
67
+
68
+ function __sendMessage(message) {
69
+ const payload = JSON.stringify(message);
70
+ const env = __currentEnv;
71
+ if (env && env[__IPC_BINDING]) {
72
+ env[__IPC_BINDING].fetch("http://localhost/__ipc", {
73
+ method: "POST",
74
+ body: payload,
75
+ }).catch(() => {});
76
+ return;
77
+ }
78
+ if (__serverWs) {
79
+ __serverWs.send(payload);
80
+ }
81
+ }
82
+
83
+ async function __handleWsMessage(env, data) {
84
+ let msg;
85
+ try { msg = JSON.parse(data); } catch { return; }
86
+
87
+ if (msg.type === "message") {
88
+ if (__userEntry?.ipc?.onMessage) {
89
+ __userEntry.ipc.onMessage(msg.data);
90
+ }
91
+ return;
92
+ }
93
+
94
+ if (msg.type === "reload" && env.__ENV_RUNNER_UNSAFE_EVAL__) {
95
+ const version = msg.version || 0;
96
+ try {
97
+ const newEntry = await __loadEntry(env, __entryPath + "?t=" + version);
98
+ if (__userEntry?.ipc?.onClose) {
99
+ await __userEntry.ipc.onClose();
100
+ }
101
+ __userEntry = newEntry;
102
+ __crosswsAdapter = undefined;
103
+ __ipcInitialized = false;
104
+ if (__userEntry.ipc?.onOpen) {
105
+ __ipcInitialized = true;
106
+ await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
107
+ }
108
+ __sendMessage({ event: "module-reloaded" });
109
+ } catch (e) {
110
+ __sendMessage({ event: "module-reloaded", error: String(e) });
111
+ }
112
+ return;
113
+ }
114
+
115
+ if (msg.type === "shutdown") {
116
+ if (__userEntry?.ipc?.onClose) {
117
+ await __userEntry.ipc.onClose();
118
+ }
119
+ return;
120
+ }
121
+ }
122
+
123
+ let __crosswsAdapter;
124
+
125
+ async function __initCrossws(env, hooks) {
126
+ if (__crosswsAdapter) return __crosswsAdapter;
127
+ const importFn = env.__ENV_RUNNER_UNSAFE_EVAL__.newAsyncFunction(
128
+ "return await import('crossws/adapters/cloudflare')",
129
+ "loadCrossws"
130
+ );
131
+ const { default: cloudflareAdapter } = await importFn();
132
+ __crosswsAdapter = cloudflareAdapter({ hooks });
133
+ return __crosswsAdapter;
134
+ }
135
+
136
+ export default {
137
+ async fetch(request, env, ctx) {
138
+ const url = new URL(request.url);
139
+
140
+ // WebSocket IPC handshake
141
+ if (url.pathname === __IPC_PATH && request.headers.get("upgrade") === "websocket") {
142
+ try {
143
+ if (!__userEntry) {
144
+ __userEntry = await __loadEntry(env, __entryPath);
145
+ }
146
+ } catch (e) {
147
+ return new Response("Failed to load entry: " + String(e), { status: 500 });
148
+ }
149
+
150
+ const pair = new WebSocketPair();
151
+ const client = pair[0];
152
+ const server = pair[1];
153
+ server.accept();
154
+ __serverWs = server;
155
+
156
+ server.addEventListener("message", (event) => {
157
+ __handleWsMessage(env, event.data);
158
+ });
159
+
160
+ // Initialize IPC hooks
161
+ if (!__ipcInitialized && __userEntry.ipc) {
162
+ __ipcInitialized = true;
163
+ if (__userEntry.ipc.onOpen) {
164
+ await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
165
+ }
166
+ }
167
+
168
+ return new Response(null, { status: 101, webSocket: client });
169
+ }
170
+
171
+ if (!__userEntry) {
172
+ return new Response("Worker not initialized", { status: 503 });
173
+ }
174
+
175
+ // Handle WebSocket upgrade via crossws cloudflare adapter
176
+ if (__userEntry.websocket && request.headers.get("upgrade") === "websocket") {
177
+ const adapter = await __initCrossws(env, __userEntry.websocket);
178
+ return adapter.handleUpgrade(request, env, ctx);
179
+ }
180
+
181
+ const entryFetch = __userEntry.fetch;
182
+ if (!entryFetch) {
183
+ return new Response("No fetch handler exported", { status: 500 });
184
+ }
185
+ __currentEnv = env;
186
+ try {
187
+ ${fetchBody}
188
+ } finally {
189
+ __currentEnv = undefined;
190
+ }
191
+ }
192
+ };
193
+ `;
194
+ }
195
+ //#endregion
3
196
  //#region src/runners/miniflare/runner.ts
4
- const IPC_HEADER = "x-env-runner-ipc";
197
+ const IPC_PATH = "/__env_runner_ipc";
198
+ const _miniflareCache = /* @__PURE__ */ new Map();
5
199
  var MiniflareEnvRunner = class extends BaseEnvRunner {
6
200
  #miniflare;
7
201
  #miniflareOptions;
202
+ #transformRequest;
203
+ #reloadCounter = 0;
204
+ #ws;
205
+ #persistent;
206
+ #cacheKey;
207
+ #exports;
208
+ #captureErrors;
8
209
  constructor(opts) {
9
210
  super({
10
211
  ...opts,
11
212
  workerEntry: ""
12
213
  });
13
214
  this.#miniflareOptions = opts.miniflareOptions || {};
215
+ this.#transformRequest = opts.transformRequest;
216
+ this.#persistent = opts.persistent ?? false;
217
+ this.#exports = opts.exports ?? {};
218
+ this.#captureErrors = opts.captureErrors ?? true;
14
219
  this.#init();
15
220
  }
221
+ /** Dispose all persistent Miniflare instances from the cache. */
222
+ static async disposeAll() {
223
+ const entries = [..._miniflareCache.values()];
224
+ _miniflareCache.clear();
225
+ for (const entry of entries) await entry.mf.dispose().catch(() => {});
226
+ }
227
+ /** Fully dispose the Miniflare instance (even if persistent). */
228
+ async dispose() {
229
+ if (this.#miniflare) {
230
+ if (this.#ws) {
231
+ this.#ws.send(JSON.stringify({ type: "shutdown" }));
232
+ this.#ws.close();
233
+ this.#ws = void 0;
234
+ }
235
+ if (this.#cacheKey) _miniflareCache.delete(this.#cacheKey);
236
+ await this.#miniflare.dispose();
237
+ this.#miniflare = void 0;
238
+ }
239
+ if (!this.closed) await this.close();
240
+ }
16
241
  async fetch(input, init) {
17
242
  if (!this.#miniflare || this.closed) return new Response("miniflare env runner is unavailable", { status: 503 });
18
243
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
19
- return this.#miniflare.dispatchFetch(url, init);
244
+ const res = await this.#miniflare.dispatchFetch(url, init);
245
+ if (res instanceof Response) return res;
246
+ return new Response(res.body, {
247
+ status: res.status,
248
+ statusText: res.statusText,
249
+ headers: res.headers
250
+ });
20
251
  }
21
252
  sendMessage(message) {
22
- if (!this.#miniflare) throw new Error("Miniflare env runner should be initialized before sending messages.");
253
+ if (!this.#ws) throw new Error("Miniflare env runner should be initialized before sending messages.");
23
254
  if (message?.type === "ping") {
24
255
  queueMicrotask(() => this._handleMessage({
25
256
  type: "pong",
@@ -27,11 +258,45 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
27
258
  }));
28
259
  return;
29
260
  }
30
- this.#miniflare.dispatchFetch("http://localhost/__env_runner_ipc", {
31
- method: "POST",
32
- headers: { [IPC_HEADER]: "message" },
33
- body: JSON.stringify(message)
34
- }).catch(() => {});
261
+ this.#ws.send(JSON.stringify({
262
+ type: "message",
263
+ data: message
264
+ }));
265
+ }
266
+ /**
267
+ * Hot-reload the user entry module without recreating the Miniflare instance.
268
+ *
269
+ * Sends `reload-module` event over the WebSocket. The worker wrapper uses
270
+ * `unsafeEvalBinding` to re-import the entry with a cache-busting query string
271
+ * and responds with `module-reloaded` when done.
272
+ */
273
+ async reloadModule(timeout = 5e3) {
274
+ if (!this.#ws) throw new Error("Miniflare env runner should be initialized before reloading.");
275
+ if (!this._data?.entry) return;
276
+ this.#reloadCounter++;
277
+ const version = this.#reloadCounter;
278
+ return new Promise((resolve, reject) => {
279
+ const timer = setTimeout(() => {
280
+ cleanup();
281
+ reject(/* @__PURE__ */ new Error("Module reload timed out"));
282
+ }, timeout);
283
+ const listener = (msg) => {
284
+ if (msg?.event === "module-reloaded") {
285
+ cleanup();
286
+ if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
287
+ else resolve();
288
+ }
289
+ };
290
+ const cleanup = () => {
291
+ clearTimeout(timer);
292
+ this.offMessage(listener);
293
+ };
294
+ this.onMessage(listener);
295
+ this.#ws.send(JSON.stringify({
296
+ type: "reload",
297
+ version
298
+ }));
299
+ });
35
300
  }
36
301
  _hasRuntime() {
37
302
  return Boolean(this.#miniflare);
@@ -41,11 +306,21 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
41
306
  }
42
307
  async _closeRuntime() {
43
308
  if (!this.#miniflare) return;
44
- await this.#miniflare.dispatchFetch("http://localhost/__env_runner_ipc", {
45
- method: "POST",
46
- headers: { [IPC_HEADER]: "shutdown" }
47
- }).catch(() => {});
48
- await this.#miniflare.dispose();
309
+ if (this.#ws) {
310
+ this.#ws.send(JSON.stringify({ type: "shutdown" }));
311
+ this.#ws.close();
312
+ this.#ws = void 0;
313
+ }
314
+ if (this.#persistent && this.#cacheKey) {
315
+ const cached = _miniflareCache.get(this.#cacheKey);
316
+ if (cached) {
317
+ cached.refCount--;
318
+ if (cached.refCount <= 0) {
319
+ _miniflareCache.delete(this.#cacheKey);
320
+ await this.#miniflare.dispose();
321
+ }
322
+ }
323
+ } else await this.#miniflare.dispose();
49
324
  this.#miniflare = void 0;
50
325
  }
51
326
  #init() {
@@ -54,32 +329,180 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
54
329
  this.close(error);
55
330
  });
56
331
  }
332
+ async upgrade(context) {
333
+ if (!this.#miniflare || this.closed) {
334
+ context.node.socket.destroy();
335
+ return;
336
+ }
337
+ const mfUrl = await this.#miniflare.unsafeGetDirectURL();
338
+ const address = new URL(mfUrl);
339
+ await proxyUpgrade({
340
+ host: address.hostname,
341
+ port: Number(address.port)
342
+ }, context.node.req, context.node.socket, context.node.head);
343
+ }
57
344
  async #initAsync() {
58
345
  const { Miniflare } = await import("miniflare");
59
346
  const entryPath = this._data?.entry;
347
+ const userFlags = this.#miniflareOptions.compatibilityFlags || [];
348
+ const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || [];
60
349
  const options = {
61
350
  compatibilityDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
62
351
  modules: true,
63
- ...this.#miniflareOptions
352
+ ...this.#miniflareOptions,
353
+ compatibilityFlags: [...new Set(["nodejs_compat", ...userFlags])],
354
+ unsafeDirectSockets: [{
355
+ host: "127.0.0.1",
356
+ port: 0
357
+ }, ...userDirectSockets]
64
358
  };
65
- options.serviceBindings = {
66
- ...options.serviceBindings || {},
67
- __ENV_RUNNER_IPC: async (request) => {
68
- const message = await request.json().catch(() => null);
69
- if (message !== null) this._handleMessage(message);
70
- return new Response(null, { status: 204 });
359
+ if (entryPath && !options.script && !options.scriptPath) {
360
+ const resolvedEntry = resolve(entryPath);
361
+ const entryDir = dirname(resolvedEntry);
362
+ const detectedExports = this.#exports === false || this.#exports === void 0 ? [] : detectExportedClasses(resolvedEntry, typeof this.#exports === "object" ? this.#exports : {});
363
+ if (detectedExports.length > 0 && !options.durableObjects) {
364
+ const autoDOs = { ...this.#miniflareOptions.durableObjects || {} };
365
+ for (const name of detectedExports) {
366
+ const bindingName = toScreamingSnakeCase(name);
367
+ if (!autoDOs[bindingName]) autoDOs[bindingName] = name;
368
+ }
369
+ options.durableObjects = autoDOs;
71
370
  }
72
- };
73
- if (entryPath && !options.script) {
74
- options.script = wrapScript(readFileSync(entryPath, "utf8"));
75
- delete options.scriptPath;
371
+ options.script = generateWrapper(resolvedEntry, {
372
+ dynamicOnly: true,
373
+ captureErrors: this.#captureErrors,
374
+ exports: detectedExports
375
+ });
376
+ options.scriptPath = entryDir + "/__env_runner_wrapper.mjs";
377
+ if (!options.modulesRoot) options.modulesRoot = "/";
378
+ options.unsafeEvalBinding = "__ENV_RUNNER_UNSAFE_EVAL__";
379
+ options.serviceBindings = {
380
+ ...options.serviceBindings || {},
381
+ [IPC_BINDING]: async (request) => {
382
+ try {
383
+ const message = await request.json();
384
+ this._handleMessage(message);
385
+ } catch {}
386
+ return new Response(null, { status: 204 });
387
+ }
388
+ };
389
+ if (this.#transformRequest && !options.modulesRules) options.modulesRules = [{
390
+ type: "ESModule",
391
+ include: [
392
+ "**/*.ts",
393
+ "**/*.tsx",
394
+ "**/*.jsx",
395
+ "**/*.mts"
396
+ ]
397
+ }];
398
+ if (!options.unsafeModuleFallbackService) {
399
+ const _require = createRequire(resolvedEntry);
400
+ const _transformRequest = this.#transformRequest;
401
+ options.unsafeUseModuleFallbackService = true;
402
+ const modulePathMap = /* @__PURE__ */ new Map();
403
+ options.unsafeModuleFallbackService = async (request) => {
404
+ const url = new URL(request.url);
405
+ const specifier = url.searchParams.get("specifier");
406
+ const rawSpecifier = url.searchParams.get("rawSpecifier");
407
+ const referrer = url.searchParams.get("referrer") || "";
408
+ if (!specifier) return new Response(null, { status: 404 });
409
+ const cleanSpecifier = specifier.split("?")[0] || specifier;
410
+ const cleanRaw = rawSpecifier?.split("?")[0];
411
+ let resolvedPath;
412
+ const fileUrlRaw = cleanRaw || cleanSpecifier;
413
+ if (fileUrlRaw.startsWith("file://")) try {
414
+ resolvedPath = fileURLToPath(fileUrlRaw);
415
+ } catch {
416
+ return new Response(null, { status: 404 });
417
+ }
418
+ else if (cleanRaw && !cleanRaw.startsWith(".") && !cleanRaw.startsWith("/")) {
419
+ const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
420
+ const referrerReal = modulePathMap.get(referrerKey);
421
+ const contextRequire = referrerReal ? createRequire(referrerReal) : _require;
422
+ if (cleanRaw.startsWith("node:")) {
423
+ const nodeName = cleanRaw.slice(5);
424
+ try {
425
+ resolvedPath = contextRequire.resolve(`unenv/node/${nodeName}`);
426
+ } catch {
427
+ return new Response(null, { status: 404 });
428
+ }
429
+ } else try {
430
+ resolvedPath = contextRequire.resolve(cleanRaw);
431
+ } catch {
432
+ const name = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
433
+ return Response.json({
434
+ name,
435
+ esModule: "export default undefined;"
436
+ });
437
+ }
438
+ } else {
439
+ const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
440
+ const referrerDir = dirname(modulePathMap.get(referrerKey) || (referrer.startsWith("/") ? referrer : "/" + referrer));
441
+ const raw = cleanRaw || cleanSpecifier;
442
+ if (raw.startsWith(".")) resolvedPath = resolve(referrerDir, raw);
443
+ else if (cleanSpecifier.startsWith("/")) resolvedPath = cleanSpecifier;
444
+ else try {
445
+ resolvedPath = _require.resolve(raw);
446
+ } catch {
447
+ return new Response(null, { status: 404 });
448
+ }
449
+ }
450
+ const rawQuery = specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "";
451
+ const name = (cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier) + rawQuery;
452
+ if (_transformRequest) try {
453
+ const result = await _transformRequest(resolvedPath);
454
+ if (result?.code) {
455
+ modulePathMap.set(name, resolvedPath);
456
+ return Response.json({
457
+ name,
458
+ esModule: result.code
459
+ });
460
+ }
461
+ } catch {}
462
+ try {
463
+ const contents = readFileSync(resolvedPath, "utf8");
464
+ modulePathMap.set(name, resolvedPath);
465
+ const isESM = resolvedPath.endsWith(".mjs") || !resolvedPath.endsWith(".cjs") && /\b(import\s|import\(|export\s|export\{|import\.meta\b)/.test(contents);
466
+ return Response.json({
467
+ name,
468
+ ...isESM ? { esModule: contents } : { commonJsModule: contents }
469
+ });
470
+ } catch {
471
+ return new Response(null, { status: 404 });
472
+ }
473
+ };
474
+ }
475
+ }
476
+ if (this.#persistent && entryPath) {
477
+ this.#cacheKey = computeCacheKey(entryPath, this.#miniflareOptions);
478
+ const cached = _miniflareCache.get(this.#cacheKey);
479
+ if (cached) {
480
+ this.#miniflare = cached.mf;
481
+ cached.refCount++;
482
+ }
483
+ }
484
+ if (!this.#miniflare) {
485
+ this.#miniflare = new Miniflare(options);
486
+ await this.#miniflare.ready;
487
+ if (this.#persistent && this.#cacheKey) _miniflareCache.set(this.#cacheKey, {
488
+ mf: this.#miniflare,
489
+ refCount: 1
490
+ });
491
+ }
492
+ const initRes = await this.#miniflare.dispatchFetch("http://localhost" + IPC_PATH, { headers: { upgrade: "websocket" } });
493
+ const ws = initRes.webSocket;
494
+ if (!ws) {
495
+ const body = await initRes.text().catch(() => "");
496
+ throw new Error(`Failed to establish WebSocket IPC channel (${initRes.status}: ${body})`);
76
497
  }
77
- this.#miniflare = new Miniflare(options);
78
- await this.#miniflare.ready;
79
- await this.#miniflare.dispatchFetch("http://localhost/__env_runner_ipc", {
80
- method: "POST",
81
- headers: { [IPC_HEADER]: "init" }
82
- }).catch(() => {});
498
+ ws.accept();
499
+ this.#ws = ws;
500
+ ws.addEventListener("message", (event) => {
501
+ try {
502
+ const parsed = JSON.parse(event.data);
503
+ this._handleMessage(parsed);
504
+ } catch {}
505
+ });
83
506
  this._handleMessage({ address: {
84
507
  host: "127.0.0.1",
85
508
  port: 0
@@ -87,60 +510,28 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
87
510
  }
88
511
  };
89
512
  /**
90
- * Wraps user module code with IPC glue.
91
- *
92
- * The user module is expected to `export default { fetch, ipc? }`.
93
- * The wrapper renames the user's default export, intercepts IPC requests,
94
- * and bridges the `ipc` hooks via `env.__ENV_RUNNER_IPC` service binding.
513
+ * Detect `export class` declarations in the entry file.
514
+ * Merges with explicitly declared exports from options.
95
515
  */
96
- function wrapScript(userCode) {
97
- return `${userCode.replace(/export\s+default\s+/, "const __userEntry = ").replace(/export\s*\{([^}]*?\b(\w+)\b\s+as\s+default\b[^}]*)\}/, (_match, _inner, identifier) => `const __userEntry = ${identifier};`)}
98
-
99
- // --- env-runner IPC glue ---
100
- let __ipcInitialized = false;
101
- const __IPC_HEADER = "${IPC_HEADER}";
102
-
103
- export default {
104
- async fetch(request, env, ctx) {
105
- // Handle IPC control messages from runner
106
- const ipcType = request.headers.get(__IPC_HEADER);
107
- if (ipcType) {
108
- if (ipcType === "init") {
109
- if (!__ipcInitialized && __userEntry.ipc && env.__ENV_RUNNER_IPC) {
110
- __ipcInitialized = true;
111
- const sendMessage = (message) => {
112
- env.__ENV_RUNNER_IPC.fetch("http://ipc/", {
113
- method: "POST",
114
- body: JSON.stringify(message),
115
- });
116
- };
117
- if (__userEntry.ipc.onOpen) {
118
- await __userEntry.ipc.onOpen({ sendMessage });
119
- }
120
- }
121
- return new Response(null, { status: 204 });
122
- }
123
- if (ipcType === "message") {
124
- const message = await request.json();
125
- if (__userEntry.ipc?.onMessage) {
126
- __userEntry.ipc.onMessage(message);
127
- }
128
- return new Response(null, { status: 204 });
129
- }
130
- if (ipcType === "shutdown") {
131
- if (__userEntry.ipc?.onClose) {
132
- await __userEntry.ipc.onClose();
133
- }
134
- return new Response(null, { status: 204 });
135
- }
136
- return new Response(null, { status: 400 });
137
- }
138
-
139
- // Delegate to user's fetch handler
140
- return __userEntry.fetch(request, env, ctx);
141
- }
142
- };
143
- `;
516
+ function detectExportedClasses(entryPath, explicit) {
517
+ const names = new Set(Object.keys(explicit));
518
+ try {
519
+ const source = readFileSync(entryPath, "utf8");
520
+ const re = /\bexport\s+class\s+(\w+)/g;
521
+ let match;
522
+ while (match = re.exec(source)) if (match[1]) names.add(match[1]);
523
+ } catch {}
524
+ return [...names];
525
+ }
526
+ /** Convert PascalCase/camelCase to SCREAMING_SNAKE_CASE (e.g. `Counter` → `COUNTER`, `MyDurableObject` → `MY_DURABLE_OBJECT`). */
527
+ function toScreamingSnakeCase(name) {
528
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
529
+ }
530
+ /** Compute a stable cache key for persistent Miniflare instances. */
531
+ function computeCacheKey(entryPath, opts) {
532
+ const serializableOpts = {};
533
+ for (const [k, v] of Object.entries(opts)) if (typeof v !== "function") serializableOpts[k] = v;
534
+ return `${resolve(entryPath)}::${JSON.stringify(serializableOpts)}`;
144
535
  }
145
536
  //#endregion
146
537
  export { MiniflareEnvRunner as t };
@@ -51,6 +51,54 @@ var RunnerManager = class {
51
51
  this._messageListeners.delete(listener);
52
52
  this._runner?.offMessage(listener);
53
53
  }
54
+ waitForReady(timeout = 5e3) {
55
+ if (this.ready) return Promise.resolve();
56
+ return new Promise((resolve, reject) => {
57
+ const timer = setTimeout(() => {
58
+ this._messageListeners.delete(listener);
59
+ reject(/* @__PURE__ */ new Error("Runner did not become ready in time"));
60
+ }, timeout);
61
+ const listener = (message) => {
62
+ if (message?.address || this.ready) {
63
+ clearTimeout(timer);
64
+ this._messageListeners.delete(listener);
65
+ resolve();
66
+ }
67
+ };
68
+ this._messageListeners.add(listener);
69
+ });
70
+ }
71
+ rpc(name, data, opts) {
72
+ const id = Math.random().toString(36).slice(2);
73
+ const timeout = opts?.timeout ?? 3e3;
74
+ return new Promise((resolve, reject) => {
75
+ const timer = setTimeout(() => {
76
+ cleanup();
77
+ reject(/* @__PURE__ */ new Error(`RPC "${name}" timed out`));
78
+ }, timeout);
79
+ const listener = (msg) => {
80
+ if (msg?.__rpc_id === id) {
81
+ cleanup();
82
+ if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
83
+ else resolve(msg.data);
84
+ }
85
+ };
86
+ const cleanup = () => {
87
+ clearTimeout(timer);
88
+ this.offMessage(listener);
89
+ };
90
+ this.onMessage(listener);
91
+ this.sendMessage({
92
+ __rpc: name,
93
+ __rpc_id: id,
94
+ data
95
+ });
96
+ });
97
+ }
98
+ async reloadModule(timeout) {
99
+ if (!this._runner?.reloadModule) throw new Error("Active runner does not support reloadModule()");
100
+ return this._runner.reloadModule(timeout);
101
+ }
54
102
  async close() {
55
103
  this._closed = true;
56
104
  this._messageQueue.length = 0;