Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dev.client.reconnect option #3333

Merged
merged 2 commits into from
Sep 1, 2024
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
121 changes: 35 additions & 86 deletions packages/core/src/client/hmr.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
/**
* This has been adapted from `create-react-app`, authored by Facebook, Inc.
* see: https://github.com/facebookincubator/create-react-app/tree/master/packages/react-dev-utils
*
* Tips: this package will be bundled and running in the browser, do not import any Node.js modules.
*/
import type { ClientConfig, Rspack } from '../types';
import type { NormalizedClientConfig, Rspack } from '../types';
import { formatStatsMessages } from './format';

const compilationName = RSBUILD_COMPILATION_NAME;
const compilationId = RSBUILD_COMPILATION_NAME;
const config: NormalizedClientConfig = RSBUILD_CLIENT_CONFIG;

function formatURL({
port,
Expand All @@ -26,7 +21,7 @@ function formatURL({
url.hostname = hostname;
url.protocol = protocol;
url.pathname = pathname;
url.searchParams.append('compilationName', compilationName);
url.searchParams.append('compilationId', compilationId);
return url.toString();
}

Expand All @@ -35,18 +30,6 @@ function formatURL({
return `${protocol}${colon}//${hostname}:${port}${pathname}`;
}

function getSocketUrl(urlParts: ClientConfig) {
const { location } = self;
const { host, port, path, protocol } = urlParts;

return formatURL({
protocol: protocol || (location.protocol === 'https:' ? 'wss' : 'ws'),
hostname: host || location.hostname,
port: port || location.port,
pathname: path || '/rsbuild-hmr',
});
}

// Remember some state related to hot module replacement.
let isFirstCompilation = true;
let lastCompilationHash: string | null = null;
Expand Down Expand Up @@ -120,7 +103,6 @@ function handleErrors(errors: Rspack.StatsError[]) {
isFirstCompilation = false;
hasCompileErrors = true;

// "Massage" webpack messages.
const formatted = formatStatsMessages({
errors,
warnings: [],
Expand All @@ -134,16 +116,11 @@ function handleErrors(errors: Rspack.StatsError[]) {
if (createOverlay) {
createOverlay(formatted.errors);
}

// Do not attempt to reload now.
// We will reload on next success instead.
}

function isUpdateAvailable() {
// __webpack_hash__ is the hash of the current compilation.
// It's a global variable injected by webpack / Rspack.
return lastCompilationHash !== __webpack_hash__;
}
// __webpack_hash__ is the hash of the current compilation.
// It's a global variable injected by Rspack.
const isUpdateAvailable = () => lastCompilationHash !== __webpack_hash__;

// Attempt to update code on the fly, fall back to a hard reload.
function tryApplyUpdates() {
Expand All @@ -158,21 +135,20 @@ function tryApplyUpdates() {
return;
}

// webpack disallows updates in other states.
// Rspack disallows updates in other states.
if (import.meta.webpackHot.status() !== 'idle') {
return;
}

function handleApplyUpdates(
const handleApplyUpdates = (
err: unknown,
updatedModules: (string | number)[] | null,
) {
) => {
const forcedReload = err || !updatedModules;
if (forcedReload) {
if (err) {
console.error('[HMR] Forced reload caused by: ', err);
}

reloadPage();
return;
}
Expand All @@ -181,22 +157,17 @@ function tryApplyUpdates() {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates();
}
}
};

// https://rspack.dev/api/modules#importmetawebpackhot-webpack-specific
import.meta.webpackHot.check(true).then(
(updatedModules) => {
handleApplyUpdates(null, updatedModules);
},
(err) => {
handleApplyUpdates(err, null);
},
(updatedModules) => handleApplyUpdates(null, updatedModules),
(err) => handleApplyUpdates(err, null),
);
}

const MAX_RETRIES = 100;
let connection: WebSocket | null = null;
let retryCount = 0;
let reconnectCount = 0;

function onOpen() {
// Notify users that the HMR has successfully connected.
Expand All @@ -206,7 +177,7 @@ function onOpen() {
function onMessage(e: MessageEvent<string>) {
const message = JSON.parse(e.data);

if (message.compilationName && message.compilationName !== compilationName) {
if (message.compilationId && message.compilationId !== compilationId) {
return;
}

Expand Down Expand Up @@ -237,44 +208,33 @@ function onMessage(e: MessageEvent<string>) {
}
}

function sleep(msec = 1000) {
return new Promise((resolve) => {
setTimeout(resolve, msec);
});
}

async function onClose() {
console.info('[HMR] disconnected. Attempting to reconnect.');

removeListeners();

await sleep(1000);
retryCount++;

if (
connection &&
(connection.readyState === connection.CONNECTING ||
connection.readyState === connection.OPEN)
) {
retryCount = 0;
return;
}

// Exceeded max retry attempts, stop retry.
if (retryCount > MAX_RETRIES) {
console.info(
'[HMR] Unable to establish a connection after exceeding the maximum retry attempts.',
);
retryCount = 0;
function onClose() {
if (reconnectCount >= config.reconnect) {
if (config.reconnect > 0) {
console.info(
'[HMR] Connection failure after maximum reconnect limit exceeded.',
);
}
return;
}

reconnect();
console.info('[HMR] disconnected. Attempting to reconnect.');
removeListeners();
connection = null;
reconnectCount++;
setTimeout(connect, 1000);
}

// Establishing a WebSocket connection with the server.
function connect() {
const socketUrl = getSocketUrl(RSBUILD_CLIENT_CONFIG);
const { location } = self;
const { host, port, path, protocol } = config;
const socketUrl = formatURL({
protocol: protocol || (location.protocol === 'https:' ? 'wss' : 'ws'),
hostname: host || location.hostname,
port: port || location.port,
pathname: path || '/rsbuild-hmr',
});

connection = new WebSocket(socketUrl);
connection.addEventListener('open', onOpen);
Expand All @@ -292,17 +252,6 @@ function removeListeners() {
}
}

/**
* Close the current connection if it exists and then establishes a new
* connection.
*/
function reconnect() {
if (connection) {
connection = null;
}
connect();
}

function reloadPage() {
if (RSBUILD_DEV_LIVE_RELOAD) {
window.location.reload();
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,10 @@ const getDefaultDevConfig = (): NormalizedDevConfig => ({
writeToDisk: false,
client: {
path: HMR_SOCKET_PATH,
// By default it is set to "location.port"
port: '',
// By default it is set to "location.hostname"
host: '',
overlay: true,
reconnect: 100,
},
});

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/server/compilerDevMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ export class CompilerDevMiddleware {
const { devConfig, serverConfig } = this;

const callbacks = {
onInvalid: (compilationName?: string) => {
onInvalid: (compilationId?: string) => {
this.socketServer.sockWrite({
type: 'invalid',
compilationName,
compilationId,
});
},
onDone: (stats: any) => {
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/server/devMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { Compiler, MultiCompiler } from '@rspack/core';
import { applyToCompiler } from '../helpers';
import type { DevMiddlewareOptions } from '../provider/createCompiler';
import type { DevConfig, NextFunction } from '../types';
import { getCompilationName } from './helper';
import { getCompilationId } from './helper';

type ServerCallbacks = {
onInvalid: (compilationName?: string) => void;
onInvalid: (compilationId?: string) => void;
onDone: (stats: any) => void;
};

Expand Down Expand Up @@ -50,10 +50,10 @@ export const setupServerHooks = (
const { compile, invalid, done } = compiler.hooks;

compile.tap('rsbuild-dev-server', () =>
hookCallbacks.onInvalid(getCompilationName(compiler)),
hookCallbacks.onInvalid(getCompilationId(compiler)),
);
invalid.tap('rsbuild-dev-server', () =>
hookCallbacks.onInvalid(getCompilationName(compiler)),
hookCallbacks.onInvalid(getCompilationId(compiler)),
);
done.tap('rsbuild-dev-server', hookCallbacks.onDone);
};
Expand All @@ -74,7 +74,7 @@ function applyHMREntry({
}

new compiler.webpack.DefinePlugin({
RSBUILD_COMPILATION_NAME: JSON.stringify(getCompilationName(compiler)),
RSBUILD_COMPILATION_NAME: JSON.stringify(getCompilationId(compiler)),
RSBUILD_CLIENT_CONFIG: JSON.stringify(clientConfig),
RSBUILD_DEV_LIVE_RELOAD: liveReload,
}).apply(compiler);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/server/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export const getAddressUrls = ({
};

// A unique name for WebSocket communication
export const getCompilationName = (
export const getCompilationId = (
compiler: Rspack.Compiler | Rspack.Compilation,
) => `${compiler.name ?? ''}_${compiler.options.output.uniqueName ?? ''}`;

Expand Down
Loading
Loading