Skip to content

Commit 5ec3790

Browse files
committed
fix: limit HMR SSE bridge to Safari on macOS and iOS
1 parent b229dd4 commit 5ec3790

3 files changed

Lines changed: 82 additions & 6 deletions

File tree

‎src/hmr-sse-bridge.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,25 @@ function handleClientMessage(
134134
})
135135
}
136136

137+
export function shouldUseHmrSseBridge(protocol: string, userAgent: string): boolean {
138+
if (protocol !== 'https:')
139+
return false
140+
141+
const isApplePlatform = /Macintosh|iPhone|iPad|iPod/.test(userAgent)
142+
const isSafari = /Version\//.test(userAgent)
143+
&& /Safari\//.test(userAgent)
144+
&& !/(?:Chrome|Chromium|CriOS|Edg|EdgiOS|OPR|OPiOS|Firefox|FxiOS)\//.test(userAgent)
145+
146+
return isApplePlatform && isSafari
147+
}
148+
137149
/**
138-
* Client-side script that patches WebSocket to use SSE+fetch on iOS Safari.
150+
* Client-side script that patches WebSocket to use SSE+fetch on macOS/iOS Safari.
139151
* This script is injected via Vite's transformIndexHtml hook.
140152
*/
141153
export const hmrSseBridgeClientScript = `
142154
(function() {
143-
// Activate when served over HTTPS to bypass WSS self-signed cert issues
144-
if (location.protocol !== 'https:') return;
155+
if (!(${shouldUseHmrSseBridge.toString()})(location.protocol, navigator.userAgent)) return;
145156
146157
var OriginalWebSocket = window.WebSocket;
147158

‎src/index.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,17 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
123123

124124
server.printUrls = () => vitePrintUrls(options, source, target, base, _printUrls)
125125

126-
// Setup SSE bridge for iOS Safari WSS workaround
126+
// Setup SSE bridge for macOS/iOS Safari WSS workaround
127127
if (options.https) {
128128
setupHmrSseBridge(server)
129129
hmrSseBridgeActive = true
130-
consola.info('HMR SSE bridge enabled for iOS Safari WSS workaround')
130+
consola.info('HMR SSE bridge enabled for macOS/iOS Safari WSS workaround')
131131
}
132132
},
133133
transformIndexHtml() {
134134
if (!hmrSseBridgeActive)
135135
return []
136-
// Inject client-side script to patch WebSocket on iOS Safari
136+
// Inject client-side script to patch WebSocket on macOS/iOS Safari
137137
return [{
138138
tag: 'script',
139139
children: hmrSseBridgeClientScript,

‎test/hmr-sse-bridge.test.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { runInNewContext } from 'node:vm'
2+
import { describe, expect, it } from 'vitest'
3+
import { hmrSseBridgeClientScript, shouldUseHmrSseBridge } from '../src/hmr-sse-bridge'
4+
5+
const userAgents = {
6+
macosSafari: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15',
7+
iphoneSafari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1',
8+
ipadSafari: 'Mozilla/5.0 (iPad; CPU OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1',
9+
macosChrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
10+
iphoneChrome: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.0.0 Mobile/15E148 Safari/604.1',
11+
macosFirefox: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0',
12+
windowsSafari: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2',
13+
}
14+
15+
describe('shouldUseHmrSseBridge', () => {
16+
it('only enables the bridge for Safari on macOS and iOS over HTTPS', () => {
17+
const result = Object.fromEntries(
18+
Object.entries(userAgents).map(([browser, userAgent]) => [
19+
browser,
20+
shouldUseHmrSseBridge('https:', userAgent),
21+
]),
22+
)
23+
24+
expect(result).toMatchInlineSnapshot(`
25+
{
26+
"ipadSafari": true,
27+
"iphoneChrome": false,
28+
"iphoneSafari": true,
29+
"macosChrome": false,
30+
"macosFirefox": false,
31+
"macosSafari": true,
32+
"windowsSafari": false,
33+
}
34+
`)
35+
})
36+
37+
it('keeps native WebSocket on HTTP', () => {
38+
expect(shouldUseHmrSseBridge('http:', userAgents.macosSafari)).toBe(false)
39+
})
40+
41+
it('uses the same guard in the injected client script', () => {
42+
function isWebSocketPatched(userAgent: string): boolean {
43+
const OriginalWebSocket = function WebSocket() {}
44+
const context = {
45+
location: { protocol: 'https:' },
46+
navigator: { userAgent },
47+
window: { WebSocket: OriginalWebSocket },
48+
}
49+
50+
runInNewContext(hmrSseBridgeClientScript, context)
51+
52+
return context.window.WebSocket !== OriginalWebSocket
53+
}
54+
55+
expect({
56+
macosChrome: isWebSocketPatched(userAgents.macosChrome),
57+
macosSafari: isWebSocketPatched(userAgents.macosSafari),
58+
}).toMatchInlineSnapshot(`
59+
{
60+
"macosChrome": false,
61+
"macosSafari": true,
62+
}
63+
`)
64+
})
65+
})

0 commit comments

Comments
 (0)