From 0543409749346011dbf2f78f5d7d3a29978db391 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 26 Dec 2024 02:25:27 +0100 Subject: [PATCH 01/23] refactor: unify wss connection handlers --- .../src/plugins/wss/WebSocketServer.ts | 64 ++++++++++------ .../plugins/wss/servers/WebSocketApiServer.ts | 30 +------- .../wss/servers/WebSocketDevClientServer.ts | 29 ++----- .../wss/servers/WebSocketEventsServer.ts | 39 ++++------ .../plugins/wss/servers/WebSocketHMRServer.ts | 76 ++----------------- .../wss/servers/WebSocketMessageServer.ts | 37 +++------ packages/dev-server/src/plugins/wss/types.ts | 7 ++ 7 files changed, 86 insertions(+), 196 deletions(-) diff --git a/packages/dev-server/src/plugins/wss/WebSocketServer.ts b/packages/dev-server/src/plugins/wss/WebSocketServer.ts index 912dd70ed..e47bed411 100644 --- a/packages/dev-server/src/plugins/wss/WebSocketServer.ts +++ b/packages/dev-server/src/plugins/wss/WebSocketServer.ts @@ -1,47 +1,52 @@ import type { IncomingMessage } from 'node:http'; import type { Socket } from 'node:net'; import type { FastifyInstance } from 'fastify'; -import { - type ServerOptions, - type WebSocket, - WebSocketServer as WebSocketServerImpl, -} from 'ws'; -import type { WebSocketServerInterface } from './types.js'; +import { type WebSocket, WebSocketServer as WebSocketServerImpl } from 'ws'; +import type { + WebSocketServerInterface, + WebSocketServerOptions, +} from './types.js'; /** * Abstract class for providing common logic (eg routing) for all WebSocket servers. * * @category Development server */ -export abstract class WebSocketServer implements WebSocketServerInterface { +export abstract class WebSocketServer + implements WebSocketServerInterface +{ /** An instance of the underlying WebSocket server. */ protected server: WebSocketServerImpl; /** Fastify instance from which {@link server} will receive upgrade connections. */ protected fastify: FastifyInstance; + protected name: string; + protected paths: string[]; + protected clients: Map; + protected nextClientId = 0; + /** * Create a new instance of the WebSocketServer. * Any logging information, will be passed through standard `fastify.log` API. * * @param fastify Fastify instance to which the WebSocket will be attached to. * @param path Path on which this WebSocketServer will be accepting connections. - * @param wssOptions WebSocket Server options. + * @param options WebSocketServer options. */ - constructor( - fastify: FastifyInstance, - path: string | string[], - wssOptions: Omit< - ServerOptions, - 'noServer' | 'server' | 'host' | 'port' | 'path' - > = {} - ) { + constructor(fastify: FastifyInstance, options: WebSocketServerOptions) { this.fastify = fastify; - this.server = new WebSocketServerImpl({ noServer: true, ...wssOptions }); + + this.name = options.name; + + this.server = new WebSocketServerImpl({ noServer: true, ...options.wss }); this.server.on('connection', this.onConnection.bind(this)); - this.paths = Array.isArray(path) ? path : [path]; + + this.paths = Array.isArray(options.path) ? options.path : [options.path]; + + this.clients = new Map(); } shouldUpgrade(pathname: string) { @@ -54,11 +59,20 @@ export abstract class WebSocketServer implements WebSocketServerInterface { }); } - /** - * Process incoming WebSocket connection. - * - * @param socket Incoming WebSocket connection. - * @param request Upgrade request for the connection. - */ - abstract onConnection(socket: WebSocket, request: IncomingMessage): void; + onConnection(socket: T, _request: IncomingMessage): string { + const clientId = `client#${this.nextClientId++}`; + this.clients.set(clientId, socket); + this.fastify.log.debug({ msg: 'API client connected', clientId }); + + const errorHandler = () => { + this.fastify.log.debug({ msg: 'API client disconnected', clientId }); + socket.removeAllListeners(); // should we do this? + this.clients.delete(clientId); + }; + + socket.addEventListener('error', errorHandler); + socket.addEventListener('close', errorHandler); + + return clientId; + } } diff --git a/packages/dev-server/src/plugins/wss/servers/WebSocketApiServer.ts b/packages/dev-server/src/plugins/wss/servers/WebSocketApiServer.ts index dd5155ca6..1b7484c29 100644 --- a/packages/dev-server/src/plugins/wss/servers/WebSocketApiServer.ts +++ b/packages/dev-server/src/plugins/wss/servers/WebSocketApiServer.ts @@ -1,5 +1,4 @@ import type { FastifyInstance } from 'fastify'; -import type WebSocket from 'ws'; import { WebSocketServer } from '../WebSocketServer.js'; /** @@ -9,9 +8,6 @@ import { WebSocketServer } from '../WebSocketServer.js'; * @category Development server */ export class WebSocketApiServer extends WebSocketServer { - private clients = new Map(); - private nextClientId = 0; - /** * Create new instance of WebSocketApiServer and attach it to the given Fastify instance. * Any logging information, will be passed through standard `fastify.log` API. @@ -19,7 +15,7 @@ export class WebSocketApiServer extends WebSocketServer { * @param fastify Fastify instance to attach the WebSocket server to. */ constructor(fastify: FastifyInstance) { - super(fastify, '/api'); + super(fastify, { name: 'API', path: '/api' }); } /** @@ -38,28 +34,4 @@ export class WebSocketApiServer extends WebSocketServer { } } } - - /** - * Process new WebSocket connection from client application. - * - * @param socket Incoming client's WebSocket connection. - */ - onConnection(socket: WebSocket) { - const clientId = `client#${this.nextClientId++}`; - this.clients.set(clientId, socket); - - this.fastify.log.debug({ msg: 'API client connected', clientId }); - this.clients.set(clientId, socket); - - const onClose = () => { - this.fastify.log.debug({ - msg: 'API client disconnected', - clientId, - }); - this.clients.delete(clientId); - }; - - socket.addEventListener('error', onClose); - socket.addEventListener('close', onClose); - } } diff --git a/packages/dev-server/src/plugins/wss/servers/WebSocketDevClientServer.ts b/packages/dev-server/src/plugins/wss/servers/WebSocketDevClientServer.ts index 54835ca78..8a48744a5 100644 --- a/packages/dev-server/src/plugins/wss/servers/WebSocketDevClientServer.ts +++ b/packages/dev-server/src/plugins/wss/servers/WebSocketDevClientServer.ts @@ -1,3 +1,4 @@ +import type { IncomingMessage } from 'node:http'; import type { FastifyInstance } from 'fastify'; import type WebSocket from 'ws'; import { WebSocketServer } from '../WebSocketServer.js'; @@ -9,9 +10,6 @@ import { WebSocketServer } from '../WebSocketServer.js'; * @category Development server */ export class WebSocketDevClientServer extends WebSocketServer { - private clients = new Map(); - private nextClientId = 0; - /** * Create new instance of WebSocketDevClientServer and attach it to the given Fastify instance. * Any logging information, will be passed through standard `fastify.log` API. @@ -19,7 +17,7 @@ export class WebSocketDevClientServer extends WebSocketServer { * @param fastify Fastify instance to attach the WebSocket server to. */ constructor(fastify: FastifyInstance) { - super(fastify, '/__client'); + super(fastify, { name: 'React Native', path: '/__client' }); } /** @@ -47,28 +45,13 @@ export class WebSocketDevClientServer extends WebSocketServer { } } - /** - * Process new WebSocket connection from client application. - * - * @param socket Incoming client's WebSocket connection. - */ - onConnection(socket: WebSocket) { - const clientId = `client#${this.nextClientId++}`; - this.clients.set(clientId, socket); - this.fastify.log.debug({ msg: 'React Native client connected', clientId }); - - const onClose = () => { - this.fastify.log.debug({ - msg: 'React Native client disconnected', - clientId, - }); - this.clients.delete(clientId); - }; + override onConnection(socket: WebSocket, request: IncomingMessage): string { + const clientId = super.onConnection(socket, request); - socket.addEventListener('error', onClose); - socket.addEventListener('close', onClose); socket.addEventListener('message', (event) => { this.processMessage(event.data.toString()); }); + + return clientId; } } diff --git a/packages/dev-server/src/plugins/wss/servers/WebSocketEventsServer.ts b/packages/dev-server/src/plugins/wss/servers/WebSocketEventsServer.ts index 286ae6368..8b4f8f75a 100644 --- a/packages/dev-server/src/plugins/wss/servers/WebSocketEventsServer.ts +++ b/packages/dev-server/src/plugins/wss/servers/WebSocketEventsServer.ts @@ -1,3 +1,4 @@ +import type { IncomingMessage } from 'node:http'; import type { FastifyInstance } from 'fastify'; import * as prettyFormat from 'pretty-format'; import type WebSocket from 'ws'; @@ -41,9 +42,6 @@ export interface EventMessage { export class WebSocketEventsServer extends WebSocketServer { static readonly PROTOCOL_VERSION = 2; - private clients = new Map(); - private nextClientId = 0; - /** * Create new instance of WebSocketHMRServer and attach it to the given Fastify instance. * Any logging information, will be passed through standard `fastify.log` API. @@ -55,10 +53,14 @@ export class WebSocketEventsServer extends WebSocketServer { fastify: FastifyInstance, private config: WebSocketEventsServerConfig ) { - super(fastify, '/events', { - verifyClient: (({ origin }) => { - return /^(https?:\/\/localhost|file:\/\/)/.test(origin); - }) as WebSocket.VerifyClientCallbackSync, + super(fastify, { + name: 'Events', + path: '/events', + wss: { + verifyClient: (({ origin }) => { + return /^(https?:\/\/localhost|file:\/\/)/.test(origin); + }) as WebSocket.VerifyClientCallbackSync, + }, }); } @@ -159,24 +161,9 @@ export class WebSocketEventsServer extends WebSocketServer { } } - /** - * Process new client's WebSocket connection. - * - * @param socket Incoming WebSocket connection. - */ - onConnection(socket: WebSocket) { - const clientId = `client#${this.nextClientId++}`; - this.clients.set(clientId, socket); - this.fastify.log.debug({ msg: 'Events client connected', clientId }); - - const onClose = () => { - this.fastify.log.debug({ msg: 'Events client disconnected', clientId }); - socket.removeAllListeners(); - this.clients.delete(clientId); - }; - - socket.addEventListener('error', onClose); - socket.addEventListener('close', onClose); + override onConnection(socket: WebSocket, request: IncomingMessage) { + const clientId = super.onConnection(socket, request); + socket.addEventListener('message', (event) => { const message = this.parseMessage(event.data.toString()); @@ -203,5 +190,7 @@ export class WebSocketEventsServer extends WebSocketServer { }); } }); + + return clientId; } } diff --git a/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts b/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts index 8d5263ab7..fd9d7b3fd 100644 --- a/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts +++ b/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts @@ -1,9 +1,5 @@ -import type { IncomingMessage } from 'node:http'; -import { URL } from 'node:url'; import type { FastifyInstance } from 'fastify'; -import type WebSocket from 'ws'; import { WebSocketServer } from '../WebSocketServer.js'; -import type { HmrDelegate } from '../types.js'; /** * Class for creating a WebSocket server for Hot Module Replacement. @@ -11,12 +7,6 @@ import type { HmrDelegate } from '../types.js'; * @category Development server */ export class WebSocketHMRServer extends WebSocketServer { - private clients = new Map< - { clientId: string; platform: string }, - WebSocket - >(); - private nextClientId = 0; - /** * Create new instance of WebSocketHMRServer and attach it to the given Fastify instance. * Any logging information, will be passed through standard `fastify.log` API. @@ -24,11 +14,11 @@ export class WebSocketHMRServer extends WebSocketServer { * @param fastify Fastify instance to attach the WebSocket server to. * @param delegate HMR delegate instance. */ - constructor( - fastify: FastifyInstance, - private delegate: HmrDelegate - ) { - super(fastify, delegate.getUriPath()); + constructor(fastify: FastifyInstance) { + super(fastify, { + name: 'HMR', + path: '/__hmr', + }); } /** @@ -38,17 +28,10 @@ export class WebSocketHMRServer extends WebSocketServer { * @param platform Platform of clients to send the event to. * @param clientIds Ids of clients who should receive the event. */ - send(event: any, platform: string, clientIds?: string[]) { + send(event: any) { const data = typeof event === 'string' ? event : JSON.stringify(event); - for (const [key, socket] of this.clients) { - if ( - key.platform !== platform || - !(clientIds ?? [key.clientId]).includes(key.clientId) - ) { - continue; - } - + this.clients.forEach((socket) => { try { socket.send(data); } catch (error) { @@ -56,51 +39,8 @@ export class WebSocketHMRServer extends WebSocketServer { msg: 'Cannot send action to client', event, error, - ...key, }); } - } - } - - /** - * Process new WebSocket connection from HMR client. - * - * @param socket Incoming HMR client's WebSocket connection. - */ - onConnection(socket: WebSocket, request: IncomingMessage) { - const { searchParams } = new URL(request.url || '', 'http://localhost'); - const platform = searchParams.get('platform'); - - if (!platform) { - this.fastify.log.debug({ - msg: 'HMR connection disconnected - missing platform', - }); - socket.close(); - return; - } - - const clientId = `client#${this.nextClientId++}`; - - const client = { - clientId, - platform, - }; - - this.clients.set(client, socket); - - this.fastify.log.debug({ msg: 'HMR client connected', ...client }); - - const onClose = () => { - this.fastify.log.debug({ - msg: 'HMR client disconnected', - ...client, - }); - this.clients.delete(client); - }; - - socket.addEventListener('error', onClose); - socket.addEventListener('close', onClose); - - this.delegate.onClientConnected(platform, clientId); + }); } } diff --git a/packages/dev-server/src/plugins/wss/servers/WebSocketMessageServer.ts b/packages/dev-server/src/plugins/wss/servers/WebSocketMessageServer.ts index 5cc7329e8..ae0d033d8 100644 --- a/packages/dev-server/src/plugins/wss/servers/WebSocketMessageServer.ts +++ b/packages/dev-server/src/plugins/wss/servers/WebSocketMessageServer.ts @@ -25,8 +25,6 @@ export interface ReactNativeMessage { params?: Record; } -type WebSocketWithUpgradeReq = WebSocket & { upgradeReq?: IncomingMessage }; - /** * Class for creating a WebSocket server and sending messages between development server * and the React Native applications. @@ -80,8 +78,7 @@ export class WebSocketMessageServer extends WebSocketServer { ); } - private clients = new Map(); - private nextClientId = 0; + private upgradeRequests: Record = {}; /** * Create new instance of WebSocketMessageServer and attach it to the given Fastify instance. @@ -90,7 +87,7 @@ export class WebSocketMessageServer extends WebSocketServer { * @param fastify Fastify instance to attach the WebSocket server to. */ constructor(fastify: FastifyInstance) { - super(fastify, '/message'); + super(fastify, { name: 'Message', path: '/message' }); } /** @@ -264,9 +261,11 @@ export class WebSocketMessageServer extends WebSocketServer { break; case 'getpeers': { const output: Record> = {}; - this.clients.forEach((peerSocket, peerId) => { + this.clients.forEach((_, peerId) => { if (clientId !== peerId) { - const { searchParams } = new URL(peerSocket.upgradeReq?.url || ''); + const { searchParams } = new URL( + this.upgradeRequests[peerId]?.url || '' + ); output[peerId] = [...searchParams.entries()].reduce( (acc, [key, value]) => { acc[key] = value; @@ -349,27 +348,11 @@ export class WebSocketMessageServer extends WebSocketServer { this.sendBroadcast(undefined, { method, params }); } - /** - * Process new client's WebSocket connection. - * - * @param socket Incoming WebSocket connection. - * @param request Upgrade request for the connection. - */ - onConnection(socket: WebSocket, request: IncomingMessage) { - const clientId = `client#${this.nextClientId++}`; - const client: WebSocketWithUpgradeReq = socket; - client.upgradeReq = request; - this.clients.set(clientId, client); - this.fastify.log.debug({ msg: 'Message client connected', clientId }); + override onConnection(socket: WebSocket, request: IncomingMessage): string { + const clientId = super.onConnection(socket, request); - const onClose = () => { - this.fastify.log.debug({ msg: 'Message client disconnected', clientId }); - socket.removeAllListeners(); - this.clients.delete(clientId); - }; + this.upgradeRequests[clientId] = request; - socket.addEventListener('error', onClose); - socket.addEventListener('close', onClose); socket.addEventListener('message', (event) => { const message = this.parseMessage( event.data.toString(), @@ -409,5 +392,7 @@ export class WebSocketMessageServer extends WebSocketServer { this.handleError(clientId, message, error as Error); } }); + + return clientId; } } diff --git a/packages/dev-server/src/plugins/wss/types.ts b/packages/dev-server/src/plugins/wss/types.ts index b9b48959a..fb615531b 100644 --- a/packages/dev-server/src/plugins/wss/types.ts +++ b/packages/dev-server/src/plugins/wss/types.ts @@ -1,5 +1,6 @@ import type { IncomingMessage } from 'node:http'; import type { Socket } from 'node:net'; +import type { ServerOptions } from 'ws'; /** * Delegate with implementation for HMR-specific functions. @@ -23,3 +24,9 @@ export interface WebSocketServerInterface { shouldUpgrade(pathname: string): boolean; upgrade(request: IncomingMessage, socket: Socket, head: Buffer): void; } + +export type WebSocketServerOptions = { + name: string; + path: string | string[]; + wss?: Omit; +}; From 1f67444367a213b91b0dadb46276a5eb63519c8a Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 26 Dec 2024 02:26:41 +0100 Subject: [PATCH 02/23] refactor: remove getUriPath --- packages/dev-server/src/plugins/wss/types.ts | 3 --- packages/repack/src/commands/rspack/start.ts | 1 - packages/repack/src/commands/webpack/start.ts | 1 - 3 files changed, 5 deletions(-) diff --git a/packages/dev-server/src/plugins/wss/types.ts b/packages/dev-server/src/plugins/wss/types.ts index fb615531b..1ecbedf26 100644 --- a/packages/dev-server/src/plugins/wss/types.ts +++ b/packages/dev-server/src/plugins/wss/types.ts @@ -6,9 +6,6 @@ import type { ServerOptions } from 'ws'; * Delegate with implementation for HMR-specific functions. */ export interface HmrDelegate { - /** Get URI under which HMR server will be running, e.g: `/hmr` */ - getUriPath: () => string; - /** * Callback for when the new HMR client is connected. * diff --git a/packages/repack/src/commands/rspack/start.ts b/packages/repack/src/commands/rspack/start.ts index d9b09cc42..accbe0fb7 100644 --- a/packages/repack/src/commands/rspack/start.ts +++ b/packages/repack/src/commands/rspack/start.ts @@ -159,7 +159,6 @@ export async function start( }, }, hmr: { - getUriPath: () => '/__hmr', onClientConnected: (platform, clientId) => { ctx.broadcastToHmrClients( { action: 'sync', body: compiler.getHmrBody(platform) }, diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index 841257dd6..a225210d3 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -192,7 +192,6 @@ export async function start(_: string[], config: Config, args: StartArguments) { }, }, hmr: { - getUriPath: () => '/__hmr', onClientConnected: (platform, clientId) => { ctx.broadcastToHmrClients( { action: 'sync', body: createHmrBody(lastStats[platform]) }, From 3e2127617dbf7251c02ecb8b1067751b73c9d2b4 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 26 Dec 2024 02:28:07 +0100 Subject: [PATCH 03/23] fix: use wss server name for logging --- .../dev-server/src/plugins/wss/WebSocketServer.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dev-server/src/plugins/wss/WebSocketServer.ts b/packages/dev-server/src/plugins/wss/WebSocketServer.ts index e47bed411..2054cb4f9 100644 --- a/packages/dev-server/src/plugins/wss/WebSocketServer.ts +++ b/packages/dev-server/src/plugins/wss/WebSocketServer.ts @@ -38,14 +38,11 @@ export abstract class WebSocketServer */ constructor(fastify: FastifyInstance, options: WebSocketServerOptions) { this.fastify = fastify; - this.name = options.name; - + this.paths = Array.isArray(options.path) ? options.path : [options.path]; this.server = new WebSocketServerImpl({ noServer: true, ...options.wss }); this.server.on('connection', this.onConnection.bind(this)); - this.paths = Array.isArray(options.path) ? options.path : [options.path]; - this.clients = new Map(); } @@ -62,10 +59,13 @@ export abstract class WebSocketServer onConnection(socket: T, _request: IncomingMessage): string { const clientId = `client#${this.nextClientId++}`; this.clients.set(clientId, socket); - this.fastify.log.debug({ msg: 'API client connected', clientId }); + this.fastify.log.debug({ msg: `${this.name} client connected`, clientId }); const errorHandler = () => { - this.fastify.log.debug({ msg: 'API client disconnected', clientId }); + this.fastify.log.debug({ + msg: `${this.name} client disconnected`, + clientId, + }); socket.removeAllListeners(); // should we do this? this.clients.delete(clientId); }; From 05533b4b86a13f50cc3486472f2a53123297c14b Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 26 Dec 2024 12:46:05 +0100 Subject: [PATCH 04/23] feat: add hearbeat to sockets --- .../dev-server/src/plugins/multipart/types.ts | 6 ++++++ .../src/plugins/wss/WebSocketServer.ts | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/dev-server/src/plugins/multipart/types.ts b/packages/dev-server/src/plugins/multipart/types.ts index f0ca53801..fef42f01d 100644 --- a/packages/dev-server/src/plugins/multipart/types.ts +++ b/packages/dev-server/src/plugins/multipart/types.ts @@ -15,3 +15,9 @@ declare module 'fastify' { asMultipart: () => MultipartHandler | undefined; } } + +declare module 'ws' { + interface WebSocket { + isAlive?: boolean; + } +} diff --git a/packages/dev-server/src/plugins/wss/WebSocketServer.ts b/packages/dev-server/src/plugins/wss/WebSocketServer.ts index 2054cb4f9..9866088b2 100644 --- a/packages/dev-server/src/plugins/wss/WebSocketServer.ts +++ b/packages/dev-server/src/plugins/wss/WebSocketServer.ts @@ -28,6 +28,8 @@ export abstract class WebSocketServer protected clients: Map; protected nextClientId = 0; + private timer: NodeJS.Timer | null = null; + /** * Create a new instance of the WebSocketServer. * Any logging information, will be passed through standard `fastify.log` API. @@ -44,6 +46,18 @@ export abstract class WebSocketServer this.server.on('connection', this.onConnection.bind(this)); this.clients = new Map(); + + // setup heartbeat timer + this.timer = setInterval(() => { + this.clients.forEach((socket) => { + if (!socket.isAlive) { + socket.terminate(); + } else { + socket.isAlive = false; + socket.ping(() => {}); + } + }); + }, 30000); } shouldUpgrade(pathname: string) { @@ -73,6 +87,12 @@ export abstract class WebSocketServer socket.addEventListener('error', errorHandler); socket.addEventListener('close', errorHandler); + // heartbeat + socket.isAlive = true; + socket.on('pong', () => { + socket.isAlive = true; + }); + return clientId; } } From 185eb6d98090bcc695c89519a6c4fad9d0257282 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 26 Dec 2024 22:00:36 +0100 Subject: [PATCH 05/23] chore: reworked HMR client --- packages/repack/src/commands/webpack/start.ts | 4 +- packages/repack/src/commands/webpack/types.ts | 13 - .../repack/src/modules/WebpackHMRClient.ts | 232 ++++++++++-------- packages/repack/src/types.ts | 4 +- 4 files changed, 137 insertions(+), 116 deletions(-) diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index a225210d3..deeeb0143 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -12,6 +12,7 @@ import { composeReporters, makeLogEntryFromFastifyLog, } from '../../logging/index.js'; +import type { HMRMessageBody } from '../../types.js'; import { getMimeType, getWebpackConfigFilePath, @@ -22,7 +23,6 @@ import { import { DEFAULT_HOSTNAME, DEFAULT_PORT } from '../consts.js'; import type { StartArguments, StartCliOptions } from '../types.js'; import { Compiler } from './Compiler.js'; -import type { HMRMessageBody } from './types.js'; /** * Start command for React Native Community CLI. @@ -247,7 +247,9 @@ function createHmrBody( name: stats.name ?? '', time: stats.time ?? 0, hash: stats.hash ?? '', + // @ts-expect-error rspack warnings are identical in shape to webpack warnings warnings: stats.warnings || [], + // @ts-expect-error rspack errors are identical in shape to webpack errors errors: stats.errors || [], }; } diff --git a/packages/repack/src/commands/webpack/types.ts b/packages/repack/src/commands/webpack/types.ts index cbf09a534..bf0799204 100644 --- a/packages/repack/src/commands/webpack/types.ts +++ b/packages/repack/src/commands/webpack/types.ts @@ -6,19 +6,6 @@ export interface WebpackWorkerOptions { platform: string; } -export interface HMRMessageBody { - name: string; - time: number; - hash: string; - warnings: StatsCompilation['warnings']; - errors: StatsCompilation['errors']; -} - -export interface HMRMessage { - action: 'building' | 'built' | 'sync'; - body: HMRMessageBody | null; -} - type WebpackStatsAsset = RemoveRecord; export interface CompilerAsset { diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index 6214cad1b..f09799e27 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -1,4 +1,4 @@ -import type { HMRMessage, HMRMessageBody } from '../types.js'; +import type { HMRMessage } from '../types.js'; import { getDevServerLocation } from './getDevServerLocation.js'; interface LoadingViewModule { @@ -9,7 +9,10 @@ interface LoadingViewModule { class HMRClient { url: string; socket: WebSocket; - lastHash = ''; + // state + isFirstCompilation = true; + lastCompilationHash: string | null = null; + hasCompileErrors = false; constructor( private app: { @@ -47,125 +50,154 @@ class HMRClient { }; } - upToDate(hash?: string) { - if (hash) { - this.lastHash = hash; - } - return this.lastHash === __webpack_hash__; - } - processMessage(message: HMRMessage) { switch (message.action) { - case 'building': - this.app.showLoadingView('Rebuilding...', 'refresh'); - console.debug('[HMRClient] Bundle rebuilding', { - name: message.body?.name, - }); + case 'compiling': + this.handleCompilationInProgress(); break; - case 'built': - console.debug('[HMRClient] Bundle rebuilt', { - name: message.body?.name, - time: message.body?.time, - }); - // Fall through - case 'sync': - if (!message.body) { - console.warn('[HMRClient] HMR message body is empty'); - return; - } + case 'hash': + this.handleHashUpdate(message.body.hash); + break; + case 'still-ok': + case 'ok': + this.handleSuccess(); + break; + case 'warnings': + this.handleWarnings(message.body.warnings); + break; + case 'errors': + this.handleErrors(message.body.errors); + break; + } + } - if (message.body.errors?.length) { - message.body.errors.forEach((error) => { - console.error('Cannot apply update due to error:', error); - }); - this.app.hideLoadingView(); - return; - } + handleCompilationInProgress() { + console.debug('[HMRClient] Processing progress update'); - if (message.body.warnings?.length) { - message.body.warnings.forEach((warning) => { - console.warn('[HMRClient] Bundle contains warnings:', warning); - }); - } + this.app.showLoadingView( + 'Compiling...', + this.isFirstCompilation ? 'load' : 'refresh' + ); + } + + handleHashUpdate(hash: string) { + console.debug('[HMRClient] Processing hash update'); - this.applyUpdate(message.body); + this.lastCompilationHash = hash; + + if (this.isUpdateAvailable()) { + this.app.dismissErrors(); } } - applyUpdate(update: HMRMessageBody) { - if (!module.hot) { - throw new Error('[HMRClient] Hot Module Replacement is disabled.'); - } + handleSuccess() { + console.debug('[HMRClient] Processing bundle update'); + + this.app.dismissErrors(); - if (!this.upToDate(update.hash) && module.hot.status() === 'idle') { - console.debug('[HMRClient] Checking for updates on the server...'); - void this.checkUpdates(update); + const isHotUpdate = !this.isFirstCompilation; + this.isFirstCompilation = false; + this.hasCompileErrors = false; + + // Attempt to apply hot updates or reload. + if (isHotUpdate) { + this.tryApplyUpdates(); } + + this.app.hideLoadingView(); } - async checkUpdates(update: HMRMessageBody) { - try { - this.app.showLoadingView('Refreshing...', 'refresh'); - const updatedModules = await module.hot?.check(false); - if (!updatedModules) { - console.warn('[HMRClient] Cannot find update - full reload needed'); - this.app.reload(); - return; - } + // Compilation with warnings (e.g. ESLint). + handleWarnings(warnings: any[] = []) { + console.debug('[HMRClient] Processing bundle warnings'); - const renewedModules = await module.hot?.apply({ - ignoreDeclined: true, - ignoreUnaccepted: false, - ignoreErrored: false, - onDeclined: (data) => { - // This module declined update, no need to do anything - console.warn('[HMRClient] Ignored an update due to declined module', { - chain: data.chain, - }); - }, - }); - - if (!this.upToDate()) { - void this.checkUpdates(update); - return; - } - - // No modules updated - leave everything as it is (including errors) - if (!renewedModules || renewedModules.length === 0) { - console.debug('[HMRClient] No renewed modules - app is up to date'); - return; - } + this.app.dismissErrors(); - // Double check to make sure all updated modules were accepted (renewed) - const unacceptedModules = updatedModules.filter((moduleId) => { - return renewedModules.indexOf(moduleId) < 0; - }); + const isHotUpdate = !this.isFirstCompilation; + this.isFirstCompilation = false; + this.hasCompileErrors = false; - if (unacceptedModules.length) { + for (let i = 0; i < warnings.length; i++) { + if (i === 5) { console.warn( - '[HMRClient] Not every module was accepted - full reload needed', - { unacceptedModules } + 'There were more warnings in other files, you can find a complete log in the terminal.' ); - this.app.reload(); - } else { - console.debug('[HMRClient] Renewed modules - app is up to date', { - renewedModules, - }); - this.app.dismissErrors(); + break; } - } catch (error) { - if (module.hot?.status() === 'fail' || module.hot?.status() === 'abort') { - console.warn( - '[HMRClient] Cannot check for update - full reload needed' - ); - console.warn('[HMRClient]', error); + console.warn(warnings[i]); + } + + // Attempt to apply hot updates or reload. + if (isHotUpdate) { + this.tryApplyUpdates(); + } + + this.app.hideLoadingView(); + } + + // Compilation with errors (e.g. syntax error or missing modules). + handleErrors(errors: any[] = []) { + console.debug('[HMRClient] Processing bundle errors'); + + this.app.dismissErrors(); + + this.isFirstCompilation = false; + this.hasCompileErrors = true; + + // Also log them to the console. + for (const error of errors) { + console.error(error); + } + + this.app.hideLoadingView(); + } + + isUpdateAvailable() { + return this.lastCompilationHash === __webpack_hash__; + } + + // Attempt to update code on the fly, fall back to a hard reload. + tryApplyUpdates() { + // detect is there a newer version of this code available + if (!this.isUpdateAvailable()) { + return; + } + + if (!module.hot) { + // HMR is not enabled + this.app.reload(); + return; + } + + if (module.hot.status() !== 'idle') { + // HMR is disallowed in other states than 'idle' + return; + } + + const handleApplyUpdates = ( + err: unknown, + updatedModules: (string | number)[] | null + ) => { + const forcedReload = err || !updatedModules; + if (forcedReload) { + if (err) { + console.error('[HMR] Forced reload caused by: ', err); + } this.app.reload(); - } else { - console.warn('[HMRClient] Update check failed', { error }); + return; } - } finally { - this.app.hideLoadingView(); - } + + if (this.isUpdateAvailable()) { + // While we were updating, there was a new update! Do it again. + this.tryApplyUpdates(); + } + }; + + console.debug('[HMRClient] Checking for updates on the server...'); + module.hot.check(true).then( + (outdatedModules) => handleApplyUpdates(null, outdatedModules), + (err) => handleApplyUpdates(err, null) + ); } } diff --git a/packages/repack/src/types.ts b/packages/repack/src/types.ts index cdb8d9577..f394dae9b 100644 --- a/packages/repack/src/types.ts +++ b/packages/repack/src/types.ts @@ -90,8 +90,8 @@ export interface HMRMessageBody { } export interface HMRMessage { - action: 'building' | 'built' | 'sync'; - body: HMRMessageBody | null; + action: 'compiling' | 'hash' | 'still-ok' | 'ok' | 'warnings' | 'errors'; + body: HMRMessageBody; } export interface Logger { From 1b36c88943690fca79c17b67dd207a88a5ae3160 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Dec 2024 00:41:43 +0100 Subject: [PATCH 06/23] refactor: dont pass delegate to HMR WSS --- packages/dev-server/src/createServer.ts | 4 ++-- packages/dev-server/src/plugins/wss/wssPlugin.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev-server/src/createServer.ts b/packages/dev-server/src/createServer.ts index b858159bf..a9e57a5ca 100644 --- a/packages/dev-server/src/createServer.ts +++ b/packages/dev-server/src/createServer.ts @@ -53,8 +53,8 @@ export async function createServer(config: Server.Config) { platform, }); }, - broadcastToHmrClients: (event, platform, clientIds) => { - instance.wss.hmrServer.send(event, platform, clientIds); + broadcastToHmrClients: (event) => { + instance.wss.hmrServer.send(event); }, broadcastToMessageClients: ({ method, params }) => { instance.wss.messageServer.broadcast(method, params); diff --git a/packages/dev-server/src/plugins/wss/wssPlugin.ts b/packages/dev-server/src/plugins/wss/wssPlugin.ts index a9bb4c8a0..332d79ee1 100644 --- a/packages/dev-server/src/plugins/wss/wssPlugin.ts +++ b/packages/dev-server/src/plugins/wss/wssPlugin.ts @@ -49,7 +49,7 @@ async function wssPlugin( webSocketMessageServer: messageServer, }); const apiServer = new WebSocketApiServer(instance); - const hmrServer = new WebSocketHMRServer(instance, delegate.hmr); + const hmrServer = new WebSocketHMRServer(instance); // @react-native/dev-middleware servers const deviceConnectionServer = new WebSocketServerAdapter( From e9a9e15938c7b5c5fad4e84dd26d6c0ab574e7fe Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Dec 2024 13:01:28 +0100 Subject: [PATCH 07/23] fix: make ios hmr work again --- .../repack/src/commands/rspack/Compiler.ts | 24 +++++++++++++------ .../repack/src/modules/WebpackHMRClient.ts | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 922f2f8a9..ee83143fa 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -86,6 +86,10 @@ export class Compiler { }); } this.devServerContext.notifyBuildStart(platform); + // @ts-ignore + this.devServerContext.broadcastToHmrClients({ + action: 'compiling', + }); }); }); @@ -112,6 +116,13 @@ export class Compiler { warnings: true, }); + // @ts-ignore + this.devServerContext.broadcastToHmrClients({ + action: 'hash', + // @ts-ignore + body: { hash: stats.children[0].hash }, + }); + try { stats.children?.map((childStats) => { const platform = childStats.name!; @@ -156,9 +167,7 @@ export class Compiler { }, // keep old assets, discard HMR-related ones Object.fromEntries( - Object.entries(this.assetsCache[platform] ?? {}).filter( - ([_, asset]) => !asset.info.hotModuleReplacement - ) + Object.entries(this.assetsCache[platform] ?? {}) ) ); }); @@ -180,10 +189,11 @@ export class Compiler { const platform = childStats.name!; this.callPendingResolvers(platform); this.devServerContext.notifyBuildEnd(platform); - this.devServerContext.broadcastToHmrClients( - { action: 'built', body: this.getHmrBody(platform) }, - platform - ); + // @ts-ignore + this.devServerContext.broadcastToHmrClients({ + action: 'ok', + body: this.getHmrBody(platform), + }); }); }); } diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index f09799e27..a8d8effd6 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -153,7 +153,7 @@ class HMRClient { } isUpdateAvailable() { - return this.lastCompilationHash === __webpack_hash__; + return this.lastCompilationHash !== __webpack_hash__; } // Attempt to update code on the fly, fall back to a hard reload. From 2642d88e116afa151f95a5a036e3bef1950db0d6 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Dec 2024 13:02:55 +0100 Subject: [PATCH 08/23] chore: update podfile lock --- apps/tester-app/ios/Podfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/tester-app/ios/Podfile.lock b/apps/tester-app/ios/Podfile.lock index e8a3d45df..1e47a932b 100644 --- a/apps/tester-app/ios/Podfile.lock +++ b/apps/tester-app/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - callstack-repack (5.0.0-rc.4): + - callstack-repack (5.0.0-rc.5): - DoubleConversion - glog - hermes-engine @@ -1942,7 +1942,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - callstack-repack: 8972107231b7175b8340e84ca0370aa2c735dcc7 + callstack-repack: dee6e27f84a4e1e153c9bd63744f02e19527ba5f DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be @@ -2005,7 +2005,7 @@ SPEC CHECKSUMS: React-utils: 2bcaf4f4dfe361344bce2fae428603d518488630 ReactCodegen: ae99a130606068ed40d1d9c0d5f25fda142a0647 ReactCommon: 89c87b343deacc8610b099ac764848f0ce937e3e - ReactNativeHost: ac28612b0443705f9aa90563ffb9647f5608613c + ReactNativeHost: 40da1d9878e16dd3b647d4c31e5afeb0ed84683d ReactTestApp-DevSupport: 4aa6f6bc658a2577bcf896c63c411cb375b89d7d ReactTestApp-Resources: 8d72c3deef156833760694a288ff334af4d427d7 RNCAsyncStorage: 3ad840f7b17b45ca7ebbbb0e80948564a9513315 @@ -2015,6 +2015,6 @@ SPEC CHECKSUMS: SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6 Yoga: f6dc1b6029519815d5516a1241821c6a9074af6d -PODFILE CHECKSUM: 591811811bdab95f1675c6871b0554706bf77020 +PODFILE CHECKSUM: 6d7cbe03444d5e87210979fb32a0eca299d758fe COCOAPODS: 1.15.2 From 03e39a0de3f5905734fcaebc1bd3c63030f5f7c2 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 18 Feb 2025 11:37:32 +0100 Subject: [PATCH 09/23] chore: move ws type extension to wss types --- packages/dev-server/src/plugins/multipart/types.ts | 6 ------ packages/dev-server/src/plugins/wss/wssPlugin.ts | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dev-server/src/plugins/multipart/types.ts b/packages/dev-server/src/plugins/multipart/types.ts index fef42f01d..f0ca53801 100644 --- a/packages/dev-server/src/plugins/multipart/types.ts +++ b/packages/dev-server/src/plugins/multipart/types.ts @@ -15,9 +15,3 @@ declare module 'fastify' { asMultipart: () => MultipartHandler | undefined; } } - -declare module 'ws' { - interface WebSocket { - isAlive?: boolean; - } -} diff --git a/packages/dev-server/src/plugins/wss/wssPlugin.ts b/packages/dev-server/src/plugins/wss/wssPlugin.ts index be48c8bd5..b3079bff4 100644 --- a/packages/dev-server/src/plugins/wss/wssPlugin.ts +++ b/packages/dev-server/src/plugins/wss/wssPlugin.ts @@ -25,6 +25,12 @@ declare module 'fastify' { } } +declare module 'ws' { + interface WebSocket { + isAlive?: boolean; + } +} + /** * Defined in @react-native/dev-middleware * Reference: https://github.com/facebook/react-native/blob/main/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js From 0fe4a852e23b3074a45dc03bdef6895c79b35121 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 18 Feb 2025 11:55:29 +0100 Subject: [PATCH 10/23] chore: remove noop sync action and hmr delegate --- packages/dev-server/src/plugins/wss/types.ts | 15 --------------- packages/dev-server/src/types.ts | 5 ----- packages/repack/src/commands/rspack/Compiler.ts | 1 - packages/repack/src/commands/rspack/start.ts | 9 --------- packages/repack/src/commands/webpack/start.ts | 9 --------- 5 files changed, 39 deletions(-) diff --git a/packages/dev-server/src/plugins/wss/types.ts b/packages/dev-server/src/plugins/wss/types.ts index 1ecbedf26..d57f4778e 100644 --- a/packages/dev-server/src/plugins/wss/types.ts +++ b/packages/dev-server/src/plugins/wss/types.ts @@ -2,21 +2,6 @@ import type { IncomingMessage } from 'node:http'; import type { Socket } from 'node:net'; import type { ServerOptions } from 'ws'; -/** - * Delegate with implementation for HMR-specific functions. - */ -export interface HmrDelegate { - /** - * Callback for when the new HMR client is connected. - * - * Useful for running initial synchronization or any other side effect. - * - * @param platform Platform of the connected client. - * @param clientId Id of the connected client. - */ - onClientConnected: (platform: string, clientId: string) => void; -} - export interface WebSocketServerInterface { shouldUpgrade(pathname: string): boolean; upgrade(request: IncomingMessage, socket: Socket, head: Buffer): void; diff --git a/packages/dev-server/src/types.ts b/packages/dev-server/src/types.ts index 5a6d8d236..01ada938b 100644 --- a/packages/dev-server/src/types.ts +++ b/packages/dev-server/src/types.ts @@ -9,7 +9,6 @@ import type { SymbolicatorDelegate, SymbolicatorResults, } from './plugins/symbolicate/types.js'; -import type { HmrDelegate } from './plugins/wss/types.js'; import type { NormalizedOptions } from './utils/normalizeOptions.js'; export type { CompilerDelegate }; @@ -21,7 +20,6 @@ export type { SymbolicatorDelegate, SymbolicatorResults, }; -export type { HmrDelegate }; export interface DevServerOptions { /** @@ -77,9 +75,6 @@ export namespace Server { /** A logger delegate. */ logger: LoggerDelegate; - /** An HMR delegate. */ - hmr: HmrDelegate; - /** An messages delegate. */ messages: MessagesDelegate; diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 77b6dee67..1e074c3e6 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -148,7 +148,6 @@ export class Compiler { return acc; }, - // keep old assets, discard HMR-related ones Object.fromEntries( Object.entries(this.assetsCache[platform] ?? {}) ) diff --git a/packages/repack/src/commands/rspack/start.ts b/packages/repack/src/commands/rspack/start.ts index 577953d37..4b65ef1fa 100644 --- a/packages/repack/src/commands/rspack/start.ts +++ b/packages/repack/src/commands/rspack/start.ts @@ -145,15 +145,6 @@ export async function start( return !/webpack[/\\]runtime[/\\].+\s/.test(frame.file); }, }, - hmr: { - onClientConnected: (platform, clientId) => { - ctx.broadcastToHmrClients( - { action: 'sync', body: compiler.getHmrBody(platform) }, - platform, - [clientId] - ); - }, - }, messages: { getHello: () => 'React Native packager is running', getStatus: () => 'packager-status:running', diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index ca2396575..509bf0aa1 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -190,15 +190,6 @@ export async function start( return !/webpack[/\\]runtime[/\\].+\s/.test(frame.file); }, }, - hmr: { - onClientConnected: (platform, clientId) => { - ctx.broadcastToHmrClients( - { action: 'sync', body: createHmrBody(lastStats[platform]) }, - platform, - [clientId] - ); - }, - }, messages: { getHello: () => 'React Native packager is running', getStatus: () => 'packager-status:running', From 67872c39f6b5c09b5f9b983401ecc7659d2c67aa Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 18 Feb 2025 22:22:59 +0100 Subject: [PATCH 11/23] feat: simplify --- .../repack/src/commands/rspack/Compiler.ts | 43 ++++------- .../repack/src/modules/WebpackHMRClient.ts | 74 +++---------------- packages/repack/src/types.ts | 2 +- 3 files changed, 26 insertions(+), 93 deletions(-) diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 1e074c3e6..8b6f7ba03 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -10,7 +10,6 @@ import type { } from '@rspack/core'; import memfs from 'memfs'; import type { Reporter } from '../../logging/types.js'; -import type { HMRMessageBody } from '../../types.js'; import { CLIError } from '../common/error.js'; import { adaptFilenameToPlatform, runAdbReverse } from '../common/index.js'; import { DEV_SERVER_ASSET_TYPES } from '../consts.js'; @@ -72,6 +71,7 @@ export class Compiler { // @ts-ignore this.devServerContext.broadcastToHmrClients({ action: 'compiling', + body: { name: platform }, }); }); }); @@ -80,10 +80,11 @@ export class Compiler { this.isCompilationInProgress = true; this.platforms.forEach((platform) => { this.devServerContext.notifyBuildStart(platform); - this.devServerContext.broadcastToHmrClients( - { action: 'building' }, - platform - ); + // @ts-ignore + this.devServerContext.broadcastToHmrClients({ + action: 'compiling', + body: { name: platform }, + }); }); }); @@ -99,15 +100,8 @@ export class Compiler { warnings: true, }); - // @ts-ignore - this.devServerContext.broadcastToHmrClients({ - action: 'hash', - // @ts-ignore - body: { hash: stats.children[0].hash }, - }); - try { - stats.children?.map((childStats) => { + stats.children!.map((childStats) => { const platform = childStats.name!; this.statsCache[platform] = childStats; @@ -168,13 +162,19 @@ export class Compiler { this.isCompilationInProgress = false; stats.children?.forEach((childStats) => { + // @ts-ignore + this.devServerContext.broadcastToHmrClients({ + action: 'hash', + body: { name: childStats.name, hash: childStats.hash }, + }); + const platform = childStats.name!; this.callPendingResolvers(platform); this.devServerContext.notifyBuildEnd(platform); // @ts-ignore this.devServerContext.broadcastToHmrClients({ action: 'ok', - body: this.getHmrBody(platform), + body: { name: platform }, }); }); }); @@ -287,19 +287,4 @@ export class Compiler { ); } } - - getHmrBody(platform: string): HMRMessageBody | null { - const stats = this.statsCache[platform]; - if (!stats) { - return null; - } - - return { - name: stats.name ?? '', - time: stats.time ?? 0, - hash: stats.hash ?? '', - warnings: stats.warnings || [], - errors: stats.errors || [], - }; - } } diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index a8d8effd6..d41edeec4 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -12,7 +12,6 @@ class HMRClient { // state isFirstCompilation = true; lastCompilationHash: string | null = null; - hasCompileErrors = false; constructor( private app: { @@ -22,7 +21,7 @@ class HMRClient { hideLoadingView: () => void; } ) { - this.url = `ws://${getDevServerLocation().host}/__hmr?platform=${__PLATFORM__}`; + this.url = `ws://${getDevServerLocation().host}/__hmr`; this.socket = new WebSocket(this.url); console.debug('[HMRClient] Connecting...', { @@ -51,6 +50,11 @@ class HMRClient { } processMessage(message: HMRMessage) { + // Only process messages for the target platform + if (message.body.name !== __PLATFORM__) { + return; + } + switch (message.action) { case 'compiling': this.handleCompilationInProgress(); @@ -58,15 +62,8 @@ class HMRClient { case 'hash': this.handleHashUpdate(message.body.hash); break; - case 'still-ok': case 'ok': - this.handleSuccess(); - break; - case 'warnings': - this.handleWarnings(message.body.warnings); - break; - case 'errors': - this.handleErrors(message.body.errors); + this.handleBundleUpdate(); break; } } @@ -90,64 +87,14 @@ class HMRClient { } } - handleSuccess() { + handleBundleUpdate() { console.debug('[HMRClient] Processing bundle update'); this.app.dismissErrors(); - - const isHotUpdate = !this.isFirstCompilation; - this.isFirstCompilation = false; - this.hasCompileErrors = false; - - // Attempt to apply hot updates or reload. - if (isHotUpdate) { - this.tryApplyUpdates(); - } - - this.app.hideLoadingView(); - } - - // Compilation with warnings (e.g. ESLint). - handleWarnings(warnings: any[] = []) { - console.debug('[HMRClient] Processing bundle warnings'); - - this.app.dismissErrors(); - - const isHotUpdate = !this.isFirstCompilation; this.isFirstCompilation = false; - this.hasCompileErrors = false; - - for (let i = 0; i < warnings.length; i++) { - if (i === 5) { - console.warn( - 'There were more warnings in other files, you can find a complete log in the terminal.' - ); - break; - } - console.warn(warnings[i]); - } // Attempt to apply hot updates or reload. - if (isHotUpdate) { - this.tryApplyUpdates(); - } - - this.app.hideLoadingView(); - } - - // Compilation with errors (e.g. syntax error or missing modules). - handleErrors(errors: any[] = []) { - console.debug('[HMRClient] Processing bundle errors'); - - this.app.dismissErrors(); - - this.isFirstCompilation = false; - this.hasCompileErrors = true; - - // Also log them to the console. - for (const error of errors) { - console.error(error); - } + this.tryApplyUpdates(); this.app.hideLoadingView(); } @@ -181,7 +128,8 @@ class HMRClient { const forcedReload = err || !updatedModules; if (forcedReload) { if (err) { - console.error('[HMR] Forced reload caused by: ', err); + console.warn('[HMRClient] Forced reload'); + console.debug('[HMRClient] Forced reload caused by: ', err); } this.app.reload(); return; diff --git a/packages/repack/src/types.ts b/packages/repack/src/types.ts index 1ee9fc4f4..f2f7879e1 100644 --- a/packages/repack/src/types.ts +++ b/packages/repack/src/types.ts @@ -90,7 +90,7 @@ export interface HMRMessageBody { } export interface HMRMessage { - action: 'compiling' | 'hash' | 'still-ok' | 'ok' | 'warnings' | 'errors'; + action: 'compiling' | 'hash' | 'ok'; body: HMRMessageBody; } From 6a7888876f98d0b28cc209f87aa3f9987f1ee356 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 10:32:51 +0100 Subject: [PATCH 12/23] chore: cleanup error logs for HMR --- .../src/plugins/compiler/compilerPlugin.ts | 6 +++--- packages/repack/src/commands/rspack/Compiler.ts | 12 ++++++------ packages/repack/src/modules/WebpackHMRClient.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/dev-server/src/plugins/compiler/compilerPlugin.ts b/packages/dev-server/src/plugins/compiler/compilerPlugin.ts index 98ad7ce48..ce032e50d 100644 --- a/packages/dev-server/src/plugins/compiler/compilerPlugin.ts +++ b/packages/dev-server/src/plugins/compiler/compilerPlugin.ts @@ -26,8 +26,8 @@ async function compilerPlugin( if (!filename) { // This technically should never happen - this route should not be called if file is missing. - request.log.error('File was not provided'); - return reply.notFound(); + request.log.debug('File was not provided'); + return reply.notFound('File was not provided'); } // Let consumer infer the platform. If function is not provided fallback @@ -70,7 +70,7 @@ async function compilerPlugin( return reply.code(200).type(mimeType).send(asset); } } catch (error) { - request.log.error(error); + request.log.debug(error); return reply.notFound((error as Error).message); } }, diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 8b6f7ba03..625312ffd 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -102,6 +102,12 @@ export class Compiler { try { stats.children!.map((childStats) => { + // @ts-ignore + this.devServerContext.broadcastToHmrClients({ + action: 'hash', + body: { name: childStats.name, hash: childStats.hash }, + }); + const platform = childStats.name!; this.statsCache[platform] = childStats; @@ -162,12 +168,6 @@ export class Compiler { this.isCompilationInProgress = false; stats.children?.forEach((childStats) => { - // @ts-ignore - this.devServerContext.broadcastToHmrClients({ - action: 'hash', - body: { name: childStats.name, hash: childStats.hash }, - }); - const platform = childStats.name!; this.callPendingResolvers(platform); this.devServerContext.notifyBuildEnd(platform); diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index d41edeec4..fe305f2d5 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -127,8 +127,8 @@ class HMRClient { ) => { const forcedReload = err || !updatedModules; if (forcedReload) { + console.warn('[HMRClient] Forced reload'); if (err) { - console.warn('[HMRClient] Forced reload'); console.debug('[HMRClient] Forced reload caused by: ', err); } this.app.reload(); From b7b1925b328bf28b722478e25ccf55ce6de408be Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 10:41:12 +0100 Subject: [PATCH 13/23] fix: types for rspack --- .../dev-server/src/plugins/favicon/faviconPlugin.ts | 1 - packages/dev-server/src/types.ts | 6 +----- packages/repack/src/commands/rspack/Compiler.ts | 4 ---- packages/repack/src/modules/WebpackHMRClient.ts | 4 +--- packages/repack/src/types.ts | 12 +----------- 5 files changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/dev-server/src/plugins/favicon/faviconPlugin.ts b/packages/dev-server/src/plugins/favicon/faviconPlugin.ts index bd989abfc..58a299d8f 100644 --- a/packages/dev-server/src/plugins/favicon/faviconPlugin.ts +++ b/packages/dev-server/src/plugins/favicon/faviconPlugin.ts @@ -4,7 +4,6 @@ import type { FastifyInstance } from 'fastify'; import fastifyFavicon from 'fastify-favicon'; import fastifyPlugin from 'fastify-plugin'; -// @ts-ignore const dirname = path.dirname(fileURLToPath(import.meta.url)); const pathToImgDir = path.join(dirname, '../../../static'); diff --git a/packages/dev-server/src/types.ts b/packages/dev-server/src/types.ts index 01ada938b..656590e37 100644 --- a/packages/dev-server/src/types.ts +++ b/packages/dev-server/src/types.ts @@ -108,11 +108,7 @@ export namespace Server { * @param clientIds Ids of the client to which broadcast should be sent. * If `undefined` the broadcast will be sent to all connected clients for the given `platform`. */ - broadcastToHmrClients: ( - event: E, - platform: string, - clientIds?: string[] - ) => void; + broadcastToHmrClients: (event: E) => void; /** * Broadcast arbitrary method-like event to all connected message clients. diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 625312ffd..c4c1705f9 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -68,7 +68,6 @@ export class Compiler { }); } this.devServerContext.notifyBuildStart(platform); - // @ts-ignore this.devServerContext.broadcastToHmrClients({ action: 'compiling', body: { name: platform }, @@ -80,7 +79,6 @@ export class Compiler { this.isCompilationInProgress = true; this.platforms.forEach((platform) => { this.devServerContext.notifyBuildStart(platform); - // @ts-ignore this.devServerContext.broadcastToHmrClients({ action: 'compiling', body: { name: platform }, @@ -102,7 +100,6 @@ export class Compiler { try { stats.children!.map((childStats) => { - // @ts-ignore this.devServerContext.broadcastToHmrClients({ action: 'hash', body: { name: childStats.name, hash: childStats.hash }, @@ -171,7 +168,6 @@ export class Compiler { const platform = childStats.name!; this.callPendingResolvers(platform); this.devServerContext.notifyBuildEnd(platform); - // @ts-ignore this.devServerContext.broadcastToHmrClients({ action: 'ok', body: { name: platform }, diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index fe305f2d5..6b1d4df67 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -60,7 +60,7 @@ class HMRClient { this.handleCompilationInProgress(); break; case 'hash': - this.handleHashUpdate(message.body.hash); + this.handleHashUpdate(message.body.hash!); break; case 'ok': this.handleBundleUpdate(); @@ -178,7 +178,6 @@ if (__DEV__ && module.hot) { LoadingView = require('react-native/Libraries/Utilities/LoadingView'); } - // @ts-ignore LoadingView.showMessage(text, type); }; @@ -190,7 +189,6 @@ if (__DEV__ && module.hot) { LoadingView = require('react-native/Libraries/Utilities/LoadingView'); } - // @ts-ignore LoadingView.hide(); }; diff --git a/packages/repack/src/types.ts b/packages/repack/src/types.ts index f2f7879e1..fce056413 100644 --- a/packages/repack/src/types.ts +++ b/packages/repack/src/types.ts @@ -1,5 +1,3 @@ -import type { StatsCompilation } from '@rspack/core'; - export type Rule = string | RegExp; /** @@ -81,17 +79,9 @@ export interface EnvOptions { devServer?: DevServerOptions; } -export interface HMRMessageBody { - name: string; - time: number; - hash: string; - warnings: StatsCompilation['warnings']; - errors: StatsCompilation['errors']; -} - export interface HMRMessage { action: 'compiling' | 'hash' | 'ok'; - body: HMRMessageBody; + body: { name: string; hash?: string }; } export interface Logger { From 34d867fcb118fc63efb8fb972f785643f62aaeff Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 11:03:19 +0100 Subject: [PATCH 14/23] feat: align webpack --- .../repack/src/commands/rspack/Compiler.ts | 5 +-- .../repack/src/commands/webpack/Compiler.ts | 8 +--- packages/repack/src/commands/webpack/start.ts | 41 ++++++++----------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index c4c1705f9..08c66f510 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -145,9 +145,8 @@ export class Compiler { return acc; }, - Object.fromEntries( - Object.entries(this.assetsCache[platform] ?? {}) - ) + // keep old assets + this.assetsCache[platform] ?? {} ); }); } catch (error) { diff --git a/packages/repack/src/commands/webpack/Compiler.ts b/packages/repack/src/commands/webpack/Compiler.ts index 0d4c4c956..9a0c1904f 100644 --- a/packages/repack/src/commands/webpack/Compiler.ts +++ b/packages/repack/src/commands/webpack/Compiler.ts @@ -91,12 +91,8 @@ export class Compiler extends EventEmitter { this.statsCache[platform] = value.stats; this.assetsCache[platform] = { - // keep old assets, discard HMR-related ones - ...Object.fromEntries( - Object.entries(this.assetsCache[platform] ?? {}).filter( - ([_, asset]) => !asset.info.hotModuleReplacement - ) - ), + // keep old assets + ...(this.assetsCache[platform] ?? {}), // convert asset data Uint8Array to Buffer ...Object.fromEntries( Object.entries(value.assets).map(([name, { data, info, size }]) => { diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index 509bf0aa1..a02cb3eda 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -11,7 +11,6 @@ import { composeReporters, makeLogEntryFromFastifyLog, } from '../../logging/index.js'; -import type { HMRMessageBody } from '../../types.js'; import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; import { CLIError } from '../common/error.js'; import { @@ -125,10 +124,12 @@ export async function start( }); } - const lastStats: Record = {}; - compiler.on('watchRun', ({ platform }) => { ctx.notifyBuildStart(platform); + ctx.broadcastToHmrClients({ + action: 'compiling', + body: { name: platform }, + }); if (platform === 'android') { void runAdbReverse({ port: ctx.options.port, @@ -139,7 +140,10 @@ export async function start( compiler.on('invalid', ({ platform }) => { ctx.notifyBuildStart(platform); - ctx.broadcastToHmrClients({ action: 'building' }, platform); + ctx.broadcastToHmrClients({ + action: 'compiling', + body: { name: platform }, + }); }); compiler.on( @@ -152,11 +156,14 @@ export async function start( stats: StatsCompilation; }) => { ctx.notifyBuildEnd(platform); - lastStats[platform] = stats; - ctx.broadcastToHmrClients( - { action: 'built', body: createHmrBody(stats) }, - platform - ); + ctx.broadcastToHmrClients({ + action: 'hash', + body: { name: platform, hash: stats.hash }, + }); + ctx.broadcastToHmrClients({ + action: 'ok', + body: { name: platform }, + }); } ); @@ -225,19 +232,3 @@ export async function start( }, }; } - -function createHmrBody(stats?: StatsCompilation): HMRMessageBody | null { - if (!stats) { - return null; - } - - return { - name: stats.name ?? '', - time: stats.time ?? 0, - hash: stats.hash ?? '', - // @ts-expect-error rspack warnings are identical in shape to webpack warnings - warnings: stats.warnings || [], - // @ts-expect-error rspack errors are identical in shape to webpack errors - errors: stats.errors || [], - }; -} From 7e1e26424b8bfbe4fd07002aef80a5a8c0475922 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 11:15:09 +0100 Subject: [PATCH 15/23] chore: cleanup --- .../plugins/wss/servers/WebSocketHMRServer.ts | 3 --- packages/dev-server/src/types.ts | 3 --- packages/repack/src/commands/rspack/Compiler.ts | 16 ++++++++-------- packages/repack/src/commands/webpack/start.ts | 9 +++++---- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts b/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts index fd9d7b3fd..39d8534a3 100644 --- a/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts +++ b/packages/dev-server/src/plugins/wss/servers/WebSocketHMRServer.ts @@ -12,7 +12,6 @@ export class WebSocketHMRServer extends WebSocketServer { * Any logging information, will be passed through standard `fastify.log` API. * * @param fastify Fastify instance to attach the WebSocket server to. - * @param delegate HMR delegate instance. */ constructor(fastify: FastifyInstance) { super(fastify, { @@ -25,8 +24,6 @@ export class WebSocketHMRServer extends WebSocketServer { * Send action to all connected HMR clients. * * @param event Event to send to the clients. - * @param platform Platform of clients to send the event to. - * @param clientIds Ids of clients who should receive the event. */ send(event: any) { const data = typeof event === 'string' ? event : JSON.stringify(event); diff --git a/packages/dev-server/src/types.ts b/packages/dev-server/src/types.ts index 656590e37..bfcc712e3 100644 --- a/packages/dev-server/src/types.ts +++ b/packages/dev-server/src/types.ts @@ -104,9 +104,6 @@ export namespace Server { * Broadcast arbitrary event to all connected HMR clients for given `platform`. * * @param event Arbitrary event to broadcast. - * @param platform Platform of the clients to which broadcast should be sent. - * @param clientIds Ids of the client to which broadcast should be sent. - * If `undefined` the broadcast will be sent to all connected clients for the given `platform`. */ broadcastToHmrClients: (event: E) => void; diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 08c66f510..0c4734f84 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -10,6 +10,7 @@ import type { } from '@rspack/core'; import memfs from 'memfs'; import type { Reporter } from '../../logging/types.js'; +import type { HMRMessage } from '../../types.js'; import { CLIError } from '../common/error.js'; import { adaptFilenameToPlatform, runAdbReverse } from '../common/index.js'; import { DEV_SERVER_ASSET_TYPES } from '../consts.js'; @@ -68,7 +69,7 @@ export class Compiler { }); } this.devServerContext.notifyBuildStart(platform); - this.devServerContext.broadcastToHmrClients({ + this.devServerContext.broadcastToHmrClients({ action: 'compiling', body: { name: platform }, }); @@ -79,7 +80,7 @@ export class Compiler { this.isCompilationInProgress = true; this.platforms.forEach((platform) => { this.devServerContext.notifyBuildStart(platform); - this.devServerContext.broadcastToHmrClients({ + this.devServerContext.broadcastToHmrClients({ action: 'compiling', body: { name: platform }, }); @@ -100,15 +101,14 @@ export class Compiler { try { stats.children!.map((childStats) => { - this.devServerContext.broadcastToHmrClients({ + const platform = childStats.name!; + this.devServerContext.broadcastToHmrClients({ action: 'hash', - body: { name: childStats.name, hash: childStats.hash }, + body: { name: platform, hash: childStats.hash }, }); - const platform = childStats.name!; this.statsCache[platform] = childStats; - - const assets = this.statsCache[platform]!.assets!; + const assets = childStats.assets!; this.assetsCache[platform] = assets .filter((asset) => asset.type === 'asset') @@ -167,7 +167,7 @@ export class Compiler { const platform = childStats.name!; this.callPendingResolvers(platform); this.devServerContext.notifyBuildEnd(platform); - this.devServerContext.broadcastToHmrClients({ + this.devServerContext.broadcastToHmrClients({ action: 'ok', body: { name: platform }, }); diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index a02cb3eda..3bbde71c3 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -11,6 +11,7 @@ import { composeReporters, makeLogEntryFromFastifyLog, } from '../../logging/index.js'; +import type { HMRMessage } from '../../types.js'; import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; import { CLIError } from '../common/error.js'; import { @@ -126,7 +127,7 @@ export async function start( compiler.on('watchRun', ({ platform }) => { ctx.notifyBuildStart(platform); - ctx.broadcastToHmrClients({ + ctx.broadcastToHmrClients({ action: 'compiling', body: { name: platform }, }); @@ -140,7 +141,7 @@ export async function start( compiler.on('invalid', ({ platform }) => { ctx.notifyBuildStart(platform); - ctx.broadcastToHmrClients({ + ctx.broadcastToHmrClients({ action: 'compiling', body: { name: platform }, }); @@ -156,11 +157,11 @@ export async function start( stats: StatsCompilation; }) => { ctx.notifyBuildEnd(platform); - ctx.broadcastToHmrClients({ + ctx.broadcastToHmrClients({ action: 'hash', body: { name: platform, hash: stats.hash }, }); - ctx.broadcastToHmrClients({ + ctx.broadcastToHmrClients({ action: 'ok', body: { name: platform }, }); From 38c055c1190949421c9535f6c02fb97decb7cfd1 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 11:25:57 +0100 Subject: [PATCH 16/23] chore: changeset --- .changeset/moody-cheetahs-lick.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/moody-cheetahs-lick.md diff --git a/.changeset/moody-cheetahs-lick.md b/.changeset/moody-cheetahs-lick.md new file mode 100644 index 000000000..4565575e0 --- /dev/null +++ b/.changeset/moody-cheetahs-lick.md @@ -0,0 +1,6 @@ +--- +"@callstack/repack-dev-server": minor +"@callstack/repack": minor +--- + +Reworked DevServer HMR pipeline - improved performance & recovery from errors From 89f75752cae3155cc98425f0bd20b819025adc76 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 11:49:32 +0100 Subject: [PATCH 17/23] fix: dont dismiss errors on hash --- packages/repack/src/modules/WebpackHMRClient.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index 6b1d4df67..7ce970fb1 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -71,20 +71,12 @@ class HMRClient { handleCompilationInProgress() { console.debug('[HMRClient] Processing progress update'); - this.app.showLoadingView( - 'Compiling...', - this.isFirstCompilation ? 'load' : 'refresh' - ); + this.app.showLoadingView('Compiling...', 'refresh'); } handleHashUpdate(hash: string) { console.debug('[HMRClient] Processing hash update'); - this.lastCompilationHash = hash; - - if (this.isUpdateAvailable()) { - this.app.dismissErrors(); - } } handleBundleUpdate() { From f0e64530466f674934820fabfe13a191050b0159 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 12:07:31 +0100 Subject: [PATCH 18/23] fix: dont dismiss errors when there are compilation errors --- .../repack/src/commands/rspack/Compiler.ts | 2 +- packages/repack/src/commands/webpack/start.ts | 2 +- .../repack/src/modules/WebpackHMRClient.ts | 21 ++++++++----------- packages/repack/src/types.ts | 6 +++++- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 0c4734f84..802128178 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -169,7 +169,7 @@ export class Compiler { this.devServerContext.notifyBuildEnd(platform); this.devServerContext.broadcastToHmrClients({ action: 'ok', - body: { name: platform }, + body: { name: platform, hasErrors: !!childStats.errors?.length }, }); }); }); diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index 3bbde71c3..5e0e8a117 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -163,7 +163,7 @@ export async function start( }); ctx.broadcastToHmrClients({ action: 'ok', - body: { name: platform }, + body: { name: platform, hasErrors: !!stats.errors?.length }, }); } ); diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index 7ce970fb1..77a59105e 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -10,7 +10,6 @@ class HMRClient { url: string; socket: WebSocket; // state - isFirstCompilation = true; lastCompilationHash: string | null = null; constructor( @@ -60,34 +59,32 @@ class HMRClient { this.handleCompilationInProgress(); break; case 'hash': - this.handleHashUpdate(message.body.hash!); + this.handleHashUpdate(message.body.hash); break; case 'ok': - this.handleBundleUpdate(); + this.handleBundleUpdate(message.body.hasErrors); break; } } handleCompilationInProgress() { console.debug('[HMRClient] Processing progress update'); - this.app.showLoadingView('Compiling...', 'refresh'); } - handleHashUpdate(hash: string) { + handleHashUpdate(hash?: string) { console.debug('[HMRClient] Processing hash update'); - this.lastCompilationHash = hash; + this.lastCompilationHash = hash ?? null; } - handleBundleUpdate() { + handleBundleUpdate(hasErrors?: boolean) { console.debug('[HMRClient] Processing bundle update'); + // only dismiss errors when there are no compilation errors + if (hasErrors) { + this.app.dismissErrors(); + } - this.app.dismissErrors(); - this.isFirstCompilation = false; - - // Attempt to apply hot updates or reload. this.tryApplyUpdates(); - this.app.hideLoadingView(); } diff --git a/packages/repack/src/types.ts b/packages/repack/src/types.ts index fce056413..6277bc156 100644 --- a/packages/repack/src/types.ts +++ b/packages/repack/src/types.ts @@ -81,7 +81,11 @@ export interface EnvOptions { export interface HMRMessage { action: 'compiling' | 'hash' | 'ok'; - body: { name: string; hash?: string }; + body: { + name: string; + hash?: string; + hasErrors?: boolean; + }; } export interface Logger { From dac87f48d2f22f8920c9cba67242b11b609a24f0 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 12:24:35 +0100 Subject: [PATCH 19/23] fix: hasErrors condition --- packages/repack/src/modules/WebpackHMRClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index 77a59105e..dec079acd 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -80,7 +80,7 @@ class HMRClient { handleBundleUpdate(hasErrors?: boolean) { console.debug('[HMRClient] Processing bundle update'); // only dismiss errors when there are no compilation errors - if (hasErrors) { + if (!hasErrors) { this.app.dismissErrors(); } From 08b91bd3ed4254062c256846b4ec6c32e32d3820 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 12:58:17 +0100 Subject: [PATCH 20/23] fix: better dismiss error handling --- .../repack/src/commands/rspack/Compiler.ts | 2 +- packages/repack/src/commands/webpack/start.ts | 2 +- .../repack/src/modules/WebpackHMRClient.ts | 25 ++++++----- packages/repack/src/types.ts | 6 +-- .../repack/src/types/runtime-globals.d.ts | 44 ++++++++++--------- 5 files changed, 41 insertions(+), 38 deletions(-) diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 802128178..0c4734f84 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -169,7 +169,7 @@ export class Compiler { this.devServerContext.notifyBuildEnd(platform); this.devServerContext.broadcastToHmrClients({ action: 'ok', - body: { name: platform, hasErrors: !!childStats.errors?.length }, + body: { name: platform }, }); }); }); diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts index 5e0e8a117..3bbde71c3 100644 --- a/packages/repack/src/commands/webpack/start.ts +++ b/packages/repack/src/commands/webpack/start.ts @@ -163,7 +163,7 @@ export async function start( }); ctx.broadcastToHmrClients({ action: 'ok', - body: { name: platform, hasErrors: !!stats.errors?.length }, + body: { name: platform }, }); } ); diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index dec079acd..d6a2a205f 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -62,7 +62,7 @@ class HMRClient { this.handleHashUpdate(message.body.hash); break; case 'ok': - this.handleBundleUpdate(message.body.hasErrors); + this.handleBundleUpdate(); break; } } @@ -77,13 +77,8 @@ class HMRClient { this.lastCompilationHash = hash ?? null; } - handleBundleUpdate(hasErrors?: boolean) { + handleBundleUpdate() { console.debug('[HMRClient] Processing bundle update'); - // only dismiss errors when there are no compilation errors - if (!hasErrors) { - this.app.dismissErrors(); - } - this.tryApplyUpdates(); this.app.hideLoadingView(); } @@ -131,10 +126,18 @@ class HMRClient { }; console.debug('[HMRClient] Checking for updates on the server...'); - module.hot.check(true).then( - (outdatedModules) => handleApplyUpdates(null, outdatedModules), - (err) => handleApplyUpdates(err, null) - ); + module.hot + .check({ + onAccepted: this.app.dismissErrors, + onDeclined: this.app.dismissErrors, + onErrored: this.app.dismissErrors, + onUnaccepted: this.app.dismissErrors, + onDisposed: this.app.dismissErrors, + }) + .then( + (outdatedModules) => handleApplyUpdates(null, outdatedModules), + (err) => handleApplyUpdates(err, null) + ); } } diff --git a/packages/repack/src/types.ts b/packages/repack/src/types.ts index 6277bc156..fce056413 100644 --- a/packages/repack/src/types.ts +++ b/packages/repack/src/types.ts @@ -81,11 +81,7 @@ export interface EnvOptions { export interface HMRMessage { action: 'compiling' | 'hash' | 'ok'; - body: { - name: string; - hash?: string; - hasErrors?: boolean; - }; + body: { name: string; hash?: string }; } export interface Logger { diff --git a/packages/repack/src/types/runtime-globals.d.ts b/packages/repack/src/types/runtime-globals.d.ts index e1e7e1111..d5cc44780 100644 --- a/packages/repack/src/types/runtime-globals.d.ts +++ b/packages/repack/src/types/runtime-globals.d.ts @@ -6,27 +6,31 @@ declare namespace RepackRuntimeGlobals { moduleId: string | number; } + declare type HMRStatus = + | 'idle' + | 'check' + | 'prepare' + | 'ready' + | 'dispose' + | 'apply' + | 'abort' + | 'fail'; + + declare interface HMRApplyOptions { + ignoreUnaccepted?: boolean; + ignoreDeclined?: boolean; + ignoreErrored?: boolean; + onDeclined?: (info: HMRInfo) => void; + onUnaccepted?: (info: HMRInfo) => void; + onAccepted?: (info: HMRInfo) => void; + onDisposed?: (info: HMRInfo) => void; + onErrored?: (info: HMRInfo) => void; + } + declare interface HotApi { - status(): - | 'idle' - | 'check' - | 'prepare' - | 'ready' - | 'dispose' - | 'apply' - | 'abort' - | 'fail'; - check(autoPlay: boolean): Promise>; - apply(options: { - ignoreUnaccepted?: boolean; - ignoreDeclined?: boolean; - ignoreErrored?: boolean; - onDeclined?: (info: HMRInfo) => void; - onUnaccepted?: (info: HMRInfo) => void; - onAccepted?: (info: HMRInfo) => void; - onDisposed?: (info: HMRInfo) => void; - onErrored?: (info: HMRInfo) => void; - }): Promise>; + status(): HMRStatus; + check(autoPlay: boolean | HMRApplyOptions): Promise>; + apply(options: HMRApplyOptions): Promise>; } declare interface LoadScriptEvent { From b2088df1be9db037af513e73af03b2352ab343e7 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 13:01:18 +0100 Subject: [PATCH 21/23] refactor: use compile-time Platform --- packages/repack/src/modules/WebpackHMRClient.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index d6a2a205f..66506b0e3 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -148,8 +148,7 @@ if (__DEV__ && module.hot) { }; const dismissErrors = () => { - const Platform = require('react-native/Libraries/Utilities/Platform'); - if (Platform.OS === 'ios') { + if (__PLATFORM__ === 'ios') { const NativeRedBox = require('react-native/Libraries/NativeModules/specs/NativeRedBox').default; NativeRedBox?.dismiss?.(); From a1659de8b63a89012884b7a76d083e3ffb0abefd Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 19 Feb 2025 18:01:23 +0100 Subject: [PATCH 22/23] chore: update podfile locks --- apps/tester-app/ios/Podfile.lock | 4 ++-- apps/tester-federation-v2/ios/Podfile.lock | 4 ++-- apps/tester-federation/ios/Podfile.lock | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/tester-app/ios/Podfile.lock b/apps/tester-app/ios/Podfile.lock index b2dd9dfcf..26afa91f7 100644 --- a/apps/tester-app/ios/Podfile.lock +++ b/apps/tester-app/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - callstack-repack (5.0.0-rc.10): + - callstack-repack (5.0.0-rc.11): - DoubleConversion - glog - hermes-engine @@ -2011,7 +2011,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - callstack-repack: 3ffe9c49dd09333ddb3cd0ac85ff6724dd6fa560 + callstack-repack: 028c13131834d77421120d3dc1de340f10899bcb DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be diff --git a/apps/tester-federation-v2/ios/Podfile.lock b/apps/tester-federation-v2/ios/Podfile.lock index 254579deb..105f74ef6 100644 --- a/apps/tester-federation-v2/ios/Podfile.lock +++ b/apps/tester-federation-v2/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - callstack-repack (5.0.0-rc.10): + - callstack-repack (5.0.0-rc.11): - DoubleConversion - glog - hermes-engine @@ -1895,7 +1895,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - callstack-repack: 3ffe9c49dd09333ddb3cd0ac85ff6724dd6fa560 + callstack-repack: 028c13131834d77421120d3dc1de340f10899bcb DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be diff --git a/apps/tester-federation/ios/Podfile.lock b/apps/tester-federation/ios/Podfile.lock index c97f90498..9b6d18e5d 100644 --- a/apps/tester-federation/ios/Podfile.lock +++ b/apps/tester-federation/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - callstack-repack (5.0.0-rc.10): + - callstack-repack (5.0.0-rc.11): - DoubleConversion - glog - hermes-engine @@ -1919,7 +1919,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - callstack-repack: 3ffe9c49dd09333ddb3cd0ac85ff6724dd6fa560 + callstack-repack: 028c13131834d77421120d3dc1de340f10899bcb DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be From 458be3df9ec81407e3dc4fa26d24d4852dff4044 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 20 Feb 2025 10:47:57 +0100 Subject: [PATCH 23/23] fix: stuck downloading 100% --- packages/dev-server/src/plugins/compiler/compilerPlugin.ts | 1 + packages/repack/src/modules/WebpackHMRClient.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/dev-server/src/plugins/compiler/compilerPlugin.ts b/packages/dev-server/src/plugins/compiler/compilerPlugin.ts index ce032e50d..a34fb091b 100644 --- a/packages/dev-server/src/plugins/compiler/compilerPlugin.ts +++ b/packages/dev-server/src/plugins/compiler/compilerPlugin.ts @@ -42,6 +42,7 @@ async function compilerPlugin( JSON.stringify({ done: completed, total, + status: 'Bundling with Re.Pack', }) ); }; diff --git a/packages/repack/src/modules/WebpackHMRClient.ts b/packages/repack/src/modules/WebpackHMRClient.ts index 66506b0e3..1c494dc81 100644 --- a/packages/repack/src/modules/WebpackHMRClient.ts +++ b/packages/repack/src/modules/WebpackHMRClient.ts @@ -29,6 +29,8 @@ class HMRClient { this.socket.onopen = () => { console.debug('[HMRClient] Connected'); + // hide the `Downloading 100%` message + this.app.hideLoadingView(); }; this.socket.onclose = () => {