env-runner 0.1.2 → 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.
package/README.md CHANGED
@@ -375,8 +375,19 @@ export default {
375
375
  fetch(request: Request) {
376
376
  return new Response("Hello!");
377
377
  },
378
+ websocket: {
379
+ // Optional: crossws WebSocket hooks (recommended)
380
+ open(peer) {
381
+ peer.send("Welcome!");
382
+ },
383
+ message(peer, message) {
384
+ peer.send(`Echo: ${message.text()}`);
385
+ },
386
+ close(peer, details) {},
387
+ error(peer, error) {},
388
+ },
378
389
  upgrade(context) {
379
- // Optional: handle WebSocket upgrade requests (Node.js only)
390
+ // Optional: raw WebSocket upgrade handler (Node.js only)
380
391
  // context.node gives { req, socket, head }
381
392
  },
382
393
  middleware: [], // Optional srvx middleware
@@ -1,5 +1,7 @@
1
1
  import { u as WorkerHooks } from "./types.mjs";
2
2
  import { n as EnvRunnerData, t as BaseEnvRunner } from "./base-runner.mjs";
3
+ import { IncomingMessage } from "node:http";
4
+ import { Socket } from "node:net";
3
5
 
4
6
  //#region src/runners/miniflare/runner.d.ts
5
7
  /** Result from a module transform (compatible with Vite's `TransformResult`). */
@@ -65,6 +67,13 @@ declare class MiniflareEnvRunner extends BaseEnvRunner {
65
67
  protected _hasRuntime(): boolean;
66
68
  protected _runtimeType(): string;
67
69
  protected _closeRuntime(): Promise<void>;
70
+ upgrade(context: {
71
+ node: {
72
+ req: IncomingMessage;
73
+ socket: Socket;
74
+ head: any;
75
+ };
76
+ }): Promise<void>;
68
77
  }
69
78
  //#endregion
70
79
  export { TransformResult as i, MiniflareEnvRunnerOptions as n, MiniflareExportInfo as r, MiniflareEnvRunner as t };
@@ -1,10 +1,13 @@
1
1
  import { t as BaseEnvRunner } from "./base-runner.mjs";
2
2
  import { createRequire } from "node:module";
3
+ import { proxyUpgrade } from "httpxy";
3
4
  import { readFileSync } from "node:fs";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { dirname, resolve } from "node:path";
6
7
  //#region src/runners/miniflare/wrapper.ts
7
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";
8
11
  /**
9
12
  * Generates a wrapper module that imports the user entry and adds IPC glue.
10
13
  *
@@ -15,6 +18,10 @@ const IPC_PATH$1 = "/__env_runner_ipc";
15
18
  * - Reload: `{ type: "reload" }` triggers cache-busted re-import
16
19
  * - Shutdown: `{ type: "shutdown" }` calls `ipc.onClose()`
17
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
+ *
18
25
  * Passed as an in-memory `script` to Miniflare (no temp files needed).
19
26
  */
20
27
  function generateWrapper(entryPath, opts) {
@@ -40,10 +47,12 @@ ${staticReExport}
40
47
  ${explicitExports}
41
48
 
42
49
  const __IPC_PATH = "${IPC_PATH$1}";
50
+ const __IPC_BINDING = "${IPC_BINDING}";
43
51
  const __entryPath = ${JSON.stringify(entryPath)};
44
52
  let __userEntry;
45
53
  let __ipcInitialized = false;
46
54
  let __serverWs;
55
+ let __currentEnv;
47
56
 
48
57
  async function __loadEntry(env, path) {
49
58
  globalThis.__ENV_RUNNER_UNSAFE_EVAL__ = env.__ENV_RUNNER_UNSAFE_EVAL__;
@@ -57,8 +66,17 @@ async function __loadEntry(env, path) {
57
66
  }
58
67
 
59
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
+ }
60
78
  if (__serverWs) {
61
- __serverWs.send(JSON.stringify(message));
79
+ __serverWs.send(payload);
62
80
  }
63
81
  }
64
82
 
@@ -81,6 +99,7 @@ async function __handleWsMessage(env, data) {
81
99
  await __userEntry.ipc.onClose();
82
100
  }
83
101
  __userEntry = newEntry;
102
+ __crosswsAdapter = undefined;
84
103
  __ipcInitialized = false;
85
104
  if (__userEntry.ipc?.onOpen) {
86
105
  __ipcInitialized = true;
@@ -101,6 +120,19 @@ async function __handleWsMessage(env, data) {
101
120
  }
102
121
  }
103
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
+
104
136
  export default {
105
137
  async fetch(request, env, ctx) {
106
138
  const url = new URL(request.url);
@@ -139,11 +171,23 @@ export default {
139
171
  if (!__userEntry) {
140
172
  return new Response("Worker not initialized", { status: 503 });
141
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
+
142
181
  const entryFetch = __userEntry.fetch;
143
182
  if (!entryFetch) {
144
183
  return new Response("No fetch handler exported", { status: 500 });
145
184
  }
146
- ${fetchBody}
185
+ __currentEnv = env;
186
+ try {
187
+ ${fetchBody}
188
+ } finally {
189
+ __currentEnv = undefined;
190
+ }
147
191
  }
148
192
  };
149
193
  `;
@@ -285,15 +329,32 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
285
329
  this.close(error);
286
330
  });
287
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
+ }
288
344
  async #initAsync() {
289
345
  const { Miniflare } = await import("miniflare");
290
346
  const entryPath = this._data?.entry;
291
347
  const userFlags = this.#miniflareOptions.compatibilityFlags || [];
348
+ const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || [];
292
349
  const options = {
293
350
  compatibilityDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
294
351
  modules: true,
295
352
  ...this.#miniflareOptions,
296
- compatibilityFlags: [...new Set(["nodejs_compat", ...userFlags])]
353
+ compatibilityFlags: [...new Set(["nodejs_compat", ...userFlags])],
354
+ unsafeDirectSockets: [{
355
+ host: "127.0.0.1",
356
+ port: 0
357
+ }, ...userDirectSockets]
297
358
  };
298
359
  if (entryPath && !options.script && !options.scriptPath) {
299
360
  const resolvedEntry = resolve(entryPath);
@@ -315,6 +376,16 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
315
376
  options.scriptPath = entryDir + "/__env_runner_wrapper.mjs";
316
377
  if (!options.modulesRoot) options.modulesRoot = "/";
317
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
+ };
318
389
  if (this.#transformRequest && !options.modulesRules) options.modulesRules = [{
319
390
  type: "ESModule",
320
391
  include: [
package/dist/index.d.mts CHANGED
@@ -3,6 +3,7 @@ import { n as EnvRunnerData, t as BaseEnvRunner } from "./_chunks/base-runner.mj
3
3
  import { t as DenoProcessEnvRunner } from "./_chunks/runner.mjs";
4
4
  import { i as TransformResult, n as MiniflareEnvRunnerOptions, r as MiniflareExportInfo, t as MiniflareEnvRunner } from "./_chunks/runner2.mjs";
5
5
  import { ServerOptions } from "srvx";
6
+ import { Hooks } from "crossws";
6
7
 
7
8
  //#region src/manager.d.ts
8
9
  /**
@@ -99,6 +100,7 @@ interface AppEntryIPC {
99
100
  interface AppEntry {
100
101
  fetch: ServerOptions["fetch"];
101
102
  upgrade?: (context: UpgradeContext) => void;
103
+ websocket?: Partial<Hooks>;
102
104
  middleware?: ServerOptions["middleware"];
103
105
  plugins?: ServerOptions["plugins"];
104
106
  ipc?: AppEntryIPC;
@@ -1,5 +1,6 @@
1
1
  import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs";
2
2
  import { serve } from "srvx";
3
+ import { plugin } from "crossws/server";
3
4
  //#region src/runners/bun-process/worker.ts
4
5
  const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}");
5
6
  let entry = await resolveEntry(data.entry);
@@ -10,7 +11,7 @@ const server = serve({
10
11
  silent: true,
11
12
  fetch: (request) => entry.fetch(request),
12
13
  middleware: entry.middleware,
13
- plugins: entry.plugins,
14
+ plugins: [...entry.plugins || [], ...entry.websocket ? [plugin(entry.websocket)] : []],
14
15
  gracefulShutdown: false
15
16
  });
16
17
  await server.ready();
@@ -1,5 +1,6 @@
1
1
  import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs";
2
2
  import { serve } from "srvx";
3
+ import { plugin } from "crossws/server";
3
4
  //#region src/runners/deno-process/worker.ts
4
5
  const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}");
5
6
  let entry = await resolveEntry(data.entry);
@@ -10,7 +11,7 @@ const server = serve({
10
11
  silent: true,
11
12
  fetch: (request) => entry.fetch(request),
12
13
  middleware: entry.middleware,
13
- plugins: entry.plugins,
14
+ plugins: [...entry.plugins || [], ...entry.websocket ? [plugin(entry.websocket)] : []],
14
15
  gracefulShutdown: false
15
16
  });
16
17
  await server.ready();
@@ -1,5 +1,6 @@
1
1
  import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs";
2
2
  import { serve } from "srvx";
3
+ import { plugin } from "crossws/server/node";
3
4
  //#region src/runners/node-process/worker.ts
4
5
  const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}");
5
6
  let entry = await resolveEntry(data.entry);
@@ -10,7 +11,7 @@ const server = serve({
10
11
  silent: true,
11
12
  fetch: (request) => entry.fetch(request),
12
13
  middleware: entry.middleware,
13
- plugins: entry.plugins,
14
+ plugins: [...entry.plugins || [], ...entry.websocket ? [plugin(entry.websocket)] : []],
14
15
  gracefulShutdown: false
15
16
  });
16
17
  await server.ready();
@@ -1,6 +1,7 @@
1
1
  import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs";
2
2
  import { parentPort, workerData } from "node:worker_threads";
3
3
  import { serve } from "srvx";
4
+ import { plugin } from "crossws/server/node";
4
5
  //#region src/runners/node-worker/worker.ts
5
6
  const data = workerData || {};
6
7
  let entry = await resolveEntry(data.entry);
@@ -11,7 +12,7 @@ const server = serve({
11
12
  silent: true,
12
13
  fetch: (request) => entry.fetch(request),
13
14
  middleware: entry.middleware,
14
- plugins: entry.plugins,
15
+ plugins: [...entry.plugins || [], ...entry.websocket ? [plugin(entry.websocket)] : []],
15
16
  gracefulShutdown: false
16
17
  });
17
18
  await server.ready();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "env-runner",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Generic environment runner for JavaScript runtimes.",
5
5
  "license": "MIT",
6
6
  "repository": "unjs/env-runner",
@@ -38,6 +38,7 @@
38
38
  "typecheck": "tsgo --noEmit --skipLibCheck"
39
39
  },
40
40
  "dependencies": {
41
+ "crossws": "^0.4.4",
41
42
  "httpxy": "^0.3.1",
42
43
  "srvx": "^0.11.9"
43
44
  },