Skip to content

Commit 7e47106

Browse files
committed
feat: netlify runner
1 parent 1df4340 commit 7e47106

File tree

10 files changed

+878
-23
lines changed

10 files changed

+878
-23
lines changed

AGENTS.md

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ src/
3030
│ │ └── runner.ts # SelfEnvRunner (in-process, no worker)
3131
│ ├── miniflare/
3232
│ │ └── runner.ts # MiniflareEnvRunner (Cloudflare Workers via miniflare)
33-
│ └── vercel/
34-
│ ├── runner.ts # VercelEnvRunner (extends NodeWorkerEnvRunner)
35-
│ └── worker.ts # Sets Vercel request context symbol, delegates to node-worker
33+
│ ├── vercel/
34+
│ │ ├── runner.ts # VercelEnvRunner (extends NodeWorkerEnvRunner)
35+
│ │ └── worker.ts # Sets Vercel request context symbol, delegates to node-worker
36+
│ └── netlify/
37+
│ ├── runner.ts # NetlifyEnvRunner (extends NodeWorkerEnvRunner)
38+
│ └── worker.ts # Sets global Netlify context, delegates to node-worker
3639
├── types.ts # Core interfaces
3740
├── index.ts # Public API exports
3841
├── loader.ts # Dynamic runner loader
@@ -57,7 +60,9 @@ src/
5760
- **`src/runners/miniflare/runner.ts`**`MiniflareEnvRunner` extends `BaseEnvRunner`: runs entry in Cloudflare Workers runtime via miniflare. Overrides `fetch()` to use `mf.dispatchFetch()`. Uses in-memory `script` (no temp files), `unsafeModuleFallbackService` for module resolution, and `unsafeEvalBinding` for hot-reload via `reloadModule()`. Requires `miniflare` peer dependency
5861
- **`src/runners/vercel/runner.ts`**`VercelEnvRunner` extends `NodeWorkerEnvRunner`: simulates Vercel deployment environment with header injection
5962
- **`src/runners/vercel/worker.ts`** — Sets `Symbol.for("@vercel/request-context")` on globalThis, delegates to node-worker worker
60-
- **`src/loader.ts`**`loadRunner(name, opts)`: dynamic loader that imports a runner by name (`node-worker` | `node-process` | `bun-process` | `deno-process` | `self` | `miniflare` | `vercel`) and returns an `EnvRunner` instance
63+
- **`src/runners/netlify/runner.ts`**`NetlifyEnvRunner` extends `NodeWorkerEnvRunner`: simulates Netlify deployment environment with 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`)
64+
- **`src/runners/netlify/worker.ts`** — Uses `@netlify/runtime` `startRuntime()` when available (sets up `globalThis.Netlify` with env/context and `globalThis.caches`), falls back to lightweight shim. Delegates to node-worker worker
65+
- **`src/loader.ts`**`loadRunner(name, opts)`: dynamic loader that imports a runner by name (`node-worker` | `node-process` | `bun-process` | `deno-process` | `self` | `miniflare` | `vercel` | `netlify`) and returns an `EnvRunner` instance
6166
- **`src/manager.ts`**`RunnerManager`: proxy manager for hot-reload, message queueing, and listener forwarding across runner swaps
6267
- **`src/server.ts`**`EnvServer` extends `RunnerManager`: high-level API combining runner loading, watch mode (`fs.watch` with 100ms debounce), and auto-reload on file changes. Supports `watch` and `watchPaths` options
6368
- **`src/cli.ts`** — CLI entry point: `env-runner <entry> [--runner] [--port] [--host] [-w/--watch]`
@@ -127,6 +132,25 @@ Extends `NodeWorkerEnvRunner` to simulate a Vercel deployment environment. The w
127132

128133
All headers are only injected when not already present in the request.
129134

135+
### NetlifyEnvRunner
136+
137+
Extends `NodeWorkerEnvRunner` to simulate a Netlify deployment environment. The worker sets `globalThis.Netlify` with `context` (null) and `env` (backed by `process.env`) for Netlify Functions API compatibility, then delegates to the node-worker worker.
138+
139+
**Header injection:** Overrides `fetch()` to inject Netlify-specific headers before delegating to the parent:
140+
141+
- `x-nf-client-connection-ip` — derived from `x-forwarded-for` (first IP) or `x-real-ip`, defaults to `127.0.0.1`
142+
- `x-nf-account-id` — defaults to `"0"`
143+
- `x-nf-site-id` — defaults to `"0"`
144+
- `x-nf-deploy-id` — defaults to `"0"`
145+
- `x-nf-deploy-context` — defaults to `"dev"`
146+
- `x-nf-geo` — base64-encoded JSON geolocation object, defaults to `{ city: "localhost", country: { code: "dev" } }`
147+
- `x-nf-request-id` — unique UUID per request via `crypto.randomUUID()`
148+
- `x-forwarded-for`, `x-real-ip` — set to client IP if not already present
149+
- `x-forwarded-proto` — protocol from request URL
150+
- `x-forwarded-host` — from `host` header or request URL
151+
152+
All headers are only injected when not already present in the request.
153+
130154
### RunnerManager
131155

132156
Proxy manager wrapping a runner with hot-reload support:
@@ -223,6 +247,7 @@ const runner2 = new NodeProcessEnvRunner({
223247
| _(no worker)_ | `SelfEnvRunner` |
224248
| _(in-memory wrapper module)_ | `MiniflareEnvRunner` |
225249
| `env-runner/runners/vercel/worker` (default) | `VercelEnvRunner` |
250+
| `env-runner/runners/netlify/worker` (default) | `NetlifyEnvRunner` |
226251

227252
## Exports
228253

@@ -239,12 +264,14 @@ const runner2 = new NodeProcessEnvRunner({
239264
- `env-runner/runners/miniflare` (`./runners/miniflare`) — Direct import of `MiniflareEnvRunner`
240265
- `env-runner/runners/vercel` (`./runners/vercel`) — Direct import of `VercelEnvRunner`
241266
- `env-runner/runners/vercel/worker` (`./runners/vercel/worker`) — Vercel worker (sets request context, delegates to node-worker)
267+
- `env-runner/runners/netlify` (`./runners/netlify`) — Direct import of `NetlifyEnvRunner`
268+
- `env-runner/runners/netlify/worker` (`./runners/netlify/worker`) — Netlify worker (sets global Netlify context, delegates to node-worker)
242269
- `env-runner/vite` (`./vite`) — Vite Environment API helpers (`createViteHotChannel`, `createViteTransport`)
243270

244271
## Testing
245272

246273
- Tests use vitest: `pnpm vitest run`
247-
- **`test/runners.test.ts`** — Parameterized test suite for all IPC-based runner implementations (NodeWorker, NodeProcess, BunProcess, DenoProcess, Vercel). Runners requiring specific runtimes (bun, deno) are auto-skipped when the runtime is not available
274+
- **`test/runners.test.ts`** — Parameterized test suite for all IPC-based runner implementations (NodeWorker, NodeProcess, BunProcess, DenoProcess, Vercel, Netlify). Runners requiring specific runtimes (bun, deno) are auto-skipped when the runtime is not available
248275
- **`test/manager.test.ts`** — Tests for `RunnerManager` lifecycle, hot-reload, message queueing, hook forwarding
249276
- **`test/miniflare.test.ts`** — Tests for `MiniflareEnvRunner`: Durable Object exports, IPC alongside custom exports, hot-reload via `reloadModule()`, IPC re-initialization after reload
250277
- **`test/vite.test.ts`** — Tests for Vite helpers: `createViteHotChannel` message namespacing/filtering/on/off, `createViteTransport` connect/send filtering
@@ -255,7 +282,8 @@ const runner2 = new NodeProcessEnvRunner({
255282
- Test fixture in `test/fixtures/app-websocket.mjs` — Entry with crossws WebSocket hooks for websocket tests
256283
- Test fixture in `test/fixtures/app-headers.mjs` — Entry that echoes all request headers as JSON for vercel header injection tests
257284
- **`test/vercel.test.ts`** — Tests for `VercelEnvRunner`: header injection (`x-vercel-deployment-url`, `x-vercel-forwarded-for`, `x-forwarded-for`, `x-real-ip`, `x-forwarded-proto`, `x-forwarded-host`), header preservation, pre-existing header respect
258-
- Tests cover: lifecycle, fetch (GET/POST), WebSocket upgrade, crossws websocket, messaging, hooks, graceful close, inspect output, manager hot-reload, message queueing, miniflare hot-reload, vercel header injection, waitForReady, vite helpers
285+
- **`test/netlify.test.ts`** — Tests for `NetlifyEnvRunner`: 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`), IP derivation, header preservation
286+
- Tests cover: lifecycle, fetch (GET/POST), WebSocket upgrade, crossws websocket, messaging, hooks, graceful close, inspect output, manager hot-reload, message queueing, miniflare hot-reload, vercel header injection, netlify header injection, waitForReady, vite helpers
259287

260288
## Scripts
261289

@@ -273,6 +301,7 @@ const runner2 = new NodeProcessEnvRunner({
273301
- `httpxy` — HTTP/WebSocket proxy
274302
- `srvx` — Universal server framework (used by built-in workers)
275303
- `miniflare` — Cloudflare Workers simulator (optional peer dependency, required for `MiniflareEnvRunner`)
304+
- `@netlify/runtime` — Netlify compute runtime (optional peer dependency, used by `NetlifyEnvRunner` worker for full `globalThis.Netlify` + `globalThis.caches` setup)
276305

277306
> **See also:** [`.agents/MINIFLARE.md`](.agents/MINIFLARE.md) — Miniflare internals, `unsafeEvalBinding`, `unsafeModuleFallbackService`, service bindings patterns
278307
> **See also:** [`.agents/PLAN.vite-compat.md`](.agents/PLAN.vite-compat.md) — Planned improvements for Vite Environment API compatibility (`waitForReady`, RPC, transport helpers)

README.md

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<!-- /automd -->
99

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.
1111

1212
## Usage
1313

@@ -120,6 +120,8 @@ import { BunProcessEnvRunner } from "env-runner/runners/bun-process";
120120
import { DenoProcessEnvRunner } from "env-runner/runners/deno-process";
121121
import { SelfEnvRunner } from "env-runner/runners/self";
122122
import { MiniflareEnvRunner } from "env-runner/runners/miniflare";
123+
import { VercelEnvRunner } from "env-runner/runners/vercel";
124+
import { NetlifyEnvRunner } from "env-runner/runners/netlify";
123125
```
124126

125127
All runners implement the [`EnvRunner`](./src/types.ts) interface:
@@ -160,14 +162,16 @@ await runner.close();
160162

161163
**Available runners:**
162164

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` |
171175

172176
#### Miniflare Runner
173177

@@ -289,6 +293,32 @@ const runner2 = new MiniflareEnvRunner({
289293
// Fully destroy: runner.dispose() or MiniflareEnvRunner.disposeAll()
290294
```
291295

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+
292322
### Vite Environment API
293323

294324
env-runner provides helpers for integrating with Vite's [Environment API](https://vite.dev/guide/api-environment-runtimes.html):

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"./runners/miniflare": "./dist/runners/miniflare/runner.mjs",
2727
"./runners/vercel": "./dist/runners/vercel/runner.mjs",
2828
"./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",
2931
"./vite": "./dist/vite.mjs"
3032
},
3133
"scripts": {
@@ -46,6 +48,7 @@
4648
"srvx": "^0.11.13"
4749
},
4850
"devDependencies": {
51+
"@netlify/runtime": "^4.1.20",
4952
"@types/node": "^25.5.0",
5053
"@typescript/native-preview": "^7.0.0-dev.20260327.2",
5154
"@vitest/coverage-v8": "^4.1.2",
@@ -61,11 +64,15 @@
6164
"vitest": "^4.1.2"
6265
},
6366
"peerDependencies": {
67+
"@netlify/runtime": "^4",
6468
"miniflare": "^4.20260317.3"
6569
},
6670
"peerDependenciesMeta": {
6771
"miniflare": {
6872
"optional": true
73+
},
74+
"@netlify/runtime": {
75+
"optional": true
6976
}
7077
},
7178
"packageManager": "pnpm@10.33.0"

0 commit comments

Comments
 (0)