env-runner 0.1.15 → 0.1.16
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 +29 -1
- package/dist/_chunks/common-base-runner.d.mts +1 -0
- package/dist/_chunks/common-base-runner.mjs +9 -2
- package/dist/_chunks/server.mjs +61 -7
- package/dist/_chunks/types.d.mts +2 -0
- package/dist/_chunks/vercel-runner.mjs +3 -1
- package/dist/cli.mjs +2 -8
- package/dist/index.d.mts +11 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -73,9 +73,36 @@ await envServer.reload();
|
|
|
73
73
|
// Use with any HTTP server
|
|
74
74
|
const server = serve({
|
|
75
75
|
fetch: (request) => envServer.fetch(request),
|
|
76
|
+
// Proxy WebSocket upgrades to the worker (see "WebSocket proxying" below)
|
|
77
|
+
plugins: [await envServer.wsSrvxPlugin()],
|
|
76
78
|
});
|
|
77
79
|
```
|
|
78
80
|
|
|
81
|
+
#### WebSocket proxying
|
|
82
|
+
|
|
83
|
+
To proxy WebSocket upgrades to the worker, attach the plugin returned by
|
|
84
|
+
`wsSrvxPlugin()` (available on both `RunnerManager` and `EnvServer`) to your
|
|
85
|
+
[srvx](https://srvx.h3.dev) server:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const server = serve({
|
|
89
|
+
fetch: (request) => envServer.fetch(request),
|
|
90
|
+
plugins: [await envServer.wsSrvxPlugin()],
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The plugin picks the proxy strategy by **host** runtime:
|
|
95
|
+
|
|
96
|
+
- **Node** — proxies the raw upgrade socket to the worker (transparent
|
|
97
|
+
passthrough; subprotocol/extension negotiation stays end-to-end).
|
|
98
|
+
- **Bun/Deno** — those runtimes serve natively and expose no Node upgrade
|
|
99
|
+
socket, so the client WebSocket is terminated with [crossws](https://crossws.h3.dev)
|
|
100
|
+
and bridged to the worker over a standard `WebSocket` client.
|
|
101
|
+
|
|
102
|
+
It reads the active runner lazily, so it keeps working across hot-reloads, and
|
|
103
|
+
waits for the worker to become ready before proxying. Your entry module should
|
|
104
|
+
expose WebSocket hooks via the `websocket` field (see [Workers](#workers)).
|
|
105
|
+
|
|
79
106
|
### Manager (`RunnerManager`)
|
|
80
107
|
|
|
81
108
|
Proxy manager for hot-reload with message queueing and listener forwarding:
|
|
@@ -147,7 +174,8 @@ await using runner = new NodeProcessEnvRunner({
|
|
|
147
174
|
// Relative URLs are resolved against a placeholder origin
|
|
148
175
|
const response = await runner.fetch("/api");
|
|
149
176
|
|
|
150
|
-
// Proxy WebSocket
|
|
177
|
+
// Proxy a raw WebSocket upgrade to the worker (Node host only — low-level;
|
|
178
|
+
// prefer `manager.wsSrvxPlugin()` for cross-runtime proxying)
|
|
151
179
|
runner.upgrade?.({ node: { req, socket, head } });
|
|
152
180
|
|
|
153
181
|
// Wait for runner to be ready
|
|
@@ -50,6 +50,7 @@ declare abstract class BaseEnvRunner implements EnvRunner, AsyncDisposable {
|
|
|
50
50
|
data?: EnvRunnerData;
|
|
51
51
|
});
|
|
52
52
|
get ready(): boolean;
|
|
53
|
+
get address(): WorkerAddress | undefined;
|
|
53
54
|
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
54
55
|
upgrade(context: {
|
|
55
56
|
node: {
|
|
@@ -23,13 +23,20 @@ var BaseEnvRunner = class {
|
|
|
23
23
|
get ready() {
|
|
24
24
|
return Boolean(!this.closed && this._address && this._hasRuntime());
|
|
25
25
|
}
|
|
26
|
+
get address() {
|
|
27
|
+
return this._address;
|
|
28
|
+
}
|
|
26
29
|
async fetch(input, init) {
|
|
27
30
|
for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
|
|
28
31
|
if (!this._address) return new Response(`${this._runtimeType()} env runner is unavailable`, { status: 503 });
|
|
29
32
|
return proxyFetch(this._address, this._resolveFetchInput(input), init);
|
|
30
33
|
}
|
|
31
34
|
async upgrade(context) {
|
|
32
|
-
if (!this.ready
|
|
35
|
+
if (!this.ready) await this.waitForReady().catch(() => {});
|
|
36
|
+
if (!this.ready || !this._address) {
|
|
37
|
+
context.node.socket.destroy();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
33
40
|
try {
|
|
34
41
|
await proxyUpgrade(this._address, context.node.req, context.node.socket, context.node.head);
|
|
35
42
|
} catch {}
|
|
@@ -40,7 +47,7 @@ var BaseEnvRunner = class {
|
|
|
40
47
|
offMessage(listener) {
|
|
41
48
|
this._messageListeners.delete(listener);
|
|
42
49
|
}
|
|
43
|
-
waitForReady(timeout =
|
|
50
|
+
waitForReady(timeout = 15e3) {
|
|
44
51
|
if (this.ready) return Promise.resolve();
|
|
45
52
|
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
|
|
46
53
|
return new Promise((resolve, reject) => {
|
package/dist/_chunks/server.mjs
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
+
async function createRunnerWSProxyPlugin(getRunner) {
|
|
3
|
+
const isBun = "Bun" in globalThis;
|
|
4
|
+
const isDeno = "Deno" in globalThis;
|
|
5
|
+
if (!isBun && !isDeno) return (server) => {
|
|
6
|
+
server.ready().then(() => {
|
|
7
|
+
(server.node?.server)?.on("upgrade", (req, socket, head) => {
|
|
8
|
+
getRunner()?.upgrade?.({ node: {
|
|
9
|
+
req,
|
|
10
|
+
socket,
|
|
11
|
+
head
|
|
12
|
+
} });
|
|
13
|
+
});
|
|
14
|
+
}).catch(() => {});
|
|
15
|
+
};
|
|
16
|
+
const { createWebSocketProxy } = await import("crossws");
|
|
17
|
+
const { plugin } = isBun ? await import("crossws/server/bun") : await import("crossws/server/deno");
|
|
18
|
+
const proxy = createWebSocketProxy({ target: async (peer) => {
|
|
19
|
+
await getRunner()?.waitForReady?.().catch(() => {});
|
|
20
|
+
return resolveWSProxyTarget(getRunner()?.address, peer.request.url);
|
|
21
|
+
} });
|
|
22
|
+
return plugin({ resolve: () => proxy });
|
|
23
|
+
}
|
|
24
|
+
function resolveWSProxyTarget(address, requestUrl) {
|
|
25
|
+
if (!address) throw new Error("env runner worker is not ready");
|
|
26
|
+
const { pathname, search } = new URL(requestUrl);
|
|
27
|
+
if (address.socketPath) return `ws+unix://${address.socketPath}:${pathname}${search}`;
|
|
28
|
+
if (!address.port) throw new Error("env runner worker is not ready");
|
|
29
|
+
const host = address.host || "127.0.0.1";
|
|
30
|
+
return `ws://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${address.port}${pathname}${search}`;
|
|
31
|
+
}
|
|
2
32
|
var RunnerManager = class {
|
|
3
33
|
_runner;
|
|
4
34
|
_messageQueue = [];
|
|
@@ -9,6 +39,7 @@ var RunnerManager = class {
|
|
|
9
39
|
_pendingModuleReload;
|
|
10
40
|
_closeListeners = /* @__PURE__ */ new Set();
|
|
11
41
|
_readyListeners = /* @__PURE__ */ new Set();
|
|
42
|
+
_readyRejectors = /* @__PURE__ */ new Set();
|
|
12
43
|
constructor(runner) {
|
|
13
44
|
if (runner) this._attach(runner);
|
|
14
45
|
}
|
|
@@ -18,6 +49,9 @@ var RunnerManager = class {
|
|
|
18
49
|
get closed() {
|
|
19
50
|
return this._closed;
|
|
20
51
|
}
|
|
52
|
+
get address() {
|
|
53
|
+
return this._runner?.address;
|
|
54
|
+
}
|
|
21
55
|
async reload(runner) {
|
|
22
56
|
this._reloading = true;
|
|
23
57
|
try {
|
|
@@ -49,9 +83,17 @@ var RunnerManager = class {
|
|
|
49
83
|
});
|
|
50
84
|
return this._pendingModuleReload;
|
|
51
85
|
}
|
|
52
|
-
upgrade = (context) => {
|
|
53
|
-
this.
|
|
86
|
+
upgrade = async (context) => {
|
|
87
|
+
const runner = await this._waitForRunner();
|
|
88
|
+
if (!runner?.upgrade) {
|
|
89
|
+
context.node.socket.destroy();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await runner.upgrade(context);
|
|
54
93
|
};
|
|
94
|
+
wsSrvxPlugin() {
|
|
95
|
+
return createRunnerWSProxyPlugin(() => this);
|
|
96
|
+
}
|
|
55
97
|
sendMessage(message) {
|
|
56
98
|
if (!this._runner || !this._runner.ready) {
|
|
57
99
|
this._messageQueue.push(message);
|
|
@@ -67,21 +109,31 @@ var RunnerManager = class {
|
|
|
67
109
|
this._messageListeners.delete(listener);
|
|
68
110
|
this._runner?.offMessage(listener);
|
|
69
111
|
}
|
|
70
|
-
waitForReady(timeout =
|
|
112
|
+
waitForReady(timeout = 15e3) {
|
|
71
113
|
if (this.ready) return Promise.resolve();
|
|
114
|
+
if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
|
|
72
115
|
return new Promise((resolve, reject) => {
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
this.offMessage(listener);
|
|
119
|
+
this._readyRejectors.delete(onClose);
|
|
120
|
+
};
|
|
73
121
|
const timer = setTimeout(() => {
|
|
74
|
-
|
|
122
|
+
cleanup();
|
|
75
123
|
reject(/* @__PURE__ */ new Error("Runner did not become ready in time"));
|
|
76
124
|
}, timeout);
|
|
77
125
|
const listener = (message) => {
|
|
78
126
|
if (message?.address || this.ready) {
|
|
79
|
-
|
|
80
|
-
this._messageListeners.delete(listener);
|
|
127
|
+
cleanup();
|
|
81
128
|
resolve();
|
|
82
129
|
}
|
|
83
130
|
};
|
|
84
|
-
|
|
131
|
+
const onClose = () => {
|
|
132
|
+
cleanup();
|
|
133
|
+
reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
|
|
134
|
+
};
|
|
135
|
+
this.onMessage(listener);
|
|
136
|
+
this._readyRejectors.add(onClose);
|
|
85
137
|
});
|
|
86
138
|
}
|
|
87
139
|
rpc(name, data, opts) {
|
|
@@ -124,6 +176,8 @@ var RunnerManager = class {
|
|
|
124
176
|
async close() {
|
|
125
177
|
this._closed = true;
|
|
126
178
|
this._messageQueue.length = 0;
|
|
179
|
+
for (const rejectReady of this._readyRejectors) rejectReady();
|
|
180
|
+
this._readyRejectors.clear();
|
|
127
181
|
const runner = this._runner;
|
|
128
182
|
this._detach();
|
|
129
183
|
if (runner) await runner.close();
|
package/dist/_chunks/types.d.mts
CHANGED
|
@@ -62,6 +62,8 @@ interface EnvRunner extends RunnerRPCHooks, AsyncDisposable {
|
|
|
62
62
|
readonly ready: boolean;
|
|
63
63
|
/** Whether the runner has been closed. */
|
|
64
64
|
readonly closed: boolean;
|
|
65
|
+
/** Address the worker is listening at, once ready (`undefined` before then). */
|
|
66
|
+
readonly address?: WorkerAddress;
|
|
65
67
|
/** Proxy an HTTP request to the worker. */
|
|
66
68
|
fetch: FetchHandler;
|
|
67
69
|
/** Proxy a WebSocket upgrade request to the worker. */
|
|
@@ -44,7 +44,9 @@ function _checkVercelOidcToken(token = process.env.VERCEL_OIDC_TOKEN) {
|
|
|
44
44
|
let _defaultEntry;
|
|
45
45
|
const _podId = Math.random().toString(32).slice(-5);
|
|
46
46
|
function generateVercelId() {
|
|
47
|
-
|
|
47
|
+
const timestamp = Date.now().toString(36);
|
|
48
|
+
const random = randomBytes(6).toString("hex");
|
|
49
|
+
return `dev1::${_podId}-${timestamp}-${random}`;
|
|
48
50
|
}
|
|
49
51
|
var VercelEnvRunner = class extends NodeWorkerEnvRunner {
|
|
50
52
|
constructor(opts) {
|
package/dist/cli.mjs
CHANGED
|
@@ -50,16 +50,10 @@ const server = serve({
|
|
|
50
50
|
port: values.port,
|
|
51
51
|
hostname: values.host,
|
|
52
52
|
gracefulShutdown: false,
|
|
53
|
-
fetch: (request) => envServer.fetch(request)
|
|
53
|
+
fetch: (request) => envServer.fetch(request),
|
|
54
|
+
plugins: [await envServer.wsSrvxPlugin()]
|
|
54
55
|
});
|
|
55
56
|
await server.ready();
|
|
56
|
-
server.node?.server?.on("upgrade", (req, socket, head) => {
|
|
57
|
-
envServer.upgrade({ node: {
|
|
58
|
-
req,
|
|
59
|
-
socket,
|
|
60
|
-
head
|
|
61
|
-
} });
|
|
62
|
-
});
|
|
63
57
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, async () => {
|
|
64
58
|
console.log(`\n\x1B[2mShutting down...\x1B[0m`);
|
|
65
59
|
await envServer.close();
|
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import { DenoProcessEnvRunner } from "./_chunks/deno-process-runner.mjs";
|
|
|
4
4
|
import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult } from "./_chunks/miniflare-runner.mjs";
|
|
5
5
|
import { VercelEnvRunner } from "./_chunks/vercel-runner.mjs";
|
|
6
6
|
import { NetlifyEnvRunner } from "./_chunks/netlify-runner.mjs";
|
|
7
|
-
import { ServerOptions } from "srvx";
|
|
7
|
+
import { ServerOptions, ServerPlugin } from "srvx";
|
|
8
8
|
import { Hooks } from "crossws";
|
|
9
9
|
/**
|
|
10
10
|
* Manages an active `EnvRunner` instance, proxying all calls to it.
|
|
@@ -20,9 +20,11 @@ declare class RunnerManager implements EnvRunner, AsyncDisposable {
|
|
|
20
20
|
private _pendingModuleReload;
|
|
21
21
|
private _closeListeners;
|
|
22
22
|
private _readyListeners;
|
|
23
|
+
private _readyRejectors;
|
|
23
24
|
constructor(runner?: EnvRunner);
|
|
24
25
|
get ready(): boolean;
|
|
25
26
|
get closed(): boolean;
|
|
27
|
+
get address(): WorkerAddress | undefined;
|
|
26
28
|
/**
|
|
27
29
|
* Replace the active runner with a new one. Closes the previous runner.
|
|
28
30
|
*
|
|
@@ -41,6 +43,14 @@ declare class RunnerManager implements EnvRunner, AsyncDisposable {
|
|
|
41
43
|
*/
|
|
42
44
|
private _flushInvalidation;
|
|
43
45
|
upgrade: UpgradeHandler;
|
|
46
|
+
/**
|
|
47
|
+
* Create a runtime-native WebSocket reverse-proxy plugin for the public srvx
|
|
48
|
+
* server. Attach it via `serve({ plugins: [await manager.wsSrvxPlugin()] })`:
|
|
49
|
+
* on Node it proxies the raw upgrade socket to the worker, and on Bun/Deno it
|
|
50
|
+
* bridges the WebSocket with crossws. The plugin reads the active runner
|
|
51
|
+
* lazily, so it keeps working across hot-reloads.
|
|
52
|
+
*/
|
|
53
|
+
wsSrvxPlugin(): Promise<ServerPlugin>;
|
|
44
54
|
sendMessage(message: unknown): void;
|
|
45
55
|
onMessage(listener: RunnerMessageListener): void;
|
|
46
56
|
offMessage(listener: RunnerMessageListener): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "env-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Generic environment runner for JavaScript runtimes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "unjs/env-runner",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"typecheck": "tsgo --noEmit --skipLibCheck"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"crossws": "^0.4.
|
|
46
|
+
"crossws": "^0.4.8",
|
|
47
47
|
"exsolve": "^1.1.0",
|
|
48
|
-
"httpxy": "^0.5.
|
|
49
|
-
"srvx": "^0.11.
|
|
48
|
+
"httpxy": "^0.5.4",
|
|
49
|
+
"srvx": "^0.11.19"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@netlify/runtime": "^4.1.25",
|