unplugin-https-reverse-proxy 3.0.0 → 4.0.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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # unplugin-https-reverse-proxy
2
2
 
3
3
  [![NPM version](https://img.shields.io/npm/v/unplugin-https-reverse-proxy?color=a1b858&label=)](https://www.npmjs.com/package/unplugin-https-reverse-proxy)
4
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/zcf0508/unplugin-https-reverse-proxy)
4
5
 
5
6
  A plugin for https reverse proxy, support `vite`、`webpack` and `rspack`.
6
7
 
@@ -170,6 +171,7 @@ const reverseProxyOptions = {
170
171
  enable: false,
171
172
  target: 'xxx',
172
173
  https: false,
174
+ healthCheck: true,
173
175
  }
174
176
 
175
177
  module.exports = {
@@ -209,7 +211,10 @@ module.exports = {
209
211
  // nuxt.config.js
210
212
  export default defineNuxtConfig({
211
213
  modules: [
212
- ['unplugin-https-reverse-proxy/nuxt', { /* options */ }],
214
+ ['unplugin-https-reverse-proxy/nuxt', {
215
+ /* options */
216
+ healthCheck: false, // important for nuxt
217
+ }],
213
218
  ],
214
219
  })
215
220
  ```
@@ -1,3 +1,7 @@
1
+ import {
2
+ hmrSseBridgeClientScript,
3
+ setupHmrSseBridge
4
+ } from "./chunk-RSBXFJ3Z.js";
1
5
  import {
2
6
  chmodRecursive,
3
7
  consola,
@@ -13391,33 +13395,63 @@ var caddyTemplate = `{
13391
13395
  }
13392
13396
  }
13393
13397
 
13394
- {% for p in proxies %}
13395
- {{ p.target }}{{ p.portSuffix }} {
13396
- {{ p.tls }}
13397
- reverse_proxy http://{{ p.source }} {% if p.healthCheck %} {
13398
- health_uri {{ p.base }}
13399
- health_interval 2s
13400
- health_timeout 5s
13401
- fail_duration 2s
13402
- unhealthy_status 502 503 504 404
13403
- lb_try_duration 3s
13404
- lb_try_interval 300ms
13398
+ {% for host in hosts %}
13399
+ {{ host.host }}{{ host.portSuffix }} {
13400
+ {{ host.tls }}
13401
+
13402
+ {% for route in host.routes %}
13403
+ handle {{ route.base }}* {
13404
+ reverse_proxy http://{{ route.source }} {% if route.healthCheck %} {
13405
+ health_uri {{ route.base }}
13406
+ health_interval 2s
13407
+ health_timeout 5s
13408
+ health_headers {
13409
+ User-Agent {http.request.header.User-Agent}
13410
+ Accept text/html,application/json,*/*
13411
+ }
13412
+ fail_duration 2s
13413
+ unhealthy_status 502 503 504 404
13414
+ lb_try_duration 3s
13415
+ lb_try_interval 300ms
13405
13416
 
13406
- @error status 502 503 504 404
13407
- handle_response @error {
13408
- respond * 503 {
13409
- body "Service Unavailable"
13410
- close
13417
+ @error status 502 503 504 404
13418
+ handle_response @error {
13419
+ respond * 503 {
13420
+ body "Service Unavailable"
13421
+ close
13422
+ }
13411
13423
  }
13412
13424
  }
13425
+ {% endif %}
13413
13426
  }
13414
- {% endif %}
13427
+ {% endfor %}
13415
13428
  }
13416
13429
  {% endfor %}
13417
13430
  `;
13418
13431
  var liquid = new Liquid();
13419
13432
  async function genCaddyfile(ctx) {
13420
- return await liquid.parseAndRender(caddyTemplate, ctx);
13433
+ const hostsMap = /* @__PURE__ */ new Map();
13434
+ for (const proxy of ctx.proxies) {
13435
+ const key = `${proxy.target}${proxy.portSuffix}`;
13436
+ if (!hostsMap.has(key)) {
13437
+ hostsMap.set(key, {
13438
+ host: proxy.target,
13439
+ portSuffix: proxy.portSuffix,
13440
+ tls: proxy.tls,
13441
+ routes: []
13442
+ });
13443
+ }
13444
+ let base = proxy.base || "/";
13445
+ if (!base.startsWith("/"))
13446
+ base = `/${base}`;
13447
+ if (base !== "/" && !base.endsWith("/"))
13448
+ base = `${base}/`;
13449
+ hostsMap.get(key).routes.push({ ...proxy, base });
13450
+ }
13451
+ for (const host of hostsMap.values())
13452
+ host.routes.sort((a, b) => b.base.length - a.base.length);
13453
+ const hosts = Array.from(hostsMap.values());
13454
+ return await liquid.parseAndRender(caddyTemplate, { ...ctx, hosts });
13421
13455
  }
13422
13456
 
13423
13457
  // src/caddy/config.ts
@@ -13582,8 +13616,13 @@ function useCaddyConfig() {
13582
13616
  function writeCaddyConfig(_proxies) {
13583
13617
  const proxies = configJsonRef() || {};
13584
13618
  _proxies.forEach((p) => {
13585
- const index = `${p.target}${p.portSuffix}`;
13586
- proxies[index] = p;
13619
+ let base = p.base || "/";
13620
+ if (!base.startsWith("/"))
13621
+ base = `/${base}`;
13622
+ if (base !== "/" && !base.endsWith("/"))
13623
+ base = `${base}/`;
13624
+ const index = `${p.target}${p.portSuffix}${base}`;
13625
+ proxies[index] = { ...p, base };
13587
13626
  });
13588
13627
  const content = JSON.stringify(proxies, null, 2);
13589
13628
  configContentRef(content);
@@ -13801,13 +13840,22 @@ var CaddyInstant = class {
13801
13840
  throw new Error("update host failed");
13802
13841
  consola.success("update host success\n");
13803
13842
  const targetHost = target.split(":")[0];
13843
+ const normalizeBase = (b) => {
13844
+ let out = b || "/";
13845
+ if (!out.startsWith("/"))
13846
+ out = `/${out}`;
13847
+ if (out !== "/" && !out.endsWith("/"))
13848
+ out = `${out}/`;
13849
+ return out;
13850
+ };
13851
+ const baseNormalized = normalizeBase(base);
13804
13852
  const proxies = [
13805
13853
  {
13806
13854
  target: targetHost,
13807
13855
  portSuffix: https ? "" : ":80",
13808
13856
  tls: https ? "tls internal" : "",
13809
13857
  source,
13810
- base,
13858
+ base: baseNormalized,
13811
13859
  healthCheck
13812
13860
  }
13813
13861
  ];
@@ -13817,14 +13865,12 @@ var CaddyInstant = class {
13817
13865
  effect(async () => {
13818
13866
  const { configJsonRef } = getCaddyConfig();
13819
13867
  const proxies2 = Object.values(configJsonRef() || {});
13820
- if (!(this.caddyfile || "").includes(`${target.split(":")[0]}${https ? "" : ":80"}`)) {
13821
- const ctx = validateTemplateContext({
13822
- caddy_root: this.caddyRoot,
13823
- proxies: proxies2
13824
- });
13825
- this.caddyfile = await genCaddyfile(ctx);
13826
- await this.writeCaddyfile();
13827
- }
13868
+ const ctx = validateTemplateContext({
13869
+ caddy_root: this.caddyRoot,
13870
+ proxies: proxies2
13871
+ });
13872
+ this.caddyfile = await genCaddyfile(ctx);
13873
+ await this.writeCaddyfile();
13828
13874
  })
13829
13875
  );
13830
13876
  if (!existsSync3(caddyLockFilePath))
@@ -13963,6 +14009,7 @@ ${line}
13963
14009
  // src/index.ts
13964
14010
  var config;
13965
14011
  var caddy;
14012
+ var hmrSseBridgeActive = false;
13966
14013
  var cwd = process5.cwd();
13967
14014
  function colorUrl(url2) {
13968
14015
  return c.green(url2.replace(/:(\d+)\//, (_, port) => `:${c.bold(port)}/`));
@@ -14049,10 +14096,24 @@ var unpluginFactory = (options) => ({
14049
14096
  }
14050
14097
  const base = server.config.base || "/";
14051
14098
  server.printUrls = () => vitePrintUrls(options, source, target, base, _printUrls);
14099
+ if (options.https) {
14100
+ setupHmrSseBridge(server);
14101
+ hmrSseBridgeActive = true;
14102
+ consola.info("HMR SSE bridge enabled for iOS Safari WSS workaround");
14103
+ }
14104
+ },
14105
+ transformIndexHtml() {
14106
+ if (!hmrSseBridgeActive)
14107
+ return [];
14108
+ return [{
14109
+ tag: "script",
14110
+ children: hmrSseBridgeClientScript,
14111
+ injectTo: "head-prepend"
14112
+ }];
14052
14113
  }
14053
14114
  },
14054
14115
  webpack(compiler) {
14055
- var _a4, _b3;
14116
+ var _a4, _b3, _c2;
14056
14117
  if (process5.env.NODE_ENV !== "development")
14057
14118
  return;
14058
14119
  const {
@@ -14083,7 +14144,16 @@ var unpluginFactory = (options) => ({
14083
14144
  const source = `${devServer.host}:${devServer.port}`;
14084
14145
  if (caddy)
14085
14146
  return;
14086
- const _close = (_b3 = compiler.close) == null ? void 0 : _b3.bind(compiler);
14147
+ let base = "/";
14148
+ const publicPath = (_b3 = compiler.options.output) == null ? void 0 : _b3.publicPath;
14149
+ if (typeof publicPath === "string" && publicPath) {
14150
+ base = publicPath;
14151
+ }
14152
+ if (!base.startsWith("/"))
14153
+ base = `/${base}`;
14154
+ if (base !== "/" && !base.endsWith("/"))
14155
+ base = `${base}/`;
14156
+ const _close = (_c2 = compiler.close) == null ? void 0 : _c2.bind(compiler);
14087
14157
  if (_close) {
14088
14158
  compiler.close = async (callback) => {
14089
14159
  process5.on("exit", () => {
@@ -14095,18 +14165,19 @@ var unpluginFactory = (options) => ({
14095
14165
  }
14096
14166
  caddy = new CaddyInstant();
14097
14167
  caddy.run(source, target, {
14168
+ base,
14098
14169
  ...options
14099
14170
  }).then(() => {
14100
14171
  compiler.hooks.done.tap("unplugin-https-reverse-proxy", async () => {
14101
14172
  await printForwardProxy();
14102
- consola.success(` ${c.green("\u279C")} ${c.bold("run caddy reverse proxy success")}: ${colorUrl(`${options.https ? "https" : "http"}://${target}`)}`);
14173
+ consola.success(` ${c.green("\u279C")} ${c.bold("run caddy reverse proxy success")}: ${colorUrl(`${options.https ? "https" : "http"}://${target}${base}`)}`);
14103
14174
  });
14104
14175
  }).catch((e) => {
14105
14176
  throw e;
14106
14177
  });
14107
14178
  },
14108
14179
  async rspack(compiler) {
14109
- var _a4, _b3, _c2;
14180
+ var _a4, _b3, _c2, _d2;
14110
14181
  if (process5.env.NODE_ENV !== "development")
14111
14182
  return;
14112
14183
  const {
@@ -14139,11 +14210,21 @@ var unpluginFactory = (options) => ({
14139
14210
  if (caddy)
14140
14211
  return;
14141
14212
  caddy = new CaddyInstant();
14213
+ let base = "/";
14214
+ const publicPath = (_d2 = compiler.options.output) == null ? void 0 : _d2.publicPath;
14215
+ if (typeof publicPath === "string" && publicPath) {
14216
+ base = publicPath;
14217
+ }
14218
+ if (!base.startsWith("/"))
14219
+ base = `/${base}`;
14220
+ if (base !== "/" && !base.endsWith("/"))
14221
+ base = `${base}/`;
14142
14222
  caddy.run(source, target, {
14223
+ base,
14143
14224
  ...options
14144
14225
  }).then(async () => {
14145
14226
  await printForwardProxy();
14146
- consola.success(` ${c.green("\u279C")} ${c.bold("run caddy reverse proxy success")}: ${colorUrl(`${options.https ? "https" : "http"}://${target}`)}`);
14227
+ consola.success(` ${c.green("\u279C")} ${c.bold("run caddy reverse proxy success")}: ${colorUrl(`${options.https ? "https" : "http"}://${target}${base}`)}`);
14147
14228
  }).catch((e) => {
14148
14229
  throw e;
14149
14230
  });
@@ -0,0 +1,232 @@
1
+ // src/hmr-sse-bridge.ts
2
+ var SSE_PATH = "/__hmr_sse";
3
+ var SEND_PATH = "/__hmr_send";
4
+ var MAX_POST_BODY = 64 * 1024;
5
+ function setupHmrSseBridge(server) {
6
+ const sseClients = /* @__PURE__ */ new Set();
7
+ const originalSend = server.ws.send.bind(server.ws);
8
+ server.ws.send = (...args) => {
9
+ ;
10
+ originalSend(...args);
11
+ let payload;
12
+ if (typeof args[0] === "string") {
13
+ payload = JSON.stringify({ type: "custom", event: args[0], data: args[1] });
14
+ } else {
15
+ payload = JSON.stringify(args[0]);
16
+ }
17
+ for (const res of sseClients) {
18
+ try {
19
+ res.write(`data: ${payload}
20
+
21
+ `);
22
+ } catch {
23
+ sseClients.delete(res);
24
+ }
25
+ }
26
+ };
27
+ server.middlewares.use((req, res, next) => {
28
+ var _a;
29
+ const url = (_a = req.url) == null ? void 0 : _a.split("?")[0];
30
+ if (url === SSE_PATH && req.method === "GET") {
31
+ handleSseConnection(req, res, sseClients);
32
+ return;
33
+ }
34
+ if (url === SEND_PATH && req.method === "POST") {
35
+ handleClientMessage(req, res, server);
36
+ return;
37
+ }
38
+ next();
39
+ });
40
+ }
41
+ function handleSseConnection(_req, res, sseClients) {
42
+ res.writeHead(200, {
43
+ "Content-Type": "text/event-stream",
44
+ "Cache-Control": "no-cache",
45
+ "Connection": "keep-alive"
46
+ });
47
+ res.write(`data: ${JSON.stringify({ type: "connected" })}
48
+
49
+ `);
50
+ sseClients.add(res);
51
+ const keepAlive = setInterval(() => {
52
+ try {
53
+ res.write(": ping\n\n");
54
+ } catch {
55
+ cleanup();
56
+ }
57
+ }, 3e4);
58
+ function cleanup() {
59
+ clearInterval(keepAlive);
60
+ sseClients.delete(res);
61
+ }
62
+ res.on("close", cleanup);
63
+ res.on("error", cleanup);
64
+ }
65
+ function handleClientMessage(req, res, server) {
66
+ let body = "";
67
+ let overflow = false;
68
+ req.on("data", (chunk) => {
69
+ body += chunk.toString();
70
+ if (body.length > MAX_POST_BODY) {
71
+ overflow = true;
72
+ req.destroy();
73
+ }
74
+ });
75
+ req.on("end", () => {
76
+ var _a, _b, _c, _d, _e;
77
+ if (overflow) {
78
+ res.writeHead(413);
79
+ res.end('{"ok":false}');
80
+ return;
81
+ }
82
+ try {
83
+ const data = JSON.parse(body);
84
+ if (data.type === "custom" && data.event) {
85
+ const dummyClient = {
86
+ send: (...args) => {
87
+ server.ws.send(...args);
88
+ }
89
+ };
90
+ (_e = (_d = (_c = (_b = (_a = server.environments) == null ? void 0 : _a.client) == null ? void 0 : _b.hot) == null ? void 0 : _c.api) == null ? void 0 : _d.emit) == null ? void 0 : _e.call(_d, data.event, data.data, dummyClient);
91
+ }
92
+ res.writeHead(200, {
93
+ "Content-Type": "application/json"
94
+ });
95
+ res.end('{"ok":true}');
96
+ } catch {
97
+ res.writeHead(400);
98
+ res.end('{"ok":false}');
99
+ }
100
+ });
101
+ }
102
+ var hmrSseBridgeClientScript = `
103
+ (function() {
104
+ // Activate when served over HTTPS to bypass WSS self-signed cert issues
105
+ if (location.protocol !== 'https:') return;
106
+
107
+ var OriginalWebSocket = window.WebSocket;
108
+
109
+ function SseWebSocket(originalUrl) {
110
+ var self = this;
111
+ // WebSocket readyState constants on instance
112
+ self.CONNECTING = 0;
113
+ self.OPEN = 1;
114
+ self.CLOSING = 2;
115
+ self.CLOSED = 3;
116
+ self.readyState = 0;
117
+ self.onopen = null;
118
+ self.onmessage = null;
119
+ self.onclose = null;
120
+ self.onerror = null;
121
+ self.bufferedAmount = 0;
122
+ self.extensions = '';
123
+ self.binaryType = 'blob';
124
+ self.protocol = 'vite-hmr';
125
+ self.url = originalUrl;
126
+ self._listeners = {};
127
+ self._opened = false;
128
+
129
+ var sseUrl = location.origin + '${SSE_PATH}';
130
+ var sendUrl = location.origin + '${SEND_PATH}';
131
+ self._sendUrl = sendUrl;
132
+
133
+ var es = new EventSource(sseUrl);
134
+ self._es = es;
135
+
136
+ es.onopen = function() {
137
+ if (self._opened) return;
138
+ self._opened = true;
139
+ self.readyState = 1;
140
+ var evt = new Event('open');
141
+ if (self.onopen) self.onopen(evt);
142
+ self._emit('open', evt);
143
+ };
144
+
145
+ es.onmessage = function(e) {
146
+ var evt = new MessageEvent('message', { data: e.data });
147
+ if (self.onmessage) self.onmessage(evt);
148
+ self._emit('message', evt);
149
+ };
150
+
151
+ es.onerror = function() {
152
+ // EventSource auto-reconnects on error; don't treat as fatal close
153
+ // Only emit error event, never close event (SSE handles reconnect internally)
154
+ if (self.readyState >= 2) return;
155
+ // EventSource auto-reconnects; no action needed
156
+ };
157
+ }
158
+
159
+ SseWebSocket.prototype.send = function(data) {
160
+ if (this.readyState !== 1) return;
161
+ fetch(this._sendUrl, {
162
+ method: 'POST',
163
+ headers: { 'Content-Type': 'application/json' },
164
+ body: data
165
+ }).catch(function() {});
166
+ };
167
+
168
+ SseWebSocket.prototype.close = function() {
169
+ if (this.readyState >= 2) return;
170
+ this.readyState = 3;
171
+ if (this._es) this._es.close();
172
+ var evt = new CloseEvent('close', { code: 1000, reason: '', wasClean: true });
173
+ if (this.onclose) this.onclose(evt);
174
+ this._emit('close', evt);
175
+ };
176
+
177
+ SseWebSocket.prototype.addEventListener = function(type, fn, options) {
178
+ if (!this._listeners[type]) this._listeners[type] = [];
179
+ var entry = { fn: fn, once: !!(options && options.once) };
180
+ this._listeners[type].push(entry);
181
+ };
182
+
183
+ SseWebSocket.prototype.removeEventListener = function(type, fn) {
184
+ var list = this._listeners[type];
185
+ if (!list) return;
186
+ this._listeners[type] = list.filter(function(e) { return e.fn !== fn; });
187
+ };
188
+
189
+ SseWebSocket.prototype.dispatchEvent = function(evt) {
190
+ this._emit(evt.type, evt);
191
+ return true;
192
+ };
193
+
194
+ SseWebSocket.prototype._emit = function(type, evt) {
195
+ var list = this._listeners[type];
196
+ if (!list) return;
197
+ var remaining = [];
198
+ list.forEach(function(entry) {
199
+ entry.fn(evt);
200
+ if (!entry.once) remaining.push(entry);
201
+ });
202
+ this._listeners[type] = remaining;
203
+ };
204
+
205
+ function PatchedWebSocket(url, protocols) {
206
+ // Only intercept Vite HMR connections (protocol 'vite-hmr')
207
+ var isViteHmr = protocols === 'vite-hmr'
208
+ || (Array.isArray(protocols) && protocols.indexOf('vite-hmr') !== -1);
209
+ if (!isViteHmr) {
210
+ if (protocols !== undefined) {
211
+ return new OriginalWebSocket(url, protocols);
212
+ }
213
+ return new OriginalWebSocket(url);
214
+ }
215
+ return new SseWebSocket(url);
216
+ }
217
+
218
+ PatchedWebSocket.CONNECTING = 0;
219
+ PatchedWebSocket.OPEN = 1;
220
+ PatchedWebSocket.CLOSING = 2;
221
+ PatchedWebSocket.CLOSED = 3;
222
+ PatchedWebSocket.prototype = Object.create(OriginalWebSocket.prototype);
223
+ PatchedWebSocket.prototype.constructor = PatchedWebSocket;
224
+
225
+ window.WebSocket = PatchedWebSocket;
226
+ })();
227
+ `;
228
+
229
+ export {
230
+ setupHmrSseBridge,
231
+ hmrSseBridgeClientScript
232
+ };