diff --git a/e2e/cases/cli/shortcut-restart-watcher/dev.js b/e2e/cases/cli/shortcut-restart-watcher/dev.js new file mode 100644 index 0000000000..5f4b5cb350 --- /dev/null +++ b/e2e/cases/cli/shortcut-restart-watcher/dev.js @@ -0,0 +1,6 @@ +import { runCLI } from '@rsbuild/core'; + +process.stdin.isTTY = true; +delete process.env.CI; + +runCLI({ argv: ['node', 'rsbuild', 'dev'] }); diff --git a/e2e/cases/cli/shortcut-restart-watcher/index.test.ts b/e2e/cases/cli/shortcut-restart-watcher/index.test.ts new file mode 100644 index 0000000000..1ec4b2fb8d --- /dev/null +++ b/e2e/cases/cli/shortcut-restart-watcher/index.test.ts @@ -0,0 +1,38 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { expect, test } from '@e2e/helper'; +import { getRandomPort } from '@rstackjs/test-utils'; + +const watchedFile = path.join(import.meta.dirname, 'test-temp-watch.txt'); +const restartLog = 'restarting server as test-temp-watch.txt changed'; + +test.beforeEach(() => { + fs.writeFileSync(watchedFile, '1'); +}); + +test.afterAll(() => { + fs.rmSync(watchedFile, { force: true }); +}); + +test('should close the old watcher after a shortcut restart', async ({ exec, logHelper }) => { + const port = await getRandomPort(); + const { childProcess } = exec('node ./dev.js', { + env: { + PORT: String(port), + }, + }); + const { clearLogs, expectBuildEnd, expectLog, logs } = logHelper; + + await expectBuildEnd(); + clearLogs(); + childProcess.stdin?.write('r\n'); + await expectLog('restarting server'); + await expectBuildEnd(); + + clearLogs(); + fs.writeFileSync(watchedFile, '2'); + await expectLog(restartLog); + await expectBuildEnd(); + + expect(logs.filter((log) => log.includes(restartLog))).toHaveLength(1); +}); diff --git a/e2e/cases/cli/shortcut-restart-watcher/rsbuild.config.mjs b/e2e/cases/cli/shortcut-restart-watcher/rsbuild.config.mjs new file mode 100644 index 0000000000..bae32e0ef7 --- /dev/null +++ b/e2e/cases/cli/shortcut-restart-watcher/rsbuild.config.mjs @@ -0,0 +1,11 @@ +export default { + dev: { + watchFiles: { + paths: './test-temp-watch.txt', + type: 'restart', + }, + }, + server: { + port: Number(process.env.PORT), + }, +}; diff --git a/e2e/cases/cli/shortcut-restart-watcher/src/index.js b/e2e/cases/cli/shortcut-restart-watcher/src/index.js new file mode 100644 index 0000000000..679c9bca67 --- /dev/null +++ b/e2e/cases/cli/shortcut-restart-watcher/src/index.js @@ -0,0 +1 @@ +console.log('shortcut restart watcher'); diff --git a/packages/core/src/build.ts b/packages/core/src/build.ts index 3c28cde902..b9fe225115 100644 --- a/packages/core/src/build.ts +++ b/packages/core/src/build.ts @@ -65,6 +65,9 @@ export const build = async ( let closingPromise: Promise | undefined; let unregisterRestart: (() => void) | undefined; + + // Keep the restart watcher active when closing build resources, + // so failed restarts can be retried. const closeBuild = () => { closingPromise ||= (async () => { unregisterRestart?.(); @@ -77,7 +80,6 @@ export const build = async ( let restartWatcher: ReturnType; if (watch) { - // Only close build resources before restart; keep the watcher alive for retries. unregisterRestart = context.restartManager.registerCleanup(closeBuild); restartWatcher = watchFilesForRestart({ watchFiles: context.normalizedConfig!.dev.watchFiles, @@ -86,6 +88,7 @@ export const build = async ( }); } + // Fully close the build and its restart watcher. const close = async () => { await restartWatcher?.close(); await closeBuild(); diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index c4caf8d32d..c42adf242b 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -203,6 +203,8 @@ export async function createDevServer< let closingPromise: Promise | undefined; let unregisterRestart: (() => void) | undefined; + // Keep the restart watcher active when closing server resources, + // so failed restarts can be retried. const closeServerResources = () => { if (!closingPromise) { unregisterRestart?.(); @@ -217,11 +219,28 @@ export async function createDevServer< return closingPromise; }; + // Fully close the server and its restart watcher. const closeServer = async () => { await state.restartWatcher?.close(); await closeServerResources(); }; + // Request a manual restart and close the old watcher only after it succeeds. + const restartServer = async () => { + const restarted = await requestRestart({ + action: 'dev', + clear: false, + logger, + restartManager: context.restartManager, + }); + + if (restarted) { + await state.restartWatcher?.close(); + } + + return restarted; + }; + if (!middlewareMode) { registerCleanup(closeServer); } @@ -237,15 +256,7 @@ export async function createDevServer< openPage, closeServer, printUrls, - restartServer: context.restartManager.canRestart - ? () => - requestRestart({ - action: 'dev', - clear: false, - logger, - restartManager: context.restartManager, - }) - : undefined, + restartServer: context.restartManager.canRestart ? restartServer : undefined, help: shortcutsOptions.help, customShortcuts: shortcutsOptions.custom, logger, @@ -440,7 +451,6 @@ export async function createDevServer< // start watching state.buildManager?.watch(); - // Only close server resources before restart; keep the watcher alive for retries. unregisterRestart = context.restartManager.registerCleanup(closeServerResources); state.restartWatcher = watchFilesForRestart({ watchFiles: config.dev.watchFiles,