From 21381f49879cbb4e5697fe66f53dd7f91d6f9449 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Thu, 22 Oct 2026 03:47:07 -0400 Subject: [PATCH] feat(windows): apply webpack HMR updates without restarting the app tryRefreshApplication returned false for webpack, so every change restarted the app even though the hot-update files were already in bin\app. After a sync, the CLI now writes an ns-livesync marker into the app's LocalState. The debug host watches for it and calls global.__onLiveSync(), which runs webpack's hot update (NativeScript/windows#32). The CLI only does this when the host has written ns-livesync-ready, so older hosts still restart. shouldRestart now also restarts on a full sync, as Android and iOS do. That is how the CLI recovers when a hot update fails. --- .../windows/windows-application-manager.ts | 25 ++++++++++++++ .../windows-device-livesync-service.ts | 33 ++++++++++++++----- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/lib/common/mobile/windows/windows-application-manager.ts b/lib/common/mobile/windows/windows-application-manager.ts index 6b18ee393b..fc1a7925a7 100644 --- a/lib/common/mobile/windows/windows-application-manager.ts +++ b/lib/common/mobile/windows/windows-application-manager.ts @@ -10,6 +10,9 @@ import { IBuildData } from "../../../definitions/build"; export const WINDOWS_INSPECTOR_MARKER = "ns-inspector"; export const WINDOWS_DEBUGBREAK_MARKER = "ns-debugbreak"; export const WINDOWS_DEBUGGER_STARTED_MARKER = "ns-debugger-started"; +// Written by a running debug host that watches for WINDOWS_LIVESYNC_MARKER. +export const WINDOWS_LIVESYNC_READY_MARKER = "ns-livesync-ready"; +export const WINDOWS_LIVESYNC_MARKER = "ns-livesync"; export class WindowsApplicationManager extends ApplicationManagerBase { private _runningPid: number | null = null; @@ -373,6 +376,28 @@ export class WindowsApplicationManager extends ApplicationManagerBase { return this._localStatePath(pfn, fileName); } + /** + * Asks the running app to apply the files just synced into bin\app: the debug host watches + * LocalState for the marker and calls global.__onLiveSync(), which runs webpack's hot update. + * Returns false when the app's host does not watch for it (EXE targets, older runtimes). + */ + public async signalLiveSync(appId: string): Promise { + const pfn = await this._resolvePackageFamilyName(appId); + const readyPath = this._localStatePath(pfn, WINDOWS_LIVESYNC_READY_MARKER); + const markerPath = this._localStatePath(pfn, WINDOWS_LIVESYNC_MARKER); + if (!readyPath || !markerPath || !fs.existsSync(readyPath)) { + return false; + } + try { + // New content on every sync, so a marker the host has not consumed yet still changes. + fs.writeFileSync(markerPath, String(Date.now()), "utf8"); + return true; + } catch (e) { + this.$logger.trace(`[Windows] Could not write ${WINDOWS_LIVESYNC_MARKER} marker: ${e}`); + return false; + } + } + private _localStatePath(pfn: string, fileName: string): string | null { const localAppData = process.env.LOCALAPPDATA; if (!localAppData) return null; diff --git a/lib/services/livesync/windows-device-livesync-service.ts b/lib/services/livesync/windows-device-livesync-service.ts index feac8c918a..e3b040a7c9 100644 --- a/lib/services/livesync/windows-device-livesync-service.ts +++ b/lib/services/livesync/windows-device-livesync-service.ts @@ -3,6 +3,7 @@ import { spawnSync } from "child_process"; import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; import { IPlatformsDataService } from "../../definitions/platform"; import { IProjectData } from "../../definitions/project"; +import { WindowsApplicationManager } from "../../common/mobile/windows/windows-application-manager"; export class WindowsDeviceLiveSyncService extends DeviceLiveSyncServiceBase @@ -40,22 +41,36 @@ export class WindowsDeviceLiveSyncService _projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo, ): Promise { - return !liveSyncInfo.useHotModuleReload || !!liveSyncInfo.waitForDebugger; + // A full sync (also the CLI's recovery after a failed hot update) needs a fresh start. + return ( + !liveSyncInfo.useHotModuleReload || + !!liveSyncInfo.isFullSync || + !!liveSyncInfo.waitForDebugger + ); } public async tryRefreshApplication( projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo, ): Promise { - // Vite HMR: a running app pulls every change from the dev server over its WebSocket - // (HTTP ES modules), so a synced file never needs a restart. But an app that isn't - // running yet (first sync after install) must still be launched. Webpack HMR isn't - // implemented for Windows: signal a restart. + // An app that isn't running yet (first sync after install) must still be launched. + if ( + !liveSyncInfo.useHotModuleReload || + !this.isAppRunning(projectData.projectName) + ) { + return false; + } + // Vite HMR: the app pulls every change from the dev server over its WebSocket + // (HTTP ES modules), so a synced file never needs a restart. + if (projectData.bundler === "vite") { + return true; + } + // Webpack HMR: the hot-update files are already in bin\app; tell the app to apply them. + const appId = + projectData.projectIdentifiers?.["windows"] ?? projectData.projectId; return ( - projectData.bundler === "vite" && - !!liveSyncInfo.useHotModuleReload && - this.isAppRunning(projectData.projectName) - ); + this.device.applicationManager as WindowsApplicationManager + ).signalLiveSync(appId); } private isAppRunning(processName: string): boolean {