env-runner 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,24 @@
1
- import { expandVirtualInvalidation, stripVirtualTypeScript, virtualModuleFormat } from "./virtual-loader.mjs";
1
+ import { encodeVirtualModules, expandVirtualInvalidation, stripVirtualTypeScript, unsupportedVirtualJSXError, virtualModuleCode, virtualModuleCodeSource, virtualModuleFormat } from "./virtual-loader.mjs";
2
2
  import { BaseEnvRunner } from "./common-base-runner.mjs";
3
- import { isVirtualSpecifier } from "./common-worker-utils.mjs";
4
3
  import { resolveRuntimeDep } from "./common-runtime-deps.mjs";
5
4
  import { init, parse } from "./libs/cjs-module-lexer.mjs";
6
5
  import { init as init$1, parse as parse$1 } from "./libs/es-module-lexer.mjs";
7
6
  import { createRequire } from "node:module";
8
7
  import { proxyUpgrade } from "httpxy";
9
- import { fileURLToPath } from "node:url";
10
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
- import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
8
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
12
11
  import { resolveModulePath } from "exsolve";
13
12
  import { tmpdir } from "node:os";
14
- const IPC_PATH = "/__env_runner_ipc";
13
+ import { parseEnv } from "node:util";
14
+ const IPC_PATH$1 = "/__env_runner_ipc";
15
15
  const IPC_BINDING = "__ENV_RUNNER_IPC";
16
16
  const UNSAFE_EVAL_BINDING = "__ENV_RUNNER_UNSAFE_EVAL__";
17
17
  function generateWrapper(entryPath, opts) {
18
+ const processShim = opts?.nodeCompat === false ? "" : `import __process from "node:process";
19
+ if (!globalThis.process) { globalThis.process = __process; }`;
18
20
  const staticReExport = opts?.dynamicOnly ? "" : `export * from ${JSON.stringify(entryPath)};`;
19
- const explicitExports = opts?.dynamicOnly && opts.exports?.length ? opts.exports.map((name) => `export { ${name} } from ${JSON.stringify(entryPath)};`).join("\n") : "";
21
+ const explicitExports = typeof opts?.exports === "string" ? `export * from ${JSON.stringify(opts.exports)};` : opts?.dynamicOnly && opts.exports?.length ? opts.exports.map((name) => `export { ${name} } from ${JSON.stringify(entryPath)};`).join("\n") : "";
20
22
  const fetchBody = opts?.captureErrors ?? true ? `try {
21
23
  return await __server.fetch(request, env, ctx);
22
24
  } catch (e) {
@@ -31,12 +33,11 @@ function generateWrapper(entryPath, opts) {
31
33
  headers: { "Content-Type": "application/json", "X-Env-Runner-Error": "1" },
32
34
  });
33
35
  }` : `return __server.fetch(request, env, ctx);`;
34
- return `import __process from "node:process";
35
- if (!globalThis.process) { globalThis.process = __process; }
36
+ return `${processShim}
36
37
  ${staticReExport}
37
38
  ${explicitExports}
38
39
 
39
- const __IPC_PATH = "${IPC_PATH}";
40
+ const __IPC_PATH = "${IPC_PATH$1}";
40
41
  const __IPC_BINDING = "${IPC_BINDING}";
41
42
  const __UNSAFE_EVAL_BINDING = "${UNSAFE_EVAL_BINDING}";
42
43
  const __entryPath = ${JSON.stringify(entryPath)};
@@ -44,7 +45,10 @@ let __userEntry;
44
45
  let __server;
45
46
  let __ipcInitialized = false;
46
47
  let __serverWs;
47
- let __currentEnv;
48
+ // Raw env of the latest user request. Kept after the request ends: requests
49
+ // overlap and streamed bodies outlive fetch(), and unlike \`__serverWs\` the
50
+ // IPC binding works from any request context.
51
+ let __ipcEnv;
48
52
 
49
53
  const __userEnvs = new WeakMap();
50
54
 
@@ -130,9 +134,16 @@ async function __loadEntry(env, path) {
130
134
  return mod.default || mod;
131
135
  }
132
136
 
137
+ // Where an entry load error was thrown (first stack frame), so one thrown by a
138
+ // virtual module names its key (\`#config:2:7\`). workerd's messages don't.
139
+ function __errorLocation(e) {
140
+ const location = /\\n\\s+at (?:async )?(?:.* \\()?([^\\n()]+:\\d+:\\d+)\\)?/.exec(String(e?.stack))?.[1];
141
+ return location ? " (at " + location + ")" : "";
142
+ }
143
+
133
144
  function __sendMessage(message) {
134
145
  const payload = JSON.stringify(message);
135
- const env = __currentEnv;
146
+ const env = __ipcEnv;
136
147
  if (env && env[__IPC_BINDING]) {
137
148
  env[__IPC_BINDING].fetch("http://localhost/__ipc", {
138
149
  method: "POST",
@@ -204,8 +215,9 @@ export default {
204
215
  async fetch(request, env, ctx) {
205
216
  const url = new URL(request.url);
206
217
 
207
- // WebSocket IPC handshake
208
- if (url.pathname === __IPC_PATH && request.headers.get("upgrade") === "websocket") {
218
+ // IPC: a plain request loads the entry (204, or a 500 with the error), then
219
+ // the WebSocket upgrade opens the channel.
220
+ if (url.pathname === __IPC_PATH) {
209
221
  try {
210
222
  if (!__userEntry) {
211
223
  const entry = await __loadEntry(env, __entryPath);
@@ -213,7 +225,11 @@ export default {
213
225
  __userEntry = entry;
214
226
  }
215
227
  } catch (e) {
216
- return new Response("Failed to load entry: " + String(e), { status: 500 });
228
+ const message = "Failed to load entry: " + String(e) + __errorLocation(e);
229
+ return new Response(message, { status: 500 });
230
+ }
231
+ if (request.headers.get("upgrade") !== "websocket") {
232
+ return new Response(null, { status: 204 });
217
233
  }
218
234
 
219
235
  const pair = new WebSocketPair();
@@ -241,22 +257,80 @@ export default {
241
257
  return new Response("Worker not initialized", { status: 503 });
242
258
  }
243
259
 
260
+ __ipcEnv = env;
261
+
244
262
  // Handle WebSocket upgrade via crossws cloudflare adapter
245
263
  if (__userEntry.websocket && request.headers.get("upgrade") === "websocket") {
246
264
  const adapter = await __initCrossws(env, __userEntry.websocket);
247
265
  return adapter.handleUpgrade(request, __userEnv(env), ctx);
248
266
  }
249
267
 
250
- __currentEnv = env;
251
- try {
252
- ${fetchBody}
253
- } finally {
254
- __currentEnv = undefined;
255
- }
268
+ ${fetchBody}
256
269
  }
257
270
  };
258
271
  `;
259
272
  }
273
+ function expandDotenv(parsed, processEnv) {
274
+ const running = {};
275
+ for (const [key, raw] of Object.entries(parsed)) running[key] = processEnv[key] = (processEnv[key] && processEnv[key] !== raw ? processEnv[key] : expandDotenvValue(raw, processEnv, running)).replace(/\\\$/g, "$");
276
+ return processEnv;
277
+ }
278
+ function expandDotenvValue(value, processEnv, running) {
279
+ const env = {
280
+ ...running,
281
+ ...processEnv
282
+ };
283
+ const regex = /(?<!\\)\$\{([^{}]+)\}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g;
284
+ const seen = /* @__PURE__ */ new Set();
285
+ let result = value;
286
+ let match;
287
+ while ((match = regex.exec(result)) !== null) {
288
+ seen.add(result);
289
+ const [template, braced, unbraced] = match;
290
+ const expression = braced || unbraced;
291
+ const splitter = expression.match(/(:\+|\+|:-|-)/)?.[0];
292
+ const parts = splitter ? expression.split(splitter) : [expression];
293
+ const key = parts.shift();
294
+ let replacement;
295
+ if (splitter === ":+" || splitter === "+") replacement = env[key] ? parts.join(splitter) : "";
296
+ else {
297
+ const found = env[key];
298
+ replacement = found && !seen.has(found) ? found : parts.join(splitter ?? "");
299
+ }
300
+ result = result.replace(template, replacement);
301
+ if (result === running[key]) break;
302
+ regex.lastIndex = 0;
303
+ }
304
+ return result;
305
+ }
306
+ function readDotenvFile(path) {
307
+ return existsSync(path) ? parseEnv(readFileSync(path, "utf8")) : void 0;
308
+ }
309
+ function booleanEnv(name, defaultValue) {
310
+ const value = process.env[name]?.toLowerCase();
311
+ return value === "true" ? true : value === "false" ? false : defaultValue;
312
+ }
313
+ function loadDevVars(opts) {
314
+ const { configDir, env, envFiles } = opts;
315
+ if (!envFiles?.length) {
316
+ const devVars = resolve(configDir, ".dev.vars");
317
+ const loaded = env !== void 0 && readDotenvFile(`${devVars}.${env}`) || readDotenvFile(devVars);
318
+ if (loaded) return loaded;
319
+ }
320
+ if (!booleanEnv("CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV", true)) return;
321
+ const files = envFiles ?? [
322
+ ".env",
323
+ ".env.local",
324
+ ...env ? [`.env.${env}`, `.env.${env}.local`] : []
325
+ ];
326
+ const parsed = {};
327
+ for (const file of files) Object.assign(parsed, readDotenvFile(resolve(configDir, file)));
328
+ const processEnv = {};
329
+ if (opts.hasSecrets || booleanEnv("CLOUDFLARE_INCLUDE_PROCESS_ENV", false)) {
330
+ for (const [key, value] of Object.entries(process.env)) if (typeof value === "string") processEnv[key] = value;
331
+ }
332
+ return expandDotenv(parsed, processEnv);
333
+ }
260
334
  const WRANGLER_CONFIG_FILENAMES = [
261
335
  "wrangler.json",
262
336
  "wrangler.jsonc",
@@ -274,45 +348,143 @@ const WRANGLER_OPTION_DENYLIST = /* @__PURE__ */ new Set([
274
348
  "unsafeModuleFallbackService",
275
349
  "unsafeUseModuleFallbackService"
276
350
  ]);
351
+ const WRANGLER_OPTION_DROPLIST = /* @__PURE__ */ new Set([
352
+ "assets",
353
+ "serviceBindings",
354
+ "workflows",
355
+ "queueConsumers",
356
+ "tails",
357
+ "streamingTails"
358
+ ]);
359
+ const WRANGLER_DROPPED_OPTION_NAMES = {
360
+ assets: "assets",
361
+ serviceBindings: "services",
362
+ workflows: "workflows",
363
+ queueConsumers: "queues.consumers",
364
+ tails: "tail_consumers",
365
+ streamingTails: "streaming_tail_consumers",
366
+ durableObjects: "durable_objects"
367
+ };
368
+ function addDropped(dropped, name, details) {
369
+ let set = dropped.get(name);
370
+ if (!set) {
371
+ set = /* @__PURE__ */ new Set();
372
+ dropped.set(name, set);
373
+ }
374
+ for (const detail of details) set.add(detail);
375
+ }
376
+ function warnDroppedWranglerOptions(dropped) {
377
+ if (dropped.size === 0) return;
378
+ const list = [...dropped].map(([name, details]) => details.size > 0 ? `${name} (${[...details].join(", ")})` : name).join(", ");
379
+ console.warn(`[env-runner] wrangler config options not supported by the miniflare dev runner were ignored: ${list}; pass them via miniflareOptions to opt in.`);
380
+ }
277
381
  function isPlainObject(value) {
278
382
  return typeof value === "object" && value !== null && !Array.isArray(value);
279
383
  }
280
384
  function isInlineWranglerConfig(opt) {
281
385
  return isPlainObject(opt);
282
386
  }
283
- async function loadWranglerConfig(opt, env, entryPath, wranglerModule) {
284
- if (!opt) return;
387
+ async function loadWranglerConfig(opts) {
388
+ const { wrangler: opt, env, entryPath, wranglerModule, envFiles } = opts;
389
+ if (!opt) return {};
285
390
  const inline = isInlineWranglerConfig(opt) ? opt : void 0;
286
391
  let configPath;
287
- if (typeof opt === "string") {
288
- configPath = resolve(opt);
392
+ const explicitPath = typeof opt === "string" ? opt : opts.configPath;
393
+ if (explicitPath) {
394
+ configPath = resolve(explicitPath);
289
395
  if (!existsSync(configPath)) {
290
396
  console.warn(`[env-runner] wrangler config requested but not found at "${configPath}"`);
291
- return;
397
+ if (!inline) return {};
398
+ configPath = void 0;
292
399
  }
293
- } else if (opt === true) {
400
+ } else {
294
401
  configPath = findWranglerConfig(entryPath);
295
- if (!configPath) {
296
- console.warn("[env-runner] wrangler config requested but none found near the entry or cwd");
297
- return;
402
+ if (!configPath && !inline) {
403
+ console.warn("[env-runner] wrangler config requested but none found (searched the entry's directory, then from the cwd up to the filesystem root)");
404
+ return {};
298
405
  }
299
- } else configPath = findWranglerConfig(entryPath);
406
+ }
300
407
  const wrangler = await resolveRuntimeDep({
301
408
  name: "wrangler",
302
409
  option: "wranglerModule",
303
410
  value: wranglerModule
304
411
  });
305
- if (!wrangler?.unstable_readConfig || !wrangler.unstable_getMiniflareWorkerOptions) return mergeWranglerMiniflareOptions(configPath ? readWranglerConfigMinimal(configPath, env) : void 0, inline ? mapWranglerConfigToMiniflare(applyWranglerEnv(inline, env)) : void 0);
306
- try {
307
- return mergeWranglerMiniflareOptions(configPath ? pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(wrangler.unstable_readConfig({
412
+ const dropped = /* @__PURE__ */ new Map();
413
+ if (!wrangler?.unstable_readConfig || !wrangler.unstable_getMiniflareWorkerOptions) {
414
+ const unsupported = /* @__PURE__ */ new Map();
415
+ const file = configPath ? readWranglerConfigMinimal(configPath, env) : void 0;
416
+ const inlineConfig = inline ? applyWranglerEnv(inline, env) : void 0;
417
+ let options = mergeWranglerMiniflareOptions(file && (mapWranglerConfigToMiniflare(file.config, dropped, unsupported) ?? {}), inlineConfig && mapWranglerConfigToMiniflare(inlineConfig, dropped, unsupported));
418
+ if (file || inlineConfig) options = applyMinimalDevVars(options, [file?.config, inlineConfig], {
419
+ configDir: configPath ? dirname(configPath) : process.cwd(),
420
+ env,
421
+ envFiles
422
+ });
423
+ filterLocalDurableObjects(options, dropped, (inline && wranglerWorkerName(inline, env)) ?? file?.workerName);
424
+ warnDroppedWranglerOptions(dropped);
425
+ warnUnsupportedMinimalBindings(unsupported);
426
+ return {
427
+ options,
428
+ configFile: file ? configPath : void 0
429
+ };
430
+ }
431
+ let fileLoaded = false;
432
+ let fileOptions;
433
+ let fileSecrets;
434
+ let fileWorkerName;
435
+ if (configPath) try {
436
+ const config = wrangler.unstable_readConfig({
308
437
  config: configPath,
309
438
  env
310
- }, { hideWarnings: true }), env).workerOptions) : void 0, inline ? pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(readInlineWranglerConfig(wrangler, inline, env), env).workerOptions) : void 0);
439
+ }, { hideWarnings: !claimWranglerWarnings(configPath, env) });
440
+ fileOptions = pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(config, env, { envFiles }).workerOptions, dropped);
441
+ fileWorkerName = config?.name;
442
+ fileSecrets = config?.secrets;
443
+ fileLoaded = true;
311
444
  } catch (error) {
312
- const desc = [configPath && `"${configPath}"`, inline && "(inline)"].filter(Boolean).join(" + ");
313
- console.warn(`[env-runner] failed to load wrangler config ${desc}: ${error.message}`);
314
- return;
445
+ warnWranglerLoadError(`"${configPath}"`, error);
446
+ }
447
+ let inlineOptions;
448
+ if (inline) try {
449
+ const inlineEnv = env && isPlainObject(inline.env) && inline.env[env] ? env : void 0;
450
+ const { env: _env, ...inlineTopLevel } = inline;
451
+ const inlineConfig = readInlineWranglerConfig(wrangler, inlineEnv ? inline : inlineTopLevel, inlineEnv);
452
+ inlineConfig.userConfigPath = configPath;
453
+ if (fileLoaded && inlineConfig.secrets === void 0) inlineConfig.secrets = fileSecrets;
454
+ inlineOptions = pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(inlineConfig, env, { envFiles }).workerOptions, dropped);
455
+ if (inlineOptions) {
456
+ delete inlineOptions.rootPath;
457
+ if (fileLoaded) filterInlineDevVarBindings(inlineOptions, fileOptions, inlineConfig);
458
+ }
459
+ } catch (error) {
460
+ warnWranglerLoadError("(inline)", error);
315
461
  }
462
+ const options = mergeWranglerMiniflareOptions(fileOptions, inlineOptions);
463
+ filterLocalDurableObjects(options, dropped, (inline && wranglerWorkerName(inline, env)) ?? fileWorkerName);
464
+ warnDroppedWranglerOptions(dropped);
465
+ return {
466
+ options,
467
+ configFile: fileLoaded ? configPath : void 0
468
+ };
469
+ }
470
+ const _shownWranglerWarnings = /* @__PURE__ */ new Set();
471
+ function claimWranglerWarnings(configPath, env) {
472
+ let version = "";
473
+ try {
474
+ const stat = statSync(configPath);
475
+ version = `${stat.mtimeMs}:${stat.size}`;
476
+ } catch {}
477
+ const key = JSON.stringify([
478
+ resolve(configPath),
479
+ env ?? null,
480
+ version
481
+ ]);
482
+ if (_shownWranglerWarnings.has(key)) return false;
483
+ _shownWranglerWarnings.add(key);
484
+ return true;
485
+ }
486
+ function warnWranglerLoadError(desc, error) {
487
+ console.warn(`[env-runner] failed to load wrangler config ${desc}: ${error.message}`);
316
488
  }
317
489
  function mergeWranglerMiniflareOptions(base, override) {
318
490
  if (!base) return override;
@@ -329,6 +501,12 @@ function mergeWranglerMiniflareOptions(base, override) {
329
501
  }
330
502
  return out;
331
503
  }
504
+ function filterInlineDevVarBindings(inlineOptions, fileOptions, inlineConfig) {
505
+ if (!isPlainObject(inlineOptions.bindings)) return;
506
+ const fileBindings = isPlainObject(fileOptions?.bindings) ? fileOptions.bindings : {};
507
+ const declared = /* @__PURE__ */ new Set([...Object.keys(inlineConfig.vars ?? {}), ...inlineConfig.secrets?.required ?? []]);
508
+ inlineOptions.bindings = Object.fromEntries(Object.entries(inlineOptions.bindings).filter(([key]) => declared.has(key) || Object.hasOwn(fileBindings, key)));
509
+ }
332
510
  function readInlineWranglerConfig(wrangler, inline, env) {
333
511
  const dir = mkdtempSync(join(tmpdir(), "env-runner-wrangler-"));
334
512
  const file = join(dir, "wrangler.json");
@@ -345,29 +523,173 @@ function readInlineWranglerConfig(wrangler, inline, env) {
345
523
  });
346
524
  }
347
525
  }
526
+ const _announcedWranglerConfigs = /* @__PURE__ */ new Set();
348
527
  function findWranglerConfig(entryPath) {
349
- const dirs = [];
528
+ const cwd = process.cwd();
529
+ let start = cwd;
350
530
  if (entryPath) {
351
- const resolved = isAbsolute(entryPath) ? entryPath : resolve(entryPath);
352
- dirs.push(dirname(resolved));
531
+ const entryDir = dirname(resolve(entryPath));
532
+ const rel = relative(cwd, entryDir);
533
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) start = entryDir;
534
+ else {
535
+ const local = findWranglerConfigInDir(entryDir);
536
+ if (local) return local;
537
+ }
538
+ }
539
+ for (let dir = start;; dir = dirname(dir)) {
540
+ const found = findWranglerConfigInDir(dir);
541
+ if (found) {
542
+ if (dir !== cwd && dir !== start && !_announcedWranglerConfigs.has(found)) {
543
+ _announcedWranglerConfigs.add(found);
544
+ console.info(`[env-runner] using wrangler config from a parent directory: ${found}`);
545
+ }
546
+ return found;
547
+ }
548
+ if (dirname(dir) === dir) return;
353
549
  }
354
- dirs.push(process.cwd());
355
- for (const dir of dirs) for (const name of WRANGLER_CONFIG_FILENAMES) {
550
+ }
551
+ function findWranglerConfigInDir(dir) {
552
+ for (const name of WRANGLER_CONFIG_FILENAMES) {
356
553
  const candidate = join(dir, name);
357
554
  if (existsSync(candidate)) return candidate;
358
555
  }
359
556
  }
360
- function pickWranglerMiniflareOptions(workerOptions) {
557
+ function pickWranglerMiniflareOptions(workerOptions, dropped) {
361
558
  const out = {};
362
559
  for (const [key, value] of Object.entries(workerOptions)) {
363
560
  if (value === void 0 || WRANGLER_OPTION_DENYLIST.has(key)) continue;
561
+ if (WRANGLER_OPTION_DROPLIST.has(key)) {
562
+ if (!isEmptyOption(value)) addDropped(dropped, WRANGLER_DROPPED_OPTION_NAMES[key], describeDroppedOption(value));
563
+ continue;
564
+ }
565
+ if (isEmptyPickedOption(key, value)) continue;
364
566
  out[key] = value;
365
567
  }
366
568
  return Object.keys(out).length > 0 ? out : void 0;
367
569
  }
570
+ function isEmptyPickedOption(key, value) {
571
+ const isEmptyArray = (v) => Array.isArray(v) && v.length === 0;
572
+ if (isEmptyArray(value) || isPlainObject(value) && Object.keys(value).length === 0) return true;
573
+ return key !== "bindings" && isPlainObject(value) && Object.values(value).every(isEmptyArray);
574
+ }
575
+ function isEmptyOption(value) {
576
+ return !value || Array.isArray(value) && value.length === 0 || isPlainObject(value) && Object.keys(value).length === 0;
577
+ }
578
+ function describeDroppedOption(value) {
579
+ if (Array.isArray(value)) return value.map((entry) => entry?.name).filter((name) => typeof name === "string");
580
+ if (isPlainObject(value)) {
581
+ if ("directory" in value) return typeof value.binding === "string" ? [value.binding] : [];
582
+ return Object.keys(value);
583
+ }
584
+ return [];
585
+ }
586
+ function filterLocalDurableObjects(options, dropped, workerName) {
587
+ const value = options?.durableObjects;
588
+ if (!options || !isPlainObject(value)) return;
589
+ const out = {};
590
+ for (const [name, binding] of Object.entries(value)) if (typeof binding === "string" || isPlainObject(binding) && !binding.scriptName) out[name] = binding;
591
+ else if (isPlainObject(binding) && workerName && binding.scriptName === workerName) {
592
+ const { scriptName: _scriptName, ...local } = binding;
593
+ out[name] = local;
594
+ } else {
595
+ const scriptName = isPlainObject(binding) ? binding.scriptName : void 0;
596
+ addDropped(dropped, "durable_objects", [`${name} → script "${String(scriptName)}"`]);
597
+ }
598
+ if (Object.keys(out).length > 0) options.durableObjects = out;
599
+ else delete options.durableObjects;
600
+ }
601
+ const WRANGLER_NON_INHERITABLE_KEYS = /* @__PURE__ */ new Set([
602
+ "vars",
603
+ "secrets",
604
+ "define",
605
+ "durable_objects",
606
+ "workflows",
607
+ "kv_namespaces",
608
+ "cloudchamber",
609
+ "containers",
610
+ "send_email",
611
+ "queues",
612
+ "connect",
613
+ "r2_buckets",
614
+ "d1_databases",
615
+ "vectorize",
616
+ "ai_search_namespaces",
617
+ "ai_search",
618
+ "websearch",
619
+ "agent_memory",
620
+ "hyperdrive",
621
+ "services",
622
+ "analytics_engine_datasets",
623
+ "dispatch_namespaces",
624
+ "mtls_certificates",
625
+ "tail_consumers",
626
+ "streaming_tail_consumers",
627
+ "unsafe",
628
+ "browser",
629
+ "ai",
630
+ "images",
631
+ "stream",
632
+ "media",
633
+ "pipelines",
634
+ "secrets_store_secrets",
635
+ "artifacts",
636
+ "unsafe_hello_world",
637
+ "flagship",
638
+ "worker_loaders",
639
+ "ratelimits",
640
+ "vpc_services",
641
+ "vpc_networks",
642
+ "version_metadata"
643
+ ]);
644
+ const MINIMAL_UNSUPPORTED_BINDING_KEYS = [
645
+ "hyperdrive",
646
+ "analytics_engine_datasets",
647
+ "ai",
648
+ "ai_search_namespaces",
649
+ "ai_search",
650
+ "websearch",
651
+ "agent_memory",
652
+ "version_metadata",
653
+ "ratelimits",
654
+ "send_email",
655
+ "secrets_store_secrets",
656
+ "vectorize",
657
+ "browser",
658
+ "images",
659
+ "stream",
660
+ "media",
661
+ "pipelines",
662
+ "dispatch_namespaces",
663
+ "mtls_certificates",
664
+ "worker_loaders",
665
+ "vpc_services",
666
+ "vpc_networks",
667
+ "artifacts",
668
+ "flagship",
669
+ "containers",
670
+ "unsafe",
671
+ "unsafe_hello_world",
672
+ "logfwdr",
673
+ "wasm_modules",
674
+ "text_blobs",
675
+ "data_blobs"
676
+ ];
677
+ const MINIMAL_BINDING_OPTIONS = [
678
+ "kvNamespaces",
679
+ "r2Buckets",
680
+ "d1Databases",
681
+ "queueProducers",
682
+ "durableObjects"
683
+ ];
684
+ function warnUnsupportedMinimalBindings(unsupported) {
685
+ if (unsupported.size === 0) return;
686
+ const list = [...unsupported].map(([name, details]) => details.size > 0 ? `${name} (${[...details].join(", ")})` : name).join(", ");
687
+ console.warn(`[env-runner] wrangler config bindings not supported by the built-in minimal wrangler config reader were ignored: ${list}; install 'wrangler' (or pass it as \`wranglerModule\`) to use them.`);
688
+ }
368
689
  function readWranglerConfigMinimal(configPath, env) {
369
- if (extname(configPath).toLowerCase() !== ".json") {
370
- console.warn(`[env-runner] reading "${basename(configPath)}" requires the 'wrangler' package; the built-in reader supports plain JSON only (install 'wrangler' for JSONC/TOML).`);
690
+ const ext = extname(configPath).toLowerCase();
691
+ if (ext !== ".json" && ext !== ".jsonc") {
692
+ console.warn(`[env-runner] reading "${basename(configPath)}" requires the 'wrangler' package; the built-in reader supports JSON/JSONC only (install 'wrangler' for TOML).`);
371
693
  return;
372
694
  }
373
695
  let raw;
@@ -378,45 +700,203 @@ function readWranglerConfigMinimal(configPath, env) {
378
700
  }
379
701
  let config;
380
702
  try {
381
- config = JSON.parse(raw);
703
+ config = parseJSONC(raw);
382
704
  } catch (error) {
383
705
  console.warn(`[env-runner] failed to parse wrangler config "${configPath}": ${error.message}`);
384
706
  return;
385
707
  }
386
- return mapWranglerConfigToMiniflare(applyWranglerEnv(config, env));
708
+ if (!isPlainObject(config)) {
709
+ warnWranglerLoadError(`"${configPath}"`, /* @__PURE__ */ new Error("config must be an object"));
710
+ return;
711
+ }
712
+ let envConfig;
713
+ try {
714
+ envConfig = applyWranglerEnv(config, env, configPath);
715
+ } catch (error) {
716
+ warnWranglerLoadError(`"${configPath}"`, error);
717
+ return;
718
+ }
719
+ return {
720
+ config: envConfig,
721
+ workerName: wranglerWorkerName(config, env)
722
+ };
723
+ }
724
+ function applyWranglerEnv(config, env, configPath) {
725
+ const { env: envs, ...topLevel } = config;
726
+ if (!env) return topLevel;
727
+ const rawEnv = isPlainObject(envs) ? envs[env] : void 0;
728
+ const warnings = [];
729
+ let out;
730
+ if (isPlainObject(rawEnv)) {
731
+ out = {};
732
+ for (const [key, value] of Object.entries(topLevel)) if (!WRANGLER_NON_INHERITABLE_KEYS.has(key)) out[key] = value;
733
+ else if (rawEnv[key] === void 0) warnings.push(`"${key}" exists at the top level, but is not inherited by "env.${env}"`);
734
+ Object.assign(out, rawEnv);
735
+ } else {
736
+ if (configPath && isPlainObject(envs)) throw new Error(`No environment found in configuration with name "${env}". The available configured environment names are: ${JSON.stringify(Object.keys(envs))}`);
737
+ warnings.push(`No environment found in configuration with name "${env}"`);
738
+ out = topLevel;
739
+ }
740
+ if (configPath && warnings.length > 0 && claimWranglerWarnings(configPath, env)) console.warn(`[env-runner] wrangler config "${configPath}": ${warnings.join("; ")}.`);
741
+ return out;
387
742
  }
388
- function applyWranglerEnv(config, env) {
389
- return env && config.env?.[env] ? {
390
- ...config,
391
- ...config.env[env]
392
- } : config;
743
+ function wranglerWorkerName(config, env) {
744
+ const envName = env ? config.env?.[env]?.name : void 0;
745
+ if (typeof envName === "string") return envName;
746
+ if (typeof config.name !== "string" || !config.name) return void 0;
747
+ return env ? `${config.name}-${env}` : config.name;
393
748
  }
394
- function mapWranglerConfigToMiniflare(config) {
749
+ function mapWranglerConfigToMiniflare(config, dropped, unsupported) {
395
750
  const out = {};
751
+ for (const [name, value, field] of [
752
+ [
753
+ "services",
754
+ config.services,
755
+ "binding"
756
+ ],
757
+ [
758
+ "assets",
759
+ config.assets,
760
+ "binding"
761
+ ],
762
+ [
763
+ "queues.consumers",
764
+ config.queues?.consumers,
765
+ "queue"
766
+ ],
767
+ [
768
+ "workflows",
769
+ config.workflows,
770
+ "binding"
771
+ ],
772
+ [
773
+ "tail_consumers",
774
+ config.tail_consumers,
775
+ "service"
776
+ ],
777
+ [
778
+ "streaming_tail_consumers",
779
+ config.streaming_tail_consumers,
780
+ "service"
781
+ ]
782
+ ]) if (!isEmptyOption(value)) addDropped(dropped, name, (Array.isArray(value) ? value : [value]).map((e) => e?.[field]).filter((d) => typeof d === "string"));
783
+ for (const key of MINIMAL_UNSUPPORTED_BINDING_KEYS) {
784
+ const value = config[key];
785
+ const bindings = isPlainObject(value) && Array.isArray(value.bindings) ? value.bindings : value;
786
+ if (!isEmptyOption(bindings)) addDropped(unsupported, key, describeBindingNames(bindings));
787
+ }
396
788
  if (typeof config.compatibility_date === "string") out.compatibilityDate = config.compatibility_date;
397
789
  if (Array.isArray(config.compatibility_flags)) out.compatibilityFlags = config.compatibility_flags;
398
790
  if (config.vars && typeof config.vars === "object") out.bindings = { ...config.vars };
399
- const kv = mapBindingArray(config.kv_namespaces, "binding", (n) => n.id ?? n.binding);
791
+ const kv = mapBindingArray(config.kv_namespaces, "binding", (n) => firstString(n.preview_id, n.id, n.binding));
400
792
  if (kv) out.kvNamespaces = kv;
401
- const r2 = mapBindingArray(config.r2_buckets, "binding", (n) => n.bucket_name ?? n.binding);
793
+ const r2 = mapBindingArray(config.r2_buckets, "binding", (n) => firstString(n.preview_bucket_name, n.bucket_name, n.binding));
402
794
  if (r2) out.r2Buckets = r2;
403
- const d1 = mapBindingArray(config.d1_databases, "binding", (n) => n.database_id ?? n.preview_database_id ?? n.binding);
795
+ const d1 = mapBindingArray(config.d1_databases, "binding", (n) => firstString(n.preview_database_id, n.database_id, n.binding));
404
796
  if (d1) out.d1Databases = d1;
405
797
  const queues = mapBindingArray(config.queues?.producers, "binding", (n) => n.queue);
406
798
  if (queues) out.queueProducers = queues;
407
- if (Array.isArray(config.durable_objects?.bindings)) {
408
- const dos = {};
409
- for (const b of config.durable_objects.bindings) {
410
- if (!b?.name || !b?.class_name) continue;
411
- dos[b.name] = b.script_name ? {
412
- className: b.class_name,
413
- scriptName: b.script_name
414
- } : b.class_name;
415
- }
416
- if (Object.keys(dos).length > 0) out.durableObjects = dos;
799
+ const useSQLite = durableObjectClassStorage(config);
800
+ const doBindings = Array.isArray(config.durable_objects?.bindings) ? config.durable_objects.bindings : [];
801
+ const dos = {};
802
+ for (const b of doBindings) {
803
+ if (!b?.name || !b?.class_name) continue;
804
+ dos[b.name] = {
805
+ className: b.class_name,
806
+ ...b.script_name ? { scriptName: b.script_name } : {},
807
+ ...useSQLite.has(b.class_name) ? { useSQLite: useSQLite.get(b.class_name) } : {}
808
+ };
417
809
  }
810
+ if (Object.keys(dos).length > 0) out.durableObjects = dos;
811
+ const unbound = [...useSQLite].filter(([className]) => !doBindings.some((b) => b?.class_name === className)).map(([className, sqlite]) => ({
812
+ className,
813
+ useSQLite: sqlite
814
+ }));
815
+ if (unbound.length > 0) out.additionalUnboundDurableObjects = unbound;
418
816
  return Object.keys(out).length > 0 ? out : void 0;
419
817
  }
818
+ function durableObjectClassStorage(config) {
819
+ const classes = /* @__PURE__ */ new Map();
820
+ for (const migration of Array.isArray(config.migrations) ? config.migrations : []) {
821
+ for (const name of migration?.deleted_classes ?? []) classes.delete(name);
822
+ for (const { from, to } of migration?.renamed_classes ?? []) {
823
+ const sqlite = classes.get(from);
824
+ if (sqlite !== void 0) {
825
+ classes.delete(from);
826
+ classes.set(to, sqlite);
827
+ }
828
+ }
829
+ for (const name of migration?.new_classes ?? []) classes.set(name, false);
830
+ for (const name of migration?.new_sqlite_classes ?? []) classes.set(name, true);
831
+ }
832
+ if (isPlainObject(config.exports)) for (const [name, entry] of Object.entries(config.exports)) {
833
+ const state = entry?.state;
834
+ if (entry?.type === "durable-object" && (state === void 0 || state === "created" || state === "expecting-transfer")) classes.set(name, entry.storage === "sqlite");
835
+ }
836
+ return classes;
837
+ }
838
+ function describeBindingNames(value) {
839
+ if (isPlainObject(value) && typeof value.binding !== "string") return Object.keys(value);
840
+ return (Array.isArray(value) ? value : [value]).map((e) => e?.binding ?? e?.name ?? e?.class_name).filter((name) => typeof name === "string");
841
+ }
842
+ function applyMinimalDevVars(options, configs, opts) {
843
+ const secrets = configs.map((c) => c?.secrets).filter(isPlainObject);
844
+ const loaded = loadDevVars({
845
+ ...opts,
846
+ hasSecrets: secrets.length > 0
847
+ });
848
+ if (!loaded) return options;
849
+ const out = { ...options };
850
+ const bindings = isPlainObject(out.bindings) ? { ...out.bindings } : {};
851
+ const taken = new Set(MINIMAL_BINDING_OPTIONS.flatMap((key) => isPlainObject(out[key]) ? Object.keys(out[key]) : []));
852
+ const required = new Set(secrets.flatMap((s) => Array.isArray(s.required) ? s.required : []));
853
+ let changed = false;
854
+ for (const [key, value] of Object.entries(loaded)) {
855
+ if (taken.has(key) || secrets.length > 0 && !Object.hasOwn(bindings, key) && !required.has(key)) continue;
856
+ bindings[key] = value;
857
+ changed = true;
858
+ }
859
+ const missing = [...required].filter((key) => !Object.hasOwn(loaded, key));
860
+ if (missing.length > 0) console.warn(`[env-runner] missing required wrangler secrets: ${missing.join(", ")}. Add them to .dev.vars, .env, or set as environment variables.`);
861
+ if (!changed) return options;
862
+ out.bindings = bindings;
863
+ return out;
864
+ }
865
+ function firstString(...values) {
866
+ return values.find((value) => typeof value === "string");
867
+ }
868
+ function parseJSONC(text) {
869
+ let out = "";
870
+ let comma = -1;
871
+ const src = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
872
+ for (let i = 0; i < src.length; i++) {
873
+ const ch = src[i];
874
+ if (ch === "/" && (src[i + 1] === "/" || src[i + 1] === "*")) {
875
+ const line = src[i + 1] === "/";
876
+ let end = line ? src.indexOf("\n", i) : src.indexOf("*/", i + 2);
877
+ end = end === -1 ? src.length : line ? end : end + 2;
878
+ out += src.slice(i, end).replace(/[^\n]/g, " ");
879
+ i = end - 1;
880
+ continue;
881
+ }
882
+ if (/\s/.test(ch)) {
883
+ out += ch;
884
+ continue;
885
+ }
886
+ if ((ch === "}" || ch === "]") && comma !== -1) out = `${out.slice(0, comma)} ${out.slice(comma + 1)}`;
887
+ comma = -1;
888
+ if (ch === "\"") {
889
+ let end = i + 1;
890
+ while (end < src.length && src[end] !== "\"") end += src[end] === "\\" ? 2 : 1;
891
+ out += src.slice(i, end + 1);
892
+ i = end;
893
+ continue;
894
+ }
895
+ if (ch === ",") comma = out.length;
896
+ out += ch;
897
+ }
898
+ return JSON.parse(out);
899
+ }
420
900
  function mapBindingArray(arr, keyField, value) {
421
901
  if (!Array.isArray(arr)) return;
422
902
  const out = {};
@@ -426,6 +906,7 @@ function mapBindingArray(arr, keyField, value) {
426
906
  }
427
907
  return Object.keys(out).length > 0 ? out : void 0;
428
908
  }
909
+ const IPC_PATH = "/__env_runner_ipc";
429
910
  const _miniflareCache = /* @__PURE__ */ new Map();
430
911
  var MiniflareEnvRunner = class extends BaseEnvRunner {
431
912
  #miniflare;
@@ -433,7 +914,6 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
433
914
  #transformRequest;
434
915
  #reloadCounter = 0;
435
916
  #virtual;
436
- #virtualVersions = /* @__PURE__ */ new Map();
437
917
  #cacheEntry;
438
918
  #ws;
439
919
  #persistent;
@@ -443,6 +923,9 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
443
923
  #exportConditions;
444
924
  #wrangler;
445
925
  #wranglerEnv;
926
+ #wranglerConfigPath;
927
+ #wranglerEnvFiles;
928
+ #compatibilityDate;
446
929
  #wranglerModule;
447
930
  #miniflareModule;
448
931
  constructor(opts) {
@@ -460,6 +943,9 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
460
943
  this.#wrangler = opts.wrangler ?? false;
461
944
  this.#wranglerEnv = opts.wranglerEnv ?? process.env.CLOUDFLARE_ENV;
462
945
  this.#wranglerModule = opts.wranglerModule;
946
+ this.#wranglerConfigPath = opts.wranglerConfigPath;
947
+ this.#wranglerEnvFiles = opts.wranglerEnvFiles;
948
+ this.#compatibilityDate = opts.compatibilityDate;
463
949
  this._initWithVirtualData(() => this.#init());
464
950
  }
465
951
  static async disposeAll() {
@@ -484,8 +970,12 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
484
970
  for (let i = 0; i < 5 && !this._address && !this.closed; i++) await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
485
971
  if (!this.#miniflare || this.closed) return new Response("miniflare env runner is unavailable", { status: 503 });
486
972
  const resolved = this._resolveFetchInput(input);
487
- const request = typeof resolved === "string" || resolved instanceof URL ? new Request(resolved, init) : new Request(new Request(resolved.url, resolved), {
973
+ const request = typeof resolved === "string" || resolved instanceof URL ? new Request(resolved, {
488
974
  ...init,
975
+ redirect: "manual"
976
+ }) : new Request(new Request(resolved.url, resolved), {
977
+ ...init,
978
+ redirect: "manual",
489
979
  referrer: init?.referrer ?? resolved.referrer,
490
980
  referrerPolicy: init?.referrerPolicy ?? resolved.referrerPolicy
491
981
  });
@@ -512,6 +1002,7 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
512
1002
  }));
513
1003
  }
514
1004
  async reloadModule(timeout = 5e3) {
1005
+ await this._virtualUpdates;
515
1006
  if (!this.#ws) throw new Error("Miniflare env runner should be initialized before reloading.");
516
1007
  if (!this._data?.entry) return;
517
1008
  this.#reloadCounter++;
@@ -525,15 +1016,27 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
525
1016
  send: (message) => this.#ws.send(JSON.stringify(message))
526
1017
  });
527
1018
  }
528
- async invalidateModule(specifier, _timeout) {
1019
+ async _applyVirtualUpdates(changes) {
529
1020
  const virtual = this.#virtual;
530
- if (!virtual || !Object.hasOwn(virtual, specifier)) {
531
- const hasVirtual = Object.keys(this._data?.virtual ?? {}).length > 0;
532
- 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)`);
1021
+ if (!virtual) throw new Error("Miniflare env runner should be initialized before updating modules.");
1022
+ const prepared = {};
1023
+ for (const [key, module] of Object.entries(changes)) if (module !== null) prepared[key] = await this.#prepareVirtualModule(key, module);
1024
+ await init$1;
1025
+ const merged = {
1026
+ ...virtual.sources,
1027
+ ...prepared
1028
+ };
1029
+ const importers = virtualImporters(merged, createVirtualKeyResolver(Object.keys(merged)));
1030
+ const invalidated = expandVirtualInvalidation(merged, Object.keys(changes), importers);
1031
+ for (const key of Object.keys(changes)) if (Object.hasOwn(prepared, key)) {
1032
+ virtual.sources[key] = prepared[key];
1033
+ virtual.removed.delete(key);
1034
+ } else if (Object.hasOwn(virtual.sources, key)) {
1035
+ delete virtual.sources[key];
1036
+ virtual.removed.add(key);
533
1037
  }
534
- const source = await this._refreshVirtualSource(specifier);
535
- if (source !== void 0) virtual[specifier] = await this.#prepareVirtualSource(specifier, source);
536
- for (const key of expandVirtualInvalidation(virtual, specifier)) this.#virtualVersions.set(key, (this.#virtualVersions.get(key) ?? 0) + 1);
1038
+ for (const key of invalidated) virtual.versions.set(key, ++virtual.version);
1039
+ refreshVirtualKeyResolvers(virtual);
537
1040
  if (this.#cacheKey && _miniflareCache.get(this.#cacheKey) === this.#cacheEntry) _miniflareCache.delete(this.#cacheKey);
538
1041
  }
539
1042
  _hasRuntime() {
@@ -591,38 +1094,60 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
591
1094
  } catch {}
592
1095
  }
593
1096
  async #prepareVirtualModules() {
594
- const virtual = this._data?.virtual;
595
- if (!virtual || Object.keys(virtual).length === 0) return;
1097
+ const virtual = this._data?.virtual ?? {};
596
1098
  const out = {};
597
- for (const [specifier, source] of Object.entries(virtual)) out[specifier] = await this.#prepareVirtualSource(specifier, source);
1099
+ for (const [key, module] of Object.entries(virtual)) out[key] = await this.#prepareVirtualModule(key, module);
598
1100
  return out;
599
1101
  }
600
- async #prepareVirtualSource(specifier, source) {
601
- if (virtualModuleFormat(specifier) !== "module-typescript") return source;
602
- return stripVirtualTypeScript(specifier, source, await _getStripTypeScriptTypes(), {
603
- requirement: "on the host (workerd does not parse TypeScript)",
604
- remedy: "upgrade Node.js"
605
- });
1102
+ async #prepareVirtualModule(key, module) {
1103
+ const format = virtualModuleFormat(key, module);
1104
+ const source = typeof module === "string" ? module : module.source;
1105
+ switch (format) {
1106
+ case "module-typescript":
1107
+ case "commonjs-typescript": return {
1108
+ source: stripVirtualTypeScript(key, source, await _getStripTypeScriptTypes(), {
1109
+ requirement: "on the host (workerd does not parse TypeScript)",
1110
+ remedy: "upgrade Node.js"
1111
+ }),
1112
+ format: format === "module-typescript" ? "module" : "commonjs"
1113
+ };
1114
+ case "jsx":
1115
+ case "tsx": throw unsupportedVirtualJSXError(key, format, "workerd");
1116
+ }
1117
+ return {
1118
+ source,
1119
+ format
1120
+ };
606
1121
  }
607
1122
  async #initAsync() {
608
- const { Miniflare, supportedCompatibilityDate } = await this.#resolveMiniflare();
1123
+ const miniflare = await this.#resolveMiniflare();
1124
+ const supportedCompatibilityDate = await resolveSupportedCompatibilityDate(miniflare);
609
1125
  const entryPath = this._data?.entry;
610
- const virtual = await this.#prepareVirtualModules();
1126
+ const initialVirtual = await this.#prepareVirtualModules();
1127
+ const virtual = createMiniflareVirtualModules({ ...initialVirtual });
611
1128
  this.#virtual = virtual;
612
- const wranglerOptions = await loadWranglerConfig(this.#wrangler, this.#wranglerEnv, entryPath, this.#wranglerModule);
1129
+ const { options: wranglerOptions, configFile: wranglerConfigFile } = await loadWranglerConfig({
1130
+ wrangler: this.#wrangler,
1131
+ env: this.#wranglerEnv,
1132
+ entryPath,
1133
+ configPath: this.#wranglerConfigPath,
1134
+ wranglerModule: this.#wranglerModule,
1135
+ envFiles: this.#wranglerEnvFiles
1136
+ });
613
1137
  const userFlags = this.#miniflareOptions.compatibilityFlags || [];
614
1138
  const wranglerFlags = wranglerOptions?.compatibilityFlags || [];
615
1139
  const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || [];
616
1140
  const options = {
617
- compatibilityDate: supportedCompatibilityDate,
618
1141
  modules: true,
1142
+ ...this.#wrangler && !hasUserPersistOptions(this.#miniflareOptions) ? { defaultPersistRoot: wranglerPersistRoot(wranglerConfigFile, typeof this.#wrangler === "string" ? this.#wrangler : this.#wranglerConfigPath) } : void 0,
619
1143
  ...wranglerOptions,
620
1144
  ...this.#miniflareOptions,
621
- compatibilityFlags: [.../* @__PURE__ */ new Set([
622
- "nodejs_compat",
623
- ...wranglerFlags,
624
- ...userFlags
625
- ])],
1145
+ compatibilityDate: resolveCompatibilityDate([
1146
+ this.#miniflareOptions.compatibilityDate,
1147
+ this.#compatibilityDate === "latest" ? supportedCompatibilityDate : this.#compatibilityDate,
1148
+ wranglerOptions?.compatibilityDate
1149
+ ], supportedCompatibilityDate),
1150
+ compatibilityFlags: resolveCompatibilityFlags(wranglerFlags, userFlags),
626
1151
  unsafeDirectSockets: [{
627
1152
  host: "127.0.0.1",
628
1153
  port: 0
@@ -635,28 +1160,42 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
635
1160
  ...uValue
636
1161
  };
637
1162
  }
1163
+ const ipc = { runner: this };
638
1164
  if (entryPath && !options.script && !options.scriptPath) {
639
- const entryIsVirtual = isVirtualSpecifier(entryPath, virtual);
640
- const resolvedEntry = entryIsVirtual ? entryPath : resolve(entryPath);
1165
+ const entryKey = virtual.keyOf(entryPath) ?? virtual.keyOf(resolve(entryPath));
1166
+ const entryIsVirtual = entryKey !== void 0;
1167
+ const resolvedEntry = entryIsVirtual ? virtualKeyPath(entryKey) ?? entryKey : resolve(entryPath);
641
1168
  const entryBase = isAbsolute(resolvedEntry) ? resolvedEntry : resolve("__env_runner_virtual_entry__.mjs");
642
1169
  const entryDir = dirname(entryBase);
643
- const entrySource = entryIsVirtual ? virtual[entryPath] : _tryReadFile(resolvedEntry);
644
- const detectedExports = this.#exports === false || this.#exports === void 0 ? [] : detectExportedClasses(entrySource, typeof this.#exports === "object" ? this.#exports : {});
645
- 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.`);
646
- if (detectedExports.length > 0 && !options.durableObjects) {
647
- const autoDOs = { ...this.#miniflareOptions.durableObjects || {} };
1170
+ const entrySource = entryIsVirtual ? virtualModuleCodeSource(entryKey, virtual.sources[entryKey]) : _tryReadFile(resolvedEntry);
1171
+ const detectedExports = this.#exports === false || typeof this.#exports === "string" ? [] : detectExportedClasses(entrySource, typeof this.#exports === "object" ? this.#exports : {});
1172
+ if (detectedExports.length > 0) {
1173
+ const existingDOs = isPlainObject(options.durableObjects) ? options.durableObjects : {};
1174
+ const boundClasses = new Set(Object.values(existingDOs).map((b) => typeof b === "string" ? b : isPlainObject(b) && !b.scriptName ? b.className : void 0));
1175
+ const autoDOs = { ...existingDOs };
648
1176
  for (const name of detectedExports) {
649
1177
  const bindingName = toScreamingSnakeCase(name);
650
- if (!autoDOs[bindingName]) autoDOs[bindingName] = name;
1178
+ if (!autoDOs[bindingName] && !boundClasses.has(name)) autoDOs[bindingName] = name;
651
1179
  }
652
1180
  options.durableObjects = autoDOs;
653
1181
  }
654
- options.script = generateWrapper(resolvedEntry, {
1182
+ const script = generateWrapper(entryIsVirtual ? toWorkerdPath(resolvedEntry) : resolvedEntry, {
655
1183
  dynamicOnly: true,
656
1184
  captureErrors: this.#captureErrors,
657
- exports: detectedExports
1185
+ exports: typeof this.#exports === "string" ? this.#exports : detectedExports,
1186
+ nodeCompat: !options.compatibilityFlags.includes("no_nodejs_compat")
658
1187
  });
659
- options.scriptPath = entryDir + "/__env_runner_wrapper.mjs";
1188
+ const scriptPath = entryDir + "/__env_runner_wrapper.mjs";
1189
+ const skipLocator = typeof this.#exports === "string" || entryIsVirtual && detectedExports.length > 0;
1190
+ if (skipLocator) options.modules = [{
1191
+ type: "ESModule",
1192
+ path: scriptPath,
1193
+ contents: script
1194
+ }];
1195
+ else {
1196
+ options.script = script;
1197
+ options.scriptPath = scriptPath;
1198
+ }
660
1199
  if (!options.modulesRoot) options.modulesRoot = "/";
661
1200
  options.unsafeEvalBinding = UNSAFE_EVAL_BINDING;
662
1201
  options.serviceBindings = {
@@ -664,12 +1203,12 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
664
1203
  [IPC_BINDING]: async (request) => {
665
1204
  try {
666
1205
  const message = await request.json();
667
- this._handleMessage(message);
1206
+ ipc.runner._handleMessage(message);
668
1207
  } catch {}
669
1208
  return new Response(null, { status: 204 });
670
1209
  }
671
1210
  };
672
- if (this.#transformRequest && !options.modulesRules) options.modulesRules = [{
1211
+ if (this.#transformRequest && !options.modulesRules && !miniflare.convertV4MiniflareOptions && !skipLocator) options.modulesRules = [{
673
1212
  type: "ESModule",
674
1213
  include: [
675
1214
  "**/*.ts",
@@ -681,10 +1220,23 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
681
1220
  if (!options.unsafeModuleFallbackService) {
682
1221
  const _require = createRequire(entryBase);
683
1222
  const _virtual = virtual;
684
- const _virtualVersions = this.#virtualVersions;
685
1223
  const _transformRequest = this.#transformRequest;
686
1224
  const _exportConditions = this.#exportConditions;
687
- const _applyVirtualVersions = (code) => applyVirtualVersions(code, _virtualVersions);
1225
+ const _applyVirtualVersions = (code, modulePath) => applyVirtualVersions(code, _virtual.versions, (specifier) => _virtual.versionedKeyOf(specifier, modulePath));
1226
+ const _serveVirtual = (module, modulePath) => {
1227
+ const { source } = module;
1228
+ switch (module.format) {
1229
+ case "module": return { esModule: _applyVirtualVersions(source, modulePath) };
1230
+ case "commonjs": return {
1231
+ commonJsModule: source,
1232
+ namedExports: commonJSExports(source).filter((e) => e !== "default")
1233
+ };
1234
+ case "json": return { json: source };
1235
+ case "text": return { text: source };
1236
+ case "bytes": return { esModule: virtualModuleCode("bytes", source) };
1237
+ case "wasm": return { wasm: Array.from(source) };
1238
+ }
1239
+ };
688
1240
  options.unsafeUseModuleFallbackService = true;
689
1241
  const modulePathMap = /* @__PURE__ */ new Map();
690
1242
  const _lexersReady = Promise.all([ensureCjsLexer(), init$1]);
@@ -697,24 +1249,30 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
697
1249
  if (!specifier) return new Response(null, { status: 404 });
698
1250
  const cleanSpecifier = specifier.split("?")[0] || specifier;
699
1251
  const cleanRaw = rawSpecifier?.split("?")[0];
700
- if (_virtual) {
701
- const bareSpecifier = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
702
- const virtualKey = [
703
- cleanRaw,
704
- cleanSpecifier,
705
- bareSpecifier
706
- ].find((key) => key !== void 0 && Object.hasOwn(_virtual, key));
707
- if (virtualKey !== void 0) {
708
- const name = bareSpecifier + (specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "");
709
- const source = _virtual[virtualKey];
710
- return virtualModuleFormat(virtualKey) === "json" ? Response.json({
711
- name,
712
- json: source
713
- }) : Response.json({
714
- name,
715
- esModule: _applyVirtualVersions(source)
1252
+ const bareSpecifier = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
1253
+ const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
1254
+ const referrerPath = modulePathMap.get(referrerKey);
1255
+ const virtualKey = [
1256
+ cleanRaw,
1257
+ cleanSpecifier,
1258
+ bareSpecifier
1259
+ ].find((key) => key !== void 0 && Object.hasOwn(_virtual.sources, key)) ?? (cleanRaw ? _virtual.keyOf(cleanRaw, referrerPath) : void 0) ?? _virtual.keyOf(cleanSpecifier);
1260
+ if (virtualKey !== void 0) {
1261
+ const query = specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "";
1262
+ const keyPath = virtualKeyPath(virtualKey);
1263
+ if (keyPath && cleanRaw?.startsWith("file:") && cleanSpecifier !== keyPath && cleanSpecifier.includes("file:")) {
1264
+ const location = Buffer.from(toWorkerdPath(keyPath) + query, "utf8").toString("latin1");
1265
+ return new Response(null, {
1266
+ status: 301,
1267
+ headers: { location }
716
1268
  });
717
1269
  }
1270
+ const name = bareSpecifier + query;
1271
+ if (keyPath) modulePathMap.set(name, keyPath);
1272
+ return Response.json({
1273
+ name,
1274
+ ..._serveVirtual(_virtual.sources[virtualKey], keyPath)
1275
+ });
718
1276
  }
719
1277
  let resolvedPath;
720
1278
  const fileUrlRaw = cleanRaw || cleanSpecifier;
@@ -723,9 +1281,8 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
723
1281
  } catch {
724
1282
  return new Response(null, { status: 404 });
725
1283
  }
726
- else if (cleanRaw && !cleanRaw.startsWith(".") && !cleanRaw.startsWith("/")) {
727
- const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
728
- const referrerReal = modulePathMap.get(referrerKey);
1284
+ else if (cleanRaw && !cleanRaw.startsWith(".") && !cleanRaw.startsWith("/") && !isAbsolute(cleanRaw)) {
1285
+ const referrerReal = referrerPath;
729
1286
  const contextRequire = referrerReal ? createRequire(referrerReal) : _require;
730
1287
  if (cleanRaw.startsWith("cloudflare:")) return new Response(null, { status: 404 });
731
1288
  if (cleanRaw.startsWith("node:")) {
@@ -749,11 +1306,11 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
749
1306
  });
750
1307
  }
751
1308
  } else {
752
- const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
753
- const referrerReal = modulePathMap.get(referrerKey) || (referrer.startsWith("/") ? referrer : "/" + referrer);
1309
+ const referrerReal = referrerPath || (referrer.startsWith("/") ? referrer : "/" + referrer);
754
1310
  const referrerDir = dirname(referrerReal);
755
1311
  const raw = cleanRaw || cleanSpecifier;
756
1312
  if (raw.startsWith(".")) resolvedPath = resolve(referrerDir, raw);
1313
+ else if (cleanRaw && isAbsolute(cleanRaw) && !cleanRaw.startsWith("/")) resolvedPath = cleanRaw;
757
1314
  else if (cleanSpecifier.startsWith("/")) resolvedPath = cleanSpecifier;
758
1315
  else try {
759
1316
  resolvedPath = _require.resolve(raw);
@@ -769,7 +1326,7 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
769
1326
  modulePathMap.set(name, resolvedPath);
770
1327
  return Response.json({
771
1328
  name,
772
- esModule: _applyVirtualVersions(result.code)
1329
+ esModule: _applyVirtualVersions(result.code, resolvedPath)
773
1330
  });
774
1331
  }
775
1332
  } catch {}
@@ -778,7 +1335,7 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
778
1335
  modulePathMap.set(name, resolvedPath);
779
1336
  if (resolvedPath.endsWith(".mjs") || !resolvedPath.endsWith(".cjs") && /\b(import\s|import\(|export\s|export\{|import\.meta\b)/.test(contents)) return Response.json({
780
1337
  name,
781
- esModule: _applyVirtualVersions(contents)
1338
+ esModule: _applyVirtualVersions(contents, resolvedPath)
782
1339
  });
783
1340
  const cjsSuffix = "?__cjs";
784
1341
  if (specifier.endsWith(cjsSuffix)) return Response.json({
@@ -800,31 +1357,35 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
800
1357
  this.#cacheKey = computeCacheKey(entryPath, {
801
1358
  ...this.#miniflareOptions,
802
1359
  _exportConditions: this.#exportConditions,
803
- _virtual: virtual
1360
+ _exports: this.#exports,
1361
+ _virtual: encodeVirtualModules(initialVirtual)
804
1362
  });
805
1363
  const cached = _miniflareCache.get(this.#cacheKey);
806
1364
  if (cached) {
807
1365
  this.#miniflare = cached.mf;
808
1366
  cached.refCount++;
809
1367
  this.#cacheEntry = cached;
1368
+ cached.ipc.runner = this;
810
1369
  this.#virtual = cached.virtual;
811
- this.#virtualVersions = cached.versions;
812
1370
  }
813
1371
  }
814
1372
  if (!this.#miniflare) {
815
- this.#miniflare = new Miniflare(options);
1373
+ this.#miniflare = new miniflare.Miniflare(toMiniflareOptions(miniflare, options));
816
1374
  await this.#miniflare.ready;
817
1375
  if (this.#persistent && this.#cacheKey) {
818
1376
  this.#cacheEntry = {
819
1377
  mf: this.#miniflare,
820
1378
  refCount: 1,
821
1379
  virtual,
822
- versions: this.#virtualVersions
1380
+ ipc
823
1381
  };
824
1382
  _miniflareCache.set(this.#cacheKey, this.#cacheEntry);
825
1383
  }
826
1384
  }
827
- const initRes = await this.#miniflare.dispatchFetch("http://localhost/__env_runner_ipc", { headers: { upgrade: "websocket" } });
1385
+ const loadRes = await this.#miniflare.dispatchFetch("http://localhost" + IPC_PATH);
1386
+ const loadBody = await loadRes.text().catch(() => "");
1387
+ if (!loadRes.ok) throw new Error(`Failed to establish WebSocket IPC channel (${loadRes.status}: ${loadBody})`);
1388
+ const initRes = await this.#miniflare.dispatchFetch("http://localhost" + IPC_PATH, { headers: { upgrade: "websocket" } });
828
1389
  const ws = initRes.webSocket;
829
1390
  if (!ws) {
830
1391
  const body = await initRes.text().catch(() => "");
@@ -853,6 +1414,55 @@ function detectExportedClasses(entrySource, explicit) {
853
1414
  }
854
1415
  return [...names];
855
1416
  }
1417
+ function toMiniflareOptions(miniflare, options) {
1418
+ if (!miniflare.convertV4MiniflareOptions) return options;
1419
+ const { defaultPersistRoot, ...rest } = options;
1420
+ return miniflare.convertV4MiniflareOptions({
1421
+ resourcePersistencePath: defaultPersistRoot,
1422
+ ...rest
1423
+ });
1424
+ }
1425
+ async function resolveSupportedCompatibilityDate(miniflare) {
1426
+ if (typeof miniflare.supportedCompatibilityDate === "string") return miniflare.supportedCompatibilityDate;
1427
+ try {
1428
+ const from = [process.cwd() + "/", import.meta.url];
1429
+ const miniflarePath = resolveModulePath("miniflare", {
1430
+ from,
1431
+ try: true
1432
+ });
1433
+ const workerdPath = resolveModulePath("workerd", { from: miniflarePath || from });
1434
+ const { compatibilityDate } = await import(pathToFileURL(workerdPath).href);
1435
+ if (!_isDateString(compatibilityDate)) return;
1436
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1437
+ return compatibilityDate > today ? today : compatibilityDate;
1438
+ } catch {
1439
+ return;
1440
+ }
1441
+ }
1442
+ function resolveCompatibilityDate(candidates, supported) {
1443
+ const date = candidates.find((d) => typeof d === "string" && d) ?? supported;
1444
+ if (date && supported && _isDateString(date) && _isDateString(supported) && date > supported) {
1445
+ console.warn(`[env-runner] compatibility date "${date}" is newer than the installed workerd supports; falling back to "${supported}".`);
1446
+ return supported;
1447
+ }
1448
+ return date;
1449
+ }
1450
+ function resolveCompatibilityFlags(wranglerFlags, userFlags) {
1451
+ const opposite = (flag) => flag === "nodejs_compat" ? "no_nodejs_compat" : flag === "no_nodejs_compat" ? "nodejs_compat" : void 0;
1452
+ const flags = [...wranglerFlags.filter((flag) => !userFlags.includes(opposite(flag))), ...userFlags];
1453
+ const defaults = flags.includes("no_nodejs_compat") ? [] : ["nodejs_compat"];
1454
+ return [.../* @__PURE__ */ new Set([...defaults, ...flags])];
1455
+ }
1456
+ function _isDateString(value) {
1457
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
1458
+ }
1459
+ function hasUserPersistOptions(options) {
1460
+ return Object.keys(options).some((key) => key === "defaultPersistRoot" || key === "resourcePersistencePath" || key.endsWith("Persist"));
1461
+ }
1462
+ function wranglerPersistRoot(configFile, explicitPath) {
1463
+ const file = configFile ?? (explicitPath ? resolve(explicitPath) : void 0);
1464
+ return join(file ? dirname(file) : process.cwd(), ".wrangler/state/v3");
1465
+ }
856
1466
  function _tryReadFile(path) {
857
1467
  try {
858
1468
  return readFileSync(path, "utf8");
@@ -868,26 +1478,93 @@ function computeCacheKey(entryPath, opts) {
868
1478
  for (const [k, v] of Object.entries(opts)) if (typeof v !== "function") serializableOpts[k] = v;
869
1479
  return `${resolve(entryPath)}::${JSON.stringify(serializableOpts)}`;
870
1480
  }
871
- function applyVirtualVersions(code, versions) {
872
- if (versions.size === 0) return code;
1481
+ const _isWin = process.platform === "win32";
1482
+ function virtualKeyPath(key) {
1483
+ if (_isWin && /^\/[A-Za-z]:\//.test(key)) key = key.slice(1);
1484
+ if (key.startsWith("file:")) try {
1485
+ return fileURLToPath(key);
1486
+ } catch {
1487
+ return;
1488
+ }
1489
+ return isAbsolute(key) ? resolve(key) : void 0;
1490
+ }
1491
+ function toWorkerdPath(path) {
1492
+ return _isWin && /^[A-Za-z]:[\\/]/.test(path) ? "/" + path.replaceAll("\\", "/") : path;
1493
+ }
1494
+ function createMiniflareVirtualModules(sources) {
1495
+ return refreshVirtualKeyResolvers({
1496
+ sources,
1497
+ removed: /* @__PURE__ */ new Set(),
1498
+ versions: /* @__PURE__ */ new Map(),
1499
+ version: 0
1500
+ });
1501
+ }
1502
+ function refreshVirtualKeyResolvers(virtual) {
1503
+ const keys = Object.keys(virtual.sources);
1504
+ virtual.keyOf = createVirtualKeyResolver(keys);
1505
+ virtual.versionedKeyOf = virtual.removed.size > 0 ? createVirtualKeyResolver([...keys, ...virtual.removed]) : virtual.keyOf;
1506
+ return virtual;
1507
+ }
1508
+ function createVirtualKeyResolver(keys) {
1509
+ const verbatim = new Set(keys);
1510
+ const pathKeys = /* @__PURE__ */ new Map();
1511
+ for (const key of verbatim) {
1512
+ const path = virtualKeyPath(key);
1513
+ if (path !== void 0) pathKeys.set(path, key);
1514
+ }
1515
+ return (specifier, importerPath) => {
1516
+ const clean = specifier.split("?")[0];
1517
+ if (verbatim.has(clean)) return clean;
1518
+ if (pathKeys.size === 0) return;
1519
+ const path = /^\.\.?\//.test(clean) ? importerPath && resolve(dirname(importerPath), clean) : virtualKeyPath(clean);
1520
+ return path ? pathKeys.get(path) : void 0;
1521
+ };
1522
+ }
1523
+ function virtualImporters(virtual, keyOf) {
1524
+ const importers = /* @__PURE__ */ new Map();
1525
+ for (const [importer, { source, format }] of Object.entries(virtual)) {
1526
+ if (format !== "module") continue;
1527
+ const importerPath = virtualKeyPath(importer);
1528
+ for (const { specifier } of versionableImports(source) ?? []) {
1529
+ const key = keyOf(specifier, importerPath);
1530
+ if (key !== void 0 && key !== importer) {
1531
+ let set = importers.get(key);
1532
+ if (!set) importers.set(key, set = /* @__PURE__ */ new Set());
1533
+ set.add(importer);
1534
+ }
1535
+ }
1536
+ }
1537
+ return importers;
1538
+ }
1539
+ function versionableImports(code) {
873
1540
  let imports;
874
1541
  try {
875
1542
  [imports] = parse$1(code);
876
1543
  } catch {
877
- return code;
1544
+ return;
878
1545
  }
1546
+ const result = [];
1547
+ for (const imp of imports) {
1548
+ if (typeof imp.specifier !== "string" || imp.type === "dynamic" && imp.glob) continue;
1549
+ result.push({
1550
+ specifier: imp.specifier,
1551
+ end: imp.type === "dynamic" ? imp.end - 1 : imp.end
1552
+ });
1553
+ }
1554
+ return result;
1555
+ }
1556
+ function applyVirtualVersions(code, versions, keyOf) {
1557
+ if (versions.size === 0) return code;
1558
+ const imports = versionableImports(code);
1559
+ if (!imports) return code;
879
1560
  let out = "";
880
1561
  let last = 0;
881
- for (const imp of imports) {
882
- if (imp.type === "import-meta") continue;
883
- const dynamic = imp.type === "dynamic";
884
- if (dynamic && imp.glob) continue;
885
- const specifier = imp.specifier;
886
- const version = specifier === void 0 ? void 0 : versions.get(specifier);
1562
+ for (const { specifier, end } of imports) {
1563
+ const key = keyOf(specifier);
1564
+ const version = key === void 0 ? void 0 : versions.get(key);
887
1565
  if (!version) continue;
888
- const versioned = `${specifier}?v=${version}`;
889
- out += code.slice(last, imp.start) + (dynamic ? JSON.stringify(versioned) : versioned);
890
- last = imp.end;
1566
+ out += code.slice(last, end) + (specifier.includes("?") ? "&" : "?") + `v=${version}`;
1567
+ last = end;
891
1568
  }
892
1569
  return out + code.slice(last);
893
1570
  }
@@ -901,12 +1578,15 @@ function ensureCjsLexer() {
901
1578
  if (!_cjsLexerReady) _cjsLexerReady = init();
902
1579
  return _cjsLexerReady;
903
1580
  }
904
- function createCjsEsmShim(cjsSpecifier, contents) {
905
- let namedExports = [];
1581
+ function commonJSExports(contents) {
906
1582
  try {
907
- const { exports } = parse(contents);
908
- namedExports = exports.filter((e) => e !== "default" && e !== "__esModule");
909
- } catch {}
1583
+ return parse(contents).exports;
1584
+ } catch {
1585
+ return [];
1586
+ }
1587
+ }
1588
+ function createCjsEsmShim(cjsSpecifier, contents) {
1589
+ const namedExports = commonJSExports(contents).filter((e) => e !== "default" && e !== "__esModule");
910
1590
  let shim = `import __cjs_mod__ from ${JSON.stringify(cjsSpecifier)};\nexport default __cjs_mod__;\n`;
911
1591
  for (const name of namedExports) shim += `export var ${name} = __cjs_mod__["${name}"];\n`;
912
1592
  return shim;