@cloudflare/vite-plugin 1.36.4 → 1.37.1

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.
package/dist/index.mjs CHANGED
@@ -1503,7 +1503,7 @@ async function assertWranglerVersion() {
1503
1503
  * The default compatibility date to use when the user omits one.
1504
1504
  * This value is injected at build time and remains fixed for each release.
1505
1505
  */
1506
- const DEFAULT_COMPAT_DATE = "2026-05-12";
1506
+ const DEFAULT_COMPAT_DATE = "2026-05-15";
1507
1507
 
1508
1508
  //#endregion
1509
1509
  //#region ../../node_modules/.pnpm/@remix-run+node-fetch-server@0.8.0/node_modules/@remix-run/node-fetch-server/dist/node-fetch-server.js
@@ -1997,7 +1997,7 @@ var MetricsRegistry = class {
1997
1997
  };
1998
1998
 
1999
1999
  //#endregion
2000
- //#region ../workers-utils/dist/chunk-XXCQEG76.mjs
2000
+ //#region ../workers-utils/dist/chunk-GMTGAG26.mjs
2001
2001
  var UserError = class extends Error {
2002
2002
  static {
2003
2003
  __name(this, "UserError");
@@ -4394,8 +4394,15 @@ function findRedirectedWranglerConfig(cwd, userConfigPath) {
4394
4394
  };
4395
4395
  }
4396
4396
  __name(findRedirectedWranglerConfig, "findRedirectedWranglerConfig");
4397
+ function isRedirectedConfig(config) {
4398
+ return config.configPath !== void 0 && config.configPath !== config.userConfigPath;
4399
+ }
4400
+ __name(isRedirectedConfig, "isRedirectedConfig");
4397
4401
  function isRedirectedRawConfig(rawConfig, configPath, userConfigPath) {
4398
- return configPath !== void 0 && configPath !== userConfigPath;
4402
+ return isRedirectedConfig({
4403
+ configPath,
4404
+ userConfigPath
4405
+ });
4399
4406
  }
4400
4407
  __name(isRedirectedRawConfig, "isRedirectedRawConfig");
4401
4408
  function configFormat(configPath) {
@@ -30887,9 +30894,9 @@ function normalizeAndValidateConfig(rawConfig, configPath, userConfigPath, args,
30887
30894
  if (useServiceEnvironments) diagnostics.warnings.push("Service environments are deprecated, and will be removed in the future. DO NOT USE IN PRODUCTION.");
30888
30895
  const isDispatchNamespace = typeof args["dispatch-namespace"] === "string" && args["dispatch-namespace"].trim() !== "";
30889
30896
  const topLevelEnv = normalizeAndValidateEnvironment(diagnostics, configPath, rawConfig, isDispatchNamespace, preserveOriginalMain);
30890
- const isRedirectedConfig = isRedirectedRawConfig(rawConfig, configPath, userConfigPath);
30897
+ const isRedirectedConfig2 = isRedirectedRawConfig(rawConfig, configPath, userConfigPath);
30891
30898
  const definedEnvironments = Object.keys(rawConfig.env ?? {});
30892
- if (isRedirectedConfig && definedEnvironments.length > 0) diagnostics.errors.push(dedent`
30899
+ if (isRedirectedConfig2 && definedEnvironments.length > 0) diagnostics.errors.push(dedent`
30893
30900
  Redirected configurations cannot include environments but the following have been found:\n${definedEnvironments.map((env$1) => ` - ${env$1}`).join("\n")}
30894
30901
 
30895
30902
 
@@ -30900,7 +30907,7 @@ function normalizeAndValidateConfig(rawConfig, configPath, userConfigPath, args,
30900
30907
  const envName = args.env ?? getCloudflareEnv();
30901
30908
  assert(envName === void 0 || typeof envName === "string");
30902
30909
  let activeEnv = topLevelEnv;
30903
- if (envName) if (isRedirectedConfig) {
30910
+ if (envName) if (isRedirectedConfig2) {
30904
30911
  if (!isPagesConfig(rawConfig) && rawConfig.targetEnvironment && rawConfig.targetEnvironment !== envName) {
30905
30912
  const via = args.env !== void 0 ? "via the `--env/-e` CLI argument" : "via the CLOUDFLARE_ENV environment variable";
30906
30913
  throw new Error(dedent`
@@ -30932,9 +30939,9 @@ Consider adding an environment configuration section to the ${configFileName(con
30932
30939
  const config = {
30933
30940
  configPath,
30934
30941
  userConfigPath,
30935
- topLevelName: isRedirectedConfig ? rawConfig.topLevelName : rawConfig.name,
30936
- definedEnvironments: isRedirectedConfig ? rawConfig.definedEnvironments : definedEnvironments,
30937
- targetEnvironment: isRedirectedConfig ? rawConfig.targetEnvironment : envName,
30942
+ topLevelName: isRedirectedConfig2 ? rawConfig.topLevelName : rawConfig.name,
30943
+ definedEnvironments: isRedirectedConfig2 ? rawConfig.definedEnvironments : definedEnvironments,
30944
+ targetEnvironment: isRedirectedConfig2 ? rawConfig.targetEnvironment : envName,
30938
30945
  pages_build_output_dir: normalizeAndValidatePagesBuildOutputDir(configPath, rawConfig.pages_build_output_dir),
30939
30946
  legacy_env: !useServiceEnvironments,
30940
30947
  send_metrics: rawConfig.send_metrics,
@@ -31263,7 +31270,7 @@ var validateStreamingTailConsumers = /* @__PURE__ */ __name((diagnostics, field,
31263
31270
  }, "validateStreamingTailConsumers");
31264
31271
  function normalizeAndValidateEnvironment(diagnostics, configPath, rawEnv, isDispatchNamespace, preserveOriginalMain, envName = "top level", topLevelEnv, useServiceEnvironments, rawConfig) {
31265
31272
  deprecated(diagnostics, rawEnv, "node_compat", `The "node_compat" field is no longer supported as of Wrangler v4. Instead, use the \`nodejs_compat\` compatibility flag. This includes the functionality from legacy \`node_compat\` polyfills and natively implemented Node.js APIs. See https://developers.cloudflare.com/workers/runtime-apis/nodejs for more information.`, true, "Removed", "error");
31266
- experimental(diagnostics, rawEnv, "unsafe");
31273
+ if (topLevelEnv === void 0 || rawConfig?.unsafe === void 0) experimental(diagnostics, rawEnv, "unsafe");
31267
31274
  const route = normalizeAndValidateRoute(diagnostics, topLevelEnv, rawEnv);
31268
31275
  const account_id = inheritableInWranglerEnvironments(diagnostics, useServiceEnvironments, topLevelEnv, mutateEmptyStringAccountIDValue(diagnostics, rawEnv), "account_id", isString$2, void 0, void 0);
31269
31276
  const routes = validateRoutes(diagnostics, topLevelEnv, rawEnv);
@@ -31426,9 +31433,11 @@ var validateDefines = /* @__PURE__ */ __name((envName) => (diagnostics, field, v
31426
31433
  if (configDefines.length > 0) {
31427
31434
  if (typeof value === "object" && value !== null) {
31428
31435
  const configEnvDefines = config === void 0 ? [] : Object.keys(value);
31429
- for (const varName of configDefines) if (!(varName in value)) diagnostics.warnings.push(`"define.${varName}" exists at the top level, but not on "${fieldPath}".
31430
- This is not what you probably want, since "define" configuration is not inherited by environments.
31431
- Please add "define.${varName}" to "env.${envName}".`);
31436
+ const missingDefines = configDefines.filter((varName) => !(varName in value));
31437
+ if (missingDefines.length > 0) diagnostics.warnings.push(`The following define entries exist at the top level, but not on "${fieldPath}".
31438
+ This is probably not what you want, since "define" configuration is not inherited by environments.
31439
+ Please add these entries to "env.${envName}.define":
31440
+ ` + missingDefines.map((varName) => `- ${varName}`).join("\n"));
31432
31441
  for (const varName of configEnvDefines) if (!configDefines.includes(varName)) diagnostics.warnings.push(`"${varName}" exists on "env.${envName}", but not on the top level.
31433
31442
  This is not what you probably want, since "define" configuration within environments can only override existing top level "define" configuration
31434
31443
  Please remove "${fieldPath}.${varName}", or add "define.${varName}".`);
@@ -31452,9 +31461,11 @@ var validateVars = /* @__PURE__ */ __name((envName) => (diagnostics, field, valu
31452
31461
  const configVars = Object.keys(config?.vars ?? {});
31453
31462
  if (configVars.length > 0) {
31454
31463
  if (typeof value === "object" && value !== null) {
31455
- for (const varName of configVars) if (!(varName in value)) diagnostics.warnings.push(`"vars.${varName}" exists at the top level, but not on "${fieldPath}".
31456
- This is not what you probably want, since "vars" configuration is not inherited by environments.
31457
- Please add "vars.${varName}" to "env.${envName}".`);
31464
+ const missingVars = configVars.filter((varName) => !(varName in value));
31465
+ if (missingVars.length > 0) diagnostics.warnings.push(`The following vars exist at the top level, but not on "${fieldPath}".
31466
+ This is probably not what you want, since "vars" configuration is not inherited by environments.
31467
+ Please add these vars to "env.${envName}.vars":
31468
+ ` + missingVars.map((varName) => `- ${varName}`).join("\n"));
31458
31469
  }
31459
31470
  }
31460
31471
  return isValid2;
@@ -33475,27 +33486,36 @@ function startTunnel(options) {
33475
33486
  const timeoutMs = options.timeoutMs ?? TUNNEL_STARTUP_TIMEOUT_MS;
33476
33487
  const reminderIntervalMs = options.reminderIntervalMs ?? DEFAULT_TUNNEL_REMINDER_INTERVAL_MS;
33477
33488
  const defaultExpiryMs = options.expiryMs ?? DEFAULT_TUNNEL_EXPIRY_MS;
33489
+ const isNamedTunnel = options.token !== void 0;
33478
33490
  const timeFormatter = new Intl.DateTimeFormat(void 0, { timeStyle: "short" });
33479
- const readyPromise = spawnCloudflared([
33491
+ const readyPromise = spawnCloudflared(isNamedTunnel ? [
33492
+ "tunnel",
33493
+ "--no-autoupdate",
33494
+ "run"
33495
+ ] : [
33480
33496
  "tunnel",
33481
33497
  "--no-autoupdate",
33482
33498
  "--url",
33483
33499
  options.origin.href
33484
33500
  ], {
33485
33501
  stdio: "pipe",
33502
+ env: options.token ? { TUNNEL_TOKEN: options.token } : void 0,
33486
33503
  skipVersionCheck: true,
33487
33504
  logger
33488
33505
  }).then((process2) => {
33489
33506
  cloudflaredProcess = process2;
33490
33507
  if (disposed) terminateCloudflared(process2);
33491
33508
  return process2;
33492
- }).then((process2) => waitForQuickTunnelReady(process2, timeoutMs, {
33493
- logger,
33494
- origin: options.origin
33495
- })).then((result) => {
33509
+ }).then((process2) => {
33510
+ if (isNamedTunnel) return { mode: "named" };
33511
+ return waitForQuickTunnelReady(process2, timeoutMs, {
33512
+ logger,
33513
+ origin: options.origin
33514
+ });
33515
+ }).then((result) => {
33496
33516
  expiresAt = Date.now() + defaultExpiryMs;
33497
33517
  scheduleExpiryTimeout();
33498
- scheduleReminder(result.publicUrl.origin);
33518
+ scheduleReminder(result.mode === "quick" ? result.publicUrl.origin : void 0);
33499
33519
  return result;
33500
33520
  });
33501
33521
  function disposeTunnel() {
@@ -33521,7 +33541,7 @@ function startTunnel(options) {
33521
33541
  if (disposed) return;
33522
33542
  const remainingMs = expiresAt - Date.now();
33523
33543
  if (remainingMs <= 0) return;
33524
- logger?.log(`The tunnel is still open at ${publicURL}. It expires in ${formatTunnelDuration(remainingMs)}. ${options.extendHint ?? ""}`);
33544
+ logger?.log(`${publicURL ? `The tunnel is still open at ${publicURL}.` : "The tunnel is still open."} It expires in ${formatTunnelDuration(remainingMs)}. ${options.extendHint ?? ""}`);
33525
33545
  }, reminderIntervalMs);
33526
33546
  reminderInterval.unref?.();
33527
33547
  }
@@ -33555,6 +33575,7 @@ function startTunnel(options) {
33555
33575
  __name(extendExpiry, "extendExpiry");
33556
33576
  return {
33557
33577
  ready: /* @__PURE__ */ __name(() => readyPromise, "ready"),
33578
+ isOpen: /* @__PURE__ */ __name(() => !disposed, "isOpen"),
33558
33579
  dispose: disposeTunnel,
33559
33580
  extendExpiry
33560
33581
  };
@@ -33600,7 +33621,10 @@ function waitForQuickTunnelReady(cloudflared, timeoutMs, options) {
33600
33621
  if (match && !resolved) {
33601
33622
  resolved = true;
33602
33623
  clearTimeout(timeoutId);
33603
- resolve$4({ publicUrl: new URL(match[0]) });
33624
+ resolve$4({
33625
+ mode: "quick",
33626
+ publicUrl: new URL(match[0])
33627
+ });
33604
33628
  }
33605
33629
  });
33606
33630
  cloudflared.on("error", (error) => {
@@ -33808,53 +33832,84 @@ var require_picocolors = /* @__PURE__ */ __commonJS$1({ "../../node_modules/.pnp
33808
33832
  //#endregion
33809
33833
  //#region src/plugins/tunnel.ts
33810
33834
  var import_picocolors$5 = /* @__PURE__ */ __toESM$1(require_picocolors(), 1);
33811
- const COMMON_PUBLIC_EXPOSURE_CONCERNS = [
33812
- "Call ungated endpoints",
33813
- "Trigger logic that uses remote bindings",
33814
- "Reach internal services if your Worker proxies requests"
33815
- ];
33816
- const COMMON_PUBLIC_EXPOSURE_GUIDANCE = "Consider using a named tunnel with Cloudflare Access to restrict access.";
33817
- function createPublicExposureWarning(concerns, guidance) {
33818
- return [
33819
- "",
33820
- ` ${import_picocolors$5.default.dim("This URL is ")}publicly accessible${import_picocolors$5.default.dim(". Anyone with it can:")}`,
33821
- ...concerns.map((concern) => import_picocolors$5.default.dim(` • ${concern}`)),
33835
+ function createPublicExposureWarning(mode, name, shortcutPressed) {
33836
+ const intro = name === void 0 ? import_picocolors$5.default.dim("Once connected, this tunnel will be ") + "publicly accessible" + import_picocolors$5.default.dim(". Anyone who can reach it can:") : import_picocolors$5.default.dim("Once connected, this tunnel may be reachable from the Internet. Anyone who can reach it can:");
33837
+ const concerns = [
33838
+ "Call ungated endpoints",
33839
+ "Trigger logic that uses remote bindings",
33840
+ "Reach internal services if your Worker proxies requests"
33841
+ ];
33842
+ const hints = [name === void 0 ? "Consider using a named tunnel with Cloudflare Access to restrict access." : "Consider using Cloudflare Access to restrict access."];
33843
+ if (mode === "dev") {
33844
+ concerns.push("Request module source and other dev-server assets", "Access HMR messages, including absolute file system paths", "Observe file-change cadence and parts of the live module graph");
33845
+ hints.unshift("If you only need to share a running build, use vite preview to avoid HMR and other dev-server exposure.");
33846
+ }
33847
+ const lines = [
33848
+ intro,
33849
+ ...concerns.map((concern) => import_picocolors$5.default.dim(`• ${concern}`)),
33822
33850
  "",
33823
- import_picocolors$5.default.dim(` ${guidance}`),
33851
+ ...hints.map((guidance) => import_picocolors$5.default.dim(guidance)),
33824
33852
  ""
33825
- ].join("\n");
33853
+ ];
33854
+ if (shortcutPressed) lines.push("Press t + enter again to close the tunnel.", "");
33855
+ const spacing = " ";
33856
+ return lines.map((line) => spacing + line).join("\n");
33826
33857
  }
33827
- const DEV_PUBLIC_EXPOSURE_WARNING = createPublicExposureWarning([
33828
- ...COMMON_PUBLIC_EXPOSURE_CONCERNS,
33829
- "Request module source and other dev-server assets",
33830
- "Access HMR messages, including absolute file system paths",
33831
- "Observe file-change cadence and parts of the live module graph"
33832
- ], `If you only need to share a running build, use vite preview to avoid HMR and other dev-server exposure. ${COMMON_PUBLIC_EXPOSURE_GUIDANCE}`);
33833
- const PREVIEW_PUBLIC_EXPOSURE_WARNING = createPublicExposureWarning(COMMON_PUBLIC_EXPOSURE_CONCERNS, COMMON_PUBLIC_EXPOSURE_GUIDANCE);
33834
33858
  const QUICK_TUNNEL_SSE_WARNING = "Quick tunnels do not support Server-Sent Events (SSE). Use a named Cloudflare Tunnel if you need SSE over a public URL.";
33835
33859
  const QUICK_TUNNEL_ALLOWED_HOST = ".trycloudflare.com";
33836
33860
  var TunnelManager = class {
33837
33861
  #logger;
33838
33862
  #origin;
33839
- #publicUrl;
33863
+ #publicUrls;
33864
+ #requestedTunnel;
33840
33865
  #tunnel;
33866
+ #abortController;
33841
33867
  #hasWarnedAboutSse = false;
33842
33868
  constructor(logger) {
33843
33869
  this.#logger = logger;
33844
33870
  }
33845
- isStarted(origin) {
33846
- return this.#origin === origin && this.#tunnel !== void 0;
33871
+ isStarted(origin, name) {
33872
+ return this.#origin === origin && this.#requestedTunnel === name;
33873
+ }
33874
+ isOpen() {
33875
+ if (!this.#tunnel) return this.#origin !== void 0;
33876
+ const isOpen = this.#tunnel.isOpen();
33877
+ if (!isOpen) {
33878
+ this.#tunnel = void 0;
33879
+ this.dispose();
33880
+ }
33881
+ return isOpen;
33847
33882
  }
33848
- async startTunnel(origin) {
33883
+ async startTunnel(options) {
33849
33884
  try {
33850
- if (this.#origin === origin && this.#tunnel) return await this.#waitForPublicUrl(this.#tunnel);
33851
- this.#logger.info(import_picocolors$5.default.dim("\n ➜ Starting tunnel (usually takes a few seconds)...\n"));
33852
33885
  if (this.#tunnel) this.dispose();
33853
- this.#origin = origin;
33854
- this.#publicUrl = void 0;
33886
+ const abortController = new AbortController();
33887
+ this.#abortController = abortController;
33888
+ this.#origin = options.origin;
33889
+ this.#requestedTunnel = options.name;
33890
+ this.#logger.info(import_picocolors$5.default.dim("\n ➜ Starting tunnel (usually takes a few seconds)...\n"));
33891
+ this.#logger.warn(createPublicExposureWarning(options.mode, options.name, options.shortcutPressed ?? false));
33892
+ const namedTunnel = options.name !== void 0 ? await wrangler.unstable_resolveNamedTunnel(options.name, new URL(options.origin), {
33893
+ accountId: options.accountId,
33894
+ complianceRegion: options.complianceRegion
33895
+ }) : void 0;
33896
+ if (abortController.signal.aborted) return null;
33897
+ if (namedTunnel) this.#publicUrls = namedTunnel.hostnames.map((hostname) => `https://${hostname}`);
33898
+ if (options.mode === "preview") {
33899
+ if (namedTunnel) {
33900
+ const allowedUrls = getAllowedTunnelUrls(this.#publicUrls ?? [], options.allowedHosts);
33901
+ if (allowedUrls.length === 0) {
33902
+ const suggestedAllowedHosts = getSuggestedAllowedHosts(namedTunnel.hostnames);
33903
+ throw new Error("The resolved tunnel hostnames are not allowed by Vite preview host validation.\n\nAdd at least one of these hosts to `preview.allowedHosts` in your Vite config.\nYou can use exact hostnames or a domain suffix:\n" + suggestedAllowedHosts.map((hostname) => ` - ${hostname}`).join("\n") + "\n");
33904
+ }
33905
+ this.#publicUrls = allowedUrls;
33906
+ } else if (!isQuickTunnelAllowed(options.allowedHosts)) throw new Error(`Quick tunnel hostnames are not allowed by Vite preview host validation.
33907
+ Add \`${QUICK_TUNNEL_ALLOWED_HOST}\` to \`preview.allowedHosts\` in your Vite config.\n`);
33908
+ }
33855
33909
  const tunnel = startTunnel({
33856
- origin: new URL(origin),
33857
- extendHint: "Press t + enter to extend by 1 hour.",
33910
+ origin: new URL(options.origin),
33911
+ token: namedTunnel?.token,
33912
+ extendHint: "Press a + enter to extend by 1 hour.",
33858
33913
  logger: {
33859
33914
  log: (message) => this.#logger.info(message),
33860
33915
  warn: (message) => this.#logger.warn(message),
@@ -33862,47 +33917,54 @@ var TunnelManager = class {
33862
33917
  }
33863
33918
  });
33864
33919
  this.#tunnel = tunnel;
33865
- return await this.#waitForPublicUrl(tunnel);
33920
+ return await this.#waitForPublicUrls(tunnel);
33866
33921
  } catch (error) {
33867
- throw new Error(`Failed to start tunnel: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
33922
+ this.#origin = void 0;
33923
+ this.#publicUrls = void 0;
33924
+ this.#requestedTunnel = void 0;
33925
+ throw error;
33868
33926
  }
33869
33927
  }
33870
- async #waitForPublicUrl(tunnel) {
33928
+ async #waitForPublicUrls(tunnel) {
33871
33929
  try {
33872
- const { publicUrl } = await tunnel.ready();
33930
+ const result = await tunnel.ready();
33873
33931
  if (this.#tunnel !== tunnel) {
33874
- debuglog("Tunnel was restarted before it finished starting. Ignoring this tunnel's public URL:", publicUrl);
33932
+ debuglog("Tunnel was restarted before it finished starting. Ignoring this tunnel's public URL:", result.mode === "quick" ? result.publicUrl : this.#publicUrls);
33875
33933
  return null;
33876
33934
  }
33935
+ if (result.mode === "named") return this.#publicUrls ?? null;
33936
+ const { publicUrl } = result;
33877
33937
  debuglog("Tunnel is ready with public URL:", publicUrl);
33878
- this.#publicUrl = publicUrl.toString();
33879
- return publicUrl.toString();
33938
+ this.#publicUrls = [publicUrl.toString()];
33939
+ return this.#publicUrls;
33880
33940
  } catch (error) {
33881
33941
  if (this.#tunnel !== tunnel) return null;
33882
- this.#publicUrl = void 0;
33883
33942
  this.#tunnel = void 0;
33884
33943
  throw error;
33885
33944
  }
33886
33945
  }
33887
- get publicUrl() {
33888
- return this.#publicUrl;
33946
+ get publicUrls() {
33947
+ return this.#publicUrls;
33889
33948
  }
33890
33949
  extendExpiry() {
33891
33950
  this.#tunnel?.extendExpiry();
33892
33951
  }
33893
33952
  warnIfQuickTunnelSseResponse(contentType) {
33894
- if (this.#hasWarnedAboutSse || !this.#tunnel || contentType === null || !contentType.toLowerCase().startsWith("text/event-stream")) return;
33953
+ if (this.#hasWarnedAboutSse || this.#requestedTunnel !== void 0 || !this.#tunnel || contentType === null || !contentType.toLowerCase().startsWith("text/event-stream")) return;
33895
33954
  this.#hasWarnedAboutSse = true;
33896
33955
  this.#logger.warn(QUICK_TUNNEL_SSE_WARNING);
33897
33956
  }
33898
33957
  dispose() {
33899
33958
  const tunnel = this.#tunnel;
33959
+ this.#abortController?.abort();
33900
33960
  this.#origin = void 0;
33901
- this.#publicUrl = void 0;
33961
+ this.#publicUrls = void 0;
33962
+ this.#requestedTunnel = void 0;
33902
33963
  this.#tunnel = void 0;
33903
33964
  this.#hasWarnedAboutSse = false;
33904
33965
  debuglog("Disposing tunnel...");
33905
33966
  if (tunnel) tunnel.dispose();
33967
+ this.#logger.info(" ➜ Tunnel closed");
33906
33968
  }
33907
33969
  disposeOnExit() {
33908
33970
  try {
@@ -33922,6 +33984,19 @@ function warnIfQuickTunnelSseResponse(contentType) {
33922
33984
  function extendTunnelExpiry() {
33923
33985
  tunnelManager?.extendExpiry();
33924
33986
  }
33987
+ function isTunnelOpen() {
33988
+ return tunnelManager?.isOpen() ?? false;
33989
+ }
33990
+ async function toggleTunnel(server, ctx) {
33991
+ if (!tunnelManager) return;
33992
+ if (tunnelManager.isOpen()) {
33993
+ ctx.clearTunnelHostnames();
33994
+ tunnelManager.dispose();
33995
+ return;
33996
+ }
33997
+ if ("restart" in server) await setupDevTunnel(server, ctx, tunnelManager, true);
33998
+ else await setupPreviewTunnel(server, ctx, tunnelManager, true);
33999
+ }
33925
34000
  /**
33926
34001
  * Resolve the dev tunnel origin from the running server.
33927
34002
  *
@@ -33952,8 +34027,8 @@ async function resolveDevTunnelOrigin(server) {
33952
34027
  async function resolvePreviewTunnelOrigin(server) {
33953
34028
  const { preview } = server.config;
33954
34029
  const host = typeof preview.host === "string" ? preview.host : void 0;
33955
- let resolvedPort;
33956
- if (preview.port === 0) resolvedPort = await getPorts({
34030
+ let resolvedPort = preview.port;
34031
+ if (!server.httpServer.listening) if (preview.port === 0) resolvedPort = await getPorts({
33957
34032
  port: 0,
33958
34033
  host
33959
34034
  });
@@ -33978,18 +34053,28 @@ async function resolvePreviewTunnelOrigin(server) {
33978
34053
  secure: !!preview.https
33979
34054
  }));
33980
34055
  }
33981
- async function setupDevTunnel(server, ctx, manager) {
34056
+ async function setupDevTunnel(server, ctx, manager, shortcutPressed) {
33982
34057
  const origin = await resolveDevTunnelOrigin(server);
33983
- if (manager.isStarted(origin)) {
34058
+ const tunnel = ctx.resolvedPluginConfig.tunnel;
34059
+ if (manager.isStarted(origin, tunnel.name)) {
33984
34060
  debuglog("Tunnel is already started on", origin);
33985
34061
  return;
33986
34062
  }
33987
- const publicUrl = await manager.startTunnel(origin);
33988
- if (!publicUrl) return;
34063
+ const publicUrls = await manager.startTunnel({
34064
+ mode: "dev",
34065
+ origin,
34066
+ shortcutPressed,
34067
+ name: tunnel.name,
34068
+ accountId: ctx.entryWorkerConfig?.account_id,
34069
+ complianceRegion: ctx.entryWorkerConfig?.compliance_region,
34070
+ allowedHosts: true
34071
+ });
34072
+ if (!publicUrls) return;
33989
34073
  const allowedHosts = server.config.server.allowedHosts;
33990
- const tunnelHostnames = [new URL(publicUrl).hostname];
34074
+ const tunnelHostnames = publicUrls.map((url) => new URL(url).hostname);
33991
34075
  ctx.replaceTunnelHostnames(tunnelHostnames);
33992
34076
  if (allowedHosts !== true && tunnelHostnames.some((hostname) => !allowedHosts.includes(hostname))) await server.restart();
34077
+ if (shortcutPressed) server.printUrls();
33993
34078
  }
33994
34079
  /**
33995
34080
  * Start a preview tunnel on the resolved preview origin.
@@ -33997,7 +34082,7 @@ async function setupDevTunnel(server, ctx, manager) {
33997
34082
  * We write the resolved port back to preview config so the server binds the
33998
34083
  * same port that the tunnel is sharing.
33999
34084
  */
34000
- async function setupPreviewTunnel(server, manager) {
34085
+ async function setupPreviewTunnel(server, ctx, manager, shortcutPressed) {
34001
34086
  const { preview } = server.config;
34002
34087
  const originalPort = preview.port;
34003
34088
  const resolvedOrigin = await resolvePreviewTunnelOrigin(server);
@@ -34005,17 +34090,58 @@ async function setupPreviewTunnel(server, manager) {
34005
34090
  preview.port = resolvedPort;
34006
34091
  preview.strictPort = true;
34007
34092
  if (originalPort !== 0 && resolvedPort !== originalPort) server.config.logger.info(import_picocolors$5.default.dim(`Port ${originalPort} is in use, using ${resolvedPort} instead for preview tunnel sharing.\n`));
34008
- await manager.startTunnel(resolvedOrigin.toString());
34009
- }
34010
- function patchPrintUrls(server, warning) {
34093
+ const tunnel = ctx.resolvedPluginConfig.tunnel;
34094
+ const origin = resolvedOrigin.toString();
34095
+ if (manager.isStarted(origin, tunnel.name)) {
34096
+ debuglog("Tunnel is already started on", origin);
34097
+ return;
34098
+ }
34099
+ if (!await manager.startTunnel({
34100
+ mode: "preview",
34101
+ origin,
34102
+ shortcutPressed,
34103
+ name: tunnel.name,
34104
+ allowedHosts: preview?.allowedHosts,
34105
+ accountId: ctx.allWorkerConfigs[0]?.account_id,
34106
+ complianceRegion: ctx.allWorkerConfigs[0]?.compliance_region
34107
+ })) return;
34108
+ if (shortcutPressed) server.printUrls();
34109
+ }
34110
+ function patchPrintUrls(server) {
34011
34111
  const serverPrintUrls = server.printUrls.bind(server);
34012
34112
  server.printUrls = () => {
34013
34113
  serverPrintUrls();
34014
- const publicUrl = tunnelManager?.publicUrl;
34015
- if (!publicUrl) return;
34016
- server.config.logger.info(`${import_picocolors$5.default.green(" ➜")} ${import_picocolors$5.default.bold("Tunnel:")} ${import_picocolors$5.default.cyan(publicUrl)}`);
34017
- server.config.logger.warnOnce(warning);
34018
- };
34114
+ const publicUrls = tunnelManager?.publicUrls;
34115
+ if (!publicUrls || publicUrls.length === 0) return;
34116
+ for (let i$1 = 0; i$1 < publicUrls.length; i$1++) if (i$1 === 0) server.config.logger.info(`${import_picocolors$5.default.green(" ➜")} ${import_picocolors$5.default.bold("Tunnel:")} ${import_picocolors$5.default.cyan(publicUrls[i$1])}`);
34117
+ else server.config.logger.info(` ${import_picocolors$5.default.cyan(publicUrls[i$1])}`);
34118
+ server.config.logger.info("");
34119
+ };
34120
+ }
34121
+ function isQuickTunnelAllowed(allowedHosts) {
34122
+ if (allowedHosts === void 0) return false;
34123
+ if (allowedHosts === true) return true;
34124
+ return allowedHosts.some((allowedHost) => allowedHost.toLowerCase() === QUICK_TUNNEL_ALLOWED_HOST);
34125
+ }
34126
+ function getAllowedTunnelUrls(publicUrls, allowedHosts) {
34127
+ if (allowedHosts === void 0) return [];
34128
+ if (allowedHosts === true) return publicUrls;
34129
+ return publicUrls.filter((publicUrl) => {
34130
+ const hostname = new URL(publicUrl).hostname.toLowerCase();
34131
+ return allowedHosts.some((allowedHost) => {
34132
+ const normalizedAllowedHost = allowedHost.toLowerCase();
34133
+ if (normalizedAllowedHost.startsWith(".")) return hostname === normalizedAllowedHost.slice(1) || hostname.endsWith(normalizedAllowedHost);
34134
+ return hostname === normalizedAllowedHost;
34135
+ });
34136
+ });
34137
+ }
34138
+ function getSuggestedAllowedHosts(hostnames) {
34139
+ const suggestions = new Set(hostnames);
34140
+ for (const hostname of hostnames) {
34141
+ const segments = hostname.split(".");
34142
+ if (segments.length > 2) suggestions.add(`.${segments.slice(1).join(".")}`);
34143
+ }
34144
+ return Array.from(suggestions);
34019
34145
  }
34020
34146
  const tunnelPlugin = createPlugin("tunnel", (ctx) => {
34021
34147
  function stopTunnel() {
@@ -34028,12 +34154,9 @@ const tunnelPlugin = createPlugin("tunnel", (ctx) => {
34028
34154
  },
34029
34155
  configureServer(server) {
34030
34156
  assertIsNotPreview(ctx);
34031
- if (!ctx.resolvedPluginConfig.tunnel) {
34032
- stopTunnel();
34033
- return;
34034
- }
34035
34157
  tunnelManager ??= new TunnelManager(server.config.logger);
34036
- patchPrintUrls(server, DEV_PUBLIC_EXPOSURE_WARNING);
34158
+ patchPrintUrls(server);
34159
+ if (!ctx.resolvedPluginConfig.tunnel.autoStart) return;
34037
34160
  const serverListen = server.listen.bind(server);
34038
34161
  server.listen = async (...args) => {
34039
34162
  const result = await serverListen(...args);
@@ -34050,13 +34173,9 @@ const tunnelPlugin = createPlugin("tunnel", (ctx) => {
34050
34173
  },
34051
34174
  async configurePreviewServer(server) {
34052
34175
  assertIsPreview(ctx);
34053
- if (!ctx.resolvedPluginConfig.tunnel) {
34054
- stopTunnel();
34055
- return;
34056
- }
34057
34176
  tunnelManager ??= new TunnelManager(server.config.logger);
34058
- patchPrintUrls(server, PREVIEW_PUBLIC_EXPOSURE_WARNING);
34059
- await setupPreviewTunnel(server, tunnelManager);
34177
+ patchPrintUrls(server);
34178
+ if (ctx.resolvedPluginConfig.tunnel.autoStart) await setupPreviewTunnel(server, ctx, tunnelManager);
34060
34179
  const closePreviewServer = server.close.bind(server);
34061
34180
  server.close = async () => {
34062
34181
  const closePromise = closePreviewServer();
@@ -34102,7 +34221,8 @@ function createRequestHandler(handler) {
34102
34221
  let request$2;
34103
34222
  try {
34104
34223
  if (req.originalUrl) req.url = req.originalUrl;
34105
- request$2 = createRequest(req, res);
34224
+ const protocol = getForwardedProto(req);
34225
+ request$2 = createRequest(req, res, protocol ? { protocol } : void 0);
34106
34226
  let response = await handler(toMiniflareRequest(request$2), req);
34107
34227
  if (req.httpVersionMajor === 2) {
34108
34228
  response = new Response$1(response.body, response);
@@ -34135,6 +34255,20 @@ function toMiniflareRequest(request$2) {
34135
34255
  });
34136
34256
  }
34137
34257
  const isRolldown = "rolldownVersion" in vite;
34258
+ /**
34259
+ * Parses the `X-Forwarded-Proto` header from an incoming Node.js request.
34260
+ *
34261
+ * If multiple proxies are in the chain, the header may be a comma-separated
34262
+ * list — the left-most value is the original client-facing protocol.
34263
+ * Returns `undefined` if the header is missing or holds an unsupported value.
34264
+ */
34265
+ function getForwardedProto(req) {
34266
+ const raw = req.headers["x-forwarded-proto"];
34267
+ const value = Array.isArray(raw) ? raw[0] : raw;
34268
+ if (!value) return;
34269
+ const first = value.split(",")[0]?.trim().toLowerCase();
34270
+ if (first === "http" || first === "https") return `${first}:`;
34271
+ }
34138
34272
 
34139
34273
  //#endregion
34140
34274
  //#region src/export-types.ts
@@ -41274,7 +41408,10 @@ function resolvePluginConfig(pluginConfig, userConfig, viteEnv) {
41274
41408
  const shared = {
41275
41409
  persistState: pluginConfig.persistState ?? true,
41276
41410
  inspectorPort: pluginConfig.inspectorPort,
41277
- tunnel: pluginConfig.tunnel ?? false,
41411
+ tunnel: typeof pluginConfig.tunnel === "boolean" ? { autoStart: pluginConfig.tunnel } : {
41412
+ autoStart: pluginConfig.tunnel?.autoStart ?? false,
41413
+ name: pluginConfig.tunnel?.name
41414
+ },
41278
41415
  experimental: { headersAndRedirectsDevModeSupport: pluginConfig.experimental?.headersAndRedirectsDevModeSupport }
41279
41416
  };
41280
41417
  const root = userConfig.root ? nodePath.resolve(userConfig.root) : process.cwd();
@@ -48520,10 +48657,7 @@ function validateWorkerEnvironmentOptions(resolvedPluginConfig, resolvedViteConf
48520
48657
  const configPlugin = createPlugin("config", (ctx) => {
48521
48658
  return {
48522
48659
  config(userConfig, env$1) {
48523
- if (ctx.resolvedPluginConfig.type === "preview") return {
48524
- appType: "custom",
48525
- preview: { allowedHosts: getAllowedHosts(ctx.resolvedPluginConfig.tunnel ? [QUICK_TUNNEL_ALLOWED_HOST] : [], userConfig.preview?.allowedHosts ?? userConfig.server?.allowedHosts) }
48526
- };
48660
+ if (ctx.resolvedPluginConfig.type === "preview") return { appType: "custom" };
48527
48661
  if (!ctx.hasShownWorkerConfigWarnings) {
48528
48662
  ctx.setHasShownWorkerConfigWarnings(true);
48529
48663
  const workerConfigWarnings = getWarningForWorkersConfigs(ctx.resolvedPluginConfig.rawConfigs);
@@ -52760,7 +52894,8 @@ function handleWebSocket(httpServer, miniflare, entryWorkerName) {
52760
52894
  httpServer.on("upgrade", async (request$2, socket, head) => {
52761
52895
  socket.on("error", () => socket.destroy());
52762
52896
  const rawHost = request$2.headers.host ?? UNKNOWN_HOST;
52763
- const base = /^https?:\/\//i.test(rawHost) ? rawHost : `http://${rawHost}`;
52897
+ const protocol = getForwardedProto(request$2) ?? "http:";
52898
+ const base = /^https?:\/\//i.test(rawHost) ? rawHost : `${protocol}//${rawHost}`;
52764
52899
  const url = new URL(request$2.url ?? "", base);
52765
52900
  const isViteRequest = request$2.headers["sec-websocket-protocol"]?.startsWith("vite");
52766
52901
  const isSandboxRequest = hasSandboxOrigin(url.origin);
@@ -53740,22 +53875,16 @@ const shortcutsPlugin = createPlugin("shortcuts", (ctx) => {
53740
53875
  async configureServer(viteDevServer) {
53741
53876
  if (!isCustomShortcutsSupported) return;
53742
53877
  assertIsNotPreview(ctx);
53743
- addBindingsShortcut(viteDevServer, ctx);
53744
- addExplorerShortcut(viteDevServer);
53745
- if (ctx.resolvedPluginConfig.tunnel) addTunnelShortcut(viteDevServer);
53878
+ addShortcuts(viteDevServer, ctx);
53746
53879
  },
53747
53880
  async configurePreviewServer(vitePreviewServer) {
53748
53881
  if (!isCustomShortcutsSupported) return;
53749
53882
  assertIsPreview(ctx);
53750
- addBindingsShortcut(vitePreviewServer, ctx);
53751
- addExplorerShortcut(vitePreviewServer);
53752
- if (ctx.resolvedPluginConfig.tunnel) addTunnelShortcut(vitePreviewServer);
53883
+ addShortcuts(vitePreviewServer, ctx);
53753
53884
  }
53754
53885
  };
53755
53886
  });
53756
- function addBindingsShortcut(server, ctx) {
53757
- const workerConfigs = ctx.allWorkerConfigs;
53758
- if (workerConfigs.length === 0) return;
53887
+ function addShortcuts(server, ctx) {
53759
53888
  if (!process.stdin.isTTY) return;
53760
53889
  const registryPath = getDefaultDevRegistryPath();
53761
53890
  const printBindingsShortcut = {
@@ -53763,6 +53892,7 @@ function addBindingsShortcut(server, ctx) {
53763
53892
  description: "list configured Cloudflare bindings",
53764
53893
  action: (viteServer) => {
53765
53894
  viteServer.config.logger.info("");
53895
+ const workerConfigs = ctx.allWorkerConfigs;
53766
53896
  for (const workerConfig of workerConfigs) {
53767
53897
  const bindings = wrangler.unstable_convertConfigBindingsToStartWorkerBindings(workerConfig);
53768
53898
  wrangler.unstable_printBindings(bindings, workerConfig.tail_consumers, workerConfig.streaming_tail_consumers, workerConfig.containers, {
@@ -53775,15 +53905,6 @@ function addBindingsShortcut(server, ctx) {
53775
53905
  }
53776
53906
  }
53777
53907
  };
53778
- const bindCLIShortcuts = server.bindCLIShortcuts.bind(server);
53779
- server.bindCLIShortcuts = (options) => {
53780
- if (server.httpServer && process.stdin.isTTY && !process.env.CI && options?.print) server.config.logger.info(import_picocolors.default.dim(import_picocolors.default.green(" ➜")) + import_picocolors.default.dim(" press ") + import_picocolors.default.bold(`${printBindingsShortcut.key} + enter`) + import_picocolors.default.dim(` to ${printBindingsShortcut.description}`));
53781
- bindCLIShortcuts(options);
53782
- };
53783
- server.bindCLIShortcuts({ customShortcuts: [printBindingsShortcut] });
53784
- }
53785
- function addExplorerShortcut(server) {
53786
- if (!process.stdin.isTTY) return;
53787
53908
  const openExplorerShortcut = {
53788
53909
  key: "e",
53789
53910
  description: "open local explorer",
@@ -53799,22 +53920,38 @@ function addExplorerShortcut(server) {
53799
53920
  });
53800
53921
  }
53801
53922
  };
53802
- const bindCLIShortcuts = server.bindCLIShortcuts.bind(server);
53803
- server.bindCLIShortcuts = (options) => {
53804
- if (server.httpServer && process.stdin.isTTY && !process.env.CI && options?.print) server.config.logger.info(import_picocolors.default.dim(import_picocolors.default.green(" ➜")) + import_picocolors.default.dim(" press ") + import_picocolors.default.bold(`${openExplorerShortcut.key} + enter`) + import_picocolors.default.dim(` to ${openExplorerShortcut.description}`));
53805
- bindCLIShortcuts(options);
53806
- };
53807
- server.bindCLIShortcuts({ customShortcuts: [openExplorerShortcut] });
53808
- }
53809
- function addTunnelShortcut(server) {
53810
- if (!process.stdin.isTTY) return;
53811
- server.bindCLIShortcuts({ customShortcuts: [{
53923
+ const toggleTunnelShortcut = {
53812
53924
  key: "t",
53925
+ description: "start or close tunnel",
53926
+ action: () => {
53927
+ toggleTunnel(server, ctx).catch((error) => {
53928
+ const message = error instanceof Error ? error.message : String(error);
53929
+ server.config.logger.error(import_picocolors.default.red(`Error: ${message}`));
53930
+ });
53931
+ }
53932
+ };
53933
+ const extendTunnelExpiryShortcut = {
53934
+ key: "a",
53813
53935
  description: "extend tunnel by 1 hour",
53814
53936
  action: () => {
53815
53937
  extendTunnelExpiry();
53816
53938
  }
53817
- }] });
53939
+ };
53940
+ const bindCLIShortcuts = server.bindCLIShortcuts.bind(server);
53941
+ server.bindCLIShortcuts = (options) => {
53942
+ if (server.httpServer && process.stdin.isTTY && !process.env.CI && options?.print) {
53943
+ if (ctx.allWorkerConfigs.length > 0) server.config.logger.info(import_picocolors.default.dim(import_picocolors.default.green(" ➜")) + import_picocolors.default.dim(" press ") + import_picocolors.default.bold(`${printBindingsShortcut.key} + enter`) + import_picocolors.default.dim(` to ${printBindingsShortcut.description}`));
53944
+ server.config.logger.info(import_picocolors.default.dim(import_picocolors.default.green(" ➜")) + import_picocolors.default.dim(" press ") + import_picocolors.default.bold(`${openExplorerShortcut.key} + enter`) + import_picocolors.default.dim(` to ${openExplorerShortcut.description}`));
53945
+ server.config.logger.info(import_picocolors.default.dim(import_picocolors.default.green(" ➜")) + import_picocolors.default.dim(" press ") + import_picocolors.default.bold(`${toggleTunnelShortcut.key} + enter`) + import_picocolors.default.dim(` to ${isTunnelOpen() ? "close tunnel" : "start tunnel"}`));
53946
+ }
53947
+ bindCLIShortcuts(options);
53948
+ };
53949
+ server.bindCLIShortcuts({ customShortcuts: [
53950
+ printBindingsShortcut,
53951
+ openExplorerShortcut,
53952
+ toggleTunnelShortcut,
53953
+ extendTunnelExpiryShortcut
53954
+ ] });
53818
53955
  }
53819
53956
 
53820
53957
  //#endregion