unplugin-https-reverse-proxy 4.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/dist/{chunk-K7HSLE6X.js → chunk-QURDG7SD.js} +19 -0
- package/dist/chunk-RSBXFJ3Z.js +232 -0
- package/dist/esbuild.cjs +243 -0
- package/dist/esbuild.js +2 -1
- package/dist/hmr-sse-bridge.cjs +258 -0
- package/dist/hmr-sse-bridge.d.cts +10 -0
- package/dist/hmr-sse-bridge.d.ts +10 -0
- package/dist/hmr-sse-bridge.js +8 -0
- package/dist/index.cjs +243 -0
- package/dist/index.js +2 -1
- package/dist/nuxt.cjs +131 -0
- package/dist/nuxt.js +2 -1
- package/dist/rollup.cjs +243 -0
- package/dist/rollup.js +2 -1
- package/dist/rspack.cjs +243 -0
- package/dist/rspack.js +2 -1
- package/dist/vite.cjs +243 -0
- package/dist/vite.js +2 -1
- package/dist/webpack.cjs +243 -0
- package/dist/webpack.js +2 -1
- package/package.json +1 -1
- package/src/hmr-sse-bridge.ts +266 -0
- package/src/index.ts +20 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import type { Buffer } from 'node:buffer'
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
|
+
import type { ViteDevServer } from 'vite'
|
|
4
|
+
|
|
5
|
+
const SSE_PATH = '/__hmr_sse'
|
|
6
|
+
const SEND_PATH = '/__hmr_send'
|
|
7
|
+
const MAX_POST_BODY = 64 * 1024 // 64KB limit for client messages
|
|
8
|
+
|
|
9
|
+
export function setupHmrSseBridge(server: ViteDevServer): void {
|
|
10
|
+
const sseClients = new Set<ServerResponse>()
|
|
11
|
+
|
|
12
|
+
// Monkey-patch server.ws.send to also push to SSE clients
|
|
13
|
+
const originalSend = server.ws.send.bind(server.ws)
|
|
14
|
+
server.ws.send = (...args: any[]) => {
|
|
15
|
+
// Call original WebSocket send
|
|
16
|
+
;(originalSend as any)(...args)
|
|
17
|
+
|
|
18
|
+
// Build the payload string same as Vite does internally
|
|
19
|
+
let payload: string
|
|
20
|
+
if (typeof args[0] === 'string') {
|
|
21
|
+
// server.ws.send(event, data) — custom event, wrap it
|
|
22
|
+
payload = JSON.stringify({ type: 'custom', event: args[0], data: args[1] })
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// server.ws.send(payloadObject)
|
|
26
|
+
payload = JSON.stringify(args[0])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Push to all SSE clients (skip errored ones)
|
|
30
|
+
for (const res of sseClients) {
|
|
31
|
+
try {
|
|
32
|
+
res.write(`data: ${payload}\n\n`)
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
sseClients.delete(res)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add middleware for SSE and POST endpoints
|
|
41
|
+
server.middlewares.use((req: IncomingMessage, res: ServerResponse, next: () => void) => {
|
|
42
|
+
const url = req.url?.split('?')[0]
|
|
43
|
+
|
|
44
|
+
if (url === SSE_PATH && req.method === 'GET') {
|
|
45
|
+
handleSseConnection(req, res, sseClients)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (url === SEND_PATH && req.method === 'POST') {
|
|
50
|
+
handleClientMessage(req, res, server)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
next()
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleSseConnection(
|
|
59
|
+
_req: IncomingMessage,
|
|
60
|
+
res: ServerResponse,
|
|
61
|
+
sseClients: Set<ServerResponse>,
|
|
62
|
+
): void {
|
|
63
|
+
res.writeHead(200, {
|
|
64
|
+
'Content-Type': 'text/event-stream',
|
|
65
|
+
'Cache-Control': 'no-cache',
|
|
66
|
+
'Connection': 'keep-alive',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Send initial connected message (same as Vite's WebSocket)
|
|
70
|
+
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`)
|
|
71
|
+
|
|
72
|
+
sseClients.add(res)
|
|
73
|
+
|
|
74
|
+
// Keep-alive ping every 30s
|
|
75
|
+
const keepAlive = setInterval(() => {
|
|
76
|
+
try {
|
|
77
|
+
res.write(': ping\n\n')
|
|
78
|
+
}
|
|
79
|
+
catch { cleanup() }
|
|
80
|
+
}, 30000)
|
|
81
|
+
|
|
82
|
+
function cleanup(): void {
|
|
83
|
+
clearInterval(keepAlive)
|
|
84
|
+
sseClients.delete(res)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
res.on('close', cleanup)
|
|
88
|
+
res.on('error', cleanup)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleClientMessage(
|
|
92
|
+
req: IncomingMessage,
|
|
93
|
+
res: ServerResponse,
|
|
94
|
+
server: ViteDevServer,
|
|
95
|
+
): void {
|
|
96
|
+
let body = ''
|
|
97
|
+
let overflow = false
|
|
98
|
+
req.on('data', (chunk: Buffer) => {
|
|
99
|
+
body += chunk.toString()
|
|
100
|
+
if (body.length > MAX_POST_BODY) {
|
|
101
|
+
overflow = true
|
|
102
|
+
req.destroy()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
req.on('end', () => {
|
|
106
|
+
if (overflow) {
|
|
107
|
+
res.writeHead(413)
|
|
108
|
+
res.end('{"ok":false}')
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const data = JSON.parse(body)
|
|
113
|
+
// Forward custom events to Vite's HMR server
|
|
114
|
+
// Vite client sends: { type: 'custom', event: string, data: any }
|
|
115
|
+
if (data.type === 'custom' && data.event) {
|
|
116
|
+
// Trigger listeners registered via server.ws.on(event, handler)
|
|
117
|
+
// by simulating the internal dispatch with a dummy client
|
|
118
|
+
const dummyClient = {
|
|
119
|
+
send: (...args: any[]) => { server.ws.send(...(args as [any])) },
|
|
120
|
+
}
|
|
121
|
+
// Access internal customListeners via the 'on' registered handlers
|
|
122
|
+
// We re-emit by calling the server's hot channel listener mechanism
|
|
123
|
+
;(server as any).environments?.client?.hot?.api?.emit?.(data.event, data.data, dummyClient)
|
|
124
|
+
}
|
|
125
|
+
res.writeHead(200, {
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
})
|
|
128
|
+
res.end('{"ok":true}')
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
res.writeHead(400)
|
|
132
|
+
res.end('{"ok":false}')
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Client-side script that patches WebSocket to use SSE+fetch on iOS Safari.
|
|
139
|
+
* This script is injected via Vite's transformIndexHtml hook.
|
|
140
|
+
*/
|
|
141
|
+
export const hmrSseBridgeClientScript = `
|
|
142
|
+
(function() {
|
|
143
|
+
// Activate when served over HTTPS to bypass WSS self-signed cert issues
|
|
144
|
+
if (location.protocol !== 'https:') return;
|
|
145
|
+
|
|
146
|
+
var OriginalWebSocket = window.WebSocket;
|
|
147
|
+
|
|
148
|
+
function SseWebSocket(originalUrl) {
|
|
149
|
+
var self = this;
|
|
150
|
+
// WebSocket readyState constants on instance
|
|
151
|
+
self.CONNECTING = 0;
|
|
152
|
+
self.OPEN = 1;
|
|
153
|
+
self.CLOSING = 2;
|
|
154
|
+
self.CLOSED = 3;
|
|
155
|
+
self.readyState = 0;
|
|
156
|
+
self.onopen = null;
|
|
157
|
+
self.onmessage = null;
|
|
158
|
+
self.onclose = null;
|
|
159
|
+
self.onerror = null;
|
|
160
|
+
self.bufferedAmount = 0;
|
|
161
|
+
self.extensions = '';
|
|
162
|
+
self.binaryType = 'blob';
|
|
163
|
+
self.protocol = 'vite-hmr';
|
|
164
|
+
self.url = originalUrl;
|
|
165
|
+
self._listeners = {};
|
|
166
|
+
self._opened = false;
|
|
167
|
+
|
|
168
|
+
var sseUrl = location.origin + '${SSE_PATH}';
|
|
169
|
+
var sendUrl = location.origin + '${SEND_PATH}';
|
|
170
|
+
self._sendUrl = sendUrl;
|
|
171
|
+
|
|
172
|
+
var es = new EventSource(sseUrl);
|
|
173
|
+
self._es = es;
|
|
174
|
+
|
|
175
|
+
es.onopen = function() {
|
|
176
|
+
if (self._opened) return;
|
|
177
|
+
self._opened = true;
|
|
178
|
+
self.readyState = 1;
|
|
179
|
+
var evt = new Event('open');
|
|
180
|
+
if (self.onopen) self.onopen(evt);
|
|
181
|
+
self._emit('open', evt);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
es.onmessage = function(e) {
|
|
185
|
+
var evt = new MessageEvent('message', { data: e.data });
|
|
186
|
+
if (self.onmessage) self.onmessage(evt);
|
|
187
|
+
self._emit('message', evt);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
es.onerror = function() {
|
|
191
|
+
// EventSource auto-reconnects on error; don't treat as fatal close
|
|
192
|
+
// Only emit error event, never close event (SSE handles reconnect internally)
|
|
193
|
+
if (self.readyState >= 2) return;
|
|
194
|
+
// EventSource auto-reconnects; no action needed
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
SseWebSocket.prototype.send = function(data) {
|
|
199
|
+
if (this.readyState !== 1) return;
|
|
200
|
+
fetch(this._sendUrl, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: data
|
|
204
|
+
}).catch(function() {});
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
SseWebSocket.prototype.close = function() {
|
|
208
|
+
if (this.readyState >= 2) return;
|
|
209
|
+
this.readyState = 3;
|
|
210
|
+
if (this._es) this._es.close();
|
|
211
|
+
var evt = new CloseEvent('close', { code: 1000, reason: '', wasClean: true });
|
|
212
|
+
if (this.onclose) this.onclose(evt);
|
|
213
|
+
this._emit('close', evt);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
SseWebSocket.prototype.addEventListener = function(type, fn, options) {
|
|
217
|
+
if (!this._listeners[type]) this._listeners[type] = [];
|
|
218
|
+
var entry = { fn: fn, once: !!(options && options.once) };
|
|
219
|
+
this._listeners[type].push(entry);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
SseWebSocket.prototype.removeEventListener = function(type, fn) {
|
|
223
|
+
var list = this._listeners[type];
|
|
224
|
+
if (!list) return;
|
|
225
|
+
this._listeners[type] = list.filter(function(e) { return e.fn !== fn; });
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
SseWebSocket.prototype.dispatchEvent = function(evt) {
|
|
229
|
+
this._emit(evt.type, evt);
|
|
230
|
+
return true;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
SseWebSocket.prototype._emit = function(type, evt) {
|
|
234
|
+
var list = this._listeners[type];
|
|
235
|
+
if (!list) return;
|
|
236
|
+
var remaining = [];
|
|
237
|
+
list.forEach(function(entry) {
|
|
238
|
+
entry.fn(evt);
|
|
239
|
+
if (!entry.once) remaining.push(entry);
|
|
240
|
+
});
|
|
241
|
+
this._listeners[type] = remaining;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
function PatchedWebSocket(url, protocols) {
|
|
245
|
+
// Only intercept Vite HMR connections (protocol 'vite-hmr')
|
|
246
|
+
var isViteHmr = protocols === 'vite-hmr'
|
|
247
|
+
|| (Array.isArray(protocols) && protocols.indexOf('vite-hmr') !== -1);
|
|
248
|
+
if (!isViteHmr) {
|
|
249
|
+
if (protocols !== undefined) {
|
|
250
|
+
return new OriginalWebSocket(url, protocols);
|
|
251
|
+
}
|
|
252
|
+
return new OriginalWebSocket(url);
|
|
253
|
+
}
|
|
254
|
+
return new SseWebSocket(url);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
PatchedWebSocket.CONNECTING = 0;
|
|
258
|
+
PatchedWebSocket.OPEN = 1;
|
|
259
|
+
PatchedWebSocket.CLOSING = 2;
|
|
260
|
+
PatchedWebSocket.CLOSED = 3;
|
|
261
|
+
PatchedWebSocket.prototype = Object.create(OriginalWebSocket.prototype);
|
|
262
|
+
PatchedWebSocket.prototype.constructor = PatchedWebSocket;
|
|
263
|
+
|
|
264
|
+
window.WebSocket = PatchedWebSocket;
|
|
265
|
+
})();
|
|
266
|
+
`
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,15 @@ import { Table } from 'console-table-printer'
|
|
|
6
6
|
import c from 'picocolors'
|
|
7
7
|
import { createUnplugin } from 'unplugin'
|
|
8
8
|
import { CaddyInstant } from './caddy'
|
|
9
|
+
import { hmrSseBridgeClientScript, setupHmrSseBridge } from './hmr-sse-bridge'
|
|
9
10
|
import { consola, generateQrcode, getIpv4List, isAdmin, once } from './utils'
|
|
10
11
|
|
|
11
12
|
let config: ResolvedConfig
|
|
12
13
|
|
|
13
14
|
let caddy: CaddyInstant
|
|
14
15
|
|
|
16
|
+
let hmrSseBridgeActive = false
|
|
17
|
+
|
|
15
18
|
const cwd = process.cwd()
|
|
16
19
|
|
|
17
20
|
function colorUrl(url: string): string {
|
|
@@ -119,6 +122,23 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
|
|
|
119
122
|
const base = server.config.base || '/'
|
|
120
123
|
|
|
121
124
|
server.printUrls = () => vitePrintUrls(options, source, target, base, _printUrls)
|
|
125
|
+
|
|
126
|
+
// Setup SSE bridge for iOS Safari WSS workaround
|
|
127
|
+
if (options.https) {
|
|
128
|
+
setupHmrSseBridge(server)
|
|
129
|
+
hmrSseBridgeActive = true
|
|
130
|
+
consola.info('HMR SSE bridge enabled for iOS Safari WSS workaround')
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
transformIndexHtml() {
|
|
134
|
+
if (!hmrSseBridgeActive)
|
|
135
|
+
return []
|
|
136
|
+
// Inject client-side script to patch WebSocket on iOS Safari
|
|
137
|
+
return [{
|
|
138
|
+
tag: 'script',
|
|
139
|
+
children: hmrSseBridgeClientScript,
|
|
140
|
+
injectTo: 'head-prepend',
|
|
141
|
+
}]
|
|
122
142
|
},
|
|
123
143
|
},
|
|
124
144
|
webpack(compiler) {
|