Skip to content

Commit 3d97912

Browse files
committed
fix: windows support
1 parent 2fc8ca7 commit 3d97912

13 files changed

Lines changed: 226 additions & 36 deletions

lib/common/file-system.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,18 @@ export class FileSystem implements IFileSystem {
135135
}
136136

137137
public deleteDirectory(directory: string): void {
138-
shelljs.rm("-rf", directory);
139-
140-
const err = shelljs.error();
141-
142-
if (err !== null) {
143-
throw new Error(err);
138+
// fs.rmSync handles Windows edge cases (read-only attributes, long paths)
139+
// more reliably than shelljs.rm on Node.js 20+.
140+
try {
141+
fs.rmSync(directory, { recursive: true, force: true });
142+
} catch (e: any) {
143+
// If rmSync itself fails (e.g., files locked by another process on Windows),
144+
// fall back to shelljs so behaviour on other platforms is unchanged.
145+
shelljs.rm("-rf", directory);
146+
const err = shelljs.error();
147+
if (err !== null) {
148+
throw new Error(e?.message ?? err);
149+
}
144150
}
145151
}
146152

lib/common/mobile/windows/windows-application-manager.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as fs from "fs";
2+
import * as os from "os";
23
import * as path from "path";
34
import { spawn } from "child_process";
45
import { ApplicationManagerBase } from "../application-manager-base";
@@ -81,6 +82,19 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
8182
}
8283

8384
this.$logger.info(`[Windows] Installing MSIX/APPX from: ${packageFilePath}`);
85+
// If we have an app identifier, try to remove any existing package first to
86+
// avoid the "package is already installed" HRESULT (0x80073CFB) which
87+
// blocks re-registration in development flows.
88+
if (appIdentifier) {
89+
try {
90+
this.$logger.info(`[Windows] Attempting to remove existing package: ${appIdentifier}`);
91+
// uninstallApplication handles EXE cleanup and runs the Remove-AppxPackage
92+
// command for UWP packages. Ignore errors and proceed to install.
93+
await this.uninstallApplication(appIdentifier);
94+
} catch (err) {
95+
this.$logger.warn(`[Windows] Pre-install uninstall failed: ${err}`);
96+
}
97+
}
8498
await this.$childProcess.spawnFromEvent(
8599
"powershell.exe",
86100
[
@@ -128,9 +142,33 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
128142
await this.startApplication(appData);
129143
}
130144

145+
/**
146+
* Returns the path of the runtime's trace log for the most recently started app.
147+
* For UWP-packaged apps the runtime DLL writes inside the app container's TempState,
148+
* not the global system temp directory. Falls back to the system temp path used when
149+
* the app is launched as an unpackaged EXE.
150+
*/
151+
public getLogFilePath(): string {
152+
const systemTempLog = path.join(os.tmpdir(), "ns_trace.log");
153+
if (this._packageFamilyNames.size > 0) {
154+
const pfn = this._packageFamilyNames.values().next().value as string;
155+
const localAppData = process.env.LOCALAPPDATA;
156+
if (localAppData && pfn) {
157+
return path.join(localAppData, "Packages", pfn, "TempState", "ns_trace.log");
158+
}
159+
}
160+
return systemTempLog;
161+
}
162+
131163
public async startApplication(
132164
appData: Mobile.IStartApplicationData,
133165
): Promise<void> {
166+
// Truncate the trace log so the streamer starts from a clean state each run.
167+
try {
168+
const logPath = this.getLogFilePath();
169+
fs.writeFileSync(logPath, "", "utf8");
170+
} catch { /* ignore — log dir may not exist yet */ }
171+
134172
const exeCandidate =
135173
(appData.appId && this._installedExePaths[appData.appId]) ||
136174
(appData.projectName && this._installedExePaths[appData.projectName]);
@@ -159,8 +197,11 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
159197
if (appData.waitForDebugger) {
160198
this._writeDebugBreakMarker(pfn);
161199
}
162-
this.$logger.info(`[Windows] Launching UWP: ${pfn}`);
163-
const proc = spawn("explorer.exe", [`ms-windows-app://${pfn}`], {
200+
// UWP apps are launched via shell:AppsFolder\<PFN>!<ApplicationId>.
201+
// The ApplicationId comes from the <Application Id="..."> attribute in the manifest.
202+
const appId = "App";
203+
this.$logger.info(`[Windows] Launching UWP: ${pfn}!${appId}`);
204+
const proc = spawn("explorer.exe", [`shell:AppsFolder\\${pfn}!${appId}`], {
164205
detached: true,
165206
stdio: "ignore",
166207
});

lib/common/mobile/windows/windows-device-file-system.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,7 @@ export class WindowsDeviceFileSystem implements Mobile.IDeviceFileSystem {
4848
_deviceAppData: Mobile.IDeviceAppData,
4949
localToDevicePaths: Mobile.ILocalToDevicePathData[],
5050
): Promise<Mobile.ILocalToDevicePathData[]> {
51-
for (const item of localToDevicePaths) {
52-
const dest = item.getDevicePath();
53-
fs.mkdirSync(path.dirname(dest), { recursive: true });
54-
fs.copyFileSync(item.getLocalPath(), dest);
55-
}
51+
this._syncPaths(localToDevicePaths);
5652
return localToDevicePaths;
5753
}
5854

@@ -61,7 +57,29 @@ export class WindowsDeviceFileSystem implements Mobile.IDeviceFileSystem {
6157
localToDevicePaths: Mobile.ILocalToDevicePathData[],
6258
_projectFilesPath: string,
6359
): Promise<Mobile.ILocalToDevicePathData[]> {
64-
return this.transferFiles(_deviceAppData, localToDevicePaths);
60+
this._syncPaths(localToDevicePaths);
61+
return localToDevicePaths;
62+
}
63+
64+
private _syncPaths(localToDevicePaths: Mobile.ILocalToDevicePathData[]): void {
65+
for (const pathData of localToDevicePaths) {
66+
const src = pathData.getLocalPath();
67+
const dest = pathData.getDevicePath();
68+
if (src === dest) {
69+
continue;
70+
}
71+
try {
72+
const stat = fs.statSync(src);
73+
if (stat.isDirectory()) {
74+
fs.mkdirSync(dest, { recursive: true });
75+
} else {
76+
fs.mkdirSync(path.dirname(dest), { recursive: true });
77+
fs.copyFileSync(src, dest);
78+
}
79+
} catch {
80+
// skip entries that disappeared between detection and sync
81+
}
82+
}
6583
}
6684

6785
public async transferFile(

lib/common/mobile/windows/windows-device.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as os from "os";
2+
import * as fs from "fs";
23
import { DeviceConnectionType } from "../../../constants";
34
import { CONNECTED_STATUS, DeviceTypes } from "../../constants";
45
import { WindowsApplicationManager } from "./windows-application-manager";
@@ -25,10 +26,12 @@ export class WindowsDevice implements Mobile.IDevice {
2526
connectionTypes: [DeviceConnectionType.Local],
2627
};
2728

29+
private _logTailInterval: ReturnType<typeof setInterval> | null = null;
30+
2831
constructor(
2932
$logger: ILogger,
3033
$hooksService: IHooksService,
31-
$deviceLogProvider: Mobile.IDeviceLogProvider,
34+
private $deviceLogProvider: Mobile.IDeviceLogProvider,
3235
$childProcess: IChildProcess,
3336
) {
3437
this.applicationManager = new WindowsApplicationManager(
@@ -41,7 +44,48 @@ export class WindowsDevice implements Mobile.IDevice {
4144
}
4245

4346
public async openDeviceLogStream(): Promise<void> {
44-
// Windows runtime logs go to stdout/stderr of the process
45-
// DevTools console output is available on ws://localhost:9229
47+
if (this._logTailInterval) {
48+
clearInterval(this._logTailInterval);
49+
this._logTailInterval = null;
50+
}
51+
52+
// For packaged UWP apps, GetTempPath() inside the app container resolves to
53+
// %LOCALAPPDATA%\Packages\<PFN>\TempState — not the system temp dir.
54+
// Ask the application manager for the correct path based on the known PFN.
55+
const manager = this.applicationManager as WindowsApplicationManager;
56+
const logPath = manager.getLogFilePath();
57+
const deviceId = this.deviceInfo.identifier;
58+
59+
// Start from the current end of the file so stale output is not replayed.
60+
let offset = 0;
61+
try { offset = fs.statSync(logPath).size; } catch { /* file not yet created */ }
62+
63+
// Rotate the log if it exceeds 10 MB to prevent unbounded disk growth.
64+
const MAX_LOG_BYTES = 10 * 1024 * 1024;
65+
66+
this._logTailInterval = setInterval(() => {
67+
try {
68+
const stat = fs.statSync(logPath);
69+
if (stat.size <= offset) return;
70+
71+
const toRead = stat.size - offset;
72+
const buf = Buffer.alloc(toRead);
73+
const fd = fs.openSync(logPath, "r");
74+
fs.readSync(fd, buf, 0, toRead, offset);
75+
fs.closeSync(fd);
76+
offset = stat.size;
77+
78+
const lines = buf.toString("utf8").split(/\r?\n/);
79+
for (const line of lines) {
80+
if (line.trim()) {
81+
this.$deviceLogProvider.logData(line, "Windows", deviceId);
82+
}
83+
}
84+
85+
if (stat.size > MAX_LOG_BYTES) {
86+
try { fs.writeFileSync(logPath, "", "utf8"); offset = 0; } catch { /* ignore */ }
87+
}
88+
} catch { /* ignore — file may not exist between app restarts */ }
89+
}, 250);
4690
}
4791
}

lib/controllers/prepare-controller.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
AnalyticsEventLabelDelimiter,
1616
BUNDLER_COMPILATION_COMPLETE,
1717
CONFIG_FILE_NAME_JS,
18+
APP_FOLDER_NAME,
1819
CONFIG_FILE_NAME_TS,
1920
PACKAGE_JSON_FILE_NAME,
2021
PLATFORMS_DIR_NAME,
@@ -485,11 +486,9 @@ export class PrepareController extends EventEmitter {
485486
} else if (
486487
this.$mobileHelper.isWindowsPlatform(platformData.platformNameLowerCase)
487488
) {
488-
// Windows apps place the packaged app under <projectRoot>/<projectName>/App
489489
packagePath = path.join(
490-
platformData.projectRoot,
491-
projectData.projectName,
492-
"app",
490+
platformData.appDestinationDirectoryPath,
491+
APP_FOLDER_NAME,
493492
"package.json",
494493
);
495494
} else {

lib/definitions/livesync.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ declare global {
508508
appIdentifier: string;
509509
getDirname?: boolean;
510510
watch?: boolean;
511+
projectData?: IProjectData;
511512
}
512513

513514
interface IDevicePathProvider {

lib/definitions/project.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ interface INsConfigIOS extends INsConfigPlaform {
134134

135135
interface INSConfigVisionOS extends INsConfigIOS {}
136136

137+
interface INsConfigWindows extends INsConfigPlaform {}
138+
137139
interface INsConfigAndroid extends INsConfigPlaform {
138140
v8Flags?: string;
139141

@@ -188,6 +190,7 @@ interface INsConfig {
188190
ios?: INsConfigIOS;
189191
android?: INsConfigAndroid;
190192
visionos?: INSConfigVisionOS;
193+
windows?: INsConfigWindows;
191194
ignoredNativeDependencies?: string[];
192195
hooks?: INsConfigHooks[];
193196
projectName?: string;

lib/device-path-provider.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,18 @@ export class DevicePathProvider implements IDevicePathProvider {
5252
} else if (
5353
this.$mobileHelper.isWindowsPlatform(device.deviceInfo.platform)
5454
) {
55-
const projectData = (options as any).projectData;
55+
const projectData = options.projectData;
5656
if (projectData) {
5757
const platformData = this.$platformsDataService.getPlatformData(
5858
device.deviceInfo.platform,
5959
projectData,
6060
);
61-
return platformData.appDestinationDirectoryPath;
61+
// Sync into bin\app — the registered package root — so the running app
62+
// picks up changes without a rebuild.
63+
return path.join(
64+
platformData.getBuildOutputPath({} as never),
65+
APP_FOLDER_NAME,
66+
);
6267
}
6368
}
6469

lib/project-data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,15 @@ export class ProjectData implements IProjectData {
326326
ios: "",
327327
android: "",
328328
visionos: "",
329+
windows: "",
329330
};
330331
}
331332

332333
const identifier: Mobile.IProjectIdentifier = {
333334
ios: config.id,
334335
android: config.id,
335336
visionos: config.id,
337+
windows: config.id,
336338
};
337339

338340
if (config.ios && config.ios.id) {
@@ -344,6 +346,9 @@ export class ProjectData implements IProjectData {
344346
if (config.visionos && config.visionos.id) {
345347
identifier.visionos = config.visionos.id;
346348
}
349+
if (config.windows && config.windows.id) {
350+
identifier.windows = config.windows.id;
351+
}
347352

348353
return identifier;
349354
}

lib/services/build-data-service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AndroidBuildData, IOSBuildData } from "../data/build-data";
1+
import { AndroidBuildData, BuildData, IOSBuildData } from "../data/build-data";
22
import { IBuildDataService } from "../definitions/build";
33
import { injector } from "../common/yok";
44

@@ -10,6 +10,8 @@ export class BuildDataService implements IBuildDataService {
1010
return new IOSBuildData(projectDir, platform, data);
1111
} else if (this.$mobileHelper.isAndroidPlatform(platform)) {
1212
return new AndroidBuildData(projectDir, platform, data);
13+
} else if (this.$mobileHelper.isWindowsPlatform(platform)) {
14+
return new BuildData(projectDir, platform, data);
1315
}
1416
}
1517
}

0 commit comments

Comments
 (0)