🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

env-runner

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

env-runner - npm Package Compare versions

Comparing version
0.1.11
to
0.1.12
+123
dist/_chunks/common-base-runner.d.mts
import { EnvRunner, RunnerMessageListener, WorkerAddress, WorkerHooks } from "./types.mjs";
import { IncomingMessage } from "node:http";
import { Socket } from "node:net";
/**
* Source for a virtual module: either a literal ES module string or a factory
* that returns one (sync or async).
*
* Factories are evaluated **once on the host side** before the worker is spawned
* (functions can't cross the `workerData`/`JSON` boundary, and Node's synchronous
* load hook can't await), so the worker always receives plain strings. See
* {@link resolveVirtualModules}.
*/
type VirtualModuleSource = string | (() => string | Promise<string>);
/** Virtual modules as a `specifier => source` map. */
type VirtualModules = Record<string, VirtualModuleSource>;
interface EnvRunnerData {
name?: string;
/**
* Virtual modules as a `specifier => source` map.
*
* Registered as Node.js ESM customization hooks in the worker so the entry
* (and its dependencies) can `import` them, e.g.
* `{ "#virtual-import": "export const foo = 1" }`.
*
* Each source may be a string or a factory `() => string | Promise<string>`.
* Factories are evaluated once on the host before the worker is spawned (so the
* worker always receives plain strings).
*
* Supported by the `node-worker`, `node-process`, `bun-process`,
* `deno-process`, `vercel`, `netlify`, and `miniflare` runners.
*/
virtual?: VirtualModules;
[key: string]: unknown;
}
declare abstract class BaseEnvRunner implements EnvRunner, AsyncDisposable {
closed: boolean;
protected _name: string;
protected _workerEntry: string;
protected _data?: EnvRunnerData;
protected _virtualSources?: VirtualModules;
protected _hooks: Partial<WorkerHooks>;
protected _address?: WorkerAddress;
protected _messageListeners: Set<(data: unknown) => void>;
protected _pendingRequests: Set<(cause?: unknown) => void>;
protected _virtualResolved?: Promise<void>;
constructor(opts: {
name: string;
workerEntry: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
get ready(): boolean;
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
upgrade(context: {
node: {
req: IncomingMessage;
socket: Socket;
head: any;
};
}): Promise<void>;
abstract sendMessage(message: unknown): void;
onMessage(listener: RunnerMessageListener): void;
offMessage(listener: RunnerMessageListener): void;
waitForReady(timeout?: number): Promise<void>;
rpc<T = unknown>(name: string, data?: unknown, opts?: {
timeout?: number;
}): Promise<T>;
reloadModule(timeout?: number): Promise<void>;
/**
* Invalidate a virtual module so the next `reloadModule()` re-evaluates it.
* A factory-valued `data.virtual` source is re-run on the host and the fresh
* source is shipped to the worker along with the invalidation. Rejects when
* the specifier is not a registered virtual module.
*/
invalidateModule(specifier: string, timeout?: number): Promise<void>;
close(cause?: unknown): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
/**
* Resolve a relative fetch input (e.g. `"/path"`) against a placeholder
* `http://localhost` origin so it parses as a full URL. The origin is a
* placeholder — requests are dispatched to the worker address regardless.
*/
protected _resolveFetchInput(input: string | URL | Request): string | URL | Request;
protected _handleMessage(message: any): void;
/**
* Send a message and await a matching response message. Shared by `rpc()`,
* `reloadModule()`, and `invalidateModule()`. Rejects on timeout, on a
* response carrying an `error`, and promptly when the runner closes mid-wait
* (instead of letting callers wait out the timeout on a dead worker).
*/
protected _request<T = unknown>(message: unknown, opts: {
match: (msg: any) => boolean;
timeout: number;
timeoutError: string;
send?: (message: unknown) => void;
}): Promise<T>;
/**
* Resolve any factory-valued `data.virtual` sources to strings before the
* worker is spawned. Returns a pending promise only when there is async work
* to do (a factory is present); otherwise returns `undefined` so subclasses can
* keep their synchronous spawn path. Factories must be resolved here because
* functions can't cross the worker boundary and the load hook can't await.
*/
protected _resolveVirtualData(): Promise<void> | undefined;
/**
* Re-run a factory-valued virtual source on the host and sync the resolved
* `data.virtual` map. Returns the fresh source, or `undefined` when the
* source is a plain string or unknown (nothing to re-evaluate).
*/
protected _refreshVirtualSource(specifier: string): Promise<string | undefined>;
/**
* Run a subclass spawn callback after `data.virtual` is resolved.
* Synchronous when no factory-valued source is present; otherwise defers
* `init` until factories resolve. A throwing/rejecting factory closes the
* runner with the error as cause instead of leaving an unhandled rejection.
*/
protected _initWithVirtualData(init: () => void): void;
protected _closeSocket(): Promise<void>;
protected abstract _hasRuntime(): boolean;
protected abstract _closeRuntime(): Promise<void>;
protected abstract _runtimeType(): string;
}
export { BaseEnvRunner, EnvRunnerData, VirtualModuleSource, VirtualModules };
import { resolveVirtualModules } from "./virtual-loader.mjs";
import { rm } from "node:fs/promises";
import { proxyFetch, proxyUpgrade } from "httpxy";
var BaseEnvRunner = class {
closed = false;
_name;
_workerEntry;
_data;
_virtualSources;
_hooks;
_address;
_messageListeners;
_pendingRequests;
_virtualResolved;
constructor(opts) {
this._name = opts.name;
this._workerEntry = opts.workerEntry;
this._data = opts.data;
this._hooks = opts.hooks || {};
this._messageListeners = /* @__PURE__ */ new Set();
this._pendingRequests = /* @__PURE__ */ new Set();
}
get ready() {
return Boolean(!this.closed && this._address && this._hasRuntime());
}
async fetch(input, init) {
for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
if (!this._address) return new Response(`${this._runtimeType()} env runner is unavailable`, { status: 503 });
return proxyFetch(this._address, this._resolveFetchInput(input), init);
}
async upgrade(context) {
if (!this.ready || !this._address) return;
await proxyUpgrade(this._address, context.node.req, context.node.socket, context.node.head);
}
onMessage(listener) {
this._messageListeners.add(listener);
}
offMessage(listener) {
this._messageListeners.delete(listener);
}
waitForReady(timeout = 5e3) {
if (this.ready) return Promise.resolve();
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this._messageListeners.delete(listener);
reject(/* @__PURE__ */ new Error("Runner did not become ready in time"));
}, timeout);
const listener = () => {
if (this.ready) {
clearTimeout(timer);
this._messageListeners.delete(listener);
resolve();
} else if (this.closed) {
clearTimeout(timer);
this._messageListeners.delete(listener);
reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
}
};
this._messageListeners.add(listener);
});
}
rpc(name, data, opts) {
const id = Math.random().toString(36).slice(2);
return this._request({
__rpc: name,
__rpc_id: id,
data
}, {
match: (msg) => msg?.__rpc_id === id,
timeout: opts?.timeout ?? 3e3,
timeoutError: `RPC "${name}" timed out`
}).then((msg) => msg.data);
}
async reloadModule(timeout = 5e3) {
await this._request({ event: "reload-module" }, {
match: (msg) => msg?.event === "module-reloaded",
timeout,
timeoutError: "Module reload timed out"
});
}
async invalidateModule(specifier, timeout = 5e3) {
const source = await this._refreshVirtualSource(specifier);
await this._request({
event: "invalidate-module",
specifier,
source
}, {
match: (msg) => msg?.event === "module-invalidated" && msg.specifier === specifier,
timeout,
timeoutError: `Module invalidation timed out for "${specifier}"`
});
}
async close(cause) {
if (this.closed) return;
this.closed = true;
for (const rejectPending of this._pendingRequests) rejectPending(cause);
this._pendingRequests.clear();
this._hooks.onClose?.(this, cause);
this._hooks = {};
const onError = (error) => console.error(error);
await this._closeRuntime().catch(onError);
await this._closeSocket().catch(onError);
}
async [Symbol.asyncDispose]() {
await this.close();
}
[Symbol.for("nodejs.util.inspect.custom")]() {
const status = this.closed ? "closed" : this.ready ? "ready" : "pending";
return `${this.constructor.name}#${this._name}(${status})`;
}
_resolveFetchInput(input) {
if (typeof input === "string" && !URL.canParse(input)) return new URL(input, "http://localhost");
return input;
}
_handleMessage(message) {
if (message?.address) {
this._address = message.address;
this._hooks.onReady?.(this, this._address);
}
if (message?.event === "init-error" && !this.ready && !this.closed) this.close(new Error(String(message.error || "Worker initialization failed")));
for (const listener of this._messageListeners) listener(message);
}
_request(message, opts) {
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner is closed"));
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
reject(new Error(opts.timeoutError));
}, opts.timeout);
const listener = (msg) => {
if (opts.match(msg)) {
cleanup();
if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
else resolve(msg);
}
};
const onClose = (cause) => {
cleanup();
reject(new Error("Runner closed before responding", cause ? { cause } : void 0));
};
const cleanup = () => {
clearTimeout(timer);
this.offMessage(listener);
this._pendingRequests.delete(onClose);
};
this.onMessage(listener);
this._pendingRequests.add(onClose);
try {
(opts.send ?? ((m) => this.sendMessage(m)))(message);
} catch (error) {
cleanup();
reject(error);
}
});
}
_resolveVirtualData() {
const virtual = this._data?.virtual;
this._virtualSources = virtual;
if (!virtual || !Object.values(virtual).some((v) => typeof v === "function")) return;
this._virtualResolved = resolveVirtualModules(virtual).then((resolved) => {
this._data = {
...this._data,
virtual: resolved
};
});
return this._virtualResolved;
}
async _refreshVirtualSource(specifier) {
await this._virtualResolved?.catch(() => {});
const original = this._virtualSources?.[specifier];
if (typeof original !== "function") return;
const source = await original();
const resolved = this._data?.virtual;
if (resolved) resolved[specifier] = source;
return source;
}
_initWithVirtualData(init) {
const pending = this._resolveVirtualData();
if (pending) pending.then(() => {
if (!this.closed) init();
}, (error) => this.close(error));
else init();
}
async _closeSocket() {
const socketPath = this._address?.socketPath;
if (socketPath && socketPath[0] !== "\0" && !socketPath.startsWith(String.raw`\\.\\pipe`)) await rm(socketPath).catch(() => {});
this._address = void 0;
}
};
export { BaseEnvRunner };
import { createVirtualHooks, expandVirtualInvalidation, stripVirtualTypeScript, virtualModuleFormat } from "./virtual-loader.mjs";
import { pathToFileURL } from "node:url";
import { existsSync, readFileSync } from "node:fs";
import { isAbsolute } from "node:path";
async function registerVirtualModules(virtual) {
if (!virtual || Object.keys(virtual).length === 0) return _noop;
const { registerHooks, stripTypeScriptTypes } = await import("node:module");
if (typeof registerHooks === "function") {
let transformSource;
if ("Deno" in globalThis) {
transformSource = (specifier, source) => _transformSourceForDeno(specifier, source, stripTypeScriptTypes);
const transformed = {};
for (const [specifier, source] of Object.entries(virtual)) transformed[specifier] = transformSource(specifier, source);
virtual = transformed;
}
const registration = {
virtual,
versions: /* @__PURE__ */ new Map(),
transformSource
};
const hooks = registerHooks(createVirtualHooks(virtual, registration.versions));
_hooksRegistrations.unshift(registration);
return _once(() => {
const index = _hooksRegistrations.indexOf(registration);
if (index !== -1) _hooksRegistrations.splice(index, 1);
hooks.deregister();
});
}
if (typeof globalThis.Bun?.plugin === "function") {
_bunVirtual = virtual;
_registerBunModules(Object.keys(virtual));
return _once(() => {
if (_bunVirtual === virtual) _bunVirtual = void 0;
});
}
console.warn("[env-runner] virtual modules require `module.registerHooks` (Node.js >= 22.15 / Deno >= 2.x) or `Bun.plugin`; skipping registration.");
return _noop;
}
function refreshVirtualModule(specifier) {
if (_bunVirtual?.[specifier] === void 0) return false;
_registerBunModules([specifier]);
return true;
}
function invalidateVirtualModule(specifier, source) {
for (const registration of _hooksRegistrations) {
if (!Object.hasOwn(registration.virtual, specifier)) continue;
const { virtual, versions, transformSource } = registration;
if (source !== void 0) virtual[specifier] = transformSource ? transformSource(specifier, source) : source;
for (const key of expandVirtualInvalidation(virtual, specifier)) versions.set(key, (versions.get(key) ?? 0) + 1);
return true;
}
if (_bunVirtual && Object.hasOwn(_bunVirtual, specifier)) {
if (source !== void 0) _bunVirtual[specifier] = source;
_registerBunModules(expandVirtualInvalidation(_bunVirtual, specifier));
return true;
}
return false;
}
function handleInvalidateModule(message, sendMessage) {
const ok = invalidateVirtualModule(message.specifier, message.source);
sendMessage({
event: "module-invalidated",
specifier: message.specifier,
error: ok ? void 0 : `Cannot invalidate "${message.specifier}" (not a registered virtual module)`
});
}
const _hooksRegistrations = [];
let _bunVirtual;
function _registerBunModules(specifiers) {
globalThis.Bun.plugin({
name: "env-runner-virtual",
setup(build) {
for (const specifier of specifiers) build.module(specifier, () => {
const source = _bunVirtual?.[specifier];
if (source === void 0) throw new Error(`Cannot find virtual module "${specifier}" (unregistered)`);
const format = virtualModuleFormat(specifier);
if (format === "json") return {
exports: { default: JSON.parse(source) },
loader: "object"
};
return {
contents: source,
loader: format === "module-typescript" ? "ts" : "js"
};
});
}
});
}
function _transformSourceForDeno(specifier, source, stripTypeScriptTypes) {
const format = virtualModuleFormat(specifier);
if (format === "module-typescript") return stripVirtualTypeScript(specifier, source, stripTypeScriptTypes, {
requirement: "(custom load hooks bypass Deno's native type stripping)",
remedy: "upgrade Deno"
});
if (format === "json") return `export default JSON.parse(${JSON.stringify(source)});`;
return source;
}
const _noop = () => {};
function _once(fn) {
let done = false;
return () => {
if (!done) {
done = true;
fn();
}
};
}
function isVirtualSpecifier(specifier, virtual) {
return Boolean(specifier && virtual && Object.hasOwn(virtual, specifier));
}
async function resolveEntry(entryPath, virtual) {
const mod = await (virtual ? import(entryPath) : import(_toImportPath(entryPath)));
const entry = mod.default || mod;
if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`);
return entry;
}
function parseServerAddress(server) {
const url = new URL(server.url);
return {
host: url.hostname,
port: Number(url.port)
};
}
async function reloadEntryModule(entryPath, currentEntry, sendMessage, virtual) {
await currentEntry.ipc?.onClose?.();
const newEntry = await _importFresh(entryPath, virtual);
await newEntry.ipc?.onOpen?.({ sendMessage });
return newEntry;
}
function _toImportPath(entryPath) {
const qIndex = entryPath.indexOf("?");
const filePath = qIndex === -1 ? entryPath : entryPath.slice(0, qIndex);
const query = qIndex === -1 ? "" : entryPath.slice(qIndex);
if (isAbsolute(filePath)) return pathToFileURL(filePath).href + query;
return entryPath;
}
let _reloadCounter = 0;
async function _importFresh(entryPath, virtual) {
const qIndex = entryPath.indexOf("?");
const filePath = qIndex === -1 ? entryPath : entryPath.slice(0, qIndex);
let mod;
if (!virtual && existsSync(filePath)) {
const code = readFileSync(filePath, "utf8");
mod = await import("data:text/javascript;base64," + Buffer.from(code).toString("base64"));
} else if (virtual && refreshVirtualModule(filePath)) mod = await import(filePath);
else mod = await import(entryPath + (qIndex === -1 ? "?" : "&") + "__envRunnerReload=" + _reloadCounter++);
const entry = mod.default || mod;
if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`);
return entry;
}
export { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry };
import { WorkerHooks } from "./types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "./common-base-runner.mjs";
declare class DenoProcessEnvRunner extends BaseEnvRunner {
#private;
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
execArgv?: string[];
});
sendMessage(message: unknown): void;
protected _hasRuntime(): boolean;
protected _runtimeType(): string;
protected _closeRuntime(): Promise<void>;
}
export { DenoProcessEnvRunner };
import { BaseEnvRunner } from "./common-base-runner.mjs";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { spawn } from "node:child_process";
let _defaultEntry;
var DenoProcessEnvRunner = class extends BaseEnvRunner {
#process;
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/deno-process/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
this._initWithVirtualData(() => this.#initProcess(opts.execArgv));
}
sendMessage(message) {
if (!this.#process) throw new Error("Deno env process should be initialized before sending messages.");
this.#process.send(message);
}
_hasRuntime() {
return Boolean(this.#process);
}
_runtimeType() {
return "process";
}
async _closeRuntime() {
if (!this.#process) return;
this.#process.removeAllListeners?.();
try {
this.#process.kill();
} catch {}
this.#process = void 0;
}
#initProcess(execArgv) {
if (!existsSync(this._workerEntry)) {
this.close(`process entry not found in "${this._workerEntry}".`);
return;
}
const env = {
...process.env,
ENV_RUNNER_NAME: this._name,
ENV_RUNNER_DATA: JSON.stringify(this._data || {})
};
const child = spawn("deno", [
"run",
"-A",
"--node-modules-dir=auto",
"--no-lock",
...execArgv || [],
this._workerEntry
], {
env,
stdio: [
"pipe",
"pipe",
"pipe"
]
});
const exited = new Promise((resolve) => {
child.once("exit", (code) => resolve(code ?? 1));
});
const handle = {
pid: child.pid,
kill: () => child.kill(),
send: (message) => {
child.stdin.write(JSON.stringify(message) + "\n");
},
exited,
_exitCode: void 0,
removeAllListeners: () => child.removeAllListeners()
};
child.once("exit", (code) => {
handle._exitCode = code;
this.close(`process exited with code ${code}`);
});
child.on("error", (error) => {
if (!this.closed) {
console.error(`Process error:`, error);
this.close(error);
}
});
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
let newlineIdx;
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, newlineIdx);
buffer = buffer.slice(newlineIdx + 1);
if (line.startsWith("{")) try {
this._handleMessage(JSON.parse(line));
continue;
} catch {}
process.stdout.write(line + "\n");
}
});
child.stderr?.pipe(process.stderr);
this.#process = handle;
}
};
export { DenoProcessEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "./common-base-runner.mjs";
import { IncomingMessage } from "node:http";
import { Socket } from "node:net";
/** Result from a module transform (compatible with Vite's `TransformResult`). */
interface TransformResult {
code: string;
}
/** Detected or declared export for auto-wiring Durable Object / Entrypoint bindings. */
interface MiniflareExportInfo {
type?: "DurableObject" | "WorkerEntrypoint" | "class";
}
interface MiniflareEnvRunnerOptions {
name: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
/** Options passed directly to the Miniflare constructor. */
miniflareOptions?: Record<string, unknown>;
/**
* Optional module transform callback. When provided, the module fallback
* service calls this instead of reading raw files from disk.
*
* This enables integration with Vite's transform pipeline — pass
* `environment.transformRequest` to get TS/JSX/etc. compiled on the fly.
*
* @param id - Absolute file path of the module to transform
* @returns Transformed code, or null/undefined to fall back to raw disk read
*/
transformRequest?: (id: string) => Promise<TransformResult | null | undefined>;
/**
* Declare named exports (Durable Objects, WorkerEntrypoints) to auto-wire
* bindings and generate re-exports in the wrapper module.
*
* When set to `true`, `export class` declarations are auto-detected from
* the entry file. When set to a record, the listed exports are used
* (merged with auto-detected ones). Disabled by default.
*/
exports?: Record<string, MiniflareExportInfo> | boolean;
/**
* When `true`, the Miniflare instance is cached and reused across runner
* swaps (e.g. via `RunnerManager.reload()`). `close()` tears down IPC but
* keeps Miniflare alive. Call `dispose()` to fully destroy it.
*/
persistent?: boolean;
/** Wrap the user's `fetch` in a try/catch that returns structured JSON error responses. Default: `true`. */
captureErrors?: boolean;
/**
* Export conditions for bare-specifier module resolution in the module
* fallback service. Ensures packages with conditional exports (e.g.
* `"workerd"`) resolve to the correct entry instead of the Node.js one.
*
* Defaults to `["workerd", "worker"]`.
*/
exportConditions?: string[];
}
declare class MiniflareEnvRunner extends BaseEnvRunner {
#private;
constructor(opts: MiniflareEnvRunnerOptions);
/** Dispose all persistent Miniflare instances from the cache. */
static disposeAll(): Promise<void>;
/** Fully dispose the Miniflare instance (even if persistent). */
dispose(): Promise<void>;
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
sendMessage(message: unknown): void;
/**
* Hot-reload the user entry module without recreating the Miniflare instance.
*
* Sends `reload-module` event over the WebSocket. The worker wrapper uses
* `unsafeEvalBinding` to re-import the entry with a cache-busting query string
* and responds with `module-reloaded` when done.
*/
reloadModule(timeout?: number): Promise<void>;
/**
* Invalidate a virtual module so the next `reloadModule()` re-evaluates it.
*
* Host-side only (no worker round-trip): the module fallback service serves
* virtual sources from a live map, so re-running a factory source and
* bumping the per-specifier versions — the module plus its transitive
* virtual importers — is enough. Import specifiers in re-served module code
* are rewritten to the versioned form, giving workerd fresh module
* identities (it caches by name). A `persistent` instance is evicted from
* the cache, since its served sources no longer match the cache key.
*/
invalidateModule(specifier: string, _timeout?: number): Promise<void>;
protected _hasRuntime(): boolean;
protected _runtimeType(): string;
protected _closeRuntime(): Promise<void>;
upgrade(context: {
node: {
req: IncomingMessage;
socket: Socket;
head: any;
};
}): Promise<void>;
}
export { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult };
import { expandVirtualInvalidation, stripVirtualTypeScript, virtualModuleFormat } from "./virtual-loader.mjs";
import { BaseEnvRunner } from "./common-base-runner.mjs";
import { init, parse } from "./libs/cjs-module-lexer.mjs";
import { init as init$1, parse as parse$1 } from "./libs/es-module-lexer.mjs";
import { isVirtualSpecifier } from "./common-worker-utils.mjs";
import { createRequire } from "node:module";
import { proxyUpgrade } from "httpxy";
import { fileURLToPath } from "node:url";
import { readFileSync } from "node:fs";
import { basename, dirname, isAbsolute, resolve } from "node:path";
import { resolveModulePath } from "exsolve";
const IPC_PATH = "/__env_runner_ipc";
const IPC_BINDING = "__ENV_RUNNER_IPC";
function generateWrapper(entryPath, opts) {
const staticReExport = opts?.dynamicOnly ? "" : `export * from ${JSON.stringify(entryPath)};`;
const explicitExports = opts?.dynamicOnly && opts.exports?.length ? opts.exports.map((name) => `export { ${name} } from ${JSON.stringify(entryPath)};`).join("\n") : "";
const fetchBody = opts?.captureErrors ?? true ? `try {
return await entryFetch(request, env, ctx);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
const body = JSON.stringify({
error: error.message,
stack: error.stack,
name: error.constructor?.name || "Error",
});
return new Response(body, {
status: 500,
headers: { "Content-Type": "application/json", "X-Env-Runner-Error": "1" },
});
}` : `return entryFetch(request, env, ctx);`;
return `import __process from "node:process";
if (!globalThis.process) { globalThis.process = __process; }
${staticReExport}
${explicitExports}
const __IPC_PATH = "${IPC_PATH}";
const __IPC_BINDING = "${IPC_BINDING}";
const __entryPath = ${JSON.stringify(entryPath)};
let __userEntry;
let __ipcInitialized = false;
let __serverWs;
let __currentEnv;
async function __loadEntry(env, path) {
globalThis.__ENV_RUNNER_UNSAFE_EVAL__ = env.__ENV_RUNNER_UNSAFE_EVAL__;
const importFn = env.__ENV_RUNNER_UNSAFE_EVAL__.newAsyncFunction(
"return await import(path)",
"loadEntry",
"path"
);
const mod = await importFn(path);
return mod.default || mod;
}
function __sendMessage(message) {
const payload = JSON.stringify(message);
const env = __currentEnv;
if (env && env[__IPC_BINDING]) {
env[__IPC_BINDING].fetch("http://localhost/__ipc", {
method: "POST",
body: payload,
}).catch(() => {});
return;
}
if (__serverWs) {
__serverWs.send(payload);
}
}
async function __handleWsMessage(env, data) {
let msg;
try { msg = JSON.parse(data); } catch { return; }
if (msg.type === "message") {
if (__userEntry?.ipc?.onMessage) {
__userEntry.ipc.onMessage(msg.data);
}
return;
}
if (msg.type === "reload" && env.__ENV_RUNNER_UNSAFE_EVAL__) {
const version = msg.version || 0;
try {
const newEntry = await __loadEntry(env, __entryPath + "?t=" + version);
if (__userEntry?.ipc?.onClose) {
await __userEntry.ipc.onClose();
}
__userEntry = newEntry;
__crosswsAdapter = undefined;
__ipcInitialized = false;
if (__userEntry.ipc?.onOpen) {
__ipcInitialized = true;
await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
}
__sendMessage({ event: "module-reloaded" });
} catch (e) {
__sendMessage({ event: "module-reloaded", error: String(e) });
}
return;
}
if (msg.type === "shutdown") {
if (__userEntry?.ipc?.onClose) {
await __userEntry.ipc.onClose();
}
return;
}
}
let __crosswsAdapter;
async function __initCrossws(env, hooks) {
if (__crosswsAdapter) return __crosswsAdapter;
const importFn = env.__ENV_RUNNER_UNSAFE_EVAL__.newAsyncFunction(
"return await import('crossws/adapters/cloudflare')",
"loadCrossws"
);
const { default: cloudflareAdapter } = await importFn();
__crosswsAdapter = cloudflareAdapter({ hooks });
return __crosswsAdapter;
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// WebSocket IPC handshake
if (url.pathname === __IPC_PATH && request.headers.get("upgrade") === "websocket") {
try {
if (!__userEntry) {
__userEntry = await __loadEntry(env, __entryPath);
}
} catch (e) {
return new Response("Failed to load entry: " + String(e), { status: 500 });
}
const pair = new WebSocketPair();
const client = pair[0];
const server = pair[1];
server.accept();
__serverWs = server;
server.addEventListener("message", (event) => {
__handleWsMessage(env, event.data);
});
// Initialize IPC hooks
if (!__ipcInitialized && __userEntry.ipc) {
__ipcInitialized = true;
if (__userEntry.ipc.onOpen) {
await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
}
}
return new Response(null, { status: 101, webSocket: client });
}
if (!__userEntry) {
return new Response("Worker not initialized", { status: 503 });
}
// Handle WebSocket upgrade via crossws cloudflare adapter
if (__userEntry.websocket && request.headers.get("upgrade") === "websocket") {
const adapter = await __initCrossws(env, __userEntry.websocket);
return adapter.handleUpgrade(request, env, ctx);
}
const entryFetch = __userEntry.fetch;
if (!entryFetch) {
return new Response("No fetch handler exported", { status: 500 });
}
__currentEnv = env;
try {
${fetchBody}
} finally {
__currentEnv = undefined;
}
}
};
`;
}
const _miniflareCache = /* @__PURE__ */ new Map();
var MiniflareEnvRunner = class extends BaseEnvRunner {
#miniflare;
#miniflareOptions;
#transformRequest;
#reloadCounter = 0;
#virtual;
#virtualVersions = /* @__PURE__ */ new Map();
#cacheEntry;
#ws;
#persistent;
#cacheKey;
#exports;
#captureErrors;
#exportConditions;
constructor(opts) {
super({
...opts,
workerEntry: ""
});
this.#miniflareOptions = opts.miniflareOptions || {};
this.#transformRequest = opts.transformRequest;
this.#persistent = opts.persistent ?? false;
this.#exports = opts.exports ?? {};
this.#captureErrors = opts.captureErrors ?? true;
this.#exportConditions = opts.exportConditions ?? ["workerd", "worker"];
this._initWithVirtualData(() => this.#init());
}
static async disposeAll() {
const entries = [..._miniflareCache.values()];
_miniflareCache.clear();
for (const entry of entries) await entry.mf.dispose().catch(() => {});
}
async dispose() {
if (this.#miniflare) {
if (this.#ws) {
this.#ws.send(JSON.stringify({ type: "shutdown" }));
this.#ws.close();
this.#ws = void 0;
}
if (this.#cacheKey && _miniflareCache.get(this.#cacheKey)?.mf === this.#miniflare) _miniflareCache.delete(this.#cacheKey);
await this.#miniflare.dispose();
this.#miniflare = void 0;
}
if (!this.closed) await this.close();
}
async fetch(input, init) {
for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
if (!this.#miniflare || this.closed) return new Response("miniflare env runner is unavailable", { status: 503 });
const resolved = this._resolveFetchInput(input);
const url = typeof resolved === "string" ? resolved : resolved instanceof URL ? resolved.href : resolved.url;
const res = await this.#miniflare.dispatchFetch(url, init);
if (res instanceof Response) return res;
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: res.headers
});
}
sendMessage(message) {
if (!this.#ws) throw new Error("Miniflare env runner should be initialized before sending messages.");
if (message?.type === "ping") {
queueMicrotask(() => this._handleMessage({
type: "pong",
data: message.data
}));
return;
}
this.#ws.send(JSON.stringify({
type: "message",
data: message
}));
}
async reloadModule(timeout = 5e3) {
if (!this.#ws) throw new Error("Miniflare env runner should be initialized before reloading.");
if (!this._data?.entry) return;
this.#reloadCounter++;
await this._request({
type: "reload",
version: this.#reloadCounter
}, {
match: (msg) => msg?.event === "module-reloaded",
timeout,
timeoutError: "Module reload timed out",
send: (message) => this.#ws.send(JSON.stringify(message))
});
}
async invalidateModule(specifier, _timeout) {
const virtual = this.#virtual;
if (!virtual || !Object.hasOwn(virtual, specifier)) {
const hasVirtual = Object.keys(this._data?.virtual ?? {}).length > 0;
throw !virtual && hasVirtual && !this.closed ? /* @__PURE__ */ new Error("Miniflare env runner should be initialized before invalidating modules.") : /* @__PURE__ */ new Error(`Cannot invalidate "${specifier}" (not a registered virtual module)`);
}
const source = await this._refreshVirtualSource(specifier);
if (source !== void 0) virtual[specifier] = await this.#prepareVirtualSource(specifier, source);
for (const key of expandVirtualInvalidation(virtual, specifier)) this.#virtualVersions.set(key, (this.#virtualVersions.get(key) ?? 0) + 1);
if (this.#cacheKey && _miniflareCache.get(this.#cacheKey) === this.#cacheEntry) _miniflareCache.delete(this.#cacheKey);
}
_hasRuntime() {
return Boolean(this.#miniflare);
}
_runtimeType() {
return "miniflare";
}
async _closeRuntime() {
if (!this.#miniflare) return;
if (this.#ws) {
this.#ws.send(JSON.stringify({ type: "shutdown" }));
this.#ws.close();
this.#ws = void 0;
}
const entry = this.#cacheEntry;
if (entry) {
entry.refCount--;
if (entry.refCount <= 0) {
if (this.#cacheKey && _miniflareCache.get(this.#cacheKey) === entry) _miniflareCache.delete(this.#cacheKey);
await this.#miniflare.dispose();
}
} else await this.#miniflare.dispose();
this.#miniflare = void 0;
}
#init() {
this.#initAsync().catch((error) => {
console.error("Miniflare runner init error:", error);
this.close(error);
});
}
async upgrade(context) {
if (!this.#miniflare || this.closed) {
context.node.socket.destroy();
return;
}
const mfUrl = await this.#miniflare.unsafeGetDirectURL();
const address = new URL(mfUrl);
await proxyUpgrade({
host: address.hostname,
port: Number(address.port)
}, context.node.req, context.node.socket, context.node.head);
}
async #prepareVirtualModules() {
const virtual = this._data?.virtual;
if (!virtual || Object.keys(virtual).length === 0) return;
const out = {};
for (const [specifier, source] of Object.entries(virtual)) out[specifier] = await this.#prepareVirtualSource(specifier, source);
return out;
}
async #prepareVirtualSource(specifier, source) {
if (virtualModuleFormat(specifier) !== "module-typescript") return source;
return stripVirtualTypeScript(specifier, source, await _getStripTypeScriptTypes(), {
requirement: "on the host (workerd does not parse TypeScript)",
remedy: "upgrade Node.js"
});
}
async #initAsync() {
const { Miniflare } = await import("miniflare");
const entryPath = this._data?.entry;
const virtual = await this.#prepareVirtualModules();
this.#virtual = virtual;
const userFlags = this.#miniflareOptions.compatibilityFlags || [];
const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || [];
const options = {
compatibilityDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
modules: true,
...this.#miniflareOptions,
compatibilityFlags: [...new Set(["nodejs_compat", ...userFlags])],
unsafeDirectSockets: [{
host: "127.0.0.1",
port: 0
}, ...userDirectSockets]
};
if (entryPath && !options.script && !options.scriptPath) {
const entryIsVirtual = isVirtualSpecifier(entryPath, virtual);
const resolvedEntry = entryIsVirtual ? entryPath : resolve(entryPath);
const entryBase = isAbsolute(resolvedEntry) ? resolvedEntry : resolve("__env_runner_virtual_entry__.mjs");
const entryDir = dirname(entryBase);
const entrySource = entryIsVirtual ? virtual[entryPath] : _tryReadFile(resolvedEntry);
const detectedExports = this.#exports === false || this.#exports === void 0 ? [] : detectExportedClasses(entrySource, typeof this.#exports === "object" ? this.#exports : {});
if (entryIsVirtual && detectedExports.length > 0) throw new Error(`[env-runner] named exports (${detectedExports.join(", ")}) are not supported with a virtual entry on the miniflare runner; pass \`exports: false\` or use a real entry file.`);
if (detectedExports.length > 0 && !options.durableObjects) {
const autoDOs = { ...this.#miniflareOptions.durableObjects || {} };
for (const name of detectedExports) {
const bindingName = toScreamingSnakeCase(name);
if (!autoDOs[bindingName]) autoDOs[bindingName] = name;
}
options.durableObjects = autoDOs;
}
options.script = generateWrapper(resolvedEntry, {
dynamicOnly: true,
captureErrors: this.#captureErrors,
exports: detectedExports
});
options.scriptPath = entryDir + "/__env_runner_wrapper.mjs";
if (!options.modulesRoot) options.modulesRoot = "/";
options.unsafeEvalBinding = "__ENV_RUNNER_UNSAFE_EVAL__";
options.serviceBindings = {
...options.serviceBindings || {},
[IPC_BINDING]: async (request) => {
try {
const message = await request.json();
this._handleMessage(message);
} catch {}
return new Response(null, { status: 204 });
}
};
if (this.#transformRequest && !options.modulesRules) options.modulesRules = [{
type: "ESModule",
include: [
"**/*.ts",
"**/*.tsx",
"**/*.jsx",
"**/*.mts"
]
}];
if (!options.unsafeModuleFallbackService) {
const _require = createRequire(entryBase);
const _virtual = virtual;
const _virtualVersions = this.#virtualVersions;
const _transformRequest = this.#transformRequest;
const _exportConditions = this.#exportConditions;
const _applyVirtualVersions = (code) => applyVirtualVersions(code, _virtualVersions);
options.unsafeUseModuleFallbackService = true;
const modulePathMap = /* @__PURE__ */ new Map();
const _lexersReady = Promise.all([ensureCjsLexer(), init$1]);
options.unsafeModuleFallbackService = async (request) => {
await _lexersReady;
const url = new URL(request.url);
const specifier = url.searchParams.get("specifier");
const rawSpecifier = url.searchParams.get("rawSpecifier");
const referrer = url.searchParams.get("referrer") || "";
if (!specifier) return new Response(null, { status: 404 });
const cleanSpecifier = specifier.split("?")[0] || specifier;
const cleanRaw = rawSpecifier?.split("?")[0];
if (_virtual) {
const bareSpecifier = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
const virtualKey = [
cleanRaw,
cleanSpecifier,
bareSpecifier
].find((key) => key !== void 0 && Object.hasOwn(_virtual, key));
if (virtualKey !== void 0) {
const name = bareSpecifier + (specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "");
const source = _virtual[virtualKey];
return virtualModuleFormat(virtualKey) === "json" ? Response.json({
name,
json: source
}) : Response.json({
name,
esModule: _applyVirtualVersions(source)
});
}
}
let resolvedPath;
const fileUrlRaw = cleanRaw || cleanSpecifier;
if (fileUrlRaw.startsWith("file://")) try {
resolvedPath = fileURLToPath(fileUrlRaw);
} catch {
return new Response(null, { status: 404 });
}
else if (cleanRaw && !cleanRaw.startsWith(".") && !cleanRaw.startsWith("/")) {
const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
const referrerReal = modulePathMap.get(referrerKey);
const contextRequire = referrerReal ? createRequire(referrerReal) : _require;
if (cleanRaw.startsWith("cloudflare:")) return new Response(null, { status: 404 });
if (cleanRaw.startsWith("node:")) {
const nodeName = cleanRaw.slice(5);
try {
resolvedPath = contextRequire.resolve(`unenv/node/${nodeName}`);
} catch {
return new Response(null, { status: 404 });
}
} else try {
resolvedPath = resolveModulePath(cleanRaw, {
from: referrerReal || entryBase,
conditions: _exportConditions,
try: true
}) || contextRequire.resolve(cleanRaw);
} catch {
const name = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
return Response.json({
name,
esModule: "export default undefined;"
});
}
} else {
const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
const referrerDir = dirname(modulePathMap.get(referrerKey) || (referrer.startsWith("/") ? referrer : "/" + referrer));
const raw = cleanRaw || cleanSpecifier;
if (raw.startsWith(".")) resolvedPath = resolve(referrerDir, raw);
else if (cleanSpecifier.startsWith("/")) resolvedPath = cleanSpecifier;
else try {
resolvedPath = _require.resolve(raw);
} catch {
return new Response(null, { status: 404 });
}
}
const rawQuery = specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "";
const name = (cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier) + rawQuery;
if (_transformRequest) try {
const result = await _transformRequest(resolvedPath);
if (result?.code) {
modulePathMap.set(name, resolvedPath);
return Response.json({
name,
esModule: _applyVirtualVersions(result.code)
});
}
} catch {}
try {
const contents = readFileSync(resolvedPath, "utf8");
modulePathMap.set(name, resolvedPath);
if (resolvedPath.endsWith(".mjs") || !resolvedPath.endsWith(".cjs") && /\b(import\s|import\(|export\s|export\{|import\.meta\b)/.test(contents)) return Response.json({
name,
esModule: _applyVirtualVersions(contents)
});
const cjsSuffix = "?__cjs";
if (specifier.endsWith(cjsSuffix)) return Response.json({
name,
commonJsModule: contents
});
const esModule = createCjsEsmShim("./" + basename(resolvedPath) + cjsSuffix, contents);
return Response.json({
name,
esModule
});
} catch {
return new Response(null, { status: 404 });
}
};
}
}
if (this.#persistent && entryPath) {
this.#cacheKey = computeCacheKey(entryPath, {
...this.#miniflareOptions,
_exportConditions: this.#exportConditions,
_virtual: virtual
});
const cached = _miniflareCache.get(this.#cacheKey);
if (cached) {
this.#miniflare = cached.mf;
cached.refCount++;
this.#cacheEntry = cached;
this.#virtual = cached.virtual;
this.#virtualVersions = cached.versions;
}
}
if (!this.#miniflare) {
this.#miniflare = new Miniflare(options);
await this.#miniflare.ready;
if (this.#persistent && this.#cacheKey) {
this.#cacheEntry = {
mf: this.#miniflare,
refCount: 1,
virtual,
versions: this.#virtualVersions
};
_miniflareCache.set(this.#cacheKey, this.#cacheEntry);
}
}
const initRes = await this.#miniflare.dispatchFetch("http://localhost/__env_runner_ipc", { headers: { upgrade: "websocket" } });
const ws = initRes.webSocket;
if (!ws) {
const body = await initRes.text().catch(() => "");
throw new Error(`Failed to establish WebSocket IPC channel (${initRes.status}: ${body})`);
}
ws.accept();
this.#ws = ws;
ws.addEventListener("message", (event) => {
try {
const parsed = JSON.parse(event.data);
this._handleMessage(parsed);
} catch {}
});
this._handleMessage({ address: {
host: "127.0.0.1",
port: 0
} });
}
};
function detectExportedClasses(entrySource, explicit) {
const names = new Set(Object.keys(explicit));
if (entrySource) {
const re = /\bexport\s+class\s+(\w+)/g;
let match;
while (match = re.exec(entrySource)) if (match[1]) names.add(match[1]);
}
return [...names];
}
function _tryReadFile(path) {
try {
return readFileSync(path, "utf8");
} catch {
return;
}
}
function toScreamingSnakeCase(name) {
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
}
function computeCacheKey(entryPath, opts) {
const serializableOpts = {};
for (const [k, v] of Object.entries(opts)) if (typeof v !== "function") serializableOpts[k] = v;
return `${resolve(entryPath)}::${JSON.stringify(serializableOpts)}`;
}
function applyVirtualVersions(code, versions) {
if (versions.size === 0) return code;
let imports;
try {
[imports] = parse$1(code);
} catch {
return code;
}
let out = "";
let last = 0;
for (const imp of imports) {
let specifier = imp.n;
if (specifier === void 0 && imp.d > -1) {
const expr = code.slice(imp.s, imp.e);
if (expr.length > 1 && expr[0] === "`" && expr.endsWith("`") && !expr.includes("${")) specifier = expr.slice(1, -1);
}
const version = specifier === void 0 ? void 0 : versions.get(specifier);
if (!version) continue;
const versioned = `${specifier}?v=${version}`;
out += code.slice(last, imp.s) + (imp.d > -1 ? JSON.stringify(versioned) : versioned);
last = imp.e;
}
return out + code.slice(last);
}
let _stripTypesPromise;
function _getStripTypeScriptTypes() {
_stripTypesPromise ??= import("node:module").then((m) => m.stripTypeScriptTypes);
return _stripTypesPromise;
}
let _cjsLexerReady;
function ensureCjsLexer() {
if (!_cjsLexerReady) _cjsLexerReady = init();
return _cjsLexerReady;
}
function createCjsEsmShim(cjsSpecifier, contents) {
let namedExports = [];
try {
const { exports } = parse(contents);
namedExports = exports.filter((e) => e !== "default" && e !== "__esModule");
} catch {}
let shim = `import __cjs_mod__ from ${JSON.stringify(cjsSpecifier)};\nexport default __cjs_mod__;\n`;
for (const name of namedExports) shim += `export var ${name} = __cjs_mod__["${name}"];\n`;
return shim;
}
export { MiniflareEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { EnvRunnerData } from "./common-base-runner.mjs";
import { NodeWorkerEnvRunner } from "./node-worker-runner.mjs";
declare class NetlifyEnvRunner extends NodeWorkerEnvRunner {
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
protected _runtimeType(): string;
}
export { NetlifyEnvRunner };
import { NodeWorkerEnvRunner } from "./node-worker-runner.mjs";
import { fileURLToPath } from "node:url";
let _defaultEntry;
var NetlifyEnvRunner = class extends NodeWorkerEnvRunner {
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/netlify/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
}
async fetch(input, init) {
input = this._resolveFetchInput(input);
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
const clientIp = headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || "127.0.0.1";
if (!headers.has("x-nf-client-connection-ip")) headers.set("x-nf-client-connection-ip", clientIp);
if (!headers.has("x-nf-account-id")) headers.set("x-nf-account-id", "0");
if (!headers.has("x-nf-site-id")) headers.set("x-nf-site-id", "0");
if (!headers.has("x-nf-deploy-id")) headers.set("x-nf-deploy-id", "0");
if (!headers.has("x-nf-deploy-context")) headers.set("x-nf-deploy-context", "dev");
if (!headers.has("x-nf-geo")) headers.set("x-nf-geo", btoa(JSON.stringify({
city: "localhost",
country: { code: "dev" }
})));
if (!headers.has("x-nf-request-id")) headers.set("x-nf-request-id", crypto.randomUUID());
if (!headers.has("x-forwarded-for")) headers.set("x-forwarded-for", clientIp);
if (!headers.has("x-real-ip")) headers.set("x-real-ip", clientIp);
try {
const url = new URL(input instanceof Request ? input.url : input.toString());
if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", headers.get("host") || url.host);
} catch {}
if (input instanceof Request) return super.fetch(new Request(input, {
...init,
headers
}));
return super.fetch(input, {
...init,
headers
});
}
_runtimeType() {
return "netlify";
}
};
export { NetlifyEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "./common-base-runner.mjs";
declare class NodeWorkerEnvRunner extends BaseEnvRunner {
#private;
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
sendMessage(message: unknown): void;
protected _hasRuntime(): boolean;
protected _runtimeType(): string;
protected _closeRuntime(): Promise<void>;
}
export { NodeWorkerEnvRunner };
import { BaseEnvRunner } from "./common-base-runner.mjs";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { Worker } from "node:worker_threads";
let _defaultEntry;
var NodeWorkerEnvRunner = class extends BaseEnvRunner {
#worker;
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/node-worker/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
this._initWithVirtualData(() => this.#initWorker());
}
sendMessage(message) {
if (!this.#worker) throw new Error("Worker thread should be initialized before sending messages.");
this.#worker.postMessage(message);
}
_hasRuntime() {
return Boolean(this.#worker);
}
_runtimeType() {
return "worker";
}
async _closeRuntime() {
if (!this.#worker) return;
this.#worker.removeAllListeners();
await this.#worker.terminate().catch((error) => {
console.error(error);
});
this.#worker = void 0;
}
#initWorker() {
if (!existsSync(this._workerEntry)) {
this.close(`worker entry not found in "${this._workerEntry}".`);
return;
}
const worker = new Worker(this._workerEntry, {
env: { ...process.env },
workerData: {
name: this._name,
...this._data
}
});
worker.once("exit", (code) => {
worker._exitCode = code;
this.close(`worker exited with code ${code}`);
});
worker.once("error", (error) => {
console.error(`Worker error:`, error);
this.close(error);
});
worker.on("message", (message) => {
this._handleMessage(message);
});
this.#worker = worker;
}
};
export { NodeWorkerEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { EnvRunnerData } from "./common-base-runner.mjs";
import { NodeWorkerEnvRunner } from "./node-worker-runner.mjs";
declare class VercelEnvRunner extends NodeWorkerEnvRunner {
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
protected _runtimeType(): string;
}
export { VercelEnvRunner };
import { NodeWorkerEnvRunner } from "./node-worker-runner.mjs";
import { fileURLToPath } from "node:url";
import { randomBytes } from "node:crypto";
let _warned = false;
function warnIfVercelOidcTokenInvalid(token) {
const result = _checkVercelOidcToken(token);
if (_warned) return result;
if (result.status === "missing") {
_warned = true;
console.warn(`\x1B[90m[env-runner]\x1B[39m \x1B[33mVERCEL_OIDC_TOKEN\x1B[39m is not set. Run \x1B[36mvercel env pull\x1B[39m to pull the latest environment variables.`);
} else if (result.status === "expired") {
_warned = true;
console.warn(`\x1B[90m[env-runner]\x1B[39m \x1B[33mVERCEL_OIDC_TOKEN\x1B[39m expired at ${result.expiresAt.toISOString()}. Run \x1B[36mvercel env pull\x1B[39m to pull a fresh OIDC token.`);
} else if (result.status === "invalid") {
_warned = true;
console.warn(`\x1B[90m[env-runner]\x1B[39m \x1B[33mVERCEL_OIDC_TOKEN\x1B[39m is malformed (not a valid JWT). Run \x1B[36mvercel env pull\x1B[39m to pull a valid OIDC token.`);
}
return result;
}
function _checkVercelOidcToken(token = process.env.VERCEL_OIDC_TOKEN) {
if (!token) return { status: "missing" };
const parts = token.split(".");
if (parts.length !== 3) return { status: "invalid" };
let payload;
try {
const json = Buffer.from(parts[1], "base64url").toString("utf8");
payload = JSON.parse(json);
} catch {
return { status: "invalid" };
}
if (!payload || typeof payload !== "object") return { status: "invalid" };
const exp = payload.exp;
if (typeof exp !== "number" || !Number.isFinite(exp)) return { status: "invalid" };
const expiresAt = /* @__PURE__ */ new Date(exp * 1e3);
if (expiresAt.getTime() <= Date.now()) return {
status: "expired",
expiresAt
};
return {
status: "valid",
expiresAt
};
}
let _defaultEntry;
const _podId = Math.random().toString(32).slice(-5);
function generateVercelId() {
return `dev1::${_podId}-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`;
}
var VercelEnvRunner = class extends NodeWorkerEnvRunner {
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/vercel/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
warnIfVercelOidcTokenInvalid();
}
async fetch(input, init) {
input = this._resolveFetchInput(input);
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
const requestId = generateVercelId();
if (this._address && this._address.port != null && !headers.has("x-vercel-deployment-url")) {
const host = this._address.host || "127.0.0.1";
headers.set("x-vercel-deployment-url", `http://${host}:${this._address.port}`);
}
if (!headers.has("x-vercel-id")) headers.set("x-vercel-id", requestId);
const clientIp = headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || "127.0.0.1";
if (!headers.has("x-vercel-forwarded-for")) headers.set("x-vercel-forwarded-for", clientIp);
if (!headers.has("x-forwarded-for")) headers.set("x-forwarded-for", clientIp);
if (!headers.has("x-real-ip")) headers.set("x-real-ip", clientIp);
try {
const url = new URL(input instanceof Request ? input.url : input.toString());
if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", headers.get("host") || url.host);
} catch {}
const res = await super.fetch(input, {
...init,
headers
});
const resHeaders = new Headers(res.headers);
if (!resHeaders.has("server")) resHeaders.set("server", "Vercel");
if (!resHeaders.has("x-vercel-id")) resHeaders.set("x-vercel-id", requestId);
if (!resHeaders.has("x-vercel-cache")) resHeaders.set("x-vercel-cache", "MISS");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: resHeaders
});
}
_runtimeType() {
return "vercel";
}
};
export { VercelEnvRunner };
+9
-1

@@ -0,1 +1,2 @@

import { pathToFileURL } from "node:url";
async function resolveVirtualModules(virtual) {

@@ -6,3 +7,3 @@ const entries = await Promise.all(Object.entries(virtual).map(async ([key, value]) => [key, typeof value === "function" ? await value() : value]));

const VIRTUAL_SCHEME = "virtual:";
function createVirtualHooks(virtual, versions) {
function createVirtualHooks(virtual, versions, parentURL = _defaultParentURL()) {
const resolve = (specifier, context, nextResolve) => {

@@ -17,2 +18,6 @@ const key = _stripQuery(specifier);

}
if (context.parentURL?.startsWith(VIRTUAL_SCHEME)) return nextResolve(specifier, {
...context,
parentURL
});
return nextResolve(specifier, context);

@@ -65,2 +70,5 @@ };

}
function _defaultParentURL() {
return pathToFileURL(process.cwd() + "/").href;
}
export { createVirtualHooks, expandVirtualInvalidation, resolveVirtualModules, stripVirtualTypeScript, virtualModuleFormat };
+5
-5
import { EnvRunner, FetchHandler, NodeUpgradeContext, RPCOptions, RunnerMessageListener, RunnerRPCHooks, UpgradeContext, UpgradeHandler, WorkerAddress, WorkerHooks } from "./_chunks/types.mjs";
import { BaseEnvRunner, EnvRunnerData, VirtualModuleSource, VirtualModules } from "./_chunks/base-runner.mjs";
import { DenoProcessEnvRunner } from "./_chunks/runner.mjs";
import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult } from "./_chunks/runner2.mjs";
import { VercelEnvRunner } from "./_chunks/runner4.mjs";
import { NetlifyEnvRunner } from "./_chunks/runner5.mjs";
import { BaseEnvRunner, EnvRunnerData, VirtualModuleSource, VirtualModules } from "./_chunks/common-base-runner.mjs";
import { DenoProcessEnvRunner } from "./_chunks/deno-process-runner.mjs";
import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult } from "./_chunks/miniflare-runner.mjs";
import { VercelEnvRunner } from "./_chunks/vercel-runner.mjs";
import { NetlifyEnvRunner } from "./_chunks/netlify-runner.mjs";
import { ServerOptions } from "srvx";

@@ -8,0 +8,0 @@ import { Hooks } from "crossws";

@@ -1,7 +0,7 @@

import { BaseEnvRunner } from "./_chunks/base-runner.mjs";
import { BaseEnvRunner } from "./_chunks/common-base-runner.mjs";
import { EnvServer, RunnerManager, loadRunner } from "./_chunks/server.mjs";
import { DenoProcessEnvRunner } from "./_chunks/runner.mjs";
import { MiniflareEnvRunner } from "./_chunks/runner2.mjs";
import { VercelEnvRunner } from "./_chunks/runner4.mjs";
import { NetlifyEnvRunner } from "./_chunks/runner5.mjs";
import { DenoProcessEnvRunner } from "./_chunks/deno-process-runner.mjs";
import { MiniflareEnvRunner } from "./_chunks/miniflare-runner.mjs";
import { VercelEnvRunner } from "./_chunks/vercel-runner.mjs";
import { NetlifyEnvRunner } from "./_chunks/netlify-runner.mjs";
export { BaseEnvRunner, DenoProcessEnvRunner, EnvServer, MiniflareEnvRunner, NetlifyEnvRunner, RunnerManager, VercelEnvRunner, loadRunner };
import { WorkerHooks } from "../../_chunks/types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
declare class BunProcessEnvRunner extends BaseEnvRunner {

@@ -4,0 +4,0 @@ #private;

@@ -1,5 +0,5 @@

import { BaseEnvRunner } from "../../_chunks/base-runner.mjs";
import { BaseEnvRunner } from "../../_chunks/common-base-runner.mjs";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { execSync, spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { join } from "node:path";

@@ -6,0 +6,0 @@ import { homedir } from "node:os";

@@ -1,2 +0,2 @@

import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs";
import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/common-worker-utils.mjs";
import { serve } from "srvx";

@@ -3,0 +3,0 @@ import { plugin } from "crossws/server";

@@ -1,3 +0,3 @@

import { EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { DenoProcessEnvRunner } from "../../_chunks/runner.mjs";
import { EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
import { DenoProcessEnvRunner } from "../../_chunks/deno-process-runner.mjs";
export { DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData };

@@ -1,2 +0,2 @@

import { DenoProcessEnvRunner } from "../../_chunks/runner.mjs";
import { DenoProcessEnvRunner } from "../../_chunks/deno-process-runner.mjs";
export { DenoProcessEnvRunner };

@@ -1,2 +0,2 @@

import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs";
import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/common-worker-utils.mjs";
import { serve } from "srvx";

@@ -3,0 +3,0 @@ import { plugin } from "crossws/server";

@@ -1,3 +0,3 @@

import { EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult } from "../../_chunks/runner2.mjs";
import { EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult } from "../../_chunks/miniflare-runner.mjs";
export { MiniflareEnvRunner, type EnvRunnerData as MiniflareEnvRunnerData, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult };

@@ -1,2 +0,2 @@

import { MiniflareEnvRunner } from "../../_chunks/runner2.mjs";
import { MiniflareEnvRunner } from "../../_chunks/miniflare-runner.mjs";
export { MiniflareEnvRunner };

@@ -1,3 +0,3 @@

import { EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { NetlifyEnvRunner } from "../../_chunks/runner5.mjs";
import { EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
import { NetlifyEnvRunner } from "../../_chunks/netlify-runner.mjs";
export { type EnvRunnerData, NetlifyEnvRunner };

@@ -1,2 +0,2 @@

import { NetlifyEnvRunner } from "../../_chunks/runner5.mjs";
import { NetlifyEnvRunner } from "../../_chunks/netlify-runner.mjs";
export { NetlifyEnvRunner };
import { WorkerHooks } from "../../_chunks/types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
declare class NodeProcessEnvRunner extends BaseEnvRunner {

@@ -4,0 +4,0 @@ #private;

@@ -1,5 +0,5 @@

import { BaseEnvRunner } from "../../_chunks/base-runner.mjs";
import { BaseEnvRunner } from "../../_chunks/common-base-runner.mjs";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { fork } from "node:child_process";
import { fileURLToPath } from "node:url";
let _defaultEntry;

@@ -6,0 +6,0 @@ var NodeProcessEnvRunner = class extends BaseEnvRunner {

@@ -1,2 +0,2 @@

import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs";
import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/common-worker-utils.mjs";
import { serve } from "srvx";

@@ -3,0 +3,0 @@ import { plugin } from "crossws/server/node";

@@ -1,3 +0,3 @@

import { EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs";
import { EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
import { NodeWorkerEnvRunner } from "../../_chunks/node-worker-runner.mjs";
export { type EnvRunnerData, NodeWorkerEnvRunner };

@@ -1,2 +0,2 @@

import { NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs";
import { NodeWorkerEnvRunner } from "../../_chunks/node-worker-runner.mjs";
export { NodeWorkerEnvRunner };

@@ -1,2 +0,2 @@

import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs";
import { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/common-worker-utils.mjs";
import { parentPort, workerData } from "node:worker_threads";

@@ -3,0 +3,0 @@ import { serve } from "srvx";

import { WorkerHooks } from "../../_chunks/types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
declare class SelfEnvRunner extends BaseEnvRunner {

@@ -4,0 +4,0 @@ #private;

@@ -1,3 +0,3 @@

import { BaseEnvRunner } from "../../_chunks/base-runner.mjs";
import { reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs";
import { BaseEnvRunner } from "../../_chunks/common-base-runner.mjs";
import { reloadEntryModule, resolveEntry } from "../../_chunks/common-worker-utils.mjs";
var SelfEnvRunner = class extends BaseEnvRunner {

@@ -4,0 +4,0 @@ #active = false;

@@ -1,3 +0,3 @@

import { EnvRunnerData } from "../../_chunks/base-runner.mjs";
import { VercelEnvRunner } from "../../_chunks/runner4.mjs";
import { EnvRunnerData } from "../../_chunks/common-base-runner.mjs";
import { VercelEnvRunner } from "../../_chunks/vercel-runner.mjs";
export { type EnvRunnerData, VercelEnvRunner };

@@ -1,2 +0,2 @@

import { VercelEnvRunner } from "../../_chunks/runner4.mjs";
import { VercelEnvRunner } from "../../_chunks/vercel-runner.mjs";
export { VercelEnvRunner };
{
"name": "env-runner",
"version": "0.1.11",
"version": "0.1.12",
"description": "Generic environment runner for JavaScript runtimes.",

@@ -5,0 +5,0 @@ "license": "MIT",

import { EnvRunner, RunnerMessageListener, WorkerAddress, WorkerHooks } from "./types.mjs";
import { IncomingMessage } from "node:http";
import { Socket } from "node:net";
/**
* Source for a virtual module: either a literal ES module string or a factory
* that returns one (sync or async).
*
* Factories are evaluated **once on the host side** before the worker is spawned
* (functions can't cross the `workerData`/`JSON` boundary, and Node's synchronous
* load hook can't await), so the worker always receives plain strings. See
* {@link resolveVirtualModules}.
*/
type VirtualModuleSource = string | (() => string | Promise<string>);
/** Virtual modules as a `specifier => source` map. */
type VirtualModules = Record<string, VirtualModuleSource>;
interface EnvRunnerData {
name?: string;
/**
* Virtual modules as a `specifier => source` map.
*
* Registered as Node.js ESM customization hooks in the worker so the entry
* (and its dependencies) can `import` them, e.g.
* `{ "#virtual-import": "export const foo = 1" }`.
*
* Each source may be a string or a factory `() => string | Promise<string>`.
* Factories are evaluated once on the host before the worker is spawned (so the
* worker always receives plain strings).
*
* Supported by the `node-worker`, `node-process`, `bun-process`,
* `deno-process`, `vercel`, `netlify`, and `miniflare` runners.
*/
virtual?: VirtualModules;
[key: string]: unknown;
}
declare abstract class BaseEnvRunner implements EnvRunner, AsyncDisposable {
closed: boolean;
protected _name: string;
protected _workerEntry: string;
protected _data?: EnvRunnerData;
protected _virtualSources?: VirtualModules;
protected _hooks: Partial<WorkerHooks>;
protected _address?: WorkerAddress;
protected _messageListeners: Set<(data: unknown) => void>;
protected _pendingRequests: Set<(cause?: unknown) => void>;
protected _virtualResolved?: Promise<void>;
constructor(opts: {
name: string;
workerEntry: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
get ready(): boolean;
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
upgrade(context: {
node: {
req: IncomingMessage;
socket: Socket;
head: any;
};
}): Promise<void>;
abstract sendMessage(message: unknown): void;
onMessage(listener: RunnerMessageListener): void;
offMessage(listener: RunnerMessageListener): void;
waitForReady(timeout?: number): Promise<void>;
rpc<T = unknown>(name: string, data?: unknown, opts?: {
timeout?: number;
}): Promise<T>;
reloadModule(timeout?: number): Promise<void>;
/**
* Invalidate a virtual module so the next `reloadModule()` re-evaluates it.
* A factory-valued `data.virtual` source is re-run on the host and the fresh
* source is shipped to the worker along with the invalidation. Rejects when
* the specifier is not a registered virtual module.
*/
invalidateModule(specifier: string, timeout?: number): Promise<void>;
close(cause?: unknown): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
/**
* Resolve a relative fetch input (e.g. `"/path"`) against a placeholder
* `http://localhost` origin so it parses as a full URL. The origin is a
* placeholder — requests are dispatched to the worker address regardless.
*/
protected _resolveFetchInput(input: string | URL | Request): string | URL | Request;
protected _handleMessage(message: any): void;
/**
* Send a message and await a matching response message. Shared by `rpc()`,
* `reloadModule()`, and `invalidateModule()`. Rejects on timeout, on a
* response carrying an `error`, and promptly when the runner closes mid-wait
* (instead of letting callers wait out the timeout on a dead worker).
*/
protected _request<T = unknown>(message: unknown, opts: {
match: (msg: any) => boolean;
timeout: number;
timeoutError: string;
send?: (message: unknown) => void;
}): Promise<T>;
/**
* Resolve any factory-valued `data.virtual` sources to strings before the
* worker is spawned. Returns a pending promise only when there is async work
* to do (a factory is present); otherwise returns `undefined` so subclasses can
* keep their synchronous spawn path. Factories must be resolved here because
* functions can't cross the worker boundary and the load hook can't await.
*/
protected _resolveVirtualData(): Promise<void> | undefined;
/**
* Re-run a factory-valued virtual source on the host and sync the resolved
* `data.virtual` map. Returns the fresh source, or `undefined` when the
* source is a plain string or unknown (nothing to re-evaluate).
*/
protected _refreshVirtualSource(specifier: string): Promise<string | undefined>;
/**
* Run a subclass spawn callback after `data.virtual` is resolved.
* Synchronous when no factory-valued source is present; otherwise defers
* `init` until factories resolve. A throwing/rejecting factory closes the
* runner with the error as cause instead of leaving an unhandled rejection.
*/
protected _initWithVirtualData(init: () => void): void;
protected _closeSocket(): Promise<void>;
protected abstract _hasRuntime(): boolean;
protected abstract _closeRuntime(): Promise<void>;
protected abstract _runtimeType(): string;
}
export { BaseEnvRunner, EnvRunnerData, VirtualModuleSource, VirtualModules };
import { resolveVirtualModules } from "./virtual-loader.mjs";
import { rm } from "node:fs/promises";
import { proxyFetch, proxyUpgrade } from "httpxy";
var BaseEnvRunner = class {
closed = false;
_name;
_workerEntry;
_data;
_virtualSources;
_hooks;
_address;
_messageListeners;
_pendingRequests;
_virtualResolved;
constructor(opts) {
this._name = opts.name;
this._workerEntry = opts.workerEntry;
this._data = opts.data;
this._hooks = opts.hooks || {};
this._messageListeners = /* @__PURE__ */ new Set();
this._pendingRequests = /* @__PURE__ */ new Set();
}
get ready() {
return Boolean(!this.closed && this._address && this._hasRuntime());
}
async fetch(input, init) {
for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
if (!this._address) return new Response(`${this._runtimeType()} env runner is unavailable`, { status: 503 });
return proxyFetch(this._address, this._resolveFetchInput(input), init);
}
async upgrade(context) {
if (!this.ready || !this._address) return;
await proxyUpgrade(this._address, context.node.req, context.node.socket, context.node.head);
}
onMessage(listener) {
this._messageListeners.add(listener);
}
offMessage(listener) {
this._messageListeners.delete(listener);
}
waitForReady(timeout = 5e3) {
if (this.ready) return Promise.resolve();
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this._messageListeners.delete(listener);
reject(/* @__PURE__ */ new Error("Runner did not become ready in time"));
}, timeout);
const listener = () => {
if (this.ready) {
clearTimeout(timer);
this._messageListeners.delete(listener);
resolve();
} else if (this.closed) {
clearTimeout(timer);
this._messageListeners.delete(listener);
reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
}
};
this._messageListeners.add(listener);
});
}
rpc(name, data, opts) {
const id = Math.random().toString(36).slice(2);
return this._request({
__rpc: name,
__rpc_id: id,
data
}, {
match: (msg) => msg?.__rpc_id === id,
timeout: opts?.timeout ?? 3e3,
timeoutError: `RPC "${name}" timed out`
}).then((msg) => msg.data);
}
async reloadModule(timeout = 5e3) {
await this._request({ event: "reload-module" }, {
match: (msg) => msg?.event === "module-reloaded",
timeout,
timeoutError: "Module reload timed out"
});
}
async invalidateModule(specifier, timeout = 5e3) {
const source = await this._refreshVirtualSource(specifier);
await this._request({
event: "invalidate-module",
specifier,
source
}, {
match: (msg) => msg?.event === "module-invalidated" && msg.specifier === specifier,
timeout,
timeoutError: `Module invalidation timed out for "${specifier}"`
});
}
async close(cause) {
if (this.closed) return;
this.closed = true;
for (const rejectPending of this._pendingRequests) rejectPending(cause);
this._pendingRequests.clear();
this._hooks.onClose?.(this, cause);
this._hooks = {};
const onError = (error) => console.error(error);
await this._closeRuntime().catch(onError);
await this._closeSocket().catch(onError);
}
async [Symbol.asyncDispose]() {
await this.close();
}
[Symbol.for("nodejs.util.inspect.custom")]() {
const status = this.closed ? "closed" : this.ready ? "ready" : "pending";
return `${this.constructor.name}#${this._name}(${status})`;
}
_resolveFetchInput(input) {
if (typeof input === "string" && !URL.canParse(input)) return new URL(input, "http://localhost");
return input;
}
_handleMessage(message) {
if (message?.address) {
this._address = message.address;
this._hooks.onReady?.(this, this._address);
}
if (message?.event === "init-error" && !this.ready && !this.closed) this.close(new Error(String(message.error || "Worker initialization failed")));
for (const listener of this._messageListeners) listener(message);
}
_request(message, opts) {
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner is closed"));
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
reject(new Error(opts.timeoutError));
}, opts.timeout);
const listener = (msg) => {
if (opts.match(msg)) {
cleanup();
if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
else resolve(msg);
}
};
const onClose = (cause) => {
cleanup();
reject(new Error("Runner closed before responding", cause ? { cause } : void 0));
};
const cleanup = () => {
clearTimeout(timer);
this.offMessage(listener);
this._pendingRequests.delete(onClose);
};
this.onMessage(listener);
this._pendingRequests.add(onClose);
try {
(opts.send ?? ((m) => this.sendMessage(m)))(message);
} catch (error) {
cleanup();
reject(error);
}
});
}
_resolveVirtualData() {
const virtual = this._data?.virtual;
this._virtualSources = virtual;
if (!virtual || !Object.values(virtual).some((v) => typeof v === "function")) return;
this._virtualResolved = resolveVirtualModules(virtual).then((resolved) => {
this._data = {
...this._data,
virtual: resolved
};
});
return this._virtualResolved;
}
async _refreshVirtualSource(specifier) {
await this._virtualResolved?.catch(() => {});
const original = this._virtualSources?.[specifier];
if (typeof original !== "function") return;
const source = await original();
const resolved = this._data?.virtual;
if (resolved) resolved[specifier] = source;
return source;
}
_initWithVirtualData(init) {
const pending = this._resolveVirtualData();
if (pending) pending.then(() => {
if (!this.closed) init();
}, (error) => this.close(error));
else init();
}
async _closeSocket() {
const socketPath = this._address?.socketPath;
if (socketPath && socketPath[0] !== "\0" && !socketPath.startsWith(String.raw`\\.\\pipe`)) await rm(socketPath).catch(() => {});
this._address = void 0;
}
};
export { BaseEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "./base-runner.mjs";
declare class DenoProcessEnvRunner extends BaseEnvRunner {
#private;
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
execArgv?: string[];
});
sendMessage(message: unknown): void;
protected _hasRuntime(): boolean;
protected _runtimeType(): string;
protected _closeRuntime(): Promise<void>;
}
export { DenoProcessEnvRunner };
import { BaseEnvRunner } from "./base-runner.mjs";
import { existsSync } from "node:fs";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
let _defaultEntry;
var DenoProcessEnvRunner = class extends BaseEnvRunner {
#process;
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/deno-process/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
this._initWithVirtualData(() => this.#initProcess(opts.execArgv));
}
sendMessage(message) {
if (!this.#process) throw new Error("Deno env process should be initialized before sending messages.");
this.#process.send(message);
}
_hasRuntime() {
return Boolean(this.#process);
}
_runtimeType() {
return "process";
}
async _closeRuntime() {
if (!this.#process) return;
this.#process.removeAllListeners?.();
try {
this.#process.kill();
} catch {}
this.#process = void 0;
}
#initProcess(execArgv) {
if (!existsSync(this._workerEntry)) {
this.close(`process entry not found in "${this._workerEntry}".`);
return;
}
const env = {
...process.env,
ENV_RUNNER_NAME: this._name,
ENV_RUNNER_DATA: JSON.stringify(this._data || {})
};
const child = spawn("deno", [
"run",
"-A",
"--node-modules-dir=auto",
"--no-lock",
...execArgv || [],
this._workerEntry
], {
env,
stdio: [
"pipe",
"pipe",
"pipe"
]
});
const exited = new Promise((resolve) => {
child.once("exit", (code) => resolve(code ?? 1));
});
const handle = {
pid: child.pid,
kill: () => child.kill(),
send: (message) => {
child.stdin.write(JSON.stringify(message) + "\n");
},
exited,
_exitCode: void 0,
removeAllListeners: () => child.removeAllListeners()
};
child.once("exit", (code) => {
handle._exitCode = code;
this.close(`process exited with code ${code}`);
});
child.on("error", (error) => {
if (!this.closed) {
console.error(`Process error:`, error);
this.close(error);
}
});
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
let newlineIdx;
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, newlineIdx);
buffer = buffer.slice(newlineIdx + 1);
if (line.startsWith("{")) try {
this._handleMessage(JSON.parse(line));
continue;
} catch {}
process.stdout.write(line + "\n");
}
});
child.stderr?.pipe(process.stderr);
this.#process = handle;
}
};
export { DenoProcessEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "./base-runner.mjs";
import { IncomingMessage } from "node:http";
import { Socket } from "node:net";
/** Result from a module transform (compatible with Vite's `TransformResult`). */
interface TransformResult {
code: string;
}
/** Detected or declared export for auto-wiring Durable Object / Entrypoint bindings. */
interface MiniflareExportInfo {
type?: "DurableObject" | "WorkerEntrypoint" | "class";
}
interface MiniflareEnvRunnerOptions {
name: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
/** Options passed directly to the Miniflare constructor. */
miniflareOptions?: Record<string, unknown>;
/**
* Optional module transform callback. When provided, the module fallback
* service calls this instead of reading raw files from disk.
*
* This enables integration with Vite's transform pipeline — pass
* `environment.transformRequest` to get TS/JSX/etc. compiled on the fly.
*
* @param id - Absolute file path of the module to transform
* @returns Transformed code, or null/undefined to fall back to raw disk read
*/
transformRequest?: (id: string) => Promise<TransformResult | null | undefined>;
/**
* Declare named exports (Durable Objects, WorkerEntrypoints) to auto-wire
* bindings and generate re-exports in the wrapper module.
*
* When set to `true`, `export class` declarations are auto-detected from
* the entry file. When set to a record, the listed exports are used
* (merged with auto-detected ones). Disabled by default.
*/
exports?: Record<string, MiniflareExportInfo> | boolean;
/**
* When `true`, the Miniflare instance is cached and reused across runner
* swaps (e.g. via `RunnerManager.reload()`). `close()` tears down IPC but
* keeps Miniflare alive. Call `dispose()` to fully destroy it.
*/
persistent?: boolean;
/** Wrap the user's `fetch` in a try/catch that returns structured JSON error responses. Default: `true`. */
captureErrors?: boolean;
/**
* Export conditions for bare-specifier module resolution in the module
* fallback service. Ensures packages with conditional exports (e.g.
* `"workerd"`) resolve to the correct entry instead of the Node.js one.
*
* Defaults to `["workerd", "worker"]`.
*/
exportConditions?: string[];
}
declare class MiniflareEnvRunner extends BaseEnvRunner {
#private;
constructor(opts: MiniflareEnvRunnerOptions);
/** Dispose all persistent Miniflare instances from the cache. */
static disposeAll(): Promise<void>;
/** Fully dispose the Miniflare instance (even if persistent). */
dispose(): Promise<void>;
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
sendMessage(message: unknown): void;
/**
* Hot-reload the user entry module without recreating the Miniflare instance.
*
* Sends `reload-module` event over the WebSocket. The worker wrapper uses
* `unsafeEvalBinding` to re-import the entry with a cache-busting query string
* and responds with `module-reloaded` when done.
*/
reloadModule(timeout?: number): Promise<void>;
/**
* Invalidate a virtual module so the next `reloadModule()` re-evaluates it.
*
* Host-side only (no worker round-trip): the module fallback service serves
* virtual sources from a live map, so re-running a factory source and
* bumping the per-specifier versions — the module plus its transitive
* virtual importers — is enough. Import specifiers in re-served module code
* are rewritten to the versioned form, giving workerd fresh module
* identities (it caches by name). A `persistent` instance is evicted from
* the cache, since its served sources no longer match the cache key.
*/
invalidateModule(specifier: string, _timeout?: number): Promise<void>;
protected _hasRuntime(): boolean;
protected _runtimeType(): string;
protected _closeRuntime(): Promise<void>;
upgrade(context: {
node: {
req: IncomingMessage;
socket: Socket;
head: any;
};
}): Promise<void>;
}
export { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult };
import { expandVirtualInvalidation, stripVirtualTypeScript, virtualModuleFormat } from "./virtual-loader.mjs";
import { BaseEnvRunner } from "./base-runner.mjs";
import { init, parse } from "./libs/cjs-module-lexer.mjs";
import { init as init$1, parse as parse$1 } from "./libs/es-module-lexer.mjs";
import { isVirtualSpecifier } from "./worker-utils.mjs";
import { createRequire } from "node:module";
import { proxyUpgrade } from "httpxy";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { basename, dirname, isAbsolute, resolve } from "node:path";
import { resolveModulePath } from "exsolve";
const IPC_PATH = "/__env_runner_ipc";
const IPC_BINDING = "__ENV_RUNNER_IPC";
function generateWrapper(entryPath, opts) {
const staticReExport = opts?.dynamicOnly ? "" : `export * from ${JSON.stringify(entryPath)};`;
const explicitExports = opts?.dynamicOnly && opts.exports?.length ? opts.exports.map((name) => `export { ${name} } from ${JSON.stringify(entryPath)};`).join("\n") : "";
const fetchBody = opts?.captureErrors ?? true ? `try {
return await entryFetch(request, env, ctx);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
const body = JSON.stringify({
error: error.message,
stack: error.stack,
name: error.constructor?.name || "Error",
});
return new Response(body, {
status: 500,
headers: { "Content-Type": "application/json", "X-Env-Runner-Error": "1" },
});
}` : `return entryFetch(request, env, ctx);`;
return `import __process from "node:process";
if (!globalThis.process) { globalThis.process = __process; }
${staticReExport}
${explicitExports}
const __IPC_PATH = "${IPC_PATH}";
const __IPC_BINDING = "${IPC_BINDING}";
const __entryPath = ${JSON.stringify(entryPath)};
let __userEntry;
let __ipcInitialized = false;
let __serverWs;
let __currentEnv;
async function __loadEntry(env, path) {
globalThis.__ENV_RUNNER_UNSAFE_EVAL__ = env.__ENV_RUNNER_UNSAFE_EVAL__;
const importFn = env.__ENV_RUNNER_UNSAFE_EVAL__.newAsyncFunction(
"return await import(path)",
"loadEntry",
"path"
);
const mod = await importFn(path);
return mod.default || mod;
}
function __sendMessage(message) {
const payload = JSON.stringify(message);
const env = __currentEnv;
if (env && env[__IPC_BINDING]) {
env[__IPC_BINDING].fetch("http://localhost/__ipc", {
method: "POST",
body: payload,
}).catch(() => {});
return;
}
if (__serverWs) {
__serverWs.send(payload);
}
}
async function __handleWsMessage(env, data) {
let msg;
try { msg = JSON.parse(data); } catch { return; }
if (msg.type === "message") {
if (__userEntry?.ipc?.onMessage) {
__userEntry.ipc.onMessage(msg.data);
}
return;
}
if (msg.type === "reload" && env.__ENV_RUNNER_UNSAFE_EVAL__) {
const version = msg.version || 0;
try {
const newEntry = await __loadEntry(env, __entryPath + "?t=" + version);
if (__userEntry?.ipc?.onClose) {
await __userEntry.ipc.onClose();
}
__userEntry = newEntry;
__crosswsAdapter = undefined;
__ipcInitialized = false;
if (__userEntry.ipc?.onOpen) {
__ipcInitialized = true;
await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
}
__sendMessage({ event: "module-reloaded" });
} catch (e) {
__sendMessage({ event: "module-reloaded", error: String(e) });
}
return;
}
if (msg.type === "shutdown") {
if (__userEntry?.ipc?.onClose) {
await __userEntry.ipc.onClose();
}
return;
}
}
let __crosswsAdapter;
async function __initCrossws(env, hooks) {
if (__crosswsAdapter) return __crosswsAdapter;
const importFn = env.__ENV_RUNNER_UNSAFE_EVAL__.newAsyncFunction(
"return await import('crossws/adapters/cloudflare')",
"loadCrossws"
);
const { default: cloudflareAdapter } = await importFn();
__crosswsAdapter = cloudflareAdapter({ hooks });
return __crosswsAdapter;
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// WebSocket IPC handshake
if (url.pathname === __IPC_PATH && request.headers.get("upgrade") === "websocket") {
try {
if (!__userEntry) {
__userEntry = await __loadEntry(env, __entryPath);
}
} catch (e) {
return new Response("Failed to load entry: " + String(e), { status: 500 });
}
const pair = new WebSocketPair();
const client = pair[0];
const server = pair[1];
server.accept();
__serverWs = server;
server.addEventListener("message", (event) => {
__handleWsMessage(env, event.data);
});
// Initialize IPC hooks
if (!__ipcInitialized && __userEntry.ipc) {
__ipcInitialized = true;
if (__userEntry.ipc.onOpen) {
await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
}
}
return new Response(null, { status: 101, webSocket: client });
}
if (!__userEntry) {
return new Response("Worker not initialized", { status: 503 });
}
// Handle WebSocket upgrade via crossws cloudflare adapter
if (__userEntry.websocket && request.headers.get("upgrade") === "websocket") {
const adapter = await __initCrossws(env, __userEntry.websocket);
return adapter.handleUpgrade(request, env, ctx);
}
const entryFetch = __userEntry.fetch;
if (!entryFetch) {
return new Response("No fetch handler exported", { status: 500 });
}
__currentEnv = env;
try {
${fetchBody}
} finally {
__currentEnv = undefined;
}
}
};
`;
}
const _miniflareCache = /* @__PURE__ */ new Map();
var MiniflareEnvRunner = class extends BaseEnvRunner {
#miniflare;
#miniflareOptions;
#transformRequest;
#reloadCounter = 0;
#virtual;
#virtualVersions = /* @__PURE__ */ new Map();
#cacheEntry;
#ws;
#persistent;
#cacheKey;
#exports;
#captureErrors;
#exportConditions;
constructor(opts) {
super({
...opts,
workerEntry: ""
});
this.#miniflareOptions = opts.miniflareOptions || {};
this.#transformRequest = opts.transformRequest;
this.#persistent = opts.persistent ?? false;
this.#exports = opts.exports ?? {};
this.#captureErrors = opts.captureErrors ?? true;
this.#exportConditions = opts.exportConditions ?? ["workerd", "worker"];
this._initWithVirtualData(() => this.#init());
}
static async disposeAll() {
const entries = [..._miniflareCache.values()];
_miniflareCache.clear();
for (const entry of entries) await entry.mf.dispose().catch(() => {});
}
async dispose() {
if (this.#miniflare) {
if (this.#ws) {
this.#ws.send(JSON.stringify({ type: "shutdown" }));
this.#ws.close();
this.#ws = void 0;
}
if (this.#cacheKey && _miniflareCache.get(this.#cacheKey)?.mf === this.#miniflare) _miniflareCache.delete(this.#cacheKey);
await this.#miniflare.dispose();
this.#miniflare = void 0;
}
if (!this.closed) await this.close();
}
async fetch(input, init) {
for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
if (!this.#miniflare || this.closed) return new Response("miniflare env runner is unavailable", { status: 503 });
const resolved = this._resolveFetchInput(input);
const url = typeof resolved === "string" ? resolved : resolved instanceof URL ? resolved.href : resolved.url;
const res = await this.#miniflare.dispatchFetch(url, init);
if (res instanceof Response) return res;
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: res.headers
});
}
sendMessage(message) {
if (!this.#ws) throw new Error("Miniflare env runner should be initialized before sending messages.");
if (message?.type === "ping") {
queueMicrotask(() => this._handleMessage({
type: "pong",
data: message.data
}));
return;
}
this.#ws.send(JSON.stringify({
type: "message",
data: message
}));
}
async reloadModule(timeout = 5e3) {
if (!this.#ws) throw new Error("Miniflare env runner should be initialized before reloading.");
if (!this._data?.entry) return;
this.#reloadCounter++;
await this._request({
type: "reload",
version: this.#reloadCounter
}, {
match: (msg) => msg?.event === "module-reloaded",
timeout,
timeoutError: "Module reload timed out",
send: (message) => this.#ws.send(JSON.stringify(message))
});
}
async invalidateModule(specifier, _timeout) {
const virtual = this.#virtual;
if (!virtual || !Object.hasOwn(virtual, specifier)) {
const hasVirtual = Object.keys(this._data?.virtual ?? {}).length > 0;
throw !virtual && hasVirtual && !this.closed ? /* @__PURE__ */ new Error("Miniflare env runner should be initialized before invalidating modules.") : /* @__PURE__ */ new Error(`Cannot invalidate "${specifier}" (not a registered virtual module)`);
}
const source = await this._refreshVirtualSource(specifier);
if (source !== void 0) virtual[specifier] = await this.#prepareVirtualSource(specifier, source);
for (const key of expandVirtualInvalidation(virtual, specifier)) this.#virtualVersions.set(key, (this.#virtualVersions.get(key) ?? 0) + 1);
if (this.#cacheKey && _miniflareCache.get(this.#cacheKey) === this.#cacheEntry) _miniflareCache.delete(this.#cacheKey);
}
_hasRuntime() {
return Boolean(this.#miniflare);
}
_runtimeType() {
return "miniflare";
}
async _closeRuntime() {
if (!this.#miniflare) return;
if (this.#ws) {
this.#ws.send(JSON.stringify({ type: "shutdown" }));
this.#ws.close();
this.#ws = void 0;
}
const entry = this.#cacheEntry;
if (entry) {
entry.refCount--;
if (entry.refCount <= 0) {
if (this.#cacheKey && _miniflareCache.get(this.#cacheKey) === entry) _miniflareCache.delete(this.#cacheKey);
await this.#miniflare.dispose();
}
} else await this.#miniflare.dispose();
this.#miniflare = void 0;
}
#init() {
this.#initAsync().catch((error) => {
console.error("Miniflare runner init error:", error);
this.close(error);
});
}
async upgrade(context) {
if (!this.#miniflare || this.closed) {
context.node.socket.destroy();
return;
}
const mfUrl = await this.#miniflare.unsafeGetDirectURL();
const address = new URL(mfUrl);
await proxyUpgrade({
host: address.hostname,
port: Number(address.port)
}, context.node.req, context.node.socket, context.node.head);
}
async #prepareVirtualModules() {
const virtual = this._data?.virtual;
if (!virtual || Object.keys(virtual).length === 0) return;
const out = {};
for (const [specifier, source] of Object.entries(virtual)) out[specifier] = await this.#prepareVirtualSource(specifier, source);
return out;
}
async #prepareVirtualSource(specifier, source) {
if (virtualModuleFormat(specifier) !== "module-typescript") return source;
return stripVirtualTypeScript(specifier, source, await _getStripTypeScriptTypes(), {
requirement: "on the host (workerd does not parse TypeScript)",
remedy: "upgrade Node.js"
});
}
async #initAsync() {
const { Miniflare } = await import("miniflare");
const entryPath = this._data?.entry;
const virtual = await this.#prepareVirtualModules();
this.#virtual = virtual;
const userFlags = this.#miniflareOptions.compatibilityFlags || [];
const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || [];
const options = {
compatibilityDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
modules: true,
...this.#miniflareOptions,
compatibilityFlags: [...new Set(["nodejs_compat", ...userFlags])],
unsafeDirectSockets: [{
host: "127.0.0.1",
port: 0
}, ...userDirectSockets]
};
if (entryPath && !options.script && !options.scriptPath) {
const entryIsVirtual = isVirtualSpecifier(entryPath, virtual);
const resolvedEntry = entryIsVirtual ? entryPath : resolve(entryPath);
const entryBase = isAbsolute(resolvedEntry) ? resolvedEntry : resolve("__env_runner_virtual_entry__.mjs");
const entryDir = dirname(entryBase);
const entrySource = entryIsVirtual ? virtual[entryPath] : _tryReadFile(resolvedEntry);
const detectedExports = this.#exports === false || this.#exports === void 0 ? [] : detectExportedClasses(entrySource, typeof this.#exports === "object" ? this.#exports : {});
if (entryIsVirtual && detectedExports.length > 0) throw new Error(`[env-runner] named exports (${detectedExports.join(", ")}) are not supported with a virtual entry on the miniflare runner; pass \`exports: false\` or use a real entry file.`);
if (detectedExports.length > 0 && !options.durableObjects) {
const autoDOs = { ...this.#miniflareOptions.durableObjects || {} };
for (const name of detectedExports) {
const bindingName = toScreamingSnakeCase(name);
if (!autoDOs[bindingName]) autoDOs[bindingName] = name;
}
options.durableObjects = autoDOs;
}
options.script = generateWrapper(resolvedEntry, {
dynamicOnly: true,
captureErrors: this.#captureErrors,
exports: detectedExports
});
options.scriptPath = entryDir + "/__env_runner_wrapper.mjs";
if (!options.modulesRoot) options.modulesRoot = "/";
options.unsafeEvalBinding = "__ENV_RUNNER_UNSAFE_EVAL__";
options.serviceBindings = {
...options.serviceBindings || {},
[IPC_BINDING]: async (request) => {
try {
const message = await request.json();
this._handleMessage(message);
} catch {}
return new Response(null, { status: 204 });
}
};
if (this.#transformRequest && !options.modulesRules) options.modulesRules = [{
type: "ESModule",
include: [
"**/*.ts",
"**/*.tsx",
"**/*.jsx",
"**/*.mts"
]
}];
if (!options.unsafeModuleFallbackService) {
const _require = createRequire(entryBase);
const _virtual = virtual;
const _virtualVersions = this.#virtualVersions;
const _transformRequest = this.#transformRequest;
const _exportConditions = this.#exportConditions;
const _applyVirtualVersions = (code) => applyVirtualVersions(code, _virtualVersions);
options.unsafeUseModuleFallbackService = true;
const modulePathMap = /* @__PURE__ */ new Map();
const _lexersReady = Promise.all([ensureCjsLexer(), init$1]);
options.unsafeModuleFallbackService = async (request) => {
await _lexersReady;
const url = new URL(request.url);
const specifier = url.searchParams.get("specifier");
const rawSpecifier = url.searchParams.get("rawSpecifier");
const referrer = url.searchParams.get("referrer") || "";
if (!specifier) return new Response(null, { status: 404 });
const cleanSpecifier = specifier.split("?")[0] || specifier;
const cleanRaw = rawSpecifier?.split("?")[0];
if (_virtual) {
const bareSpecifier = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
const virtualKey = [
cleanRaw,
cleanSpecifier,
bareSpecifier
].find((key) => key !== void 0 && Object.hasOwn(_virtual, key));
if (virtualKey !== void 0) {
const name = bareSpecifier + (specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "");
const source = _virtual[virtualKey];
return virtualModuleFormat(virtualKey) === "json" ? Response.json({
name,
json: source
}) : Response.json({
name,
esModule: _applyVirtualVersions(source)
});
}
}
let resolvedPath;
const fileUrlRaw = cleanRaw || cleanSpecifier;
if (fileUrlRaw.startsWith("file://")) try {
resolvedPath = fileURLToPath(fileUrlRaw);
} catch {
return new Response(null, { status: 404 });
}
else if (cleanRaw && !cleanRaw.startsWith(".") && !cleanRaw.startsWith("/")) {
const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
const referrerReal = modulePathMap.get(referrerKey);
const contextRequire = referrerReal ? createRequire(referrerReal) : _require;
if (cleanRaw.startsWith("cloudflare:")) return new Response(null, { status: 404 });
if (cleanRaw.startsWith("node:")) {
const nodeName = cleanRaw.slice(5);
try {
resolvedPath = contextRequire.resolve(`unenv/node/${nodeName}`);
} catch {
return new Response(null, { status: 404 });
}
} else try {
resolvedPath = resolveModulePath(cleanRaw, {
from: referrerReal || entryBase,
conditions: _exportConditions,
try: true
}) || contextRequire.resolve(cleanRaw);
} catch {
const name = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
return Response.json({
name,
esModule: "export default undefined;"
});
}
} else {
const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
const referrerDir = dirname(modulePathMap.get(referrerKey) || (referrer.startsWith("/") ? referrer : "/" + referrer));
const raw = cleanRaw || cleanSpecifier;
if (raw.startsWith(".")) resolvedPath = resolve(referrerDir, raw);
else if (cleanSpecifier.startsWith("/")) resolvedPath = cleanSpecifier;
else try {
resolvedPath = _require.resolve(raw);
} catch {
return new Response(null, { status: 404 });
}
}
const rawQuery = specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "";
const name = (cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier) + rawQuery;
if (_transformRequest) try {
const result = await _transformRequest(resolvedPath);
if (result?.code) {
modulePathMap.set(name, resolvedPath);
return Response.json({
name,
esModule: _applyVirtualVersions(result.code)
});
}
} catch {}
try {
const contents = readFileSync(resolvedPath, "utf8");
modulePathMap.set(name, resolvedPath);
if (resolvedPath.endsWith(".mjs") || !resolvedPath.endsWith(".cjs") && /\b(import\s|import\(|export\s|export\{|import\.meta\b)/.test(contents)) return Response.json({
name,
esModule: _applyVirtualVersions(contents)
});
const cjsSuffix = "?__cjs";
if (specifier.endsWith(cjsSuffix)) return Response.json({
name,
commonJsModule: contents
});
const esModule = createCjsEsmShim("./" + basename(resolvedPath) + cjsSuffix, contents);
return Response.json({
name,
esModule
});
} catch {
return new Response(null, { status: 404 });
}
};
}
}
if (this.#persistent && entryPath) {
this.#cacheKey = computeCacheKey(entryPath, {
...this.#miniflareOptions,
_exportConditions: this.#exportConditions,
_virtual: virtual
});
const cached = _miniflareCache.get(this.#cacheKey);
if (cached) {
this.#miniflare = cached.mf;
cached.refCount++;
this.#cacheEntry = cached;
this.#virtual = cached.virtual;
this.#virtualVersions = cached.versions;
}
}
if (!this.#miniflare) {
this.#miniflare = new Miniflare(options);
await this.#miniflare.ready;
if (this.#persistent && this.#cacheKey) {
this.#cacheEntry = {
mf: this.#miniflare,
refCount: 1,
virtual,
versions: this.#virtualVersions
};
_miniflareCache.set(this.#cacheKey, this.#cacheEntry);
}
}
const initRes = await this.#miniflare.dispatchFetch("http://localhost/__env_runner_ipc", { headers: { upgrade: "websocket" } });
const ws = initRes.webSocket;
if (!ws) {
const body = await initRes.text().catch(() => "");
throw new Error(`Failed to establish WebSocket IPC channel (${initRes.status}: ${body})`);
}
ws.accept();
this.#ws = ws;
ws.addEventListener("message", (event) => {
try {
const parsed = JSON.parse(event.data);
this._handleMessage(parsed);
} catch {}
});
this._handleMessage({ address: {
host: "127.0.0.1",
port: 0
} });
}
};
function detectExportedClasses(entrySource, explicit) {
const names = new Set(Object.keys(explicit));
if (entrySource) {
const re = /\bexport\s+class\s+(\w+)/g;
let match;
while (match = re.exec(entrySource)) if (match[1]) names.add(match[1]);
}
return [...names];
}
function _tryReadFile(path) {
try {
return readFileSync(path, "utf8");
} catch {
return;
}
}
function toScreamingSnakeCase(name) {
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
}
function computeCacheKey(entryPath, opts) {
const serializableOpts = {};
for (const [k, v] of Object.entries(opts)) if (typeof v !== "function") serializableOpts[k] = v;
return `${resolve(entryPath)}::${JSON.stringify(serializableOpts)}`;
}
function applyVirtualVersions(code, versions) {
if (versions.size === 0) return code;
let imports;
try {
[imports] = parse$1(code);
} catch {
return code;
}
let out = "";
let last = 0;
for (const imp of imports) {
let specifier = imp.n;
if (specifier === void 0 && imp.d > -1) {
const expr = code.slice(imp.s, imp.e);
if (expr.length > 1 && expr[0] === "`" && expr.endsWith("`") && !expr.includes("${")) specifier = expr.slice(1, -1);
}
const version = specifier === void 0 ? void 0 : versions.get(specifier);
if (!version) continue;
const versioned = `${specifier}?v=${version}`;
out += code.slice(last, imp.s) + (imp.d > -1 ? JSON.stringify(versioned) : versioned);
last = imp.e;
}
return out + code.slice(last);
}
let _stripTypesPromise;
function _getStripTypeScriptTypes() {
_stripTypesPromise ??= import("node:module").then((m) => m.stripTypeScriptTypes);
return _stripTypesPromise;
}
let _cjsLexerReady;
function ensureCjsLexer() {
if (!_cjsLexerReady) _cjsLexerReady = init();
return _cjsLexerReady;
}
function createCjsEsmShim(cjsSpecifier, contents) {
let namedExports = [];
try {
const { exports } = parse(contents);
namedExports = exports.filter((e) => e !== "default" && e !== "__esModule");
} catch {}
let shim = `import __cjs_mod__ from ${JSON.stringify(cjsSpecifier)};\nexport default __cjs_mod__;\n`;
for (const name of namedExports) shim += `export var ${name} = __cjs_mod__["${name}"];\n`;
return shim;
}
export { MiniflareEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { BaseEnvRunner, EnvRunnerData } from "./base-runner.mjs";
declare class NodeWorkerEnvRunner extends BaseEnvRunner {
#private;
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
sendMessage(message: unknown): void;
protected _hasRuntime(): boolean;
protected _runtimeType(): string;
protected _closeRuntime(): Promise<void>;
}
export { NodeWorkerEnvRunner };
import { BaseEnvRunner } from "./base-runner.mjs";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { Worker } from "node:worker_threads";
let _defaultEntry;
var NodeWorkerEnvRunner = class extends BaseEnvRunner {
#worker;
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/node-worker/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
this._initWithVirtualData(() => this.#initWorker());
}
sendMessage(message) {
if (!this.#worker) throw new Error("Worker thread should be initialized before sending messages.");
this.#worker.postMessage(message);
}
_hasRuntime() {
return Boolean(this.#worker);
}
_runtimeType() {
return "worker";
}
async _closeRuntime() {
if (!this.#worker) return;
this.#worker.removeAllListeners();
await this.#worker.terminate().catch((error) => {
console.error(error);
});
this.#worker = void 0;
}
#initWorker() {
if (!existsSync(this._workerEntry)) {
this.close(`worker entry not found in "${this._workerEntry}".`);
return;
}
const worker = new Worker(this._workerEntry, {
env: { ...process.env },
workerData: {
name: this._name,
...this._data
}
});
worker.once("exit", (code) => {
worker._exitCode = code;
this.close(`worker exited with code ${code}`);
});
worker.once("error", (error) => {
console.error(`Worker error:`, error);
this.close(error);
});
worker.on("message", (message) => {
this._handleMessage(message);
});
this.#worker = worker;
}
};
export { NodeWorkerEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { EnvRunnerData } from "./base-runner.mjs";
import { NodeWorkerEnvRunner } from "./runner3.mjs";
declare class VercelEnvRunner extends NodeWorkerEnvRunner {
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
protected _runtimeType(): string;
}
export { VercelEnvRunner };
import { NodeWorkerEnvRunner } from "./runner3.mjs";
import { fileURLToPath } from "node:url";
import { randomBytes } from "node:crypto";
let _warned = false;
function warnIfVercelOidcTokenInvalid(token) {
const result = _checkVercelOidcToken(token);
if (_warned) return result;
if (result.status === "missing") {
_warned = true;
console.warn(`\x1B[90m[env-runner]\x1B[39m \x1B[33mVERCEL_OIDC_TOKEN\x1B[39m is not set. Run \x1B[36mvercel env pull\x1B[39m to pull the latest environment variables.`);
} else if (result.status === "expired") {
_warned = true;
console.warn(`\x1B[90m[env-runner]\x1B[39m \x1B[33mVERCEL_OIDC_TOKEN\x1B[39m expired at ${result.expiresAt.toISOString()}. Run \x1B[36mvercel env pull\x1B[39m to pull a fresh OIDC token.`);
} else if (result.status === "invalid") {
_warned = true;
console.warn(`\x1B[90m[env-runner]\x1B[39m \x1B[33mVERCEL_OIDC_TOKEN\x1B[39m is malformed (not a valid JWT). Run \x1B[36mvercel env pull\x1B[39m to pull a valid OIDC token.`);
}
return result;
}
function _checkVercelOidcToken(token = process.env.VERCEL_OIDC_TOKEN) {
if (!token) return { status: "missing" };
const parts = token.split(".");
if (parts.length !== 3) return { status: "invalid" };
let payload;
try {
const json = Buffer.from(parts[1], "base64url").toString("utf8");
payload = JSON.parse(json);
} catch {
return { status: "invalid" };
}
if (!payload || typeof payload !== "object") return { status: "invalid" };
const exp = payload.exp;
if (typeof exp !== "number" || !Number.isFinite(exp)) return { status: "invalid" };
const expiresAt = /* @__PURE__ */ new Date(exp * 1e3);
if (expiresAt.getTime() <= Date.now()) return {
status: "expired",
expiresAt
};
return {
status: "valid",
expiresAt
};
}
let _defaultEntry;
const _podId = Math.random().toString(32).slice(-5);
function generateVercelId() {
return `dev1::${_podId}-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`;
}
var VercelEnvRunner = class extends NodeWorkerEnvRunner {
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/vercel/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
warnIfVercelOidcTokenInvalid();
}
async fetch(input, init) {
input = this._resolveFetchInput(input);
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
const requestId = generateVercelId();
if (this._address && this._address.port != null && !headers.has("x-vercel-deployment-url")) {
const host = this._address.host || "127.0.0.1";
headers.set("x-vercel-deployment-url", `http://${host}:${this._address.port}`);
}
if (!headers.has("x-vercel-id")) headers.set("x-vercel-id", requestId);
const clientIp = headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || "127.0.0.1";
if (!headers.has("x-vercel-forwarded-for")) headers.set("x-vercel-forwarded-for", clientIp);
if (!headers.has("x-forwarded-for")) headers.set("x-forwarded-for", clientIp);
if (!headers.has("x-real-ip")) headers.set("x-real-ip", clientIp);
try {
const url = new URL(input instanceof Request ? input.url : input.toString());
if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", headers.get("host") || url.host);
} catch {}
const res = await super.fetch(input, {
...init,
headers
});
const resHeaders = new Headers(res.headers);
if (!resHeaders.has("server")) resHeaders.set("server", "Vercel");
if (!resHeaders.has("x-vercel-id")) resHeaders.set("x-vercel-id", requestId);
if (!resHeaders.has("x-vercel-cache")) resHeaders.set("x-vercel-cache", "MISS");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: resHeaders
});
}
_runtimeType() {
return "vercel";
}
};
export { VercelEnvRunner };
import { WorkerHooks } from "./types.mjs";
import { EnvRunnerData } from "./base-runner.mjs";
import { NodeWorkerEnvRunner } from "./runner3.mjs";
declare class NetlifyEnvRunner extends NodeWorkerEnvRunner {
constructor(opts: {
name: string;
workerEntry?: string;
hooks?: WorkerHooks;
data?: EnvRunnerData;
});
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
protected _runtimeType(): string;
}
export { NetlifyEnvRunner };
import { NodeWorkerEnvRunner } from "./runner3.mjs";
import { fileURLToPath } from "node:url";
let _defaultEntry;
var NetlifyEnvRunner = class extends NodeWorkerEnvRunner {
constructor(opts) {
_defaultEntry ||= fileURLToPath(import.meta.resolve("env-runner/runners/netlify/worker"));
super({
...opts,
workerEntry: opts.workerEntry || _defaultEntry
});
}
async fetch(input, init) {
input = this._resolveFetchInput(input);
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
const clientIp = headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || "127.0.0.1";
if (!headers.has("x-nf-client-connection-ip")) headers.set("x-nf-client-connection-ip", clientIp);
if (!headers.has("x-nf-account-id")) headers.set("x-nf-account-id", "0");
if (!headers.has("x-nf-site-id")) headers.set("x-nf-site-id", "0");
if (!headers.has("x-nf-deploy-id")) headers.set("x-nf-deploy-id", "0");
if (!headers.has("x-nf-deploy-context")) headers.set("x-nf-deploy-context", "dev");
if (!headers.has("x-nf-geo")) headers.set("x-nf-geo", btoa(JSON.stringify({
city: "localhost",
country: { code: "dev" }
})));
if (!headers.has("x-nf-request-id")) headers.set("x-nf-request-id", crypto.randomUUID());
if (!headers.has("x-forwarded-for")) headers.set("x-forwarded-for", clientIp);
if (!headers.has("x-real-ip")) headers.set("x-real-ip", clientIp);
try {
const url = new URL(input instanceof Request ? input.url : input.toString());
if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", headers.get("host") || url.host);
} catch {}
if (input instanceof Request) return super.fetch(new Request(input, {
...init,
headers
}));
return super.fetch(input, {
...init,
headers
});
}
_runtimeType() {
return "netlify";
}
};
export { NetlifyEnvRunner };
import { createVirtualHooks, expandVirtualInvalidation, stripVirtualTypeScript, virtualModuleFormat } from "./virtual-loader.mjs";
import { existsSync, readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { isAbsolute } from "node:path";
async function registerVirtualModules(virtual) {
if (!virtual || Object.keys(virtual).length === 0) return _noop;
const { registerHooks, stripTypeScriptTypes } = await import("node:module");
if (typeof registerHooks === "function") {
let transformSource;
if ("Deno" in globalThis) {
transformSource = (specifier, source) => _transformSourceForDeno(specifier, source, stripTypeScriptTypes);
const transformed = {};
for (const [specifier, source] of Object.entries(virtual)) transformed[specifier] = transformSource(specifier, source);
virtual = transformed;
}
const registration = {
virtual,
versions: /* @__PURE__ */ new Map(),
transformSource
};
const hooks = registerHooks(createVirtualHooks(virtual, registration.versions));
_hooksRegistrations.unshift(registration);
return _once(() => {
const index = _hooksRegistrations.indexOf(registration);
if (index !== -1) _hooksRegistrations.splice(index, 1);
hooks.deregister();
});
}
if (typeof globalThis.Bun?.plugin === "function") {
_bunVirtual = virtual;
_registerBunModules(Object.keys(virtual));
return _once(() => {
if (_bunVirtual === virtual) _bunVirtual = void 0;
});
}
console.warn("[env-runner] virtual modules require `module.registerHooks` (Node.js >= 22.15 / Deno >= 2.x) or `Bun.plugin`; skipping registration.");
return _noop;
}
function refreshVirtualModule(specifier) {
if (_bunVirtual?.[specifier] === void 0) return false;
_registerBunModules([specifier]);
return true;
}
function invalidateVirtualModule(specifier, source) {
for (const registration of _hooksRegistrations) {
if (!Object.hasOwn(registration.virtual, specifier)) continue;
const { virtual, versions, transformSource } = registration;
if (source !== void 0) virtual[specifier] = transformSource ? transformSource(specifier, source) : source;
for (const key of expandVirtualInvalidation(virtual, specifier)) versions.set(key, (versions.get(key) ?? 0) + 1);
return true;
}
if (_bunVirtual && Object.hasOwn(_bunVirtual, specifier)) {
if (source !== void 0) _bunVirtual[specifier] = source;
_registerBunModules(expandVirtualInvalidation(_bunVirtual, specifier));
return true;
}
return false;
}
function handleInvalidateModule(message, sendMessage) {
const ok = invalidateVirtualModule(message.specifier, message.source);
sendMessage({
event: "module-invalidated",
specifier: message.specifier,
error: ok ? void 0 : `Cannot invalidate "${message.specifier}" (not a registered virtual module)`
});
}
const _hooksRegistrations = [];
let _bunVirtual;
function _registerBunModules(specifiers) {
globalThis.Bun.plugin({
name: "env-runner-virtual",
setup(build) {
for (const specifier of specifiers) build.module(specifier, () => {
const source = _bunVirtual?.[specifier];
if (source === void 0) throw new Error(`Cannot find virtual module "${specifier}" (unregistered)`);
const format = virtualModuleFormat(specifier);
if (format === "json") return {
exports: { default: JSON.parse(source) },
loader: "object"
};
return {
contents: source,
loader: format === "module-typescript" ? "ts" : "js"
};
});
}
});
}
function _transformSourceForDeno(specifier, source, stripTypeScriptTypes) {
const format = virtualModuleFormat(specifier);
if (format === "module-typescript") return stripVirtualTypeScript(specifier, source, stripTypeScriptTypes, {
requirement: "(custom load hooks bypass Deno's native type stripping)",
remedy: "upgrade Deno"
});
if (format === "json") return `export default JSON.parse(${JSON.stringify(source)});`;
return source;
}
const _noop = () => {};
function _once(fn) {
let done = false;
return () => {
if (!done) {
done = true;
fn();
}
};
}
function isVirtualSpecifier(specifier, virtual) {
return Boolean(specifier && virtual && Object.hasOwn(virtual, specifier));
}
async function resolveEntry(entryPath, virtual) {
const mod = await (virtual ? import(entryPath) : import(_toImportPath(entryPath)));
const entry = mod.default || mod;
if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`);
return entry;
}
function parseServerAddress(server) {
const url = new URL(server.url);
return {
host: url.hostname,
port: Number(url.port)
};
}
async function reloadEntryModule(entryPath, currentEntry, sendMessage, virtual) {
await currentEntry.ipc?.onClose?.();
const newEntry = await _importFresh(entryPath, virtual);
await newEntry.ipc?.onOpen?.({ sendMessage });
return newEntry;
}
function _toImportPath(entryPath) {
const qIndex = entryPath.indexOf("?");
const filePath = qIndex === -1 ? entryPath : entryPath.slice(0, qIndex);
const query = qIndex === -1 ? "" : entryPath.slice(qIndex);
if (isAbsolute(filePath)) return pathToFileURL(filePath).href + query;
return entryPath;
}
let _reloadCounter = 0;
async function _importFresh(entryPath, virtual) {
const qIndex = entryPath.indexOf("?");
const filePath = qIndex === -1 ? entryPath : entryPath.slice(0, qIndex);
let mod;
if (!virtual && existsSync(filePath)) {
const code = readFileSync(filePath, "utf8");
mod = await import("data:text/javascript;base64," + Buffer.from(code).toString("base64"));
} else if (virtual && refreshVirtualModule(filePath)) mod = await import(filePath);
else mod = await import(entryPath + (qIndex === -1 ? "?" : "&") + "__envRunnerReload=" + _reloadCounter++);
const entry = mod.default || mod;
if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`);
return entry;
}
export { handleInvalidateModule, isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry };