Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions e2e/cases/cli/shortcut-restart-watcher/dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { runCLI } from '@rsbuild/core';

process.stdin.isTTY = true;
delete process.env.CI;

runCLI({ argv: ['node', 'rsbuild', 'dev'] });
38 changes: 38 additions & 0 deletions e2e/cases/cli/shortcut-restart-watcher/index.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
11 changes: 11 additions & 0 deletions e2e/cases/cli/shortcut-restart-watcher/rsbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
dev: {
watchFiles: {
paths: './test-temp-watch.txt',
type: 'restart',
},
},
server: {
port: Number(process.env.PORT),
},
};
1 change: 1 addition & 0 deletions e2e/cases/cli/shortcut-restart-watcher/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('shortcut restart watcher');
5 changes: 4 additions & 1 deletion packages/core/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export const build = async (

let closingPromise: Promise<void> | 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?.();
Expand All @@ -77,7 +80,6 @@ export const build = async (

let restartWatcher: ReturnType<typeof watchFilesForRestart>;
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,
Expand All @@ -86,6 +88,7 @@ export const build = async (
});
}

// Fully close the build and its restart watcher.
const close = async () => {
await restartWatcher?.close();
await closeBuild();
Expand Down
30 changes: 20 additions & 10 deletions packages/core/src/server/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ export async function createDevServer<
let closingPromise: Promise<void> | 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?.();
Expand All @@ -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);
}
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down