diff --git a/packages/vite/src/node/server/__tests__/restart.spec.ts b/packages/vite/src/node/server/__tests__/restart.spec.ts new file mode 100644 index 00000000000000..7ac0de37cf2340 --- /dev/null +++ b/packages/vite/src/node/server/__tests__/restart.spec.ts @@ -0,0 +1,53 @@ +import { expect, onTestFinished, test } from 'vitest' +import { promiseWithResolvers } from '../../../shared/utils' +import { createServer } from '../index' + +test('queues one follow-up restart while a restart is in flight', async () => { + const restartEntered = promiseWithResolvers() + const restartGate = promiseWithResolvers() + const forceOptimizeDeps: boolean[] = [] + let configCalls = 0 + + const server = await createServer({ + configFile: false, + root: import.meta.dirname, + logLevel: 'silent', + server: { middlewareMode: true, ws: false }, + plugins: [ + { + name: 'restart-during-restart', + async config(config) { + configCalls++ + forceOptimizeDeps.push( + (config as { forceOptimizeDeps?: boolean }).forceOptimizeDeps === + true, + ) + + if (configCalls === 2) { + restartEntered.resolve() + await restartGate.promise + } + }, + }, + ], + }) + + onTestFinished(async () => { + await server.close() + }) + + const firstRestart = server.restart() + await restartEntered.promise + + const queuedRestarts = [ + server.restart(), + server.restart(true), + server.restart(), + ] + + restartGate.resolve() + await Promise.all([firstRestart, ...queuedRestarts]) + + expect(configCalls).toBe(3) + expect(forceOptimizeDeps).toEqual([false, false, true]) +}) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index c93bbb6eb1e2d5..169b2ad1350caf 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -469,6 +469,10 @@ export interface ViteDevServer { * @internal */ _restartPromise: Promise | null + /** + * @internal + */ + _pendingRestart: boolean /** * @internal */ @@ -516,6 +520,7 @@ export async function _createServer( previousEnvironments?: Record previousShortcutsState?: ShortcutsState previousRestartPromise?: Promise | null + previousPendingRestart?: boolean previousForceOptimizeOnRestart?: boolean }, ): Promise { @@ -840,10 +845,23 @@ export async function _createServer( bindCLIShortcuts(server, options) }, async restart(forceOptimize?: boolean) { - if (!server._restartPromise) { + if (server._restartPromise) { + server._pendingRestart = true + server._forceOptimizeOnRestart ||= !!forceOptimize + } else { + server._pendingRestart = false server._forceOptimizeOnRestart = !!forceOptimize - server._restartPromise = restartServer(server).finally(() => { + server._restartPromise = (async () => { + while (true) { + await restartServer(server) + if (!server._pendingRestart) { + break + } + server._pendingRestart = false + } + })().finally(() => { server._restartPromise = null + server._pendingRestart = false server._forceOptimizeOnRestart = false }) } @@ -866,6 +884,7 @@ export async function _createServer( return closeServerPromise }, _restartPromise: options.previousRestartPromise ?? null, + _pendingRestart: options.previousPendingRestart ?? false, _forceOptimizeOnRestart: options.previousForceOptimizeOnRestart ?? false, _shortcutsState: options.previousShortcutsState, } @@ -1407,6 +1426,7 @@ async function restartServer(server: ViteDevServer) { previousEnvironments: server.environments, previousShortcutsState: server._shortcutsState, previousRestartPromise: server._restartPromise, + previousPendingRestart: server._pendingRestart, previousForceOptimizeOnRestart: server._forceOptimizeOnRestart, }) } catch (err: any) { @@ -1424,12 +1444,20 @@ async function restartServer(server: ViteDevServer) { // restart from a real close. await server._closeServer('restart') + const restartPromise = server._restartPromise + const pendingRestart = server._pendingRestart + const forceOptimizeOnRestart = server._forceOptimizeOnRestart + // Assign new server props to existing server instance const middlewares = server.middlewares newServer._configServerPort = server._configServerPort newServer._currentServerPort = server._currentServerPort Object.assign(server, newServer) + server._restartPromise = restartPromise + server._pendingRestart = pendingRestart + server._forceOptimizeOnRestart = forceOptimizeOnRestart + // Keep the same connect instance so app.use(vite.middlewares) works // after a restart in middlewareMode (.route is always '/') middlewares.stack = newServer.middlewares.stack