From f5c903cce05ecea89b9189a121293e5b72814e8b Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sun, 19 Jun 2022 01:10:49 +0900 Subject: [PATCH 1/7] fix: infer hmr ws target by client location --- packages/vite/src/client/client.ts | 13 +++++++++---- .../vite/src/node/plugins/clientInjections.ts | 16 ++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 8f39164e05a774..3b02cad019e526 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -7,18 +7,23 @@ 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_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 socketHost = `${__HMR_HOSTNAME__ || importMetaUrl.hostname}:${ + __HMR_PORT__ || importMetaUrl.port +}${__HMR_BASE__}` const base = __BASE__ || '/' const messageBuffer: string[] = [] diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 84811dfc10df2a..8c6a06cfb04b04 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -23,22 +23,21 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { const protocol = options.protocol || null const timeout = options.timeout || 30000 const overlay = options.overlay !== false - let port: number | string | undefined + + // hmr.clientPort -> hmr.port + // -> (24678 if middleware mode) -> new URL(import.meta.url).port + let port: string | null = null if (isObject(config.server.hmr)) { - port = config.server.hmr.clientPort || config.server.hmr.port + port = String(config.server.hmr.clientPort || config.server.hmr.port) } if (config.server.middlewareMode) { - port = String(port || 24678) - } else { - port = String(port || options.port || config.server.port!) + port ||= '24678' } + let hmrBase = config.base if (options.path) { hmrBase = path.posix.join(hmrBase, options.path) } - if (hmrBase !== '/') { - port = path.posix.normalize(`${port}${hmrBase}`) - } return code .replace(`__MODE__`, JSON.stringify(config.mode)) @@ -47,6 +46,7 @@ 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_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')) { From a4f1914038682bbef8fef1d5c7405fb84b563add Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sun, 19 Jun 2022 02:50:30 +0900 Subject: [PATCH 2/7] refactor: hmr options --- .../vite/src/node/plugins/clientInjections.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 8c6a06cfb04b04..6665837f102174 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -17,26 +17,25 @@ 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 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: string | null = null - if (isObject(config.server.hmr)) { - port = String(config.server.hmr.clientPort || config.server.hmr.port) - } + let port = hmrConfig + ? String(hmrConfig.clientPort || hmrConfig.port) + : null if (config.server.middlewareMode) { port ||= '24678' } let hmrBase = config.base - if (options.path) { - hmrBase = path.posix.join(hmrBase, options.path) + if (hmrConfig?.path) { + hmrBase = path.posix.join(hmrBase, hmrConfig.path) } return code From 86ee69adc1ae576c46018814bc7af0176f6e19de Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sun, 19 Jun 2022 16:46:11 +0900 Subject: [PATCH 3/7] feat: fallback to directly connect hmr server --- packages/vite/src/client/client.ts | 61 ++++++++++++++++--- .../vite/src/node/plugins/clientInjections.ts | 8 ++- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 3b02cad019e526..4c813172cc4b38 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -10,6 +10,7 @@ declare const __BASE__: 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 @@ -21,15 +22,55 @@ const importMetaUrl = new URL(import.meta.url) // use server configuration, then fallback to inference const socketProtocol = __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws') +const hmrPort = __HMR_PORT__ const socketHost = `${__HMR_HOSTNAME__ || importMetaUrl.hostname}:${ - __HMR_PORT__ || importMetaUrl.port + hmrPort || importMetaUrl.port }${__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}). `) +} + +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 }) => { @@ -39,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[]) { @@ -227,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 diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 6665837f102174..eaac1ca4bd69d7 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -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) @@ -33,6 +33,11 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { 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 (hmrConfig?.path) { hmrBase = path.posix.join(hmrBase, hmrConfig.path) @@ -45,6 +50,7 @@ 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)) From 47952e45108460abe7952b954791eb7b3136af12 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sun, 19 Jun 2022 20:13:38 +0900 Subject: [PATCH 4/7] docs: about server.hmr --- docs/config/server-options.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index cb493c01421ceb..6b14c58be37cba 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -139,10 +139,22 @@ Disable or configure HMR connection (in cases where the HMR websocket must use a Set `server.hmr.overlay` to `false` to disable the server error overlay. -`clientPort` is an advanced option that overrides the port only on the client side, allowing you to serve the websocket on a different port than the client code looks for it on. Useful if you're using an SSL proxy in front of your dev server. +`clientPort` is an advanced option that overrides the port only on the client side, allowing you to serve the websocket on a different port than the client code looks for it on. If specifying `server.hmr.server`, Vite will process HMR connection requests through the provided server. If not in middleware mode, Vite will attempt to process HMR connection requests through the existing server. This can be helpful when using self-signed certificates or when you want to expose Vite over a network on a single port. +::: tip NOTE + +By default configuration, reverse proxies in front of Vite is expected to support proxying WebSocket. Especially in this case, when Vite HMR client fails to connect WebSocket, the client fallbacks to connect WebSocket directly to the Vite HMR server bypassing reverse proxies. +Browser will show an error when the fallback happened but it could be ignored. If you want to completely remove this error, you could either: + +- set `server.strictPort = true` and set `server.hmr.clientPort` to the same value with `server.port` +- set `server.hmr.port` to a different value from `server.port` + +then Vite will bypass reverse proxies from the beginning. + +::: + ## server.watch - **Type:** `object` From 750e95ab43f6139bf423d888bbf29b558769417e Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sun, 19 Jun 2022 20:15:17 +0900 Subject: [PATCH 5/7] chore: add link from console.info --- packages/vite/src/client/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 4c813172cc4b38..5b2f7689f99854 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -43,7 +43,7 @@ try { 'open', () => { console.info( - '[vite] falled back to connect websocket directly. ignore the connection error above.' + '[vite] falled back to connect websocket directly. ignore the connection error above. more details: https://vitejs.dev/config/server-options.html#server-hmr' ) }, { once: true } From 9a0839f56d23f275680fe922ca3bab38881bfa59 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sun, 19 Jun 2022 20:17:03 +0900 Subject: [PATCH 6/7] docs: add line break --- docs/config/server-options.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 6b14c58be37cba..b0b190a65c84ef 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -146,6 +146,7 @@ If specifying `server.hmr.server`, Vite will process HMR connection requests thr ::: tip NOTE By default configuration, reverse proxies in front of Vite is expected to support proxying WebSocket. Especially in this case, when Vite HMR client fails to connect WebSocket, the client fallbacks to connect WebSocket directly to the Vite HMR server bypassing reverse proxies. + Browser will show an error when the fallback happened but it could be ignored. If you want to completely remove this error, you could either: - set `server.strictPort = true` and set `server.hmr.clientPort` to the same value with `server.port` From c800a4552d4b00f1cabba65662ed71c019506b73 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 20 Jun 2022 18:20:11 +0200 Subject: [PATCH 7/7] chore: wording --- docs/config/server-options.md | 12 +++++++----- packages/vite/src/client/client.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 9556ad1753e374..3065fe34ba0caf 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -141,19 +141,21 @@ Set `server.hmr.overlay` to `false` to disable the server error overlay. `clientPort` is an advanced option that overrides the port only on the client side, allowing you to serve the websocket on a different port than the client code looks for it on. -If specifying `server.hmr.server`, Vite will process HMR connection requests through the provided server. If not in middleware mode, Vite will attempt to process HMR connection requests through the existing server. This can be helpful when using self-signed certificates or when you want to expose Vite over a network on a single port. +When `server.hmr.server` is defined, Vite will process the HMR connection requests through the provided server. If not in middleware mode, Vite will attempt to process HMR connection requests through the existing server. This can be helpful when using self-signed certificates or when you want to expose Vite over a network on a single port. ::: tip NOTE -By default configuration, reverse proxies in front of Vite is expected to support proxying WebSocket. Especially in this case, when Vite HMR client fails to connect WebSocket, the client fallbacks to connect WebSocket directly to the Vite HMR server bypassing reverse proxies. +With the default configuration, reverse proxies in front of Vite are expected to support proxying WebSocket. If the Vite HMR client fails to connect WebSocket, the client will fallback to connecting the WebSocket directly to the Vite HMR server bypassing the reverse proxies: -Browser will show an error when the fallback happened but it could be ignored. If you want to completely remove this error, you could either: +``` +Direct websocket connection fallback. Check out https://vitejs.dev/config/server-options.html#server-hmr to remove the previous connection error. +``` + +The error that appears in the Browser when the fallback happens can be ignored. To avoid the error by directly bypassing reverse proxies, you could either: - set `server.strictPort = true` and set `server.hmr.clientPort` to the same value with `server.port` - set `server.hmr.port` to a different value from `server.port` -then Vite will bypass reverse proxies from the beginning. - ::: ## server.watch diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 9246c2b1bfc177..ceccdf345fbce4 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -43,7 +43,7 @@ try { 'open', () => { console.info( - '[vite] falled back to connect websocket directly. ignore the connection error above. more details: https://vitejs.dev/config/server-options.html#server-hmr' + '[vite] Direct websocket connection fallback. Check out https://vitejs.dev/config/server-options.html#server-hmr to remove the previous connection error.' ) }, { once: true }