env-runner 0.1.6 → 0.1.7

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
@@ -7,7 +7,7 @@
7
7
 
8
8
  <!-- /automd -->
9
9
 
10
- Generic environment runner for JavaScript runtimes. Run your server apps across Node.js worker threads, child processes, Bun, Deno, Cloudflare Workers (via miniflare), or in-process — with hot-reload, WebSocket proxying, and bidirectional messaging.
10
+ Generic environment runner for JavaScript runtimes. Run your server apps across Node.js worker threads, child processes, Bun, Deno, Cloudflare Workers (via miniflare), Vercel, Netlify, or in-process — with hot-reload, WebSocket proxying, and bidirectional messaging.
11
11
 
12
12
  ## Usage
13
13
 
@@ -120,6 +120,8 @@ import { BunProcessEnvRunner } from "env-runner/runners/bun-process";
120
120
  import { DenoProcessEnvRunner } from "env-runner/runners/deno-process";
121
121
  import { SelfEnvRunner } from "env-runner/runners/self";
122
122
  import { MiniflareEnvRunner } from "env-runner/runners/miniflare";
123
+ import { VercelEnvRunner } from "env-runner/runners/vercel";
124
+ import { NetlifyEnvRunner } from "env-runner/runners/netlify";
123
125
  ```
124
126
 
125
127
  All runners implement the [`EnvRunner`](./src/types.ts) interface:
@@ -160,14 +162,16 @@ await runner.close();
160
162
 
161
163
  **Available runners:**
162
164
 
163
- | Runner | Isolation | IPC mechanism |
164
- | ---------------------- | ------------------------------ | ---------------------------------- |
165
- | `NodeWorkerEnvRunner` | Worker thread | `workerData` / `parentPort` |
166
- | `NodeProcessEnvRunner` | Child process (`fork`) | `ENV_RUNNER_DATA` / `process.send` |
167
- | `BunProcessEnvRunner` | Bun or Node.js process | `Bun.spawn` IPC or `fork()` |
168
- | `DenoProcessEnvRunner` | Deno process | `deno run` with IPC channel |
169
- | `SelfEnvRunner` | In-process | In-memory channel |
170
- | `MiniflareEnvRunner` | Cloudflare Workers (miniflare) | WebSocket pair via `dispatchFetch` |
165
+ | Runner | Isolation | IPC mechanism |
166
+ | ---------------------- | ------------------------------- | ---------------------------------- |
167
+ | `NodeWorkerEnvRunner` | Worker thread | `workerData` / `parentPort` |
168
+ | `NodeProcessEnvRunner` | Child process (`fork`) | `ENV_RUNNER_DATA` / `process.send` |
169
+ | `BunProcessEnvRunner` | Bun or Node.js process | `Bun.spawn` IPC or `fork()` |
170
+ | `DenoProcessEnvRunner` | Deno process | `deno run` with IPC channel |
171
+ | `SelfEnvRunner` | In-process | In-memory channel |
172
+ | `MiniflareEnvRunner` | Cloudflare Workers (miniflare) | WebSocket pair via `dispatchFetch` |
173
+ | `VercelEnvRunner` | Worker thread (Vercel context) | `workerData` / `parentPort` |
174
+ | `NetlifyEnvRunner` | Worker thread (Netlify context) | `workerData` / `parentPort` |
171
175
 
172
176
  #### Miniflare Runner
173
177
 
@@ -289,6 +293,32 @@ const runner2 = new MiniflareEnvRunner({
289
293
  // Fully destroy: runner.dispose() or MiniflareEnvRunner.disposeAll()
290
294
  ```
291
295
 
296
+ #### Vercel Runner
297
+
298
+ Simulates a Vercel deployment environment with automatic header injection (`x-vercel-deployment-url`, `x-vercel-forwarded-for`, forwarding headers) and global context.
299
+
300
+ ```ts
301
+ import { VercelEnvRunner } from "env-runner/runners/vercel";
302
+
303
+ const runner = new VercelEnvRunner({
304
+ name: "my-app",
305
+ data: { entry: "./app.ts" },
306
+ });
307
+ ```
308
+
309
+ #### Netlify Runner
310
+
311
+ Simulates a Netlify deployment environment with automatic header injection (`x-nf-client-connection-ip`, `x-nf-account-id`, `x-nf-site-id`, `x-nf-deploy-id`, `x-nf-deploy-context`, `x-nf-geo`, `x-nf-request-id`, forwarding headers) and `globalThis.Netlify` setup:
312
+
313
+ ```ts
314
+ import { NetlifyEnvRunner } from "env-runner/runners/netlify";
315
+
316
+ const runner = new NetlifyEnvRunner({
317
+ name: "my-app",
318
+ data: { entry: "./app.ts" },
319
+ });
320
+ ```
321
+
292
322
  ### Vite Environment API
293
323
 
294
324
  env-runner provides helpers for integrating with Vite's [Environment API](https://vite.dev/guide/api-environment-runtimes.html):
@@ -44,8 +44,9 @@ var DenoProcessEnvRunner = class extends BaseEnvRunner {
44
44
  };
45
45
  const child = spawn("deno", [
46
46
  "run",
47
- "--allow-all",
47
+ "-A",
48
48
  "--node-modules-dir=auto",
49
+ "--no-lock",
49
50
  ...execArgv || [],
50
51
  this._workerEntry
51
52
  ], {
@@ -53,8 +54,7 @@ var DenoProcessEnvRunner = class extends BaseEnvRunner {
53
54
  stdio: [
54
55
  "pipe",
55
56
  "pipe",
56
- "pipe",
57
- "ipc"
57
+ "pipe"
58
58
  ]
59
59
  });
60
60
  const exited = new Promise((resolve) => {
@@ -63,7 +63,9 @@ var DenoProcessEnvRunner = class extends BaseEnvRunner {
63
63
  const handle = {
64
64
  pid: child.pid,
65
65
  kill: () => child.kill(),
66
- send: (message) => child.send(message),
66
+ send: (message) => {
67
+ child.stdin.write(JSON.stringify(message) + "\n");
68
+ },
67
69
  exited,
68
70
  _exitCode: void 0,
69
71
  removeAllListeners: () => child.removeAllListeners()
@@ -78,8 +80,17 @@ var DenoProcessEnvRunner = class extends BaseEnvRunner {
78
80
  this.close(error);
79
81
  }
80
82
  });
81
- child.on("message", (message) => {
82
- this._handleMessage(message);
83
+ let buffer = "";
84
+ child.stdout.on("data", (chunk) => {
85
+ buffer += chunk.toString();
86
+ let newlineIdx;
87
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
88
+ const line = buffer.slice(0, newlineIdx);
89
+ buffer = buffer.slice(newlineIdx + 1);
90
+ if (line.startsWith("{")) try {
91
+ this._handleMessage(JSON.parse(line));
92
+ } catch {}
93
+ }
83
94
  });
84
95
  this.#process = handle;
85
96
  }
@@ -46,6 +46,14 @@ interface MiniflareEnvRunnerOptions {
46
46
  persistent?: boolean;
47
47
  /** Wrap the user's `fetch` in a try/catch that returns structured JSON error responses. Default: `true`. */
48
48
  captureErrors?: boolean;
49
+ /**
50
+ * Export conditions for bare-specifier module resolution in the module
51
+ * fallback service. Ensures packages with conditional exports (e.g.
52
+ * `"workerd"`) resolve to the correct entry instead of the Node.js one.
53
+ *
54
+ * Defaults to `["workerd", "worker"]`.
55
+ */
56
+ exportConditions?: string[];
49
57
  }
50
58
  declare class MiniflareEnvRunner extends BaseEnvRunner {
51
59
  #private;
@@ -5,6 +5,7 @@ import { proxyUpgrade } from "httpxy";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { basename, dirname, resolve } from "node:path";
8
+ import { resolveModulePath } from "exsolve";
8
9
  //#region src/runners/miniflare/wrapper.ts
9
10
  const IPC_PATH$1 = "/__env_runner_ipc";
10
11
  /** Service binding name used for cross-request IPC (worker → runner). */
@@ -207,6 +208,7 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
207
208
  #cacheKey;
208
209
  #exports;
209
210
  #captureErrors;
211
+ #exportConditions;
210
212
  constructor(opts) {
211
213
  super({
212
214
  ...opts,
@@ -217,6 +219,7 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
217
219
  this.#persistent = opts.persistent ?? false;
218
220
  this.#exports = opts.exports ?? {};
219
221
  this.#captureErrors = opts.captureErrors ?? true;
222
+ this.#exportConditions = opts.exportConditions ?? ["workerd", "worker"];
220
223
  this.#init();
221
224
  }
222
225
  /** Dispose all persistent Miniflare instances from the cache. */
@@ -399,6 +402,7 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
399
402
  if (!options.unsafeModuleFallbackService) {
400
403
  const _require = createRequire(resolvedEntry);
401
404
  const _transformRequest = this.#transformRequest;
405
+ const _exportConditions = this.#exportConditions;
402
406
  options.unsafeUseModuleFallbackService = true;
403
407
  const modulePathMap = /* @__PURE__ */ new Map();
404
408
  const _cjsLexerReady = ensureCjsLexer();
@@ -422,6 +426,7 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
422
426
  const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
423
427
  const referrerReal = modulePathMap.get(referrerKey);
424
428
  const contextRequire = referrerReal ? createRequire(referrerReal) : _require;
429
+ if (cleanRaw.startsWith("cloudflare:")) return new Response(null, { status: 404 });
425
430
  if (cleanRaw.startsWith("node:")) {
426
431
  const nodeName = cleanRaw.slice(5);
427
432
  try {
@@ -430,7 +435,11 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
430
435
  return new Response(null, { status: 404 });
431
436
  }
432
437
  } else try {
433
- resolvedPath = contextRequire.resolve(cleanRaw);
438
+ resolvedPath = resolveModulePath(cleanRaw, {
439
+ from: referrerReal || resolvedEntry,
440
+ conditions: _exportConditions,
441
+ try: true
442
+ }) || contextRequire.resolve(cleanRaw);
434
443
  } catch {
435
444
  const name = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
436
445
  return Response.json({
@@ -486,7 +495,10 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
486
495
  }
487
496
  }
488
497
  if (this.#persistent && entryPath) {
489
- this.#cacheKey = computeCacheKey(entryPath, this.#miniflareOptions);
498
+ this.#cacheKey = computeCacheKey(entryPath, {
499
+ ...this.#miniflareOptions,
500
+ _exportConditions: this.#exportConditions
501
+ });
490
502
  const cached = _miniflareCache.get(this.#cacheKey);
491
503
  if (cached) {
492
504
  this.#miniflare = cached.mf;
@@ -0,0 +1,19 @@
1
+ import { u as WorkerHooks } from "./types.mjs";
2
+ import { n as EnvRunnerData, t as BaseEnvRunner } from "./base-runner.mjs";
3
+
4
+ //#region src/runners/node-worker/runner.d.ts
5
+ declare class NodeWorkerEnvRunner extends BaseEnvRunner {
6
+ #private;
7
+ constructor(opts: {
8
+ name: string;
9
+ workerEntry?: string;
10
+ hooks?: WorkerHooks;
11
+ data?: EnvRunnerData;
12
+ });
13
+ sendMessage(message: unknown): void;
14
+ protected _hasRuntime(): boolean;
15
+ protected _runtimeType(): string;
16
+ protected _closeRuntime(): Promise<void>;
17
+ }
18
+ //#endregion
19
+ export { NodeWorkerEnvRunner as t };
@@ -0,0 +1,62 @@
1
+ import { t as BaseEnvRunner } from "./base-runner.mjs";
2
+ import { existsSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Worker } from "node:worker_threads";
5
+ //#region src/runners/node-worker/runner.ts
6
+ let _defaultEntry;
7
+ var NodeWorkerEnvRunner = class extends BaseEnvRunner {
8
+ #worker;
9
+ constructor(opts) {
10
+ _defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/node-worker/worker"));
11
+ super({
12
+ ...opts,
13
+ workerEntry: opts.workerEntry || _defaultEntry
14
+ });
15
+ this.#initWorker();
16
+ }
17
+ sendMessage(message) {
18
+ if (!this.#worker) throw new Error("Worker thread should be initialized before sending messages.");
19
+ this.#worker.postMessage(message);
20
+ }
21
+ _hasRuntime() {
22
+ return Boolean(this.#worker);
23
+ }
24
+ _runtimeType() {
25
+ return "worker";
26
+ }
27
+ async _closeRuntime() {
28
+ if (!this.#worker) return;
29
+ this.#worker.removeAllListeners();
30
+ await this.#worker.terminate().catch((error) => {
31
+ console.error(error);
32
+ });
33
+ this.#worker = void 0;
34
+ }
35
+ #initWorker() {
36
+ if (!existsSync(this._workerEntry)) {
37
+ this.close(`worker entry not found in "${this._workerEntry}".`);
38
+ return;
39
+ }
40
+ const worker = new Worker(this._workerEntry, {
41
+ env: { ...process.env },
42
+ workerData: {
43
+ name: this._name,
44
+ ...this._data
45
+ }
46
+ });
47
+ worker.once("exit", (code) => {
48
+ worker._exitCode = code;
49
+ this.close(`worker exited with code ${code}`);
50
+ });
51
+ worker.once("error", (error) => {
52
+ console.error(`Worker error:`, error);
53
+ this.close(error);
54
+ });
55
+ worker.on("message", (message) => {
56
+ this._handleMessage(message);
57
+ });
58
+ this.#worker = worker;
59
+ }
60
+ };
61
+ //#endregion
62
+ export { NodeWorkerEnvRunner as t };
@@ -0,0 +1,17 @@
1
+ import { u as WorkerHooks } from "./types.mjs";
2
+ import { n as EnvRunnerData } from "./base-runner.mjs";
3
+ import { t as NodeWorkerEnvRunner } from "./runner3.mjs";
4
+
5
+ //#region src/runners/vercel/runner.d.ts
6
+ declare class VercelEnvRunner extends NodeWorkerEnvRunner {
7
+ constructor(opts: {
8
+ name: string;
9
+ workerEntry?: string;
10
+ hooks?: WorkerHooks;
11
+ data?: EnvRunnerData;
12
+ });
13
+ fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
14
+ protected _runtimeType(): string;
15
+ }
16
+ //#endregion
17
+ export { VercelEnvRunner as t };
@@ -0,0 +1,42 @@
1
+ import { t as NodeWorkerEnvRunner } from "./runner3.mjs";
2
+ import { fileURLToPath } from "node:url";
3
+ //#region src/runners/vercel/runner.ts
4
+ let _defaultEntry;
5
+ var VercelEnvRunner = class extends NodeWorkerEnvRunner {
6
+ constructor(opts) {
7
+ _defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/vercel/worker"));
8
+ super({
9
+ ...opts,
10
+ workerEntry: opts.workerEntry || _defaultEntry
11
+ });
12
+ }
13
+ async fetch(input, init) {
14
+ const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
15
+ if (this._address && this._address.port != null && !headers.has("x-vercel-deployment-url")) {
16
+ const host = this._address.host || "127.0.0.1";
17
+ headers.set("x-vercel-deployment-url", `http://${host}:${this._address.port}`);
18
+ }
19
+ const clientIp = headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || "127.0.0.1";
20
+ if (!headers.has("x-vercel-forwarded-for")) headers.set("x-vercel-forwarded-for", clientIp);
21
+ if (!headers.has("x-forwarded-for")) headers.set("x-forwarded-for", clientIp);
22
+ if (!headers.has("x-real-ip")) headers.set("x-real-ip", clientIp);
23
+ try {
24
+ const url = new URL(input instanceof Request ? input.url : input.toString());
25
+ if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
26
+ if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", headers.get("host") || url.host);
27
+ } catch {}
28
+ if (input instanceof Request) return super.fetch(new Request(input, {
29
+ ...init,
30
+ headers
31
+ }));
32
+ return super.fetch(input, {
33
+ ...init,
34
+ headers
35
+ });
36
+ }
37
+ _runtimeType() {
38
+ return "vercel";
39
+ }
40
+ };
41
+ //#endregion
42
+ export { VercelEnvRunner as t };
@@ -0,0 +1,17 @@
1
+ import { u as WorkerHooks } from "./types.mjs";
2
+ import { n as EnvRunnerData } from "./base-runner.mjs";
3
+ import { t as NodeWorkerEnvRunner } from "./runner3.mjs";
4
+
5
+ //#region src/runners/netlify/runner.d.ts
6
+ declare class NetlifyEnvRunner extends NodeWorkerEnvRunner {
7
+ constructor(opts: {
8
+ name: string;
9
+ workerEntry?: string;
10
+ hooks?: WorkerHooks;
11
+ data?: EnvRunnerData;
12
+ });
13
+ fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
14
+ protected _runtimeType(): string;
15
+ }
16
+ //#endregion
17
+ export { NetlifyEnvRunner as t };
@@ -0,0 +1,47 @@
1
+ import { t as NodeWorkerEnvRunner } from "./runner3.mjs";
2
+ import { fileURLToPath } from "node:url";
3
+ //#region src/runners/netlify/runner.ts
4
+ let _defaultEntry;
5
+ var NetlifyEnvRunner = class extends NodeWorkerEnvRunner {
6
+ constructor(opts) {
7
+ _defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/netlify/worker"));
8
+ super({
9
+ ...opts,
10
+ workerEntry: opts.workerEntry || _defaultEntry
11
+ });
12
+ }
13
+ async fetch(input, init) {
14
+ const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
15
+ const clientIp = headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || "127.0.0.1";
16
+ if (!headers.has("x-nf-client-connection-ip")) headers.set("x-nf-client-connection-ip", clientIp);
17
+ if (!headers.has("x-nf-account-id")) headers.set("x-nf-account-id", "0");
18
+ if (!headers.has("x-nf-site-id")) headers.set("x-nf-site-id", "0");
19
+ if (!headers.has("x-nf-deploy-id")) headers.set("x-nf-deploy-id", "0");
20
+ if (!headers.has("x-nf-deploy-context")) headers.set("x-nf-deploy-context", "dev");
21
+ if (!headers.has("x-nf-geo")) headers.set("x-nf-geo", btoa(JSON.stringify({
22
+ city: "localhost",
23
+ country: { code: "dev" }
24
+ })));
25
+ if (!headers.has("x-nf-request-id")) headers.set("x-nf-request-id", crypto.randomUUID());
26
+ if (!headers.has("x-forwarded-for")) headers.set("x-forwarded-for", clientIp);
27
+ if (!headers.has("x-real-ip")) headers.set("x-real-ip", clientIp);
28
+ try {
29
+ const url = new URL(input instanceof Request ? input.url : input.toString());
30
+ if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
31
+ if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", headers.get("host") || url.host);
32
+ } catch {}
33
+ if (input instanceof Request) return super.fetch(new Request(input, {
34
+ ...init,
35
+ headers
36
+ }));
37
+ return super.fetch(input, {
38
+ ...init,
39
+ headers
40
+ });
41
+ }
42
+ _runtimeType() {
43
+ return "netlify";
44
+ }
45
+ };
46
+ //#endregion
47
+ export { NetlifyEnvRunner as t };
@@ -175,7 +175,9 @@ const loaders = {
175
175
  "bun-process": () => import("env-runner/runners/bun-process").then((m) => m.BunProcessEnvRunner),
176
176
  "deno-process": () => import("env-runner/runners/deno-process").then((m) => m.DenoProcessEnvRunner),
177
177
  self: () => import("env-runner/runners/self").then((m) => m.SelfEnvRunner),
178
- miniflare: () => import("env-runner/runners/miniflare").then((m) => m.MiniflareEnvRunner)
178
+ miniflare: () => import("env-runner/runners/miniflare").then((m) => m.MiniflareEnvRunner),
179
+ vercel: () => import("env-runner/runners/vercel").then((m) => m.VercelEnvRunner),
180
+ netlify: () => import("env-runner/runners/netlify").then((m) => m.NetlifyEnvRunner)
179
181
  };
180
182
  async function loadRunner(runner, opts) {
181
183
  return new (await (loaders[runner]()))(opts);
package/dist/index.d.mts CHANGED
@@ -2,6 +2,8 @@ import { a as RunnerMessageListener, c as UpgradeHandler, i as RPCOptions, l as
2
2
  import { n as EnvRunnerData, t as BaseEnvRunner } from "./_chunks/base-runner.mjs";
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
+ import { t as VercelEnvRunner } from "./_chunks/runner4.mjs";
6
+ import { t as NetlifyEnvRunner } from "./_chunks/runner5.mjs";
5
7
  import { ServerOptions } from "srvx";
6
8
  import { Hooks } from "crossws";
7
9
 
@@ -46,13 +48,15 @@ declare class RunnerManager implements EnvRunner {
46
48
  }
47
49
  //#endregion
48
50
  //#region src/loader.d.ts
49
- type RunnerName = "node-worker" | "node-process" | "bun-process" | "deno-process" | "self" | "miniflare";
51
+ type RunnerName = "node-worker" | "node-process" | "bun-process" | "deno-process" | "self" | "miniflare" | "vercel" | "netlify";
50
52
  interface LoadRunnerOptions {
51
53
  name: string;
52
54
  workerEntry?: string;
53
55
  hooks?: WorkerHooks;
54
56
  data?: EnvRunnerData;
55
57
  execArgv?: string[];
58
+ /** Additional runner-specific options (passed through to the runner constructor). */
59
+ [key: string]: unknown;
56
60
  }
57
61
  declare function loadRunner(runner: RunnerName, opts: LoadRunnerOptions): Promise<EnvRunner>;
58
62
  //#endregion
@@ -113,4 +117,4 @@ interface AppEntry {
113
117
  ipc?: AppEntryIPC;
114
118
  }
115
119
  //#endregion
116
- export { type AppEntry, type AppEntryIPC, type AppEntryIPCContext, BaseEnvRunner, DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData, type EnvRunnerData, type EnvRunner, EnvServer, type EnvServerOptions, type FetchHandler, type LoadRunnerOptions, MiniflareEnvRunner, type MiniflareEnvRunnerOptions, type MiniflareExportInfo, type NodeUpgradeContext, type RPCOptions, RunnerManager, type RunnerMessageListener, type RunnerName, type RunnerRPCHooks, type TransformResult, type UpgradeContext, type UpgradeHandler, type WorkerAddress, type WorkerHooks, loadRunner };
120
+ export { type AppEntry, type AppEntryIPC, type AppEntryIPCContext, BaseEnvRunner, DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData, type EnvRunnerData, type EnvRunner, EnvServer, type EnvServerOptions, type FetchHandler, type LoadRunnerOptions, MiniflareEnvRunner, type MiniflareEnvRunnerOptions, type MiniflareExportInfo, NetlifyEnvRunner, type NodeUpgradeContext, type RPCOptions, RunnerManager, type RunnerMessageListener, type RunnerName, type RunnerRPCHooks, type TransformResult, type UpgradeContext, type UpgradeHandler, VercelEnvRunner, type WorkerAddress, type WorkerHooks, loadRunner };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { t as BaseEnvRunner } from "./_chunks/base-runner.mjs";
2
2
  import { n as loadRunner, r as RunnerManager, t as EnvServer } from "./_chunks/server.mjs";
3
3
  import { t as DenoProcessEnvRunner } from "./_chunks/runner.mjs";
4
- import "./_chunks/libs/cjs-module-lexer.mjs";
5
4
  import { t as MiniflareEnvRunner } from "./_chunks/runner2.mjs";
6
- export { BaseEnvRunner, DenoProcessEnvRunner, EnvServer, MiniflareEnvRunner, RunnerManager, loadRunner };
5
+ import { t as VercelEnvRunner } from "./_chunks/runner4.mjs";
6
+ import { t as NetlifyEnvRunner } from "./_chunks/runner5.mjs";
7
+ export { BaseEnvRunner, DenoProcessEnvRunner, EnvServer, MiniflareEnvRunner, NetlifyEnvRunner, RunnerManager, VercelEnvRunner, loadRunner };
@@ -1,3 +1,2 @@
1
- import "../../_chunks/base-runner.mjs";
2
1
  import { t as DenoProcessEnvRunner } from "../../_chunks/runner.mjs";
3
2
  export { DenoProcessEnvRunner };
@@ -4,7 +4,9 @@ import { plugin } from "crossws/server";
4
4
  //#region src/runners/deno-process/worker.ts
5
5
  const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}");
6
6
  let entry = await resolveEntry(data.entry);
7
- const sendMessage = (message) => process.send(message);
7
+ const _stdout = globalThis.Deno?.stdout ? { write: (s) => globalThis.Deno.stdout.writeSync(new TextEncoder().encode(s)) } : process.stdout;
8
+ const sendMessage = (message) => _stdout.write(JSON.stringify(message) + "\n");
9
+ const _stdin = globalThis.Deno?.stdin?.readable || process.stdin;
8
10
  const server = serve({
9
11
  port: 0,
10
12
  hostname: "127.0.0.1",
@@ -23,20 +25,40 @@ if (entry.upgrade) server.node?.server?.on("upgrade", (req, socket, head) => {
23
25
  } });
24
26
  });
25
27
  if (entry.ipc) await entry.ipc.onOpen?.({ sendMessage });
26
- process.send({ address: parseServerAddress(server) });
27
- process.on("message", async (message) => {
28
+ sendMessage({ address: parseServerAddress(server) });
29
+ async function readMessages() {
30
+ const decoder = new TextDecoder();
31
+ let buffer = "";
32
+ for await (const chunk of _stdin) {
33
+ buffer += decoder.decode(chunk, { stream: true });
34
+ let newlineIdx;
35
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
36
+ const line = buffer.slice(0, newlineIdx);
37
+ buffer = buffer.slice(newlineIdx + 1);
38
+ if (!line) continue;
39
+ let message;
40
+ try {
41
+ message = JSON.parse(line);
42
+ } catch {
43
+ continue;
44
+ }
45
+ await handleMessage(message);
46
+ }
47
+ }
48
+ }
49
+ async function handleMessage(message) {
28
50
  if (message?.event === "shutdown") {
29
51
  Promise.resolve(entry.ipc?.onClose?.()).then(() => server.close()).then(() => {
30
- process.send({ event: "exit" });
52
+ sendMessage({ event: "exit" });
31
53
  });
32
54
  return;
33
55
  }
34
56
  if (message?.event === "reload-module") {
35
57
  try {
36
58
  entry = await reloadEntryModule(data.entry, entry, sendMessage);
37
- process.send({ event: "module-reloaded" });
59
+ sendMessage({ event: "module-reloaded" });
38
60
  } catch (error) {
39
- process.send({
61
+ sendMessage({
40
62
  event: "module-reloaded",
41
63
  error: error?.message || String(error)
42
64
  });
@@ -44,13 +66,14 @@ process.on("message", async (message) => {
44
66
  return;
45
67
  }
46
68
  if (message?.type === "ping") {
47
- process.send({
69
+ sendMessage({
48
70
  type: "pong",
49
71
  data: message.data
50
72
  });
51
73
  return;
52
74
  }
53
75
  entry.ipc?.onMessage?.(message);
54
- });
76
+ }
77
+ readMessages().catch(() => {});
55
78
  //#endregion
56
79
  export {};
@@ -1,4 +1,2 @@
1
- import "../../_chunks/base-runner.mjs";
2
- import "../../_chunks/libs/cjs-module-lexer.mjs";
3
1
  import { t as MiniflareEnvRunner } from "../../_chunks/runner2.mjs";
4
2
  export { MiniflareEnvRunner };
@@ -0,0 +1,3 @@
1
+ import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs";
2
+ import { t as NetlifyEnvRunner } from "../../_chunks/runner5.mjs";
3
+ export { EnvRunnerData, NetlifyEnvRunner };
@@ -0,0 +1,2 @@
1
+ import { t as NetlifyEnvRunner } from "../../_chunks/runner5.mjs";
2
+ export { NetlifyEnvRunner };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,43 @@
1
+ //#region src/runners/netlify/worker.ts
2
+ try {
3
+ const { startRuntime } = await import("@netlify/runtime");
4
+ startRuntime({
5
+ deployID: "0",
6
+ siteID: "0",
7
+ env: {
8
+ get: (key) => process.env[key],
9
+ has: (key) => key in process.env,
10
+ set: (key, value) => {
11
+ process.env[key] = value;
12
+ },
13
+ delete: (key) => {
14
+ delete process.env[key];
15
+ },
16
+ toObject: () => ({ ...process.env })
17
+ },
18
+ getRequestContext: () => null,
19
+ cache: { getCacheAPIContext: () => null }
20
+ });
21
+ } catch {
22
+ if (!process.env.__ENV_RUNNER_NETLIFY_WARNED) {
23
+ process.env.__ENV_RUNNER_NETLIFY_WARNED = "1";
24
+ console.warn("@netlify/runtime is not installed. Install it for full Netlify runtime emulation: npx nypm i -D @netlify/runtime");
25
+ }
26
+ globalThis.Netlify = {
27
+ context: null,
28
+ env: {
29
+ get: (key) => process.env[key],
30
+ has: (key) => key in process.env,
31
+ set: (key, value) => {
32
+ process.env[key] = value;
33
+ },
34
+ delete: (key) => {
35
+ delete process.env[key];
36
+ },
37
+ toObject: () => ({ ...process.env })
38
+ }
39
+ };
40
+ }
41
+ await import("../node-worker/worker.mjs");
42
+ //#endregion
43
+ export {};
@@ -1,19 +1,3 @@
1
- import { u as WorkerHooks } from "../../_chunks/types.mjs";
2
- import { n as EnvRunnerData, t as BaseEnvRunner } from "../../_chunks/base-runner.mjs";
3
-
4
- //#region src/runners/node-worker/runner.d.ts
5
- declare class NodeWorkerEnvRunner extends BaseEnvRunner {
6
- #private;
7
- constructor(opts: {
8
- name: string;
9
- workerEntry?: string;
10
- hooks?: WorkerHooks;
11
- data?: EnvRunnerData;
12
- });
13
- sendMessage(message: unknown): void;
14
- protected _hasRuntime(): boolean;
15
- protected _runtimeType(): string;
16
- protected _closeRuntime(): Promise<void>;
17
- }
18
- //#endregion
19
- export { type EnvRunnerData, NodeWorkerEnvRunner };
1
+ import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs";
2
+ import { t as NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs";
3
+ export { EnvRunnerData, NodeWorkerEnvRunner };
@@ -1,62 +1,2 @@
1
- import { t as BaseEnvRunner } from "../../_chunks/base-runner.mjs";
2
- import { existsSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import { Worker } from "node:worker_threads";
5
- //#region src/runners/node-worker/runner.ts
6
- let _defaultEntry;
7
- var NodeWorkerEnvRunner = class extends BaseEnvRunner {
8
- #worker;
9
- constructor(opts) {
10
- _defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/node-worker/worker"));
11
- super({
12
- ...opts,
13
- workerEntry: opts.workerEntry || _defaultEntry
14
- });
15
- this.#initWorker();
16
- }
17
- sendMessage(message) {
18
- if (!this.#worker) throw new Error("Node env worker should be initialized before sending messages.");
19
- this.#worker.postMessage(message);
20
- }
21
- _hasRuntime() {
22
- return Boolean(this.#worker);
23
- }
24
- _runtimeType() {
25
- return "worker";
26
- }
27
- async _closeRuntime() {
28
- if (!this.#worker) return;
29
- this.#worker.removeAllListeners();
30
- await this.#worker.terminate().catch((error) => {
31
- console.error(error);
32
- });
33
- this.#worker = void 0;
34
- }
35
- #initWorker() {
36
- if (!existsSync(this._workerEntry)) {
37
- this.close(`worker entry not found in "${this._workerEntry}".`);
38
- return;
39
- }
40
- const worker = new Worker(this._workerEntry, {
41
- env: { ...process.env },
42
- workerData: {
43
- name: this._name,
44
- ...this._data
45
- }
46
- });
47
- worker.once("exit", (code) => {
48
- worker._exitCode = code;
49
- this.close(`worker exited with code ${code}`);
50
- });
51
- worker.once("error", (error) => {
52
- console.error(`Worker error:`, error);
53
- this.close(error);
54
- });
55
- worker.on("message", (message) => {
56
- this._handleMessage(message);
57
- });
58
- this.#worker = worker;
59
- }
60
- };
61
- //#endregion
1
+ import { t as NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs";
62
2
  export { NodeWorkerEnvRunner };
@@ -0,0 +1,3 @@
1
+ import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs";
2
+ import { t as VercelEnvRunner } from "../../_chunks/runner4.mjs";
3
+ export { EnvRunnerData, VercelEnvRunner };
@@ -0,0 +1,2 @@
1
+ import { t as VercelEnvRunner } from "../../_chunks/runner4.mjs";
2
+ export { VercelEnvRunner };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,50 @@
1
+ //#region src/runners/vercel/worker.ts
2
+ const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
3
+ const waitUntilPromises = /* @__PURE__ */ new Set();
4
+ const cacheStore = /* @__PURE__ */ new Map();
5
+ const cacheTags = /* @__PURE__ */ new Set();
6
+ globalThis[SYMBOL_FOR_REQ_CONTEXT] = { get: () => ({
7
+ waitUntil(promise) {
8
+ waitUntilPromises.add(promise);
9
+ promise.finally(() => waitUntilPromises.delete(promise));
10
+ },
11
+ cache: {
12
+ async get(key) {
13
+ const entry = cacheStore.get(key);
14
+ if (!entry) return null;
15
+ if (entry.ttl && entry.lastModified + entry.ttl * 1e3 < Date.now()) {
16
+ cacheStore.delete(key);
17
+ return null;
18
+ }
19
+ return JSON.parse(entry.value);
20
+ },
21
+ async set(key, value, options) {
22
+ cacheStore.set(key, {
23
+ value: JSON.stringify(value ?? null),
24
+ lastModified: Date.now(),
25
+ ttl: options?.ttl,
26
+ tags: new Set(options?.tags || [])
27
+ });
28
+ },
29
+ async delete(key) {
30
+ cacheStore.delete(key);
31
+ },
32
+ async expireTag(tag) {
33
+ const tags = Array.isArray(tag) ? tag : [tag];
34
+ for (const [key, entry] of cacheStore) if (tags.some((t) => entry.tags.has(t))) cacheStore.delete(key);
35
+ }
36
+ },
37
+ purge: {
38
+ invalidateByTag: (_tag) => Promise.resolve(),
39
+ dangerouslyDeleteByTag: (_tag) => Promise.resolve(),
40
+ invalidateBySrcImage: (_src) => Promise.resolve(),
41
+ dangerouslyDeleteBySrcImage: (_src) => Promise.resolve()
42
+ },
43
+ addCacheTag(tag) {
44
+ for (const t of Array.isArray(tag) ? tag : [tag]) cacheTags.add(t);
45
+ return Promise.resolve();
46
+ }
47
+ }) };
48
+ await import("../node-worker/worker.mjs");
49
+ //#endregion
50
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "env-runner",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Generic environment runner for JavaScript runtimes.",
5
5
  "license": "MIT",
6
6
  "repository": "unjs/env-runner",
@@ -24,6 +24,10 @@
24
24
  "./runners/deno-process/worker": "./dist/runners/deno-process/worker.mjs",
25
25
  "./runners/self": "./dist/runners/self/runner.mjs",
26
26
  "./runners/miniflare": "./dist/runners/miniflare/runner.mjs",
27
+ "./runners/vercel": "./dist/runners/vercel/runner.mjs",
28
+ "./runners/vercel/worker": "./dist/runners/vercel/worker.mjs",
29
+ "./runners/netlify": "./dist/runners/netlify/runner.mjs",
30
+ "./runners/netlify/worker": "./dist/runners/netlify/worker.mjs",
27
31
  "./vite": "./dist/vite.mjs"
28
32
  },
29
33
  "scripts": {
@@ -39,31 +43,37 @@
39
43
  },
40
44
  "dependencies": {
41
45
  "crossws": "^0.4.4",
42
- "httpxy": "^0.3.1",
43
- "srvx": "^0.11.9"
46
+ "exsolve": "^1.0.8",
47
+ "httpxy": "^0.5.0",
48
+ "srvx": "^0.11.13"
44
49
  },
45
50
  "devDependencies": {
46
- "@types/node": "latest",
47
- "@typescript/native-preview": "latest",
48
- "@vitest/coverage-v8": "latest",
49
- "automd": "latest",
50
- "changelogen": "latest",
51
+ "@netlify/runtime": "^4.1.20",
52
+ "@types/node": "^25.5.0",
53
+ "@typescript/native-preview": "^7.0.0-dev.20260327.2",
54
+ "@vitest/coverage-v8": "^4.1.2",
55
+ "automd": "^0.4.3",
56
+ "changelogen": "^0.6.2",
51
57
  "cjs-module-lexer": "^2.2.0",
52
- "env-runner-fixture": "link:.",
53
- "miniflare": "^4.20260301.1",
54
- "obuild": "latest",
55
- "oxfmt": "latest",
56
- "oxlint": "latest",
57
- "typescript": "latest",
58
- "vitest": "latest"
58
+ "env-runner-fixture": "link:",
59
+ "miniflare": "^4.20260317.3",
60
+ "obuild": "^0.4.32",
61
+ "oxfmt": "^0.42.0",
62
+ "oxlint": "^1.57.0",
63
+ "typescript": "^6.0.2",
64
+ "vitest": "^4.1.2"
59
65
  },
60
66
  "peerDependencies": {
61
- "miniflare": "^4.0.0"
67
+ "@netlify/runtime": "^4",
68
+ "miniflare": "^4.20260317.3"
62
69
  },
63
70
  "peerDependenciesMeta": {
64
71
  "miniflare": {
65
72
  "optional": true
73
+ },
74
+ "@netlify/runtime": {
75
+ "optional": true
66
76
  }
67
77
  },
68
- "packageManager": "pnpm@10.29.3"
78
+ "packageManager": "pnpm@10.33.0"
69
79
  }