From c94499a8b693e1e30d136f311e92f44b1f3577dc Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 13 Dec 2023 14:42:56 +0100 Subject: [PATCH 01/11] feat: support multiple HMR clients on the server --- docs/guide/api-plugin.md | 2 + packages/vite/src/node/optimizer/optimizer.ts | 2 +- packages/vite/src/node/plugins/esbuild.ts | 2 +- packages/vite/src/node/server/hmr.ts | 79 ++++++++++++++++--- packages/vite/src/node/server/index.ts | 16 +++- .../vite/src/node/server/middlewares/error.ts | 2 +- packages/vite/src/node/server/ws.ts | 11 +-- 7 files changed, 89 insertions(+), 25 deletions(-) diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index d2f3b6ff8e25c9..c89f1e236baeaa 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -534,6 +534,8 @@ Since Vite 2.9, we provide some utilities for plugins to help handle the communi ### Server to Client + + On the plugin side, we could use `server.ws.send` to broadcast events to all the clients: ```js diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index f4a278bd54bcfd..420b4951200a91 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -487,7 +487,7 @@ async function createDepsOptimizer( // reloaded. server.moduleGraph.invalidateAll() - server.ws.send({ + server.hot.send({ type: 'full-reload', path: '*', }) diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 6589756f37f5a6..ea0c1604beac4a 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -491,7 +491,7 @@ async function reloadOnTsconfigChange(changedFile: string) { // server may not be available if vite config is updated at the same time if (server) { // force full reload - server.ws.send({ + server.hot.send({ type: 'full-reload', path: '*', }) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index ec95dcdd5d6e0b..c775e169e60185 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -2,7 +2,7 @@ import fsp from 'node:fs/promises' import path from 'node:path' import type { Server } from 'node:http' import colors from 'picocolors' -import type { Update } from 'types/hmrPayload' +import type { HMRPayload, Update } from 'types/hmrPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' import { @@ -12,7 +12,7 @@ import { withTrailingSlash, wrapId, } from '../utils' -import type { ViteDevServer } from '..' +import type { InferCustomEventPayload, ViteDevServer } from '..' import { isCSSRequest } from '../plugins/css' import { getAffectedGlobModules } from '../plugins/importMetaGlob' import { isExplicitImportRequired } from '../plugins/importAnalysis' @@ -51,6 +51,38 @@ interface PropagationBoundary { isWithinCircularImport: boolean } +export interface HMRChannel { + /** + * Broadcast events to all clients + */ + send(payload: HMRPayload): void + /** + * Send custom event + */ + send(event: T, payload?: InferCustomEventPayload): void + /** + * Handle custom event emitted by `import.meta.hot.send` + */ + on( + event: T, + listener: (data: InferCustomEventPayload, ...args: any[]) => void, + ): void + /** + * Unregister event listener. + */ + off(event: string, listener: Function): void + /** + * Called when server is closed. + */ + close(): void +} + +// TODO: more jsdoc +export interface HMRBroadcaster extends HMRChannel { + channels: HMRChannel[] + addChannel(connection: HMRChannel): void +} + export function getShortName(file: string, root: string): string { return file.startsWith(withTrailingSlash(root)) ? path.posix.relative(root, file) @@ -62,7 +94,7 @@ export async function handleHMRUpdate( server: ViteDevServer, configOnly: boolean, ): Promise { - const { ws, config, moduleGraph } = server + const { config, hot, moduleGraph } = server const shortFile = getShortName(file, config.root) const fileName = path.basename(file) @@ -100,7 +132,7 @@ export async function handleHMRUpdate( // (dev only) the client itself cannot be hot updated. if (file.startsWith(withTrailingSlash(normalizedClientDir))) { - ws.send({ + hot.send({ type: 'full-reload', path: '*', }) @@ -133,7 +165,7 @@ export async function handleHMRUpdate( clear: true, timestamp: true, }) - ws.send({ + hot.send({ type: 'full-reload', path: config.server.middlewareMode ? '*' @@ -155,7 +187,7 @@ export function updateModules( file: string, modules: ModuleNode[], timestamp: number, - { config, ws, moduleGraph }: ViteDevServer, + { config, hot, moduleGraph }: ViteDevServer, afterInvalidation?: boolean, ): void { const updates: Update[] = [] @@ -204,7 +236,7 @@ export function updateModules( colors.green(`page reload `) + colors.dim(file) + reason, { clear: !afterInvalidation, timestamp: true }, ) - ws.send({ + hot.send({ type: 'full-reload', }) return @@ -220,7 +252,7 @@ export function updateModules( colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), { clear: !afterInvalidation, timestamp: true }, ) - ws.send({ + hot.send({ type: 'update', updates, }) @@ -455,7 +487,7 @@ function isNodeWithinCircularImports( export function handlePrunedModules( mods: Set, - { ws }: ViteDevServer, + { hot }: ViteDevServer, ): void { // update the disposed modules' hmr timestamp // since if it's re-imported, it should re-apply side effects @@ -465,7 +497,7 @@ export function handlePrunedModules( mod.lastHMRTimestamp = t debugHmr?.(`[dispose] ${colors.dim(mod.file)}`) }) - ws.send({ + hot.send({ type: 'prune', paths: [...mods].map((m) => m.url), }) @@ -644,3 +676,30 @@ async function readModifiedFile(file: string): Promise { return content } } + +export function createHMRBroadcaster(): HMRBroadcaster { + const channels: HMRChannel[] = [] + const broadcaster: HMRBroadcaster = { + get channels() { + return channels + }, + addChannel(connection) { + channels.push(connection) + }, + on(event, listener) { + channels.forEach((connection) => connection.on(event, listener)) + return broadcaster + }, + off(event, listener) { + channels.forEach((connection) => connection.off(event, listener)) + return broadcaster + }, + send(...args: any[]) { + channels.forEach((connection) => connection.send(...(args as [any]))) + }, + close() { + return channels.map((connection) => connection.close()) + }, + } + return broadcaster +} diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index b73ecfa07a2517..3b27544eebf8ca 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -74,8 +74,9 @@ import type { ModuleNode } from './moduleGraph' import { ModuleGraph } from './moduleGraph' import { notFoundMiddleware } from './middlewares/notFound' import { errorMiddleware, prepareError } from './middlewares/error' -import type { HmrOptions } from './hmr' +import type { HMRBroadcaster, HmrOptions } from './hmr' import { + createHMRBroadcaster, getShortName, handleFileAddUnlink, handleHMRUpdate, @@ -322,6 +323,11 @@ export interface ViteDevServer { */ restart(forceOptimize?: boolean): Promise + /** + * TODO: docs + */ + hot: HMRBroadcaster + /** * Open browser */ @@ -403,7 +409,10 @@ export async function _createServer( const httpServer = middlewareMode ? null : await resolveHttpServer(serverConfig, middlewares, httpsOptions) + + const hot = createHMRBroadcaster() const ws = createWebSocketServer(httpServer, config, httpsOptions) + hot.addChannel(ws) if (httpServer) { setClientErrorHandler(httpServer, config.logger) @@ -437,6 +446,7 @@ export async function _createServer( watcher, pluginContainer: container, ws, + hot, moduleGraph, resolvedUrls: null, // will be set on listen ssrTransform( @@ -558,7 +568,7 @@ export async function _createServer( } await Promise.allSettled([ watcher.close(), - ws.close(), + hot.close(), container.close(), getDepsOptimizer(server.config)?.close(), getDepsOptimizer(server.config, true)?.close(), @@ -642,7 +652,7 @@ export async function _createServer( try { await handleHMRUpdate(file, server, configOnly) } catch (err) { - ws.send({ + hot.send({ type: 'error', err: prepareError(err), }) diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index 966d1663749ba2..1d67f1aa55e4ed 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -51,7 +51,7 @@ export function logError(server: ViteDevServer, err: RollupError): void { error: err, }) - server.ws.send({ + server.hot.send({ type: 'error', err: prepareError(err), }) diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index d9a67e4934c42f..05b40e0f2ce70a 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -13,6 +13,7 @@ import type { CustomPayload, ErrorPayload, HMRPayload } from 'types/hmrPayload' import type { InferCustomEventPayload } from 'types/customEvent' import type { ResolvedConfig } from '..' import { isObject } from '../utils' +import type { HMRChannel } from './hmr' import type { HttpServer } from '.' /* In Bun, the `ws` module is overridden to hook into the native code. Using the bundled `js` version @@ -30,7 +31,7 @@ export type WebSocketCustomListener = ( client: WebSocketClient, ) => void -export interface WebSocketServer { +export interface WebSocketServer extends HMRChannel { /** * Listen on port and host */ @@ -39,14 +40,6 @@ export interface WebSocketServer { * Get all connected clients. */ clients: Set - /** - * Broadcast events to all clients - */ - send(payload: HMRPayload): void - /** - * Send custom event - */ - send(event: T, payload?: InferCustomEventPayload): void /** * Disconnect all clients and terminate the server. */ From d744019f45e2e479a64e5d1e40d2fe8837bd5e39 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 29 Dec 2023 10:53:20 +0100 Subject: [PATCH 02/11] refactor: cleanup --- packages/vite/src/node/server/hmr.ts | 42 +++++++++++++++++++------- packages/vite/src/node/server/index.ts | 14 +++++---- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index c775e169e60185..d5e2774e7e9638 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -2,7 +2,7 @@ import fsp from 'node:fs/promises' import path from 'node:path' import type { Server } from 'node:http' import colors from 'picocolors' -import type { HMRPayload, Update } from 'types/hmrPayload' +import type { CustomPayload, HMRPayload, Update } from 'types/hmrPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' import { @@ -51,6 +51,17 @@ interface PropagationBoundary { isWithinCircularImport: boolean } +export interface HMRBroadcasterClient { + /** + * Send event to the client + */ + send(payload: HMRPayload): void + /** + * Send custom event + */ + send(event: string, payload?: CustomPayload['data']): void +} + export interface HMRChannel { /** * Broadcast events to all clients @@ -65,7 +76,11 @@ export interface HMRChannel { */ on( event: T, - listener: (data: InferCustomEventPayload, ...args: any[]) => void, + listener: ( + data: InferCustomEventPayload, + client: HMRBroadcasterClient, + ...args: any[] + ) => void, ): void /** * Unregister event listener. @@ -77,9 +92,14 @@ export interface HMRChannel { close(): void } -// TODO: more jsdoc export interface HMRBroadcaster extends HMRChannel { - channels: HMRChannel[] + /** + * All registered channels. Always has websocket channel. + */ + readonly channels: HMRChannel[] + /** + * Add a new third-party channel. + */ addChannel(connection: HMRChannel): void } @@ -681,24 +701,24 @@ export function createHMRBroadcaster(): HMRBroadcaster { const channels: HMRChannel[] = [] const broadcaster: HMRBroadcaster = { get channels() { - return channels + return [...channels] }, - addChannel(connection) { - channels.push(connection) + addChannel(channel) { + channels.push(channel) }, on(event, listener) { - channels.forEach((connection) => connection.on(event, listener)) + channels.forEach((channel) => channel.on(event, listener)) return broadcaster }, off(event, listener) { - channels.forEach((connection) => connection.off(event, listener)) + channels.forEach((channel) => channel.off(event, listener)) return broadcaster }, send(...args: any[]) { - channels.forEach((connection) => connection.send(...(args as [any]))) + channels.forEach((channel) => channel.send(...(args as [any]))) }, close() { - return channels.map((connection) => connection.close()) + return channels.map((channel) => channel.close()) }, } return broadcaster diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 3b27544eebf8ca..d4f402d076d0da 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -233,8 +233,16 @@ export interface ViteDevServer { watcher: FSWatcher /** * web socket server with `send(payload)` method + * @deprecated use `hot` instead */ ws: WebSocketServer + /** + * HMR broadcaster that can be used to send custom HMR messages to the client + * + * Always sends a message to at least a WebSocket client. Any third party can + * add a channel to the broadcaster to process messages so be aware of that + */ + hot: HMRBroadcaster /** * Rollup plugin container that can run plugin hooks on a given file */ @@ -322,12 +330,6 @@ export interface ViteDevServer { * @param forceOptimize - force the optimizer to re-bundle, same as --force cli flag */ restart(forceOptimize?: boolean): Promise - - /** - * TODO: docs - */ - hot: HMRBroadcaster - /** * Open browser */ From 784052162bfb51ad251f21edc01efd27a4cb0876 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 29 Dec 2023 11:01:33 +0100 Subject: [PATCH 03/11] chore: require "connection" event, use rserver.hot instead of server.ws --- docs/guide/api-plugin.md | 16 +++++++--------- packages/vite/src/node/plugin.ts | 2 +- packages/vite/src/node/server/hmr.ts | 3 ++- packages/vite/src/node/server/index.ts | 3 +-- playground/hmr/vite.config.ts | 8 ++++---- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index c89f1e236baeaa..e8423f6092264d 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -423,11 +423,11 @@ Vite plugins can also provide hooks that serve Vite-specific purposes. These hoo - Filter and narrow down the affected module list so that the HMR is more accurate. - - Return an empty array and perform complete custom HMR handling by sending custom events to the client: + - Return an empty array and perform complete custom HMR handling by sending custom events to the client (example uses `server.hot` which was introduced in Vite #, it is recommended to also use `server.ws` if you support lower versions): ```js handleHotUpdate({ server }) { - server.ws.send({ + server.hot.send({ type: 'custom', event: 'special-update', data: {} @@ -534,9 +534,7 @@ Since Vite 2.9, we provide some utilities for plugins to help handle the communi ### Server to Client - - -On the plugin side, we could use `server.ws.send` to broadcast events to all the clients: +On the plugin side, we could use `server.hot.send` (since Vite #) or `server.ws.send` to broadcast events to all the clients: ```js // vite.config.js @@ -546,8 +544,8 @@ export default defineConfig({ // ... configureServer(server) { // Example: wait for a client to connect before sending a message - server.ws.on('connection', () => { - server.ws.send('my:greetings', { msg: 'hello' }) + server.hot.on('connection', () => { + server.hot.send('my:greetings', { msg: 'hello' }) }) }, }, @@ -581,7 +579,7 @@ if (import.meta.hot) { } ``` -Then use `server.ws.on` and listen to the events on the server side: +Then use `server.hot.on` (since Vite #) or `server.ws.on` and listen to the events on the server side: ```js // vite.config.js @@ -590,7 +588,7 @@ export default defineConfig({ { // ... configureServer(server) { - server.ws.on('my:from-client', (data, client) => { + server.hot.on('my:from-client', (data, client) => { console.log('Message from client:', data.msg) // Hey! // reply only to the client (if needed) client.send('my:ack', { msg: 'Hi! I got your message!' }) diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index a110de16033034..63b7598908c984 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -129,7 +129,7 @@ export interface Plugin extends RollupPlugin { * the descriptors. * * - The hook can also return an empty array and then perform custom updates - * by sending a custom hmr payload via server.ws.send(). + * by sending a custom hmr payload via server.hot.send(). * * - If the hook doesn't return a value, the hmr update will be performed as * normal. diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index d5e2774e7e9638..0d58059825e36d 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -82,6 +82,7 @@ export interface HMRChannel { ...args: any[] ) => void, ): void + on(event: 'connection', listener: () => void): void /** * Unregister event listener. */ @@ -706,7 +707,7 @@ export function createHMRBroadcaster(): HMRBroadcaster { addChannel(channel) { channels.push(channel) }, - on(event, listener) { + on(event: string, listener: (...args: any[]) => any) { channels.forEach((channel) => channel.on(event, listener)) return broadcaster }, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index d4f402d076d0da..a5b818152ab75b 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -15,7 +15,6 @@ import launchEditorMiddleware from 'launch-editor-middleware' import type { SourceMap } from 'rollup' import picomatch from 'picomatch' import type { Matcher } from 'picomatch' -import type { InvalidatePayload } from 'types/customEvent' import type { CommonServerOptions } from '../http' import { httpServerStart, @@ -694,7 +693,7 @@ export async function _createServer( onFileAddUnlink(file, true) }) - ws.on('vite:invalidate', async ({ path, message }: InvalidatePayload) => { + hot.on('vite:invalidate', async ({ path, message }) => { const mod = moduleGraph.urlToModuleMap.get(path) if (mod && mod.isSelfAccepting && mod.lastHMRTimestamp > 0) { config.logger.info( diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index 9ae2186d1b8b5e..054fe0635a96a2 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -12,12 +12,12 @@ export default defineConfig({ if (file.endsWith('customFile.js')) { const content = await read() const msg = content.match(/export const msg = '(\w+)'/)[1] - server.ws.send('custom:foo', { msg }) - server.ws.send('custom:remove', { msg }) + server.hot.send('custom:foo', { msg }) + server.hot.send('custom:remove', { msg }) } }, configureServer(server) { - server.ws.on('custom:remote-add', ({ a, b }, client) => { + server.hot.on('custom:remote-add', ({ a, b }, client) => { client.send('custom:remote-add-result', { result: a + b }) }) }, @@ -44,7 +44,7 @@ export const virtual = _virtual + '${num}';` } }, configureServer(server) { - server.ws.on('virtual:increment', async () => { + server.hot.on('virtual:increment', async () => { const mod = await server.moduleGraph.getModuleByUrl('\0virtual:file') if (mod) { num++ From ff50e1d2360827576434ba87deba127112dc8760 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 29 Dec 2023 11:27:47 +0100 Subject: [PATCH 04/11] test: use inject/provide to define wsEndpoint --- playground/vitestGlobalSetup.ts | 8 +++----- playground/vitestSetup.ts | 15 +++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/playground/vitestGlobalSetup.ts b/playground/vitestGlobalSetup.ts index d62edca8f23daf..230ca820212664 100644 --- a/playground/vitestGlobalSetup.ts +++ b/playground/vitestGlobalSetup.ts @@ -1,15 +1,14 @@ import os from 'node:os' import path from 'node:path' import fs from 'fs-extra' +import type { GlobalSetupContext } from 'vitest/node' import type { BrowserServer } from 'playwright-chromium' import { chromium } from 'playwright-chromium' import { hasWindowsUnicodeFsBug } from './hasWindowsUnicodeFsBug' -const DIR = path.join(os.tmpdir(), 'vitest_playwright_global_setup') - let browserServer: BrowserServer | undefined -export async function setup(): Promise { +export async function setup({ provide }: GlobalSetupContext): Promise { process.env.NODE_ENV = process.env.VITE_TEST_BUILD ? 'production' : 'development' @@ -21,8 +20,7 @@ export async function setup(): Promise { : undefined, }) - await fs.mkdirp(DIR) - await fs.writeFile(path.join(DIR, 'wsEndpoint'), browserServer.wsEndpoint()) + provide('wsEndpoint', browserServer.wsEndpoint()) const tempDir = path.resolve(__dirname, '../playground-temp') await fs.ensureDir(tempDir) diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index c73b358f5731c7..cb4ab8f125a9df 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -1,6 +1,5 @@ import type * as http from 'node:http' -import path, { dirname, join, resolve } from 'node:path' -import os from 'node:os' +import path, { dirname, resolve } from 'node:path' import fs from 'fs-extra' import { chromium } from 'playwright-chromium' import type { @@ -22,7 +21,7 @@ import { import type { Browser, Page } from 'playwright-chromium' import type { RollupError, RollupWatcher, RollupWatcherEvent } from 'rollup' import type { File } from 'vitest' -import { beforeAll } from 'vitest' +import { beforeAll, inject } from 'vitest' // #region env @@ -80,8 +79,6 @@ export function setViteUrl(url: string): void { // #endregion -const DIR = join(os.tmpdir(), 'vitest_playwright_global_setup') - beforeAll(async (s) => { const suite = s as File // skip browser setup for non-playground tests @@ -89,7 +86,7 @@ beforeAll(async (s) => { return } - const wsEndpoint = fs.readFileSync(join(DIR, 'wsEndpoint'), 'utf-8') + const wsEndpoint = inject('wsEndpoint') if (!wsEndpoint) { throw new Error('wsEndpoint not found') } @@ -354,3 +351,9 @@ declare module 'vite' { __test__?: () => void } } + +declare module 'vitest' { + export interface ProvidedContext { + wsEndpoint: string + } +} From 6081e53201f7ff9eb05832333a5575e35638caea Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 29 Dec 2023 11:27:55 +0100 Subject: [PATCH 05/11] chore: cleanup --- packages/vite/src/node/server/hmr.ts | 5 +++-- packages/vite/src/node/server/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 0d58059825e36d..2a661edc39d144 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -101,7 +101,7 @@ export interface HMRBroadcaster extends HMRChannel { /** * Add a new third-party channel. */ - addChannel(connection: HMRChannel): void + addChannel(connection: HMRChannel): HMRBroadcaster } export function getShortName(file: string, root: string): string { @@ -115,7 +115,7 @@ export async function handleHMRUpdate( server: ViteDevServer, configOnly: boolean, ): Promise { - const { config, hot, moduleGraph } = server + const { hot, config, moduleGraph } = server const shortFile = getShortName(file, config.root) const fileName = path.basename(file) @@ -706,6 +706,7 @@ export function createHMRBroadcaster(): HMRBroadcaster { }, addChannel(channel) { channels.push(channel) + return broadcaster }, on(event: string, listener: (...args: any[]) => any) { channels.forEach((channel) => channel.on(event, listener)) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index a5b818152ab75b..3f259f5ef01f9c 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -329,6 +329,7 @@ export interface ViteDevServer { * @param forceOptimize - force the optimizer to re-bundle, same as --force cli flag */ restart(forceOptimize?: boolean): Promise + /** * Open browser */ @@ -411,9 +412,8 @@ export async function _createServer( ? null : await resolveHttpServer(serverConfig, middlewares, httpsOptions) - const hot = createHMRBroadcaster() const ws = createWebSocketServer(httpServer, config, httpsOptions) - hot.addChannel(ws) + const hot = createHMRBroadcaster().addChannel(ws) if (httpServer) { setClientErrorHandler(httpServer, config.logger) From a948ab6daba0bb029b9ad4ded01a7c7d1cd34eba Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 29 Dec 2023 11:28:07 +0100 Subject: [PATCH 06/11] chore: cleanup --- playground/vitestGlobalSetup.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/playground/vitestGlobalSetup.ts b/playground/vitestGlobalSetup.ts index 230ca820212664..7f85d9d12748bf 100644 --- a/playground/vitestGlobalSetup.ts +++ b/playground/vitestGlobalSetup.ts @@ -1,4 +1,3 @@ -import os from 'node:os' import path from 'node:path' import fs from 'fs-extra' import type { GlobalSetupContext } from 'vitest/node' From e67ca0243b1f789f62eae22d84c200a266a8c094 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 29 Dec 2023 11:55:28 +0100 Subject: [PATCH 07/11] chore: docs --- packages/vite/src/node/server/hmr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 2a661edc39d144..521da5edc24dd1 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -84,11 +84,11 @@ export interface HMRChannel { ): void on(event: 'connection', listener: () => void): void /** - * Unregister event listener. + * Unregister event listener */ off(event: string, listener: Function): void /** - * Called when server is closed. + * Disconnect all clients, called when server is closed or restarted. */ close(): void } From 8467cd12e2d42810d755c528c418ee5294c2278a Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sat, 30 Dec 2023 20:59:14 +0900 Subject: [PATCH 08/11] fix: await for close --- packages/vite/src/node/server/hmr.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 521da5edc24dd1..ad7a81c79ed16c 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -90,10 +90,10 @@ export interface HMRChannel { /** * Disconnect all clients, called when server is closed or restarted. */ - close(): void + close(): void | Promise } -export interface HMRBroadcaster extends HMRChannel { +export interface HMRBroadcaster extends Omit { /** * All registered channels. Always has websocket channel. */ @@ -102,6 +102,7 @@ export interface HMRBroadcaster extends HMRChannel { * Add a new third-party channel. */ addChannel(connection: HMRChannel): HMRBroadcaster + close(): Promise } export function getShortName(file: string, root: string): string { @@ -720,7 +721,7 @@ export function createHMRBroadcaster(): HMRBroadcaster { channels.forEach((channel) => channel.send(...(args as [any]))) }, close() { - return channels.map((channel) => channel.close()) + return Promise.all(channels.map((channel) => channel.close())) }, } return broadcaster From 3b07e33e4885c423292ae3505c9a0451710d5d85 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 9 Jan 2024 19:02:16 +0100 Subject: [PATCH 09/11] chore: use Vite 5.1 version --- docs/guide/api-plugin.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index e8423f6092264d..4dd76122fc4f6f 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -423,7 +423,7 @@ Vite plugins can also provide hooks that serve Vite-specific purposes. These hoo - Filter and narrow down the affected module list so that the HMR is more accurate. - - Return an empty array and perform complete custom HMR handling by sending custom events to the client (example uses `server.hot` which was introduced in Vite #, it is recommended to also use `server.ws` if you support lower versions): + - Return an empty array and perform complete custom HMR handling by sending custom events to the client (example uses `server.hot` which was introduced in Vite 5.1, it is recommended to also use `server.ws` if you support lower versions): ```js handleHotUpdate({ server }) { @@ -534,7 +534,7 @@ Since Vite 2.9, we provide some utilities for plugins to help handle the communi ### Server to Client -On the plugin side, we could use `server.hot.send` (since Vite #) or `server.ws.send` to broadcast events to all the clients: +On the plugin side, we could use `server.hot.send` (since Vite 5.1) or `server.ws.send` to broadcast events to all the clients: ```js // vite.config.js @@ -579,7 +579,7 @@ if (import.meta.hot) { } ``` -Then use `server.hot.on` (since Vite #) or `server.ws.on` and listen to the events on the server side: +Then use `server.hot.on` (since Vite 5.1) or `server.ws.on` and listen to the events on the server side: ```js // vite.config.js From 736b972f4254a22dbd3134dcb45c1b4aacda3dad Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 10 Jan 2024 11:10:16 +0100 Subject: [PATCH 10/11] chore: require names in channels --- packages/vite/src/node/server/hmr.ts | 9 ++++++++- packages/vite/src/node/server/ws.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index ad7a81c79ed16c..5186dad44e0844 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -63,6 +63,10 @@ export interface HMRBroadcasterClient { } export interface HMRChannel { + /** + * Unique channel name + */ + name: string /** * Broadcast events to all clients */ @@ -93,7 +97,7 @@ export interface HMRChannel { close(): void | Promise } -export interface HMRBroadcaster extends Omit { +export interface HMRBroadcaster extends Omit { /** * All registered channels. Always has websocket channel. */ @@ -706,6 +710,9 @@ export function createHMRBroadcaster(): HMRBroadcaster { return [...channels] }, addChannel(channel) { + if (channels.some((c) => c.name === channel.name)) { + throw new Error(`HMR channel "${channel.name}" is already defined.`) + } channels.push(channel) return broadcaster }, diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 05b40e0f2ce70a..6b70d1fbea5e77 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -223,6 +223,7 @@ export function createWebSocketServer( let bufferedError: ErrorPayload | null = null return { + name: 'ws', listen: () => { wsHttpServer?.listen(port, host) }, From 63811f7e4d4dfdf8bc1e8bcd52a8effd24da7de5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 10 Jan 2024 11:11:38 +0100 Subject: [PATCH 11/11] feat: add channels option --- packages/vite/src/node/server/hmr.ts | 30 +++++++++++++++++++++++--- packages/vite/src/node/server/index.ts | 19 +++++++++------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 5186dad44e0844..0d0d6beef02a4e 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -35,6 +35,8 @@ export interface HmrOptions { timeout?: number overlay?: boolean server?: Server + /** @internal */ + channels?: HMRChannel[] } export interface HmrContext { @@ -91,10 +93,14 @@ export interface HMRChannel { * Unregister event listener */ off(event: string, listener: Function): void + /** + * Start listening for messages + */ + listen(): void /** * Disconnect all clients, called when server is closed or restarted. */ - close(): void | Promise + close(): void } export interface HMRBroadcaster extends Omit { @@ -705,6 +711,7 @@ async function readModifiedFile(file: string): Promise { export function createHMRBroadcaster(): HMRBroadcaster { const channels: HMRChannel[] = [] + const readyChannels = new WeakSet() const broadcaster: HMRBroadcaster = { get channels() { return [...channels] @@ -717,16 +724,33 @@ export function createHMRBroadcaster(): HMRBroadcaster { return broadcaster }, on(event: string, listener: (...args: any[]) => any) { + // emit connection event only when all channels are ready + if (event === 'connection') { + // make a copy so we don't wait for channels that might be added after this is triggered + const channels = this.channels + channels.forEach((channel) => + channel.on('connection', () => { + readyChannels.add(channel) + if (channels.every((c) => readyChannels.has(c))) { + listener() + } + }), + ) + return + } channels.forEach((channel) => channel.on(event, listener)) - return broadcaster + return }, off(event, listener) { channels.forEach((channel) => channel.off(event, listener)) - return broadcaster + return }, send(...args: any[]) { channels.forEach((channel) => channel.send(...(args as [any]))) }, + listen() { + channels.forEach((channel) => channel.listen()) + }, close() { return Promise.all(channels.map((channel) => channel.close())) }, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 3f259f5ef01f9c..1ac5d25454d87d 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -239,7 +239,7 @@ export interface ViteDevServer { * HMR broadcaster that can be used to send custom HMR messages to the client * * Always sends a message to at least a WebSocket client. Any third party can - * add a channel to the broadcaster to process messages so be aware of that + * add a channel to the broadcaster to process messages */ hot: HMRBroadcaster /** @@ -387,12 +387,12 @@ export interface ResolvedServerUrls { export function createServer( inlineConfig: InlineConfig = {}, ): Promise { - return _createServer(inlineConfig, { ws: true }) + return _createServer(inlineConfig, { hotListen: true }) } export async function _createServer( inlineConfig: InlineConfig = {}, - options: { ws: boolean }, + options: { hotListen: boolean }, ): Promise { const config = await resolveConfig(inlineConfig, 'serve') @@ -414,6 +414,9 @@ export async function _createServer( const ws = createWebSocketServer(httpServer, config, httpsOptions) const hot = createHMRBroadcaster().addChannel(ws) + if (typeof config.server.hmr === 'object' && config.server.hmr.channels) { + config.server.hmr.channels.forEach((channel) => hot.addChannel(channel)) + } if (httpServer) { setClientErrorHandler(httpServer, config.logger) @@ -836,7 +839,7 @@ export async function _createServer( httpServer.listen = (async (port: number, ...args: any[]) => { try { // ensure ws server started - ws.listen() + hot.listen() await initServer() } catch (e) { httpServer.emit('error', e) @@ -845,8 +848,8 @@ export async function _createServer( return listen(port, ...args) }) as any } else { - if (options.ws) { - ws.listen() + if (options.hotListen) { + hot.listen() } await initServer() } @@ -997,7 +1000,7 @@ async function restartServer(server: ViteDevServer) { let newServer = null try { // delay ws server listen - newServer = await _createServer(inlineConfig, { ws: false }) + newServer = await _createServer(inlineConfig, { hotListen: false }) } catch (err: any) { server.config.logger.error(err.message, { timestamp: true, @@ -1030,7 +1033,7 @@ async function restartServer(server: ViteDevServer) { if (!middlewareMode) { await server.listen(port, true) } else { - server.ws.listen() + server.hot.listen() } logger.info('server restarted.', { timestamp: true })