env-runner 0.1.9 → 0.1.11
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 +130 -16
- package/dist/THIRD-PARTY-LICENSES.md +19 -0
- package/dist/_chunks/base-runner.d.mts +80 -3
- package/dist/_chunks/base-runner.mjs +108 -44
- package/dist/_chunks/libs/cjs-module-lexer.mjs +1 -1
- package/dist/_chunks/libs/es-module-lexer.mjs +83 -0
- package/dist/_chunks/runner.d.mts +3 -3
- package/dist/_chunks/runner.mjs +6 -3
- package/dist/_chunks/runner2.d.mts +15 -3
- package/dist/_chunks/runner2.mjs +145 -56
- package/dist/_chunks/runner3.d.mts +3 -3
- package/dist/_chunks/runner3.mjs +3 -3
- package/dist/_chunks/runner4.d.mts +4 -4
- package/dist/_chunks/runner4.mjs +3 -2
- package/dist/_chunks/runner5.d.mts +4 -4
- package/dist/_chunks/runner5.mjs +3 -2
- package/dist/_chunks/server.mjs +60 -17
- package/dist/_chunks/types.d.mts +15 -3
- package/dist/_chunks/virtual-loader.mjs +63 -0
- package/dist/_chunks/worker-utils.mjs +123 -9
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +52 -14
- package/dist/index.mjs +6 -6
- package/dist/runners/bun-process/runner.d.mts +2 -2
- package/dist/runners/bun-process/runner.mjs +12 -2
- package/dist/runners/bun-process/worker.mjs +23 -3
- package/dist/runners/deno-process/runner.d.mts +2 -2
- package/dist/runners/deno-process/runner.mjs +1 -1
- package/dist/runners/deno-process/worker.mjs +22 -3
- package/dist/runners/miniflare/runner.d.mts +2 -2
- package/dist/runners/miniflare/runner.mjs +1 -1
- package/dist/runners/netlify/runner.d.mts +2 -2
- package/dist/runners/netlify/runner.mjs +1 -1
- package/dist/runners/node-process/runner.d.mts +2 -2
- package/dist/runners/node-process/runner.mjs +4 -2
- package/dist/runners/node-process/worker.mjs +23 -3
- package/dist/runners/node-worker/runner.d.mts +2 -2
- package/dist/runners/node-worker/runner.mjs +1 -1
- package/dist/runners/node-worker/worker.mjs +22 -3
- package/dist/runners/self/runner.d.mts +3 -2
- package/dist/runners/self/runner.mjs +6 -3
- package/dist/runners/vercel/runner.d.mts +2 -2
- package/dist/runners/vercel/runner.mjs +1 -1
- package/dist/vite.d.mts +1 -1
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ import { serve } from "srvx";
|
|
|
50
50
|
import { EnvServer } from "env-runner";
|
|
51
51
|
|
|
52
52
|
const envServer = new EnvServer({
|
|
53
|
-
runner: "node-process",
|
|
53
|
+
runner: "node-process", // optional, defaults to "node-worker"
|
|
54
54
|
entry: "./app.ts",
|
|
55
55
|
watch: true,
|
|
56
56
|
watchPaths: ["./src"],
|
|
@@ -64,8 +64,12 @@ envServer.onReload(() => {
|
|
|
64
64
|
console.log("Reloaded!");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
// Optional — the server auto-starts on first fetch()
|
|
67
68
|
await envServer.start();
|
|
68
69
|
|
|
70
|
+
// Restart with a fresh runner created from the server options
|
|
71
|
+
await envServer.reload();
|
|
72
|
+
|
|
69
73
|
// Use with any HTTP server
|
|
70
74
|
const server = serve({
|
|
71
75
|
fetch: (request) => envServer.fetch(request),
|
|
@@ -79,7 +83,7 @@ Proxy manager for hot-reload with message queueing and listener forwarding:
|
|
|
79
83
|
```ts
|
|
80
84
|
import { RunnerManager, NodeProcessEnvRunner } from "env-runner";
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
await using manager = new RunnerManager();
|
|
83
87
|
|
|
84
88
|
manager.onReady((_runner, address) => {
|
|
85
89
|
console.log("Ready:", address);
|
|
@@ -106,9 +110,11 @@ await manager.reload(newRunner); // old runner is closed automatically
|
|
|
106
110
|
manager.sendMessage({ type: "config", value: 42 });
|
|
107
111
|
manager.onMessage((msg) => console.log("From worker:", msg));
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
// manager.close() is awaited automatically at the end of the scope (`await using`)
|
|
110
114
|
```
|
|
111
115
|
|
|
116
|
+
All runners, `RunnerManager`, and `EnvServer` implement `AsyncDisposable`, so they can be auto-closed with [explicit resource management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/await_using) (`await using`) — or closed manually with `await runner.close()`.
|
|
117
|
+
|
|
112
118
|
### Runners
|
|
113
119
|
|
|
114
120
|
Use runners directly for lower-level control:
|
|
@@ -127,7 +133,7 @@ import { NetlifyEnvRunner } from "env-runner/runners/netlify";
|
|
|
127
133
|
All runners implement the [`EnvRunner`](./src/types.ts) interface:
|
|
128
134
|
|
|
129
135
|
```ts
|
|
130
|
-
|
|
136
|
+
await using runner = new NodeProcessEnvRunner({
|
|
131
137
|
name: "my-app",
|
|
132
138
|
data: { entry: "./app.ts" },
|
|
133
139
|
hooks: {
|
|
@@ -138,7 +144,8 @@ const runner = new NodeProcessEnvRunner({
|
|
|
138
144
|
});
|
|
139
145
|
|
|
140
146
|
// Proxy HTTP requests (retries with exponential backoff)
|
|
141
|
-
|
|
147
|
+
// Relative URLs are resolved against a placeholder origin
|
|
148
|
+
const response = await runner.fetch("/api");
|
|
142
149
|
|
|
143
150
|
// Proxy WebSocket upgrades
|
|
144
151
|
runner.upgrade?.({ node: { req, socket, head } });
|
|
@@ -156,8 +163,12 @@ const result = await runner.rpc<string>("transformHTML", "<html>...</html>");
|
|
|
156
163
|
// Hot-reload entry module without restarting the worker
|
|
157
164
|
await runner.reloadModule();
|
|
158
165
|
|
|
159
|
-
//
|
|
160
|
-
await runner.
|
|
166
|
+
// Invalidate a virtual module (re-runs a factory source), then reload
|
|
167
|
+
await runner.invalidateModule("#config.json");
|
|
168
|
+
await runner.reloadModule();
|
|
169
|
+
|
|
170
|
+
// Graceful shutdown happens automatically at the end of the scope
|
|
171
|
+
// (`await using`) — or call `await runner.close()` explicitly
|
|
161
172
|
```
|
|
162
173
|
|
|
163
174
|
**Available runners:**
|
|
@@ -173,6 +184,107 @@ await runner.close();
|
|
|
173
184
|
| `VercelEnvRunner` | Worker thread (Vercel context) | `workerData` / `parentPort` |
|
|
174
185
|
| `NetlifyEnvRunner` | Worker thread (Netlify context) | `workerData` / `parentPort` |
|
|
175
186
|
|
|
187
|
+
#### Virtual Modules
|
|
188
|
+
|
|
189
|
+
The Node.js runners (`NodeWorkerEnvRunner`, `NodeProcessEnvRunner`, and the runners built on top of them), `BunProcessEnvRunner`, `DenoProcessEnvRunner` (Deno >= 2.x), and `MiniflareEnvRunner` can serve **virtual modules** from an in-memory `specifier => source` map passed via `data.virtual`. The entry (and its dependencies) can then `import` them as if they were real files:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { NodeWorkerEnvRunner } from "env-runner/runners/node-worker";
|
|
193
|
+
|
|
194
|
+
await using runner = new NodeWorkerEnvRunner({
|
|
195
|
+
name: "my-app",
|
|
196
|
+
data: {
|
|
197
|
+
entry: "./app.ts",
|
|
198
|
+
virtual: {
|
|
199
|
+
"#config": `export const apiBase = "https://api.example.com";`,
|
|
200
|
+
"#banner": `export default "Hello from a virtual module!";`,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// app.ts
|
|
208
|
+
import banner from "#banner";
|
|
209
|
+
import { apiBase } from "#config";
|
|
210
|
+
|
|
211
|
+
export default {
|
|
212
|
+
fetch: () => new Response(`${banner} (${apiBase})`),
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The **entry itself can be virtual** — set `data.entry` to one of the `data.virtual` keys to run an entry whose source lives in memory (it may import other virtual modules too):
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
await using runner = new NodeWorkerEnvRunner({
|
|
220
|
+
name: "my-app",
|
|
221
|
+
data: {
|
|
222
|
+
entry: "#entry",
|
|
223
|
+
virtual: {
|
|
224
|
+
"#entry": `import { body } from "#dep";
|
|
225
|
+
export default { fetch: () => new Response(body) };`,
|
|
226
|
+
"#dep": `export const body = "Hello from a virtual entry!";`,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Each source may also be a **factory** `() => string | Promise<string>` instead of a literal string — useful for lazily computed or asynchronously loaded sources:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
await using runner = new NodeWorkerEnvRunner({
|
|
236
|
+
name: "my-app",
|
|
237
|
+
data: {
|
|
238
|
+
entry: "./app.ts",
|
|
239
|
+
virtual: {
|
|
240
|
+
"#config": () => `export const apiBase = ${JSON.stringify(getApiBase())};`,
|
|
241
|
+
"#schema": async () => `export default ${await loadSchemaJson()};`,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Factories are invoked once on the host (before the worker is spawned), so the worker always receives plain strings — functions can't cross the `workerData`/`JSON` boundary, and Node's synchronous load hook can't await. For the same reason, **all** factories are resolved eagerly at startup (in parallel), not lazily on first import — so keep them cheap, or use plain strings for sources that don't need computation. Maps containing only strings skip this step entirely.
|
|
248
|
+
|
|
249
|
+
To refresh a single virtual module without restarting the worker, call `invalidateModule(specifier)`: a factory-valued source is re-run on the host and the module is invalidated in the worker so its **next import evaluates fresh**. Virtual modules that import the invalidated one (directly or transitively) are invalidated along with it, so the fresh module is picked up even through intermediate virtual importers. Already-imported modules keep their instances, so pair it with `reloadModule()` to re-import the entry graph:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
await runner.invalidateModule("#config"); // re-runs the factory, busts the module
|
|
253
|
+
await runner.reloadModule(); // re-imports the entry, picking up the fresh module
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
When fetching through `RunnerManager` or `EnvServer`, the reload is automatic: `invalidateModule()` marks the manager dirty and the next `fetch()` reloads the entry once before serving (concurrent fetches share the reload), so no explicit `reloadModule()` call is needed.
|
|
257
|
+
|
|
258
|
+
The module format is derived from the specifier extension: `.ts`/`.mts` sources are served as **TypeScript** and `.json` sources as **JSON modules**; everything else is plain JavaScript ESM:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
await using runner = new NodeWorkerEnvRunner({
|
|
262
|
+
name: "my-app",
|
|
263
|
+
data: {
|
|
264
|
+
entry: "#entry.ts",
|
|
265
|
+
virtual: {
|
|
266
|
+
"#entry.ts": `
|
|
267
|
+
import { getGreeting } from "#util.ts";
|
|
268
|
+
import config from "#config.json";
|
|
269
|
+
const handler: () => Response = () => new Response(getGreeting(config.name));
|
|
270
|
+
export default { fetch: handler };
|
|
271
|
+
`,
|
|
272
|
+
"#util.ts": `export function getGreeting(name: string): string {
|
|
273
|
+
return \`Hello, \${name}!\`;
|
|
274
|
+
}`,
|
|
275
|
+
"#config.json": JSON.stringify({ name: "virtual" }),
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
- **TypeScript** is type-stripped by Node's native [type stripping](https://nodejs.org/api/typescript.html#type-stripping) (Node.js >= 22.18 / 23.6 — erasable syntax only) and by Bun's `ts` loader. On Deno, custom load hooks bypass its native type stripping, so sources are pre-stripped with [`module.stripTypeScriptTypes`](https://docs.deno.com/api/node/module/~/Module.stripTypeScriptTypes) (Deno >= 2.8.2); on older Deno without it, virtual `.ts`/`.mts` sources **throw at registration** — pass pre-transpiled JavaScript instead. On miniflare, sources are likewise pre-stripped with `module.stripTypeScriptTypes` on the host (workerd does not parse TypeScript).
|
|
282
|
+
- **JSON** sources expose the parsed value as the default export on all runtimes. The `with { type: "json" }` import attribute is optional on Node.js and Bun; on Deno and miniflare it must be **omitted** (static imports carrying an import attribute bypass `registerHooks` resolution on Deno, and workerd rejects import attributes outright).
|
|
283
|
+
|
|
284
|
+
Virtual modules are registered inside the worker, before the entry is imported. On Node.js (>= 22.15 / 23.5) and Deno (>= 2.x) this uses [ESM customization hooks](https://nodejs.org/api/module.html#moduleregisterhooksoptions) (`module.registerHooks`); on Bun (which does not implement `registerHooks`) it uses [`Bun.plugin()`](https://bun.com/docs/runtime/plugins) virtual modules instead. The source string is treated as an ES module, and virtual specifiers (including a virtual entry) resolve across `reloadModule()`. On runtimes supporting neither mechanism, a warning is logged and registration is skipped. When the worker shuts down gracefully the registration is unregistered again (the `registerHooks` registration is deregistered; on Bun, which has no plugin-removal API, the in-memory source map is detached so fresh loads and reloads stop resolving).
|
|
285
|
+
|
|
286
|
+
On `MiniflareEnvRunner` there is no in-worker registration: the runner's module fallback service serves virtual specifiers to workerd directly (taking precedence over disk files and the `transformRequest` pipeline, so a virtual key overrides a real file with the same path). One limitation: named `exports` (Durable Objects / WorkerEntrypoints) cannot be combined with a **virtual entry** — the wrapper would need a static re-export that miniflare cannot resolve at startup — and the runner fails fast with a clear error in that case.
|
|
287
|
+
|
|
176
288
|
#### Miniflare Runner
|
|
177
289
|
|
|
178
290
|
Run your app in the Cloudflare Workers runtime using [miniflare](https://github.com/cloudflare/workers-sdk/tree/main/packages/miniflare):
|
|
@@ -184,7 +296,7 @@ npm install miniflare
|
|
|
184
296
|
```ts
|
|
185
297
|
import { MiniflareEnvRunner } from "env-runner/runners/miniflare";
|
|
186
298
|
|
|
187
|
-
|
|
299
|
+
await using runner = new MiniflareEnvRunner({
|
|
188
300
|
name: "my-worker",
|
|
189
301
|
data: { entry: "./worker.ts" },
|
|
190
302
|
miniflareOptions: {
|
|
@@ -194,7 +306,6 @@ const runner = new MiniflareEnvRunner({
|
|
|
194
306
|
});
|
|
195
307
|
|
|
196
308
|
const response = await runner.fetch("http://localhost/api");
|
|
197
|
-
await runner.close();
|
|
198
309
|
```
|
|
199
310
|
|
|
200
311
|
The `miniflareOptions` object is passed directly to the [Miniflare constructor](https://developers.cloudflare.com/workers/testing/miniflare/) — you can configure bindings, KV, D1, Durable Objects, and any other Miniflare option.
|
|
@@ -206,7 +317,7 @@ Pass a `transformRequest` callback to route module resolution through Vite's (or
|
|
|
206
317
|
```ts
|
|
207
318
|
import { MiniflareEnvRunner } from "env-runner/runners/miniflare";
|
|
208
319
|
|
|
209
|
-
|
|
320
|
+
await using runner = new MiniflareEnvRunner({
|
|
210
321
|
name: "my-worker",
|
|
211
322
|
data: { entry: "./worker.ts" },
|
|
212
323
|
// Route module resolution through Vite's transform pipeline
|
|
@@ -245,7 +356,7 @@ export default {
|
|
|
245
356
|
To explicitly declare exports or override auto-detection:
|
|
246
357
|
|
|
247
358
|
```ts
|
|
248
|
-
|
|
359
|
+
await using runner = new MiniflareEnvRunner({
|
|
249
360
|
name: "my-worker",
|
|
250
361
|
data: { entry: "./worker.ts" },
|
|
251
362
|
// Explicit exports (merged with auto-detected ones)
|
|
@@ -300,7 +411,7 @@ Simulates a Vercel deployment environment with automatic header injection (`x-ve
|
|
|
300
411
|
```ts
|
|
301
412
|
import { VercelEnvRunner } from "env-runner/runners/vercel";
|
|
302
413
|
|
|
303
|
-
|
|
414
|
+
await using runner = new VercelEnvRunner({
|
|
304
415
|
name: "my-app",
|
|
305
416
|
data: { entry: "./app.ts" },
|
|
306
417
|
});
|
|
@@ -313,7 +424,7 @@ Simulates a Netlify deployment environment with automatic header injection (`x-n
|
|
|
313
424
|
```ts
|
|
314
425
|
import { NetlifyEnvRunner } from "env-runner/runners/netlify";
|
|
315
426
|
|
|
316
|
-
|
|
427
|
+
await using runner = new NetlifyEnvRunner({
|
|
317
428
|
name: "my-app",
|
|
318
429
|
data: { entry: "./app.ts" },
|
|
319
430
|
});
|
|
@@ -343,7 +454,10 @@ const env = new DevEnvironment("ssr", config, { hot: true, transport });
|
|
|
343
454
|
import { createViteTransport } from "env-runner/vite";
|
|
344
455
|
|
|
345
456
|
const transport = createViteTransport(sendMessage, onMessage, "ssr");
|
|
346
|
-
const runner = new ModuleRunner({
|
|
457
|
+
const runner = new ModuleRunner({
|
|
458
|
+
transport,
|
|
459
|
+
sourcemapInterceptor: "prepareStackTrace",
|
|
460
|
+
});
|
|
347
461
|
```
|
|
348
462
|
|
|
349
463
|
Messages are namespaced by environment name, so multiple Vite environments can share a single runner's IPC channel.
|
|
@@ -389,7 +503,7 @@ You can also use `loadRunner()` to dynamically load a runner by name:
|
|
|
389
503
|
```ts
|
|
390
504
|
import { loadRunner } from "env-runner";
|
|
391
505
|
|
|
392
|
-
|
|
506
|
+
await using runner = await loadRunner("node-worker", {
|
|
393
507
|
name: "my-app",
|
|
394
508
|
data: { entry: "./app.ts" },
|
|
395
509
|
});
|
|
@@ -448,7 +562,7 @@ The built-in worker automatically:
|
|
|
448
562
|
For advanced use cases, you can provide a custom worker entry:
|
|
449
563
|
|
|
450
564
|
```ts
|
|
451
|
-
|
|
565
|
+
await using runner = new NodeProcessEnvRunner({
|
|
452
566
|
name: "my-app",
|
|
453
567
|
workerEntry: "/path/to/custom-worker.ts",
|
|
454
568
|
data: { entry: "./app.ts" },
|
|
@@ -21,3 +21,22 @@ Repository: https://github.com/nodejs/cjs-module-lexer
|
|
|
21
21
|
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
22
22
|
>
|
|
23
23
|
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
---------------------------------------
|
|
26
|
+
|
|
27
|
+
## es-module-lexer
|
|
28
|
+
|
|
29
|
+
License: MIT
|
|
30
|
+
By: Guy Bedford
|
|
31
|
+
Repository: https://github.com/guybedford/es-module-lexer
|
|
32
|
+
|
|
33
|
+
> MIT License
|
|
34
|
+
> -----------
|
|
35
|
+
>
|
|
36
|
+
> Copyright (C) 2018-2022 Guy Bedford
|
|
37
|
+
>
|
|
38
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
39
|
+
>
|
|
40
|
+
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
41
|
+
>
|
|
42
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -1,18 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EnvRunner, RunnerMessageListener, WorkerAddress, WorkerHooks } from "./types.mjs";
|
|
2
2
|
import { IncomingMessage } from "node:http";
|
|
3
3
|
import { Socket } from "node:net";
|
|
4
|
+
/**
|
|
5
|
+
* Source for a virtual module: either a literal ES module string or a factory
|
|
6
|
+
* that returns one (sync or async).
|
|
7
|
+
*
|
|
8
|
+
* Factories are evaluated **once on the host side** before the worker is spawned
|
|
9
|
+
* (functions can't cross the `workerData`/`JSON` boundary, and Node's synchronous
|
|
10
|
+
* load hook can't await), so the worker always receives plain strings. See
|
|
11
|
+
* {@link resolveVirtualModules}.
|
|
12
|
+
*/
|
|
13
|
+
type VirtualModuleSource = string | (() => string | Promise<string>);
|
|
14
|
+
/** Virtual modules as a `specifier => source` map. */
|
|
15
|
+
type VirtualModules = Record<string, VirtualModuleSource>;
|
|
4
16
|
interface EnvRunnerData {
|
|
5
17
|
name?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Virtual modules as a `specifier => source` map.
|
|
20
|
+
*
|
|
21
|
+
* Registered as Node.js ESM customization hooks in the worker so the entry
|
|
22
|
+
* (and its dependencies) can `import` them, e.g.
|
|
23
|
+
* `{ "#virtual-import": "export const foo = 1" }`.
|
|
24
|
+
*
|
|
25
|
+
* Each source may be a string or a factory `() => string | Promise<string>`.
|
|
26
|
+
* Factories are evaluated once on the host before the worker is spawned (so the
|
|
27
|
+
* worker always receives plain strings).
|
|
28
|
+
*
|
|
29
|
+
* Supported by the `node-worker`, `node-process`, `bun-process`,
|
|
30
|
+
* `deno-process`, `vercel`, `netlify`, and `miniflare` runners.
|
|
31
|
+
*/
|
|
32
|
+
virtual?: VirtualModules;
|
|
6
33
|
[key: string]: unknown;
|
|
7
34
|
}
|
|
8
|
-
declare abstract class BaseEnvRunner implements EnvRunner {
|
|
35
|
+
declare abstract class BaseEnvRunner implements EnvRunner, AsyncDisposable {
|
|
9
36
|
closed: boolean;
|
|
10
37
|
protected _name: string;
|
|
11
38
|
protected _workerEntry: string;
|
|
12
39
|
protected _data?: EnvRunnerData;
|
|
40
|
+
protected _virtualSources?: VirtualModules;
|
|
13
41
|
protected _hooks: Partial<WorkerHooks>;
|
|
14
42
|
protected _address?: WorkerAddress;
|
|
15
43
|
protected _messageListeners: Set<(data: unknown) => void>;
|
|
44
|
+
protected _pendingRequests: Set<(cause?: unknown) => void>;
|
|
45
|
+
protected _virtualResolved?: Promise<void>;
|
|
16
46
|
constructor(opts: {
|
|
17
47
|
name: string;
|
|
18
48
|
workerEntry: string;
|
|
@@ -36,11 +66,58 @@ declare abstract class BaseEnvRunner implements EnvRunner {
|
|
|
36
66
|
timeout?: number;
|
|
37
67
|
}): Promise<T>;
|
|
38
68
|
reloadModule(timeout?: number): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Invalidate a virtual module so the next `reloadModule()` re-evaluates it.
|
|
71
|
+
* A factory-valued `data.virtual` source is re-run on the host and the fresh
|
|
72
|
+
* source is shipped to the worker along with the invalidation. Rejects when
|
|
73
|
+
* the specifier is not a registered virtual module.
|
|
74
|
+
*/
|
|
75
|
+
invalidateModule(specifier: string, timeout?: number): Promise<void>;
|
|
39
76
|
close(cause?: unknown): Promise<void>;
|
|
77
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Resolve a relative fetch input (e.g. `"/path"`) against a placeholder
|
|
80
|
+
* `http://localhost` origin so it parses as a full URL. The origin is a
|
|
81
|
+
* placeholder — requests are dispatched to the worker address regardless.
|
|
82
|
+
*/
|
|
83
|
+
protected _resolveFetchInput(input: string | URL | Request): string | URL | Request;
|
|
40
84
|
protected _handleMessage(message: any): void;
|
|
85
|
+
/**
|
|
86
|
+
* Send a message and await a matching response message. Shared by `rpc()`,
|
|
87
|
+
* `reloadModule()`, and `invalidateModule()`. Rejects on timeout, on a
|
|
88
|
+
* response carrying an `error`, and promptly when the runner closes mid-wait
|
|
89
|
+
* (instead of letting callers wait out the timeout on a dead worker).
|
|
90
|
+
*/
|
|
91
|
+
protected _request<T = unknown>(message: unknown, opts: {
|
|
92
|
+
match: (msg: any) => boolean;
|
|
93
|
+
timeout: number;
|
|
94
|
+
timeoutError: string;
|
|
95
|
+
send?: (message: unknown) => void;
|
|
96
|
+
}): Promise<T>;
|
|
97
|
+
/**
|
|
98
|
+
* Resolve any factory-valued `data.virtual` sources to strings before the
|
|
99
|
+
* worker is spawned. Returns a pending promise only when there is async work
|
|
100
|
+
* to do (a factory is present); otherwise returns `undefined` so subclasses can
|
|
101
|
+
* keep their synchronous spawn path. Factories must be resolved here because
|
|
102
|
+
* functions can't cross the worker boundary and the load hook can't await.
|
|
103
|
+
*/
|
|
104
|
+
protected _resolveVirtualData(): Promise<void> | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Re-run a factory-valued virtual source on the host and sync the resolved
|
|
107
|
+
* `data.virtual` map. Returns the fresh source, or `undefined` when the
|
|
108
|
+
* source is a plain string or unknown (nothing to re-evaluate).
|
|
109
|
+
*/
|
|
110
|
+
protected _refreshVirtualSource(specifier: string): Promise<string | undefined>;
|
|
111
|
+
/**
|
|
112
|
+
* Run a subclass spawn callback after `data.virtual` is resolved.
|
|
113
|
+
* Synchronous when no factory-valued source is present; otherwise defers
|
|
114
|
+
* `init` until factories resolve. A throwing/rejecting factory closes the
|
|
115
|
+
* runner with the error as cause instead of leaving an unhandled rejection.
|
|
116
|
+
*/
|
|
117
|
+
protected _initWithVirtualData(init: () => void): void;
|
|
41
118
|
protected _closeSocket(): Promise<void>;
|
|
42
119
|
protected abstract _hasRuntime(): boolean;
|
|
43
120
|
protected abstract _closeRuntime(): Promise<void>;
|
|
44
121
|
protected abstract _runtimeType(): string;
|
|
45
122
|
}
|
|
46
|
-
export { EnvRunnerData
|
|
123
|
+
export { BaseEnvRunner, EnvRunnerData, VirtualModuleSource, VirtualModules };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveVirtualModules } from "./virtual-loader.mjs";
|
|
1
2
|
import { rm } from "node:fs/promises";
|
|
2
3
|
import { proxyFetch, proxyUpgrade } from "httpxy";
|
|
3
4
|
var BaseEnvRunner = class {
|
|
@@ -5,15 +6,19 @@ var BaseEnvRunner = class {
|
|
|
5
6
|
_name;
|
|
6
7
|
_workerEntry;
|
|
7
8
|
_data;
|
|
9
|
+
_virtualSources;
|
|
8
10
|
_hooks;
|
|
9
11
|
_address;
|
|
10
12
|
_messageListeners;
|
|
13
|
+
_pendingRequests;
|
|
14
|
+
_virtualResolved;
|
|
11
15
|
constructor(opts) {
|
|
12
16
|
this._name = opts.name;
|
|
13
17
|
this._workerEntry = opts.workerEntry;
|
|
14
18
|
this._data = opts.data;
|
|
15
19
|
this._hooks = opts.hooks || {};
|
|
16
20
|
this._messageListeners = /* @__PURE__ */ new Set();
|
|
21
|
+
this._pendingRequests = /* @__PURE__ */ new Set();
|
|
17
22
|
}
|
|
18
23
|
get ready() {
|
|
19
24
|
return Boolean(!this.closed && this._address && this._hasRuntime());
|
|
@@ -21,7 +26,7 @@ var BaseEnvRunner = class {
|
|
|
21
26
|
async fetch(input, init) {
|
|
22
27
|
for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
|
|
23
28
|
if (!this._address) return new Response(`${this._runtimeType()} env runner is unavailable`, { status: 503 });
|
|
24
|
-
return proxyFetch(this._address, input, init);
|
|
29
|
+
return proxyFetch(this._address, this._resolveFetchInput(input), init);
|
|
25
30
|
}
|
|
26
31
|
async upgrade(context) {
|
|
27
32
|
if (!this.ready || !this._address) return;
|
|
@@ -35,6 +40,7 @@ var BaseEnvRunner = class {
|
|
|
35
40
|
}
|
|
36
41
|
waitForReady(timeout = 5e3) {
|
|
37
42
|
if (this.ready) return Promise.resolve();
|
|
43
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
|
|
38
44
|
return new Promise((resolve, reject) => {
|
|
39
45
|
const timer = setTimeout(() => {
|
|
40
46
|
this._messageListeners.delete(listener);
|
|
@@ -45,6 +51,10 @@ var BaseEnvRunner = class {
|
|
|
45
51
|
clearTimeout(timer);
|
|
46
52
|
this._messageListeners.delete(listener);
|
|
47
53
|
resolve();
|
|
54
|
+
} else if (this.closed) {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
this._messageListeners.delete(listener);
|
|
57
|
+
reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
|
|
48
58
|
}
|
|
49
59
|
};
|
|
50
60
|
this._messageListeners.add(listener);
|
|
@@ -52,76 +62,130 @@ var BaseEnvRunner = class {
|
|
|
52
62
|
}
|
|
53
63
|
rpc(name, data, opts) {
|
|
54
64
|
const id = Math.random().toString(36).slice(2);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
|
|
65
|
-
else resolve(msg.data);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
const cleanup = () => {
|
|
69
|
-
clearTimeout(timer);
|
|
70
|
-
this.offMessage(listener);
|
|
71
|
-
};
|
|
72
|
-
this.onMessage(listener);
|
|
73
|
-
this.sendMessage({
|
|
74
|
-
__rpc: name,
|
|
75
|
-
__rpc_id: id,
|
|
76
|
-
data
|
|
77
|
-
});
|
|
78
|
-
});
|
|
65
|
+
return this._request({
|
|
66
|
+
__rpc: name,
|
|
67
|
+
__rpc_id: id,
|
|
68
|
+
data
|
|
69
|
+
}, {
|
|
70
|
+
match: (msg) => msg?.__rpc_id === id,
|
|
71
|
+
timeout: opts?.timeout ?? 3e3,
|
|
72
|
+
timeoutError: `RPC "${name}" timed out`
|
|
73
|
+
}).then((msg) => msg.data);
|
|
79
74
|
}
|
|
80
75
|
async reloadModule(timeout = 5e3) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
this.onMessage(listener);
|
|
98
|
-
this.sendMessage({ event: "reload-module" });
|
|
76
|
+
await this._request({ event: "reload-module" }, {
|
|
77
|
+
match: (msg) => msg?.event === "module-reloaded",
|
|
78
|
+
timeout,
|
|
79
|
+
timeoutError: "Module reload timed out"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async invalidateModule(specifier, timeout = 5e3) {
|
|
83
|
+
const source = await this._refreshVirtualSource(specifier);
|
|
84
|
+
await this._request({
|
|
85
|
+
event: "invalidate-module",
|
|
86
|
+
specifier,
|
|
87
|
+
source
|
|
88
|
+
}, {
|
|
89
|
+
match: (msg) => msg?.event === "module-invalidated" && msg.specifier === specifier,
|
|
90
|
+
timeout,
|
|
91
|
+
timeoutError: `Module invalidation timed out for "${specifier}"`
|
|
99
92
|
});
|
|
100
93
|
}
|
|
101
94
|
async close(cause) {
|
|
102
95
|
if (this.closed) return;
|
|
103
96
|
this.closed = true;
|
|
97
|
+
for (const rejectPending of this._pendingRequests) rejectPending(cause);
|
|
98
|
+
this._pendingRequests.clear();
|
|
104
99
|
this._hooks.onClose?.(this, cause);
|
|
105
100
|
this._hooks = {};
|
|
106
101
|
const onError = (error) => console.error(error);
|
|
107
102
|
await this._closeRuntime().catch(onError);
|
|
108
103
|
await this._closeSocket().catch(onError);
|
|
109
104
|
}
|
|
105
|
+
async [Symbol.asyncDispose]() {
|
|
106
|
+
await this.close();
|
|
107
|
+
}
|
|
110
108
|
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
111
109
|
const status = this.closed ? "closed" : this.ready ? "ready" : "pending";
|
|
112
110
|
return `${this.constructor.name}#${this._name}(${status})`;
|
|
113
111
|
}
|
|
112
|
+
_resolveFetchInput(input) {
|
|
113
|
+
if (typeof input === "string" && !URL.canParse(input)) return new URL(input, "http://localhost");
|
|
114
|
+
return input;
|
|
115
|
+
}
|
|
114
116
|
_handleMessage(message) {
|
|
115
117
|
if (message?.address) {
|
|
116
118
|
this._address = message.address;
|
|
117
119
|
this._hooks.onReady?.(this, this._address);
|
|
118
120
|
}
|
|
121
|
+
if (message?.event === "init-error" && !this.ready && !this.closed) this.close(new Error(String(message.error || "Worker initialization failed")));
|
|
119
122
|
for (const listener of this._messageListeners) listener(message);
|
|
120
123
|
}
|
|
124
|
+
_request(message, opts) {
|
|
125
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner is closed"));
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
cleanup();
|
|
129
|
+
reject(new Error(opts.timeoutError));
|
|
130
|
+
}, opts.timeout);
|
|
131
|
+
const listener = (msg) => {
|
|
132
|
+
if (opts.match(msg)) {
|
|
133
|
+
cleanup();
|
|
134
|
+
if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
|
|
135
|
+
else resolve(msg);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const onClose = (cause) => {
|
|
139
|
+
cleanup();
|
|
140
|
+
reject(new Error("Runner closed before responding", cause ? { cause } : void 0));
|
|
141
|
+
};
|
|
142
|
+
const cleanup = () => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
this.offMessage(listener);
|
|
145
|
+
this._pendingRequests.delete(onClose);
|
|
146
|
+
};
|
|
147
|
+
this.onMessage(listener);
|
|
148
|
+
this._pendingRequests.add(onClose);
|
|
149
|
+
try {
|
|
150
|
+
(opts.send ?? ((m) => this.sendMessage(m)))(message);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
cleanup();
|
|
153
|
+
reject(error);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
_resolveVirtualData() {
|
|
158
|
+
const virtual = this._data?.virtual;
|
|
159
|
+
this._virtualSources = virtual;
|
|
160
|
+
if (!virtual || !Object.values(virtual).some((v) => typeof v === "function")) return;
|
|
161
|
+
this._virtualResolved = resolveVirtualModules(virtual).then((resolved) => {
|
|
162
|
+
this._data = {
|
|
163
|
+
...this._data,
|
|
164
|
+
virtual: resolved
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
return this._virtualResolved;
|
|
168
|
+
}
|
|
169
|
+
async _refreshVirtualSource(specifier) {
|
|
170
|
+
await this._virtualResolved?.catch(() => {});
|
|
171
|
+
const original = this._virtualSources?.[specifier];
|
|
172
|
+
if (typeof original !== "function") return;
|
|
173
|
+
const source = await original();
|
|
174
|
+
const resolved = this._data?.virtual;
|
|
175
|
+
if (resolved) resolved[specifier] = source;
|
|
176
|
+
return source;
|
|
177
|
+
}
|
|
178
|
+
_initWithVirtualData(init) {
|
|
179
|
+
const pending = this._resolveVirtualData();
|
|
180
|
+
if (pending) pending.then(() => {
|
|
181
|
+
if (!this.closed) init();
|
|
182
|
+
}, (error) => this.close(error));
|
|
183
|
+
else init();
|
|
184
|
+
}
|
|
121
185
|
async _closeSocket() {
|
|
122
186
|
const socketPath = this._address?.socketPath;
|
|
123
187
|
if (socketPath && socketPath[0] !== "\0" && !socketPath.startsWith(String.raw`\\.\\pipe`)) await rm(socketPath).catch(() => {});
|
|
124
188
|
this._address = void 0;
|
|
125
189
|
}
|
|
126
190
|
};
|
|
127
|
-
export { BaseEnvRunner
|
|
191
|
+
export { BaseEnvRunner };
|