env-runner 0.2.2 → 0.2.3

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.
@@ -7,14 +7,17 @@ import { init as init$1, parse as parse$1 } from "./libs/es-module-lexer.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { proxyUpgrade } from "httpxy";
9
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";
10
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
11
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
12
  import { resolveModulePath } from "exsolve";
13
13
  import { tmpdir } from "node:os";
14
+ import { parseEnv } from "node:util";
14
15
  const IPC_PATH = "/__env_runner_ipc";
15
16
  const IPC_BINDING = "__ENV_RUNNER_IPC";
16
17
  const UNSAFE_EVAL_BINDING = "__ENV_RUNNER_UNSAFE_EVAL__";
17
18
  function generateWrapper(entryPath, opts) {
19
+ const processShim = opts?.nodeCompat === false ? "" : `import __process from "node:process";
20
+ if (!globalThis.process) { globalThis.process = __process; }`;
18
21
  const staticReExport = opts?.dynamicOnly ? "" : `export * from ${JSON.stringify(entryPath)};`;
19
22
  const explicitExports = opts?.dynamicOnly && opts.exports?.length ? opts.exports.map((name) => `export { ${name} } from ${JSON.stringify(entryPath)};`).join("\n") : "";
20
23
  const fetchBody = opts?.captureErrors ?? true ? `try {
@@ -31,8 +34,7 @@ function generateWrapper(entryPath, opts) {
31
34
  headers: { "Content-Type": "application/json", "X-Env-Runner-Error": "1" },
32
35
  });
33
36
  }` : `return __server.fetch(request, env, ctx);`;
34
- return `import __process from "node:process";
35
- if (!globalThis.process) { globalThis.process = __process; }
37
+ return `${processShim}
36
38
  ${staticReExport}
37
39
  ${explicitExports}
38
40
 
@@ -257,6 +259,67 @@ export default {
257
259
  };
258
260
  `;
259
261
  }
262
+ function expandDotenv(parsed, processEnv) {
263
+ const running = {};
264
+ 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, "$");
265
+ return processEnv;
266
+ }
267
+ function expandDotenvValue(value, processEnv, running) {
268
+ const env = {
269
+ ...running,
270
+ ...processEnv
271
+ };
272
+ const regex = /(?<!\\)\$\{([^{}]+)\}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g;
273
+ const seen = /* @__PURE__ */ new Set();
274
+ let result = value;
275
+ let match;
276
+ while ((match = regex.exec(result)) !== null) {
277
+ seen.add(result);
278
+ const [template, braced, unbraced] = match;
279
+ const expression = braced || unbraced;
280
+ const splitter = expression.match(/(:\+|\+|:-|-)/)?.[0];
281
+ const parts = splitter ? expression.split(splitter) : [expression];
282
+ const key = parts.shift();
283
+ let replacement;
284
+ if (splitter === ":+" || splitter === "+") replacement = env[key] ? parts.join(splitter) : "";
285
+ else {
286
+ const found = env[key];
287
+ replacement = found && !seen.has(found) ? found : parts.join(splitter ?? "");
288
+ }
289
+ result = result.replace(template, replacement);
290
+ if (result === running[key]) break;
291
+ regex.lastIndex = 0;
292
+ }
293
+ return result;
294
+ }
295
+ function readDotenvFile(path) {
296
+ return existsSync(path) ? parseEnv(readFileSync(path, "utf8")) : void 0;
297
+ }
298
+ function booleanEnv(name, defaultValue) {
299
+ const value = process.env[name]?.toLowerCase();
300
+ return value === "true" ? true : value === "false" ? false : defaultValue;
301
+ }
302
+ function loadDevVars(opts) {
303
+ const { configDir, env, envFiles } = opts;
304
+ if (!envFiles?.length) {
305
+ const devVars = resolve(configDir, ".dev.vars");
306
+ const loaded = env !== void 0 && readDotenvFile(`${devVars}.${env}`) || readDotenvFile(devVars);
307
+ if (loaded) return loaded;
308
+ }
309
+ if (!booleanEnv("CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV", true)) return;
310
+ const files = envFiles ?? [
311
+ ".env",
312
+ ".env.local",
313
+ ...env ? [`.env.${env}`, `.env.${env}.local`] : []
314
+ ];
315
+ const parsed = {};
316
+ for (const file of files) Object.assign(parsed, readDotenvFile(resolve(configDir, file)));
317
+ const processEnv = {};
318
+ if (opts.hasSecrets || booleanEnv("CLOUDFLARE_INCLUDE_PROCESS_ENV", false)) {
319
+ for (const [key, value] of Object.entries(process.env)) if (typeof value === "string") processEnv[key] = value;
320
+ }
321
+ return expandDotenv(parsed, processEnv);
322
+ }
260
323
  const WRANGLER_CONFIG_FILENAMES = [
261
324
  "wrangler.json",
262
325
  "wrangler.jsonc",
@@ -274,45 +337,143 @@ const WRANGLER_OPTION_DENYLIST = /* @__PURE__ */ new Set([
274
337
  "unsafeModuleFallbackService",
275
338
  "unsafeUseModuleFallbackService"
276
339
  ]);
340
+ const WRANGLER_OPTION_DROPLIST = /* @__PURE__ */ new Set([
341
+ "assets",
342
+ "serviceBindings",
343
+ "workflows",
344
+ "queueConsumers",
345
+ "tails",
346
+ "streamingTails"
347
+ ]);
348
+ const WRANGLER_DROPPED_OPTION_NAMES = {
349
+ assets: "assets",
350
+ serviceBindings: "services",
351
+ workflows: "workflows",
352
+ queueConsumers: "queues.consumers",
353
+ tails: "tail_consumers",
354
+ streamingTails: "streaming_tail_consumers",
355
+ durableObjects: "durable_objects"
356
+ };
357
+ function addDropped(dropped, name, details) {
358
+ let set = dropped.get(name);
359
+ if (!set) {
360
+ set = /* @__PURE__ */ new Set();
361
+ dropped.set(name, set);
362
+ }
363
+ for (const detail of details) set.add(detail);
364
+ }
365
+ function warnDroppedWranglerOptions(dropped) {
366
+ if (dropped.size === 0) return;
367
+ const list = [...dropped].map(([name, details]) => details.size > 0 ? `${name} (${[...details].join(", ")})` : name).join(", ");
368
+ console.warn(`[env-runner] wrangler config options not supported by the miniflare dev runner were ignored: ${list}; pass them via miniflareOptions to opt in.`);
369
+ }
277
370
  function isPlainObject(value) {
278
371
  return typeof value === "object" && value !== null && !Array.isArray(value);
279
372
  }
280
373
  function isInlineWranglerConfig(opt) {
281
374
  return isPlainObject(opt);
282
375
  }
283
- async function loadWranglerConfig(opt, env, entryPath, wranglerModule) {
284
- if (!opt) return;
376
+ async function loadWranglerConfig(opts) {
377
+ const { wrangler: opt, env, entryPath, wranglerModule, envFiles } = opts;
378
+ if (!opt) return {};
285
379
  const inline = isInlineWranglerConfig(opt) ? opt : void 0;
286
380
  let configPath;
287
- if (typeof opt === "string") {
288
- configPath = resolve(opt);
381
+ const explicitPath = typeof opt === "string" ? opt : opts.configPath;
382
+ if (explicitPath) {
383
+ configPath = resolve(explicitPath);
289
384
  if (!existsSync(configPath)) {
290
385
  console.warn(`[env-runner] wrangler config requested but not found at "${configPath}"`);
291
- return;
386
+ if (!inline) return {};
387
+ configPath = void 0;
292
388
  }
293
- } else if (opt === true) {
389
+ } else {
294
390
  configPath = findWranglerConfig(entryPath);
295
- if (!configPath) {
296
- console.warn("[env-runner] wrangler config requested but none found near the entry or cwd");
297
- return;
391
+ if (!configPath && !inline) {
392
+ console.warn("[env-runner] wrangler config requested but none found (searched the entry's directory, then from the cwd up to the filesystem root)");
393
+ return {};
298
394
  }
299
- } else configPath = findWranglerConfig(entryPath);
395
+ }
300
396
  const wrangler = await resolveRuntimeDep({
301
397
  name: "wrangler",
302
398
  option: "wranglerModule",
303
399
  value: wranglerModule
304
400
  });
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({
401
+ const dropped = /* @__PURE__ */ new Map();
402
+ if (!wrangler?.unstable_readConfig || !wrangler.unstable_getMiniflareWorkerOptions) {
403
+ const unsupported = /* @__PURE__ */ new Map();
404
+ const file = configPath ? readWranglerConfigMinimal(configPath, env) : void 0;
405
+ const inlineConfig = inline ? applyWranglerEnv(inline, env) : void 0;
406
+ let options = mergeWranglerMiniflareOptions(file && (mapWranglerConfigToMiniflare(file.config, dropped, unsupported) ?? {}), inlineConfig && mapWranglerConfigToMiniflare(inlineConfig, dropped, unsupported));
407
+ if (file || inlineConfig) options = applyMinimalDevVars(options, [file?.config, inlineConfig], {
408
+ configDir: configPath ? dirname(configPath) : process.cwd(),
409
+ env,
410
+ envFiles
411
+ });
412
+ filterLocalDurableObjects(options, dropped, (inline && wranglerWorkerName(inline, env)) ?? file?.workerName);
413
+ warnDroppedWranglerOptions(dropped);
414
+ warnUnsupportedMinimalBindings(unsupported);
415
+ return {
416
+ options,
417
+ configFile: file ? configPath : void 0
418
+ };
419
+ }
420
+ let fileLoaded = false;
421
+ let fileOptions;
422
+ let fileSecrets;
423
+ let fileWorkerName;
424
+ if (configPath) try {
425
+ const config = wrangler.unstable_readConfig({
308
426
  config: configPath,
309
427
  env
310
- }, { hideWarnings: true }), env).workerOptions) : void 0, inline ? pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(readInlineWranglerConfig(wrangler, inline, env), env).workerOptions) : void 0);
428
+ }, { hideWarnings: !claimWranglerWarnings(configPath, env) });
429
+ fileOptions = pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(config, env, { envFiles }).workerOptions, dropped);
430
+ fileWorkerName = config?.name;
431
+ fileSecrets = config?.secrets;
432
+ fileLoaded = true;
311
433
  } 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;
434
+ warnWranglerLoadError(`"${configPath}"`, error);
435
+ }
436
+ let inlineOptions;
437
+ if (inline) try {
438
+ const inlineEnv = env && isPlainObject(inline.env) && inline.env[env] ? env : void 0;
439
+ const { env: _env, ...inlineTopLevel } = inline;
440
+ const inlineConfig = readInlineWranglerConfig(wrangler, inlineEnv ? inline : inlineTopLevel, inlineEnv);
441
+ inlineConfig.userConfigPath = configPath;
442
+ if (fileLoaded && inlineConfig.secrets === void 0) inlineConfig.secrets = fileSecrets;
443
+ inlineOptions = pickWranglerMiniflareOptions(wrangler.unstable_getMiniflareWorkerOptions(inlineConfig, env, { envFiles }).workerOptions, dropped);
444
+ if (inlineOptions) {
445
+ delete inlineOptions.rootPath;
446
+ if (fileLoaded) filterInlineDevVarBindings(inlineOptions, fileOptions, inlineConfig);
447
+ }
448
+ } catch (error) {
449
+ warnWranglerLoadError("(inline)", error);
315
450
  }
451
+ const options = mergeWranglerMiniflareOptions(fileOptions, inlineOptions);
452
+ filterLocalDurableObjects(options, dropped, (inline && wranglerWorkerName(inline, env)) ?? fileWorkerName);
453
+ warnDroppedWranglerOptions(dropped);
454
+ return {
455
+ options,
456
+ configFile: fileLoaded ? configPath : void 0
457
+ };
458
+ }
459
+ const _shownWranglerWarnings = /* @__PURE__ */ new Set();
460
+ function claimWranglerWarnings(configPath, env) {
461
+ let version = "";
462
+ try {
463
+ const stat = statSync(configPath);
464
+ version = `${stat.mtimeMs}:${stat.size}`;
465
+ } catch {}
466
+ const key = JSON.stringify([
467
+ resolve(configPath),
468
+ env ?? null,
469
+ version
470
+ ]);
471
+ if (_shownWranglerWarnings.has(key)) return false;
472
+ _shownWranglerWarnings.add(key);
473
+ return true;
474
+ }
475
+ function warnWranglerLoadError(desc, error) {
476
+ console.warn(`[env-runner] failed to load wrangler config ${desc}: ${error.message}`);
316
477
  }
317
478
  function mergeWranglerMiniflareOptions(base, override) {
318
479
  if (!base) return override;
@@ -329,6 +490,12 @@ function mergeWranglerMiniflareOptions(base, override) {
329
490
  }
330
491
  return out;
331
492
  }
493
+ function filterInlineDevVarBindings(inlineOptions, fileOptions, inlineConfig) {
494
+ if (!isPlainObject(inlineOptions.bindings)) return;
495
+ const fileBindings = isPlainObject(fileOptions?.bindings) ? fileOptions.bindings : {};
496
+ const declared = /* @__PURE__ */ new Set([...Object.keys(inlineConfig.vars ?? {}), ...inlineConfig.secrets?.required ?? []]);
497
+ inlineOptions.bindings = Object.fromEntries(Object.entries(inlineOptions.bindings).filter(([key]) => declared.has(key) || Object.hasOwn(fileBindings, key)));
498
+ }
332
499
  function readInlineWranglerConfig(wrangler, inline, env) {
333
500
  const dir = mkdtempSync(join(tmpdir(), "env-runner-wrangler-"));
334
501
  const file = join(dir, "wrangler.json");
@@ -345,29 +512,173 @@ function readInlineWranglerConfig(wrangler, inline, env) {
345
512
  });
346
513
  }
347
514
  }
515
+ const _announcedWranglerConfigs = /* @__PURE__ */ new Set();
348
516
  function findWranglerConfig(entryPath) {
349
- const dirs = [];
517
+ const cwd = process.cwd();
518
+ let start = cwd;
350
519
  if (entryPath) {
351
- const resolved = isAbsolute(entryPath) ? entryPath : resolve(entryPath);
352
- dirs.push(dirname(resolved));
520
+ const entryDir = dirname(resolve(entryPath));
521
+ const rel = relative(cwd, entryDir);
522
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) start = entryDir;
523
+ else {
524
+ const local = findWranglerConfigInDir(entryDir);
525
+ if (local) return local;
526
+ }
353
527
  }
354
- dirs.push(process.cwd());
355
- for (const dir of dirs) for (const name of WRANGLER_CONFIG_FILENAMES) {
528
+ for (let dir = start;; dir = dirname(dir)) {
529
+ const found = findWranglerConfigInDir(dir);
530
+ if (found) {
531
+ if (dir !== cwd && dir !== start && !_announcedWranglerConfigs.has(found)) {
532
+ _announcedWranglerConfigs.add(found);
533
+ console.info(`[env-runner] using wrangler config from a parent directory: ${found}`);
534
+ }
535
+ return found;
536
+ }
537
+ if (dirname(dir) === dir) return;
538
+ }
539
+ }
540
+ function findWranglerConfigInDir(dir) {
541
+ for (const name of WRANGLER_CONFIG_FILENAMES) {
356
542
  const candidate = join(dir, name);
357
543
  if (existsSync(candidate)) return candidate;
358
544
  }
359
545
  }
360
- function pickWranglerMiniflareOptions(workerOptions) {
546
+ function pickWranglerMiniflareOptions(workerOptions, dropped) {
361
547
  const out = {};
362
548
  for (const [key, value] of Object.entries(workerOptions)) {
363
549
  if (value === void 0 || WRANGLER_OPTION_DENYLIST.has(key)) continue;
550
+ if (WRANGLER_OPTION_DROPLIST.has(key)) {
551
+ if (!isEmptyOption(value)) addDropped(dropped, WRANGLER_DROPPED_OPTION_NAMES[key], describeDroppedOption(value));
552
+ continue;
553
+ }
554
+ if (isEmptyPickedOption(key, value)) continue;
364
555
  out[key] = value;
365
556
  }
366
557
  return Object.keys(out).length > 0 ? out : void 0;
367
558
  }
559
+ function isEmptyPickedOption(key, value) {
560
+ const isEmptyArray = (v) => Array.isArray(v) && v.length === 0;
561
+ if (isEmptyArray(value) || isPlainObject(value) && Object.keys(value).length === 0) return true;
562
+ return key !== "bindings" && isPlainObject(value) && Object.values(value).every(isEmptyArray);
563
+ }
564
+ function isEmptyOption(value) {
565
+ return !value || Array.isArray(value) && value.length === 0 || isPlainObject(value) && Object.keys(value).length === 0;
566
+ }
567
+ function describeDroppedOption(value) {
568
+ if (Array.isArray(value)) return value.map((entry) => entry?.name).filter((name) => typeof name === "string");
569
+ if (isPlainObject(value)) {
570
+ if ("directory" in value) return typeof value.binding === "string" ? [value.binding] : [];
571
+ return Object.keys(value);
572
+ }
573
+ return [];
574
+ }
575
+ function filterLocalDurableObjects(options, dropped, workerName) {
576
+ const value = options?.durableObjects;
577
+ if (!options || !isPlainObject(value)) return;
578
+ const out = {};
579
+ for (const [name, binding] of Object.entries(value)) if (typeof binding === "string" || isPlainObject(binding) && !binding.scriptName) out[name] = binding;
580
+ else if (isPlainObject(binding) && workerName && binding.scriptName === workerName) {
581
+ const { scriptName: _scriptName, ...local } = binding;
582
+ out[name] = local;
583
+ } else {
584
+ const scriptName = isPlainObject(binding) ? binding.scriptName : void 0;
585
+ addDropped(dropped, "durable_objects", [`${name} → script "${String(scriptName)}"`]);
586
+ }
587
+ if (Object.keys(out).length > 0) options.durableObjects = out;
588
+ else delete options.durableObjects;
589
+ }
590
+ const WRANGLER_NON_INHERITABLE_KEYS = /* @__PURE__ */ new Set([
591
+ "vars",
592
+ "secrets",
593
+ "define",
594
+ "durable_objects",
595
+ "workflows",
596
+ "kv_namespaces",
597
+ "cloudchamber",
598
+ "containers",
599
+ "send_email",
600
+ "queues",
601
+ "connect",
602
+ "r2_buckets",
603
+ "d1_databases",
604
+ "vectorize",
605
+ "ai_search_namespaces",
606
+ "ai_search",
607
+ "websearch",
608
+ "agent_memory",
609
+ "hyperdrive",
610
+ "services",
611
+ "analytics_engine_datasets",
612
+ "dispatch_namespaces",
613
+ "mtls_certificates",
614
+ "tail_consumers",
615
+ "streaming_tail_consumers",
616
+ "unsafe",
617
+ "browser",
618
+ "ai",
619
+ "images",
620
+ "stream",
621
+ "media",
622
+ "pipelines",
623
+ "secrets_store_secrets",
624
+ "artifacts",
625
+ "unsafe_hello_world",
626
+ "flagship",
627
+ "worker_loaders",
628
+ "ratelimits",
629
+ "vpc_services",
630
+ "vpc_networks",
631
+ "version_metadata"
632
+ ]);
633
+ const MINIMAL_UNSUPPORTED_BINDING_KEYS = [
634
+ "hyperdrive",
635
+ "analytics_engine_datasets",
636
+ "ai",
637
+ "ai_search_namespaces",
638
+ "ai_search",
639
+ "websearch",
640
+ "agent_memory",
641
+ "version_metadata",
642
+ "ratelimits",
643
+ "send_email",
644
+ "secrets_store_secrets",
645
+ "vectorize",
646
+ "browser",
647
+ "images",
648
+ "stream",
649
+ "media",
650
+ "pipelines",
651
+ "dispatch_namespaces",
652
+ "mtls_certificates",
653
+ "worker_loaders",
654
+ "vpc_services",
655
+ "vpc_networks",
656
+ "artifacts",
657
+ "flagship",
658
+ "containers",
659
+ "unsafe",
660
+ "unsafe_hello_world",
661
+ "logfwdr",
662
+ "wasm_modules",
663
+ "text_blobs",
664
+ "data_blobs"
665
+ ];
666
+ const MINIMAL_BINDING_OPTIONS = [
667
+ "kvNamespaces",
668
+ "r2Buckets",
669
+ "d1Databases",
670
+ "queueProducers",
671
+ "durableObjects"
672
+ ];
673
+ function warnUnsupportedMinimalBindings(unsupported) {
674
+ if (unsupported.size === 0) return;
675
+ const list = [...unsupported].map(([name, details]) => details.size > 0 ? `${name} (${[...details].join(", ")})` : name).join(", ");
676
+ 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.`);
677
+ }
368
678
  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).`);
679
+ const ext = extname(configPath).toLowerCase();
680
+ if (ext !== ".json" && ext !== ".jsonc") {
681
+ console.warn(`[env-runner] reading "${basename(configPath)}" requires the 'wrangler' package; the built-in reader supports JSON/JSONC only (install 'wrangler' for TOML).`);
371
682
  return;
372
683
  }
373
684
  let raw;
@@ -378,45 +689,203 @@ function readWranglerConfigMinimal(configPath, env) {
378
689
  }
379
690
  let config;
380
691
  try {
381
- config = JSON.parse(raw);
692
+ config = parseJSONC(raw);
382
693
  } catch (error) {
383
694
  console.warn(`[env-runner] failed to parse wrangler config "${configPath}": ${error.message}`);
384
695
  return;
385
696
  }
386
- return mapWranglerConfigToMiniflare(applyWranglerEnv(config, env));
697
+ if (!isPlainObject(config)) {
698
+ warnWranglerLoadError(`"${configPath}"`, /* @__PURE__ */ new Error("config must be an object"));
699
+ return;
700
+ }
701
+ let envConfig;
702
+ try {
703
+ envConfig = applyWranglerEnv(config, env, configPath);
704
+ } catch (error) {
705
+ warnWranglerLoadError(`"${configPath}"`, error);
706
+ return;
707
+ }
708
+ return {
709
+ config: envConfig,
710
+ workerName: wranglerWorkerName(config, env)
711
+ };
387
712
  }
388
- function applyWranglerEnv(config, env) {
389
- return env && config.env?.[env] ? {
390
- ...config,
391
- ...config.env[env]
392
- } : config;
713
+ function applyWranglerEnv(config, env, configPath) {
714
+ const { env: envs, ...topLevel } = config;
715
+ if (!env) return topLevel;
716
+ const rawEnv = isPlainObject(envs) ? envs[env] : void 0;
717
+ const warnings = [];
718
+ let out;
719
+ if (isPlainObject(rawEnv)) {
720
+ out = {};
721
+ for (const [key, value] of Object.entries(topLevel)) if (!WRANGLER_NON_INHERITABLE_KEYS.has(key)) out[key] = value;
722
+ else if (rawEnv[key] === void 0) warnings.push(`"${key}" exists at the top level, but is not inherited by "env.${env}"`);
723
+ Object.assign(out, rawEnv);
724
+ } else {
725
+ 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))}`);
726
+ warnings.push(`No environment found in configuration with name "${env}"`);
727
+ out = topLevel;
728
+ }
729
+ if (configPath && warnings.length > 0 && claimWranglerWarnings(configPath, env)) console.warn(`[env-runner] wrangler config "${configPath}": ${warnings.join("; ")}.`);
730
+ return out;
731
+ }
732
+ function wranglerWorkerName(config, env) {
733
+ const envName = env ? config.env?.[env]?.name : void 0;
734
+ if (typeof envName === "string") return envName;
735
+ if (typeof config.name !== "string" || !config.name) return void 0;
736
+ return env ? `${config.name}-${env}` : config.name;
393
737
  }
394
- function mapWranglerConfigToMiniflare(config) {
738
+ function mapWranglerConfigToMiniflare(config, dropped, unsupported) {
395
739
  const out = {};
740
+ for (const [name, value, field] of [
741
+ [
742
+ "services",
743
+ config.services,
744
+ "binding"
745
+ ],
746
+ [
747
+ "assets",
748
+ config.assets,
749
+ "binding"
750
+ ],
751
+ [
752
+ "queues.consumers",
753
+ config.queues?.consumers,
754
+ "queue"
755
+ ],
756
+ [
757
+ "workflows",
758
+ config.workflows,
759
+ "binding"
760
+ ],
761
+ [
762
+ "tail_consumers",
763
+ config.tail_consumers,
764
+ "service"
765
+ ],
766
+ [
767
+ "streaming_tail_consumers",
768
+ config.streaming_tail_consumers,
769
+ "service"
770
+ ]
771
+ ]) if (!isEmptyOption(value)) addDropped(dropped, name, (Array.isArray(value) ? value : [value]).map((e) => e?.[field]).filter((d) => typeof d === "string"));
772
+ for (const key of MINIMAL_UNSUPPORTED_BINDING_KEYS) {
773
+ const value = config[key];
774
+ const bindings = isPlainObject(value) && Array.isArray(value.bindings) ? value.bindings : value;
775
+ if (!isEmptyOption(bindings)) addDropped(unsupported, key, describeBindingNames(bindings));
776
+ }
396
777
  if (typeof config.compatibility_date === "string") out.compatibilityDate = config.compatibility_date;
397
778
  if (Array.isArray(config.compatibility_flags)) out.compatibilityFlags = config.compatibility_flags;
398
779
  if (config.vars && typeof config.vars === "object") out.bindings = { ...config.vars };
399
- const kv = mapBindingArray(config.kv_namespaces, "binding", (n) => n.id ?? n.binding);
780
+ const kv = mapBindingArray(config.kv_namespaces, "binding", (n) => firstString(n.preview_id, n.id, n.binding));
400
781
  if (kv) out.kvNamespaces = kv;
401
- const r2 = mapBindingArray(config.r2_buckets, "binding", (n) => n.bucket_name ?? n.binding);
782
+ const r2 = mapBindingArray(config.r2_buckets, "binding", (n) => firstString(n.preview_bucket_name, n.bucket_name, n.binding));
402
783
  if (r2) out.r2Buckets = r2;
403
- const d1 = mapBindingArray(config.d1_databases, "binding", (n) => n.database_id ?? n.preview_database_id ?? n.binding);
784
+ const d1 = mapBindingArray(config.d1_databases, "binding", (n) => firstString(n.preview_database_id, n.database_id, n.binding));
404
785
  if (d1) out.d1Databases = d1;
405
786
  const queues = mapBindingArray(config.queues?.producers, "binding", (n) => n.queue);
406
787
  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;
788
+ const useSQLite = durableObjectClassStorage(config);
789
+ const doBindings = Array.isArray(config.durable_objects?.bindings) ? config.durable_objects.bindings : [];
790
+ const dos = {};
791
+ for (const b of doBindings) {
792
+ if (!b?.name || !b?.class_name) continue;
793
+ dos[b.name] = {
794
+ className: b.class_name,
795
+ ...b.script_name ? { scriptName: b.script_name } : {},
796
+ ...useSQLite.has(b.class_name) ? { useSQLite: useSQLite.get(b.class_name) } : {}
797
+ };
417
798
  }
799
+ if (Object.keys(dos).length > 0) out.durableObjects = dos;
800
+ const unbound = [...useSQLite].filter(([className]) => !doBindings.some((b) => b?.class_name === className)).map(([className, sqlite]) => ({
801
+ className,
802
+ useSQLite: sqlite
803
+ }));
804
+ if (unbound.length > 0) out.additionalUnboundDurableObjects = unbound;
418
805
  return Object.keys(out).length > 0 ? out : void 0;
419
806
  }
807
+ function durableObjectClassStorage(config) {
808
+ const classes = /* @__PURE__ */ new Map();
809
+ for (const migration of Array.isArray(config.migrations) ? config.migrations : []) {
810
+ for (const name of migration?.deleted_classes ?? []) classes.delete(name);
811
+ for (const { from, to } of migration?.renamed_classes ?? []) {
812
+ const sqlite = classes.get(from);
813
+ if (sqlite !== void 0) {
814
+ classes.delete(from);
815
+ classes.set(to, sqlite);
816
+ }
817
+ }
818
+ for (const name of migration?.new_classes ?? []) classes.set(name, false);
819
+ for (const name of migration?.new_sqlite_classes ?? []) classes.set(name, true);
820
+ }
821
+ if (isPlainObject(config.exports)) for (const [name, entry] of Object.entries(config.exports)) {
822
+ const state = entry?.state;
823
+ if (entry?.type === "durable-object" && (state === void 0 || state === "created" || state === "expecting-transfer")) classes.set(name, entry.storage === "sqlite");
824
+ }
825
+ return classes;
826
+ }
827
+ function describeBindingNames(value) {
828
+ if (isPlainObject(value) && typeof value.binding !== "string") return Object.keys(value);
829
+ return (Array.isArray(value) ? value : [value]).map((e) => e?.binding ?? e?.name ?? e?.class_name).filter((name) => typeof name === "string");
830
+ }
831
+ function applyMinimalDevVars(options, configs, opts) {
832
+ const secrets = configs.map((c) => c?.secrets).filter(isPlainObject);
833
+ const loaded = loadDevVars({
834
+ ...opts,
835
+ hasSecrets: secrets.length > 0
836
+ });
837
+ if (!loaded) return options;
838
+ const out = { ...options };
839
+ const bindings = isPlainObject(out.bindings) ? { ...out.bindings } : {};
840
+ const taken = new Set(MINIMAL_BINDING_OPTIONS.flatMap((key) => isPlainObject(out[key]) ? Object.keys(out[key]) : []));
841
+ const required = new Set(secrets.flatMap((s) => Array.isArray(s.required) ? s.required : []));
842
+ let changed = false;
843
+ for (const [key, value] of Object.entries(loaded)) {
844
+ if (taken.has(key) || secrets.length > 0 && !Object.hasOwn(bindings, key) && !required.has(key)) continue;
845
+ bindings[key] = value;
846
+ changed = true;
847
+ }
848
+ const missing = [...required].filter((key) => !Object.hasOwn(loaded, key));
849
+ 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.`);
850
+ if (!changed) return options;
851
+ out.bindings = bindings;
852
+ return out;
853
+ }
854
+ function firstString(...values) {
855
+ return values.find((value) => typeof value === "string");
856
+ }
857
+ function parseJSONC(text) {
858
+ let out = "";
859
+ let comma = -1;
860
+ const src = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
861
+ for (let i = 0; i < src.length; i++) {
862
+ const ch = src[i];
863
+ if (ch === "/" && (src[i + 1] === "/" || src[i + 1] === "*")) {
864
+ const line = src[i + 1] === "/";
865
+ let end = line ? src.indexOf("\n", i) : src.indexOf("*/", i + 2);
866
+ end = end === -1 ? src.length : line ? end : end + 2;
867
+ out += src.slice(i, end).replace(/[^\n]/g, " ");
868
+ i = end - 1;
869
+ continue;
870
+ }
871
+ if (/\s/.test(ch)) {
872
+ out += ch;
873
+ continue;
874
+ }
875
+ if ((ch === "}" || ch === "]") && comma !== -1) out = `${out.slice(0, comma)} ${out.slice(comma + 1)}`;
876
+ comma = -1;
877
+ if (ch === "\"") {
878
+ let end = i + 1;
879
+ while (end < src.length && src[end] !== "\"") end += src[end] === "\\" ? 2 : 1;
880
+ out += src.slice(i, end + 1);
881
+ i = end;
882
+ continue;
883
+ }
884
+ if (ch === ",") comma = out.length;
885
+ out += ch;
886
+ }
887
+ return JSON.parse(out);
888
+ }
420
889
  function mapBindingArray(arr, keyField, value) {
421
890
  if (!Array.isArray(arr)) return;
422
891
  const out = {};
@@ -443,6 +912,9 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
443
912
  #exportConditions;
444
913
  #wrangler;
445
914
  #wranglerEnv;
915
+ #wranglerConfigPath;
916
+ #wranglerEnvFiles;
917
+ #compatibilityDate;
446
918
  #wranglerModule;
447
919
  #miniflareModule;
448
920
  constructor(opts) {
@@ -460,6 +932,9 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
460
932
  this.#wrangler = opts.wrangler ?? false;
461
933
  this.#wranglerEnv = opts.wranglerEnv ?? process.env.CLOUDFLARE_ENV;
462
934
  this.#wranglerModule = opts.wranglerModule;
935
+ this.#wranglerConfigPath = opts.wranglerConfigPath;
936
+ this.#wranglerEnvFiles = opts.wranglerEnvFiles;
937
+ this.#compatibilityDate = opts.compatibilityDate;
463
938
  this._initWithVirtualData(() => this.#init());
464
939
  }
465
940
  static async disposeAll() {
@@ -609,20 +1084,28 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
609
1084
  const entryPath = this._data?.entry;
610
1085
  const virtual = await this.#prepareVirtualModules();
611
1086
  this.#virtual = virtual;
612
- const wranglerOptions = await loadWranglerConfig(this.#wrangler, this.#wranglerEnv, entryPath, this.#wranglerModule);
1087
+ const { options: wranglerOptions, configFile: wranglerConfigFile } = await loadWranglerConfig({
1088
+ wrangler: this.#wrangler,
1089
+ env: this.#wranglerEnv,
1090
+ entryPath,
1091
+ configPath: this.#wranglerConfigPath,
1092
+ wranglerModule: this.#wranglerModule,
1093
+ envFiles: this.#wranglerEnvFiles
1094
+ });
613
1095
  const userFlags = this.#miniflareOptions.compatibilityFlags || [];
614
1096
  const wranglerFlags = wranglerOptions?.compatibilityFlags || [];
615
1097
  const userDirectSockets = this.#miniflareOptions.unsafeDirectSockets || [];
616
1098
  const options = {
617
- compatibilityDate: supportedCompatibilityDate,
618
1099
  modules: true,
1100
+ ...this.#wrangler && !hasUserPersistOptions(this.#miniflareOptions) ? { defaultPersistRoot: wranglerPersistRoot(wranglerConfigFile, typeof this.#wrangler === "string" ? this.#wrangler : this.#wranglerConfigPath) } : void 0,
619
1101
  ...wranglerOptions,
620
1102
  ...this.#miniflareOptions,
621
- compatibilityFlags: [.../* @__PURE__ */ new Set([
622
- "nodejs_compat",
623
- ...wranglerFlags,
624
- ...userFlags
625
- ])],
1103
+ compatibilityDate: resolveCompatibilityDate([
1104
+ this.#miniflareOptions.compatibilityDate,
1105
+ this.#compatibilityDate === "latest" ? supportedCompatibilityDate : this.#compatibilityDate,
1106
+ wranglerOptions?.compatibilityDate
1107
+ ], supportedCompatibilityDate),
1108
+ compatibilityFlags: resolveCompatibilityFlags(wranglerFlags, userFlags),
626
1109
  unsafeDirectSockets: [{
627
1110
  host: "127.0.0.1",
628
1111
  port: 0
@@ -643,18 +1126,21 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
643
1126
  const entrySource = entryIsVirtual ? virtual[entryPath] : _tryReadFile(resolvedEntry);
644
1127
  const detectedExports = this.#exports === false || this.#exports === void 0 ? [] : detectExportedClasses(entrySource, typeof this.#exports === "object" ? this.#exports : {});
645
1128
  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 || {} };
1129
+ if (detectedExports.length > 0) {
1130
+ const existingDOs = isPlainObject(options.durableObjects) ? options.durableObjects : {};
1131
+ const boundClasses = new Set(Object.values(existingDOs).map((b) => typeof b === "string" ? b : isPlainObject(b) && !b.scriptName ? b.className : void 0));
1132
+ const autoDOs = { ...existingDOs };
648
1133
  for (const name of detectedExports) {
649
1134
  const bindingName = toScreamingSnakeCase(name);
650
- if (!autoDOs[bindingName]) autoDOs[bindingName] = name;
1135
+ if (!autoDOs[bindingName] && !boundClasses.has(name)) autoDOs[bindingName] = name;
651
1136
  }
652
1137
  options.durableObjects = autoDOs;
653
1138
  }
654
1139
  options.script = generateWrapper(resolvedEntry, {
655
1140
  dynamicOnly: true,
656
1141
  captureErrors: this.#captureErrors,
657
- exports: detectedExports
1142
+ exports: detectedExports,
1143
+ nodeCompat: !options.compatibilityFlags.includes("no_nodejs_compat")
658
1144
  });
659
1145
  options.scriptPath = entryDir + "/__env_runner_wrapper.mjs";
660
1146
  if (!options.modulesRoot) options.modulesRoot = "/";
@@ -853,6 +1339,30 @@ function detectExportedClasses(entrySource, explicit) {
853
1339
  }
854
1340
  return [...names];
855
1341
  }
1342
+ function resolveCompatibilityDate(candidates, supported) {
1343
+ const date = candidates.find((d) => typeof d === "string" && d) ?? supported;
1344
+ if (date && supported && _isDateString(date) && _isDateString(supported) && date > supported) {
1345
+ console.warn(`[env-runner] compatibility date "${date}" is newer than the installed workerd supports; falling back to "${supported}".`);
1346
+ return supported;
1347
+ }
1348
+ return date;
1349
+ }
1350
+ function resolveCompatibilityFlags(wranglerFlags, userFlags) {
1351
+ const opposite = (flag) => flag === "nodejs_compat" ? "no_nodejs_compat" : flag === "no_nodejs_compat" ? "nodejs_compat" : void 0;
1352
+ const flags = [...wranglerFlags.filter((flag) => !userFlags.includes(opposite(flag))), ...userFlags];
1353
+ const defaults = flags.includes("no_nodejs_compat") ? [] : ["nodejs_compat"];
1354
+ return [.../* @__PURE__ */ new Set([...defaults, ...flags])];
1355
+ }
1356
+ function _isDateString(value) {
1357
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
1358
+ }
1359
+ function hasUserPersistOptions(options) {
1360
+ return Object.keys(options).some((key) => key === "defaultPersistRoot" || key.endsWith("Persist"));
1361
+ }
1362
+ function wranglerPersistRoot(configFile, explicitPath) {
1363
+ const file = configFile ?? (explicitPath ? resolve(explicitPath) : void 0);
1364
+ return join(file ? dirname(file) : process.cwd(), ".wrangler/state/v3");
1365
+ }
856
1366
  function _tryReadFile(path) {
857
1367
  try {
858
1368
  return readFileSync(path, "utf8");