Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b5171b8
feat: windows runtime support
triniwiz May 11, 2026
ba29271
fix(windows): syncing
triniwiz May 12, 2026
a2a6041
fix: missing import
triniwiz May 12, 2026
2fc8ca7
chore(windows): polish
triniwiz May 14, 2026
3d97912
fix: windows support
triniwiz May 16, 2026
5bbe284
fix(windows): logging
triniwiz May 17, 2026
1ee87fc
fix(windows): plugins & .net support
triniwiz May 21, 2026
dbff762
chore: improve windows app configs
triniwiz May 24, 2026
77609c0
fix: missing zeroGuid
triniwiz May 28, 2026
6c26545
chore: window.0
triniwiz May 28, 2026
b2a6cb5
fix(windows): test
triniwiz May 28, 2026
32f33ff
Merge branch 'main' into feat/windows
triniwiz May 29, 2026
ae5c765
feat(windows): types generation
triniwiz Jun 1, 2026
c9d9c4f
fix(windows): logs
triniwiz Jun 4, 2026
8f03ee4
fix(windows): buildOutputPath
triniwiz Jun 10, 2026
bc517c0
feat(doctor): add windows support
triniwiz Jun 13, 2026
f73ecf7
fix: fs rename add retry with backoff
triniwiz Jun 14, 2026
c702308
fix(windows): sync lock
triniwiz Jun 14, 2026
1dbb30b
fix(windows): fileSync
triniwiz Jun 14, 2026
7a54c25
fix(windows): devtools auto-start
triniwiz Jun 16, 2026
95cc0ed
feat(windows): release
triniwiz Jun 26, 2026
c7f8432
chore: windows help
triniwiz Jun 26, 2026
e5ef0ac
fix(windows): avoid EPERM rename error during platform project creation
dyazincahya Jun 29, 2026
ea72c74
Merge pull request #6066 from dyazincahya/feat/windows
triniwiz Jul 12, 2026
0f05f01
fix(windows): framework name
triniwiz Jul 30, 2026
2053117
feat(windows): include napi packages
triniwiz Jul 30, 2026
c80a64b
feat(windows): encrypt bundle
triniwiz Aug 5, 2026
6286e82
Merge remote-tracking branch 'origin/main' into feat/windows
triniwiz Sep 23, 2026
eecb511
feat(windows): vite HMR livesync, comment cleanup
triniwiz Sep 24, 2026
a299797
fix(windows): drop dead dotnet-bridge marker check
triniwiz Sep 29, 2026
86dd40c
feat(windows): print the DevTools URL for ns debug windows
triniwiz Sep 30, 2026
e9be2f0
fix(windows): honor --no-auto-guid
triniwiz Sep 30, 2026
69f2c1f
fix(doctor): check .NET SDK 10+ and Developer Mode on Windows
triniwiz Sep 30, 2026
3c839a5
docs(windows): fix inspector port, prerequisites and build output
triniwiz Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chore(windows): polish
  • Loading branch information
triniwiz committed May 14, 2026
commit 2fc8ca7d579946fe8ada00df7177d71a91aa655e
186 changes: 129 additions & 57 deletions lib/common/mobile/windows/windows-application-manager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import * as fs from "fs";
import * as path from "path";
import { spawn } from "child_process";
import { ApplicationManagerBase } from "../application-manager-base";
import { IHooksService, IChildProcess, IDictionary } from "../../declarations";
import { IBuildData } from "../../../definitions/build";

export class WindowsApplicationManager extends ApplicationManagerBase {
private _runningPid: number = null;
private _packageFamilyName: string = null;
private _runningPid: number | null = null;
// Keyed by appId so multiple UWP apps don't stomp each other's cached PFN.
private _packageFamilyNames: Map<string, string> = new Map();
// Populated by installApplication for .exe builds; keyed by both appIdentifier
// and exe-basename so the two lookup paths in startApplication both work.
private _installedExePaths: IDictionary<string> = {};

constructor(
$logger: ILogger,
Expand Down Expand Up @@ -39,12 +44,43 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
}
}

// The base class checks getInstalledApplications(), which only knows about UWP
// packages. Override so that EXE-based apps registered via installApplication()
// are also considered "installed" without a PowerShell round-trip.
public async isApplicationInstalled(appIdentifier: string): Promise<boolean> {
if (
appIdentifier &&
Object.prototype.hasOwnProperty.call(
this._installedExePaths,
appIdentifier,
)
) {
return true;
}
return super.isApplicationInstalled(appIdentifier);
}

public async installApplication(
packageFilePath: string,
appIdentifier?: string,
_buildData?: IBuildData,
): Promise<void> {
this.$logger.info(`[Windows] Installing from: ${packageFilePath}`);
if (packageFilePath?.toLowerCase().endsWith(".exe")) {
this.$logger.info(`[Windows] Registering EXE: ${packageFilePath}`);
const exeBase = path.basename(
packageFilePath,
path.extname(packageFilePath),
);
if (appIdentifier) {
this._installedExePaths[appIdentifier] = packageFilePath;
}
// Secondary key so the projectName-based lookup in startApplication works
// even when appIdentifier differs from the exe filename.
this._installedExePaths[exeBase] = packageFilePath;
return;
}

this.$logger.info(`[Windows] Installing MSIX/APPX from: ${packageFilePath}`);
await this.$childProcess.spawnFromEvent(
"powershell.exe",
[
Expand All @@ -58,13 +94,17 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
{},
{ throwError: true },
);
Comment on lines +133 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Trace where appIdentifier / projectName / packageFilePath originate for the Windows app manager.
rg -nP --type=ts -C3 '\b(appIdentifier|projectName|packageFilePath)\b' lib/common/mobile/windows/windows-application-manager.ts
rg -nP --type=ts -C3 'startApplication|installApplication' lib/services/livesync/windows-livesync-service.ts lib/controllers/run-controller.ts

Repository: NativeScript/nativescript-cli

Length of output: 6243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the Windows app manager structure, then inspect the relevant methods and call sites.
ast-grep outline lib/common/mobile/windows/windows-application-manager.ts --view expanded

printf '\n--- install/uninstall/resolve/stop sections ---\n'
sed -n '1,380p' lib/common/mobile/windows/windows-application-manager.ts

printf '\n--- callers / provenance ---\n'
rg -n -C 3 'installApplication\(|uninstallApplication\(|stopApplication\(|_resolvePackageFamilyName\(' lib -g '*.ts'

Repository: NativeScript/nativescript-cli

Length of output: 41640


Escape the PowerShell command arguments here and in the related uninstall/resolve/stop paths.

packageFilePath, appIdentifier, and projectName are interpolated directly into -Command strings, so a quote or $(...) in one of those values can break out of the literal and be interpreted by PowerShell. Pass parameters out-of-band or escape the literals before building the command.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/common/mobile/windows/windows-application-manager.ts` around lines 99 -
111, The PowerShell command construction in WindowsApplicationManager is
vulnerable because packageFilePath, appIdentifier, and projectName are
interpolated directly into -Command strings. Update the
register/uninstall/resolve/stop paths to avoid raw string interpolation by
passing values as parameters or properly escaping them before building the
command. Use the existing spawnFromEvent call sites in WindowsApplicationManager
as the fix points and keep the command literals safe from quote or $(...)
injection.

Source: Linters/SAST tools

// Cache the PFN immediately after install so startApplication can use it.
// Pre-warm the PFN cache so startApplication does not need to resolve it.
if (appIdentifier) {
await this._resolvePackageFamilyName(appIdentifier);
}
}

public async uninstallApplication(appIdentifier: string): Promise<void> {
// Clean up EXE registration so isApplicationInstalled returns false.
delete this._installedExePaths[appIdentifier];
this._packageFamilyNames.delete(appIdentifier);

await this.$childProcess.spawnFromEvent(
"powershell.exe",
[
Expand All @@ -76,78 +116,65 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
{},
{ throwError: false },
);
this._packageFamilyName = null;
}

// Explicit override matching the interface contract (IStartApplicationData)
// rather than relying on the base-class forwarding appData typed as the
// narrower IApplicationData, which would silently drop waitForDebugger.
public async restartApplication(
appData: Mobile.IStartApplicationData,
): Promise<void> {
await this.stopApplication(appData);
await this.startApplication(appData);
}

public async startApplication(
appData: Mobile.IStartApplicationData,
): Promise<void> {
const pfn = await this._resolvePackageFamilyName(appData.appId);
const exeCandidate =
(appData.appId && this._installedExePaths[appData.appId]) ||
(appData.projectName && this._installedExePaths[appData.projectName]);

if (exeCandidate && fs.existsSync(exeCandidate)) {
if (appData.waitForDebugger) {
this.$logger.info(
`[Windows] --debug-brk is not supported for EXE targets.`,
);
}
this.$logger.info(`[Windows] Launching EXE: ${exeCandidate}`);
const proc = spawn(exeCandidate, [], { detached: true, stdio: "ignore" });
proc.unref();
this._runningPid = proc.pid ?? null;
// Clear stale PID when the process exits so stopApplication falls back to
// the Stop-Process path on the next restart instead of killing a reused PID.
proc.on("exit", () => {
if (this._runningPid === proc.pid) {
this._runningPid = null;
}
});
return;
}

// Mirror the Android sentinel-file pattern: write ns-debugbreak to the
// app's LocalFolder before launch so the runtime knows to open DevTools.
const pfn = await this._resolvePackageFamilyName(appData.appId);
if (appData.waitForDebugger) {
this._writeDebugBreakMarker(pfn);
}

this.$logger.info(`[Windows] Launching: ${pfn}`);
const proc = require("child_process").spawn(
"explorer.exe",
[`ms-windows-app://${pfn}`],
{ detached: true, stdio: "ignore" },
);
this.$logger.info(`[Windows] Launching UWP: ${pfn}`);
const proc = spawn("explorer.exe", [`ms-windows-app://${pfn}`], {
detached: true,
stdio: "ignore",
});
proc.unref();
}

private async _resolvePackageFamilyName(appId: string): Promise<string> {
if (this._packageFamilyName) return this._packageFamilyName;
try {
const result = await this.$childProcess.spawnFromEvent(
"powershell.exe",
[
"-NoProfile",
"-Command",
`(Get-AppxPackage | Where-Object { $_.Name -eq "${appId}" -or $_.PackageFullName -like "*${appId}*" } | Select-Object -First 1).PackageFamilyName`,
],
"close",
{},
{ throwError: false },
);
const pfn = (result.stdout || "").trim();
if (pfn) this._packageFamilyName = pfn;
} catch {
/* ignore, fall back to appId */
}
return this._packageFamilyName ?? appId;
}

private _writeDebugBreakMarker(pfn: string): void {
const localAppData = process.env.LOCALAPPDATA;
if (!localAppData) return;
const markerPath = path.join(
localAppData,
"Packages",
pfn,
"LocalState",
"ns-debugbreak",
);
try {
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
fs.writeFileSync(markerPath, "", "utf8");
this.$logger.info(`[Windows] Debug break marker: ${markerPath}`);
} catch (e) {
this.$logger.warn(`[Windows] Could not write debug break marker: ${e}`);
}
}

public async stopApplication(
appData: Mobile.IApplicationData,
): Promise<void> {
if (this._runningPid) {
try {
process.kill(this._runningPid);
} catch {
/* already dead */
/* already gone */
}
this._runningPid = null;
} else {
Expand Down Expand Up @@ -186,4 +213,49 @@ export class WindowsApplicationManager extends ApplicationManagerBase {
): Promise<IDictionary<Mobile.IDebugWebViewInfo[]>> {
return {} as IDictionary<Mobile.IDebugWebViewInfo[]>;
}

private async _resolvePackageFamilyName(appId: string): Promise<string> {
const cached = this._packageFamilyNames.get(appId);
if (cached) return cached;

try {
const result = await this.$childProcess.spawnFromEvent(
"powershell.exe",
[
"-NoProfile",
"-Command",
`(Get-AppxPackage | Where-Object { $_.Name -eq "${appId}" -or $_.PackageFullName -like "*${appId}*" } | Select-Object -First 1).PackageFamilyName`,
],
"close",
{},
{ throwError: false },
);
const pfn = (result.stdout || "").trim();
if (pfn) {
this._packageFamilyNames.set(appId, pfn);
}
} catch {
/* fall back to appId as the protocol target */
}
return this._packageFamilyNames.get(appId) ?? appId;
}

private _writeDebugBreakMarker(pfn: string): void {
const localAppData = process.env.LOCALAPPDATA;
if (!localAppData) return;
const markerPath = path.join(
localAppData,
"Packages",
pfn,
"LocalState",
"ns-debugbreak",
);
try {
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
fs.writeFileSync(markerPath, "", "utf8");
this.$logger.info(`[Windows] Debug break marker: ${markerPath}`);
} catch (e) {
this.$logger.warn(`[Windows] Could not write debug break marker: ${e}`);
}
}
}
18 changes: 14 additions & 4 deletions lib/services/livesync/windows-device-livesync-service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from "fs";
import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base";
import { IPlatformsDataService } from "../../definitions/platform";
import { IProjectData } from "../../definitions/project";
Expand All @@ -18,10 +19,19 @@ export class WindowsDeviceLiveSyncService
projectData: IProjectData,
_liveSyncInfo: ILiveSyncResultInfo,
): Promise<void> {
// TODO: kill the running Windows app process and relaunch it
this.$logger.info(
`[Windows LiveSync] Restart required for ${projectData.projectName}`,
`[Windows LiveSync] Restarting application ${projectData.projectName}`,
);

const appId =
projectData.projectIdentifiers?.["windows"] ?? projectData.projectId;

await this.device.applicationManager.restartApplication({
appId,
projectName: projectData.projectName,
projectDir: projectData.projectDir,
waitForDebugger: _liveSyncInfo?.waitForDebugger,
} as Mobile.IStartApplicationData);
}

public async shouldRestart(
Expand All @@ -45,8 +55,8 @@ export class WindowsDeviceLiveSyncService
): Promise<void> {
for (const localToDevicePathData of localToDevicePaths) {
const devicePath = localToDevicePathData.getDevicePath();
if (require("fs").existsSync(devicePath)) {
require("fs").unlinkSync(devicePath);
if (fs.existsSync(devicePath)) {
fs.unlinkSync(devicePath);
}
}
}
Comment on lines +70 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

removeFiles aborts on the first failing unlink.

fs.unlinkSync runs unguarded inside the loop; a single locked file (common on Windows when the file handle is still open) throws EPERM/EBUSY and aborts removal of the remaining paths. Consider wrapping each unlink in a try/catch (and/or logging) so one stuck file doesn't block the rest of the sync.

🛡️ Proposed fix
 		for (const localToDevicePathData of localToDevicePaths) {
 			const devicePath = localToDevicePathData.getDevicePath();
-			if (fs.existsSync(devicePath)) {
-				fs.unlinkSync(devicePath);
-			}
+			try {
+				if (fs.existsSync(devicePath)) {
+					fs.unlinkSync(devicePath);
+				}
+			} catch (err) {
+				this.$logger.trace(
+					`[Windows LiveSync] Failed to remove ${devicePath}: ${err}`,
+				);
+			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async removeFiles(
_deviceAppData: Mobile.IDeviceAppData,
localToDevicePaths: Mobile.ILocalToDevicePathData[],
): Promise<void> {
for (const localToDevicePathData of localToDevicePaths) {
const devicePath = localToDevicePathData.getDevicePath();
if (fs.existsSync(devicePath)) {
fs.unlinkSync(devicePath);
}
}
}
public async removeFiles(
_deviceAppData: Mobile.IDeviceAppData,
localToDevicePaths: Mobile.ILocalToDevicePathData[],
): Promise<void> {
for (const localToDevicePathData of localToDevicePaths) {
const devicePath = localToDevicePathData.getDevicePath();
try {
if (fs.existsSync(devicePath)) {
fs.unlinkSync(devicePath);
}
} catch (err) {
this.$logger.trace(
`[Windows LiveSync] Failed to remove ${devicePath}: ${err}`,
);
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/livesync/windows-device-livesync-service.ts` around lines 53 -
63, The removeFiles method in WindowsDeviceLivesyncService currently calls
fs.unlinkSync inside the loop without handling per-file failures, so one locked
file can stop deletion of the बाकी paths. Update removeFiles to wrap each
unlinkSync call in its own try/catch (optionally logging the error) while
keeping the loop going, so a single EPERM or EBUSY on one devicePath does not
abort cleanup of the remaining localToDevicePaths.

Expand Down
23 changes: 10 additions & 13 deletions lib/services/windows-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class WindowsProjectService
super($fs, $projectDataService);
}

private _platformData: IPlatformData = null;
private _platformData: IPlatformData | null = null;

public getPlatformData(projectData: IProjectData): IPlatformData {
if (!projectData && !this._platformData) {
Expand Down Expand Up @@ -86,6 +86,7 @@ export class WindowsProjectService
packageNames: [
`${projectData.projectName}.msix`,
`${projectData.projectName}.appx`,
`${projectData.projectName}.exe`,
],
};
},
Expand All @@ -97,7 +98,7 @@ export class WindowsProjectService
};
}

return this._platformData;
return this._platformData as IPlatformData;
}

public async validateOptions(
Expand Down Expand Up @@ -434,12 +435,11 @@ export class WindowsProjectService
}

public async preparePluginNativeCode(
_pluginData: IPluginData,
_options?: any,
pluginData: IPluginData,
projectData: IProjectData,
): Promise<void> {
const pluginData = _pluginData;

// stage native files found under plugin's platforms/windows folder into the app's plugins dir
// Stage native files found under the plugin's platforms/windows folder into
// the app's plugins directory so the csproj can import them.
const platformFolder = path.join(
pluginData.fullPath,
"platforms",
Expand All @@ -452,13 +452,10 @@ export class WindowsProjectService
? fallbackFolder
: null;
if (!sourcesFolder) {
// Nothing to stage for this plugin
return;
}

const projectData = arguments.length >= 2 ? arguments[1] : null;
if (!projectData) {
// attempt to find a projectData by walking up (best-effort); otherwise skip
return;
}

Expand All @@ -471,15 +468,15 @@ export class WindowsProjectService
this.$fs.ensureDirectoryExists(pluginStageDir);

// recursively copy native files (exclude JS/TS/JSON)
const walk = (dir: string, out: string) => {
const walk = (dir: string) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
const src = path.join(dir, e.name);
const rel = path.relative(sourcesFolder, src);
const dest = path.join(pluginStageDir, rel);
if (e.isDirectory()) {
this.$fs.ensureDirectoryExists(dest);
walk(src, dest);
walk(src);
} else if (e.isFile()) {
const ext = path.extname(e.name).toLowerCase();
if (
Expand All @@ -496,7 +493,7 @@ export class WindowsProjectService
}
};

walk(sourcesFolder, pluginStageDir);
walk(sourcesFolder);

// copy provided plugin.props/targets if present in plugin root
const providedProps = path.join(pluginData.fullPath, "plugin.props");
Expand Down