From 5b7786090e1fb2d836e96b8fd797be257bf3f4e6 Mon Sep 17 00:00:00 2001 From: chenfan02 Date: Tue, 21 Jul 2026 12:38:52 +0800 Subject: [PATCH 1/2] fix(hmr): preserve environment snapshot during server restart --- .../src/node/server/__tests__/hmr.spec.ts | 42 +++++++++++++++++++ packages/vite/src/node/server/hmr.ts | 10 ++--- 2 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 packages/vite/src/node/server/__tests__/hmr.spec.ts diff --git a/packages/vite/src/node/server/__tests__/hmr.spec.ts b/packages/vite/src/node/server/__tests__/hmr.spec.ts new file mode 100644 index 00000000000000..19ce08f2c994c9 --- /dev/null +++ b/packages/vite/src/node/server/__tests__/hmr.spec.ts @@ -0,0 +1,42 @@ +import path from 'node:path' +import { expect, onTestFinished, test } from 'vitest' +import type { DevEnvironment } from '../environment' +import { handleHMRUpdate } from '../hmr' +import { createServer } from '../index' + +test('uses the environment snapshot when the server restarts during HMR', async () => { + let hookCalls = 0 + const server = await createServer({ + configFile: false, + root: import.meta.dirname, + logLevel: 'silent', + server: { + middlewareMode: true, + ws: false, + }, + plugins: [ + { + name: 'restart-during-hot-update', + async hotUpdate() { + if (hookCalls++ === 0) { + server.environments.client = {} as DevEnvironment + throw new Error('hot update interrupted by restart') + } + }, + }, + ], + }) + const clientEnvironment = server.environments.client + onTestFinished(async () => { + server.environments.client = clientEnvironment + await server.close() + }) + + await expect( + handleHMRUpdate( + 'update', + path.join(import.meta.dirname, 'fixture.js'), + server, + ), + ).resolves.toBeUndefined() +}) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 65ad383673ba34..190ed6cb360581 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -590,7 +590,7 @@ export async function handleHMRUpdate( } } } catch (error) { - hotMap.get(server.environments.client)!.error = error + hotMap.get(clientEnvironment)!.error = error } for (const environment of environments) { @@ -656,13 +656,9 @@ export async function handleHMRUpdate( const hotUpdateEnvironments = server.config.server.hotUpdateEnvironments ?? - ((server, hmr) => { + ((_server, hmr) => { // Run HMR in parallel for all environments by default - return Promise.all( - Object.values(server.environments).map((environment) => - hmr(environment), - ), - ) + return Promise.all(environments.map((environment) => hmr(environment))) }) await hotUpdateEnvironments(server, hmr) From 2450ddacd180316ea7743dd887d4d383656bddc0 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Tue, 21 Jul 2026 23:19:10 +0900 Subject: [PATCH 2/2] fix: fix even if `server.config.server.hotUpdateEnvironments` is specified --- .../src/node/server/__tests__/hmr.spec.ts | 25 +++++++++++++----- packages/vite/src/node/server/hmr.ts | 26 ++++++++++++++----- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/vite/src/node/server/__tests__/hmr.spec.ts b/packages/vite/src/node/server/__tests__/hmr.spec.ts index 19ce08f2c994c9..a639c0cec418b1 100644 --- a/packages/vite/src/node/server/__tests__/hmr.spec.ts +++ b/packages/vite/src/node/server/__tests__/hmr.spec.ts @@ -1,10 +1,9 @@ import path from 'node:path' import { expect, onTestFinished, test } from 'vitest' -import type { DevEnvironment } from '../environment' import { handleHMRUpdate } from '../hmr' -import { createServer } from '../index' +import { type ServerOptions, createServer } from '../index' -test('uses the environment snapshot when the server restarts during HMR', async () => { +async function testRestartDuringHotUpdate(serverOptions: ServerOptions = {}) { let hookCalls = 0 const server = await createServer({ configFile: false, @@ -13,22 +12,21 @@ test('uses the environment snapshot when the server restarts during HMR', async server: { middlewareMode: true, ws: false, + ...serverOptions, }, plugins: [ { name: 'restart-during-hot-update', async hotUpdate() { if (hookCalls++ === 0) { - server.environments.client = {} as DevEnvironment + await server.restart() throw new Error('hot update interrupted by restart') } }, }, ], }) - const clientEnvironment = server.environments.client onTestFinished(async () => { - server.environments.client = clientEnvironment await server.close() }) @@ -39,4 +37,19 @@ test('uses the environment snapshot when the server restarts during HMR', async server, ), ).resolves.toBeUndefined() +} + +test('cancels HMR when the server restarts during a hot update', async () => { + await testRestartDuringHotUpdate() +}) + +test('does not schedule stale HMR with a custom environment handler', async () => { + let hotUpdateEnvironmentsCalls = 0 + await testRestartDuringHotUpdate({ + async hotUpdateEnvironments(server, hmr) { + hotUpdateEnvironmentsCalls++ + await Promise.all(Object.values(server.environments).map(hmr)) + }, + }) + expect(hotUpdateEnvironmentsCalls).toBe(0) }) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 190ed6cb360581..84e9616b5d45f1 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -416,7 +416,11 @@ export async function handleHMRUpdate( const { config } = server const mixedModuleGraph = ignoreDeprecationWarnings(() => server.moduleGraph) - const environments = Object.values(server.environments) + const environmentSnapshot = server.environments + const environments = Object.values(environmentSnapshot) + // A plugin hook may restart the server, replacing the environments and + // invalidating this HMR transaction. + const isStale = () => server.environments !== environmentSnapshot const shortFile = getShortName(file, config.root) const isConfig = file === config.configFile @@ -509,14 +513,13 @@ export async function handleHMRUpdate( const clientHotUpdateOptions = hotMap.get(clientEnvironment)!.options const ssrHotUpdateOptions = hotMap.get(ssrEnvironment)?.options try { - for (const plugin of getSortedHotUpdatePlugins( - server.environments.client, - )) { + for (const plugin of getSortedHotUpdatePlugins(clientEnvironment)) { if (plugin.hotUpdate) { const filteredModules = await getHookHandler(plugin.hotUpdate).call( clientContext, clientHotUpdateOptions, ) + if (isStale()) return if (filteredModules) { clientHotUpdateOptions.modules = filteredModules // Invalidate the hmrContext to force compat modules to be updated @@ -552,6 +555,7 @@ export async function handleHMRUpdate( const filteredModules = await getHookHandler( plugin.handleHotUpdate!, ).call(contextForHandleHotUpdate, mixedHmrContext) + if (isStale()) return if (filteredModules) { mixedHmrContext.modules = filteredModules clientHotUpdateOptions.modules = @@ -590,6 +594,7 @@ export async function handleHMRUpdate( } } } catch (error) { + if (isStale()) return hotMap.get(clientEnvironment)!.error = error } @@ -604,17 +609,20 @@ export async function handleHMRUpdate( context, hot.options, ) + if (isStale()) return if (filteredModules) { hot.options.modules = filteredModules } } } } catch (error) { + if (isStale()) return hot.error = error } } async function hmr(environment: DevEnvironment) { + if (isStale()) return try { const { options, error } = hotMap.get(environment)! if (error) { @@ -654,11 +662,17 @@ export async function handleHMRUpdate( } } + if (isStale()) return + const hotUpdateEnvironments = server.config.server.hotUpdateEnvironments ?? - ((_server, hmr) => { + ((server, hmr) => { // Run HMR in parallel for all environments by default - return Promise.all(environments.map((environment) => hmr(environment))) + return Promise.all( + Object.values(server.environments).map((environment) => + hmr(environment), + ), + ) }) await hotUpdateEnvironments(server, hmr)