diff --git a/.changeset/common-games-hope.md b/.changeset/common-games-hope.md new file mode 100644 index 0000000000..1d8e3875a7 --- /dev/null +++ b/.changeset/common-games-hope.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/webpack-dev-transport": patch +--- + +Fix live-reload not working on Lynx 3.3 diff --git a/packages/webpack/webpack-dev-transport/client/reloadApp.ts b/packages/webpack/webpack-dev-transport/client/reloadApp.ts index f375d259d0..99a7891848 100644 --- a/packages/webpack/webpack-dev-transport/client/reloadApp.ts +++ b/packages/webpack/webpack-dev-transport/client/reloadApp.ts @@ -6,15 +6,28 @@ import hotEmitter from 'webpack/hot/emitter.js'; import type { Options, Status } from './index.js'; declare const NativeModules: { - LynxDevtoolSetModule: LynxDevtoolSetModule; + // Added in Lynx 3.2 + LynxDevToolSetModule?: LynxDevToolSetModule; + + // Remove in Lynx 3.2 + LynxDevtoolSetModule?: LynxDevtoolSetModule; }; +interface LynxDevToolSetModule { + // Added in Lynx 3.3 + invokeCdp?: ( + message: string, + callback: (data?: string) => void, + ) => void; +} + interface LynxDevtoolSetModule { - invokeCdp( + // Theoretically, this method should always available. + invokeCdp?: ( type: string, message: string, callback: (data?: string) => void, - ): void; + ) => void; } function reloadApp({ hot, liveReload }: Options, status: Status): void { @@ -38,8 +51,24 @@ function reloadApp({ hot, liveReload }: Options, status: Status): void { function applyReload(intervalId: number) { clearInterval(intervalId); - NativeModules.LynxDevtoolSetModule.invokeCdp( + + if ( + typeof NativeModules.LynxDevToolSetModule?.invokeCdp !== 'function' + && typeof NativeModules.LynxDevtoolSetModule?.invokeCdp !== 'function' + ) { + console.error('[HMR] live-reload failed: cannot invoke cdp from DevTool.'); + console.error('[HMR] Please reload the page manually.'); + return; + } + + const invokeCdp = NativeModules.LynxDevToolSetModule?.invokeCdp?.bind( + NativeModules.LynxDevToolSetModule, + ) ?? NativeModules.LynxDevtoolSetModule?.invokeCdp?.bind( + NativeModules.LynxDevtoolSetModule, 'Page.reload', + ); + + invokeCdp?.( JSON.stringify({ method: 'Page.reload', params: { diff --git a/packages/webpack/webpack-dev-transport/test/client/reloadApp.test.ts b/packages/webpack/webpack-dev-transport/test/client/reloadApp.test.ts new file mode 100644 index 0000000000..876ad210ed --- /dev/null +++ b/packages/webpack/webpack-dev-transport/test/client/reloadApp.test.ts @@ -0,0 +1,162 @@ +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import reloadApp from '../../client/reloadApp.js'; + +describe('reloadApp', () => { + describe('liveReload', () => { + // New NativeModule that introduced in 3.2 + const LynxDevToolSetModule = { + invokeCdp: vi.fn(), + }; + + const EmptyModule = {}; + + // Old NativeModule + const LynxDevtoolSetModule = { + invokeCdp: vi.fn(), + }; + + const status = { + currentHash: '123', + previousHash: '456', + isReconnecting: false, + }; + + beforeEach(() => { + vi.unstubAllGlobals(); + LynxDevToolSetModule.invokeCdp.mockClear(); + LynxDevtoolSetModule.invokeCdp.mockClear(); + }); + + it('should live reload on legacy Lynx', () => { + vi.useFakeTimers(); + + vi.stubGlobal('NativeModules', { + LynxDevtoolSetModule, + }); + + reloadApp({ liveReload: true, hot: false, progress: false }, status); + + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + + vi.runAllTimers(); + + expect(LynxDevtoolSetModule.invokeCdp).toHaveBeenCalledWith( + 'Page.reload', + JSON.stringify({ + method: 'Page.reload', + params: { + ignoreCache: true, + }, + }), + expect.any(Function), + ); + }); + + it('should live reload on Lynx >= 3.3', () => { + vi.useFakeTimers(); + + vi.stubGlobal('NativeModules', { + LynxDevToolSetModule, + }); + + reloadApp({ liveReload: true, hot: false, progress: false }, status); + + expect(LynxDevToolSetModule.invokeCdp).not.toBeCalled(); + + vi.runAllTimers(); + + expect(LynxDevToolSetModule.invokeCdp).toHaveBeenCalledWith( + JSON.stringify({ + method: 'Page.reload', + params: { + ignoreCache: true, + }, + }), + expect.any(Function), + ); + }); + + it('should not throw on Lynx 3.2', () => { + vi.useFakeTimers(); + + vi.stubGlobal('NativeModules', { + LynxDevToolSetModule: EmptyModule, + }); + + reloadApp({ liveReload: true, hot: false, progress: false }, status); + + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + + vi.runAllTimers(); + + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + }); + + it('should not throw on both modules are empty', () => { + vi.useFakeTimers(); + + vi.stubGlobal('NativeModules', { + LynxDevToolSetModule: EmptyModule, + LynxDevtoolSetModule: EmptyModule, + }); + + reloadApp({ liveReload: true, hot: false, progress: false }, status); + + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + expect(LynxDevToolSetModule.invokeCdp).not.toBeCalled(); + + vi.runAllTimers(); + + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + expect(LynxDevToolSetModule.invokeCdp).not.toBeCalled(); + }); + + it('should not throw when no modules are present', () => { + vi.useFakeTimers(); + + vi.stubGlobal('NativeModules', {}); + + reloadApp({ liveReload: true, hot: false, progress: false }, status); + + expect(LynxDevToolSetModule.invokeCdp).not.toBeCalled(); + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + + vi.runAllTimers(); + + expect(LynxDevToolSetModule.invokeCdp).not.toBeCalled(); + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + }); + + it('should call new method when both modules are present', () => { + vi.useFakeTimers(); + + vi.stubGlobal('NativeModules', { + LynxDevToolSetModule, + LynxDevtoolSetModule, + }); + + reloadApp({ liveReload: true, hot: false, progress: false }, status); + + expect(LynxDevToolSetModule.invokeCdp).not.toBeCalled(); + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + + vi.runAllTimers(); + + expect(LynxDevToolSetModule.invokeCdp).toHaveBeenCalledWith( + JSON.stringify({ + method: 'Page.reload', + params: { + ignoreCache: true, + }, + }), + expect.any(Function), + ); + + expect(LynxDevtoolSetModule.invokeCdp).not.toBeCalled(); + }); + }); +});