vite-plugin-rpx 0.11.22 → 0.11.24

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.
Files changed (2) hide show
  1. package/dist/index.js +280 -83
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -22443,6 +22443,151 @@ var init_https = __esm(() => {
22443
22443
  SHARED_DEV_HOST_CERT_PATH = join12(homedir8(), ".stacks", "ssl", "rpx.localhost.crt");
22444
22444
  });
22445
22445
 
22446
+ // ../rpx/src/load-balancer.ts
22447
+ function resolveHealthCheckConfig(config6) {
22448
+ return {
22449
+ enabled: config6?.enabled ?? false,
22450
+ path: config6?.path ?? DEFAULT_HEALTH_CHECK_PATH,
22451
+ interval: config6?.interval ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS,
22452
+ timeout: config6?.timeout ?? DEFAULT_HEALTH_CHECK_TIMEOUT_MS,
22453
+ healthyThreshold: config6?.healthyThreshold ?? DEFAULT_HEALTHY_THRESHOLD,
22454
+ unhealthyThreshold: config6?.unhealthyThreshold ?? DEFAULT_UNHEALTHY_THRESHOLD
22455
+ };
22456
+ }
22457
+ function normalizeUpstreams(from) {
22458
+ const list = Array.isArray(from) ? from : [from];
22459
+ return list.map((entry) => {
22460
+ const target = typeof entry === "string" ? { url: entry } : entry;
22461
+ return {
22462
+ url: target.url,
22463
+ weight: target.weight && target.weight > 0 ? target.weight : 1,
22464
+ healthy: true,
22465
+ activeConnections: 0,
22466
+ consecutiveFailures: 0,
22467
+ consecutiveSuccesses: 0
22468
+ };
22469
+ });
22470
+ }
22471
+ function primaryUpstreamUrl(from) {
22472
+ if (!from)
22473
+ return "localhost:5173";
22474
+ const first = Array.isArray(from) ? from[0] : from;
22475
+ if (!first)
22476
+ return "localhost:5173";
22477
+ return typeof first === "string" ? first : first.url;
22478
+ }
22479
+ function createUpstreamPool(from, lbConfig) {
22480
+ return {
22481
+ upstreams: normalizeUpstreams(from),
22482
+ strategy: lbConfig?.strategy ?? "round-robin",
22483
+ healthCheck: resolveHealthCheckConfig(lbConfig?.healthCheck),
22484
+ cursor: 0,
22485
+ wrrCurrentWeights: [],
22486
+ healthCheckTimer: null
22487
+ };
22488
+ }
22489
+ function healthyUpstreams(pool) {
22490
+ const healthy = pool.upstreams.filter((u2) => u2.healthy);
22491
+ if (healthy.length === 0 && pool.upstreams.length === 1)
22492
+ return pool.upstreams;
22493
+ return healthy;
22494
+ }
22495
+ function selectRoundRobin(pool, candidates) {
22496
+ const idx = pool.cursor % candidates.length;
22497
+ pool.cursor = (pool.cursor + 1) % candidates.length;
22498
+ return candidates[idx];
22499
+ }
22500
+ function selectWeightedRoundRobin(pool, candidates) {
22501
+ if (pool.wrrCurrentWeights.length !== candidates.length)
22502
+ pool.wrrCurrentWeights = candidates.map(() => 0);
22503
+ const totalWeight = candidates.reduce((sum, u2) => sum + u2.weight, 0);
22504
+ let bestIdx = 0;
22505
+ for (let i2 = 0;i2 < candidates.length; i2++) {
22506
+ pool.wrrCurrentWeights[i2] += candidates[i2].weight;
22507
+ if (pool.wrrCurrentWeights[i2] > pool.wrrCurrentWeights[bestIdx])
22508
+ bestIdx = i2;
22509
+ }
22510
+ pool.wrrCurrentWeights[bestIdx] -= totalWeight;
22511
+ return candidates[bestIdx];
22512
+ }
22513
+ function selectLeastConnections(candidates) {
22514
+ let best = candidates[0];
22515
+ for (let i2 = 1;i2 < candidates.length; i2++) {
22516
+ if (candidates[i2].activeConnections < best.activeConnections)
22517
+ best = candidates[i2];
22518
+ }
22519
+ return best;
22520
+ }
22521
+ function selectUpstream(pool) {
22522
+ const candidates = healthyUpstreams(pool);
22523
+ if (candidates.length === 0)
22524
+ return;
22525
+ if (candidates.length === 1)
22526
+ return candidates[0];
22527
+ switch (pool.strategy) {
22528
+ case "weighted-round-robin":
22529
+ return selectWeightedRoundRobin(pool, candidates);
22530
+ case "least-connections":
22531
+ return selectLeastConnections(candidates);
22532
+ case "round-robin":
22533
+ default:
22534
+ return selectRoundRobin(pool, candidates);
22535
+ }
22536
+ }
22537
+ function markSuccess(pool, upstream) {
22538
+ upstream.consecutiveFailures = 0;
22539
+ upstream.consecutiveSuccesses += 1;
22540
+ if (!upstream.healthy && upstream.consecutiveSuccesses >= pool.healthCheck.healthyThreshold)
22541
+ upstream.healthy = true;
22542
+ }
22543
+ function markFailure(pool, upstream) {
22544
+ upstream.consecutiveSuccesses = 0;
22545
+ upstream.consecutiveFailures += 1;
22546
+ if (upstream.healthy && upstream.consecutiveFailures >= pool.healthCheck.unhealthyThreshold)
22547
+ upstream.healthy = false;
22548
+ }
22549
+ function splitHostPort(hostPort) {
22550
+ const idx = hostPort.lastIndexOf(":");
22551
+ if (idx === -1)
22552
+ return { hostname: hostPort, port: 80 };
22553
+ const port = Number(hostPort.slice(idx + 1));
22554
+ return { hostname: hostPort.slice(0, idx), port: Number.isFinite(port) ? port : 80 };
22555
+ }
22556
+ async function probeUpstream(pool, upstream) {
22557
+ const { hostname, port } = splitHostPort(upstream.url);
22558
+ const controller = new AbortController;
22559
+ const timer = setTimeout(() => controller.abort(), pool.healthCheck.timeout);
22560
+ try {
22561
+ const res = await fetch(`http://${hostname}:${port}${pool.healthCheck.path}`, {
22562
+ signal: controller.signal,
22563
+ redirect: "manual"
22564
+ });
22565
+ res.body?.cancel().catch(() => {});
22566
+ markSuccess(pool, upstream);
22567
+ } catch {
22568
+ markFailure(pool, upstream);
22569
+ } finally {
22570
+ clearTimeout(timer);
22571
+ }
22572
+ }
22573
+ function startHealthChecks(pool) {
22574
+ if (!pool.healthCheck.enabled || pool.healthCheckTimer || pool.upstreams.length < 2)
22575
+ return;
22576
+ const timer = setInterval(() => {
22577
+ for (const upstream of pool.upstreams)
22578
+ probeUpstream(pool, upstream);
22579
+ }, pool.healthCheck.interval);
22580
+ timer.unref?.();
22581
+ pool.healthCheckTimer = timer;
22582
+ }
22583
+ function stopHealthChecks(pool) {
22584
+ if (pool.healthCheckTimer) {
22585
+ clearInterval(pool.healthCheckTimer);
22586
+ pool.healthCheckTimer = null;
22587
+ }
22588
+ }
22589
+ var DEFAULT_HEALTHY_THRESHOLD = 2, DEFAULT_UNHEALTHY_THRESHOLD = 3, DEFAULT_HEALTH_CHECK_PATH = "/", DEFAULT_HEALTH_CHECK_INTERVAL_MS = 1e4, DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 2000;
22590
+
22446
22591
  // ../rpx/src/auth.ts
22447
22592
  import { createHash, timingSafeEqual } from "node:crypto";
22448
22593
  import { readFileSync as readFileSync2 } from "node:fs";
@@ -23542,8 +23687,15 @@ function stripBasePath(pathname, basePath) {
23542
23687
  }
23543
23688
  return pathname;
23544
23689
  }
23545
- function resolveTarget(pathname, route, verbose) {
23546
- let targetHost = route.sourceHost ?? "";
23690
+ function pickUpstream(route) {
23691
+ if (route.upstreamPool)
23692
+ return selectUpstream(route.upstreamPool);
23693
+ if (route.sourceHost)
23694
+ return { url: route.sourceHost, weight: 1, healthy: true, activeConnections: 0, consecutiveFailures: 0, consecutiveSuccesses: 0 };
23695
+ return;
23696
+ }
23697
+ function resolveTarget(pathname, route, upstream, verbose) {
23698
+ let targetHost = upstream?.url ?? route.sourceHost ?? "";
23547
23699
  const stripBase = route.stripBasePathPrefix ?? false;
23548
23700
  let targetPath = stripBase ? stripBasePath(pathname, route.basePath) : pathname;
23549
23701
  const rewriteMatch = resolvePathRewrite(targetPath, route.pathRewrites);
@@ -23598,9 +23750,10 @@ function createProxyFetchHandler(getRoute, verbose, onNoRoute) {
23598
23750
  return serveStaticFile(staticPath, route.static);
23599
23751
  }
23600
23752
  if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
23601
- if (!server || !route.sourceHost)
23753
+ const wsUpstream = pickUpstream(route);
23754
+ if (!server || !wsUpstream)
23602
23755
  return new Response("WebSocket upgrade not supported here", { status: 400 });
23603
- const { targetHost: targetHost2, targetPath: targetPath2 } = resolveTarget(pathname, route, verbose);
23756
+ const { targetHost: targetHost2, targetPath: targetPath2 } = resolveTarget(pathname, route, wsUpstream, verbose);
23604
23757
  const targetUrl = `ws://${targetHost2}${targetPath2}${search}`;
23605
23758
  const forwardHeaders = {};
23606
23759
  for (const [k2, v2] of req.headers) {
@@ -23619,8 +23772,10 @@ function createProxyFetchHandler(getRoute, verbose, onNoRoute) {
23619
23772
  }
23620
23773
  return new Response("WebSocket upgrade failed", { status: 400 });
23621
23774
  }
23622
- if (!route.sourceHost)
23775
+ const upstream = pickUpstream(route);
23776
+ if (!upstream) {
23623
23777
  return new Response(`No upstream configured for ${hostname}`, { status: 502 });
23778
+ }
23624
23779
  if (route.cleanUrls && pathname.endsWith(".html")) {
23625
23780
  const cleanPath = pathname.replace(/\.html$/, "");
23626
23781
  return new Response(null, {
@@ -23628,11 +23783,14 @@ function createProxyFetchHandler(getRoute, verbose, onNoRoute) {
23628
23783
  headers: { Location: cleanPath }
23629
23784
  });
23630
23785
  }
23631
- const { targetHost, targetPath } = resolveTarget(pathname, route, verbose);
23632
- const originOverride = route.changeOrigin ? `http://${route.sourceHost}` : undefined;
23786
+ const { targetHost, targetPath } = resolveTarget(pathname, route, upstream, verbose);
23787
+ const originOverride = route.changeOrigin ? `http://${targetHost}` : undefined;
23633
23788
  const hasBody = req.body != null && req.method !== "GET" && req.method !== "HEAD";
23789
+ const pool = route.upstreamPool;
23790
+ if (pool)
23791
+ upstream.activeConnections++;
23634
23792
  try {
23635
- return await proxyViaPool({
23793
+ const res = await proxyViaPool({
23636
23794
  hostPort: targetHost,
23637
23795
  method: req.method,
23638
23796
  path: `${targetPath}${search}`,
@@ -23641,9 +23799,14 @@ function createProxyFetchHandler(getRoute, verbose, onNoRoute) {
23641
23799
  originOverride,
23642
23800
  body: req.body
23643
23801
  });
23802
+ if (pool)
23803
+ markSuccess(pool, upstream);
23804
+ return res;
23644
23805
  } catch (err) {
23645
23806
  if (err === TIMEOUT) {
23646
23807
  debugLog("request", `Upstream timeout for ${hostname}`, verbose);
23808
+ if (pool)
23809
+ markFailure(pool, upstream);
23647
23810
  return new Response("Gateway Timeout", { status: 504 });
23648
23811
  }
23649
23812
  if (err === POOL_BUSY) {
@@ -23659,19 +23822,29 @@ function createProxyFetchHandler(getRoute, verbose, onNoRoute) {
23659
23822
  headers.set("x-forwarded-host", hostname);
23660
23823
  if (originOverride !== undefined)
23661
23824
  headers.set("origin", originOverride);
23662
- return await fetch(`http://${targetHost}${targetPath}${search}`, {
23825
+ const res = await fetch(`http://${targetHost}${targetPath}${search}`, {
23663
23826
  method: req.method,
23664
23827
  headers,
23665
23828
  body: req.body,
23666
23829
  redirect: "manual"
23667
23830
  });
23831
+ if (pool)
23832
+ markSuccess(pool, upstream);
23833
+ return res;
23668
23834
  } catch (fetchErr) {
23669
23835
  debugLog("request", `Proxy error for ${hostname}: ${fetchErr}`, verbose);
23836
+ if (pool)
23837
+ markFailure(pool, upstream);
23670
23838
  return new Response(`Proxy Error: ${fetchErr}`, { status: 502 });
23671
23839
  }
23672
23840
  }
23673
23841
  debugLog("request", `Proxy error for ${hostname}: ${err}`, verbose);
23842
+ if (pool)
23843
+ markFailure(pool, upstream);
23674
23844
  return new Response(`Proxy Error: ${err}`, { status: 502 });
23845
+ } finally {
23846
+ if (pool)
23847
+ upstream.activeConnections--;
23675
23848
  }
23676
23849
  };
23677
23850
  return async (req, server) => {
@@ -23778,6 +23951,24 @@ var init_proxy_handler = __esm(() => {
23778
23951
  ]);
23779
23952
  });
23780
23953
 
23954
+ // ../rpx/src/acme-challenge.ts
23955
+ import * as fs3 from "node:fs";
23956
+ import * as path3 from "node:path";
23957
+ function readAcmeChallenge(webroot, pathname) {
23958
+ if (!webroot || !pathname.startsWith(ACME_CHALLENGE_PREFIX2))
23959
+ return null;
23960
+ const token = pathname.slice(ACME_CHALLENGE_PREFIX2.length);
23961
+ if (!token || !/^[A-Za-z0-9_-]+$/.test(token))
23962
+ return null;
23963
+ try {
23964
+ return fs3.readFileSync(path3.join(webroot, token), "utf8");
23965
+ } catch {
23966
+ return null;
23967
+ }
23968
+ }
23969
+ var ACME_CHALLENGE_PREFIX2 = "/.well-known/acme-challenge/";
23970
+ var init_acme_challenge = () => {};
23971
+
23781
23972
  // ../rpx/src/host-match.ts
23782
23973
  function isWildcardPattern(pattern) {
23783
23974
  return pattern.startsWith("*.");
@@ -23790,10 +23981,10 @@ function matchesWildcard(hostname, pattern) {
23790
23981
  }
23791
23982
 
23792
23983
  // ../rpx/src/host-routes.ts
23793
- function normalizePathPrefix(path3) {
23794
- if (!path3 || path3 === "/")
23984
+ function normalizePathPrefix(path4) {
23985
+ if (!path4 || path4 === "/")
23795
23986
  return "/";
23796
- let p2 = path3.trim();
23987
+ let p2 = path4.trim();
23797
23988
  if (!p2.startsWith("/"))
23798
23989
  p2 = `/${p2}`;
23799
23990
  p2 = p2.replace(/\/+$/, "");
@@ -23820,8 +24011,8 @@ function buildHostRoutes(entries) {
23820
24011
  const table = new Map;
23821
24012
  for (const [host, paths] of byHost) {
23822
24013
  const list = [];
23823
- for (const [path3, route] of paths)
23824
- list.push({ path: path3, route });
24014
+ for (const [path4, route] of paths)
24015
+ list.push({ path: path4, route });
23825
24016
  list.sort((a2, b2) => b2.path.length - a2.path.length);
23826
24017
  table.set(host, list);
23827
24018
  }
@@ -23860,7 +24051,7 @@ var init_host_routes = () => {};
23860
24051
 
23861
24052
  // ../rpx/src/sni.ts
23862
24053
  import * as fsp from "node:fs/promises";
23863
- import * as path3 from "node:path";
24054
+ import * as path4 from "node:path";
23864
24055
  function serverNameFromCertFilename(filename) {
23865
24056
  if (!filename.endsWith(".crt"))
23866
24057
  return null;
@@ -23898,8 +24089,8 @@ async function buildSniTlsConfig(cfg, verbose) {
23898
24089
  continue;
23899
24090
  const base = name.slice(0, -".crt".length);
23900
24091
  bySrvName.set(serverName, {
23901
- certPath: path3.join(cfg.certsDir, name),
23902
- keyPath: path3.join(cfg.certsDir, `${base}.key`)
24092
+ certPath: path4.join(cfg.certsDir, name),
24093
+ keyPath: path4.join(cfg.certsDir, `${base}.key`)
23903
24094
  });
23904
24095
  }
23905
24096
  }
@@ -23921,7 +24112,7 @@ var init_sni = __esm(() => {
23921
24112
 
23922
24113
  // ../rpx/src/on-demand.ts
23923
24114
  import * as fsp2 from "node:fs/promises";
23924
- import * as path4 from "node:path";
24115
+ import * as path5 from "node:path";
23925
24116
  function matchesAllowedSuffix(host, suffixes) {
23926
24117
  if (!suffixes || suffixes.length === 0)
23927
24118
  return false;
@@ -24072,8 +24263,8 @@ class OnDemandCertManager {
24072
24263
  }
24073
24264
  pathsFor(host) {
24074
24265
  return {
24075
- certPath: path4.join(this.certsDir, `${host}.crt`),
24076
- keyPath: path4.join(this.certsDir, `${host}.key`)
24266
+ certPath: path5.join(this.certsDir, `${host}.crt`),
24267
+ keyPath: path5.join(this.certsDir, `${host}.key`)
24077
24268
  };
24078
24269
  }
24079
24270
  async persist(host, certPem, keyPem) {
@@ -24232,10 +24423,10 @@ var init_port_manager = __esm(() => {
24232
24423
  // ../rpx/src/registry.ts
24233
24424
  import * as fsp3 from "node:fs/promises";
24234
24425
  import { homedir as homedir9 } from "node:os";
24235
- import * as path5 from "node:path";
24426
+ import * as path6 from "node:path";
24236
24427
  import * as process23 from "node:process";
24237
24428
  function getRegistryDir() {
24238
- return path5.join(homedir9(), ".stacks", "rpx", "registry.d");
24429
+ return path6.join(homedir9(), ".stacks", "rpx", "registry.d");
24239
24430
  }
24240
24431
  function isValidId(id) {
24241
24432
  return typeof id === "string" && id.length > 0 && id.length <= 128 && ID_PATTERN.test(id);
@@ -24243,7 +24434,7 @@ function isValidId(id) {
24243
24434
  function entryPath(dir, id) {
24244
24435
  if (!isValidId(id))
24245
24436
  throw new Error(`invalid registry id: ${JSON.stringify(id)}`);
24246
- return path5.join(dir, `${id}.json`);
24437
+ return path6.join(dir, `${id}.json`);
24247
24438
  }
24248
24439
  function isPidAlive(pid2) {
24249
24440
  if (!Number.isInteger(pid2) || pid2 <= 0)
@@ -24260,7 +24451,7 @@ function isValidEntry(value) {
24260
24451
  return false;
24261
24452
  const e = value;
24262
24453
  const pidOk = e.pid === undefined || typeof e.pid === "number" && Number.isInteger(e.pid) && e.pid > 0;
24263
- const hasFrom = typeof e.from === "string" && e.from.length > 0;
24454
+ const hasFrom = typeof e.from === "string" && e.from.length > 0 || Array.isArray(e.from) && e.from.length > 0;
24264
24455
  const hasStatic = typeof e.static === "string" || !!e.static && typeof e.static === "object" && typeof e.static.dir === "string";
24265
24456
  const pathOk = e.path === undefined || typeof e.path === "string";
24266
24457
  return typeof e.id === "string" && isValidId(e.id) && (hasFrom || hasStatic) && typeof e.to === "string" && e.to.length > 0 && pathOk && pidOk && typeof e.createdAt === "string";
@@ -24301,11 +24492,11 @@ var init_registry = __esm(() => {
24301
24492
  });
24302
24493
 
24303
24494
  // ../rpx/src/site-supervisor.ts
24304
- import { closeSync as closeSync6, openSync as openSync6, readFileSync as readFileSync3 } from "node:fs";
24495
+ import { closeSync as closeSync6, openSync as openSync6, readFileSync as readFileSync4 } from "node:fs";
24305
24496
  import * as fsp4 from "node:fs/promises";
24306
24497
  import { spawn } from "node:child_process";
24307
24498
  import { homedir as homedir10 } from "node:os";
24308
- import * as path6 from "node:path";
24499
+ import * as path7 from "node:path";
24309
24500
  import * as process25 from "node:process";
24310
24501
 
24311
24502
  class SiteSupervisor {
@@ -24331,7 +24522,7 @@ class SiteSupervisor {
24331
24522
  constructor(opts) {
24332
24523
  this.resolver = opts.resolver;
24333
24524
  this.registryDir = opts.registryDir ?? getRegistryDir();
24334
- this.rpxDir = opts.rpxDir ?? path6.join(homedir10(), ".stacks", "rpx");
24525
+ this.rpxDir = opts.rpxDir ?? path7.join(homedir10(), ".stacks", "rpx");
24335
24526
  this.verbose = opts.verbose ?? false;
24336
24527
  this.startupTimeoutMs = opts.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
24337
24528
  this.pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
@@ -24386,8 +24577,8 @@ class SiteSupervisor {
24386
24577
  ports.set(route.portEnv, port);
24387
24578
  }
24388
24579
  }
24389
- const logPath = path6.join(this.rpxDir, "sites", `${site.id}.log`);
24390
- await fsp4.mkdir(path6.dirname(logPath), { recursive: true }).catch(() => {});
24580
+ const logPath = path7.join(this.rpxDir, "sites", `${site.id}.log`);
24581
+ await fsp4.mkdir(path7.dirname(logPath), { recursive: true }).catch(() => {});
24391
24582
  const env2 = this.buildEnv(site, ports);
24392
24583
  let handle = null;
24393
24584
  let startError;
@@ -24525,7 +24716,7 @@ class SiteSupervisor {
24525
24716
  }
24526
24717
  readLogTail(state, lines = 40) {
24527
24718
  try {
24528
- const text = readFileSync3(state.logPath, "utf8");
24719
+ const text = readFileSync4(state.logPath, "utf8");
24529
24720
  return text.split(`
24530
24721
  `).slice(-lines).join(`
24531
24722
  `).trim();
@@ -24683,12 +24874,12 @@ var init_site_supervisor = __esm(() => {
24683
24874
  // ../rpx/src/dns-state.ts
24684
24875
  import * as fsp5 from "node:fs/promises";
24685
24876
  import { homedir as homedir11 } from "node:os";
24686
- import * as path7 from "node:path";
24877
+ import * as path8 from "node:path";
24687
24878
  function defaultRpxDir() {
24688
- return path7.join(homedir11(), ".stacks", "rpx");
24879
+ return path8.join(homedir11(), ".stacks", "rpx");
24689
24880
  }
24690
24881
  function getDnsStatePath(rpxDir = defaultRpxDir()) {
24691
- return path7.join(rpxDir, RPX_DNS_STATE_FILE);
24882
+ return path8.join(rpxDir, RPX_DNS_STATE_FILE);
24692
24883
  }
24693
24884
  async function loadDnsState(rpxDir = defaultRpxDir()) {
24694
24885
  try {
@@ -24792,7 +24983,7 @@ __export(exports_dns, {
24792
24983
  });
24793
24984
  import dgram from "node:dgram";
24794
24985
  import * as fsp6 from "node:fs/promises";
24795
- import * as path8 from "node:path";
24986
+ import * as path9 from "node:path";
24796
24987
  import * as process26 from "node:process";
24797
24988
  function parseHeader(buffer) {
24798
24989
  return {
@@ -25027,7 +25218,7 @@ port ${DNS_PORT}
25027
25218
  `;
25028
25219
  }
25029
25220
  function resolverFilePath(basename) {
25030
- return path8.join(MACOS_RESOLVER_DIR, basename);
25221
+ return path9.join(MACOS_RESOLVER_DIR, basename);
25031
25222
  }
25032
25223
  function contentLooksLikeRpxResolver(content) {
25033
25224
  return content.includes("127.0.0.1") && content.includes(String(DNS_PORT));
@@ -25212,13 +25403,13 @@ var init_dns = __esm(() => {
25212
25403
  import { spawn as nodeSpawn } from "node:child_process";
25213
25404
  import * as fsp7 from "node:fs/promises";
25214
25405
  import { homedir as homedir12 } from "node:os";
25215
- import * as path9 from "node:path";
25406
+ import * as path10 from "node:path";
25216
25407
  import * as process27 from "node:process";
25217
25408
  function getDaemonRpxDir() {
25218
- return path9.join(homedir12(), ".stacks", "rpx");
25409
+ return path10.join(homedir12(), ".stacks", "rpx");
25219
25410
  }
25220
25411
  function getDaemonPidPath(rpxDir = getDaemonRpxDir()) {
25221
- return path9.join(rpxDir, "daemon.pid");
25412
+ return path10.join(rpxDir, "daemon.pid");
25222
25413
  }
25223
25414
  async function readDaemonPid(rpxDir = getDaemonRpxDir()) {
25224
25415
  try {
@@ -25238,7 +25429,7 @@ async function releaseDaemonLock(rpxDir = getDaemonRpxDir()) {
25238
25429
  }
25239
25430
  function defaultDaemonSpawnCommand() {
25240
25431
  const exec = process27.execPath;
25241
- const interpName = path9.basename(exec).toLowerCase();
25432
+ const interpName = path10.basename(exec).toLowerCase();
25242
25433
  const isInterpreter = interpName === "bun" || interpName === "node" || interpName.startsWith("bun-");
25243
25434
  if (isInterpreter && process27.argv[1])
25244
25435
  return [exec, process27.argv[1], "daemon:start"];
@@ -25292,10 +25483,12 @@ async function ensureDaemonRunning(opts = {}) {
25292
25483
  throw spawnError;
25293
25484
  throw new Error(`rpx daemon failed to start within ${timeoutMs}ms (rpxDir=${rpxDir})`);
25294
25485
  }
25486
+ var registryUpstreamPools;
25295
25487
  var init_daemon = __esm(() => {
25296
25488
  init_logger();
25297
25489
  init_https();
25298
25490
  init_proxy_handler();
25491
+ init_acme_challenge();
25299
25492
  init_host_routes();
25300
25493
  init_sni();
25301
25494
  init_on_demand();
@@ -25306,6 +25499,7 @@ var init_daemon = __esm(() => {
25306
25499
  init_registry();
25307
25500
  init_dns();
25308
25501
  init_utils();
25502
+ registryUpstreamPools = new Map;
25309
25503
  });
25310
25504
 
25311
25505
  // src/index.ts
@@ -25337,8 +25531,8 @@ init_daemon();
25337
25531
  init_logger();
25338
25532
  init_registry();
25339
25533
  init_utils();
25340
- import * as fs3 from "node:fs";
25341
- import * as path10 from "node:path";
25534
+ import * as fs4 from "node:fs";
25535
+ import * as path11 from "node:path";
25342
25536
  import * as process28 from "node:process";
25343
25537
  function deriveIdFromTarget(to, routePath) {
25344
25538
  const base = routePath && routePath !== "/" ? `${to}${routePath}` : to;
@@ -25373,7 +25567,8 @@ async function runViaDaemon(opts) {
25373
25567
  cleanUrls: p2.cleanUrls,
25374
25568
  changeOrigin: p2.changeOrigin,
25375
25569
  pathRewrites: p2.pathRewrites,
25376
- static: p2.static
25570
+ static: p2.static,
25571
+ loadBalancer: p2.loadBalancer
25377
25572
  }, registryDir, verbose);
25378
25573
  }
25379
25574
  const result = await ensureDaemonRunning({
@@ -25414,7 +25609,7 @@ async function runViaDaemon(opts) {
25414
25609
  return;
25415
25610
  for (const id of idsForCleanup) {
25416
25611
  try {
25417
- fs3.unlinkSync(path10.join(dirForCleanup, `${id}.json`));
25612
+ fs4.unlinkSync(path11.join(dirForCleanup, `${id}.json`));
25418
25613
  } catch {}
25419
25614
  }
25420
25615
  });
@@ -25424,9 +25619,9 @@ async function runViaDaemon(opts) {
25424
25619
  // ../rpx/src/hosts.ts
25425
25620
  init_utils();
25426
25621
  import { exec } from "node:child_process";
25427
- import fs4 from "node:fs";
25622
+ import fs5 from "node:fs";
25428
25623
  import os2 from "node:os";
25429
- import path11 from "node:path";
25624
+ import path12 from "node:path";
25430
25625
  import * as process29 from "node:process";
25431
25626
  import { promisify } from "node:util";
25432
25627
  var execAsync = promisify(exec);
@@ -25434,7 +25629,7 @@ function isLoopbackDevelopmentHost(host) {
25434
25629
  const normalized = host.trim().toLowerCase();
25435
25630
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".localhost.");
25436
25631
  }
25437
- var hostsFilePath = process29.platform === "win32" ? path11.join(process29.env.windir || "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts";
25632
+ var hostsFilePath = process29.platform === "win32" ? path12.join(process29.env.windir || "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts";
25438
25633
  var sudoPrivilegesAcquired = false;
25439
25634
  async function execSudo(command) {
25440
25635
  if (process29.platform === "win32")
@@ -25483,7 +25678,7 @@ async function addHosts(hosts, verbose) {
25483
25678
  try {
25484
25679
  let existingContent;
25485
25680
  try {
25486
- existingContent = await fs4.promises.readFile(hostsFilePath, "utf-8");
25681
+ existingContent = await fs5.promises.readFile(hostsFilePath, "utf-8");
25487
25682
  } catch {
25488
25683
  debugLog("hosts", "Reading hosts file requires elevated permissions, using sudo", verbose);
25489
25684
  try {
@@ -25508,9 +25703,9 @@ async function addHosts(hosts, verbose) {
25508
25703
  127.0.0.1 ${host}
25509
25704
  ::1 ${host}`).join(`
25510
25705
  `);
25511
- const tmpFile = path11.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
25706
+ const tmpFile = path12.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
25512
25707
  try {
25513
- await fs4.promises.writeFile(tmpFile, existingContent + hostEntries, "utf8");
25708
+ await fs5.promises.writeFile(tmpFile, existingContent + hostEntries, "utf8");
25514
25709
  await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`);
25515
25710
  console.log(` Hosts updated: ${newEntries.join(", ")}`);
25516
25711
  } catch (error) {
@@ -25523,7 +25718,7 @@ async function addHosts(hosts, verbose) {
25523
25718
  console.log(` Or run: sudo nano ${hostsFilePath}`);
25524
25719
  } finally {
25525
25720
  try {
25526
- await fs4.promises.unlink(tmpFile);
25721
+ await fs5.promises.unlink(tmpFile);
25527
25722
  } catch {}
25528
25723
  }
25529
25724
  } catch (err) {
@@ -25536,7 +25731,7 @@ async function removeHosts(hosts, verbose) {
25536
25731
  try {
25537
25732
  let content;
25538
25733
  try {
25539
- content = await fs4.promises.readFile(hostsFilePath, "utf-8");
25734
+ content = await fs5.promises.readFile(hostsFilePath, "utf-8");
25540
25735
  } catch {
25541
25736
  debugLog("hosts", "Reading hosts file requires elevated permissions, using sudo", verbose);
25542
25737
  try {
@@ -25570,16 +25765,16 @@ async function removeHosts(hosts, verbose) {
25570
25765
  const newContent = `${filteredLines.join(`
25571
25766
  `)}
25572
25767
  `;
25573
- const tmpFile = path11.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
25768
+ const tmpFile = path12.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
25574
25769
  try {
25575
- await fs4.promises.writeFile(tmpFile, newContent, "utf8");
25770
+ await fs5.promises.writeFile(tmpFile, newContent, "utf8");
25576
25771
  await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`);
25577
25772
  debugLog("hosts", "Hosts removed successfully", verbose);
25578
25773
  } catch (error) {
25579
25774
  debugLog("hosts", "Could not clean up hosts file automatically", verbose);
25580
25775
  } finally {
25581
25776
  try {
25582
- await fs4.promises.unlink(tmpFile);
25777
+ await fs5.promises.unlink(tmpFile);
25583
25778
  } catch (unlinkErr) {
25584
25779
  debugLog("hosts", `Failed to remove temporary file: ${unlinkErr}`, verbose);
25585
25780
  }
@@ -25592,7 +25787,7 @@ async function checkHosts(hosts, verbose) {
25592
25787
  debugLog("hosts", `Checking hosts: ${hosts}`, verbose);
25593
25788
  let content;
25594
25789
  try {
25595
- content = await fs4.promises.readFile(hostsFilePath, "utf-8");
25790
+ content = await fs5.promises.readFile(hostsFilePath, "utf-8");
25596
25791
  } catch (readErr) {
25597
25792
  debugLog("hosts", `Error reading hosts file: ${readErr}`, verbose);
25598
25793
  try {
@@ -25805,25 +26000,7 @@ function createOriginGuard(options) {
25805
26000
 
25806
26001
  // ../rpx/src/start.ts
25807
26002
  init_proxy_handler();
25808
-
25809
- // ../rpx/src/acme-challenge.ts
25810
- import * as fs5 from "node:fs";
25811
- import * as path12 from "node:path";
25812
- var ACME_CHALLENGE_PREFIX2 = "/.well-known/acme-challenge/";
25813
- function readAcmeChallenge(webroot, pathname) {
25814
- if (!webroot || !pathname.startsWith(ACME_CHALLENGE_PREFIX2))
25815
- return null;
25816
- const token = pathname.slice(ACME_CHALLENGE_PREFIX2.length);
25817
- if (!token || !/^[A-Za-z0-9_-]+$/.test(token))
25818
- return null;
25819
- try {
25820
- return fs5.readFileSync(path12.join(webroot, token), "utf8");
25821
- } catch {
25822
- return null;
25823
- }
25824
- }
25825
-
25826
- // ../rpx/src/start.ts
26003
+ init_acme_challenge();
25827
26004
  init_auth();
25828
26005
  init_host_routes();
25829
26006
  init_sni();
@@ -25833,6 +26010,7 @@ var processManager2 = new ProcessManager;
25833
26010
  var version3 = "0.12.0";
25834
26011
  var globalPortManager = new DefaultPortManager("0.0.0.0");
25835
26012
  var activeServers = new Set;
26013
+ var activeUpstreamPools = new Set;
25836
26014
  var isCleaningUp = false;
25837
26015
  var cleanupPromiseResolve = null;
25838
26016
  var cleanupPromise = null;
@@ -25872,6 +26050,9 @@ async function cleanup(options) {
25872
26050
  }));
25873
26051
  cleanupPromises.push(...serverClosePromises);
25874
26052
  activeServers.clear();
26053
+ for (const pool of activeUpstreamPools)
26054
+ stopHealthChecks(pool);
26055
+ activeUpstreamPools.clear();
25875
26056
  if (options?.hosts && options.domains?.length) {
25876
26057
  debugLog("cleanup", "Cleaning up hosts file entries", options?.verbose);
25877
26058
  debugLog("cleanup", `Original domains for cleanup: ${JSON.stringify(options.domains)}`, options?.verbose);
@@ -26029,7 +26210,8 @@ async function testConnection(hostname, port, verbose, retries = 5) {
26029
26210
  }
26030
26211
  async function startServer(options) {
26031
26212
  debugLog("server", `Starting server with options: ${safeStringify(options)}`, options.verbose);
26032
- const fromUrl = new URL((options.from?.startsWith("http") ? options.from : `http://${options.from}`) || "localhost:5173");
26213
+ const primaryFrom = primaryUpstreamUrl(options.from);
26214
+ const fromUrl = new URL(primaryFrom.startsWith("http") ? primaryFrom : `http://${primaryFrom}`);
26033
26215
  const toUrl = new URL((options.to?.startsWith("http") ? options.to : `http://${options.to}`) || "rpx.localhost");
26034
26216
  const fromPort = Number.parseInt(fromUrl.port) || (fromUrl.protocol.includes("https:") ? 443 : 80);
26035
26217
  const hostsToCheck = [toUrl.hostname];
@@ -26110,7 +26292,8 @@ async function startServer(options) {
26110
26292
  debugLog("server", `Setting up reverse proxy with SSL config for ${toUrl.hostname}`, options.verbose);
26111
26293
  await setupProxy({
26112
26294
  ...options,
26113
- from: options.from || "localhost:5173",
26295
+ from: primaryFrom,
26296
+ originalFrom: options.from || primaryFrom,
26114
26297
  to: toUrl.hostname,
26115
26298
  fromPort,
26116
26299
  sourceUrl: {
@@ -26120,12 +26303,15 @@ async function startServer(options) {
26120
26303
  ssl: sslConfig
26121
26304
  });
26122
26305
  }
26123
- async function createProxyServer(from, to, listenPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin, auth) {
26306
+ async function createProxyServer(from, to, listenPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin, auth, originalFrom, loadBalancer) {
26124
26307
  debugLog("proxy", `Creating proxy server ${from} -> ${to} with cleanUrls: ${cleanUrls}`, verbose);
26308
+ const pool = createUpstreamPool(originalFrom ?? sourceUrl.host, loadBalancer);
26309
+ startHealthChecks(pool);
26125
26310
  const routeEntries = [{
26126
26311
  host: to,
26127
26312
  route: {
26128
26313
  sourceHost: sourceUrl.host,
26314
+ upstreamPool: pool,
26129
26315
  cleanUrls: cleanUrls || false,
26130
26316
  changeOrigin: changeOrigin || false,
26131
26317
  basePath: "/",
@@ -26133,8 +26319,11 @@ async function createProxyServer(from, to, listenPort, sourceUrl, ssl, vitePlugi
26133
26319
  }
26134
26320
  }];
26135
26321
  const server = createSharedProxyServer({ routeEntries, listenPort, sslConfig: ssl, originGuard: null, verbose: verbose ?? false });
26136
- if (!server)
26322
+ if (!server) {
26323
+ stopHealthChecks(pool);
26137
26324
  throw new Error(`Failed to start proxy server for ${to} on port ${listenPort}`);
26325
+ }
26326
+ activeUpstreamPools.add(pool);
26138
26327
  logToConsole({
26139
26328
  from,
26140
26329
  to,
@@ -26147,7 +26336,7 @@ async function createProxyServer(from, to, listenPort, sourceUrl, ssl, vitePlugi
26147
26336
  }
26148
26337
  async function setupProxy(options) {
26149
26338
  debugLog("setup", `Setting up reverse proxy: ${safeStringify(options)}`, options.verbose);
26150
- const { from, to, sourceUrl, ssl, verbose, cleanup: cleanupOptions, vitePluginUsage, changeOrigin, cleanUrls } = options;
26339
+ const { from, originalFrom, to, sourceUrl, ssl, verbose, cleanup: cleanupOptions, vitePluginUsage, changeOrigin, cleanUrls } = options;
26151
26340
  const httpPort = 80;
26152
26341
  const httpsPort = 443;
26153
26342
  const hostname = "0.0.0.0";
@@ -26214,7 +26403,7 @@ async function setupProxy(options) {
26214
26403
  portManager2.usedPorts.add(finalPort);
26215
26404
  debugLog("setup", `Using standard ${targetPort === 443 ? "HTTPS" : "HTTP"} port ${targetPort} for ${to}`, verbose);
26216
26405
  }
26217
- await createProxyServer(from, to, finalPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin, resolveAuth(options.auth));
26406
+ await createProxyServer(from, to, finalPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin, resolveAuth(options.auth), originalFrom, options.loadBalancer);
26218
26407
  } catch (err) {
26219
26408
  debugLog("setup", `Setup failed: ${err}`, verbose);
26220
26409
  log.error(`Failed to setup reverse proxy: ${err.message}`);
@@ -26322,7 +26511,8 @@ async function startProxies(options) {
26322
26511
  debugLog("watch", `Starting command for ${proxyId} with command: ${proxy.start.command}`, verbose);
26323
26512
  log.info(`Starting command for ${proxyId}...`);
26324
26513
  await processManager2.startProcess(proxyId, proxy.start, verbose);
26325
- const fromUrl = new URL(proxy.from?.startsWith("http") ? proxy.from : `http://${proxy.from}`);
26514
+ const proxyPrimaryFrom = primaryUpstreamUrl(proxy.from);
26515
+ const fromUrl = new URL(proxyPrimaryFrom.startsWith("http") ? proxyPrimaryFrom : `http://${proxyPrimaryFrom}`);
26326
26516
  const hostname = fromUrl.hostname || "localhost";
26327
26517
  const port = Number(fromUrl.port) || 80;
26328
26518
  try {
@@ -26348,7 +26538,8 @@ async function startProxies(options) {
26348
26538
  debugLog("watch", `Starting command: ${mergedOptions.start.command}`, verbose);
26349
26539
  await processManager2.startProcess(proxyId, mergedOptions.start, verbose);
26350
26540
  }
26351
- const fromUrl = new URL(mergedOptions.from?.startsWith("http") ? mergedOptions.from : `http://${mergedOptions.from}`);
26541
+ const mergedPrimaryFrom = primaryUpstreamUrl(mergedOptions.from);
26542
+ const fromUrl = new URL(mergedPrimaryFrom.startsWith("http") ? mergedPrimaryFrom : `http://${mergedPrimaryFrom}`);
26352
26543
  const hostname = fromUrl.hostname || "localhost";
26353
26544
  const port = Number(fromUrl.port) || 80;
26354
26545
  try {
@@ -26558,12 +26749,17 @@ async function collectRouteEntries(proxyOptions, hostsEnabled, verbose) {
26558
26749
  });
26559
26750
  debugLog("proxies", `Route: ${domain}${routePath ?? ""} → static ${typeof option.static === "string" ? option.static : option.static.dir}${auth ? " (auth)" : ""}`, verbose);
26560
26751
  } else {
26561
- const fromUrl = new URL(option.from?.startsWith("http") ? option.from : `http://${option.from}`);
26752
+ const primaryFrom = primaryUpstreamUrl(option.from);
26753
+ const fromUrl = new URL(primaryFrom.startsWith("http") ? primaryFrom : `http://${primaryFrom}`);
26754
+ const pool = createUpstreamPool(option.from ?? fromUrl.host, option.loadBalancer);
26755
+ startHealthChecks(pool);
26756
+ activeUpstreamPools.add(pool);
26562
26757
  routeEntries.push({
26563
26758
  host: domain,
26564
26759
  path: routePath,
26565
26760
  route: {
26566
26761
  sourceHost: fromUrl.host,
26762
+ upstreamPool: pool,
26567
26763
  cleanUrls,
26568
26764
  changeOrigin: option.changeOrigin || false,
26569
26765
  pathRewrites: option.pathRewrites,
@@ -26655,6 +26851,7 @@ init_dns();
26655
26851
  init_dns_state();
26656
26852
  init_daemon();
26657
26853
  init_proxy_handler();
26854
+ init_acme_challenge();
26658
26855
  init_auth();
26659
26856
  init_host_routes();
26660
26857
  init_static_files();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vite-plugin-rpx",
3
3
  "type": "module",
4
- "version": "0.11.22",
4
+ "version": "0.11.24",
5
5
  "description": "A modern and smart reverse proxy. Vite plugin.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "typecheck": "bunx tsc --noEmit"
48
48
  },
49
49
  "dependencies": {
50
- "@stacksjs/rpx": "0.11.22",
50
+ "@stacksjs/rpx": "0.11.24",
51
51
  "@stacksjs/tlsx": "^0.13.9"
52
52
  },
53
53
  "simple-git-hooks": {