env-runner
Advanced tools
| async function resolveVirtualModules(virtual) { | ||
| const entries = await Promise.all(Object.entries(virtual).map(async ([key, value]) => [key, typeof value === "function" ? await value() : value])); | ||
| return Object.fromEntries(entries); | ||
| } | ||
| const VIRTUAL_SCHEME = "virtual:"; | ||
| function createVirtualHooks(virtual) { | ||
| const resolve = (specifier, context, nextResolve) => { | ||
| if (Object.hasOwn(virtual, _stripQuery(specifier))) return { | ||
| url: VIRTUAL_SCHEME + encodeURIComponent(specifier), | ||
| shortCircuit: true | ||
| }; | ||
| return nextResolve(specifier, context); | ||
| }; | ||
| const load = (url, context, nextLoad) => { | ||
| if (url.startsWith(VIRTUAL_SCHEME)) { | ||
| const key = _stripQuery(decodeURIComponent(url.slice(8))); | ||
| if (Object.hasOwn(virtual, key)) return { | ||
| format: virtualModuleFormat(key), | ||
| source: virtual[key], | ||
| shortCircuit: true | ||
| }; | ||
| } | ||
| return nextLoad(url, context); | ||
| }; | ||
| return { | ||
| resolve, | ||
| load | ||
| }; | ||
| } | ||
| function virtualModuleFormat(specifier) { | ||
| if (specifier.endsWith(".json")) return "json"; | ||
| if (specifier.endsWith(".ts") || specifier.endsWith(".mts")) return "module-typescript"; | ||
| return "module"; | ||
| } | ||
| function _stripQuery(specifier) { | ||
| const qIndex = specifier.indexOf("?"); | ||
| return qIndex === -1 ? specifier : specifier.slice(0, qIndex); | ||
| } | ||
| export { createVirtualHooks, resolveVirtualModules, virtualModuleFormat }; |
@@ -1,9 +0,36 @@ | ||
| import { a as RunnerMessageListener, l as WorkerAddress, t as EnvRunner, u as WorkerHooks } from "./types.mjs"; | ||
| 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 { | ||
| declare abstract class BaseEnvRunner implements EnvRunner, AsyncDisposable { | ||
| closed: boolean; | ||
@@ -40,3 +67,19 @@ protected _name: string; | ||
| close(cause?: unknown): Promise<void>; | ||
| [Symbol.asyncDispose](): Promise<void>; | ||
| protected _handleMessage(message: any): void; | ||
| /** | ||
| * 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; | ||
| /** | ||
| * 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>; | ||
@@ -47,2 +90,2 @@ protected abstract _hasRuntime(): boolean; | ||
| } | ||
| export { EnvRunnerData as n, BaseEnvRunner as t }; | ||
| export { BaseEnvRunner, EnvRunnerData, VirtualModuleSource, VirtualModules }; |
@@ -0,1 +1,2 @@ | ||
| import { resolveVirtualModules } from "./virtual-loader.mjs"; | ||
| import { rm } from "node:fs/promises"; | ||
@@ -38,2 +39,3 @@ import { proxyFetch, proxyUpgrade } from "httpxy"; | ||
| 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) => { | ||
@@ -49,2 +51,6 @@ const timer = setTimeout(() => { | ||
| resolve(); | ||
| } else if (this.closed) { | ||
| clearTimeout(timer); | ||
| this._messageListeners.delete(listener); | ||
| reject(/* @__PURE__ */ new Error("Runner closed before becoming ready")); | ||
| } | ||
@@ -112,2 +118,5 @@ }; | ||
| } | ||
| async [Symbol.asyncDispose]() { | ||
| await this.close(); | ||
| } | ||
| [Symbol.for("nodejs.util.inspect.custom")]() { | ||
@@ -122,4 +131,22 @@ const status = this.closed ? "closed" : this.ready ? "ready" : "pending"; | ||
| } | ||
| 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); | ||
| } | ||
| _resolveVirtualData() { | ||
| const virtual = this._data?.virtual; | ||
| if (!virtual || !Object.values(virtual).some((v) => typeof v === "function")) return; | ||
| return resolveVirtualModules(virtual).then((resolved) => { | ||
| this._data = { | ||
| ...this._data, | ||
| virtual: resolved | ||
| }; | ||
| }); | ||
| } | ||
| _initWithVirtualData(init) { | ||
| const pending = this._resolveVirtualData(); | ||
| if (pending) pending.then(() => { | ||
| if (!this.closed) init(); | ||
| }, (error) => this.close(error)); | ||
| else init(); | ||
| } | ||
| async _closeSocket() { | ||
@@ -131,2 +158,2 @@ const socketPath = this._address?.socketPath; | ||
| }; | ||
| export { BaseEnvRunner as t }; | ||
| export { BaseEnvRunner }; |
@@ -145,2 +145,2 @@ let A; | ||
| } | ||
| export { parse as n, init as t }; | ||
| export { init, parse }; |
@@ -1,3 +0,3 @@ | ||
| import { u as WorkerHooks } from "./types.mjs"; | ||
| import { n as EnvRunnerData, t as BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { WorkerHooks } from "./types.mjs"; | ||
| import { BaseEnvRunner, EnvRunnerData } from "./base-runner.mjs"; | ||
| declare class DenoProcessEnvRunner extends BaseEnvRunner { | ||
@@ -17,2 +17,2 @@ #private; | ||
| } | ||
| export { DenoProcessEnvRunner as t }; | ||
| export { DenoProcessEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { t as BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { existsSync } from "node:fs"; | ||
@@ -14,3 +14,3 @@ import { spawn } from "node:child_process"; | ||
| }); | ||
| this.#initProcess(opts.execArgv); | ||
| this._initWithVirtualData(() => this.#initProcess(opts.execArgv)); | ||
| } | ||
@@ -92,8 +92,11 @@ sendMessage(message) { | ||
| this._handleMessage(JSON.parse(line)); | ||
| continue; | ||
| } catch {} | ||
| process.stdout.write(line + "\n"); | ||
| } | ||
| }); | ||
| child.stderr?.pipe(process.stderr); | ||
| this.#process = handle; | ||
| } | ||
| }; | ||
| export { DenoProcessEnvRunner as t }; | ||
| export { DenoProcessEnvRunner }; |
@@ -1,3 +0,3 @@ | ||
| import { u as WorkerHooks } from "./types.mjs"; | ||
| import { n as EnvRunnerData, t as BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { WorkerHooks } from "./types.mjs"; | ||
| import { BaseEnvRunner, EnvRunnerData } from "./base-runner.mjs"; | ||
| import { IncomingMessage } from "node:http"; | ||
@@ -84,2 +84,2 @@ import { Socket } from "node:net"; | ||
| } | ||
| export { TransformResult as i, MiniflareEnvRunnerOptions as n, MiniflareExportInfo as r, MiniflareEnvRunner as t }; | ||
| export { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult }; |
+66
-16
@@ -1,3 +0,5 @@ | ||
| import { t as BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { n as parse, t as init } from "./libs/cjs-module-lexer.mjs"; | ||
| import { virtualModuleFormat } from "./virtual-loader.mjs"; | ||
| import { BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { init, parse } from "./libs/cjs-module-lexer.mjs"; | ||
| import { isVirtualSpecifier } from "./worker-utils.mjs"; | ||
| import { createRequire } from "node:module"; | ||
@@ -7,3 +9,3 @@ import { proxyUpgrade } from "httpxy"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { basename, dirname, resolve } from "node:path"; | ||
| import { basename, dirname, isAbsolute, resolve } from "node:path"; | ||
| import { resolveModulePath } from "exsolve"; | ||
@@ -203,3 +205,3 @@ const IPC_PATH = "/__env_runner_ipc"; | ||
| this.#exportConditions = opts.exportConditions ?? ["workerd", "worker"]; | ||
| this.#init(); | ||
| this._initWithVirtualData(() => this.#init()); | ||
| } | ||
@@ -225,2 +227,3 @@ static async disposeAll() { | ||
| 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 }); | ||
@@ -321,5 +324,21 @@ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; | ||
| } | ||
| async #prepareVirtualModules() { | ||
| const virtual = this._data?.virtual; | ||
| if (!virtual || Object.keys(virtual).length === 0) return; | ||
| const out = {}; | ||
| let strip; | ||
| for (const [specifier, source] of Object.entries(virtual)) if (virtualModuleFormat(specifier) === "module-typescript") { | ||
| if (!strip) { | ||
| const { stripTypeScriptTypes } = await import("node:module"); | ||
| if (typeof stripTypeScriptTypes !== "function") throw new TypeError(`[env-runner] virtual TypeScript module "${specifier}" requires \`module.stripTypeScriptTypes\` on the host (workerd does not parse TypeScript); upgrade Node.js or provide a pre-transpiled JavaScript source instead.`); | ||
| strip = stripTypeScriptTypes; | ||
| } | ||
| out[specifier] = strip(source); | ||
| } else out[specifier] = source; | ||
| return out; | ||
| } | ||
| async #initAsync() { | ||
| const { Miniflare } = await import("miniflare"); | ||
| const entryPath = this._data?.entry; | ||
| const virtual = await this.#prepareVirtualModules(); | ||
| const userFlags = this.#miniflareOptions.compatibilityFlags || []; | ||
@@ -338,5 +357,9 @@ const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || []; | ||
| if (entryPath && !options.script && !options.scriptPath) { | ||
| const resolvedEntry = resolve(entryPath); | ||
| const entryDir = dirname(resolvedEntry); | ||
| const detectedExports = this.#exports === false || this.#exports === void 0 ? [] : detectExportedClasses(resolvedEntry, typeof this.#exports === "object" ? this.#exports : {}); | ||
| 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) { | ||
@@ -378,3 +401,4 @@ const autoDOs = { ...this.#miniflareOptions.durableObjects || {} }; | ||
| if (!options.unsafeModuleFallbackService) { | ||
| const _require = createRequire(resolvedEntry); | ||
| const _require = createRequire(entryBase); | ||
| const _virtual = virtual; | ||
| const _transformRequest = this.#transformRequest; | ||
@@ -394,2 +418,21 @@ const _exportConditions = this.#exportConditions; | ||
| 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: source | ||
| }); | ||
| } | ||
| } | ||
| let resolvedPath; | ||
@@ -416,3 +459,3 @@ const fileUrlRaw = cleanRaw || cleanSpecifier; | ||
| resolvedPath = resolveModulePath(cleanRaw, { | ||
| from: referrerReal || resolvedEntry, | ||
| from: referrerReal || entryBase, | ||
| conditions: _exportConditions, | ||
@@ -478,3 +521,4 @@ try: true | ||
| ...this.#miniflareOptions, | ||
| _exportConditions: this.#exportConditions | ||
| _exportConditions: this.#exportConditions, | ||
| _virtual: virtual | ||
| }); | ||
@@ -515,12 +559,18 @@ const cached = _miniflareCache.get(this.#cacheKey); | ||
| }; | ||
| function detectExportedClasses(entryPath, explicit) { | ||
| function detectExportedClasses(entrySource, explicit) { | ||
| const names = new Set(Object.keys(explicit)); | ||
| try { | ||
| const source = readFileSync(entryPath, "utf8"); | ||
| if (entrySource) { | ||
| const re = /\bexport\s+class\s+(\w+)/g; | ||
| let match; | ||
| while (match = re.exec(source)) if (match[1]) names.add(match[1]); | ||
| } catch {} | ||
| 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) { | ||
@@ -549,2 +599,2 @@ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase(); | ||
| } | ||
| export { MiniflareEnvRunner as t }; | ||
| export { MiniflareEnvRunner }; |
@@ -1,3 +0,3 @@ | ||
| import { u as WorkerHooks } from "./types.mjs"; | ||
| import { n as EnvRunnerData, t as BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { WorkerHooks } from "./types.mjs"; | ||
| import { BaseEnvRunner, EnvRunnerData } from "./base-runner.mjs"; | ||
| declare class NodeWorkerEnvRunner extends BaseEnvRunner { | ||
@@ -16,2 +16,2 @@ #private; | ||
| } | ||
| export { NodeWorkerEnvRunner as t }; | ||
| export { NodeWorkerEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { t as BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { BaseEnvRunner } from "./base-runner.mjs"; | ||
| import { existsSync } from "node:fs"; | ||
@@ -14,3 +14,3 @@ import { fileURLToPath } from "node:url"; | ||
| }); | ||
| this.#initWorker(); | ||
| this._initWithVirtualData(() => this.#initWorker()); | ||
| } | ||
@@ -61,2 +61,2 @@ sendMessage(message) { | ||
| }; | ||
| export { NodeWorkerEnvRunner as t }; | ||
| export { NodeWorkerEnvRunner }; |
@@ -1,4 +0,4 @@ | ||
| import { u as WorkerHooks } from "./types.mjs"; | ||
| import { n as EnvRunnerData } from "./base-runner.mjs"; | ||
| import { t as NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| import { WorkerHooks } from "./types.mjs"; | ||
| import { EnvRunnerData } from "./base-runner.mjs"; | ||
| import { NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| declare class VercelEnvRunner extends NodeWorkerEnvRunner { | ||
@@ -14,2 +14,2 @@ constructor(opts: { | ||
| } | ||
| export { VercelEnvRunner as t }; | ||
| export { VercelEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { t as NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| import { NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| import { fileURLToPath } from "node:url"; | ||
@@ -93,2 +93,2 @@ import { randomBytes } from "node:crypto"; | ||
| }; | ||
| export { VercelEnvRunner as t }; | ||
| export { VercelEnvRunner }; |
@@ -1,4 +0,4 @@ | ||
| import { u as WorkerHooks } from "./types.mjs"; | ||
| import { n as EnvRunnerData } from "./base-runner.mjs"; | ||
| import { t as NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| import { WorkerHooks } from "./types.mjs"; | ||
| import { EnvRunnerData } from "./base-runner.mjs"; | ||
| import { NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| declare class NetlifyEnvRunner extends NodeWorkerEnvRunner { | ||
@@ -14,2 +14,2 @@ constructor(opts: { | ||
| } | ||
| export { NetlifyEnvRunner as t }; | ||
| export { NetlifyEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { t as NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| import { NodeWorkerEnvRunner } from "./runner3.mjs"; | ||
| import { fileURLToPath } from "node:url"; | ||
@@ -45,2 +45,2 @@ let _defaultEntry; | ||
| }; | ||
| export { NetlifyEnvRunner as t }; | ||
| export { NetlifyEnvRunner }; |
@@ -27,7 +27,8 @@ import { watch } from "node:fs"; | ||
| } | ||
| fetch = async (input, init) => { | ||
| fetch = (input, init) => this._fetch(input, init); | ||
| async _fetch(input, init) { | ||
| const runner = await this._waitForRunner(); | ||
| if (!runner) return new Response("Runner is unavailable", { status: 503 }); | ||
| return runner.fetch(input, init); | ||
| }; | ||
| } | ||
| upgrade = (context) => { | ||
@@ -106,2 +107,5 @@ this._runner?.upgrade?.(context); | ||
| } | ||
| async [Symbol.asyncDispose]() { | ||
| await this.close(); | ||
| } | ||
| onClose(listener) { | ||
@@ -184,2 +188,3 @@ this._closeListeners.add(listener); | ||
| _reloadListeners = /* @__PURE__ */ new Set(); | ||
| _startPromise; | ||
| runner = null; | ||
@@ -196,3 +201,18 @@ onReload(listener) { | ||
| } | ||
| async start() { | ||
| start() { | ||
| this._startPromise ??= this._start().catch((error) => { | ||
| this._startPromise = void 0; | ||
| throw error; | ||
| }); | ||
| return this._startPromise; | ||
| } | ||
| async close() { | ||
| this._stopWatching(); | ||
| await super.close(); | ||
| } | ||
| async _fetch(input, init) { | ||
| if (!this.closed) await this.start(); | ||
| return super._fetch(input, init); | ||
| } | ||
| async _start() { | ||
| this.runner = await this._createRunner(); | ||
@@ -203,8 +223,4 @@ await this.reload(this.runner); | ||
| } | ||
| async close() { | ||
| this._stopWatching(); | ||
| await super.close(); | ||
| } | ||
| async _createRunner() { | ||
| return loadRunner(this._opts.runner, { | ||
| return loadRunner(this._opts.runner || "node-worker", { | ||
| name: this._opts.name || this._opts.entry, | ||
@@ -246,2 +262,2 @@ hooks: this._opts.hooks, | ||
| }; | ||
| export { loadRunner as n, RunnerManager as r, EnvServer as t }; | ||
| export { EnvServer, RunnerManager, loadRunner }; |
@@ -55,3 +55,3 @@ import { IncomingMessage } from "node:http"; | ||
| /** Core runner interface combining lifecycle hooks, RPC, and request proxying. */ | ||
| interface EnvRunner extends RunnerRPCHooks { | ||
| interface EnvRunner extends RunnerRPCHooks, AsyncDisposable { | ||
| /** Whether the worker is ready to accept requests. */ | ||
@@ -73,3 +73,5 @@ readonly ready: boolean; | ||
| close(): Promise<void>; | ||
| /** Alias for `close()`, enabling `await using` (explicit resource management). */ | ||
| [Symbol.asyncDispose](): Promise<void>; | ||
| } | ||
| export { RunnerMessageListener as a, UpgradeHandler as c, RPCOptions as i, WorkerAddress as l, FetchHandler as n, RunnerRPCHooks as o, NodeUpgradeContext as r, UpgradeContext as s, EnvRunner as t, WorkerHooks as u }; | ||
| export { EnvRunner, FetchHandler, NodeUpgradeContext, RPCOptions, RunnerMessageListener, RunnerRPCHooks, UpgradeContext, UpgradeHandler, WorkerAddress, WorkerHooks }; |
@@ -1,6 +0,76 @@ | ||
| import { readFileSync } from "node:fs"; | ||
| import { createVirtualHooks, virtualModuleFormat } from "./virtual-loader.mjs"; | ||
| import { existsSync, readFileSync } from "node:fs"; | ||
| import { pathToFileURL } from "node:url"; | ||
| import { isAbsolute } from "node:path"; | ||
| async function resolveEntry(entryPath) { | ||
| const mod = await import(_toImportPath(entryPath)); | ||
| async function registerVirtualModules(virtual) { | ||
| if (!virtual || Object.keys(virtual).length === 0) return _noop; | ||
| const { registerHooks, stripTypeScriptTypes } = await import("node:module"); | ||
| if (typeof registerHooks === "function") { | ||
| if ("Deno" in globalThis) virtual = _transformForDeno(virtual, stripTypeScriptTypes); | ||
| const hooks = registerHooks(createVirtualHooks(virtual)); | ||
| return _once(() => 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; | ||
| } | ||
| 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 _transformForDeno(virtual, stripTypeScriptTypes) { | ||
| const out = {}; | ||
| for (const [specifier, source] of Object.entries(virtual)) { | ||
| const format = virtualModuleFormat(specifier); | ||
| if (format === "module-typescript") { | ||
| if (typeof stripTypeScriptTypes !== "function") throw new TypeError(`[env-runner] virtual TypeScript module "${specifier}" requires \`module.stripTypeScriptTypes\` (custom load hooks bypass Deno's native type stripping); upgrade Deno or provide a pre-transpiled JavaScript source instead.`); | ||
| out[specifier] = stripTypeScriptTypes(source); | ||
| } else if (format === "json") out[specifier] = `export default JSON.parse(${JSON.stringify(source)});`; | ||
| else out[specifier] = source; | ||
| } | ||
| return out; | ||
| } | ||
| 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; | ||
@@ -17,5 +87,5 @@ if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`); | ||
| } | ||
| async function reloadEntryModule(entryPath, currentEntry, sendMessage) { | ||
| async function reloadEntryModule(entryPath, currentEntry, sendMessage, virtual) { | ||
| await currentEntry.ipc?.onClose?.(); | ||
| const newEntry = await _importFresh(entryPath); | ||
| const newEntry = await _importFresh(entryPath, virtual); | ||
| await newEntry.ipc?.onOpen?.({ sendMessage }); | ||
@@ -31,5 +101,12 @@ return newEntry; | ||
| } | ||
| async function _importFresh(entryPath) { | ||
| const code = readFileSync(entryPath, "utf8"); | ||
| const mod = await import("data:text/javascript;base64," + Buffer.from(code).toString("base64")); | ||
| 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; | ||
@@ -39,2 +116,2 @@ if (typeof entry.fetch !== "function") throw new Error(`[env-runner] Entry module "${entryPath}" must export a \`fetch\` handler (export default { fetch(req) { ... } }).`); | ||
| } | ||
| export { reloadEntryModule as n, resolveEntry as r, parseServerAddress as t }; | ||
| export { isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry }; |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import { t as EnvServer } from "./_chunks/server.mjs"; | ||
| import { EnvServer } from "./_chunks/server.mjs"; | ||
| import { resolve } from "node:path"; | ||
@@ -3,0 +3,0 @@ import { serve } from "srvx"; |
+23
-11
@@ -1,7 +0,7 @@ | ||
| import { a as RunnerMessageListener, c as UpgradeHandler, i as RPCOptions, l as WorkerAddress, n as FetchHandler, o as RunnerRPCHooks, r as NodeUpgradeContext, s as UpgradeContext, t as EnvRunner, u as WorkerHooks } from "./_chunks/types.mjs"; | ||
| import { n as EnvRunnerData, t as BaseEnvRunner } from "./_chunks/base-runner.mjs"; | ||
| import { t as DenoProcessEnvRunner } from "./_chunks/runner.mjs"; | ||
| import { i as TransformResult, n as MiniflareEnvRunnerOptions, r as MiniflareExportInfo, t as MiniflareEnvRunner } from "./_chunks/runner2.mjs"; | ||
| import { t as VercelEnvRunner } from "./_chunks/runner4.mjs"; | ||
| import { t as NetlifyEnvRunner } from "./_chunks/runner5.mjs"; | ||
| 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 { ServerOptions } from "srvx"; | ||
@@ -13,3 +13,3 @@ import { Hooks } from "crossws"; | ||
| */ | ||
| declare class RunnerManager implements EnvRunner { | ||
| declare class RunnerManager implements EnvRunner, AsyncDisposable { | ||
| private _runner; | ||
@@ -28,2 +28,3 @@ private _messageQueue; | ||
| fetch: FetchHandler; | ||
| protected _fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>; | ||
| upgrade: UpgradeHandler; | ||
@@ -39,2 +40,3 @@ sendMessage(message: unknown): void; | ||
| close(): Promise<void>; | ||
| [Symbol.asyncDispose](): Promise<void>; | ||
| onClose(listener: (runner: EnvRunner, cause?: unknown) => void): void; | ||
@@ -62,4 +64,4 @@ offClose(listener: (runner: EnvRunner, cause?: unknown) => void): void; | ||
| interface EnvServerOptions { | ||
| /** Runner implementation to use. */ | ||
| runner: RunnerName; | ||
| /** Runner implementation to use (defaults to `"node-worker"`). */ | ||
| runner?: RunnerName; | ||
| /** Path to the user entry module (passed as `data.entry`). */ | ||
@@ -85,2 +87,3 @@ entry: string; | ||
| private _reloadListeners; | ||
| private _startPromise; | ||
| runner: Awaited<ReturnType<typeof loadRunner>> | null; | ||
@@ -92,5 +95,14 @@ /** Register a listener called when the runner is reloaded due to a file change. */ | ||
| constructor(opts: EnvServerOptions); | ||
| /** Start the server by loading and attaching the runner. */ | ||
| /** | ||
| * Start the server by loading and attaching the runner. | ||
| * | ||
| * Idempotent — concurrent and repeated calls share one startup. Calling | ||
| * `start()` explicitly is optional: the first `fetch()` auto-starts the | ||
| * server. A failed start resets so a later call can retry. | ||
| */ | ||
| start(): Promise<this>; | ||
| close(): Promise<void>; | ||
| /** Auto-start on first fetch so an explicit `start()` call is optional. */ | ||
| protected _fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>; | ||
| private _start; | ||
| private _createRunner; | ||
@@ -117,2 +129,2 @@ private _startWatching; | ||
| } | ||
| export { type AppEntry, type AppEntryIPC, type AppEntryIPCContext, BaseEnvRunner, DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData, type EnvRunnerData, type EnvRunner, EnvServer, type EnvServerOptions, type FetchHandler, type LoadRunnerOptions, MiniflareEnvRunner, type MiniflareEnvRunnerOptions, type MiniflareExportInfo, NetlifyEnvRunner, type NodeUpgradeContext, type RPCOptions, RunnerManager, type RunnerMessageListener, type RunnerName, type RunnerRPCHooks, type TransformResult, type UpgradeContext, type UpgradeHandler, VercelEnvRunner, type WorkerAddress, type WorkerHooks, loadRunner }; | ||
| export { type AppEntry, type AppEntryIPC, type AppEntryIPCContext, BaseEnvRunner, DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData, type EnvRunner, type EnvRunnerData, EnvServer, type EnvServerOptions, type FetchHandler, type LoadRunnerOptions, MiniflareEnvRunner, type MiniflareEnvRunnerOptions, type MiniflareExportInfo, NetlifyEnvRunner, type NodeUpgradeContext, type RPCOptions, RunnerManager, type RunnerMessageListener, type RunnerName, type RunnerRPCHooks, type TransformResult, type UpgradeContext, type UpgradeHandler, VercelEnvRunner, type VirtualModuleSource, type VirtualModules, type WorkerAddress, type WorkerHooks, loadRunner }; |
+6
-6
@@ -1,7 +0,7 @@ | ||
| import { t as BaseEnvRunner } from "./_chunks/base-runner.mjs"; | ||
| import { n as loadRunner, r as RunnerManager, t as EnvServer } from "./_chunks/server.mjs"; | ||
| import { t as DenoProcessEnvRunner } from "./_chunks/runner.mjs"; | ||
| import { t as MiniflareEnvRunner } from "./_chunks/runner2.mjs"; | ||
| import { t as VercelEnvRunner } from "./_chunks/runner4.mjs"; | ||
| import { t as NetlifyEnvRunner } from "./_chunks/runner5.mjs"; | ||
| import { BaseEnvRunner } from "./_chunks/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"; | ||
| export { BaseEnvRunner, DenoProcessEnvRunner, EnvServer, MiniflareEnvRunner, NetlifyEnvRunner, RunnerManager, VercelEnvRunner, loadRunner }; |
@@ -1,3 +0,3 @@ | ||
| import { u as WorkerHooks } from "../../_chunks/types.mjs"; | ||
| import { n as EnvRunnerData, t as BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { WorkerHooks } from "../../_chunks/types.mjs"; | ||
| import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| declare class BunProcessEnvRunner extends BaseEnvRunner { | ||
@@ -4,0 +4,0 @@ #private; |
@@ -1,2 +0,2 @@ | ||
| import { t as BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { existsSync } from "node:fs"; | ||
@@ -10,2 +10,8 @@ import { execSync, spawn } from "node:child_process"; | ||
| const _isBun = typeof Bun !== "undefined"; | ||
| async function forwardStream(stream, dest) { | ||
| if (!stream) return; | ||
| try { | ||
| for await (const chunk of stream) dest.write(chunk); | ||
| } catch {} | ||
| } | ||
| function resolveBunPath() { | ||
@@ -29,3 +35,3 @@ if (_bunPath) return _bunPath; | ||
| }); | ||
| this.#initProcess(opts.execArgv); | ||
| this._initWithVirtualData(() => this.#initProcess(opts.execArgv)); | ||
| } | ||
@@ -91,2 +97,4 @@ sendMessage(message) { | ||
| }); | ||
| forwardStream(proc.stdout, process.stdout); | ||
| forwardStream(proc.stderr, process.stderr); | ||
| this.#process = child; | ||
@@ -128,2 +136,4 @@ } | ||
| }); | ||
| child.stdout?.pipe(process.stdout); | ||
| child.stderr?.pipe(process.stderr); | ||
| this.#process = handle; | ||
@@ -130,0 +140,0 @@ } |
@@ -1,7 +0,22 @@ | ||
| import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs"; | ||
| import { isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs"; | ||
| import { serve } from "srvx"; | ||
| import { plugin } from "crossws/server"; | ||
| process.on("disconnect", () => process.exit(0)); | ||
| const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}"); | ||
| let entry = await resolveEntry(data.entry); | ||
| const sendMessage = (message) => process.send(message); | ||
| const virtualEntry = isVirtualSpecifier(data.entry, data.virtual); | ||
| let unregisterVirtualModules; | ||
| let entry; | ||
| try { | ||
| unregisterVirtualModules = await registerVirtualModules(data.virtual); | ||
| entry = await resolveEntry(data.entry, virtualEntry); | ||
| } catch (error) { | ||
| const message = error?.message || String(error); | ||
| sendMessage({ | ||
| event: "init-error", | ||
| error: message | ||
| }); | ||
| console.error(`[env-runner] worker init failed: ${message}`); | ||
| process.exit(1); | ||
| } | ||
| const server = serve({ | ||
@@ -29,2 +44,3 @@ port: 0, | ||
| Promise.resolve(entry.ipc?.onClose?.()).then(() => server.close()).then(() => { | ||
| unregisterVirtualModules(); | ||
| process.send({ event: "exit" }); | ||
@@ -36,3 +52,3 @@ }); | ||
| try { | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage); | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage, virtualEntry); | ||
| process.send({ event: "module-reloaded" }); | ||
@@ -39,0 +55,0 @@ } catch (error) { |
@@ -1,3 +0,3 @@ | ||
| import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { t as DenoProcessEnvRunner } from "../../_chunks/runner.mjs"; | ||
| import { EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { DenoProcessEnvRunner } from "../../_chunks/runner.mjs"; | ||
| export { DenoProcessEnvRunner, type EnvRunnerData as DenoProcessEnvRunnerData }; |
@@ -1,2 +0,2 @@ | ||
| import { t as DenoProcessEnvRunner } from "../../_chunks/runner.mjs"; | ||
| import { DenoProcessEnvRunner } from "../../_chunks/runner.mjs"; | ||
| export { DenoProcessEnvRunner }; |
@@ -1,9 +0,23 @@ | ||
| import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs"; | ||
| import { isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs"; | ||
| import { serve } from "srvx"; | ||
| import { plugin } from "crossws/server"; | ||
| const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}"); | ||
| let entry = await resolveEntry(data.entry); | ||
| const _stdout = globalThis.Deno?.stdout ? { write: (s) => globalThis.Deno.stdout.writeSync(new TextEncoder().encode(s)) } : process.stdout; | ||
| const sendMessage = (message) => _stdout.write(JSON.stringify(message) + "\n"); | ||
| const _stdin = globalThis.Deno?.stdin?.readable || process.stdin; | ||
| const virtualEntry = isVirtualSpecifier(data.entry, data.virtual); | ||
| let unregisterVirtualModules; | ||
| let entry; | ||
| try { | ||
| unregisterVirtualModules = await registerVirtualModules(data.virtual); | ||
| entry = await resolveEntry(data.entry, virtualEntry); | ||
| } catch (error) { | ||
| const message = error?.message || String(error); | ||
| sendMessage({ | ||
| event: "init-error", | ||
| error: message | ||
| }); | ||
| console.error(`[env-runner] worker init failed: ${message}`); | ||
| process.exit(1); | ||
| } | ||
| const server = serve({ | ||
@@ -51,2 +65,3 @@ port: 0, | ||
| Promise.resolve(entry.ipc?.onClose?.()).then(() => server.close()).then(() => { | ||
| unregisterVirtualModules(); | ||
| sendMessage({ event: "exit" }); | ||
@@ -58,3 +73,3 @@ }); | ||
| try { | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage); | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage, virtualEntry); | ||
| sendMessage({ event: "module-reloaded" }); | ||
@@ -61,0 +76,0 @@ } catch (error) { |
@@ -1,3 +0,3 @@ | ||
| import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { i as TransformResult, n as MiniflareEnvRunnerOptions, r as MiniflareExportInfo, t as MiniflareEnvRunner } from "../../_chunks/runner2.mjs"; | ||
| import { EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult } from "../../_chunks/runner2.mjs"; | ||
| export { MiniflareEnvRunner, type EnvRunnerData as MiniflareEnvRunnerData, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult }; |
@@ -1,2 +0,2 @@ | ||
| import { t as MiniflareEnvRunner } from "../../_chunks/runner2.mjs"; | ||
| import { MiniflareEnvRunner } from "../../_chunks/runner2.mjs"; | ||
| export { MiniflareEnvRunner }; |
@@ -1,3 +0,3 @@ | ||
| import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { t as NetlifyEnvRunner } from "../../_chunks/runner5.mjs"; | ||
| import { EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { NetlifyEnvRunner } from "../../_chunks/runner5.mjs"; | ||
| export { type EnvRunnerData, NetlifyEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { t as NetlifyEnvRunner } from "../../_chunks/runner5.mjs"; | ||
| import { NetlifyEnvRunner } from "../../_chunks/runner5.mjs"; | ||
| export { NetlifyEnvRunner }; |
@@ -1,3 +0,3 @@ | ||
| import { u as WorkerHooks } from "../../_chunks/types.mjs"; | ||
| import { n as EnvRunnerData, t as BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { WorkerHooks } from "../../_chunks/types.mjs"; | ||
| import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| declare class NodeProcessEnvRunner extends BaseEnvRunner { | ||
@@ -4,0 +4,0 @@ #private; |
@@ -1,2 +0,2 @@ | ||
| import { t as BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { existsSync } from "node:fs"; | ||
@@ -14,3 +14,3 @@ import { fork } from "node:child_process"; | ||
| }); | ||
| this.#initProcess(opts.execArgv); | ||
| this._initWithVirtualData(() => this.#initProcess(opts.execArgv)); | ||
| } | ||
@@ -63,2 +63,4 @@ sendMessage(message) { | ||
| }); | ||
| child.stdout?.pipe(process.stdout); | ||
| child.stderr?.pipe(process.stderr); | ||
| this.#process = child; | ||
@@ -65,0 +67,0 @@ } |
@@ -1,7 +0,22 @@ | ||
| import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs"; | ||
| import { isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs"; | ||
| import { serve } from "srvx"; | ||
| import { plugin } from "crossws/server/node"; | ||
| process.on("disconnect", () => process.exit(0)); | ||
| const data = JSON.parse(process.env.ENV_RUNNER_DATA || "{}"); | ||
| let entry = await resolveEntry(data.entry); | ||
| const sendMessage = (message) => process.send(message); | ||
| const virtualEntry = isVirtualSpecifier(data.entry, data.virtual); | ||
| let unregisterVirtualModules; | ||
| let entry; | ||
| try { | ||
| unregisterVirtualModules = await registerVirtualModules(data.virtual); | ||
| entry = await resolveEntry(data.entry, virtualEntry); | ||
| } catch (error) { | ||
| const message = error?.message || String(error); | ||
| sendMessage({ | ||
| event: "init-error", | ||
| error: message | ||
| }); | ||
| console.error(`[env-runner] worker init failed: ${message}`); | ||
| process.exit(1); | ||
| } | ||
| const server = serve({ | ||
@@ -29,2 +44,3 @@ port: 0, | ||
| Promise.resolve(entry.ipc?.onClose?.()).then(() => server.close()).then(() => { | ||
| unregisterVirtualModules(); | ||
| process.send({ event: "exit" }); | ||
@@ -36,3 +52,3 @@ }); | ||
| try { | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage); | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage, virtualEntry); | ||
| process.send({ event: "module-reloaded" }); | ||
@@ -39,0 +55,0 @@ } catch (error) { |
@@ -1,3 +0,3 @@ | ||
| import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { t as NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs"; | ||
| import { EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs"; | ||
| export { type EnvRunnerData, NodeWorkerEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { t as NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs"; | ||
| import { NodeWorkerEnvRunner } from "../../_chunks/runner3.mjs"; | ||
| export { NodeWorkerEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { n as reloadEntryModule, r as resolveEntry, t as parseServerAddress } from "../../_chunks/worker-utils.mjs"; | ||
| import { isVirtualSpecifier, parseServerAddress, registerVirtualModules, reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs"; | ||
| import { parentPort, workerData } from "node:worker_threads"; | ||
@@ -6,4 +6,18 @@ import { serve } from "srvx"; | ||
| const data = workerData || {}; | ||
| let entry = await resolveEntry(data.entry); | ||
| const sendMessage = (message) => parentPort?.postMessage(message); | ||
| const virtualEntry = isVirtualSpecifier(data.entry, data.virtual); | ||
| let unregisterVirtualModules; | ||
| let entry; | ||
| try { | ||
| unregisterVirtualModules = await registerVirtualModules(data.virtual); | ||
| entry = await resolveEntry(data.entry, virtualEntry); | ||
| } catch (error) { | ||
| const message = error?.message || String(error); | ||
| sendMessage({ | ||
| event: "init-error", | ||
| error: message | ||
| }); | ||
| console.error(`[env-runner] worker init failed: ${message}`); | ||
| process.exit(1); | ||
| } | ||
| const server = serve({ | ||
@@ -31,2 +45,3 @@ port: 0, | ||
| Promise.resolve(entry.ipc?.onClose?.()).then(() => server.close()).then(() => { | ||
| unregisterVirtualModules(); | ||
| parentPort?.postMessage({ event: "exit" }); | ||
@@ -38,3 +53,3 @@ }); | ||
| try { | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage); | ||
| entry = await reloadEntryModule(data.entry, entry, sendMessage, virtualEntry); | ||
| parentPort?.postMessage({ event: "module-reloaded" }); | ||
@@ -41,0 +56,0 @@ } catch (error) { |
@@ -1,3 +0,3 @@ | ||
| import { u as WorkerHooks } from "../../_chunks/types.mjs"; | ||
| import { n as EnvRunnerData, t as BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { WorkerHooks } from "../../_chunks/types.mjs"; | ||
| import { BaseEnvRunner, EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| declare class SelfEnvRunner extends BaseEnvRunner { | ||
@@ -4,0 +4,0 @@ #private; |
@@ -1,3 +0,3 @@ | ||
| import { t as BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { n as reloadEntryModule, r as resolveEntry } from "../../_chunks/worker-utils.mjs"; | ||
| import { BaseEnvRunner } from "../../_chunks/base-runner.mjs"; | ||
| import { reloadEntryModule, resolveEntry } from "../../_chunks/worker-utils.mjs"; | ||
| var SelfEnvRunner = class extends BaseEnvRunner { | ||
@@ -4,0 +4,0 @@ #active = false; |
@@ -1,3 +0,3 @@ | ||
| import { n as EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { t as VercelEnvRunner } from "../../_chunks/runner4.mjs"; | ||
| import { EnvRunnerData } from "../../_chunks/base-runner.mjs"; | ||
| import { VercelEnvRunner } from "../../_chunks/runner4.mjs"; | ||
| export { type EnvRunnerData, VercelEnvRunner }; |
@@ -1,2 +0,2 @@ | ||
| import { t as VercelEnvRunner } from "../../_chunks/runner4.mjs"; | ||
| import { VercelEnvRunner } from "../../_chunks/runner4.mjs"; | ||
| export { VercelEnvRunner }; |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import { o as RunnerRPCHooks } from "./_chunks/types.mjs"; | ||
| import { RunnerRPCHooks } from "./_chunks/types.mjs"; | ||
| /** Vite HotChannel-compatible interface (avoids hard dependency on vite types). */ | ||
@@ -3,0 +3,0 @@ interface ViteHotChannel { |
+11
-11
| { | ||
| "name": "env-runner", | ||
| "version": "0.1.9", | ||
| "version": "0.1.10", | ||
| "description": "Generic environment runner for JavaScript runtimes.", | ||
@@ -49,10 +49,10 @@ "license": "MIT", | ||
| "httpxy": "^0.5.3", | ||
| "srvx": "^0.11.15" | ||
| "srvx": "^0.11.16" | ||
| }, | ||
| "devDependencies": { | ||
| "@netlify/runtime": "^4.1.24", | ||
| "@types/node": "^25.9.1", | ||
| "@netlify/runtime": "^4.1.25", | ||
| "@types/node": "^25.9.2", | ||
| "@typescript/native-preview": "7.0.0-dev.20260521.1", | ||
| "@vercel/queue": "^0.2.0", | ||
| "@vitest/coverage-v8": "^4.1.7", | ||
| "@vercel/queue": "^0.2.1", | ||
| "@vitest/coverage-v8": "^4.1.8", | ||
| "automd": "^0.4.3", | ||
@@ -62,8 +62,8 @@ "changelogen": "^0.6.2", | ||
| "env-runner-fixture": "link:", | ||
| "miniflare": "^4.20260520.0", | ||
| "obuild": "^0.4.35", | ||
| "miniflare": "^4.20260603.0", | ||
| "obuild": "^0.4.36", | ||
| "oxfmt": "^0.51.0", | ||
| "oxlint": "^1.66.0", | ||
| "oxlint": "^1.69.0", | ||
| "typescript": "^6.0.3", | ||
| "vitest": "^4.1.7" | ||
| "vitest": "^4.1.8" | ||
| }, | ||
@@ -86,3 +86,3 @@ "peerDependencies": { | ||
| }, | ||
| "packageManager": "pnpm@11.2.2" | ||
| "packageManager": "pnpm@11.5.2" | ||
| } |
+112
-15
@@ -53,3 +53,3 @@ # env-runner | ||
| const envServer = new EnvServer({ | ||
| runner: "node-process", | ||
| runner: "node-process", // optional, defaults to "node-worker" | ||
| entry: "./app.ts", | ||
@@ -68,2 +68,3 @@ watch: true, | ||
| // Optional — the server auto-starts on first fetch() | ||
| await envServer.start(); | ||
@@ -84,3 +85,3 @@ | ||
| const manager = new RunnerManager(); | ||
| await using manager = new RunnerManager(); | ||
@@ -112,5 +113,7 @@ manager.onReady((_runner, address) => { | ||
| await manager.close(); | ||
| // manager.close() is awaited automatically at the end of the scope (`await using`) | ||
| ``` | ||
| All runners, `RunnerManager`, and `EnvServer` implement `AsyncDisposable`, so they can be auto-closed with [explicit resource management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/await_using) (`await using`) — or closed manually with `await runner.close()`. | ||
| ### Runners | ||
@@ -134,3 +137,3 @@ | ||
| ```ts | ||
| const runner = new NodeProcessEnvRunner({ | ||
| await using runner = new NodeProcessEnvRunner({ | ||
| name: "my-app", | ||
@@ -164,4 +167,4 @@ data: { entry: "./app.ts" }, | ||
| // Graceful shutdown | ||
| await runner.close(); | ||
| // Graceful shutdown happens automatically at the end of the scope | ||
| // (`await using`) — or call `await runner.close()` explicitly | ||
| ``` | ||
@@ -182,2 +185,94 @@ | ||
| #### Virtual Modules | ||
| The Node.js runners (`NodeWorkerEnvRunner`, `NodeProcessEnvRunner`, and the runners built on top of them), `BunProcessEnvRunner`, `DenoProcessEnvRunner` (Deno >= 2.x), and `MiniflareEnvRunner` can serve **virtual modules** from an in-memory `specifier => source` map passed via `data.virtual`. The entry (and its dependencies) can then `import` them as if they were real files: | ||
| ```ts | ||
| import { NodeWorkerEnvRunner } from "env-runner/runners/node-worker"; | ||
| await using runner = new NodeWorkerEnvRunner({ | ||
| name: "my-app", | ||
| data: { | ||
| entry: "./app.ts", | ||
| virtual: { | ||
| "#config": `export const apiBase = "https://api.example.com";`, | ||
| "#banner": `export default "Hello from a virtual module!";`, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| ```ts | ||
| // app.ts | ||
| import banner from "#banner"; | ||
| import { apiBase } from "#config"; | ||
| export default { | ||
| fetch: () => new Response(`${banner} (${apiBase})`), | ||
| }; | ||
| ``` | ||
| The **entry itself can be virtual** — set `data.entry` to one of the `data.virtual` keys to run an entry whose source lives in memory (it may import other virtual modules too): | ||
| ```ts | ||
| await using runner = new NodeWorkerEnvRunner({ | ||
| name: "my-app", | ||
| data: { | ||
| entry: "#entry", | ||
| virtual: { | ||
| "#entry": `import { body } from "#dep"; | ||
| export default { fetch: () => new Response(body) };`, | ||
| "#dep": `export const body = "Hello from a virtual entry!";`, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| Each source may also be a **factory** `() => string | Promise<string>` instead of a literal string — useful for lazily computed or asynchronously loaded sources: | ||
| ```ts | ||
| await using runner = new NodeWorkerEnvRunner({ | ||
| name: "my-app", | ||
| data: { | ||
| entry: "./app.ts", | ||
| virtual: { | ||
| "#config": () => `export const apiBase = ${JSON.stringify(getApiBase())};`, | ||
| "#schema": async () => `export default ${await loadSchemaJson()};`, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| Factories are invoked once on the host (before the worker is spawned), so the worker always receives plain strings — functions can't cross the `workerData`/`JSON` boundary, and Node's synchronous load hook can't await. For the same reason, **all** factories are resolved eagerly at startup (in parallel), not lazily on first import — so keep them cheap, or use plain strings for sources that don't need computation. Maps containing only strings skip this step entirely. | ||
| The module format is derived from the specifier extension: `.ts`/`.mts` sources are served as **TypeScript** and `.json` sources as **JSON modules**; everything else is plain JavaScript ESM: | ||
| ```ts | ||
| await using runner = new NodeWorkerEnvRunner({ | ||
| name: "my-app", | ||
| data: { | ||
| entry: "#entry.ts", | ||
| virtual: { | ||
| "#entry.ts": ` | ||
| import { getGreeting } from "#util.ts"; | ||
| import config from "#config.json"; | ||
| const handler: () => Response = () => new Response(getGreeting(config.name)); | ||
| export default { fetch: handler }; | ||
| `, | ||
| "#util.ts": `export function getGreeting(name: string): string { | ||
| return \`Hello, \${name}!\`; | ||
| }`, | ||
| "#config.json": JSON.stringify({ name: "virtual" }), | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| - **TypeScript** is type-stripped by Node's native [type stripping](https://nodejs.org/api/typescript.html#type-stripping) (Node.js >= 22.18 / 23.6 — erasable syntax only) and by Bun's `ts` loader. On Deno, custom load hooks bypass its native type stripping, so sources are pre-stripped with [`module.stripTypeScriptTypes`](https://docs.deno.com/api/node/module/~/Module.stripTypeScriptTypes) (Deno >= 2.8.2); on older Deno without it, virtual `.ts`/`.mts` sources **throw at registration** — pass pre-transpiled JavaScript instead. On miniflare, sources are likewise pre-stripped with `module.stripTypeScriptTypes` on the host (workerd does not parse TypeScript). | ||
| - **JSON** sources expose the parsed value as the default export on all runtimes. The `with { type: "json" }` import attribute is optional on Node.js and Bun; on Deno and miniflare it must be **omitted** (static imports carrying an import attribute bypass `registerHooks` resolution on Deno, and workerd rejects import attributes outright). | ||
| Virtual modules are registered inside the worker, before the entry is imported. On Node.js (>= 22.15 / 23.5) and Deno (>= 2.x) this uses [ESM customization hooks](https://nodejs.org/api/module.html#moduleregisterhooksoptions) (`module.registerHooks`); on Bun (which does not implement `registerHooks`) it uses [`Bun.plugin()`](https://bun.com/docs/runtime/plugins) virtual modules instead. The source string is treated as an ES module, and virtual specifiers (including a virtual entry) resolve across `reloadModule()`. On runtimes supporting neither mechanism, a warning is logged and registration is skipped. When the worker shuts down gracefully the registration is unregistered again (the `registerHooks` registration is deregistered; on Bun, which has no plugin-removal API, the in-memory source map is detached so fresh loads and reloads stop resolving). | ||
| On `MiniflareEnvRunner` there is no in-worker registration: the runner's module fallback service serves virtual specifiers to workerd directly (taking precedence over disk files and the `transformRequest` pipeline, so a virtual key overrides a real file with the same path). One limitation: named `exports` (Durable Objects / WorkerEntrypoints) cannot be combined with a **virtual entry** — the wrapper would need a static re-export that miniflare cannot resolve at startup — and the runner fails fast with a clear error in that case. | ||
| #### Miniflare Runner | ||
@@ -194,3 +289,3 @@ | ||
| const runner = new MiniflareEnvRunner({ | ||
| await using runner = new MiniflareEnvRunner({ | ||
| name: "my-worker", | ||
@@ -205,3 +300,2 @@ data: { entry: "./worker.ts" }, | ||
| const response = await runner.fetch("http://localhost/api"); | ||
| await runner.close(); | ||
| ``` | ||
@@ -218,3 +312,3 @@ | ||
| const runner = new MiniflareEnvRunner({ | ||
| await using runner = new MiniflareEnvRunner({ | ||
| name: "my-worker", | ||
@@ -258,3 +352,3 @@ data: { entry: "./worker.ts" }, | ||
| ```ts | ||
| const runner = new MiniflareEnvRunner({ | ||
| await using runner = new MiniflareEnvRunner({ | ||
| name: "my-worker", | ||
@@ -314,3 +408,3 @@ data: { entry: "./worker.ts" }, | ||
| const runner = new VercelEnvRunner({ | ||
| await using runner = new VercelEnvRunner({ | ||
| name: "my-app", | ||
@@ -328,3 +422,3 @@ data: { entry: "./app.ts" }, | ||
| const runner = new NetlifyEnvRunner({ | ||
| await using runner = new NetlifyEnvRunner({ | ||
| name: "my-app", | ||
@@ -359,3 +453,6 @@ data: { entry: "./app.ts" }, | ||
| const transport = createViteTransport(sendMessage, onMessage, "ssr"); | ||
| const runner = new ModuleRunner({ transport, sourcemapInterceptor: "prepareStackTrace" }); | ||
| const runner = new ModuleRunner({ | ||
| transport, | ||
| sourcemapInterceptor: "prepareStackTrace", | ||
| }); | ||
| ``` | ||
@@ -406,3 +503,3 @@ | ||
| const runner = await loadRunner("node-worker", { | ||
| await using runner = await loadRunner("node-worker", { | ||
| name: "my-app", | ||
@@ -466,3 +563,3 @@ data: { entry: "./app.ts" }, | ||
| ```ts | ||
| const runner = new NodeProcessEnvRunner({ | ||
| await using runner = new NodeProcessEnvRunner({ | ||
| name: "my-app", | ||
@@ -469,0 +566,0 @@ workerEntry: "/path/to/custom-worker.ts", |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
157954
14.09%57
1.79%2549
12.69%572
20.42%32
6.67%Updated