diff --git a/README.md b/README.md index 7324ffcff2..cb42250be8 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ NativeScript provides platform APIs directly to the JavaScript runtime (_with st Some popular use cases: -- Building Web, iOS, Android and Vision Pro apps with a shared codebase (aka, cross platform apps) +- Building Web, iOS, Android, Windows and Vision Pro apps with a shared codebase (aka, cross platform apps) - Building native platform apps with portable JavaScript skills - Augmenting JavaScript projects with platform API capabilities - AndroidTV and Watch development @@ -84,20 +84,21 @@ The NativeScript CLI is the command-line interface for interacting with NativeSc ![NativeScript CLI diagram](https://github.com/NativeScript/nativescript-cli/raw/release/ns-cli.png) * **Commands** - pretty much what every CLI does - support of different command options, input validation and help -* **Devices Service** - provides the communication between NativeScript and devices/emulators/simulators used to run/debug the app. Uses iTunes to talk to iOS and adb for Android +* **Devices Service** - provides the communication between NativeScript and devices/emulators/simulators used to run/debug the app. Uses iTunes to talk to iOS, adb for Android, and local MSIX deployment for Windows. * **LiveSync Service** - redeploys applications when code changes during development * **Hooks Service** - executes custom-written hooks in developed application, thus modifying the build process -* **Platforms Service** - provides app build functionalities, uses Gradle to build Android packages and Xcode for iOS. +* **Platforms Service** - provides app build functionalities, uses Gradle to build Android packages, Xcode for iOS, and MSBuild for Windows. [Back to Top][1] Supported Platforms === -With the NativeScript CLI, you can target the following mobile platforms. +With the NativeScript CLI, you can target the following platforms. * Android 4.2 or a later stable official release * iOS 9.0 or later stable official release +* Windows 10 version 1809 (build 17763) or later — via `@nativescript/windows` [Back to Top][1] @@ -277,11 +278,12 @@ You can always override the generated entitlements file, by pointing to your own ## Build Your Project -You can build it for your target mobile platforms. +You can build it for your target platforms. ```Shell ns build android ns build ios +ns build windows ``` The NativeScript CLI calls the SDK for the selected target platform and uses it to build your app locally. @@ -290,11 +292,13 @@ When you build for iOS, the NativeScript CLI will either build for a device, if > **IMPORTANT:** To build your app for an iOS device, you must configure a valid certificate and provisioning profile pair, and have that pair present on your system for code signing your application package. For more information, see [iOS Code Signing - A Complete Walkthrough](https://seventhsoulmountain.blogspot.com/2013/09/ios-code-sign-in-complete-walkthrough.html). +To build for Windows, you need the [.NET 10 SDK](https://dotnet.microsoft.com/download) with the Windows App SDK workload (`dotnet workload install windows`) and Developer Mode enabled. `ns build windows` produces an MSIX package. + [Back to Top][1] ## Run Your Project -You can test your work in progress on connected Android or iOS devices. +You can test your work in progress on connected Android or iOS devices, or on the local Windows machine. To verify that the NativeScript CLI recognizes your connected devices, run the following command. @@ -302,13 +306,14 @@ To verify that the NativeScript CLI recognizes your connected devices, run the f ns devices ``` -The NativeScript CLI lists all connected physical devices and running emulators/simulators. +The NativeScript CLI lists all connected physical devices and running emulators/simulators. On Windows, the local machine is also listed as a Windows device. -After you have listed the available devices, you can quickly run your app on connected devices by executing: +After you have listed the available devices, you can quickly run your app by executing: ```Shell ns run android ns run ios +ns run windows # Windows only — runs on the local machine ``` [Back to Top][1] diff --git a/docs/man_pages/project/testing/build-windows.md b/docs/man_pages/project/testing/build-windows.md new file mode 100644 index 0000000000..6511d22f00 --- /dev/null +++ b/docs/man_pages/project/testing/build-windows.md @@ -0,0 +1,56 @@ +<% if (isJekyll) { %>--- +title: ns build windows +position: 4 +---<% } %> + +# ns build windows + +### Description + +Builds the project for Windows and produces an MSIX package that you can deploy on any Windows 10/11 machine with Developer Mode enabled. + +<% if(isConsole && (isLinux || isMacOS)) { %>WARNING: You can run this command only on Windows systems. To view the complete help for this command, run `$ ns help build windows`<% } %> +<% if((isConsole && isWindows) || isHtml) { %> +### Commands + +Usage | Synopsis +---|--- +General | `$ ns build windows [--release] [--env.*]` + +### Options + +* `--release` - If set, produces a release build. Otherwise, produces a debug build with DevTools support enabled. +* `--env.*` - Specifies additional flags that the bundler may process. Can be passed multiple times. Supported additional flags: + * `--env.aot` - creates Ahead-Of-Time build (Angular only). + * `--env.uglify` - provides basic obfuscation and smaller app size. + * `--env.report` - creates a Webpack report inside a `report` folder in the root folder. + * `--env.sourceMap` - creates inline source maps. +* `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. + +<% } %> + +<% if(isHtml) { %> + +### Prerequisites + +* Windows 10 version 1809 (build 17763) or later. +* [.NET 10 SDK](https://dotnet.microsoft.com/download) with the Windows App SDK workload installed (`dotnet workload install windows`). +* MSBuild available in `PATH` (installed with Visual Studio or the .NET SDK). + +### Command Limitations + +* You can run `$ ns build windows` only on Windows systems. + +### Related Commands + +Command | Description +----------|---------- +[build android](build-android.html) | Builds the project for Android and produces an APK. +[build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA. +[build](build.html) | Builds the project for the selected target platform. +[debug windows](debug-windows.html) | Debugs your project on the local Windows machine. +[run windows](run-windows.html) | Runs your project on the local Windows machine. +[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. +[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. +[run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. +<% } %> diff --git a/docs/man_pages/project/testing/build.md b/docs/man_pages/project/testing/build.md index 8b726b7219..ace05eab42 100644 --- a/docs/man_pages/project/testing/build.md +++ b/docs/man_pages/project/testing/build.md @@ -22,9 +22,10 @@ Usage | Synopsis <% if((isConsole && isMacOS) || isHtml) { %>General | `$ ns build `<% } %><% if(isConsole && (isLinux || isWindows)) { %>General | `$ ns build android`<% } %> <% if((isConsole && isMacOS) || isHtml) { %>### Arguments -`` is the target mobile platform for which you want to build your project. You can set the following target platforms. +`` is the target platform for which you want to build your project. You can set the following target platforms. * `android` - Build the project for Android and produces an `APK` that you can manually deploy on a device or in the native emulator. -* `ios` - Build the project for iOS and produces an `APP` or `IPA` that you can manually deploy in the iOS Simulator or on a device.<% } %> +* `ios` - Build the project for iOS and produces an `APP` or `IPA` that you can manually deploy in the iOS Simulator or on a device. +* `windows` - Build the project for Windows and produces an `MSIX` package (Windows only).<% } %> ### Options @@ -53,6 +54,7 @@ Command | Description [appstore upload](../../publishing/appstore-upload.html) | Uploads project to iTunes Connect. [build android](build-android.html) | Builds the project for Android and produces an APK that you can manually deploy on device or in the native emulator. [build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively. +[build windows](build-windows.html) | Builds the project for Windows and produces an MSIX package. [debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. diff --git a/docs/man_pages/project/testing/debug-windows.md b/docs/man_pages/project/testing/debug-windows.md new file mode 100644 index 0000000000..b2d9edbc26 --- /dev/null +++ b/docs/man_pages/project/testing/debug-windows.md @@ -0,0 +1,69 @@ +<% if (isJekyll) { %>--- +title: ns debug windows +position: 7 +---<% } %> + +# ns debug windows + +### Description + +Initiates a debugging session for your project on the local Windows machine. When necessary, the command will prepare, build, deploy and launch the app before starting the debug session. The NativeScript runtime starts a Chrome DevTools Protocol server on port 9229 — attach Chrome DevTools or any CDP-compatible debugger to `ws://localhost:9229`. + +<% if(isConsole && (isLinux || isMacOS)) { %>WARNING: You can run this command only on Windows systems. To view the complete help for this command, run `$ ns help debug windows`<% } %> +<% if((isConsole && isWindows) || isHtml) { %> +### Commands + +Usage | Synopsis +---|--- +Deploy, run and attach the Chrome DevTools debugger | `$ ns debug windows [--device ] [--timeout ]` +Deploy, run and stop at the first code statement | `$ ns debug windows --debug-brk [--timeout ]` +Attach the debug tools to a running app | `$ ns debug windows --start [--timeout ]` + +### Options + +* `--debug-brk` - Builds, deploys and launches the application and stops at the first JavaScript statement. +* `--start` - Attaches the debug tools to a deployed and running app without restarting it. +* `--timeout` - Sets the number of seconds that the NativeScript CLI will wait for the app to launch. Default: 90 seconds. +* `--no-watch` - If set, changes in your code will not be reflected during the execution of this command. +* `--no-hmr` - Disables Hot Module Replacement (HMR). +* `--env.*` - Specifies additional flags that the bundler may process. Can be passed multiple times. +* `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. + +<% } %> + +<% if(isHtml) { %> + +### Prerequisites + +* Windows 10 version 1809 (build 17763) or later. +* [.NET 10 SDK](https://dotnet.microsoft.com/download) with the Windows App SDK workload installed. +* Developer Mode enabled in Windows Settings. +* Google Chrome or any debugger supporting the Chrome DevTools Protocol (CDP). + +### How to attach Chrome DevTools + +1. Run `ns debug windows` +2. Open Chrome and navigate to `chrome://inspect` +3. Under **Devices**, click **Configure** and add `localhost:9229` +4. The NativeScript runtime will appear under **Remote Target** — click **inspect** + +### Command Limitations + +* You can run `$ ns debug windows` only on Windows systems. + +### Related Commands + +Command | Description +----------|---------- +[build windows](build-windows.html) | Builds the project for Windows and produces an MSIX package. +[build android](build-android.html) | Builds the project for Android. +[build ios](build-ios.html) | Builds the project for iOS. +[build](build.html) | Builds the project for the selected target platform. +[debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. +[debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. +[debug](debug.html) | Debugs your project on a connected device or in a native emulator. +[run windows](run-windows.html) | Runs your project on the local Windows machine. +[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. +[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. +[run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. +<% } %> diff --git a/docs/man_pages/project/testing/debug.md b/docs/man_pages/project/testing/debug.md index 02cc125e2e..505856b638 100644 --- a/docs/man_pages/project/testing/debug.md +++ b/docs/man_pages/project/testing/debug.md @@ -38,15 +38,17 @@ Usage | Synopsis <% if((isConsole && isMacOS) || isHtml) { %>General | `$ ns debug `<% } %><% if(isConsole && (isLinux || isWindows)) { %>General | `$ ns debug android`<% } %> <% if((isConsole && isMacOS) || isHtml) { %>### Arguments -`` is the target mobile platform for which you want to debug your project. You can set the following target platforms: +`` is the target platform for which you want to debug your project. You can set the following target platforms: * `android` - Start a debugging session for your project on a connected Android device or Android emulator. -* `ios` - Start a debugging session for your project on a connected iOS device or in the native iOS simulator.<% } %> +* `ios` - Start a debugging session for your project on a connected iOS device or in the native iOS simulator. +* `windows` - Start a debugging session on the local Windows machine via Chrome DevTools Protocol on port 9229 (Windows only).<% } %> <% if(isHtml) { %> ### Command Limitations * You can run `$ ns debug ios` only on macOS systems. +* You can run `$ ns debug windows` only on Windows systems. ### Related Commands @@ -57,6 +59,7 @@ Command | Description [build](build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator. [debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. +[debug windows](debug-windows.html) | Debugs your project on the local Windows machine. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. diff --git a/docs/man_pages/project/testing/run-windows.md b/docs/man_pages/project/testing/run-windows.md new file mode 100644 index 0000000000..664acc7309 --- /dev/null +++ b/docs/man_pages/project/testing/run-windows.md @@ -0,0 +1,63 @@ +<% if (isJekyll) { %>--- +title: ns run windows +position: 11 +---<% } %> + +# ns run windows + +### Description + +Runs your project on the local Windows machine. This is shorthand for prepare, build, deploy and launch. While your app is running, prints the output from the application in the console and watches for changes in your code. Once a change is detected, it synchronizes the change with the running application. + +<% if(isConsole && (isLinux || isMacOS)) { %>WARNING: You can run this command only on Windows systems. To view the complete help for this command, run `$ ns help run windows`<% } %> +<% if((isConsole && isWindows) || isHtml) { %> +When running this command without passing `--release` flag, the app is built in debug configuration with the DevTools server enabled on port 9229. +<% } %> + +### Commands + +Usage | Synopsis +---|--- +Run on the local Windows device | `$ ns run windows [--release] [--justlaunch] [--env.*]` + +### Options + +* `--justlaunch` - If set, does not print the application output in the console. +* `--release` - If set, produces a release build. Otherwise, produces a debug build. +* `--no-hmr` - Disables Hot Module Replacement (HMR). When a change in the code is applied, CLI will transfer the modified files and restart the application. +* `--env.*` - Specifies additional flags that the bundler may process. Can be passed multiple times. Supported additional flags: + * `--env.aot` - creates Ahead-Of-Time build (Angular only). + * `--env.uglify` - provides basic obfuscation and smaller app size. + * `--env.report` - creates a Webpack report inside a `report` folder in the root folder. + * `--env.sourceMap` - creates inline source maps. +* `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. + +<% if(isHtml) { %> + +### Prerequisites + +Before running your app, verify that your system meets the following requirements. +* Windows 10 version 1809 (build 17763) or later. +* [.NET 10 SDK](https://dotnet.microsoft.com/download) with the Windows App SDK workload installed. +* Developer Mode enabled in Windows Settings → Privacy & Security → For Developers. + +### Command Limitations + +* You can run `$ ns run windows` only on Windows systems. + +### Related Commands + +Command | Description +----------|---------- +[build windows](build-windows.html) | Builds the project for Windows and produces an MSIX package. +[build android](build-android.html) | Builds the project for Android. +[build ios](build-ios.html) | Builds the project for iOS. +[build](build.html) | Builds the project for the selected target platform. +[debug windows](debug-windows.html) | Debugs your project on the local Windows machine. +[debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. +[debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. +[debug](debug.html) | Debugs your project on a connected device or in a native emulator. +[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. +[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. +[run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. +<% } %> diff --git a/docs/man_pages/project/testing/run.md b/docs/man_pages/project/testing/run.md index ee45bb9510..039bed446e 100644 --- a/docs/man_pages/project/testing/run.md +++ b/docs/man_pages/project/testing/run.md @@ -58,9 +58,10 @@ Run on a selected connected device or running emulator. Will start emulator with <% if((isConsole && isMacOS) || isHtml) { %>### Arguments -`` is the target mobile platform for which you want to run your project. You can set the following target platforms: +`` is the target platform for which you want to run your project. You can set the following target platforms: * `android` - Run your project on all Android devices and emulators. * `ios` - Run your project on all iOS devices and simulators. + * `windows` - Run your project on the local Windows machine (Windows only). <% } %> @@ -68,7 +69,8 @@ Run on a selected connected device or running emulator. Will start emulator with ### Command Limitations -* The command will work with all connected devices and running emulators on macOS. On Windows and Linux the command will work with Android devices only. +* The command will work with all connected devices and running emulators on macOS. On Linux the command will work with Android devices only. +* On Windows the command works with Android devices, emulators, and the local Windows machine (via `ns run windows`). * In case a platform is not specified and there's no running devices and emulators, the command will fail. ### Related Commands @@ -86,6 +88,7 @@ Command | Description [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. +[run windows](run-windows.html) | Runs your project on the local Windows machine. [test init](test-init.html) | Configures your project for unit testing with a selected framework. [test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. [test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 3281d84930..31e275e608 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -56,6 +56,7 @@ injector.require( injector.require("iOSExtensionsService", "./services/ios-extensions-service"); injector.require("iOSWatchAppService", "./services/ios-watch-app-service"); injector.require("iOSProjectService", "./services/ios-project-service"); +injector.require("windowsProjectService", "./services/windows-project-service"); injector.require("iOSProvisionService", "./services/ios-provision-service"); injector.require("xcconfigService", "./services/xcconfig-service"); injector.require("iOSSigningService", "./services/ios/ios-signing-service"); @@ -182,6 +183,7 @@ injector.requireCommand("run|ios", "./commands/run"); injector.requireCommand("run|android", "./commands/run"); injector.requireCommand("run|vision", "./commands/run"); injector.requireCommand("run|visionos", "./commands/run"); +injector.requireCommand("run|windows", "./commands/run"); injector.requireCommand("typings", "./commands/typings"); injector.requireCommand("preview", "./commands/preview"); @@ -190,6 +192,7 @@ injector.requireCommand("debug|ios", "./commands/debug"); injector.requireCommand("debug|android", "./commands/debug"); injector.requireCommand("debug|vision", "./commands/debug"); injector.requireCommand("debug|visionos", "./commands/debug"); +injector.requireCommand("debug|windows", "./commands/debug"); injector.requireCommand("fonts", "./commands/fonts"); injector.requireCommand("prepare", "./commands/prepare"); @@ -197,6 +200,7 @@ injector.requireCommand("build|ios", "./commands/build"); injector.requireCommand("build|android", "./commands/build"); injector.requireCommand("build|vision", "./commands/build"); injector.requireCommand("build|visionos", "./commands/build"); +injector.requireCommand("build|windows", "./commands/build"); injector.requireCommand("deploy", "./commands/deploy"); injector.requireCommand("embed", "./commands/embedding/embed"); @@ -321,6 +325,10 @@ injector.require( "iOSLiveSyncService", "./services/livesync/ios-livesync-service", ); +injector.require( + "windowsLiveSyncService", + "./services/livesync/windows-livesync-service", +); injector.require("usbLiveSyncService", "./services/livesync/livesync-service"); // The name is used in https://github.com/NativeScript/nativescript-dev-typescript injector.requirePublic("sysInfo", "./sys-info"); diff --git a/lib/commands/build.ts b/lib/commands/build.ts index f3c3c1adb3..e20e8c74e4 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -277,3 +277,57 @@ export class BuildVisionOsCommand extends BuildIosCommand implements ICommand { injector.registerCommand("build|vision", BuildVisionOsCommand); injector.registerCommand("build|visionos", BuildVisionOsCommand); + +export class BuildWindowsCommand extends BuildCommandBase implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + protected $options: IOptions, + $errors: IErrors, + $projectData: IProjectData, + $platformsDataService: IPlatformsDataService, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $buildController: IBuildController, + $platformValidationService: IPlatformValidationService, + $buildDataService: IBuildDataService, + protected $logger: ILogger, + private $migrateController: IMigrateController, + ) { + super( + $options, + $errors, + $projectData, + $platformsDataService, + $devicePlatformsConstants, + $buildController, + $platformValidationService, + $buildDataService, + $logger, + ); + } + + public async execute(args: string[]): Promise { + await this.executeCore([ + this.$devicePlatformsConstants.Windows.toLowerCase(), + ]); + } + + public async canExecute(args: string[]): Promise { + const platform = this.$devicePlatformsConstants.Windows; + if (!this.$options.force) { + await this.$migrateController.validate({ + projectDir: this.$projectData.projectDir, + platforms: [platform], + }); + } + + let canExecute = await super.canExecuteCommandBase(platform); + if (canExecute) { + canExecute = await super.validateArgs(args, platform); + } + + return canExecute; + } +} + +injector.registerCommand("build|windows", BuildWindowsCommand); diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index c7478ab09f..5b9033a6e6 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -248,3 +248,35 @@ export class DebugAndroidCommand implements ICommand { } injector.registerCommand("debug|android", DebugAndroidCommand); + +export class DebugWindowsCommand implements ICommand { + @cache() + private get debugPlatformCommand(): DebugPlatformCommand { + return this.$injector.resolve(DebugPlatformCommand, { + platform: this.$devicePlatformsConstants.Windows, + }); + } + + public allowedParameters: ICommandParameter[] = []; + + constructor( + protected $errors: IErrors, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $injector: IInjector, + private $projectData: IProjectData, + ) { + this.$projectData.initializeProjectData(); + } + + public async execute(args: string[]): Promise { + return this.debugPlatformCommand.execute(args); + } + + public async canExecute(args: string[]): Promise { + return this.debugPlatformCommand.canExecute(args); + } + + public platform = this.$devicePlatformsConstants.Windows; +} + +injector.registerCommand("debug|windows", DebugWindowsCommand); diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 8b7e789c1b..800b99a79f 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -30,20 +30,20 @@ export class RunCommandBase implements ICommand { private $migrateController: IMigrateController, private $options: IOptions, private $projectData: IProjectData, - private $keyCommandHelper: IKeyCommandHelper + private $keyCommandHelper: IKeyCommandHelper, ) {} public allowedParameters: ICommandParameter[] = []; public async execute(args: string[]): Promise { await this.$liveSyncCommandHelper.executeCommandLiveSync( this.platform, - this.liveSyncCommandHelperAdditionalOptions + this.liveSyncCommandHelperAdditionalOptions, ); if (process.env.NS_IS_INTERACTIVE) { this.$keyCommandHelper.attachKeyCommands( this.platform as IKeyCommandPlatform, - "run" + "run", ); } } @@ -64,7 +64,7 @@ export class RunCommandBase implements ICommand { : [ this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.iOS, - ]; + ]; if (!this.$options.force) { await this.$migrateController.validate({ @@ -100,7 +100,7 @@ export class RunIosCommand implements ICommand { protected $injector: IInjector, protected $options: IOptions, protected $platformValidationService: IPlatformValidationService, - protected $projectDataService: IProjectDataService + protected $projectDataService: IProjectDataService, ) {} public async execute(args: string[]): Promise { @@ -113,11 +113,11 @@ export class RunIosCommand implements ICommand { if ( !this.$platformValidationService.isPlatformSupportedForOS( this.platform, - projectData + projectData, ) ) { this.$errors.fail( - `Applications for platform ${this.platform} can not be built on this OS` + `Applications for platform ${this.platform} can not be built on this OS`, ); } @@ -127,7 +127,7 @@ export class RunIosCommand implements ICommand { this.$options.provision, this.$options.teamId, projectData, - this.platform.toLowerCase() + this.platform.toLowerCase(), )); return result; } @@ -154,7 +154,7 @@ export class RunAndroidCommand implements ICommand { private $injector: IInjector, private $options: IOptions, private $platformValidationService: IPlatformValidationService, - private $projectData: IProjectData + private $projectData: IProjectData, ) {} public async execute(args: string[]): Promise { @@ -167,11 +167,11 @@ export class RunAndroidCommand implements ICommand { if ( !this.$platformValidationService.isPlatformSupportedForOS( this.$devicePlatformsConstants.Android, - this.$projectData + this.$projectData, ) ) { this.$errors.fail( - `Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS` + `Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS`, ); } @@ -190,7 +190,7 @@ export class RunAndroidCommand implements ICommand { this.$options.provision, this.$options.teamId, this.$projectData, - this.$devicePlatformsConstants.Android.toLowerCase() + this.$devicePlatformsConstants.Android.toLowerCase(), ); } } @@ -208,7 +208,7 @@ export class RunVisionOSCommand extends RunIosCommand { protected $injector: IInjector, protected $options: IOptions, protected $platformValidationService: IPlatformValidationService, - protected $projectDataService: IProjectDataService + protected $projectDataService: IProjectDataService, ) { super( $devicePlatformsConstants, @@ -216,10 +216,61 @@ export class RunVisionOSCommand extends RunIosCommand { $injector, $options, $platformValidationService, - $projectDataService + $projectDataService, ); } } injector.registerCommand("run|vision", RunVisionOSCommand); injector.registerCommand("run|visionos", RunVisionOSCommand); + +export class RunWindowsCommand implements ICommand { + @cache() + private get runCommand(): RunCommandBase { + const runCommand = this.$injector.resolve(RunCommandBase); + runCommand.platform = this.platform; + return runCommand; + } + + public allowedParameters: ICommandParameter[] = []; + public get platform(): string { + return this.$devicePlatformsConstants.Windows; + } + + constructor( + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $errors: IErrors, + private $injector: IInjector, + private $options: IOptions, + private $platformValidationService: IPlatformValidationService, + private $projectData: IProjectData, + ) {} + + public async execute(args: string[]): Promise { + return this.runCommand.execute(args); + } + + public async canExecute(args: string[]): Promise { + await this.runCommand.canExecute(args); + + if ( + !this.$platformValidationService.isPlatformSupportedForOS( + this.$devicePlatformsConstants.Windows, + this.$projectData, + ) + ) { + this.$errors.fail( + `Applications for platform ${this.$devicePlatformsConstants.Windows} can not be built on this OS`, + ); + } + + return this.$platformValidationService.validateOptions( + this.$options.provision, + this.$options.teamId, + this.$projectData, + this.$devicePlatformsConstants.Windows.toLowerCase(), + ); + } +} + +injector.registerCommand("run|windows", RunWindowsCommand); diff --git a/lib/common/bootstrap.ts b/lib/common/bootstrap.ts index 9905f452b9..cb8fcf2d11 100644 --- a/lib/common/bootstrap.ts +++ b/lib/common/bootstrap.ts @@ -42,15 +42,15 @@ injector.requireCommand("autocomplete|status", "./commands/autocompletion"); injector.requireCommand( ["device|*list", "devices|*list"], - "./commands/device/list-devices" + "./commands/device/list-devices", ); injector.requireCommand( ["device|android", "devices|android"], - "./commands/device/list-devices" + "./commands/device/list-devices", ); injector.requireCommand( ["device|ios", "devices|ios"], - "./commands/device/list-devices" + "./commands/device/list-devices", ); injector.requireCommand("device|log", "./commands/device/device-log-stream"); @@ -58,11 +58,11 @@ injector.requireCommand("device|run", "./commands/device/run-application"); injector.requireCommand("device|stop", "./commands/device/stop-application"); injector.requireCommand( "device|list-applications", - "./commands/device/list-applications" + "./commands/device/list-applications", ); injector.requireCommand( "device|uninstall", - "./commands/device/uninstall-application" + "./commands/device/uninstall-application", ); injector.requireCommand("device|list-files", "./commands/device/list-files"); injector.requireCommand("device|get-file", "./commands/device/get-file"); @@ -70,82 +70,86 @@ injector.requireCommand("device|put-file", "./commands/device/put-file"); injector.require( "iosDeviceOperations", - "./mobile/ios/device/ios-device-operations" + "./mobile/ios/device/ios-device-operations", ); injector.require("deviceDiscovery", "./mobile/mobile-core/device-discovery"); injector.require( "iOSDeviceDiscovery", - "./mobile/mobile-core/ios-device-discovery" + "./mobile/mobile-core/ios-device-discovery", ); injector.require( "iOSSimulatorDiscovery", - "./mobile/mobile-core/ios-simulator-discovery" + "./mobile/mobile-core/ios-simulator-discovery", ); injector.require( "androidDeviceDiscovery", - "./mobile/mobile-core/android-device-discovery" + "./mobile/mobile-core/android-device-discovery", +); +injector.require( + "windowsDeviceDiscovery", + "./mobile/windows/windows-device-discovery", ); injector.require( "androidEmulatorDiscovery", - "./mobile/mobile-core/android-emulator-discovery" + "./mobile/mobile-core/android-emulator-discovery", ); injector.require("iOSDevice", "./mobile/ios/device/ios-device"); injector.require( "iOSDeviceProductNameMapper", - "./mobile/ios/ios-device-product-name-mapper" + "./mobile/ios/ios-device-product-name-mapper", ); injector.require("androidDevice", "./mobile/android/android-device"); injector.require("adb", "./mobile/android/android-debug-bridge"); injector.require( "androidDebugBridgeResultHandler", - "./mobile/android/android-debug-bridge-result-handler" + "./mobile/android/android-debug-bridge-result-handler", ); injector.require( "androidVirtualDeviceService", - "./mobile/android/android-virtual-device-service" + "./mobile/android/android-virtual-device-service", ); injector.require( "androidIniFileParser", - "./mobile/android/android-ini-file-parser" + "./mobile/android/android-ini-file-parser", ); injector.require( "androidGenymotionService", - "./mobile/android/genymotion/genymotion-service" + "./mobile/android/genymotion/genymotion-service", ); injector.require( "virtualBoxService", - "./mobile/android/genymotion/virtualbox-service" + "./mobile/android/genymotion/virtualbox-service", ); injector.require("logcatHelper", "./mobile/android/logcat-helper"); injector.require("iOSSimResolver", "./mobile/ios/simulator/ios-sim-resolver"); injector.require( "iOSSimulatorLogProvider", - "./mobile/ios/simulator/ios-simulator-log-provider" + "./mobile/ios/simulator/ios-simulator-log-provider", ); injector.require( "localToDevicePathDataFactory", - "./mobile/local-to-device-path-data-factory" + "./mobile/local-to-device-path-data-factory", ); injector.requirePublic( "devicesService", - "./mobile/mobile-core/devices-service" + "./mobile/mobile-core/devices-service", ); injector.requirePublic( "androidProcessService", - "./mobile/mobile-core/android-process-service" + "./mobile/mobile-core/android-process-service", ); injector.require("projectNameValidator", "./validators/project-name-validator"); injector.require( "androidEmulatorServices", - "./mobile/android/android-emulator-services" + "./mobile/android/android-emulator-services", ); injector.require( "iOSEmulatorServices", - "./mobile/ios/simulator/ios-emulator-services" + "./mobile/ios/simulator/ios-emulator-services", ); injector.require("wp8EmulatorServices", "./mobile/wp8/wp8-emulator-services"); @@ -157,18 +161,18 @@ injector.require("mobileHelper", "./mobile/mobile-helper"); injector.require("emulatorHelper", "./mobile/emulator-helper"); injector.require( "devicePlatformsConstants", - "./mobile/device-platforms-constants" + "./mobile/device-platforms-constants", ); injector.require("helpService", "./services/help-service"); injector.require( "messageContractGenerator", - "./services/message-contract-generator" + "./services/message-contract-generator", ); injector.require("proxyService", "./services/proxy-service"); injector.requireCommand("dev-preuninstall", "./commands/preuninstall"); injector.requireCommand( "dev-generate-messages", - "./commands/generate-messages" + "./commands/generate-messages", ); injector.requireCommand("doctor|*all", "./commands/doctor"); injector.requireCommand("doctor|ios", "./commands/doctor"); diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 0d4b58638b..998a533937 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -258,8 +258,7 @@ declare global { * Describes different options for filtering device logs. */ interface IDeviceLogOptions - extends IDictionary, - Partial { + extends IDictionary, Partial { /** * Process id of the application on the device. */ @@ -284,8 +283,7 @@ declare global { * Describes required methods for getting iOS Simulator's logs. */ interface IiOSSimulatorLogProvider - extends NodeJS.EventEmitter, - IShouldDispose { + extends NodeJS.EventEmitter, IShouldDispose { /** * Starts the process for getting simulator logs and emits and DEVICE_LOG_EVENT_NAME event. * @param {string} deviceId The unique identifier of the device. @@ -533,8 +531,7 @@ declare global { /** * Describes options that can be passed to devices service's initialization method. */ - interface IDevicesServicesInitializationOptions - extends Partial { + interface IDevicesServicesInitializationOptions extends Partial { /** * If passed will start an emulator if necesasry. */ @@ -1197,6 +1194,7 @@ declare global { isAndroidPlatform(platform: string): boolean; isiOSPlatform(platform: string): boolean; isvisionOSPlatform(platform: string): boolean; + isWindowsPlatform(platform: string): boolean; isApplePlatform(platform: string): boolean; normalizePlatformName(platform: string): string; validatePlatformName(platform: string): string; @@ -1241,10 +1239,12 @@ declare global { iOS: string; Android: string; visionOS: string; + Windows: string; isiOS(value: string): boolean; isAndroid(value: string): boolean; isvisionOS(value: string): boolean; + isWindows(value: string): boolean; } interface IDeviceApplication { @@ -1261,8 +1261,7 @@ declare global { } interface IDeviceLookingOptions - extends IHasEmulatorOption, - IHasDetectionInterval { + extends IHasEmulatorOption, IHasDetectionInterval { shouldReturnImmediateResult: boolean; platform: string; fullDiscovery?: boolean; @@ -1388,8 +1387,7 @@ declare global { /** * Describes information about application on device. */ - interface IDeviceApplicationInformation - extends IDeviceApplicationInformationBase { + interface IDeviceApplicationInformation extends IDeviceApplicationInformationBase { /** * The framework of the project (Cordova or NativeScript). */ diff --git a/lib/common/file-system.ts b/lib/common/file-system.ts index d4ef58d970..108f86a9b7 100644 --- a/lib/common/file-system.ts +++ b/lib/common/file-system.ts @@ -135,12 +135,18 @@ export class FileSystem implements IFileSystem { } public deleteDirectory(directory: string): void { - shelljs.rm("-rf", directory); - - const err = shelljs.error(); - - if (err !== null) { - throw new Error(err); + // fs.rmSync handles Windows edge cases (read-only attributes, long paths) + // more reliably than shelljs.rm on Node.js 20+. + try { + fs.rmSync(directory, { recursive: true, force: true }); + } catch (e: any) { + // If rmSync itself fails (e.g., files locked by another process on Windows), + // fall back to shelljs so behaviour on other platforms is unchanged. + shelljs.rm("-rf", directory); + const err = shelljs.error(); + if (err !== null) { + throw new Error(e?.message ?? err); + } } } diff --git a/lib/common/mobile/device-platforms-constants.ts b/lib/common/mobile/device-platforms-constants.ts index a02eb88ffe..19f1ced9ff 100644 --- a/lib/common/mobile/device-platforms-constants.ts +++ b/lib/common/mobile/device-platforms-constants.ts @@ -6,6 +6,7 @@ export class DevicePlatformsConstants public iOS = "iOS"; public Android = "Android"; public visionOS = "visionOS"; + public Windows = "Windows"; public isiOS(value: string) { return value.toLowerCase() === this.iOS.toLowerCase(); @@ -18,5 +19,9 @@ export class DevicePlatformsConstants public isvisionOS(value: string) { return value.toLowerCase() === this.visionOS.toLowerCase(); } + + public isWindows(value: string) { + return value.toLowerCase() === this.Windows.toLowerCase(); + } } injector.register("devicePlatformsConstants", DevicePlatformsConstants); diff --git a/lib/common/mobile/mobile-core/devices-service.ts b/lib/common/mobile/mobile-core/devices-service.ts index 15bf11787f..64cf77668a 100644 --- a/lib/common/mobile/mobile-core/devices-service.ts +++ b/lib/common/mobile/mobile-core/devices-service.ts @@ -42,6 +42,7 @@ export class DevicesService private $iOSSimulatorDiscovery: Mobile.IiOSSimulatorDiscovery, private $iOSDeviceDiscovery: Mobile.IDeviceDiscovery, private $androidDeviceDiscovery: Mobile.IDeviceDiscovery, + private $windowsDeviceDiscovery: Mobile.IDeviceDiscovery, private $staticConfig: Config.IStaticConfig, private $messages: IMessages, private $mobileHelper: Mobile.IMobileHelper, @@ -64,6 +65,7 @@ export class DevicesService this.$iOSDeviceDiscovery, this.$androidDeviceDiscovery, this.$iOSSimulatorDiscovery, + this.$windowsDeviceDiscovery, ]; } @@ -324,6 +326,7 @@ export class DevicesService this.$iOSSimulatorDiscovery, this.$iOSDeviceDiscovery, this.$androidDeviceDiscovery, + this.$windowsDeviceDiscovery, ].forEach(this.attachToDeviceDiscoveryEvents.bind(this)); } @@ -1147,6 +1150,10 @@ export class DevicesService await this.$iOSSimulatorDiscovery.startLookingForDevices( deviceLookingOptions, ); + } else if (this.$mobileHelper.isWindowsPlatform(platform)) { + await this.$windowsDeviceDiscovery.startLookingForDevices( + deviceLookingOptions, + ); } } diff --git a/lib/common/mobile/mobile-helper.ts b/lib/common/mobile/mobile-helper.ts index 666f2ffae9..5b5ee33de8 100644 --- a/lib/common/mobile/mobile-helper.ts +++ b/lib/common/mobile/mobile-helper.ts @@ -13,7 +13,7 @@ export class MobileHelper implements Mobile.IMobileHelper { private $errors: IErrors, private $fs: IFileSystem, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $tempService: ITempService + private $tempService: ITempService, ) {} public get platformNames(): string[] { @@ -21,6 +21,7 @@ export class MobileHelper implements Mobile.IMobileHelper { this.$devicePlatformsConstants.iOS, this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.visionOS, + this.$devicePlatformsConstants.Windows, ]; } @@ -48,6 +49,14 @@ export class MobileHelper implements Mobile.IMobileHelper { ); } + public isWindowsPlatform(platform: string): boolean { + return !!( + platform && + this.$devicePlatformsConstants.Windows.toLowerCase() === + platform.toLowerCase() + ); + } + public isApplePlatform(platform: string): boolean { return this.isiOSPlatform(platform) || this.isvisionOSPlatform(platform); } @@ -59,6 +68,8 @@ export class MobileHelper implements Mobile.IMobileHelper { return "iOS"; } else if (this.isvisionOSPlatform(platform)) { return "visionOS"; + } else if (this.isWindowsPlatform(platform)) { + return "Windows"; } return undefined; @@ -77,7 +88,7 @@ export class MobileHelper implements Mobile.IMobileHelper { this.$errors.fail( "'%s' is not a valid device platform. Valid platforms are %s.", platform, - helpers.formatListOfNames(this.platformNames) + helpers.formatListOfNames(this.platformNames), ); } @@ -86,7 +97,7 @@ export class MobileHelper implements Mobile.IMobileHelper { public buildDevicePath(...args: string[]): string { return this.correctDevicePath( - args.join(MobileHelper.DEVICE_PATH_SEPARATOR) + args.join(MobileHelper.DEVICE_PATH_SEPARATOR), ); } @@ -101,7 +112,7 @@ export class MobileHelper implements Mobile.IMobileHelper { public async getDeviceFileContent( device: Mobile.IDevice, deviceFilePath: string, - projectData: IProjectData + projectData: IProjectData, ): Promise { const uniqueFilePath = await this.$tempService.path({ suffix: ".tmp" }); const platform = device.deviceInfo.platform.toLowerCase(); @@ -109,7 +120,7 @@ export class MobileHelper implements Mobile.IMobileHelper { await device.fileSystem.getFile( deviceFilePath, projectData.projectIdentifiers[platform], - uniqueFilePath + uniqueFilePath, ); } catch (e) { return null; diff --git a/lib/common/mobile/windows/windows-application-manager.ts b/lib/common/mobile/windows/windows-application-manager.ts new file mode 100644 index 0000000000..3472fe881c --- /dev/null +++ b/lib/common/mobile/windows/windows-application-manager.ts @@ -0,0 +1,335 @@ +import * as fs from "fs"; +import * as os from "os"; +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 = null; + // Keyed by appId so multiple UWP apps don't stomp each other's cached PFN. + private _packageFamilyNames: Map = 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 = {}; + + constructor( + $logger: ILogger, + $hooksService: IHooksService, + $deviceLogProvider: Mobile.IDeviceLogProvider, + private $childProcess: IChildProcess, + private _restartLogStream?: () => Promise, + ) { + super($logger, $hooksService, $deviceLogProvider); + } + + public async getInstalledApplications(): Promise { + try { + const result = await this.$childProcess.spawnFromEvent( + "powershell.exe", + [ + "-NoProfile", + "-Command", + "Get-AppxPackage | Select-Object -ExpandProperty PackageFamilyName", + ], + "close", + {}, + { throwError: false }, + ); + return (result.stdout || "") + .split(/\r?\n/) + .map((s: string) => s.trim()) + .filter(Boolean); + } catch { + return []; + } + } + + // 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 { + 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 { + 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}`); + // If we have an app identifier, try to remove any existing package first to + // avoid the "package is already installed" HRESULT (0x80073CFB) which + // blocks re-registration in development flows. + if (appIdentifier) { + try { + this.$logger.info(`[Windows] Attempting to remove existing package: ${appIdentifier}`); + // uninstallApplication handles EXE cleanup and runs the Remove-AppxPackage + // command for UWP packages. Ignore errors and proceed to install. + await this.uninstallApplication(appIdentifier); + } catch (err) { + this.$logger.warn(`[Windows] Pre-install uninstall failed: ${err}`); + } + } + await this.$childProcess.spawnFromEvent( + "powershell.exe", + [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + `Add-AppxPackage -ForceApplicationShutdown -Register -Path "${packageFilePath}"`, + ], + "close", + {}, + { throwError: true }, + ); + // 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 { + // Clean up EXE registration so isApplicationInstalled returns false. + delete this._installedExePaths[appIdentifier]; + this._packageFamilyNames.delete(appIdentifier); + + await this.$childProcess.spawnFromEvent( + "powershell.exe", + [ + "-NoProfile", + "-Command", + `Get-AppxPackage -Name "${appIdentifier}" | Remove-AppxPackage`, + ], + "close", + {}, + { throwError: false }, + ); + } + + // 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 { + await this.stopApplication(appData); + await this.startApplication(appData); + } + + /** + * Returns the path of the runtime's trace log for the most recently started app. + * Inside a UWP process, Win32 GetTempPathW() virtualises to AC\Temp (the app + * container's isolated temp folder) — NOT the WinRT TemporaryFolder (TempState). + * Rust's std::env::temp_dir() calls GetTempPathW(), so the DLL writes to AC\Temp. + * Falls back to the system temp path when no PFN is known (unpackaged EXE). + */ + public getLogFilePath(): string { + const systemTempLog = path.join(os.tmpdir(), "ns_trace.log"); + if (this._packageFamilyNames.size > 0) { + const pfn = this._packageFamilyNames.values().next().value as string; + const localAppData = process.env.LOCALAPPDATA; + if (localAppData && pfn) { + return path.join(localAppData, "Packages", pfn, "AC", "Temp", "ns_trace.log"); + } + } + return systemTempLog; + } + + /** + * Returns the path of the C# crash/exception log written by CrashDiagnostics. + * Lives in LocalState (persistent app data). + * Returns null when no PFN is known (e.g. unpackaged EXE targets). + */ + public getCrashLogPath(): string | null { + if (this._packageFamilyNames.size > 0) { + const pfn = this._packageFamilyNames.values().next().value as string; + const localAppData = process.env.LOCALAPPDATA; + if (localAppData && pfn) { + return path.join(localAppData, "Packages", pfn, "LocalState", "nativescript-crash.log"); + } + } + return null; + } + + public async startApplication( + appData: Mobile.IStartApplicationData, + ): Promise { + const exeCandidate = + (appData.appId && this._installedExePaths[appData.appId]) || + (appData.projectName && this._installedExePaths[appData.projectName]); + const isExe = !!(exeCandidate && fs.existsSync(exeCandidate)); + + // For UWP, pre-populate the PFN cache before calling getLogFilePath() so + // that the truncation and the subsequent log stream restart both target the + // correct container TempState path instead of the system temp fallback. + if (!isExe) { + await this._resolvePackageFamilyName(appData.appId); + } + + // Truncate the trace log so the streamer starts from a clean state each run. + try { + fs.writeFileSync(this.getLogFilePath(), "", "utf8"); + } catch { /* ignore — log dir may not exist yet */ } + + if (isExe) { + 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 as string, [], { 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; + } + }); + } else { + // PFN already cached from the pre-resolve above; no extra round-trip. + const pfn = this._packageFamilyNames.get(appData.appId) ?? appData.appId; + if (appData.waitForDebugger) { + this._writeDebugBreakMarker(pfn); + } + // UWP apps are launched via shell:AppsFolder\!. + // The ApplicationId comes from the attribute in the manifest. + const appId = "App"; + this.$logger.info(`[Windows] Launching UWP: ${pfn}!${appId}`); + const proc = spawn("explorer.exe", [`shell:AppsFolder\\${pfn}!${appId}`], { + detached: true, + stdio: "ignore", + }); + proc.unref(); + } + + // Restart the log stream so the tailer picks up the correct path (UWP + // container TempState vs. system temp) and resets its offset to 0. Without + // this the tailer keeps the stale offset from device-discovery time and + // skips every log line written after the file was truncated above. + if (this._restartLogStream) { + await this._restartLogStream(); + } + } + + public async stopApplication( + appData: Mobile.IApplicationData, + ): Promise { + if (this._runningPid) { + try { + process.kill(this._runningPid); + } catch { + /* already gone */ + } + this._runningPid = null; + } else { + await this.$childProcess.spawnFromEvent( + "powershell.exe", + [ + "-NoProfile", + "-Command", + `Stop-Process -Name "${appData.projectName}" -ErrorAction SilentlyContinue`, + ], + "close", + {}, + { throwError: false }, + ); + } + } + + public async tryStartApplication( + appData: Mobile.IApplicationData, + ): Promise { + try { + await this.startApplication(appData as Mobile.IStartApplicationData); + } catch { + /* ignore */ + } + } + + public async getDebuggableApps(): Promise< + Mobile.IDeviceApplicationInformation[] + > { + return []; + } + + public async getDebuggableAppViews( + _appIdentifiers: string[], + ): Promise> { + return {} as IDictionary; + } + + private async _resolvePackageFamilyName(appId: string): Promise { + 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}`); + } + } +} diff --git a/lib/common/mobile/windows/windows-device-discovery.ts b/lib/common/mobile/windows/windows-device-discovery.ts new file mode 100644 index 0000000000..38fd757fc2 --- /dev/null +++ b/lib/common/mobile/windows/windows-device-discovery.ts @@ -0,0 +1,36 @@ +import { DeviceDiscovery } from "../mobile-core/device-discovery"; +import { WindowsDevice } from "./windows-device"; +import { IInjector } from "../../definitions/yok"; +import { injector } from "../../yok"; + +export class WindowsDeviceDiscovery + extends DeviceDiscovery + implements Mobile.IDeviceDiscovery +{ + private _deviceAdded = false; + + constructor( + private $injector: IInjector, + private $mobileHelper: Mobile.IMobileHelper, + ) { + super(); + } + + public async startLookingForDevices( + options?: Mobile.IDeviceLookingOptions, + ): Promise { + if ( + options?.platform && + !this.$mobileHelper.isWindowsPlatform(options.platform) + ) { + return; + } + + if (!this._deviceAdded) { + this._deviceAdded = true; + const device = this.$injector.resolve(WindowsDevice); + this.addDevice(device); + } + } +} +injector.register("windowsDeviceDiscovery", WindowsDeviceDiscovery); diff --git a/lib/common/mobile/windows/windows-device-file-system.ts b/lib/common/mobile/windows/windows-device-file-system.ts new file mode 100644 index 0000000000..656f9824e4 --- /dev/null +++ b/lib/common/mobile/windows/windows-device-file-system.ts @@ -0,0 +1,114 @@ +import * as fs from "fs"; +import * as path from "path"; +import { IStringDictionary } from "../../declarations"; + +export class WindowsDeviceFileSystem implements Mobile.IDeviceFileSystem { + public async listFiles(_devicePath: string): Promise { + // no-op for local desktop + } + + public async getFile( + deviceFilePath: string, + _appIdentifier: string, + outputFilePath?: string, + ): Promise { + if (outputFilePath) { + fs.copyFileSync(deviceFilePath, outputFilePath); + } + } + + public async getFileContent( + deviceFilePath: string, + _appIdentifier: string, + ): Promise { + return fs.existsSync(deviceFilePath) + ? fs.readFileSync(deviceFilePath, "utf8") + : null; + } + + public async putFile( + localFilePath: string, + deviceFilePath: string, + _appIdentifier: string, + ): Promise { + fs.mkdirSync(path.dirname(deviceFilePath), { recursive: true }); + fs.copyFileSync(localFilePath, deviceFilePath); + } + + public async deleteFile( + deviceFilePath: string, + _appIdentifier: string, + ): Promise { + if (fs.existsSync(deviceFilePath)) { + fs.unlinkSync(deviceFilePath); + } + } + + public async transferFiles( + _deviceAppData: Mobile.IDeviceAppData, + localToDevicePaths: Mobile.ILocalToDevicePathData[], + ): Promise { + this._syncPaths(localToDevicePaths); + return localToDevicePaths; + } + + public async transferDirectory( + _deviceAppData: Mobile.IDeviceAppData, + localToDevicePaths: Mobile.ILocalToDevicePathData[], + _projectFilesPath: string, + ): Promise { + this._syncPaths(localToDevicePaths); + return localToDevicePaths; + } + + private _syncPaths(localToDevicePaths: Mobile.ILocalToDevicePathData[]): void { + for (const pathData of localToDevicePaths) { + const src = pathData.getLocalPath(); + const dest = pathData.getDevicePath(); + if (src === dest) { + continue; + } + try { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + fs.mkdirSync(dest, { recursive: true }); + } else { + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); + } + } catch { + // skip entries that disappeared between detection and sync + } + } + } + + public async transferFile( + localFilePath: string, + deviceFilePath: string, + ): Promise { + fs.mkdirSync(path.dirname(deviceFilePath), { recursive: true }); + fs.copyFileSync(localFilePath, deviceFilePath); + } + + public async createFileOnDevice( + deviceFilePath: string, + fileContent: string, + ): Promise { + fs.mkdirSync(path.dirname(deviceFilePath), { recursive: true }); + fs.writeFileSync(deviceFilePath, fileContent, "utf8"); + } + + public async updateHashesOnDevice( + _hashes: IStringDictionary, + _appIdentifier: string, + ): Promise { + // no-op — Windows LiveSync does not use hash-based diffing yet + } + + public getDeviceHashService(_appIdentifier: string): any { + return { + generateHashesFromLocalToDevicePaths: async (_paths: any[]) => ({}), + removeHashes: async (_paths: any[]) => {}, + }; + } +} diff --git a/lib/common/mobile/windows/windows-device.ts b/lib/common/mobile/windows/windows-device.ts new file mode 100644 index 0000000000..893c8a9588 --- /dev/null +++ b/lib/common/mobile/windows/windows-device.ts @@ -0,0 +1,141 @@ +import * as os from "os"; +import * as fs from "fs"; +import { DeviceConnectionType } from "../../../constants"; +import { CONNECTED_STATUS, DeviceTypes } from "../../constants"; +import { WindowsApplicationManager } from "./windows-application-manager"; +import { WindowsDeviceFileSystem } from "./windows-device-file-system"; +import { IHooksService, IChildProcess } from "../../declarations"; + +export class WindowsDevice implements Mobile.IDevice { + public applicationManager: Mobile.IDeviceApplicationManager; + public fileSystem: Mobile.IDeviceFileSystem; + public readonly isEmulator = false; + public readonly isOnlyWiFiConnected = false; + + public readonly deviceInfo: Mobile.IDeviceInfo = { + identifier: os.hostname(), + displayName: `${os.hostname()} (Windows ${os.release()})`, + model: "PC", + version: os.release(), + vendor: "Microsoft", + status: CONNECTED_STATUS, + errorHelp: null, + isTablet: false, + type: DeviceTypes.Device, + platform: "Windows", + connectionTypes: [DeviceConnectionType.Local], + }; + + private _logTailInterval: ReturnType | null = null; + private _crashLogTailInterval: ReturnType | null = null; + + constructor( + $logger: ILogger, + $hooksService: IHooksService, + private $deviceLogProvider: Mobile.IDeviceLogProvider, + $childProcess: IChildProcess, + ) { + this.applicationManager = new WindowsApplicationManager( + $logger, + $hooksService, + $deviceLogProvider, + $childProcess, + () => this.openDeviceLogStream(), + ); + this.fileSystem = new WindowsDeviceFileSystem(); + } + + public async openDeviceLogStream(): Promise { + if (this._logTailInterval) { + clearInterval(this._logTailInterval); + this._logTailInterval = null; + } + if (this._crashLogTailInterval) { + clearInterval(this._crashLogTailInterval); + this._crashLogTailInterval = null; + } + + // For packaged UWP apps, GetTempPath() inside the app container resolves to + // %LOCALAPPDATA%\Packages\\TempState — not the system temp dir. + // Ask the application manager for the correct path based on the known PFN. + const manager = this.applicationManager as WindowsApplicationManager; + const logPath = manager.getLogFilePath(); + const crashLogPath = manager.getCrashLogPath(); + const deviceId = this.deviceInfo.identifier; + + // startApplication() truncates the trace log before calling this method, so the + // file is either empty (size 0) or absent. Start from 0 to capture all + // output from the new process. The initial call from DeviceEmitter (before + // any app starts) also starts at 0; the truncation in startApplication() + // is what prevents stale output from being replayed. + let offset = 0; + + // Rotate the log if it exceeds 10 MB to prevent unbounded disk growth. + const MAX_LOG_BYTES = 10 * 1024 * 1024; + + // Internal Rust runtime diagnostics written via debug_output() — useful in + // VS Output / DebugView but noisy in the CLI. Suppress them; errors/exceptions + // are kept because their prefix contains "error", "exception", or "PANIC". + const INTERNAL_PREFIXES = [ + "[NativeScript] init_console:", + "[NativeScript] log file:", + "[NativeScript] delegate ctor:", + ]; + const isInternalTrace = (line: string) => + INTERNAL_PREFIXES.some((p) => line.startsWith(p)); + + this._logTailInterval = setInterval(() => { + try { + const stat = fs.statSync(logPath); + if (stat.size <= offset) return; + + const toRead = stat.size - offset; + const buf = Buffer.alloc(toRead); + const fd = fs.openSync(logPath, "r"); + fs.readSync(fd, buf, 0, toRead, offset); + fs.closeSync(fd); + offset = stat.size; + + const lines = buf.toString("utf8").split(/\r?\n/); + for (const line of lines) { + if (line.trim() && !isInternalTrace(line)) { + this.$deviceLogProvider.logData(line, "Windows", deviceId); + } + } + + if (stat.size > MAX_LOG_BYTES) { + try { fs.writeFileSync(logPath, "", "utf8"); offset = 0; } catch { /* ignore */ } + } + } catch { /* ignore — file may not exist between app restarts */ } + }, 50); + + // Also tail nativescript-crash.log from LocalState so C# exception reports + // and JS errors caught by the host surface in the CLI — same as Android/iOS + // crash log streaming. Truncate on each run so only errors from this session appear. + if (crashLogPath) { + try { fs.writeFileSync(crashLogPath, "", "utf8"); } catch { /* may not exist yet */ } + let crashOffset = 0; + + this._crashLogTailInterval = setInterval(() => { + try { + const stat = fs.statSync(crashLogPath); + if (stat.size <= crashOffset) return; + + const toRead = stat.size - crashOffset; + const buf = Buffer.alloc(toRead); + const fd = fs.openSync(crashLogPath, "r"); + fs.readSync(fd, buf, 0, toRead, crashOffset); + fs.closeSync(fd); + crashOffset = stat.size; + + const lines = buf.toString("utf8").split(/\r?\n/); + for (const line of lines) { + if (line.trim()) { + this.$deviceLogProvider.logData(`[crash] ${line}`, "Windows", deviceId); + } + } + } catch { /* ignore — file may not exist until first crash */ } + }, 50); + } + } +} diff --git a/lib/constants.ts b/lib/constants.ts index c76d6f6696..133279ea8f 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -24,6 +24,7 @@ export const TNS_IOS_RUNTIME_NAME = "tns-ios"; export const SCOPED_ANDROID_RUNTIME_NAME = "@nativescript/android"; export const SCOPED_IOS_RUNTIME_NAME = "@nativescript/ios"; export const SCOPED_VISIONOS_RUNTIME_NAME = "@nativescript/visionos"; +export const SCOPED_WINDOWS_RUNTIME_NAME = "@nativescript/windows"; export const PACKAGE_JSON_FILE_NAME = "package.json"; export const PACKAGE_LOCK_JSON_FILE_NAME = "package-lock.json"; export const ANDROID_DEVICE_APP_ROOT_TEMPLATE = `/data/data/%s/files`; @@ -172,9 +173,7 @@ export class ITMSConstants { static altoolExecutableName = "altool"; } -class ItunesConnectApplicationTypesClass - implements IiTunesConnectApplicationType -{ +class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationType { public iOS = "iOS App"; public Mac = "Mac OS X App"; } @@ -348,12 +347,14 @@ export const enum PlatformTypes { ios = "ios", android = "android", visionos = "visionos", + windows = "windows", } export type SupportedPlatform = | PlatformTypes.ios | PlatformTypes.android - | PlatformTypes.visionos; + | PlatformTypes.visionos + | PlatformTypes.windows; export const PODFILE_NAME = "Podfile"; diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index b92ea3bbf8..4e4ede64a3 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -15,6 +15,7 @@ import { AnalyticsEventLabelDelimiter, BUNDLER_COMPILATION_COMPLETE, CONFIG_FILE_NAME_JS, + APP_FOLDER_NAME, CONFIG_FILE_NAME_TS, PACKAGE_JSON_FILE_NAME, PLATFORMS_DIR_NAME, @@ -482,6 +483,14 @@ export class PrepareController extends EventEmitter { "app", "package.json", ); + } else if ( + this.$mobileHelper.isWindowsPlatform(platformData.platformNameLowerCase) + ) { + packagePath = path.join( + platformData.appDestinationDirectoryPath, + APP_FOLDER_NAME, + "package.json", + ); } else { packagePath = path.join( platformData.projectRoot, diff --git a/lib/data/prepare-data.ts b/lib/data/prepare-data.ts index 2af06adbeb..b7e3ff8f97 100644 --- a/lib/data/prepare-data.ts +++ b/lib/data/prepare-data.ts @@ -14,7 +14,7 @@ export class PrepareData extends ControllerDataBase { constructor( public projectDir: string, public platform: string, - data: IOptions + data: IOptions, ) { super(projectDir, platform, data); @@ -64,3 +64,12 @@ export class IOSPrepareData extends PrepareData { } export class AndroidPrepareData extends PrepareData {} + +export class WindowsPrepareData extends PrepareData { + public packageFamilyName?: string; + + constructor(projectDir: string, platform: string, data: IOptions) { + super(projectDir, platform, data); + this.packageFamilyName = (data as any).packageFamilyName; + } +} diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts index faba420116..1f8ad6633d 100644 --- a/lib/definitions/livesync.d.ts +++ b/lib/definitions/livesync.d.ts @@ -508,6 +508,7 @@ declare global { appIdentifier: string; getDirname?: boolean; watch?: boolean; + projectData?: IProjectData; } interface IDevicePathProvider { diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index c8046b2ed4..5f04a0792e 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -134,6 +134,8 @@ interface INsConfigIOS extends INsConfigPlaform { interface INSConfigVisionOS extends INsConfigIOS {} +interface INsConfigWindows extends INsConfigPlaform {} + interface INsConfigAndroid extends INsConfigPlaform { v8Flags?: string; @@ -188,6 +190,7 @@ interface INsConfig { ios?: INsConfigIOS; android?: INsConfigAndroid; visionos?: INSConfigVisionOS; + windows?: INsConfigWindows; ignoredNativeDependencies?: string[]; hooks?: INsConfigHooks[]; projectName?: string; diff --git a/lib/device-path-provider.ts b/lib/device-path-provider.ts index 6995c41b31..61ec448c53 100644 --- a/lib/device-path-provider.ts +++ b/lib/device-path-provider.ts @@ -3,26 +3,28 @@ import { APP_FOLDER_NAME } from "./constants"; import { LiveSyncPaths } from "./common/constants"; import * as path from "path"; import { IErrors } from "./common/declarations"; +import { IPlatformsDataService } from "./definitions/platform"; import { injector } from "./common/yok"; export class DevicePathProvider implements IDevicePathProvider { constructor( private $mobileHelper: Mobile.IMobileHelper, private $iOSSimResolver: Mobile.IiOSSimResolver, - private $errors: IErrors + private $errors: IErrors, + private $platformsDataService: IPlatformsDataService, ) {} public async getDeviceProjectRootPath( device: Mobile.IDevice, - options: IDeviceProjectRootOptions + options: IDeviceProjectRootOptions, ): Promise { let projectRoot = ""; if (this.$mobileHelper.isApplePlatform(device.deviceInfo.platform)) { projectRoot = device.isEmulator ? await this.$iOSSimResolver.iOSSim.getApplicationPath( device.deviceInfo.identifier, - options.appIdentifier - ) + options.appIdentifier, + ) : LiveSyncPaths.IOS_DEVICE_PROJECT_ROOT_PATH; if (!projectRoot) { @@ -47,6 +49,22 @@ export class DevicePathProvider implements IDevicePathProvider { : LiveSyncPaths.FULLSYNC_DIR_NAME; projectRoot = path.join(projectRoot, syncFolderName); } + } else if ( + this.$mobileHelper.isWindowsPlatform(device.deviceInfo.platform) + ) { + const projectData = options.projectData; + if (projectData) { + const platformData = this.$platformsDataService.getPlatformData( + device.deviceInfo.platform, + projectData, + ); + // Sync into bin\app — the registered package root — so the running app + // picks up changes without a rebuild. + return path.join( + platformData.getBuildOutputPath({} as never), + APP_FOLDER_NAME, + ); + } } return fromWindowsRelativePathToUnix(projectRoot); diff --git a/lib/project-data.ts b/lib/project-data.ts index 277dbf32d1..f8df45e18d 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -326,6 +326,7 @@ export class ProjectData implements IProjectData { ios: "", android: "", visionos: "", + windows: "", }; } @@ -333,6 +334,7 @@ export class ProjectData implements IProjectData { ios: config.id, android: config.id, visionos: config.id, + windows: config.id, }; if (config.ios && config.ios.id) { @@ -344,6 +346,9 @@ export class ProjectData implements IProjectData { if (config.visionos && config.visionos.id) { identifier.visionos = config.visionos.id; } + if (config.windows && config.windows.id) { + identifier.windows = config.windows.id; + } return identifier; } diff --git a/lib/resolvers/livesync-service-resolver.ts b/lib/resolvers/livesync-service-resolver.ts index 714374117b..bf5e8d74fc 100644 --- a/lib/resolvers/livesync-service-resolver.ts +++ b/lib/resolvers/livesync-service-resolver.ts @@ -6,7 +6,7 @@ export class LiveSyncServiceResolver implements ILiveSyncServiceResolver { constructor( private $errors: IErrors, private $injector: IInjector, - private $mobileHelper: Mobile.IMobileHelper + private $mobileHelper: Mobile.IMobileHelper, ) {} public resolveLiveSyncService(platform: string): IPlatformLiveSyncService { @@ -14,12 +14,14 @@ export class LiveSyncServiceResolver implements ILiveSyncServiceResolver { return this.$injector.resolve("iOSLiveSyncService"); } else if (this.$mobileHelper.isAndroidPlatform(platform)) { return this.$injector.resolve("androidLiveSyncService"); + } else if (this.$mobileHelper.isWindowsPlatform(platform)) { + return this.$injector.resolve("windowsLiveSyncService"); } this.$errors.fail( `Invalid platform ${platform}. Supported platforms are: ${this.$mobileHelper.platformNames.join( - ", " - )}` + ", ", + )}`, ); } } diff --git a/lib/services/build-data-service.ts b/lib/services/build-data-service.ts index e8a13f7c6f..1a01a36781 100644 --- a/lib/services/build-data-service.ts +++ b/lib/services/build-data-service.ts @@ -1,4 +1,4 @@ -import { AndroidBuildData, IOSBuildData } from "../data/build-data"; +import { AndroidBuildData, BuildData, IOSBuildData } from "../data/build-data"; import { IBuildDataService } from "../definitions/build"; import { injector } from "../common/yok"; @@ -10,6 +10,8 @@ export class BuildDataService implements IBuildDataService { return new IOSBuildData(projectDir, platform, data); } else if (this.$mobileHelper.isAndroidPlatform(platform)) { return new AndroidBuildData(projectDir, platform, data); + } else if (this.$mobileHelper.isWindowsPlatform(platform)) { + return new BuildData(projectDir, platform, data); } } } diff --git a/lib/services/livesync/windows-device-livesync-service.ts b/lib/services/livesync/windows-device-livesync-service.ts new file mode 100644 index 0000000000..83f0b24c93 --- /dev/null +++ b/lib/services/livesync/windows-device-livesync-service.ts @@ -0,0 +1,63 @@ +import * as fs from "fs"; +import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; +import { IPlatformsDataService } from "../../definitions/platform"; +import { IProjectData } from "../../definitions/project"; + +export class WindowsDeviceLiveSyncService + extends DeviceLiveSyncServiceBase + implements INativeScriptDeviceLiveSyncService +{ + constructor( + protected platformsDataService: IPlatformsDataService, + protected device: Mobile.IDevice, + private $logger: ILogger, + ) { + super(platformsDataService, device); + } + + public async restartApplication( + projectData: IProjectData, + _liveSyncInfo: ILiveSyncResultInfo, + ): Promise { + this.$logger.info( + `[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( + _projectData: IProjectData, + liveSyncInfo: ILiveSyncResultInfo, + ): Promise { + return !liveSyncInfo.useHotModuleReload; + } + + public async tryRefreshApplication( + _projectData: IProjectData, + _liveSyncInfo: ILiveSyncResultInfo, + ): Promise { + // HMR not yet implemented for Windows — signal full restart + return false; + } + + public async removeFiles( + _deviceAppData: Mobile.IDeviceAppData, + localToDevicePaths: Mobile.ILocalToDevicePathData[], + ): Promise { + for (const localToDevicePathData of localToDevicePaths) { + const devicePath = localToDevicePathData.getDevicePath(); + if (fs.existsSync(devicePath)) { + fs.unlinkSync(devicePath); + } + } + } +} diff --git a/lib/services/livesync/windows-livesync-service.ts b/lib/services/livesync/windows-livesync-service.ts new file mode 100644 index 0000000000..ff9623a6de --- /dev/null +++ b/lib/services/livesync/windows-livesync-service.ts @@ -0,0 +1,44 @@ +import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base"; +import { WindowsDeviceLiveSyncService } from "./windows-device-livesync-service"; +import { IPlatformsDataService } from "../../definitions/platform"; +import { IProjectData } from "../../definitions/project"; +import { IProjectFilesManager, IFileSystem } from "../../common/declarations"; +import { IInjector } from "../../common/definitions/yok"; +import { IOptions } from "../../declarations"; +import { injector } from "../../common/yok"; + +export class WindowsLiveSyncService + extends PlatformLiveSyncServiceBase + implements IPlatformLiveSyncService +{ + constructor( + protected $platformsDataService: IPlatformsDataService, + protected $projectFilesManager: IProjectFilesManager, + private $injector: IInjector, + $devicePathProvider: IDevicePathProvider, + $fs: IFileSystem, + $logger: ILogger, + $options: IOptions, + ) { + super( + $fs, + $logger, + $platformsDataService, + $projectFilesManager, + $devicePathProvider, + $options, + ); + } + + protected _getDeviceLiveSyncService( + device: Mobile.IDevice, + _data: IProjectData, + _frameworkVersion: string, + ): INativeScriptDeviceLiveSyncService { + return this.$injector.resolve( + WindowsDeviceLiveSyncService, + { device, platformsDataService: this.$platformsDataService }, + ); + } +} +injector.register("windowsLiveSyncService", WindowsLiveSyncService); diff --git a/lib/services/platform/add-platform-service.ts b/lib/services/platform/add-platform-service.ts index b93aba4ea7..e8eb7e83d0 100644 --- a/lib/services/platform/add-platform-service.ts +++ b/lib/services/platform/add-platform-service.ts @@ -25,7 +25,7 @@ export class AddPlatformService implements IAddPlatformService { // private $projectDataService: IProjectDataService, private $packageManager: IPackageManager, private $terminalSpinnerService: ITerminalSpinnerService, - private $analyticsService: IAnalyticsService // private $tempService: ITempService + private $analyticsService: IAnalyticsService, // private $tempService: ITempService ) {} public async addProjectHost() {} @@ -34,7 +34,7 @@ export class AddPlatformService implements IAddPlatformService { projectData: IProjectData, platformData: IPlatformData, packageToInstall: string, - addPlatformData: IAddPlatformData + addPlatformData: IAddPlatformData, ): Promise { const spinner = this.$terminalSpinnerService.createSpinner(); @@ -46,11 +46,34 @@ export class AddPlatformService implements IAddPlatformService { // : await this.installPackage(projectData.projectDir, packageToInstall); const frameworkDirPath = await this.installPackage( projectData.projectDir, - packageToInstall + packageToInstall, ); + + const frameworkPackageJsonPath = path.join( + frameworkDirPath || "", + "..", + "package.json", + ); + + if (!frameworkDirPath || !this.$fs.exists(frameworkPackageJsonPath)) { + throw new Error( + `Installed framework package.json not found at ${frameworkPackageJsonPath}`, + ); + } + const frameworkPackageJsonContent = this.$fs.readJson( - path.join(frameworkDirPath, "..", "package.json") + frameworkPackageJsonPath, ); + + if ( + !frameworkPackageJsonContent || + !frameworkPackageJsonContent.version + ) { + throw new Error( + `Installed framework package.json at ${frameworkPackageJsonPath} does not contain a version`, + ); + } + const frameworkVersion = frameworkPackageJsonContent.version; // await this.setPlatformVersion(platformData, projectData, frameworkVersion); @@ -64,7 +87,7 @@ export class AddPlatformService implements IAddPlatformService { platformData, projectData, frameworkDirPath, - frameworkVersion + frameworkVersion, ); } @@ -72,7 +95,7 @@ export class AddPlatformService implements IAddPlatformService { } catch (err) { const platformPath = path.join( projectData.platformsDir, - platformData.platformNameLowerCase + platformData.platformNameLowerCase, ); this.$fs.deleteDirectory(platformPath); throw err; @@ -84,11 +107,11 @@ export class AddPlatformService implements IAddPlatformService { public async setPlatformVersion( platformData: IPlatformData, projectData: IProjectData, - frameworkVersion: string + frameworkVersion: string, ): Promise { await this.installPackage( projectData.projectDir, - `${platformData.frameworkPackageName}@${frameworkVersion}` + `${platformData.frameworkPackageName}@${frameworkVersion}`, ); } @@ -106,7 +129,7 @@ export class AddPlatformService implements IAddPlatformService { private async installPackage( projectDir: string, - packageName: string + packageName: string, ): Promise { const frameworkDir = this.resolveFrameworkDir(projectDir, packageName); if (frameworkDir && this.$fs.exists(frameworkDir)) { @@ -122,7 +145,7 @@ export class AddPlatformService implements IAddPlatformService { dev: true, "save-dev": true, "save-exact": true, - } as any + } as any, ); if (!installedPackage.name) { @@ -172,7 +195,7 @@ export class AddPlatformService implements IAddPlatformService { } catch (err) { this.$logger.trace( `Couldn't resolve installed framework. Continuing with install...`, - err + err, ); } return null; @@ -183,14 +206,14 @@ export class AddPlatformService implements IAddPlatformService { platformData: IPlatformData, projectData: IProjectData, frameworkDirPath: string, - frameworkVersion: string + frameworkVersion: string, ): Promise { // here we should use ios OR android const platformDir = this.$options.hostProjectPath ?? path.join( projectData.platformsDir, - platformData.normalizedPlatformName.toLowerCase() + platformData.normalizedPlatformName.toLowerCase(), ); this.$fs.deleteDirectory(platformDir); @@ -198,21 +221,21 @@ export class AddPlatformService implements IAddPlatformService { await platformData.platformProjectService.createProject( path.resolve(frameworkDirPath), frameworkVersion, - projectData + projectData, ); platformData.platformProjectService.ensureConfigurationFileInAppResources( - projectData + projectData, ); await platformData.platformProjectService.interpolateData(projectData); platformData.platformProjectService.afterCreateProject( platformData.projectRoot, - projectData + projectData, ); } private async trackPlatformVersion( frameworkVersion: string, - platformData: IPlatformData + platformData: IPlatformData, ): Promise { await this.$analyticsService.trackEventActionInGoogleAnalytics({ action: TrackActionNames.AddPlatform, diff --git a/lib/services/platforms-data-service.ts b/lib/services/platforms-data-service.ts index 0f0ac30848..7383114a50 100644 --- a/lib/services/platforms-data-service.ts +++ b/lib/services/platforms-data-service.ts @@ -10,18 +10,20 @@ export class PlatformsDataService implements IPlatformsDataService { constructor( private $options: IOptions, $androidProjectService: IPlatformProjectService, - $iOSProjectService: IPlatformProjectService + $iOSProjectService: IPlatformProjectService, + $windowsProjectService: IPlatformProjectService, ) { this.platformsDataService = { ios: $iOSProjectService, android: $androidProjectService, visionos: $iOSProjectService, + windows: $windowsProjectService, }; } public getPlatformData( platform: string, - projectData: IProjectData + projectData: IProjectData, ): IPlatformData { const platformKey = platform && _.first(platform.toLowerCase().split("@")); let platformData: IPlatformData; diff --git a/lib/services/prepare-data-service.ts b/lib/services/prepare-data-service.ts index dc198b3d38..a10f06aee1 100644 --- a/lib/services/prepare-data-service.ts +++ b/lib/services/prepare-data-service.ts @@ -1,4 +1,8 @@ -import { IOSPrepareData, AndroidPrepareData } from "../data/prepare-data"; +import { + IOSPrepareData, + AndroidPrepareData, + WindowsPrepareData, +} from "../data/prepare-data"; import { injector } from "../common/yok"; import { IOptions } from "../declarations"; @@ -6,12 +10,14 @@ export class PrepareDataService implements IPrepareDataService { constructor(private $mobileHelper: Mobile.IMobileHelper) {} public getPrepareData(projectDir: string, platform: string, data: IOptions) { - const platformLowerCase = platform.toLowerCase(); + const platformLowerCase = platform && platform.toLowerCase(); if (this.$mobileHelper.isApplePlatform(platform)) { return new IOSPrepareData(projectDir, platformLowerCase, data); } else if (this.$mobileHelper.isAndroidPlatform(platform)) { return new AndroidPrepareData(projectDir, platformLowerCase, data); + } else if (this.$mobileHelper.isWindowsPlatform(platform)) { + return new WindowsPrepareData(projectDir, platformLowerCase, data); } } } diff --git a/lib/services/project-cleanup-service.ts b/lib/services/project-cleanup-service.ts index 25d7b01edf..852f0104c4 100644 --- a/lib/services/project-cleanup-service.ts +++ b/lib/services/project-cleanup-service.ts @@ -7,6 +7,8 @@ import { import { IFileSystem, IProjectHelper } from "../common/declarations"; import { injector } from "../common/yok"; import * as path from "path"; +import * as fs from "fs"; +import { execSync } from "child_process"; import { color } from "../color"; import { ITerminalSpinner, @@ -95,7 +97,10 @@ export class ProjectCleanupService implements IProjectCleanupService { this.$logger.trace( `${logPrefix}Path '${filePath}' is a directory, deleting.` ); - !dryRun && this.$fs.deleteDirectorySafe(filePath); + if (!dryRun) { + this._releaseWindowsFileLocks(filePath); + this.$fs.deleteDirectorySafe(filePath); + } fileType = "directory"; } else { this.$logger.trace( @@ -130,6 +135,34 @@ export class ProjectCleanupService implements IProjectCleanupService { } return { ok: true }; } + + /** + * On Windows, DLLs and EXEs inside the platforms build tree can be held open + * by a previously-running app or by the .NET build tooling. Kill any matching + * processes so the subsequent rmSync can succeed. + */ + private _releaseWindowsFileLocks(dirPath: string): void { + if (process.platform !== "win32") return; + + // Collect exe names from the directory that might be running. + const exeNames: string[] = []; + try { + for (const entry of fs.readdirSync(dirPath, { withFileTypes: true, recursive: true } as any) as unknown as fs.Dirent[]) { + if (!entry.isFile()) continue; + const name: string = (entry as any).name ?? ""; + if (name.toLowerCase().endsWith(".exe")) { + exeNames.push(name.slice(0, -4)); + } + } + } catch { /* directory might be partially deleted */ } + + for (const name of new Set(exeNames)) { + try { + execSync(`taskkill /F /IM "${name}.exe" /T`, { stdio: "ignore" }); + this.$logger.trace(`[clean] Killed process: ${name}.exe`); + } catch { /* process not running — ignore */ } + } + } } injector.register("projectCleanupService", ProjectCleanupService); diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 52f1ff49a5..42c8c874d6 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -52,7 +52,7 @@ export class ProjectDataService implements IProjectDataService { private $logger: ILogger, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $androidResourcesMigrationService: IAndroidResourcesMigrationService, - private $injector: IInjector + private $injector: IInjector, ) { try { // add the ProjectData of the default projectDir to the projectData cache @@ -72,7 +72,7 @@ export class ProjectDataService implements IProjectDataService { public getNSValue(projectDir: string, propertyName: string): any { return this.getValue( projectDir, - this.getNativeScriptPropertyName(propertyName) + this.getNativeScriptPropertyName(propertyName), ); } @@ -80,11 +80,11 @@ export class ProjectDataService implements IProjectDataService { try { return this.getPropertyValueFromJson( jsonData, - this.getNativeScriptPropertyName(propertyName) + this.getNativeScriptPropertyName(propertyName), ); } catch (e) { this.$logger.trace( - "Failed to get NS property value from JSON project data." + "Failed to get NS property value from JSON project data.", ); } @@ -98,7 +98,7 @@ export class ProjectDataService implements IProjectDataService { public removeNSProperty(projectDir: string, propertyName: string): void { this.removeProperty( projectDir, - this.getNativeScriptPropertyName(propertyName) + this.getNativeScriptPropertyName(propertyName), ); } @@ -109,7 +109,7 @@ export class ProjectDataService implements IProjectDataService { ][dependencyName]; this.$fs.writeJson( projectFileInfo.projectFilePath, - projectFileInfo.projectData + projectFileInfo.projectData, ); } @@ -128,7 +128,7 @@ export class ProjectDataService implements IProjectDataService { @exported("projectDataService") public getProjectDataFromContent( packageJsonContent: string, - projectDir?: string + projectDir?: string, ): IProjectData { projectDir = projectDir || this.defaultProjectDir; this.projectDataCache[projectDir] = @@ -136,25 +136,25 @@ export class ProjectDataService implements IProjectDataService { this.$injector.resolve(ProjectData); this.projectDataCache[projectDir].initializeProjectDataFromContent( packageJsonContent, - projectDir + projectDir, ); return this.projectDataCache[projectDir]; } @exported("projectDataService") public async getAssetsStructure( - opts: IProjectDir + opts: IProjectDir, ): Promise { const iOSAssetStructure = await this.getIOSAssetsStructure(opts); const androidAssetStructure = await this.getAndroidAssetsStructure(opts); this.$logger.trace( "iOS Assets structure:", - JSON.stringify(iOSAssetStructure, null, 2) + JSON.stringify(iOSAssetStructure, null, 2), ); this.$logger.trace( "Android Assets structure:", - JSON.stringify(androidAssetStructure, null, 2) + JSON.stringify(androidAssetStructure, null, 2), ); return { @@ -171,30 +171,30 @@ export class ProjectDataService implements IProjectDataService { const basePath = path.join( projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.iOS, - AssetConstants.iOSAssetsDirName + AssetConstants.iOSAssetsDirName, ); const pathToIcons = path.join(basePath, AssetConstants.iOSIconsDirName); const icons = await this.getIOSAssetSubGroup(pathToIcons); const pathToSplashBackgrounds = path.join( basePath, - AssetConstants.iOSSplashBackgroundsDirName + AssetConstants.iOSSplashBackgroundsDirName, ); const splashBackgrounds = await this.getIOSAssetSubGroup( - pathToSplashBackgrounds + pathToSplashBackgrounds, ); const pathToSplashCenterImages = path.join( basePath, - AssetConstants.iOSSplashCenterImagesDirName + AssetConstants.iOSSplashCenterImagesDirName, ); const splashCenterImages = await this.getIOSAssetSubGroup( - pathToSplashCenterImages + pathToSplashCenterImages, ); const pathToSplashImages = path.join( basePath, - AssetConstants.iOSSplashImagesDirName + AssetConstants.iOSSplashImagesDirName, ); const splashImages = await this.getIOSAssetSubGroup(pathToSplashImages); @@ -208,7 +208,7 @@ export class ProjectDataService implements IProjectDataService { public removeNSConfigProperty( projectDir: string, - propertyName: string + propertyName: string, ): void { this.$logger.trace(`Removing "${propertyName}" property from nsconfig.`); this.updateNsConfigValue(projectDir, null, [propertyName]); @@ -217,7 +217,7 @@ export class ProjectDataService implements IProjectDataService { @exported("projectDataService") public async getAndroidAssetsStructure( - opts: IProjectDir + opts: IProjectDir, ): Promise { // TODO: Use image-size package to get the width and height of an image. // TODO: Parse the splash_screen.xml in nodpi directory and get from it the names of the background and center image. @@ -227,10 +227,10 @@ export class ProjectDataService implements IProjectDataService { const projectData = this.getProjectData(projectDir); const pathToAndroidDir = path.join( projectData.appResourcesDirectoryPath, - this.$devicePlatformsConstants.Android + this.$devicePlatformsConstants.Android, ); const hasMigrated = this.$androidResourcesMigrationService.hasMigrated( - projectData.appResourcesDirectoryPath + projectData.appResourcesDirectoryPath, ); const basePath = hasMigrated ? path.join(pathToAndroidDir, SRC_DIR, MAIN_DIR, RESOURCES_DIR) @@ -239,7 +239,7 @@ export class ProjectDataService implements IProjectDataService { let useLegacy = false; try { const manifest = this.$fs.readText( - path.resolve(basePath, "../AndroidManifest.xml") + path.resolve(basePath, "../AndroidManifest.xml"), ); useLegacy = !manifest.includes(`android:icon="@mipmap/ic_launcher"`); } catch (err) { @@ -253,11 +253,11 @@ export class ProjectDataService implements IProjectDataService { icons: this.getAndroidAssetSubGroup(content.icons, basePath), splashBackgrounds: this.getAndroidAssetSubGroup( content.splashBackgrounds, - basePath + basePath, ), splashCenterImages: this.getAndroidAssetSubGroup( content.splashCenterImages, - basePath + basePath, ), splashImages: null, }; @@ -276,7 +276,7 @@ export class ProjectDataService implements IProjectDataService { const pathToProjectNodeModules = path.join( projectDir, - NODE_MODULES_FOLDER_NAME + NODE_MODULES_FOLDER_NAME, ); const files = this.$fs.enumerateFilesInDirectorySync( projectData.appDirectoryPath, @@ -296,7 +296,7 @@ export class ProjectDataService implements IProjectDataService { } return path.extname(filePath) === supportedFileExtension; - } + }, ); return files; @@ -311,7 +311,7 @@ export class ProjectDataService implements IProjectDataService { private updateNsConfigValue( projectDir: string, updateObject?: INsConfig, - propertiesToRemove?: string[] + propertiesToRemove?: string[], ): void { // todo: figure out a way to update js/ts configs // most likely needs an ast parser/writer @@ -322,7 +322,7 @@ export class ProjectDataService implements IProjectDataService { if (updateObject) { newNsConfig = _.assign( newNsConfig || this.getNsConfigDefaultObject(), - updateObject + updateObject, ); } @@ -345,7 +345,7 @@ export class ProjectDataService implements IProjectDataService { } catch (e) { this.$logger.trace( "The `nsconfig` content is not a valid JSON. Parse error: ", - e + e, ); } } @@ -360,7 +360,7 @@ export class ProjectDataService implements IProjectDataService { "..", CLI_RESOURCES_DIR_NAME, AssetConstants.assets, - AssetConstants.imageDefinitionsFileName + AssetConstants.imageDefinitionsFileName, ); const imageDefinitions = this.$fs.readJson(pathToImageDefinitions); @@ -370,7 +370,7 @@ export class ProjectDataService implements IProjectDataService { private async getIOSAssetSubGroup(dirPath: string): Promise { const pathToContentJson = path.join( dirPath, - AssetConstants.iOSResourcesFileName + AssetConstants.iOSResourcesFileName, ); const content = (this.$fs.exists(pathToContentJson) && this.$fs.readJson(pathToContentJson)) || { images: [] }; @@ -404,7 +404,7 @@ export class ProjectDataService implements IProjectDataService { assetSubGroup, (assetElement) => assetElement.filename === image.filename && - path.basename(assetElement.directory) === path.basename(dirPath) + path.basename(assetElement.directory) === path.basename(dirPath), ); if (assetItem) { @@ -434,20 +434,20 @@ export class ProjectDataService implements IProjectDataService { this.$logger.trace( "Missing data for image", image, - " in CLI's resource file, but we will try to generate images based on the size from Contents.json" + " in CLI's resource file, but we will try to generate images based on the size from Contents.json", ); finalContent.images.push(image); } else if (image.filename) { this.$logger.warn( `Didn't find a matching image definition for file ${path.join( path.basename(dirPath), - image.filename - )}. This file will be skipped from resources generation.` + image.filename, + )}. This file will be skipped from resources generation.`, ); } else { this.$logger.trace( `Unable to detect data for image generation of image`, - image + image, ); } } @@ -458,7 +458,7 @@ export class ProjectDataService implements IProjectDataService { private getAndroidAssetSubGroup( assetItems: IAssetItem[], - basePath: string + basePath: string, ): IAssetSubGroup { const assetSubGroup: IAssetSubGroup = { images: [], @@ -468,7 +468,7 @@ export class ProjectDataService implements IProjectDataService { const imagePath = path.join( basePath, assetItem.directory, - assetItem.filename + assetItem.filename, ); assetItem.path = imagePath; if (assetItem.width && assetItem.height) { @@ -489,7 +489,7 @@ export class ProjectDataService implements IProjectDataService { } catch (err) { this.$logger.trace( `Error while trying to get property ${propertyName} from ${projectDir}. Error is:`, - err + err, ); } } @@ -503,10 +503,10 @@ export class ProjectDataService implements IProjectDataService { private getPropertyValueFromJson( jsonData: any, - dottedPropertyName: string + dottedPropertyName: string, ): any { const props = dottedPropertyName.split( - NATIVESCRIPT_PROPS_INTERNAL_DELIMITER + NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, ); let result = jsonData[props.shift()]; @@ -555,7 +555,7 @@ export class ProjectDataService implements IProjectDataService { private getProjectFileData(projectDir: string): IProjectFileData { const projectFilePath = path.join( projectDir, - this.$staticConfig.PROJECT_FILE_NAME + this.$staticConfig.PROJECT_FILE_NAME, ); const projectFileContent = this.$fs.readText(projectFilePath); const projectData = projectFileContent @@ -577,11 +577,11 @@ export class ProjectDataService implements IProjectDataService { public getRuntimePackage( projectDir: string, - platform: constants.SupportedPlatform + platform: constants.SupportedPlatform, ): IBasePluginData { platform = platform.toLowerCase() as constants.SupportedPlatform; const packageJson = this.$fs.readJson( - path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME) + path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME), ); const runtimeName = platform === PlatformTypes.android @@ -611,6 +611,11 @@ export class ProjectDataService implements IProjectDataService { return projectDir + ":" + platform; }, shouldCache(result: IBasePluginData) { + // don't cache when there's no result + if (!result) { + return false; + } + // don't cache coerced versions if ((result as any)._coerced) { return false; @@ -622,7 +627,7 @@ export class ProjectDataService implements IProjectDataService { }) private getInstalledRuntimePackage( projectDir: string, - platform: constants.SupportedPlatform + platform: constants.SupportedPlatform, ): IBasePluginData { const runtimePackage = this.$pluginsService .getDependenciesFromPackageJson(projectDir) @@ -639,6 +644,8 @@ export class ProjectDataService implements IProjectDataService { ].includes(d.name); } else if (platform === constants.PlatformTypes.visionos) { return d.name === constants.SCOPED_VISIONOS_RUNTIME_NAME; + } else if (platform === constants.PlatformTypes.windows) { + return d.name === constants.SCOPED_WINDOWS_RUNTIME_NAME; } }); @@ -654,7 +661,7 @@ export class ProjectDataService implements IProjectDataService { runtimePackage.name, { paths: [projectDir], - } + }, ); if (!runtimePackageJsonPath) { @@ -663,12 +670,12 @@ export class ProjectDataService implements IProjectDataService { } runtimePackage.version = this.$fs.readJson( - runtimePackageJsonPath + runtimePackageJsonPath, ).version; } catch (err) { if (isRange) { runtimePackage.version = semver.coerce( - runtimePackage.version + runtimePackage.version, ).version; (runtimePackage as any)._coerced = true; @@ -683,7 +690,7 @@ export class ProjectDataService implements IProjectDataService { // default to the scoped runtimes this.$logger.trace( - "Could not find an installed runtime, falling back to default runtimes" + "Could not find an installed runtime, falling back to default runtimes", ); if (platform === constants.PlatformTypes.ios) { return { @@ -700,6 +707,11 @@ export class ProjectDataService implements IProjectDataService { name: constants.SCOPED_VISIONOS_RUNTIME_NAME, version: null, }; + } else if (platform === constants.PlatformTypes.windows) { + return { + name: constants.SCOPED_WINDOWS_RUNTIME_NAME, + version: null, + }; } } diff --git a/lib/services/windows-project-service.ts b/lib/services/windows-project-service.ts new file mode 100644 index 0000000000..1cdc76748a --- /dev/null +++ b/lib/services/windows-project-service.ts @@ -0,0 +1,782 @@ +import * as path from "path"; +import * as shell from "shelljs"; +import * as constants from "../constants"; +import { Configurations } from "../common/constants"; +import * as projectServiceBaseLib from "./platform-project-service-base"; +import * as fs from "fs"; +import { + IPlatformData, + IValidBuildOutputData, + IPlatformEnvironmentRequirements, +} from "../definitions/platform"; +import { + IProjectData, + IProjectDataService, + IValidatePlatformOutput, +} from "../definitions/project"; +import { IOptions, IDependencyData } from "../declarations"; +import { IPluginData } from "../definitions/plugins"; +import { + IFileSystem, + IChildProcess, + IRelease, + ISpawnResult, +} from "../common/declarations"; +import { injector } from "../common/yok"; +import { INotConfiguredEnvOptions } from "../common/definitions/commands"; +import { IProjectChangesInfo } from "../definitions/project-changes"; + +export class WindowsProjectService + extends projectServiceBaseLib.PlatformProjectServiceBase +{ + private static WINDOWS_PLATFORM_NAME = "windows"; + + constructor( + $fs: IFileSystem, + $projectDataService: IProjectDataService, + private $options: IOptions, + private $logger: ILogger, + private $childProcess: IChildProcess, + private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, + ) { + super($fs, $projectDataService); + } + + private _platformData: IPlatformData | null = null; + + public getPlatformData(projectData: IProjectData): IPlatformData { + if (!projectData && !this._platformData) { + throw new Error( + "First call of getPlatformData without providing projectData.", + ); + } + + if (projectData && projectData.platformsDir) { + const projectRoot = this.$options.hostProjectPath + ? this.$options.hostProjectPath + : path.join( + projectData.platformsDir, + WindowsProjectService.WINDOWS_PLATFORM_NAME, + ); + + const runtimePackage = this.$projectDataService.getRuntimePackage( + projectData.projectDir, + constants.PlatformTypes.windows, + ); + + this._platformData = { + frameworkPackageName: runtimePackage?.name ?? "@nativescript/windows", + normalizedPlatformName: "Windows", + platformNameLowerCase: "windows", + appDestinationDirectoryPath: path.join( + projectRoot, + projectData.projectName, + ), + platformProjectService: this, + projectRoot: projectRoot, + getBuildOutputPath: (_options?: any): string => { + return path.join(projectRoot, projectData.projectName, "bin"); + }, + getValidBuildOutputData: (): IValidBuildOutputData => { + return { + // AppxManifest.xml triggers Add-AppxPackage -Register (dev flow). + // msix/appx handle release builds. + packageNames: [ + "AppxManifest.xml", + `${projectData.projectName}.msix`, + `${projectData.projectName}.appx`, + ], + }; + }, + configurationFileName: "Package.appxmanifest", + frameworkDirectoriesExtensions: [], + frameworkDirectoriesNames: ["metadata", "NativeScript", "internal"], + targetedOS: ["win32"], + relativeToFrameworkConfigurationFilePath: "app.config", + fastLivesyncFileExtensions: [".jpg", ".jpeg", ".png", ".gif", ".bmp"], + }; + } + + return this._platformData as IPlatformData; + } + + public async validateOptions( + _projectId?: string, + _provision?: true | string, + _teamId?: true | string, + ): Promise { + return true; + } + + public getAppResourcesDestinationDirectoryPath( + projectData: IProjectData, + ): string { + return path.join( + this.getPlatformData(projectData).projectRoot, + projectData.projectName, + "Assets", + ); + } + + public async validate( + projectData: IProjectData, + options: IOptions, + notConfiguredEnvOptions?: INotConfiguredEnvOptions, + ): Promise { + const checkEnvironmentRequirementsOutput = + await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ + platform: this.getPlatformData(projectData).normalizedPlatformName, + projectDir: projectData.projectDir, + options, + notConfiguredEnvOptions, + }); + + return { checkEnvironmentRequirementsOutput }; + } + + public async createProject( + frameworkDir: string, + _frameworkVersion: string, + projectData: IProjectData, + ): Promise { + this.$fs.ensureDirectoryExists( + this.getPlatformData(projectData).projectRoot, + ); + shell.cp( + "-R", + path.join(frameworkDir, "*"), + this.getPlatformData(projectData).projectRoot, + ); + } + + public async interpolateData(projectData: IProjectData): Promise { + const projectRoot = this.getPlatformData(projectData).projectRoot; + const placeholder = "__PROJECT_NAME__"; + const name = projectData.projectName; + const appId = + projectData.projectIdentifiers?.["windows"] ?? projectData.projectId; + + const templateDir = path.join(projectRoot, placeholder); + const projectDir = path.join(projectRoot, name); + + if (this.$fs.exists(templateDir)) { + this.$fs.rename(templateDir, projectDir); + } + + if (!this.$fs.exists(projectDir)) { + return; + } + + const csprojPlaceholder = path.join(projectDir, `${placeholder}.csproj`); + const csprojDest = path.join(projectDir, `${name}.csproj`); + if (this.$fs.exists(csprojPlaceholder)) { + this.$fs.rename(csprojPlaceholder, csprojDest); + } + + this._replaceInFiles(projectDir, placeholder, name); + if (appId) { + this._replaceInFiles(projectDir, "__APP_IDENTIFIER__", appId); + } + } + + private _replaceInFiles(dir: string, from: string, to: string): void { + const textExtensions = new Set([ + ".cs", + ".csproj", + ".xaml", + ".xml", + ".json", + ".appxmanifest", + ]); + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + this._replaceInFiles(fullPath, from, to); + } else if (textExtensions.has(path.extname(entry.name).toLowerCase())) { + const content = fs.readFileSync(fullPath, "utf8"); + if (content.includes(from)) { + fs.writeFileSync(fullPath, content.split(from).join(to), "utf8"); + } + } + } + } + + public interpolateConfigurationFile(projectData: IProjectData): void { + const platformData = this.getPlatformData(projectData); + const manifestPath = path.join( + platformData.projectRoot, + projectData.projectName, + "Package.appxmanifest", + ); + if (!this.$fs.exists(manifestPath)) { + return; + } + + let content = fs.readFileSync(manifestPath, "utf8"); + const appId = + projectData.projectIdentifiers?.["windows"] ?? projectData.projectId; + if (appId) { + content = content.split("__APP_IDENTIFIER__").join(appId); + } + content = content.split("__PROJECT_NAME__").join(projectData.projectName); + this.$fs.writeFile(manifestPath, content); + } + + public afterCreateProject( + _projectRoot: string, + _projectData: IProjectData, + ): void { + // no-op for Windows + } + + public async buildProject( + projectRoot: string, + projectData: IProjectData, + buildConfig: any, + ): Promise { + const config = buildConfig?.release + ? Configurations.Release + : Configurations.Debug; + const arch = buildConfig?.architectures?.[0] ?? "x64"; + const csproj = path.join( + projectRoot, + projectData.projectName, + `${projectData.projectName}.csproj`, + ); + const outputPath = path.join(projectRoot, projectData.projectName, "bin"); + + this.$logger.info( + `Building Windows project: ${csproj} [${config}|${arch}]`, + ); + + const result = await this.$childProcess.spawnFromEvent( + "dotnet", + [ + "build", + csproj, + "-c", + config, + `-p:Platform=${arch}`, + "--output", + outputPath, + ], + "close", + { cwd: path.join(projectRoot, projectData.projectName) }, + { throwError: false }, + ); + + if (result.stdout) { + this.$logger.info(result.stdout); + } + if (result.exitCode !== 0) { + throw new Error( + `dotnet build failed (exit ${result.exitCode}):\n${result.stdout || result.stderr}`, + ); + } + + // Add-AppxPackage -Register requires the manifest to be named AppxManifest.xml + // with $targetnametoken$ expanded to the actual EXE name. + const manifestSrc = path.join( + projectRoot, + projectData.projectName, + "Package.appxmanifest", + ); + const manifestDest = path.join(outputPath, "AppxManifest.xml"); + if (this.$fs.exists(manifestSrc)) { + const raw = fs.readFileSync(manifestSrc, "utf8"); + const expanded = raw.split("$targetnametoken$").join(projectData.projectName); + fs.writeFileSync(manifestDest, expanded, "utf8"); + } + } + + public async prepareProject( + _projectData: IProjectData, + _prepareData: T, + ): Promise { + // Stage native plugin sources into + // platforms/windows//plugins// and generate + // Plugins.props / Plugins.targets that the template csproj imports. + const projectData = _projectData; + const pluginsService: any = injector.resolve("pluginsService"); + const platformData = this.getPlatformData(projectData); + const appProjectDir = path.join( + platformData.projectRoot, + projectData.projectName, + ); + + // Attempt to run dotnet-tool to publish/copy DotNetBridge and app projects if available + try { + const marker = path.join(appProjectDir, "dotnet-bridge", "publish", ".dotnet_tool_done"); + if (fs.existsSync(marker)) { + this.$logger.info("DotNetBridge publish marker found; skipping dotnet-tool"); + } + else { + const arch = process.arch === "arm64" ? "arm64" : "x64"; + const exeCandidates = [ + process.env.DOTNET_TOOL_PATH, + path.join(platformData.projectRoot, "tools", `dotnet-tool-${arch}.exe`), + path.join(platformData.projectRoot, "tools", "dotnet-tool.exe"), + ].filter(Boolean as any); + let exePath: string | null = null; + for (const p of exeCandidates) { + if (p && fs.existsSync(p)) { exePath = p as string; break; } + } + if (exePath) { + this.$logger.info(`Running dotnet-tool: ${exePath}`); + try { + + const result = await this.$childProcess.spawnFromEvent(exePath, ["--app-root", platformData.projectRoot, "--dir", "app", "--force"], "close", { cwd: platformData.projectRoot }, { throwError: false }); + if (result && result.stdout) { this.$logger.info(result.stdout); } + } + catch (err) { + this.$logger.warn(`dotnet-tool execution failed: ${err}`); + } + + // Ensure sentinel exists: if publish/ contains DotNetBridge.dll, write marker so MSBuild waits succeed + try { + const markerPath = path.join(appProjectDir, "dotnet-bridge", "publish", ".dotnet_tool_done"); + if (!fs.existsSync(markerPath)) { + const publishDir = path.join(appProjectDir, "dotnet-bridge", "publish"); + if (fs.existsSync(publishDir)) { + const files = fs.readdirSync(publishDir); + if (files && files.length > 0) { + const bridgeDll = path.join(publishDir, "DotNetBridge.dll"); + if (fs.existsSync(bridgeDll)) { + try { + fs.writeFileSync(markerPath, "done", "utf8"); + this.$logger.info(`Created dotnet-tool marker at ${markerPath}`); + } + catch (werr) { + this.$logger.warn(`Failed creating dotnet-tool marker: ${werr}`); + } + } + else { + this.$logger.info(`[NativeScript] publish directory exists but DotNetBridge.dll missing; files=${files.join(',')}`); + } + } + } + else { + this.$logger.info(`[NativeScript] publish directory not found at ${publishDir}`); + } + } + } + catch (e) { + this.$logger.warn(`dotnet-tool sentinel check failed: ${e}`); + } + } + } + } + catch (err) { + this.$logger.warn(`dotnet-tool check failed: ${err}`); + } + const pluginsDir = path.join(appProjectDir, "plugins"); + + // Ensure plugins directory exists inside the platform app folder (where csproj expects it) + if (!this.$fs.exists(pluginsDir)) { + this.$fs.ensureDirectoryExists(pluginsDir); + } + + // Discover installed plugins and stage their native artifacts + const installedPlugins = + await pluginsService.getAllInstalledPlugins(projectData); + const manifest: any = {}; + const stagedPlugins: Array<{ name: string }> = []; + + for (const pluginData of installedPlugins) { + try { + await this.preparePluginNativeCode(pluginData, projectData); + stagedPlugins.push({ name: pluginData.name }); + const stagedPath = path.join(pluginsDir, pluginData.name); + const collect = (root: string) => { + const out: string[] = []; + if (!this.$fs.exists(root)) return out; + const walk = (dir: string) => { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, e.name); + if (e.isDirectory()) walk(full); + else + out.push(path.relative(root, full).split(path.sep).join("\\")); + } + }; + walk(root); + return out; + }; + const stagedFiles = collect(stagedPath); + manifest[pluginData.name] = { + stagedFiles, + package: pluginData.nativescript || null, + }; + } catch (err) { + this.$logger.warn( + `Failed to stage native files for plugin ${pluginData.name}: ${err}`, + ); + } + } + + // Write aggregate imports that the project csproj imports via "plugins\Plugins.props" + const aggregatePropsPath = path.join(pluginsDir, "Plugins.props"); + const aggregateTargetsPath = path.join(pluginsDir, "Plugins.targets"); + const propsLines: string[] = [ + '', + "", + ]; + const targetsLines: string[] = [ + '', + "", + ]; + for (const p of stagedPlugins) { + const pluginRelDir = p.name.split("/").join("\\"); + const importPathProps = `$(MSBuildThisFileDirectory)${pluginRelDir}\\plugin.props`; + const importPathTargets = `$(MSBuildThisFileDirectory)${pluginRelDir}\\plugin.targets`; + propsLines.push( + ` `, + ); + targetsLines.push( + ` `, + ); + } + propsLines.push(""); + targetsLines.push(""); + this.$fs.writeFile(aggregatePropsPath, propsLines.join("\n")); + this.$fs.writeFile(aggregateTargetsPath, targetsLines.join("\n")); + + // Write installed.json for incremental/uninstall support + const installedJsonPath = path.join(pluginsDir, "installed.json"); + this.$fs.writeJson(installedJsonPath, manifest); + } + + public async checkForChanges( + _changesInfo: IProjectChangesInfo, + _prepareData: IPrepareData, + _projectData: IProjectData, + ): Promise { + // Windows currently has no extra checks. This method exists so the + // ProjectChangesService can call it uniformly for all platforms. + return; + } + + public prepareAppResources(projectData: IProjectData): void { + // Copy app-level Windows resources into the platform project. We preserve + // the original `App_Resources/Windows` layout (so imports like + // App_Resources\Windows\app.csproj work) and additionally stage any + // `Assets` subfolder into the project `Assets` directory (where the + // manifest expects images). + const projectRoot = projectData.projectDir; + const candidates = [ + path.join(projectRoot, "App_Resources", "Windows"), + path.join(projectRoot, "App_Resources", "windows"), + path.join(projectRoot, "app", "App_Resources", "Windows"), + path.join(projectRoot, "app", "App_Resources", "windows"), + ]; + let srcRoot: string | null = null; + for (const c of candidates) { + if (this.$fs.exists(c)) { + srcRoot = c; + break; + } + } + if (!srcRoot) return; + + const platformData = this.getPlatformData(projectData); + const platformAppDir = path.join( + platformData.projectRoot, + projectData.projectName, + ); + + // Copy App_Resources/Windows -> platforms/windows//App_Resources/Windows + const destAppResourcesWindows = path.join( + platformAppDir, + "App_Resources", + "Windows", + ); + this.$fs.ensureDirectoryExists(destAppResourcesWindows); + + const copyRecursive = (srcDir: string, destDir: string) => { + const entries = fs.readdirSync(srcDir, { withFileTypes: true }); + for (const e of entries) { + const srcPath = path.join(srcDir, e.name); + const destPath = path.join(destDir, e.name); + if (e.isDirectory()) { + this.$fs.ensureDirectoryExists(destPath); + copyRecursive(srcPath, destPath); + } else if (e.isFile()) { + this.$fs.copyFile(srcPath, destPath); + } + } + }; + + copyRecursive(srcRoot, destAppResourcesWindows); + + // If the app resources include an Assets folder, stage its contents into the project Assets folder + const srcAssets = path.join(srcRoot, "Assets"); + if (this.$fs.exists(srcAssets)) { + const destAssets = path.join(platformAppDir, "Assets"); + this.$fs.ensureDirectoryExists(destAssets); + copyRecursive(srcAssets, destAssets); + } + + // If there's a Package.appxmanifest in App_Resources/Windows, copy and interpolate it into the project root + const manifestInSrc = path.join(srcRoot, "Package.appxmanifest"); + if (this.$fs.exists(manifestInSrc)) { + const destManifest = path.join(platformAppDir, "Package.appxmanifest"); + this.$fs.copyFile(manifestInSrc, destManifest); + this.interpolateConfigurationFile(projectData); + } + } + + public isPlatformPrepared( + projectRoot: string, + projectData: IProjectData, + ): boolean { + return this.$fs.exists(path.join(projectRoot, projectData.projectName)); + } + + public async preparePluginNativeCode( + pluginData: IPluginData, + projectData: IProjectData, + ): Promise { + // 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", + "windows", + ); + const fallbackFolder = path.join(pluginData.fullPath, "windows"); + const sourcesFolder = this.$fs.exists(platformFolder) + ? platformFolder + : this.$fs.exists(fallbackFolder) + ? fallbackFolder + : null; + if (!sourcesFolder) { + return; + } + + if (!projectData) { + return; + } + + const platformData = this.getPlatformData(projectData); + const appProjectDir = path.join( + platformData.projectRoot, + projectData.projectName, + ); + const pluginStageDir = path.join(appProjectDir, "plugins", pluginData.name); + this.$fs.ensureDirectoryExists(pluginStageDir); + + // recursively copy native files (exclude JS/TS/JSON) + 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); + } else if (e.isFile()) { + const ext = path.extname(e.name).toLowerCase(); + if ( + ext === ".js" || + ext === ".ts" || + ext === ".json" || + ext === ".map" || + ext === ".md" + ) + continue; + this.$fs.ensureDirectoryExists(path.dirname(dest)); + fs.copyFileSync(src, dest); + } + } + }; + + walk(sourcesFolder); + + // copy provided plugin.props/targets if present in plugin root + const providedProps = path.join(pluginData.fullPath, "plugin.props"); + const providedTargets = path.join(pluginData.fullPath, "plugin.targets"); + if (this.$fs.exists(providedProps)) { + this.$fs.copyFile( + providedProps, + path.join(pluginStageDir, "plugin.props"), + ); + } + if (this.$fs.exists(providedTargets)) { + this.$fs.copyFile( + providedTargets, + path.join(pluginStageDir, "plugin.targets"), + ); + } + + const collectStagedFiles = (root: string): string[] => { + const out: string[] = []; + if (!this.$fs.exists(root)) return out; + const walk = (dir: string) => { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, e.name); + if (e.isDirectory()) walk(full); + else out.push(path.relative(root, full).split(path.sep).join("\\")); + } + }; + walk(root); + return out; + }; + + // generate plugin.props if not provided + if (!this.$fs.exists(path.join(pluginStageDir, "plugin.props"))) { + const stagedFiles = collectStagedFiles(pluginStageDir); + const lines: string[] = [ + '', + "", + " ", + ]; + for (const f of stagedFiles) { + const rel = f.split(path.sep).join("\\"); + const link = `plugins\\${pluginData.name}\\${rel}`; + lines.push(` `); + lines.push(` ${link}`); + lines.push( + " PreserveNewest", + ); + lines.push(" "); + } + lines.push(" "); + lines.push(""); + this.$fs.writeFile( + path.join(pluginStageDir, "plugin.props"), + lines.join("\n"), + ); + } + + // generate plugin.targets if not provided — uses an explicit Copy task so + // that DLLs reach bin/ even when EnableMsixTooling=true intercepts Content items. + if (!this.$fs.exists(path.join(pluginStageDir, "plugin.targets"))) { + const stagedFiles = collectStagedFiles(pluginStageDir); + if (stagedFiles.length > 0) { + const safeName = pluginData.name.replace(/[^a-zA-Z0-9]/g, "_"); + const pluginOutDir = `plugins\\${pluginData.name.split("/").join("\\")}`; + const lines: string[] = [ + '', + "", + ` `, + ` `, + ]; + for (const f of stagedFiles) { + const rel = f.split(path.sep).join("\\"); + lines.push( + ` `); + } + lines.push(" "); + lines.push(""); + this.$fs.writeFile( + path.join(pluginStageDir, "plugin.targets"), + lines.join("\n"), + ); + } + } + } + + public async removePluginNativeCode( + _pluginData: IPluginData, + _projectData: IProjectData, + ): Promise { + // no-op for Windows + } + + public async beforePrepareAllPlugins( + _projectData: IProjectData, + dependencies?: IDependencyData[], + ): Promise { + return dependencies ?? []; + } + + public async handleNativeDependenciesChange( + _projectData: IProjectData, + _opts: IRelease, + ): Promise { + // no-op for Windows + } + + public async cleanDeviceTempFolder( + _deviceIdentifier: string, + _projectData: IProjectData, + ): Promise { + // no-op for Windows + } + + public async processConfigurationFilesFromAppResources( + _projectData: IProjectData, + _opts: { release: boolean }, + ): Promise { + // no-op for Windows + } + + public ensureConfigurationFileInAppResources( + projectData: IProjectData, + ): void { + // If the project provides a Package.appxmanifest under App_Resources/Windows, + // copy it into the platform project so the build picks it up. + const projectRoot = projectData.projectDir; + const candidates = [ + path.join( + projectRoot, + "App_Resources", + "Windows", + "Package.appxmanifest", + ), + path.join( + projectRoot, + "App_Resources", + "windows", + "Package.appxmanifest", + ), + path.join( + projectRoot, + "app", + "App_Resources", + "Windows", + "Package.appxmanifest", + ), + path.join( + projectRoot, + "app", + "App_Resources", + "windows", + "Package.appxmanifest", + ), + ]; + let src: string | null = null; + for (const c of candidates) { + if (this.$fs.exists(c)) { + src = c; + break; + } + } + if (!src) return; + + const dest = path.join( + this.getPlatformData(projectData).projectRoot, + projectData.projectName, + "Package.appxmanifest", + ); + this.$fs.copyFile(src, dest); + } + + public async stopServices(_projectRoot: string): Promise { + return { stderr: "", stdout: "", exitCode: 0 }; + } + + public async cleanProject(projectRoot: string): Promise { + const buildDir = path.join(projectRoot, constants.BUILD_DIR); + if (this.$fs.exists(buildDir)) { + this.$fs.deleteDirectory(buildDir); + } + } +} +injector.register("windowsProjectService", WindowsProjectService); diff --git a/package-lock.json b/package-lock.json index a4e4386333..ec15541a99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@foxt/js-srp": "0.0.3-patch2", "@nativescript/doctor": "2.0.17", - "@nativescript/hook": "3.0.4", + "@nativescript/hook": "3.0.5", "@npmcli/arborist": "9.1.8", "@nstudio/trapezedev-project": "7.2.3", "@rigor789/resolve-package-path": "1.0.7", @@ -344,6 +344,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -851,14 +852,10 @@ } }, "node_modules/@nativescript/hook": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@nativescript/hook/-/hook-3.0.4.tgz", - "integrity": "sha512-oahiN7V0D+fgl9o8mjGRgExujTpgSBB0DAFr3eX91qdlJZV8ywJ6mnvtHZyEI2j46yPgAE8jmNIw/Z/d3aWetw==", - "license": "Apache-2.0", - "dependencies": { - "glob": "^11.0.0", - "mkdirp": "^3.0.1" - } + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@nativescript/hook/-/hook-3.0.5.tgz", + "integrity": "sha512-MzL7R/nPZU2qnvDWWuJ8RB7H3luEwANgFX/d/ILdg7bSYxl6uANCfzlueHnWgrQmBxu6dTkvPsFW2WNVUxlhUg==", + "license": "Apache-2.0" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -4487,6 +4484,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -4503,6 +4501,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -4759,6 +4758,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", @@ -6891,6 +6891,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^9.0.0" @@ -9045,6 +9046,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pacote": {