Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: infer hmr ws target by client location #8650

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 62 additions & 10 deletions packages/vite/src/client/client.ts
Expand Up @@ -7,24 +7,70 @@ import '@vite/env'

// injected by the hmr plugin when served
declare const __BASE__: string
declare const __HMR_PROTOCOL__: string
declare const __HMR_HOSTNAME__: string
declare const __HMR_PORT__: string
declare const __HMR_PROTOCOL__: string | null
declare const __HMR_HOSTNAME__: string | null
declare const __HMR_PORT__: string | null
declare const __HMR_DIRECT_TARGET__: string
declare const __HMR_BASE__: string
declare const __HMR_TIMEOUT__: number
declare const __HMR_ENABLE_OVERLAY__: boolean

console.debug('[vite] connecting...')

const importMetaUrl = new URL(import.meta.url)

// use server configuration, then fallback to inference
const socketProtocol =
__HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
const hmrPort = __HMR_PORT__
const socketHost = `${__HMR_HOSTNAME__ || importMetaUrl.hostname}:${
hmrPort || importMetaUrl.port
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: I forgot to handle a case when importMetaUrl.port is ''. But surprisingly this actually works.

new URL("ws://localhost:").port // ''
new WebSocket("ws://localhost:") // connects to `ws://localhost`

}${__HMR_BASE__}`
const directSocketHost = __HMR_DIRECT_TARGET__
const base = __BASE__ || '/'
const messageBuffer: string[] = []

let socket: WebSocket
try {
socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
let fallback: (() => void) | undefined
// only use fallback when port is inferred to prevent confusion
if (!hmrPort) {
fallback = () => {
// fallback to connecting directly to the hmr server
// for servers which does not support proxying websocket
socket = setupWebSocket(socketProtocol, directSocketHost)
socket.addEventListener(
'open',
() => {
console.info(
'[vite] falled back to connect websocket directly. ignore the connection error above.'
)
},
{ once: true }
)
}
}

socket = setupWebSocket(socketProtocol, socketHost, fallback)
} catch (error) {
console.error(`[vite] failed to connect to websocket (${error}). `)
}
sapphi-red marked this conversation as resolved.
Show resolved Hide resolved

function setupWebSocket(
protocol: string,
hostAndPath: string,
onCloseWithoutOpen?: () => void
) {
const socket = new WebSocket(`${protocol}://${hostAndPath}`, 'vite-hmr')
let isOpened = false

socket.addEventListener(
'open',
() => {
isOpened = true
},
{ once: true }
)

// Listen for messages
socket.addEventListener('message', async ({ data }) => {
Expand All @@ -34,12 +80,18 @@ try {
// ping server
socket.addEventListener('close', async ({ wasClean }) => {
if (wasClean) return

if (!isOpened && onCloseWithoutOpen) {
onCloseWithoutOpen()
return
}

console.log(`[vite] server connection lost. polling for restart...`)
await waitForSuccessfulPing()
await waitForSuccessfulPing(hostAndPath)
location.reload()
})
} catch (error) {
console.error(`[vite] failed to connect to websocket (${error}). `)

return socket
}

function warnFailedFetch(err: Error, path: string | string[]) {
Expand Down Expand Up @@ -222,13 +274,13 @@ async function queueUpdate(p: Promise<(() => void) | undefined>) {
}
}

async function waitForSuccessfulPing(ms = 1000) {
async function waitForSuccessfulPing(hostAndPath: string, ms = 1000) {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
// A fetch on a websocket URL will return a successful promise with status 400,
// but will reject a networking error.
await fetch(`${location.protocol}//${socketHost}`)
await fetch(`${location.protocol}//${hostAndPath}`)
break
} catch (e) {
// wait ms before attempting to ping again
Expand Down
43 changes: 24 additions & 19 deletions packages/vite/src/node/plugins/clientInjections.ts
Expand Up @@ -2,7 +2,7 @@ import path from 'path'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { CLIENT_ENTRY, ENV_ENTRY } from '../constants'
import { isObject, normalizePath } from '../utils'
import { isObject, normalizePath, resolveHostname } from '../utils'

// ids in transform are normalized to unix style
const normalizedClientEntry = normalizePath(CLIENT_ENTRY)
Expand All @@ -17,27 +17,30 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
name: 'vite:client-inject',
transform(code, id, options) {
if (id === normalizedClientEntry || id === normalizedEnvEntry) {
let options = config.server.hmr
options = options && typeof options !== 'boolean' ? options : {}
const host = options.host || null
const protocol = options.protocol || null
const timeout = options.timeout || 30000
const overlay = options.overlay !== false
let port: number | string | undefined
if (isObject(config.server.hmr)) {
port = config.server.hmr.clientPort || config.server.hmr.port
}
let hmrConfig = config.server.hmr
hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined
const host = hmrConfig?.host || null
const protocol = hmrConfig?.protocol || null
const timeout = hmrConfig?.timeout || 30000
const overlay = hmrConfig?.overlay !== false

// hmr.clientPort -> hmr.port
// -> (24678 if middleware mode) -> new URL(import.meta.url).port
let port = hmrConfig
? String(hmrConfig.clientPort || hmrConfig.port)
: null
if (config.server.middlewareMode) {
port = String(port || 24678)
} else {
port = String(port || options.port || config.server.port!)
port ||= '24678'
}

let directTarget =
hmrConfig?.host || resolveHostname(config.server.host).name
directTarget += `:${hmrConfig?.port || config.server.port!}`
directTarget += config.base

let hmrBase = config.base
if (options.path) {
hmrBase = path.posix.join(hmrBase, options.path)
}
if (hmrBase !== '/') {
port = path.posix.normalize(`${port}${hmrBase}`)
if (hmrConfig?.path) {
hmrBase = path.posix.join(hmrBase, hmrConfig.path)
}

return code
Expand All @@ -47,6 +50,8 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
.replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol))
.replace(`__HMR_HOSTNAME__`, JSON.stringify(host))
.replace(`__HMR_PORT__`, JSON.stringify(port))
.replace(`__HMR_DIRECT_TARGET__`, JSON.stringify(directTarget))
.replace(`__HMR_BASE__`, JSON.stringify(hmrBase))
.replace(`__HMR_TIMEOUT__`, JSON.stringify(timeout))
.replace(`__HMR_ENABLE_OVERLAY__`, JSON.stringify(overlay))
} else if (!options?.ssr && code.includes('process.env.NODE_ENV')) {
Expand Down