env-runner
Advanced tools
| 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 }; |
@@ -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"; |
+5
-5
@@ -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 }; |
+1
-1
| { | ||
| "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 }; |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
188227
0.36%2877
0.28%32
3.23%