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/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..128c9e0542 --- /dev/null +++ b/lib/common/mobile/windows/windows-application-manager.ts @@ -0,0 +1,189 @@ +import * as fs from "fs"; +import * as path from "path"; +import { ApplicationManagerBase } from "../application-manager-base"; +import { IHooksService, IChildProcess, IDictionary } from "../../declarations"; +import { IBuildData } from "../../../definitions/build"; + +export class WindowsApplicationManager extends ApplicationManagerBase { + private _runningPid: number = null; + private _packageFamilyName: string = null; + + constructor( + $logger: ILogger, + $hooksService: IHooksService, + $deviceLogProvider: Mobile.IDeviceLogProvider, + private $childProcess: IChildProcess, + ) { + 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 []; + } + } + + public async installApplication( + packageFilePath: string, + appIdentifier?: string, + _buildData?: IBuildData, + ): Promise { + this.$logger.info(`[Windows] Installing from: ${packageFilePath}`); + await this.$childProcess.spawnFromEvent( + "powershell.exe", + [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + `Add-AppxPackage -ForceApplicationShutdown -Register -Path "${packageFilePath}"`, + ], + "close", + {}, + { throwError: true }, + ); + // Cache the PFN immediately after install so startApplication can use it. + if (appIdentifier) { + await this._resolvePackageFamilyName(appIdentifier); + } + } + + public async uninstallApplication(appIdentifier: string): Promise { + await this.$childProcess.spawnFromEvent( + "powershell.exe", + [ + "-NoProfile", + "-Command", + `Get-AppxPackage -Name "${appIdentifier}" | Remove-AppxPackage`, + ], + "close", + {}, + { throwError: false }, + ); + this._packageFamilyName = null; + } + + public async startApplication( + appData: Mobile.IStartApplicationData, + ): Promise { + const pfn = await this._resolvePackageFamilyName(appData.appId); + + // Mirror the Android sentinel-file pattern: write ns-debugbreak to the + // app's LocalFolder before launch so the runtime knows to open DevTools. + if (appData.waitForDebugger) { + this._writeDebugBreakMarker(pfn); + } + + this.$logger.info(`[Windows] Launching: ${pfn}`); + const proc = require("child_process").spawn( + "explorer.exe", + [`ms-windows-app://${pfn}`], + { detached: true, stdio: "ignore" }, + ); + proc.unref(); + } + + private async _resolvePackageFamilyName(appId: string): Promise { + if (this._packageFamilyName) return this._packageFamilyName; + try { + const result = await this.$childProcess.spawnFromEvent( + "powershell.exe", + [ + "-NoProfile", + "-Command", + `(Get-AppxPackage | Where-Object { $_.Name -eq "${appId}" -or $_.PackageFullName -like "*${appId}*" } | Select-Object -First 1).PackageFamilyName`, + ], + "close", + {}, + { throwError: false }, + ); + const pfn = (result.stdout || "").trim(); + if (pfn) this._packageFamilyName = pfn; + } catch { + /* ignore, fall back to appId */ + } + return this._packageFamilyName ?? appId; + } + + private _writeDebugBreakMarker(pfn: string): void { + const localAppData = process.env.LOCALAPPDATA; + if (!localAppData) return; + const markerPath = path.join( + localAppData, + "Packages", + pfn, + "LocalState", + "ns-debugbreak", + ); + try { + fs.mkdirSync(path.dirname(markerPath), { recursive: true }); + fs.writeFileSync(markerPath, "", "utf8"); + this.$logger.info(`[Windows] Debug break marker: ${markerPath}`); + } catch (e) { + this.$logger.warn(`[Windows] Could not write debug break marker: ${e}`); + } + } + + public async stopApplication( + appData: Mobile.IApplicationData, + ): Promise { + if (this._runningPid) { + try { + process.kill(this._runningPid); + } catch { + /* already dead */ + } + 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; + } +} 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..4faf053ba6 --- /dev/null +++ b/lib/common/mobile/windows/windows-device-file-system.ts @@ -0,0 +1,96 @@ +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 { + for (const item of localToDevicePaths) { + const dest = item.getDevicePath(); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(item.getLocalPath(), dest); + } + return localToDevicePaths; + } + + public async transferDirectory( + _deviceAppData: Mobile.IDeviceAppData, + localToDevicePaths: Mobile.ILocalToDevicePathData[], + _projectFilesPath: string, + ): Promise { + return this.transferFiles(_deviceAppData, localToDevicePaths); + } + + 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..a1b93d835f --- /dev/null +++ b/lib/common/mobile/windows/windows-device.ts @@ -0,0 +1,47 @@ +import * as os from "os"; +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], + }; + + constructor( + $logger: ILogger, + $hooksService: IHooksService, + $deviceLogProvider: Mobile.IDeviceLogProvider, + $childProcess: IChildProcess, + ) { + this.applicationManager = new WindowsApplicationManager( + $logger, + $hooksService, + $deviceLogProvider, + $childProcess, + ); + this.fileSystem = new WindowsDeviceFileSystem(); + } + + public async openDeviceLogStream(): Promise { + // Windows runtime logs go to stdout/stderr of the process + // DevTools console output is available on ws://localhost:9229 + } +} 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..c564b74b4a 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -482,6 +482,16 @@ export class PrepareController extends EventEmitter { "app", "package.json", ); + } else if ( + this.$mobileHelper.isWindowsPlatform(platformData.platformNameLowerCase) + ) { + // Windows apps place the packaged app under //App + packagePath = path.join( + platformData.projectRoot, + projectData.projectName, + "app", + "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/device-path-provider.ts b/lib/device-path-provider.ts index 6995c41b31..bd0b81c117 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,17 @@ 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 as any).projectData; + if (projectData) { + const platformData = this.$platformsDataService.getPlatformData( + device.deviceInfo.platform, + projectData, + ); + return platformData.appDestinationDirectoryPath; + } } return fromWindowsRelativePathToUnix(projectRoot); 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/livesync/windows-device-livesync-service.ts b/lib/services/livesync/windows-device-livesync-service.ts new file mode 100644 index 0000000000..ec8d14b007 --- /dev/null +++ b/lib/services/livesync/windows-device-livesync-service.ts @@ -0,0 +1,53 @@ +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 { + // TODO: kill the running Windows app process and relaunch it + this.$logger.info( + `[Windows LiveSync] Restart required for ${projectData.projectName}`, + ); + } + + 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 (require("fs").existsSync(devicePath)) { + require("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-data-service.ts b/lib/services/project-data-service.ts index 52f1ff49a5..124513b5f6 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) @@ -654,7 +659,7 @@ export class ProjectDataService implements IProjectDataService { runtimePackage.name, { paths: [projectDir], - } + }, ); if (!runtimePackageJsonPath) { @@ -663,12 +668,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 +688,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 +705,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..1db51fcb7a --- /dev/null +++ b/lib/services/windows-project-service.ts @@ -0,0 +1,654 @@ +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; + + 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, + "App", + ), + platformProjectService: this, + projectRoot: projectRoot, + getBuildOutputPath: (options: any): string => { + const config = options?.release + ? Configurations.Release + : Configurations.Debug; + return path.join(projectRoot, constants.BUILD_DIR, config); + }, + getValidBuildOutputData: (): IValidBuildOutputData => { + return { + packageNames: [ + `${projectData.projectName}.msix`, + `${projectData.projectName}.appx`, + ], + }; + }, + frameworkDirectoriesExtensions: [], + frameworkDirectoriesNames: ["metadata", "NativeScript", "internal"], + targetedOS: ["win32"], + relativeToFrameworkConfigurationFilePath: "app.config", + fastLivesyncFileExtensions: [".jpg", ".jpeg", ".png", ".gif", ".bmp"], + }; + } + + return this._platformData; + } + + 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`, + ); + + this.$logger.info( + `Building Windows project: ${csproj} [${config}|${arch}]`, + ); + + await this.$childProcess.spawnFromEvent( + "msbuild", + [csproj, `/p:Configuration=${config}`, `/p:Platform=${arch}`], + "close", + { cwd: path.join(projectRoot, projectData.projectName) }, + { throwError: true }, + ); + } + + 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, + ); + 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 importPathProps = `plugins\\${p.name}\\plugin.props`; + const importPathTargets = `plugins\\${p.name}\\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, + _options?: any, + ): Promise { + const pluginData = _pluginData; + + // stage native files found under plugin's platforms/windows folder into the app's plugins dir + 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) { + // Nothing to stage for this plugin + return; + } + + const projectData = arguments.length >= 2 ? arguments[1] : null; + if (!projectData) { + // attempt to find a projectData by walking up (best-effort); otherwise skip + return; + } + + 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, out: string) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const e of entries) { + const src = path.join(dir, e.name); + const rel = path.relative(sourcesFolder, src); + const dest = path.join(pluginStageDir, rel); + if (e.isDirectory()) { + this.$fs.ensureDirectoryExists(dest); + walk(src, dest); + } 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, pluginStageDir); + + // 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"), + ); + } + + // generate plugin.props if not provided + if (!this.$fs.exists(path.join(pluginStageDir, "plugin.props"))) { + 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(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"), + ); + } + } + + 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": {