env-runner
Advanced tools
@@ -5,2 +5,4 @@ import { WorkerHooks } from "./types.mjs"; | ||
| import { Socket } from "node:net"; | ||
| /** Raw (snake_case) Wrangler config object, mirroring `wrangler.json` contents. */ | ||
| type WranglerInlineConfig = Record<string, unknown>; | ||
| /** Result from a module transform (compatible with Vite's `TransformResult`). */ | ||
@@ -56,2 +58,31 @@ interface TransformResult { | ||
| exportConditions?: string[]; | ||
| /** | ||
| * Load a Cloudflare `wrangler` config to populate Miniflare options | ||
| * (compatibility date/flags and bindings: `vars`, KV, R2, D1, Durable | ||
| * Objects, queues). | ||
| * | ||
| * - `true` — auto-discover `wrangler.{json,jsonc,toml}` next to the entry | ||
| * file, then in the current working directory. | ||
| * - `string` — explicit path to a wrangler config file. | ||
| * - `object` — an inline raw (snake_case) wrangler config, as you would | ||
| * write in `wrangler.json` (no file needed). A config file is still | ||
| * auto-discovered (next to the entry, then cwd) and the inline config is | ||
| * merged on top of it (inline wins per key, binding records merge, | ||
| * `compatibilityFlags` are unioned). | ||
| * | ||
| * The installed `wrangler` package is preferred (full fidelity: TOML, | ||
| * `env` inheritance, `.dev.vars`, every binding type; an inline config is | ||
| * normalized through a short-lived temp file). When `wrangler` is not | ||
| * installed, a built-in minimal reader handles plain JSON files and inline | ||
| * objects (common fields only) and a one-time warning is logged. JSONC and | ||
| * TOML files without `wrangler` are skipped with a warning. Values from | ||
| * `miniflareOptions` always win over config-derived ones; binding records | ||
| * (e.g. `bindings`) merge per key and `compatibilityFlags` are unioned. | ||
| */ | ||
| wrangler?: boolean | string | WranglerInlineConfig; | ||
| /** | ||
| * Wrangler environment (`--env`) to select when loading the config. | ||
| * Defaults to the `CLOUDFLARE_ENV` environment variable. | ||
| */ | ||
| wranglerEnv?: string; | ||
| } | ||
@@ -98,2 +129,2 @@ declare class MiniflareEnvRunner extends BaseEnvRunner { | ||
| } | ||
| export { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult }; | ||
| export { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult, WranglerInlineConfig }; |
@@ -9,5 +9,6 @@ import { expandVirtualInvalidation, stripVirtualTypeScript, virtualModuleFormat } from "./virtual-loader.mjs"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { basename, dirname, isAbsolute, resolve } from "node:path"; | ||
| import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; | ||
| import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path"; | ||
| import { resolveModulePath } from "exsolve"; | ||
| import { tmpdir } from "node:os"; | ||
| const IPC_PATH = "/__env_runner_ipc"; | ||
@@ -183,2 +184,176 @@ const IPC_BINDING = "__ENV_RUNNER_IPC"; | ||
| } | ||
| let _warnedNoWrangler = false; | ||
| const WRANGLER_CONFIG_FILENAMES = [ | ||
| "wrangler.json", | ||
| "wrangler.jsonc", | ||
| "wrangler.toml" | ||
| ]; | ||
| const WRANGLER_OPTION_DENYLIST = new Set([ | ||
| "name", | ||
| "script", | ||
| "scriptPath", | ||
| "modules", | ||
| "modulesRoot", | ||
| "modulesRules", | ||
| "unsafeDirectSockets", | ||
| "unsafeEvalBinding", | ||
| "unsafeModuleFallbackService", | ||
| "unsafeUseModuleFallbackService" | ||
| ]); | ||
| function isPlainObject(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||
| } | ||
| function isInlineWranglerConfig(opt) { | ||
| return isPlainObject(opt); | ||
| } | ||
| async function loadWranglerConfig(opt, env, entryPath) { | ||
| if (!opt) return; | ||
| const inline = isInlineWranglerConfig(opt) ? opt : void 0; | ||
| let configPath; | ||
| if (typeof opt === "string") { | ||
| configPath = resolve(opt); | ||
| if (!existsSync(configPath)) { | ||
| console.warn(`[env-runner] wrangler config requested but not found at "${configPath}"`); | ||
| return; | ||
| } | ||
| } else if (opt === true) { | ||
| configPath = findWranglerConfig(entryPath); | ||
| if (!configPath) { | ||
| console.warn("[env-runner] wrangler config requested but none found near the entry or cwd"); | ||
| return; | ||
| } | ||
| } else configPath = findWranglerConfig(entryPath); | ||
| let wrangler; | ||
| try { | ||
| wrangler = await import("wrangler"); | ||
| } catch { | ||
| if (!_warnedNoWrangler) { | ||
| _warnedNoWrangler = true; | ||
| console.warn("[env-runner] 'wrangler' is not installed; using the built-in minimal config reader (plain JSON, common fields only). Install 'wrangler' for full fidelity (JSONC, TOML, env inheritance, all binding types)."); | ||
| } | ||
| return mergeWranglerMiniflareOptions(configPath ? readWranglerConfigMinimal(configPath, env) : void 0, inline ? mapWranglerConfigToMiniflare(applyWranglerEnv(inline, env)) : void 0); | ||
| } | ||
| try { | ||
| return mergeWranglerMiniflareOptions(configPath ? pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(wrangler.unstable_readConfig({ | ||
| config: configPath, | ||
| env | ||
| }, { hideWarnings: true }), env).workerOptions) : void 0, inline ? pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(readInlineWranglerConfig(wrangler, inline, env), env).workerOptions) : void 0); | ||
| } catch (error) { | ||
| const desc = [configPath && `"${configPath}"`, inline && "(inline)"].filter(Boolean).join(" + "); | ||
| console.warn(`[env-runner] failed to load wrangler config ${desc}: ${error.message}`); | ||
| return; | ||
| } | ||
| } | ||
| function mergeWranglerMiniflareOptions(base, override) { | ||
| if (!base) return override; | ||
| if (!override) return base; | ||
| const out = { ...base }; | ||
| for (const [key, value] of Object.entries(override)) { | ||
| const prev = out[key]; | ||
| if (Array.isArray(value) && Array.isArray(prev)) out[key] = [...new Set([...prev, ...value])]; | ||
| else if (isPlainObject(value) && isPlainObject(prev)) out[key] = { | ||
| ...prev, | ||
| ...value | ||
| }; | ||
| else out[key] = value; | ||
| } | ||
| return out; | ||
| } | ||
| function readInlineWranglerConfig(wrangler, inline, env) { | ||
| const dir = mkdtempSync(join(tmpdir(), "env-runner-wrangler-")); | ||
| const file = join(dir, "wrangler.json"); | ||
| try { | ||
| writeFileSync(file, JSON.stringify(inline)); | ||
| return wrangler.unstable_readConfig({ | ||
| config: file, | ||
| env | ||
| }, { hideWarnings: true }); | ||
| } finally { | ||
| rmSync(dir, { | ||
| recursive: true, | ||
| force: true | ||
| }); | ||
| } | ||
| } | ||
| function findWranglerConfig(entryPath) { | ||
| const dirs = []; | ||
| if (entryPath) { | ||
| const resolved = isAbsolute(entryPath) ? entryPath : resolve(entryPath); | ||
| dirs.push(dirname(resolved)); | ||
| } | ||
| dirs.push(process.cwd()); | ||
| for (const dir of dirs) for (const name of WRANGLER_CONFIG_FILENAMES) { | ||
| const candidate = join(dir, name); | ||
| if (existsSync(candidate)) return candidate; | ||
| } | ||
| } | ||
| function pickWranglerMiniflareOptions(workerOptions) { | ||
| const out = {}; | ||
| for (const [key, value] of Object.entries(workerOptions)) { | ||
| if (value === void 0 || WRANGLER_OPTION_DENYLIST.has(key)) continue; | ||
| out[key] = value; | ||
| } | ||
| return Object.keys(out).length > 0 ? out : void 0; | ||
| } | ||
| function readWranglerConfigMinimal(configPath, env) { | ||
| if (extname(configPath).toLowerCase() !== ".json") { | ||
| console.warn(`[env-runner] reading "${basename(configPath)}" requires the 'wrangler' package; the built-in reader supports plain JSON only (install 'wrangler' for JSONC/TOML).`); | ||
| return; | ||
| } | ||
| let raw; | ||
| try { | ||
| raw = readFileSync(configPath, "utf8"); | ||
| } catch { | ||
| return; | ||
| } | ||
| let config; | ||
| try { | ||
| config = JSON.parse(raw); | ||
| } catch (error) { | ||
| console.warn(`[env-runner] failed to parse wrangler config "${configPath}": ${error.message}`); | ||
| return; | ||
| } | ||
| return mapWranglerConfigToMiniflare(applyWranglerEnv(config, env)); | ||
| } | ||
| function applyWranglerEnv(config, env) { | ||
| return env && config.env?.[env] ? { | ||
| ...config, | ||
| ...config.env[env] | ||
| } : config; | ||
| } | ||
| function mapWranglerConfigToMiniflare(config) { | ||
| const out = {}; | ||
| if (typeof config.compatibility_date === "string") out.compatibilityDate = config.compatibility_date; | ||
| if (Array.isArray(config.compatibility_flags)) out.compatibilityFlags = config.compatibility_flags; | ||
| if (config.vars && typeof config.vars === "object") out.bindings = { ...config.vars }; | ||
| const kv = mapBindingArray(config.kv_namespaces, "binding", (n) => n.id ?? n.binding); | ||
| if (kv) out.kvNamespaces = kv; | ||
| const r2 = mapBindingArray(config.r2_buckets, "binding", (n) => n.bucket_name ?? n.binding); | ||
| if (r2) out.r2Buckets = r2; | ||
| const d1 = mapBindingArray(config.d1_databases, "binding", (n) => n.database_id ?? n.preview_database_id ?? n.binding); | ||
| if (d1) out.d1Databases = d1; | ||
| const queues = mapBindingArray(config.queues?.producers, "binding", (n) => n.queue); | ||
| if (queues) out.queueProducers = queues; | ||
| if (Array.isArray(config.durable_objects?.bindings)) { | ||
| const dos = {}; | ||
| for (const b of config.durable_objects.bindings) { | ||
| if (!b?.name || !b?.class_name) continue; | ||
| dos[b.name] = b.script_name ? { | ||
| className: b.class_name, | ||
| scriptName: b.script_name | ||
| } : b.class_name; | ||
| } | ||
| if (Object.keys(dos).length > 0) out.durableObjects = dos; | ||
| } | ||
| return Object.keys(out).length > 0 ? out : void 0; | ||
| } | ||
| function mapBindingArray(arr, keyField, value) { | ||
| if (!Array.isArray(arr)) return; | ||
| const out = {}; | ||
| for (const entry of arr) { | ||
| const key = entry?.[keyField]; | ||
| if (typeof key === "string") out[key] = value(entry); | ||
| } | ||
| return Object.keys(out).length > 0 ? out : void 0; | ||
| } | ||
| const _miniflareCache = /* @__PURE__ */ new Map(); | ||
@@ -199,2 +374,4 @@ var MiniflareEnvRunner = class extends BaseEnvRunner { | ||
| #exportConditions; | ||
| #wrangler; | ||
| #wranglerEnv; | ||
| constructor(opts) { | ||
@@ -211,2 +388,4 @@ super({ | ||
| this.#exportConditions = opts.exportConditions ?? ["workerd", "worker"]; | ||
| this.#wrangler = opts.wrangler ?? false; | ||
| this.#wranglerEnv = opts.wranglerEnv ?? process.env.CLOUDFLARE_ENV; | ||
| this._initWithVirtualData(() => this.#init()); | ||
@@ -342,13 +521,20 @@ } | ||
| async #initAsync() { | ||
| const { Miniflare } = await import("miniflare"); | ||
| const { Miniflare, supportedCompatibilityDate } = await import("miniflare"); | ||
| const entryPath = this._data?.entry; | ||
| const virtual = await this.#prepareVirtualModules(); | ||
| this.#virtual = virtual; | ||
| const wranglerOptions = await loadWranglerConfig(this.#wrangler, this.#wranglerEnv, entryPath); | ||
| const userFlags = this.#miniflareOptions.compatibilityFlags || []; | ||
| const wranglerFlags = wranglerOptions?.compatibilityFlags || []; | ||
| const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || []; | ||
| const options = { | ||
| compatibilityDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0], | ||
| compatibilityDate: supportedCompatibilityDate, | ||
| modules: true, | ||
| ...wranglerOptions, | ||
| ...this.#miniflareOptions, | ||
| compatibilityFlags: [...new Set(["nodejs_compat", ...userFlags])], | ||
| compatibilityFlags: [...new Set([ | ||
| "nodejs_compat", | ||
| ...wranglerFlags, | ||
| ...userFlags | ||
| ])], | ||
| unsafeDirectSockets: [{ | ||
@@ -359,2 +545,9 @@ host: "127.0.0.1", | ||
| }; | ||
| if (wranglerOptions) for (const [key, wValue] of Object.entries(wranglerOptions)) { | ||
| const uValue = this.#miniflareOptions[key]; | ||
| if (isPlainObject(wValue) && isPlainObject(uValue)) options[key] = { | ||
| ...wValue, | ||
| ...uValue | ||
| }; | ||
| } | ||
| if (entryPath && !options.script && !options.scriptPath) { | ||
@@ -361,0 +554,0 @@ const entryIsVirtual = isVirtualSpecifier(entryPath, virtual); |
| 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 }; | ||
| import { MiniflareEnvRunner, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult, WranglerInlineConfig } from "../../_chunks/miniflare-runner.mjs"; | ||
| export { MiniflareEnvRunner, type EnvRunnerData as MiniflareEnvRunnerData, MiniflareEnvRunnerOptions, MiniflareExportInfo, TransformResult, type WranglerInlineConfig }; |
+8
-3
| { | ||
| "name": "env-runner", | ||
| "version": "0.1.13", | ||
| "version": "0.1.14", | ||
| "description": "Generic environment runner for JavaScript runtimes.", | ||
@@ -67,3 +67,4 @@ "license": "MIT", | ||
| "typescript": "^6.0.3", | ||
| "vitest": "^4.1.8" | ||
| "vitest": "^4.1.8", | ||
| "wrangler": "^4.99.0" | ||
| }, | ||
@@ -73,3 +74,4 @@ "peerDependencies": { | ||
| "@vercel/queue": "^0.2.0", | ||
| "miniflare": "^4.20260515.0" | ||
| "miniflare": "^4.20260515.0", | ||
| "wrangler": "^4.0.0" | ||
| }, | ||
@@ -85,2 +87,5 @@ "peerDependenciesMeta": { | ||
| "optional": true | ||
| }, | ||
| "wrangler": { | ||
| "optional": true | ||
| } | ||
@@ -87,0 +92,0 @@ }, |
+39
-0
@@ -313,2 +313,41 @@ # env-runner | ||
| When you don't set a `compatibilityDate` (via `miniflareOptions` or a wrangler config), it defaults to the date supported by the installed `workerd` binary rather than today's date — the binary always lags the calendar slightly, and pinning a future date makes `workerd` refuse to start. | ||
| #### Wrangler Config | ||
| Set the `wrangler` option to load a Cloudflare [Wrangler config](https://developers.cloudflare.com/workers/wrangler/configuration/) (`wrangler.json` / `wrangler.jsonc` / `wrangler.toml`) into the Miniflare options — compatibility date/flags and bindings (`vars`, KV, R2, D1, Durable Objects, queues): | ||
| ```ts | ||
| import { MiniflareEnvRunner } from "env-runner/runners/miniflare"; | ||
| await using runner = new MiniflareEnvRunner({ | ||
| name: "my-worker", | ||
| data: { entry: "./worker.ts" }, | ||
| wrangler: true, // auto-discover wrangler.{json,jsonc,toml} next to the entry, then cwd | ||
| // wrangler: "./config/wrangler.toml", // or an explicit path | ||
| // wranglerEnv: "production", // select a `[env.production]` block | ||
| }); | ||
| ``` | ||
| `wranglerEnv` selects a named Wrangler environment (`--env`). When omitted, it defaults to the `CLOUDFLARE_ENV` environment variable, so `CLOUDFLARE_ENV=production` selects the `production` env without passing the option. | ||
| You can also pass an **inline** config object (raw `wrangler.json` shape) instead of (or in addition to) a file — handy for programmatic setups: | ||
| ```ts | ||
| await using runner = new MiniflareEnvRunner({ | ||
| name: "my-worker", | ||
| data: { entry: "./worker.ts" }, | ||
| wrangler: { | ||
| compatibility_date: "2024-09-01", | ||
| compatibility_flags: ["nodejs_compat"], | ||
| vars: { GREETING: "hello" }, | ||
| kv_namespaces: [{ binding: "MY_KV", id: "..." }], | ||
| }, | ||
| }); | ||
| ``` | ||
| When an inline config is passed, a `wrangler.{json,jsonc,toml}` file is still auto-discovered (next to the entry, then cwd) and loaded, and the inline config is **merged on top of it** — inline values win per key, binding records (e.g. `vars`) merge, and `compatibilityFlags` are unioned. This lets you keep a committed `wrangler` file and override a few fields programmatically. | ||
| When the [`wrangler`](https://www.npmjs.com/package/wrangler) package is installed (an optional peer dependency), it is used for full fidelity — TOML, `env` inheritance, `.dev.vars`, and every binding type. When `wrangler` is **not** installed, a built-in minimal reader handles plain JSON files and inline objects (common fields only) and logs a one-time warning; JSONC and TOML files are skipped with a warning (they need `wrangler` to parse). Values you pass in `miniflareOptions` always take precedence over config-derived ones — binding records (e.g. `bindings`) merge per key, and `compatibilityFlags` are merged. | ||
| #### Module Transform Pipeline | ||
@@ -315,0 +354,0 @@ |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
199821
6.08%3078
6.69%628
6.62%8
14.29%17
6.25%33
3.13%