diff --git a/PublicAPI.md b/PublicAPI.md index 3e3a89b629..ce9e13e09c 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -7,6 +7,32 @@ This document describes all methods that can be invoked when NativeScript CLI is const tns = require("nativescript"); ``` +# Contents +* [projectService](#projectservice) + * [createProject](#createproject) + * [isValidNativeScriptProject](#isvalidnativescriptproject) +* [extensibilityService](#extensibilityservice) + * [installExtension](#installextension) + * [uninstallExtension](#uninstallextension) + * [getInstalledExtensions](#getinstalledextensions) + * [loadExtensions](#loadextensions) +* [settingsService](#settingsservice) + * [setSettings](#setsettings) +* [npm](#npm) + * [install](#install) + * [uninstall](#uninstall) + * [search](#search) + * [view](#view) +* [analyticsService](#analyticsservice) + * [startEqatecMonitor](#starteqatecmonitor) +* [debugService](#debugservice) + * [debug](#debug) +* [liveSyncService](#livesyncservice) + * [liveSync](#livesync) + * [stopLiveSync](#stopLiveSync) + * [events](#events) + + ## Module projectService `projectService` modules allow you to create new NativeScript application. @@ -498,6 +524,183 @@ tns.debugService.debug(debugData, debugOptions) .catch(err => console.log(`Unable to start debug operation, reason: ${err.message}.`)); ``` +## liveSyncService +Used to LiveSync changes on devices. The operation can be started for multiple devices and stopped for each of them. During LiveSync operation, the service will emit different events based on the action that's executing. + +### liveSync +Starts a LiveSync operation for specified devices. During the operation, application may have to be rebuilt (for example in case a change in App_Resources is detected). +By default the LiveSync operation will start file system watcher for `/app` directory and any change in it will trigger a LiveSync operation. +After calling the method once, you can add new devices to the same LiveSync operation by calling the method again with the new device identifiers. + +> NOTE: Consecutive calls to `liveSync` method for the same project will execute the initial sync (deploy and fullSync) only for new device identifiers. So in case the first call is for devices with ids [ 'A' , 'B' ] and the second one is for devices with ids [ 'B', 'C' ], the initial sync will be executed only for device with identifier 'C'. + +> NOTE: In case a consecutive call to `liveSync` method requires change in the pattern for watching files (i.e. `liveSyncData.syncAllFiles` option has changed), current watch operation will be stopped and a new one will be started. + +* Definition +```TypeScript +/** + * Starts LiveSync operation by rebuilding the application if necessary and starting watcher. + * @param {ILiveSyncDeviceInfo[]} deviceDescriptors Describes each device for which we would like to sync the application - identifier, outputPath and action to rebuild the app. + * @param {ILiveSyncInfo} liveSyncData Describes the LiveSync operation - for which project directory is the operation and other settings. + * @returns {Promise} + */ +liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise; +``` + +* Usage: +```JavaScript +const projectDir = "myProjectDir"; +const androidDeviceDescriptor = { + identifier: "4df18f307d8a8f1b", + buildAction: () => { + return tns.localBuildService.build("Android", { projectDir, bundle: false, release: false, buildForDevice: true }); + }, + outputPath: null +}; + +const iOSDeviceDescriptor = { + identifier: "12318af23ebc0e25", + buildAction: () => { + return tns.localBuildService.build("iOS", { projectDir, bundle: false, release: false, buildForDevice: true }); + }, + outputPath: null +}; + +const liveSyncData = { + projectDir, + skipWatcher: false, + watchAllFiles: false, + useLiveEdit: false +}; + +tns.liveSyncService.liveSync([ androidDeviceDescriptor, iOSDeviceDescriptor ], liveSyncData) + .then(() => { + console.log("LiveSync operation started."); + }, err => { + console.log("An error occurred during LiveSync", err); + }); +``` + +### stopLiveSync +Stops LiveSync operation. In case deviceIdentifires are passed, the operation will be stopped only for these devices. + +* Definition +```TypeScript +/** + * Stops LiveSync operation for specified directory. + * @param {string} projectDir The directory for which to stop the operation. + * @param {string[]} @optional deviceIdentifiers Device ids for which to stop the application. In case nothing is passed, LiveSync operation will be stopped for all devices. + * @returns {Promise} + */ +stopLiveSync(projectDir: string, deviceIdentifiers?: string[]): Promise; +``` + +* Usage +```JavaScript +const projectDir = "myProjectDir"; +const deviceIdentifiers = [ "4df18f307d8a8f1b", "12318af23ebc0e25" ]; +tns.liveSyncService.stopLiveSync(projectDir, deviceIdentifiers) + .then(() => { + console.log("LiveSync operation stopped."); + }, err => { + console.log("An error occurred during stopage.", err); + }); +``` + +### Events +`liveSyncService` raises several events in order to provide information for current state of the operation. +* liveSyncStarted - raised whenever CLI starts a LiveSync operation for specific device. When `liveSync` method is called, the initial LiveSync operation will emit `liveSyncStarted` for each specified device. After that the event will be emitted only in case when liveSync method is called again with different device instances. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncStarted", data => { + console.log(`Started LiveSync on ${data.deviceIdentifier} for ${data.applicationIdentifier}.`); +}); +``` + +* liveSyncExecuted - raised whenever CLI finishes a LiveSync operation for specific device. When `liveSync` method is called, the initial LiveSync operation will emit `liveSyncExecuted` for each specified device once it finishes the operation. After that the event will be emitted whenever a change is detected (in case file system watcher is started) and the LiveSync operation is executed for each device. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; + /** + * Full paths to files synced during the operation. In case the `syncedFiles.length` is 0, the operation is "fullSync" (i.e. all project files are synced). + */ + syncedFiles: string[]; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncExecuted", data => { + console.log(`Executed LiveSync on ${data.deviceIdentifier} for ${data.applicationIdentifier}. Uploaded files are: ${data.syncedFiles.join(" ")}.`); +}); +``` + +* liveSyncStopped - raised when LiveSync operation is stopped. The event will be raised when the operation is stopped for each device and will be raised when the whole operation is stopped. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + /** + * Passed only when the LiveSync operation is stopped for a specific device. In case it is not passed, the whole LiveSync operation is stopped. + */ + deviceIdentifier?: string; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncStopped", data => { + if (data.deviceIdentifier) { + console.log(`Stopped LiveSync on ${data.deviceIdentifier} for ${data.projectDir}.`); + } else { + console.log(`Stopped LiveSync for ${data.projectDir}.`); + } +}); +``` + +* liveSyncError - raised whenever an error is detected during LiveSync operation. The event is raised for specific device. Once an error is detected, the event will be raised and the LiveSync operation will be stopped for this device, i.e. `liveSyncStopped` event will be raised for it. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; + error: Error; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncError", data => { + console.log(`Error detected during LiveSync on ${data.deviceIdentifier} for ${data.projectDir}. Error: ${data.error.message}.`); +}); +``` + +* notify - raised when LiveSync operation has some data that is important for the user. The event is raised for specific device. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; + notification: string; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("notify", data => { + console.log(`Notification: ${data.notification} for LiveSync operation on ${data.deviceIdentifier} for ${data.projectDir}. `); +}); +``` + ## How to add a new method to Public API CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification. For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`. diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 37ffb4c4ef..0ebcc14815 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -24,6 +24,7 @@ $injector.require("platformsData", "./platforms-data"); $injector.require("platformService", "./services/platform-service"); $injector.require("debugDataService", "./services/debug-data-service"); +$injector.requirePublicClass("debugService", "./services/debug-service"); $injector.require("iOSDebugService", "./services/ios-debug-service"); $injector.require("androidDebugService", "./services/android-debug-service"); @@ -39,6 +40,7 @@ $injector.requireCommand("platform|*list", "./commands/list-platforms"); $injector.requireCommand("platform|add", "./commands/add-platform"); $injector.requireCommand("platform|remove", "./commands/remove-platform"); $injector.requireCommand("platform|update", "./commands/update-platform"); +$injector.requireCommand("run|*all", "./commands/run"); $injector.requireCommand("run|ios", "./commands/run"); $injector.requireCommand("run|android", "./commands/run"); @@ -74,7 +76,6 @@ $injector.require("commandsServiceProvider", "./providers/commands-service-provi $injector.require("deviceAppDataProvider", "./providers/device-app-data-provider"); $injector.require("deviceLogProvider", "./common/mobile/device-log-provider"); -$injector.require("liveSyncProvider", "./providers/livesync-provider"); $injector.require("projectFilesProvider", "./providers/project-files-provider"); $injector.require("nodeModulesBuilder", "./tools/node-modules/node-modules-builder"); @@ -99,14 +100,15 @@ $injector.require("infoService", "./services/info-service"); $injector.requireCommand("info", "./commands/info"); $injector.require("androidToolsInfo", "./android-tools-info"); +$injector.require("devicePathProvider", "./device-path-provider"); $injector.requireCommand("platform|clean", "./commands/platform-clean"); +$injector.requirePublicClass("liveSyncService", "./services/livesync/livesync-service"); +$injector.require("debugLiveSyncService", "./services/livesync/debug-livesync-service"); +$injector.require("androidLiveSyncService", "./services/livesync/android-livesync-service"); +$injector.require("iOSLiveSyncService", "./services/livesync/ios-livesync-service"); $injector.require("usbLiveSyncService", "./services/livesync/livesync-service"); // The name is used in https://github.com/NativeScript/nativescript-dev-typescript -$injector.require("iosLiveSyncServiceLocator", "./services/livesync/ios-device-livesync-service"); -$injector.require("androidLiveSyncServiceLocator", "./services/livesync/android-device-livesync-service"); -$injector.require("platformLiveSyncService", "./services/livesync/platform-livesync-service"); - $injector.require("sysInfo", "./sys-info"); $injector.require("iOSNotificationService", "./services/ios-notification-service"); diff --git a/lib/commands/appstore-list.ts b/lib/commands/appstore-list.ts index 2ba7b8a435..379508f538 100644 --- a/lib/commands/appstore-list.ts +++ b/lib/commands/appstore-list.ts @@ -7,9 +7,19 @@ export class ListiOSApps implements ICommand { constructor(private $injector: IInjector, private $itmsTransporterService: IITMSTransporterService, private $logger: ILogger, - private $prompter: IPrompter) { } + private $projectData: IProjectData, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $platformService: IPlatformService, + private $errors: IErrors, + private $prompter: IPrompter) { + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); + } + let username = args[0], password = args[1]; diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index 7576664371..dfca00ff9c 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -7,7 +7,6 @@ export class PublishIOS implements ICommand { new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)]; constructor(private $errors: IErrors, - private $hostInfo: IHostInfo, private $injector: IInjector, private $itmsTransporterService: IITMSTransporterService, private $logger: ILogger, @@ -100,8 +99,8 @@ export class PublishIOS implements ICommand { } public async canExecute(args: string[]): Promise { - if (!this.$hostInfo.isDarwin) { - this.$errors.failWithoutHelp("This command is only available on Mac OS X."); + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } return true; diff --git a/lib/commands/build.ts b/lib/commands/build.ts index 65be9a7ac4..810baa4334 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -1,7 +1,9 @@ export class BuildCommandBase { constructor(protected $options: IOptions, + protected $errors: IErrors, protected $projectData: IProjectData, protected $platformsData: IPlatformsData, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, protected $platformService: IPlatformService) { this.$projectData.initializeProjectData(); } @@ -29,16 +31,24 @@ export class BuildCommandBase { this.$platformService.copyLastOutput(platform, this.$options.copyTo, buildConfig, this.$projectData); } } + + protected validatePlatform(platform: string): void { + if (!this.$platformService.isPlatformSupportedForOS(platform, this.$projectData)) { + this.$errors.fail(`Applications for platform ${platform} can not be built on this OS`); + } + } } export class BuildIosCommand extends BuildCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; constructor(protected $options: IOptions, + $errors: IErrors, $projectData: IProjectData, $platformsData: IPlatformsData, + $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $platformService: IPlatformService) { - super($options, $projectData, $platformsData, $platformService); + super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService); } public async execute(args: string[]): Promise { @@ -46,6 +56,7 @@ export class BuildIosCommand extends BuildCommandBase implements ICommand { } public canExecute(args: string[]): Promise { + super.validatePlatform(this.$devicePlatformsConstants.iOS); return args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } @@ -56,11 +67,12 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; constructor(protected $options: IOptions, + protected $errors: IErrors, $projectData: IProjectData, $platformsData: IPlatformsData, - private $errors: IErrors, + $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $platformService: IPlatformService) { - super($options, $projectData, $platformsData, $platformService); + super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService); } public async execute(args: string[]): Promise { @@ -68,6 +80,7 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { } public async canExecute(args: string[]): Promise { + super.validatePlatform(this.$devicePlatformsConstants.Android); if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } diff --git a/lib/commands/clean-app.ts b/lib/commands/clean-app.ts index ed79c44977..2d760fa04d 100644 --- a/lib/commands/clean-app.ts +++ b/lib/commands/clean-app.ts @@ -1,7 +1,7 @@ export class CleanAppCommandBase { constructor(protected $options: IOptions, protected $projectData: IProjectData, - private $platformService: IPlatformService) { + protected $platformService: IPlatformService) { this.$projectData.initializeProjectData(); } @@ -14,7 +14,9 @@ export class CleanAppCommandBase { export class CleanAppIosCommand extends CleanAppCommandBase implements ICommand { constructor(protected $options: IOptions, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $platformsData: IPlatformsData, + private $errors: IErrors, $platformService: IPlatformService, $projectData: IProjectData) { super($options, $projectData, $platformService); @@ -23,6 +25,9 @@ export class CleanAppIosCommand extends CleanAppCommandBase implements ICommand public allowedParameters: ICommandParameter[] = []; public async execute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); + } return super.execute([this.$platformsData.availablePlatforms.iOS]); } } @@ -33,13 +38,18 @@ export class CleanAppAndroidCommand extends CleanAppCommandBase implements IComm public allowedParameters: ICommandParameter[] = []; constructor(protected $options: IOptions, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $platformsData: IPlatformsData, + private $errors: IErrors, $platformService: IPlatformService, $projectData: IProjectData) { super($options, $projectData, $platformService); } public async execute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); + } return super.execute([this.$platformsData.availablePlatforms.Android]); } } diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 7165a38905..d8ba7a9447 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -1,66 +1,85 @@ -import { EOL } from "os"; - -export abstract class DebugPlatformCommand implements ICommand { +export abstract class DebugPlatformCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; + public platform: string; constructor(private debugService: IPlatformDebugService, private $devicesService: Mobile.IDevicesService, - private $injector: IInjector, - private $config: IConfiguration, - private $usbLiveSyncService: ILiveSyncService, private $debugDataService: IDebugDataService, protected $platformService: IPlatformService, protected $projectData: IProjectData, protected $options: IOptions, protected $platformsData: IPlatformsData, - protected $logger: ILogger) { + protected $logger: ILogger, + private $debugLiveSyncService: IDebugLiveSyncService, + private $config: IConfiguration) { this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { const debugOptions = this.$options; - const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, - emulator: this.$options.emulator, - platformTemplate: this.$options.platformTemplate, - projectDir: this.$options.path, - release: this.$options.release, - provision: this.$options.provision, - teamId: this.$options.teamId - }; let debugData = this.$debugDataService.createDebugData(this.$projectData, this.$options); await this.$platformService.trackProjectType(this.$projectData); if (this.$options.start) { - return this.printDebugInformation(await this.debugService.debug(debugData, debugOptions)); + return this.$debugLiveSyncService.printDebugInformation(await this.debugService.debug(debugData, debugOptions)); } - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - - await this.$platformService.deployPlatform(this.$devicesService.platform, appFilesUpdaterOptions, deployOptions, this.$projectData, this.$options); this.$config.debugLivesync = true; - let applicationReloadAction = async (deviceAppData: Mobile.IDeviceAppData): Promise => { - let projectData: IProjectData = this.$injector.resolve("projectData"); - - await this.debugService.debugStop(); - let applicationId = deviceAppData.appIdentifier; - await deviceAppData.device.applicationManager.stopApplication(applicationId, projectData.projectName); + await this.$devicesService.initialize({ - const buildConfig: IBuildConfig = _.merge({ buildForDevice: !deviceAppData.device.isEmulator }, deployOptions); - debugData.pathToAppPackage = this.$platformService.lastOutputPath(this.debugService.platform, buildConfig, projectData); - - this.printDebugInformation(await this.debugService.debug(debugData, debugOptions)); + }); + await this.$devicesService.detectCurrentlyAttachedDevices(); + + const devices = this.$devicesService.getDeviceInstances(); + // Now let's take data for each device: + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !this.platform || d.deviceInfo.platform === this.platform) + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, this.$projectData); + const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, this.$projectData); + return pathToBuildResult; + } + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { + projectDir: this.$projectData.projectDir, + skipWatcher: !this.$options.watch || this.$options.justlaunch, + watchAllFiles: this.$options.syncAllFiles }; - return this.$usbLiveSyncService.liveSync(this.$devicesService.platform, this.$projectData, applicationReloadAction); + await this.$debugLiveSyncService.liveSync(deviceDescriptors, liveSyncInfo); } public async canExecute(args: string[]): Promise { - await this.$devicesService.initialize({ platform: this.debugService.platform, deviceId: this.$options.device }); + await this.$devicesService.initialize({ + platform: this.platform, + deviceId: this.$options.device, + emulator: this.$options.emulator, + skipDeviceDetectionInterval: true, + skipInferPlatform: true + }); // Start emulator if --emulator is selected or no devices found. if (this.$options.emulator || this.$devicesService.deviceCount === 0) { return true; @@ -75,29 +94,23 @@ export abstract class DebugPlatformCommand implements ICommand { return true; } - - protected printDebugInformation(information: string[]): void { - _.each(information, i => { - this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${i}${EOL}`.cyan); - }); - } } export class DebugIOSCommand extends DebugPlatformCommand { - constructor(protected $logger: ILogger, + constructor(private $errors: IErrors, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $logger: ILogger, $iOSDebugService: IPlatformDebugService, $devicesService: Mobile.IDevicesService, - $injector: IInjector, - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $config: IConfiguration, - $usbLiveSyncService: ILiveSyncService, $debugDataService: IDebugDataService, $platformService: IPlatformService, $options: IOptions, $projectData: IProjectData, $platformsData: IPlatformsData, - $iosDeviceOperations: IIOSDeviceOperations) { - super($iOSDebugService, $devicesService, $injector, $config, $usbLiveSyncService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger); + $iosDeviceOperations: IIOSDeviceOperations, + $debugLiveSyncService: IDebugLiveSyncService) { + super($iOSDebugService, $devicesService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger, $debugLiveSyncService, $config); // Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket. // In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket. // That's why the `$ tns debug ios --justlaunch` command will not release the terminal. @@ -106,37 +119,43 @@ export class DebugIOSCommand extends DebugPlatformCommand { } public async canExecute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); + } + return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); } - protected printDebugInformation(information: string[]): void { - if (this.$options.chrome) { - super.printDebugInformation(information); - } - } + public platform = this.$devicePlatformsConstants.iOS; } $injector.registerCommand("debug|ios", DebugIOSCommand); export class DebugAndroidCommand extends DebugPlatformCommand { - constructor($logger: ILogger, + constructor(private $errors: IErrors, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $logger: ILogger, $androidDebugService: IPlatformDebugService, $devicesService: Mobile.IDevicesService, - $injector: IInjector, - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $config: IConfiguration, - $usbLiveSyncService: ILiveSyncService, $debugDataService: IDebugDataService, $platformService: IPlatformService, $options: IOptions, $projectData: IProjectData, - $platformsData: IPlatformsData) { - super($androidDebugService, $devicesService, $injector, $config, $usbLiveSyncService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger); + $platformsData: IPlatformsData, + $debugLiveSyncService: IDebugLiveSyncService) { + super($androidDebugService, $devicesService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger, $debugLiveSyncService, $config); } public async canExecute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.Android, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS`); + } + return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); } + + public platform = this.$devicePlatformsConstants.Android; } $injector.registerCommand("debug|android", DebugAndroidCommand); diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 3da1b60f6b..3540b92e53 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -1,69 +1,147 @@ -export class RunCommandBase { +import { ERROR_NO_VALID_SUBCOMMAND_FORMAT } from "../common/constants"; + +export class RunCommandBase implements ICommand { + protected platform: string; + constructor(protected $platformService: IPlatformService, - protected $usbLiveSyncService: ILiveSyncService, + protected $liveSyncService: ILiveSyncService, protected $projectData: IProjectData, protected $options: IOptions, - protected $emulatorPlatformService: IEmulatorPlatformService) { - this.$projectData.initializeProjectData(); + protected $emulatorPlatformService: IEmulatorPlatformService, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $errors: IErrors, + private $devicesService: Mobile.IDevicesService, + private $hostInfo: IHostInfo, + private $iosDeviceOperations: IIOSDeviceOperations, + private $mobileHelper: Mobile.IMobileHelper) { } - public async executeCore(args: string[]): Promise { + public allowedParameters: ICommandParameter[] = []; + public async execute(args: string[]): Promise { + return this.executeCore(args); + } - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, - emulator: this.$options.emulator, - projectDir: this.$options.path, - platformTemplate: this.$options.platformTemplate, - release: this.$options.release, - provision: this.$options.provision, - teamId: this.$options.teamId, - keyStoreAlias: this.$options.keyStoreAlias, - keyStoreAliasPassword: this.$options.keyStoreAliasPassword, - keyStorePassword: this.$options.keyStorePassword, - keyStorePath: this.$options.keyStorePath - }; + public async canExecute(args: string[]): Promise { + if (args.length) { + this.$errors.fail(ERROR_NO_VALID_SUBCOMMAND_FORMAT, "run"); + } - await this.$platformService.deployPlatform(args[0], appFilesUpdaterOptions, deployOptions, this.$projectData, this.$options); + this.$projectData.initializeProjectData(); + if (!this.platform && !this.$hostInfo.isDarwin) { + this.platform = this.$devicePlatformsConstants.Android; + } + + return true; + } + public async executeCore(args: string[]): Promise { if (this.$options.bundle) { this.$options.watch = false; } + await this.$devicesService.initialize({ + deviceId: this.$options.device, + platform: this.platform, + emulator: this.$options.emulator, + skipDeviceDetectionInterval: true, + skipInferPlatform: !this.platform + }); + await this.$devicesService.detectCurrentlyAttachedDevices(); + + const devices = this.$devicesService.getDeviceInstances(); + // Now let's take data for each device: + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !this.platform || d.deviceInfo.platform === this.platform) + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, this.$projectData); + const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, this.$projectData); + return pathToBuildResult; + } + }; + + return info; + }); + + const workingWithiOSDevices = !this.platform || this.$mobileHelper.isiOSPlatform(this.platform); + const shouldKeepProcessAlive = this.$options.watch || !this.$options.justlaunch; + if (workingWithiOSDevices && shouldKeepProcessAlive) { + this.$iosDeviceOperations.setShouldDispose(false); + } + if (this.$options.release) { - const deployOpts: IRunPlatformOptions = { + const runPlatformOptions: IRunPlatformOptions = { device: this.$options.device, emulator: this.$options.emulator, justlaunch: this.$options.justlaunch, }; - await this.$platformService.startApplication(args[0], deployOpts, this.$projectData.projectId); + const deployOptions = _.merge({ projectDir: this.$projectData.projectDir, clean: true }, this.$options); + + await this.$platformService.deployPlatform(args[0], this.$options, deployOptions, this.$projectData, this.$options); + await this.$platformService.startApplication(args[0], runPlatformOptions, this.$projectData.projectId); return this.$platformService.trackProjectType(this.$projectData); } - return this.$usbLiveSyncService.liveSync(args[0], this.$projectData); + const liveSyncInfo: ILiveSyncInfo = { + projectDir: this.$projectData.projectDir, + skipWatcher: !this.$options.watch, + watchAllFiles: this.$options.syncAllFiles, + clean: this.$options.clean + }; + + await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); } } +$injector.registerCommand("run|*all", RunCommandBase); + export class RunIosCommand extends RunCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; + public get platform(): string { + return this.$devicePlatformsConstants.iOS; + } constructor($platformService: IPlatformService, private $platformsData: IPlatformsData, - $usbLiveSyncService: ILiveSyncService, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $errors: IErrors, + $liveSyncService: ILiveSyncService, $projectData: IProjectData, $options: IOptions, - $emulatorPlatformService: IEmulatorPlatformService) { - super($platformService, $usbLiveSyncService, $projectData, $options, $emulatorPlatformService); + $emulatorPlatformService: IEmulatorPlatformService, + $devicesService: Mobile.IDevicesService, + $hostInfo: IHostInfo, + $iosDeviceOperations: IIOSDeviceOperations, + $mobileHelper: Mobile.IMobileHelper) { + super($platformService, $liveSyncService, $projectData, $options, $emulatorPlatformService, $devicePlatformsConstants, $errors, $devicesService, $hostInfo, $iosDeviceOperations, $mobileHelper); } public async execute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); + } + return this.executeCore([this.$platformsData.availablePlatforms.iOS]); } public async canExecute(args: string[]): Promise { - return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } @@ -71,15 +149,23 @@ $injector.registerCommand("run|ios", RunIosCommand); export class RunAndroidCommand extends RunCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; + public get platform(): string { + return this.$devicePlatformsConstants.Android; + } constructor($platformService: IPlatformService, private $platformsData: IPlatformsData, - $usbLiveSyncService: ILiveSyncService, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $errors: IErrors, + $liveSyncService: ILiveSyncService, $projectData: IProjectData, $options: IOptions, $emulatorPlatformService: IEmulatorPlatformService, - private $errors: IErrors) { - super($platformService, $usbLiveSyncService, $projectData, $options, $emulatorPlatformService); + $devicesService: Mobile.IDevicesService, + $hostInfo: IHostInfo, + $iosDeviceOperations: IIOSDeviceOperations, + $mobileHelper: Mobile.IMobileHelper) { + super($platformService, $liveSyncService, $projectData, $options, $emulatorPlatformService, $devicePlatformsConstants, $errors, $devicesService, $hostInfo, $iosDeviceOperations, $mobileHelper); } public async execute(args: string[]): Promise { @@ -87,10 +173,15 @@ export class RunAndroidCommand extends RunCommandBase implements ICommand { } public async canExecute(args: string[]): Promise { + await super.canExecute(args); + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.Android, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS`); + } + if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } - return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + return this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); } } diff --git a/lib/common b/lib/common index 1829df1b87..cf3276e6bd 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 1829df1b871af9da7bba6dd1225480c67ed12698 +Subproject commit cf3276e6bd5fdd66070a88cd70bc40d29fc65a5a diff --git a/lib/constants.ts b/lib/constants.ts index 4345525a4d..984accc309 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -23,6 +23,13 @@ export class PackageVersion { static LATEST = "latest"; } +const liveSyncOperation = "LiveSync Operation"; +export class LiveSyncTrackActionNames { + static LIVESYNC_OPERATION = liveSyncOperation; + static LIVESYNC_OPERATION_BUILD = `${liveSyncOperation} - Build`; + static DEVICE_INFO = `Device Info for ${liveSyncOperation}`; +} + export const PackageJsonKeysToKeep: Array = ["name", "main", "android", "version"]; export class SaveOptions { @@ -66,7 +73,13 @@ class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationTyp } export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass(); - +export class LiveSyncPaths { + static SYNC_DIR_NAME = "sync"; + static REMOVEDSYNC_DIR_NAME = "removedsync"; + static FULLSYNC_DIR_NAME = "fullsync"; + static IOS_DEVICE_PROJECT_ROOT_PATH = "Library/Application Support/LiveSync"; + static IOS_DEVICE_SYNC_ZIP_PATH = "Library/Application Support/LiveSync/sync.zip"; +}; export const ANGULAR_NAME = "angular"; export const TYPESCRIPT_NAME = "typescript"; export const BUILD_OUTPUT_EVENT_NAME = "buildOutput"; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 448c546933..2d9637d780 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -257,37 +257,6 @@ interface IOpener { open(target: string, appname: string): void; } -interface ILiveSyncService { - liveSync(platform: string, projectData: IProjectData, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise; -} - -interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase { - /** - * Refreshes the application's content on a device - * @param {Mobile.IDeviceAppData} deviceAppData Information about the application and the device. - * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. - * @param {boolean} forceExecuteFullSync If this is passed a full LiveSync is performed instead of an incremental one. - * @param {IProjectData} projectData Project data. - * @return {Promise} - */ - refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], forceExecuteFullSync: boolean, projectData: IProjectData): Promise; - /** - * Removes specified files from a connected device - * @param {string} appIdentifier Application identifier. - * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. - * @param {string} projectId Project identifier - for example org.nativescript.livesync. - * @return {Promise} - */ - removeFiles(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise; - afterInstallApplicationAction?(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise; -} - -interface IPlatformLiveSyncService { - fullSync(projectData: IProjectData, postAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise): Promise; - partialSync(event: string, filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise; - refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], isFullSync: boolean, projectData: IProjectData): Promise; -} - interface IBundle { bundle: boolean; } @@ -516,11 +485,6 @@ interface IiOSNotification { getAttachAvailable(projectId: string): string; } -interface IiOSNotificationService { - awaitNotification(deviceIdentifier: string, socket: number, timeout: number): Promise; - postNotification(deviceIdentifier: string, notification: string, commandType?: string): Promise; -} - interface IiOSSocketRequestExecutor { executeLaunchRequest(deviceIdentifier: string, timeout: number, readyForAttachTimeout: number, projectId: string, shouldBreak?: boolean): Promise; executeAttachRequest(device: Mobile.IiOSDevice, timeout: number, projectId: string): Promise; diff --git a/lib/definitions/debug.d.ts b/lib/definitions/debug.d.ts index 3a1c62a37b..916fd9e391 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -89,7 +89,7 @@ interface IDebugDataService { /** * Describes methods for debug operation. */ -interface IDebugService extends NodeJS.EventEmitter { +interface IDebugServiceBase extends NodeJS.EventEmitter { /** * Starts debug operation based on the specified debug data. * @param {IDebugData} debugData Describes information for device and application that will be debugged. @@ -99,10 +99,14 @@ interface IDebugService extends NodeJS.EventEmitter { debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; } +interface IDebugService { + getDebugService(device: Mobile.IDevice): IPlatformDebugService; +} + /** * Describes actions required for debugging on specific platform (Android or iOS). */ -interface IPlatformDebugService extends IDebugService { +interface IPlatformDebugService extends IDebugServiceBase { /** * Starts debug operation. * @param {IDebugData} debugData Describes information for device and application that will be debugged. diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts new file mode 100644 index 0000000000..e936c86d56 --- /dev/null +++ b/lib/definitions/livesync.d.ts @@ -0,0 +1,250 @@ +// This interface is a mashup of NodeJS' along with Chokidar's event watchers +interface IFSWatcher extends NodeJS.EventEmitter { + // from fs.FSWatcher + close(): void; + + /** + * events.EventEmitter + * 1. change + * 2. error + */ + addListener(event: string, listener: Function): this; + addListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + addListener(event: "error", listener: (code: number, signal: string) => void): this; + + on(event: string, listener: Function): this; + on(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + on(event: "error", listener: (code: number, signal: string) => void): this; + + once(event: string, listener: Function): this; + once(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + once(event: "error", listener: (code: number, signal: string) => void): this; + + prependListener(event: string, listener: Function): this; + prependListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + prependListener(event: "error", listener: (code: number, signal: string) => void): this; + + prependOnceListener(event: string, listener: Function): this; + prependOnceListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + prependOnceListener(event: "error", listener: (code: number, signal: string) => void): this; + + // From chokidar FSWatcher + + /** + * Add files, directories, or glob patterns for tracking. Takes an array of strings or just one + * string. + */ + add(paths: string | string[]): void; + + /** + * Stop watching files, directories, or glob patterns. Takes an array of strings or just one + * string. + */ + unwatch(paths: string | string[]): void; + + /** + * Returns an object representing all the paths on the file system being watched by this + * `FSWatcher` instance. The object's keys are all the directories (using absolute paths unless + * the `cwd` option was used), and the values are arrays of the names of the items contained in + * each directory. + */ + getWatched(): IDictionary; + + /** + * Removes all listeners from watched files. + */ + close(): void; +} + +interface ILiveSyncProcessInfo { + timer: NodeJS.Timer; + watcherInfo: { + watcher: IFSWatcher, + pattern: string | string[] + }; + actionsChain: Promise; + isStopped: boolean; + deviceDescriptors: ILiveSyncDeviceInfo[]; +} + +/** + * Describes information for LiveSync on a device. + */ +interface ILiveSyncDeviceInfo { + /** + * Device identifier. + */ + identifier: string; + + /** + * Action that will rebuild the application. The action must return a Promise, which is resolved with at path to build artifact. + * @returns {Promise} Path to build artifact (.ipa, .apk or .zip). + */ + buildAction: () => Promise; + + /** + * Path where the build result is located (directory containing .ipa, .apk or .zip). + * This is required for initial checks where LiveSync will skip the rebuild in case there's already a build result and no change requiring rebuild is made since then. + * In case it is not passed, the default output for local builds will be used. + */ + outputPath?: string; +} + +/** + * Describes a LiveSync operation. + */ +interface ILiveSyncInfo { + /** + * Directory of the project that will be synced. + */ + projectDir: string; + + /** + * Defines if the watcher should be skipped. If not passed, fs.Watcher will be started. + */ + skipWatcher?: boolean; + + /** + * Defines if all project files should be watched for changes. In case it is not passed, only `app` dir of the project will be watched for changes. + * In case it is set to true, the package.json of the project and node_modules directory will also be watched, so any change there will be transferred to device(s). + */ + watchAllFiles?: boolean; + + /** + * Defines if the liveEdit functionality should be used, i.e. LiveSync of .js files without restart. + * NOTE: Currently this is available only for iOS. + */ + useLiveEdit?: boolean; + + /** + * Forces a build before the initial livesync. + */ + clean?: boolean; +} + +interface ILatestAppPackageInstalledSettings extends IDictionary> { /* empty */ } + +interface ILiveSyncBuildInfo { + platform: string; + isEmulator: boolean; + pathToBuildItem: string; +} + +/** + * Desribes object that can be passed to ensureLatestAppPackageIsInstalledOnDevice method. + */ +interface IEnsureLatestAppPackageIsInstalledOnDeviceOptions { + device: Mobile.IDevice; + preparedPlatforms: string[]; + rebuiltInformation: ILiveSyncBuildInfo[]; + projectData: IProjectData; + deviceBuildInfoDescriptor: ILiveSyncDeviceInfo; + settings: ILatestAppPackageInstalledSettings; + liveSyncData?: ILiveSyncInfo; + modifiedFiles?: string[]; +} + +/** + * Describes LiveSync operations. + */ +interface ILiveSyncService { + /** + * Starts LiveSync operation by rebuilding the application if necessary and starting watcher. + * @param {ILiveSyncDeviceInfo[]} deviceDescriptors Describes each device for which we would like to sync the application - identifier, outputPath and action to rebuild the app. + * @param {ILiveSyncInfo} liveSyncData Describes the LiveSync operation - for which project directory is the operation and other settings. + * @returns {Promise} + */ + liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise; + + /** + * Stops LiveSync operation for specified directory. + * @param {string} projectDir The directory for which to stop the operation. + * @param {string[]} @optional deviceIdentifiers Device ids for which to stop the application. In case nothing is passed, LiveSync operation will be stopped for all devices. + * @returns {Promise} + */ + stopLiveSync(projectDir: string, deviceIdentifiers?: string[]): Promise; +} + +/** + * Describes LiveSync operations while debuggging. + */ +interface IDebugLiveSyncService extends ILiveSyncService { + /** + * Prints debug information. + * @param {string[]} information Array of information to be printed. Note that false-like values will be stripped from the array. + * @returns {void} + */ + printDebugInformation(information: string[]): void; +} + +interface ILiveSyncWatchInfo { + projectData: IProjectData; + filesToRemove: string[]; + filesToSync: string[]; + isRebuilt: boolean; + syncAllFiles: boolean; + useLiveEdit?: boolean; +} + +interface ILiveSyncResultInfo { + modifiedFilesData: Mobile.ILocalToDevicePathData[]; + isFullSync: boolean; + deviceAppData: Mobile.IDeviceAppData; + useLiveEdit?: boolean; +} + +interface IFullSyncInfo { + projectData: IProjectData; + device: Mobile.IDevice; + watch: boolean; + syncAllFiles: boolean; + useLiveEdit?: boolean; +} + +interface IPlatformLiveSyncService { + fullSync(syncInfo: IFullSyncInfo): Promise; + liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise; + refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise; +} + +interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase { + /** + * Refreshes the application's content on a device + * @param {Mobile.IDeviceAppData} deviceAppData Information about the application and the device. + * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. + * @param {boolean} forceExecuteFullSync If this is passed a full LiveSync is performed instead of an incremental one. + * @param {IProjectData} projectData Project data. + * @return {Promise} + */ + refreshApplication(projectData: IProjectData, + liveSyncInfo: ILiveSyncResultInfo): Promise; + + /** + * Removes specified files from a connected device + * @param {Mobile.IDeviceAppData} deviceAppData Data about device and app. + * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. + * @return {Promise} + */ + removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise; +} + +interface IAndroidNativeScriptDeviceLiveSyncService { + /** + * Retrieves the android device's hash service. + * @param {string} appIdentifier Application identifier. + * @return {Promise} The hash service + */ + getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService; +} + +interface IDeviceProjectRootOptions { + appIdentifier: string; + getDirname?: boolean; + syncAllFiles?: boolean; + watch?: boolean; +} + +interface IDevicePathProvider { + getDeviceProjectRootPath(device: Mobile.IDevice, options: IDeviceProjectRootOptions): Promise; + getDeviceSyncZipPath(device: Mobile.IDevice): string; +} diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index f1c6dd968d..d0d2b914f7 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -55,10 +55,11 @@ interface IPlatformService extends NodeJS.EventEmitter { * - the .nsbuildinfo file in product folder points to an old prepare. * @param {string} platform The platform to build. * @param {IProjectData} projectData DTO with information about the project. - * @param {IBuildConfig} buildConfig Indicates whether the build is for device or emulator. + * @param {IBuildConfig} @optional buildConfig Indicates whether the build is for device or emulator. + * @param {string} @optional outputPath Directory containing build information and artifacts. * @returns {boolean} true indicates that the platform should be build. */ - shouldBuild(platform: string, projectData: IProjectData, buildConfig?: IBuildConfig): Promise; + shouldBuild(platform: string, projectData: IProjectData, buildConfig?: IBuildConfig, outputPath?: string): Promise; /** * Builds the native project for the specified platform for device or emulator. @@ -77,9 +78,10 @@ interface IPlatformService extends NodeJS.EventEmitter { * - the .nsbuildinfo file located in application root folder is different than the local .nsbuildinfo file * @param {Mobile.IDevice} device The device where the application should be installed. * @param {IProjectData} projectData DTO with information about the project. + * @param {string} @optional outputPath Directory containing build information and artifacts. * @returns {Promise} true indicates that the application should be installed. */ - shouldInstall(device: Mobile.IDevice, projectData: IProjectData): Promise; + shouldInstall(device: Mobile.IDevice, projectData: IProjectData, outputPath?: string): Promise; /** * Installs the application on specified device. @@ -87,10 +89,12 @@ interface IPlatformService extends NodeJS.EventEmitter { * * .nsbuildinfo is not persisted when building for release. * @param {Mobile.IDevice} device The device where the application should be installed. * @param {IRelease} options Whether the application was built in release configuration. + * @param {string} @optional pathToBuiltApp Path to build artifact. + * @param {string} @optional outputPath Directory containing build information and artifacts. * @param {IProjectData} projectData DTO with information about the project. * @returns {void} */ - installApplication(device: Mobile.IDevice, options: IRelease, projectData: IProjectData): Promise; + installApplication(device: Mobile.IDevice, options: IRelease, projectData: IProjectData, pathToBuiltApp?: string, outputPath?: string): Promise; /** * Gets first chance to validate the options provided as command line arguments. @@ -125,25 +129,34 @@ interface IPlatformService extends NodeJS.EventEmitter { /** * Ensures the passed platform is a valid one (from the supported ones) - * and that it can be built on the current OS */ validatePlatform(platform: string, projectData: IProjectData): void; + /** + * Checks whether passed platform can be built on the current OS + * @param {string} platform The mobile platform. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {boolean} Whether the platform is supported for current OS or not. + */ + isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean; + /** * Returns information about the latest built application for device in the current project. * @param {IPlatformData} platformData Data describing the current platform. * @param {IBuildConfig} buildConfig Defines if the build is for release configuration. + * @param {string} @optional outputPath Directory that should contain the build artifact. * @returns {IApplicationPackage} Information about latest built application. */ - getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage; + getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage; /** * Returns information about the latest built application for simulator in the current project. * @param {IPlatformData} platformData Data describing the current platform. * @param {IBuildConfig} buildConfig Defines if the build is for release configuration. + * @param {string} @optional outputPath Directory that should contain the build artifact. * @returns {IApplicationPackage} Information about latest built application. */ - getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage; + getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage; /** * Copies latest build output to a specified location. @@ -160,9 +173,10 @@ interface IPlatformService extends NodeJS.EventEmitter { * @param {string} platform Mobile platform - Android, iOS. * @param {IBuildConfig} buildConfig Defines if the searched artifact should be for simulator and is it built for release. * @param {IProjectData} projectData DTO with information about the project. + * @param {string} @optional outputPath Directory that should contain the build artifact. * @returns {string} The path to latest built artifact. */ - lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): string; + lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData, outputPath?: string): string; /** * Reads contents of a file on device. @@ -188,6 +202,15 @@ interface IPlatformService extends NodeJS.EventEmitter { * @returns {Promise} */ trackActionForPlatform(actionData: ITrackPlatformAction): Promise; + + /** + * Saves build information in a proprietary file. + * @param {string} platform The build platform. + * @param {string} projectDir The project's directory. + * @param {string} buildInfoFileDirname The directory where the build file should be written to. + * @returns {void} + */ + saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void } interface IAddPlatformCoreOptions extends IPlatformSpecificData, ICreateProjectOptions { } diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 09e9293c6c..a0aa63b25a 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -105,6 +105,8 @@ interface IProjectDataService { * @returns {void} */ removeDependency(projectDir: string, dependencyName: string): void; + + getProjectData(projectDir: string): IProjectData; } /** diff --git a/lib/definitions/simple-plist.d.ts b/lib/definitions/simple-plist.d.ts new file mode 100644 index 0000000000..adc559fc81 --- /dev/null +++ b/lib/definitions/simple-plist.d.ts @@ -0,0 +1,4 @@ +declare module "simple-plist" { + export function readFile(filePath: string, callback?:(err: Error, obj: any) => void): void; + export function readFileSync(filePath: string): any; +} diff --git a/lib/device-path-provider.ts b/lib/device-path-provider.ts new file mode 100644 index 0000000000..ee360590ec --- /dev/null +++ b/lib/device-path-provider.ts @@ -0,0 +1,44 @@ +import { fromWindowsRelativePathToUnix } from "./common/helpers"; +import { APP_FOLDER_NAME, LiveSyncPaths } from "./constants"; +import { AndroidDeviceLiveSyncService } from "./services/livesync/android-device-livesync-service"; +import * as path from "path"; + +export class DevicePathProvider implements IDevicePathProvider { + constructor(private $mobileHelper: Mobile.IMobileHelper, + private $injector: IInjector, + private $iOSSimResolver: Mobile.IiOSSimResolver) { + } + + public async getDeviceProjectRootPath(device: Mobile.IDevice, options: IDeviceProjectRootOptions): Promise { + let projectRoot = ""; + if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { + if (device.isEmulator) { + let applicationPath = this.$iOSSimResolver.iOSSim.getApplicationPath(device.deviceInfo.identifier, options.appIdentifier); + projectRoot = path.join(applicationPath); + } else { + projectRoot = LiveSyncPaths.IOS_DEVICE_PROJECT_ROOT_PATH; + } + + if (!options.getDirname) { + projectRoot = path.join(projectRoot, APP_FOLDER_NAME); + } + } else if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { + projectRoot = `/data/local/tmp/${options.appIdentifier}`; + if (!options.getDirname) { + const deviceLiveSyncService = this.$injector.resolve(AndroidDeviceLiveSyncService, { _device: device }); + const hashService = deviceLiveSyncService.getDeviceHashService(options.appIdentifier); + const hashFile = options.syncAllFiles ? null : await hashService.doesShasumFileExistsOnDevice(); + const syncFolderName = options.watch || hashFile ? LiveSyncPaths.SYNC_DIR_NAME : LiveSyncPaths.FULLSYNC_DIR_NAME; + projectRoot = path.join(projectRoot, syncFolderName); + } + } + + return fromWindowsRelativePathToUnix(projectRoot); + } + + public getDeviceSyncZipPath(device: Mobile.IDevice): string { + return this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform) && !device.isEmulator ? LiveSyncPaths.IOS_DEVICE_SYNC_ZIP_PATH : undefined; + } +} + +$injector.register("devicePathProvider", DevicePathProvider); diff --git a/lib/device-sockets/ios/socket-proxy-factory.ts b/lib/device-sockets/ios/socket-proxy-factory.ts index 45f32abf8b..4f67a7a242 100644 --- a/lib/device-sockets/ios/socket-proxy-factory.ts +++ b/lib/device-sockets/ios/socket-proxy-factory.ts @@ -75,7 +75,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact public async createWebSocketProxy(factory: () => Promise): Promise { // NOTE: We will try to provide command line options to select ports, at least on the localhost. - const localPort = await this.$net.getAvailablePortInRange(8080); + const localPort = await this.$net.getFreePort(); this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); diff --git a/lib/nativescript-cli-lib-bootstrap.ts b/lib/nativescript-cli-lib-bootstrap.ts index ae0721440d..95dcf39e2f 100644 --- a/lib/nativescript-cli-lib-bootstrap.ts +++ b/lib/nativescript-cli-lib-bootstrap.ts @@ -9,7 +9,6 @@ $injector.requirePublic("companionAppsService", "./common/appbuilder/services/li $injector.requirePublicClass("deviceEmitter", "./common/appbuilder/device-emitter"); $injector.requirePublicClass("deviceLogProvider", "./common/appbuilder/device-log-provider"); $injector.requirePublicClass("localBuildService", "./services/local-build-service"); -$injector.requirePublicClass("debugService", "./services/debug-service"); // We need this because some services check if (!$options.justlaunch) to start the device log after some operation. // We don't want this behaviour when the CLI is required as library. diff --git a/lib/providers/device-app-data-provider.ts b/lib/providers/device-app-data-provider.ts deleted file mode 100644 index 1ee89f013f..0000000000 --- a/lib/providers/device-app-data-provider.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as deviceAppDataBaseLib from "../common/mobile/device-app-data/device-app-data-base"; -import * as path from "path"; -import { AndroidDeviceHashService } from "../common/mobile/android/android-device-hash-service"; -import { DeviceAndroidDebugBridge } from "../common/mobile/android/device-android-debug-bridge"; - -const SYNC_DIR_NAME = "sync"; -const FULLSYNC_DIR_NAME = "fullsync"; - -export class IOSAppIdentifier extends deviceAppDataBaseLib.DeviceAppDataBase implements Mobile.IDeviceAppData { - private static DEVICE_PROJECT_ROOT_PATH = "Library/Application Support/LiveSync/app"; - private _deviceProjectRootPath: string = null; - - constructor(_appIdentifier: string, - public device: Mobile.IDevice, - public platform: string, - private $iOSSimResolver: Mobile.IiOSSimResolver) { - super(_appIdentifier); - } - - public async getDeviceProjectRootPath(): Promise { - if (!this._deviceProjectRootPath) { - if (this.device.isEmulator) { - let applicationPath = this.$iOSSimResolver.iOSSim.getApplicationPath(this.device.deviceInfo.identifier, this.appIdentifier); - this._deviceProjectRootPath = path.join(applicationPath, "app"); - } else { - this._deviceProjectRootPath = IOSAppIdentifier.DEVICE_PROJECT_ROOT_PATH; - } - } - - return this._getDeviceProjectRootPath(this._deviceProjectRootPath); - } - - public get deviceSyncZipPath(): string { - if (this.device.isEmulator) { - return undefined; - } else { - return "Library/Application Support/LiveSync/sync.zip"; - } - } - - public async isLiveSyncSupported(): Promise { - return true; - } -} - -export class AndroidAppIdentifier extends deviceAppDataBaseLib.DeviceAppDataBase implements Mobile.IDeviceAppData { - constructor(_appIdentifier: string, - public device: Mobile.IDevice, - public platform: string, - private $options: IOptions, - private $injector: IInjector) { - super(_appIdentifier); - } - - private _deviceProjectRootPath: string; - - public async getDeviceProjectRootPath(): Promise { - if (!this._deviceProjectRootPath) { - let syncFolderName = await this.getSyncFolderName(); - this._deviceProjectRootPath = `/data/local/tmp/${this.appIdentifier}/${syncFolderName}`; - } - - return this._deviceProjectRootPath; - } - - public async isLiveSyncSupported(): Promise { - return true; - } - - private async getSyncFolderName(): Promise { - let adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - let deviceHashService: AndroidDeviceHashService = this.$injector.resolve(AndroidDeviceHashService, { adb: adb, appIdentifier: this.appIdentifier }); - let hashFile = this.$options.force ? null : await deviceHashService.doesShasumFileExistsOnDevice(); - return this.$options.watch || hashFile ? SYNC_DIR_NAME : FULLSYNC_DIR_NAME; - } -} - -export class DeviceAppDataProvider implements Mobile.IDeviceAppDataProvider { - public createFactoryRules(): IDictionary { - return { - iOS: { - vanilla: IOSAppIdentifier - }, - Android: { - vanilla: AndroidAppIdentifier - } - }; - } -} -$injector.register("deviceAppDataProvider", DeviceAppDataProvider); diff --git a/lib/providers/livesync-provider.ts b/lib/providers/livesync-provider.ts deleted file mode 100644 index 0724a34fd0..0000000000 --- a/lib/providers/livesync-provider.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as path from "path"; -import * as temp from "temp"; - -export class LiveSyncProvider implements ILiveSyncProvider { - constructor(private $androidLiveSyncServiceLocator: { factory: Function }, - private $iosLiveSyncServiceLocator: { factory: Function }, - private $platformService: IPlatformService, - private $platformsData: IPlatformsData, - private $logger: ILogger, - private $childProcess: IChildProcess, - private $options: IOptions) { } - - private static FAST_SYNC_FILE_EXTENSIONS = [".css", ".xml", ".html"]; - - private deviceSpecificLiveSyncServicesCache: IDictionary = {}; - public get deviceSpecificLiveSyncServices(): IDictionary { - return { - android: (_device: Mobile.IDevice, $injector: IInjector) => { - if (!this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]) { - this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier] = $injector.resolve(this.$androidLiveSyncServiceLocator.factory, { _device: _device }); - } - - return this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]; - }, - ios: (_device: Mobile.IDevice, $injector: IInjector) => { - if (!this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]) { - this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier] = $injector.resolve(this.$iosLiveSyncServiceLocator.factory, { _device: _device }); - } - - return this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]; - } - }; - } - - public async buildForDevice(device: Mobile.IDevice, projectData: IProjectData): Promise { - let buildConfig: IBuildConfig = { - buildForDevice: !device.isEmulator, - projectDir: this.$options.path, - release: this.$options.release, - teamId: this.$options.teamId, - device: this.$options.device, - provision: this.$options.provision, - }; - - await this.$platformService.buildPlatform(device.deviceInfo.platform, buildConfig, projectData); - let platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); - if (device.isEmulator) { - return this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; - } - - return this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; - } - - public async preparePlatformForSync(platform: string, provision: any, projectData: IProjectData): Promise { - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, this.$options); - } - - public canExecuteFastSync(filePath: string, projectData: IProjectData, platform: string): boolean { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let fastSyncFileExtensions = LiveSyncProvider.FAST_SYNC_FILE_EXTENSIONS.concat(platformData.fastLivesyncFileExtensions); - return _.includes(fastSyncFileExtensions, path.extname(filePath)); - } - - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { - if (deviceAppData.platform.toLowerCase() === "android" || !deviceAppData.deviceSyncZipPath || !isFullSync) { - await deviceAppData.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); - } else { - temp.track(); - let tempZip = temp.path({ prefix: "sync", suffix: ".zip" }); - this.$logger.trace("Creating zip file: " + tempZip); - - if (this.$options.syncAllFiles) { - await this.$childProcess.spawnFromEvent("zip", ["-r", "-0", tempZip, "app"], "close", { cwd: path.dirname(projectFilesPath) }); - } else { - this.$logger.info("Skipping node_modules folder! Use the syncAllFiles option to sync files from this folder."); - await this.$childProcess.spawnFromEvent("zip", ["-r", "-0", tempZip, "app", "-x", "app/tns_modules/*"], "close", { cwd: path.dirname(projectFilesPath) }); - } - - deviceAppData.device.fileSystem.transferFiles(deviceAppData, [{ - getLocalPath: () => tempZip, - getDevicePath: () => deviceAppData.deviceSyncZipPath, - getRelativeToProjectBasePath: () => "../sync.zip", - deviceProjectRootPath: await deviceAppData.getDeviceProjectRootPath() - }]); - } - } -} -$injector.register("liveSyncProvider", LiveSyncProvider); diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index b4e3d63f02..e04613141e 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -65,7 +65,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return [ `${packageName}-${buildMode}.apk`, - `${projectData.projectName}-${buildMode}.apk` + `${projectData.projectName}-${buildMode}.apk`, + `${projectData.projectName}.apk` ]; }, frameworkFilesExtensions: [".jar", ".dat", ".so"], diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts index afc9b5d81b..379e0f79bd 100644 --- a/lib/services/debug-service.ts +++ b/lib/services/debug-service.ts @@ -8,6 +8,7 @@ export class DebugService extends EventEmitter implements IDebugService { private $androidDebugService: IPlatformDebugService, private $iOSDebugService: IPlatformDebugService, private $errors: IErrors, + private $logger: ILogger, private $hostInfo: IHostInfo, private $mobileHelper: Mobile.IMobileHelper) { super(); @@ -29,6 +30,14 @@ export class DebugService extends EventEmitter implements IDebugService { this.$errors.failWithoutHelp(`The application ${debugData.applicationIdentifier} is not installed on device with identifier ${debugData.deviceIdentifier}.`); } + if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { + try { + await device.applicationManager.restartApplication(debugData.applicationIdentifier, debugData.projectName); + } catch (err) { + this.$logger.trace("Failed to restart app", err); + } + } + const debugOptions: IDebugOptions = _.merge({}, options); debugOptions.start = true; @@ -67,7 +76,7 @@ export class DebugService extends EventEmitter implements IDebugService { return _.first(result); } - private getDebugService(device: Mobile.IDevice): IPlatformDebugService { + public getDebugService(device: Mobile.IDevice): IPlatformDebugService { if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { return this.$iOSDebugService; } else if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { diff --git a/lib/services/emulator-platform-service.ts b/lib/services/emulator-platform-service.ts index 6bcd24de6f..ead601fa6d 100644 --- a/lib/services/emulator-platform-service.ts +++ b/lib/services/emulator-platform-service.ts @@ -1,4 +1,5 @@ import { createTable, deferPromise } from "../common/helpers"; +import { DeviceTypes } from "../common/constants"; export class EmulatorPlatformService implements IEmulatorPlatformService { @@ -70,7 +71,7 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { name: device.deviceInfo.displayName, version: device.deviceInfo.version, platform: "Android", - type: "emulator", + type: DeviceTypes.Emulator, isRunning: true }; } diff --git a/lib/services/ios-notification-service.ts b/lib/services/ios-notification-service.ts deleted file mode 100644 index 36b8f6dc87..0000000000 --- a/lib/services/ios-notification-service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as constants from "../common/constants"; - -export class IOSNotificationService implements IiOSNotificationService { - constructor(private $iosDeviceOperations: IIOSDeviceOperations) { } - - public async awaitNotification(deviceIdentifier: string, socket: number, timeout: number): Promise { - const notificationResponse = await this.$iosDeviceOperations.awaitNotificationResponse([{ - deviceId: deviceIdentifier, - socket: socket, - timeout: timeout, - responseCommandType: constants.IOS_RELAY_NOTIFICATION_COMMAND_TYPE, - responsePropertyName: "Name" - }]); - - return _.first(notificationResponse[deviceIdentifier]).response; - } - - public async postNotification(deviceIdentifier: string, notification: string, commandType?: string): Promise { - commandType = commandType || constants.IOS_POST_NOTIFICATION_COMMAND_TYPE; - const response = await this.$iosDeviceOperations.postNotification([{ deviceId: deviceIdentifier, commandType: commandType, notificationName: notification }]); - return _.first(response[deviceIdentifier]).response; - } -} - -$injector.register("iOSNotificationService", IOSNotificationService); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 79362f2a91..7a2b72c520 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -13,6 +13,7 @@ import * as plist from "plist"; import { IOSProvisionService } from "./ios-provision-service"; import { IOSEntitlementsService } from "./ios-entitlements-service"; import { XCConfigService } from "./xcconfig-service"; +import * as simplePlist from "simple-plist"; export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { private static XCODE_PROJECT_EXT_NAME = ".xcodeproj"; @@ -39,6 +40,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $devicesService: Mobile.IDevicesService, private $mobileHelper: Mobile.IMobileHelper, + private $hostInfo: IHostInfo, private $pluginVariablesService: IPluginVariablesService, private $xcprojService: IXcprojService, private $iOSProvisionService: IOSProvisionService, @@ -71,10 +73,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ emulatorBuildOutputPath: path.join(projectRoot, "build", "emulator"), getValidPackageNames: (buildOptions: { isReleaseBuild?: boolean, isForDevice?: boolean }): string[] => { if (buildOptions.isForDevice) { - return [projectData.projectName + ".ipa"]; + return [`${projectData.projectName}.ipa`]; } - return [projectData.projectName + ".app"]; + return [`${projectData.projectName}.app`, `${projectData.projectName}.zip`]; }, frameworkFilesExtensions: [".a", ".framework", ".bin"], frameworkDirectoriesExtensions: [".framework"], @@ -111,6 +113,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } public async validate(): Promise { + if (!this.$hostInfo.isDarwin) { + return; + } + try { await this.$childProcess.exec("which xcodebuild"); } catch (error) { @@ -492,23 +498,23 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } private async addFramework(frameworkPath: string, projectData: IProjectData): Promise { - await this.validateFramework(frameworkPath); + if (!this.$hostInfo.isWindows) { + this.validateFramework(frameworkPath); - let project = this.createPbxProj(projectData); - let frameworkName = path.basename(frameworkPath, path.extname(frameworkPath)); - let frameworkBinaryPath = path.join(frameworkPath, frameworkName); - let isDynamic = _.includes((await this.$childProcess.spawnFromEvent("otool", ["-Vh", frameworkBinaryPath], "close")).stdout, " DYLIB "); + let project = this.createPbxProj(projectData); + let frameworkName = path.basename(frameworkPath, path.extname(frameworkPath)); + let frameworkBinaryPath = path.join(frameworkPath, frameworkName); + let isDynamic = _.includes((await this.$childProcess.spawnFromEvent("file", [frameworkBinaryPath], "close")).stdout, "dynamically linked"); + let frameworkAddOptions: IXcode.Options = { customFramework: true }; - let frameworkAddOptions: IXcode.Options = { customFramework: true }; + if (isDynamic) { + frameworkAddOptions["embed"] = true; + } - if (isDynamic) { - frameworkAddOptions["embed"] = true; + let frameworkRelativePath = '$(SRCROOT)/' + this.getLibSubpathRelativeToProjectPath(frameworkPath, projectData); + project.addFramework(frameworkRelativePath, frameworkAddOptions); + this.savePbxProj(project, projectData); } - - let frameworkRelativePath = '$(SRCROOT)/' + this.getLibSubpathRelativeToProjectPath(frameworkPath, projectData); - project.addFramework(frameworkRelativePath, frameworkAddOptions); - this.savePbxProj(project, projectData); - } private async addStaticLibrary(staticLibPath: string, projectData: IProjectData): Promise { @@ -918,17 +924,18 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return path.join(newModulesDir, constants.PROJECT_FRAMEWORK_FOLDER_NAME, `${IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER}.xcodeproj`, "project.pbxproj"); } - private async validateFramework(libraryPath: string): Promise { - let infoPlistPath = path.join(libraryPath, "Info.plist"); + private validateFramework(libraryPath: string): void { + const infoPlistPath = path.join(libraryPath, "Info.plist"); if (!this.$fs.exists(infoPlistPath)) { this.$errors.failWithoutHelp("The bundle at %s does not contain an Info.plist file.", libraryPath); } - let packageType = (await this.$childProcess.spawnFromEvent("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundlePackageType", infoPlistPath], "close")).stdout.trim(); + const plistJson = simplePlist.readFileSync(infoPlistPath); + const packageType = plistJson["CFBundlePackageType"]; + if (packageType !== "FMWK") { this.$errors.failWithoutHelp("The bundle at %s does not appear to be a dynamic framework.", libraryPath); } - } private async validateStaticLibrary(libraryPath: string): Promise { @@ -1107,11 +1114,13 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$fs.writeFile(projectFile, ""); } - await this.checkIfXcodeprojIsRequired(); - let escapedProjectFile = projectFile.replace(/'/g, "\\'"), - escapedPluginFile = pluginFile.replace(/'/g, "\\'"), - mergeScript = `require 'xcodeproj'; Xcodeproj::Config.new('${escapedProjectFile}').merge(Xcodeproj::Config.new('${escapedPluginFile}')).save_as(Pathname.new('${escapedProjectFile}'))`; - await this.$childProcess.exec(`ruby -e "${mergeScript}"`); + if (this.$hostInfo.isDarwin) { + await this.checkIfXcodeprojIsRequired(); + let escapedProjectFile = projectFile.replace(/'/g, "\\'"), + escapedPluginFile = pluginFile.replace(/'/g, "\\'"), + mergeScript = `require 'xcodeproj'; Xcodeproj::Config.new('${escapedProjectFile}').merge(Xcodeproj::Config.new('${escapedPluginFile}')).save_as(Pathname.new('${escapedProjectFile}'))`; + await this.$childProcess.exec(`ruby -e "${mergeScript}"`); + } } private async mergeProjectXcconfigFiles(release: boolean, projectData: IProjectData): Promise { diff --git a/lib/services/livesync/android-device-livesync-service.ts b/lib/services/livesync/android-device-livesync-service.ts index 0ea189229b..bf1c0edbd9 100644 --- a/lib/services/livesync/android-device-livesync-service.ts +++ b/lib/services/livesync/android-device-livesync-service.ts @@ -1,35 +1,43 @@ import { DeviceAndroidDebugBridge } from "../../common/mobile/android/device-android-debug-bridge"; import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; +import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; import * as helpers from "../../common/helpers"; +import { LiveSyncPaths } from "../../constants"; +import { cache } from "../../common/decorators"; import * as path from "path"; import * as net from "net"; -class AndroidLiveSyncService implements INativeScriptDeviceLiveSyncService { +export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { private static BACKEND_PORT = 18182; private device: Mobile.IAndroidDevice; constructor(_device: Mobile.IDevice, private $mobileHelper: Mobile.IMobileHelper, + private $devicePathProvider: IDevicePathProvider, private $injector: IInjector, - private $androidDebugService: IDebugService, - private $liveSyncProvider: ILiveSyncProvider) { + protected $platformsData: IPlatformsData) { + super($platformsData); this.device = (_device); } - public get debugService(): IDebugService { - return this.$androidDebugService; - } + public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { + const deviceAppData = liveSyncInfo.deviceAppData; + const localToDevicePaths = liveSyncInfo.modifiedFilesData; + const deviceProjectRootDirname = await this.$devicePathProvider.getDeviceProjectRootPath(liveSyncInfo.deviceAppData.device, { + appIdentifier: liveSyncInfo.deviceAppData.appIdentifier, + getDirname: true + }); - public async refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], forceExecuteFullSync: boolean, projectData: IProjectData): Promise { await this.device.adb.executeShellCommand( ["chmod", - "777", - await deviceAppData.getDeviceProjectRootPath(), - `/data/local/tmp/${deviceAppData.appIdentifier}`, - `/data/local/tmp/${deviceAppData.appIdentifier}/sync`] - ); + "777", + path.dirname(deviceProjectRootDirname), + deviceProjectRootDirname, + `${deviceProjectRootDirname}/sync`] + ); - let canExecuteFastSync = !forceExecuteFullSync && !_.some(localToDevicePaths, (localToDevicePath: any) => !this.$liveSyncProvider.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, deviceAppData.platform)); + const canExecuteFastSync = !liveSyncInfo.isFullSync && !_.some(localToDevicePaths, + (localToDevicePath: Mobile.ILocalToDevicePathData) => !this.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, this.device.deviceInfo.platform)); if (canExecuteFastSync) { return this.reloadPage(deviceAppData, localToDevicePaths); @@ -47,49 +55,52 @@ class AndroidLiveSyncService implements INativeScriptDeviceLiveSyncService { } public async beforeLiveSyncAction(deviceAppData: Mobile.IDeviceAppData): Promise { - let deviceRootPath = this.getDeviceRootPath(deviceAppData.appIdentifier), - deviceRootDir = path.dirname(deviceRootPath), - deviceRootBasename = path.basename(deviceRootPath), - listResult = await this.device.adb.executeShellCommand(["ls", "-l", deviceRootDir]), - regex = new RegExp(`^-.*${deviceRootBasename}$`, "m"), - matchingFile = (listResult || "").match(regex); + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(deviceAppData.device, { + appIdentifier: deviceAppData.appIdentifier, + getDirname: true + }); + const deviceRootDir = path.dirname(deviceRootPath); + const deviceRootBasename = path.basename(deviceRootPath); + const listResult = await this.device.adb.executeShellCommand(["ls", "-l", deviceRootDir]); + const regex = new RegExp(`^-.*${deviceRootBasename}$`, "m"); + const matchingFile = (listResult || "").match(regex); // Check if there is already a file with deviceRootBasename. If so, delete it as it breaks LiveSyncing. if (matchingFile && matchingFile[0] && _.startsWith(matchingFile[0], '-')) { await this.device.adb.executeShellCommand(["rm", "-f", deviceRootPath]); } - this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, "fullsync"), - this.$mobileHelper.buildDevicePath(deviceRootPath, "sync"), - await this.$mobileHelper.buildDevicePath(deviceRootPath, "removedsync")]); + this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.FULLSYNC_DIR_NAME), + this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.SYNC_DIR_NAME), + await this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.REMOVEDSYNC_DIR_NAME)]); } private async reloadPage(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - await this.device.adb.executeCommand(["forward", `tcp:${AndroidLiveSyncService.BACKEND_PORT.toString()}`, `localabstract:${deviceAppData.appIdentifier}-livesync`]); + await this.device.adb.executeCommand(["forward", `tcp:${AndroidDeviceLiveSyncService.BACKEND_PORT.toString()}`, `localabstract:${deviceAppData.appIdentifier}-livesync`]); if (!await this.sendPageReloadMessage()) { await this.restartApplication(deviceAppData); } } - public async removeFiles(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise { - let deviceRootPath = this.getDeviceRootPath(appIdentifier); + public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(deviceAppData.device, { + appIdentifier: deviceAppData.appIdentifier, + getDirname: true + }); - for (let localToDevicePathData of localToDevicePaths) { - let relativeUnixPath = _.trimStart(helpers.fromWindowsRelativePathToUnix(localToDevicePathData.getRelativeToProjectBasePath()), "/"); - let deviceFilePath = this.$mobileHelper.buildDevicePath(deviceRootPath, "removedsync", relativeUnixPath); + for (const localToDevicePathData of localToDevicePaths) { + const relativeUnixPath = _.trimStart(helpers.fromWindowsRelativePathToUnix(localToDevicePathData.getRelativeToProjectBasePath()), "/"); + const deviceFilePath = this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.REMOVEDSYNC_DIR_NAME, relativeUnixPath); await this.device.adb.executeShellCommand(["mkdir", "-p", path.dirname(deviceFilePath), " && ", "touch", deviceFilePath]); } - await this.getDeviceHashService(projectId).removeHashes(localToDevicePaths); - } - - public async afterInstallApplicationAction(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise { - await this.getDeviceHashService(projectId).uploadHashFileToDevice(localToDevicePaths); - return false; + await this.getDeviceHashService(deviceAppData.appIdentifier).removeHashes(localToDevicePaths); } - private getDeviceRootPath(appIdentifier: string): string { - return `/data/local/tmp/${appIdentifier}`; + @cache() + public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { + let adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); + return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); } private async sendPageReloadMessage(): Promise { @@ -97,7 +108,7 @@ class AndroidLiveSyncService implements INativeScriptDeviceLiveSyncService { let isResolved = false; let socket = new net.Socket(); - socket.connect(AndroidLiveSyncService.BACKEND_PORT, '127.0.0.1', () => { + socket.connect(AndroidDeviceLiveSyncService.BACKEND_PORT, '127.0.0.1', () => { socket.write(new Buffer([0, 0, 0, 1, 1])); }); socket.on("data", (data: any) => { @@ -118,15 +129,4 @@ class AndroidLiveSyncService implements INativeScriptDeviceLiveSyncService { }); }); } - - private _deviceHashService: Mobile.IAndroidDeviceHashService; - private getDeviceHashService(projectId: string): Mobile.IAndroidDeviceHashService { - if (!this._deviceHashService) { - let adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - this._deviceHashService = this.$injector.resolve(AndroidDeviceHashService, { adb: adb, appIdentifier: projectId }); - } - - return this._deviceHashService; - } } -$injector.register("androidLiveSyncServiceLocator", { factory: AndroidLiveSyncService }); diff --git a/lib/services/livesync/android-livesync-service.ts b/lib/services/livesync/android-livesync-service.ts new file mode 100644 index 0000000000..d9416643c5 --- /dev/null +++ b/lib/services/livesync/android-livesync-service.ts @@ -0,0 +1,21 @@ +import { AndroidDeviceLiveSyncService } from "./android-device-livesync-service"; +import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base"; + +export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { + constructor(protected $platformsData: IPlatformsData, + protected $projectFilesManager: IProjectFilesManager, + private $injector: IInjector, + $devicePathProvider: IDevicePathProvider, + $fs: IFileSystem, + $logger: ILogger, + $projectFilesProvider: IProjectFilesProvider, + ) { + super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider); + } + + protected _getDeviceLiveSyncService(device: Mobile.IDevice): INativeScriptDeviceLiveSyncService { + const service = this.$injector.resolve(AndroidDeviceLiveSyncService, { _device: device }); + return service; + } +} +$injector.register("androidLiveSyncService", AndroidLiveSyncService); diff --git a/lib/services/livesync/debug-livesync-service.ts b/lib/services/livesync/debug-livesync-service.ts new file mode 100644 index 0000000000..be75692cf1 --- /dev/null +++ b/lib/services/livesync/debug-livesync-service.ts @@ -0,0 +1,78 @@ +import { EOL } from "os"; +import { LiveSyncService } from "./livesync-service"; + +export class DebugLiveSyncService extends LiveSyncService implements IDebugLiveSyncService { + + constructor(protected $platformService: IPlatformService, + $projectDataService: IProjectDataService, + protected $devicesService: Mobile.IDevicesService, + $mobileHelper: Mobile.IMobileHelper, + $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, + protected $logger: ILogger, + $processService: IProcessService, + $hooksService: IHooksService, + protected $injector: IInjector, + private $options: IOptions, + private $debugDataService: IDebugDataService, + private $projectData: IProjectData, + private $debugService: IDebugService, + private $config: IConfiguration) { + + super($platformService, + $projectDataService, + $devicesService, + $mobileHelper, + $devicePlatformsConstants, + $nodeModulesDependenciesBuilder, + $logger, + $processService, + $hooksService, + $injector); + } + + protected async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo): Promise { + const debugOptions = this.$options; + const deployOptions: IDeployPlatformOptions = { + clean: this.$options.clean, + device: this.$options.device, + emulator: this.$options.emulator, + platformTemplate: this.$options.platformTemplate, + projectDir: this.$options.path, + release: this.$options.release, + provision: this.$options.provision, + teamId: this.$options.teamId + }; + + let debugData = this.$debugDataService.createDebugData(this.$projectData, this.$options); + const debugService = this.$debugService.getDebugService(liveSyncResultInfo.deviceAppData.device); + + await this.$platformService.trackProjectType(this.$projectData); + + if (this.$options.start) { + return this.printDebugInformation(await debugService.debug(debugData, debugOptions)); + } + + const deviceAppData = liveSyncResultInfo.deviceAppData; + this.$config.debugLivesync = true; + + await debugService.debugStop(); + + let applicationId = deviceAppData.appIdentifier; + await deviceAppData.device.applicationManager.stopApplication(applicationId, projectData.projectName); + + const buildConfig: IBuildConfig = _.merge({ buildForDevice: !deviceAppData.device.isEmulator }, deployOptions); + debugData.pathToAppPackage = this.$platformService.lastOutputPath(debugService.platform, buildConfig, projectData); + + this.printDebugInformation(await debugService.debug(debugData, debugOptions)); + } + + public printDebugInformation(information: string[]): void { + information = information.filter(i => !!i); + _.each(information, i => { + this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${i}${EOL}`.cyan); + }); + } +} + +$injector.register("debugLiveSyncService", DebugLiveSyncService); diff --git a/lib/services/livesync/device-livesync-service-base.ts b/lib/services/livesync/device-livesync-service-base.ts new file mode 100644 index 0000000000..bb1f44961f --- /dev/null +++ b/lib/services/livesync/device-livesync-service-base.ts @@ -0,0 +1,21 @@ +import { cache } from "../../common/decorators"; +import * as path from "path"; + +export abstract class DeviceLiveSyncServiceBase { + private static FAST_SYNC_FILE_EXTENSIONS = [".css", ".xml", ".html"]; + + constructor(protected $platformsData: IPlatformsData) { } + + public canExecuteFastSync(filePath: string, projectData: IProjectData, platform: string): boolean { + const fastSyncFileExtensions = this.getFastLiveSyncFileExtensions(platform, projectData); + return _.includes(fastSyncFileExtensions, path.extname(filePath)); + } + + @cache() + private getFastLiveSyncFileExtensions(platform: string, projectData: IProjectData): string[] { + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const fastSyncFileExtensions = DeviceLiveSyncServiceBase.FAST_SYNC_FILE_EXTENSIONS.concat(platformData.fastLivesyncFileExtensions); + return fastSyncFileExtensions; + } + +} diff --git a/lib/services/livesync/ios-device-livesync-service.ts b/lib/services/livesync/ios-device-livesync-service.ts index a309233506..23ea563a4e 100644 --- a/lib/services/livesync/ios-device-livesync-service.ts +++ b/lib/services/livesync/ios-device-livesync-service.ts @@ -2,10 +2,11 @@ import * as helpers from "../../common/helpers"; import * as constants from "../../constants"; import * as minimatch from "minimatch"; import * as net from "net"; +import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; let currentPageReloadId = 0; -class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { +export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements INativeScriptDeviceLiveSyncService { private static BACKEND_PORT = 18181; private socket: net.Socket; private device: Mobile.IiOSDevice; @@ -15,23 +16,13 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { private $iOSNotification: IiOSNotification, private $iOSEmulatorServices: Mobile.IiOSSimulatorService, private $logger: ILogger, - private $options: IOptions, - private $iOSDebugService: IDebugService, private $fs: IFileSystem, - private $liveSyncProvider: ILiveSyncProvider, - private $processService: IProcessService) { - + private $processService: IProcessService, + protected $platformsData: IPlatformsData) { + super($platformsData); this.device = _device; } - public get debugService(): IDebugService { - return this.$iOSDebugService; - } - - public async afterInstallApplicationAction(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - return this.$options.watch; - } - private async setupSocketIfNeeded(projectId: string): Promise { if (this.socket) { return true; @@ -40,7 +31,7 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { if (this.device.isEmulator) { await this.$iOSEmulatorServices.postDarwinNotification(this.$iOSNotification.getAttachRequest(projectId)); try { - this.socket = await helpers.connectEventuallyUntilTimeout(() => net.connect(IOSLiveSyncService.BACKEND_PORT), 5000); + this.socket = await helpers.connectEventuallyUntilTimeout(() => net.connect(IOSDeviceLiveSyncService.BACKEND_PORT), 5000); } catch (e) { this.$logger.debug(e); return false; @@ -48,7 +39,7 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { } else { let timeout = 9000; await this.$iOSSocketRequestExecutor.executeAttachRequest(this.device, timeout, projectId); - this.socket = await this.device.connectToPort(IOSLiveSyncService.BACKEND_PORT); + this.socket = await this.device.connectToPort(IOSDeviceLiveSyncService.BACKEND_PORT); } this.attachEventHandlers(); @@ -56,24 +47,26 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { return true; } - public async removeFiles(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - await Promise.all(_.map(localToDevicePaths, localToDevicePathData => this.device.fileSystem.deleteFile(localToDevicePathData.getDevicePath(), appIdentifier))); + public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + await Promise.all(_.map(localToDevicePaths, localToDevicePathData => this.device.fileSystem.deleteFile(localToDevicePathData.getDevicePath(), deviceAppData.appIdentifier))); } - public async refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], forceExecuteFullSync: boolean, projectData: IProjectData): Promise { - if (forceExecuteFullSync) { + public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { + const deviceAppData = liveSyncInfo.deviceAppData; + const localToDevicePaths = liveSyncInfo.modifiedFilesData; + if (liveSyncInfo.isFullSync) { await this.restartApplication(deviceAppData, projectData.projectName); return; } let scriptRelatedFiles: Mobile.ILocalToDevicePathData[] = []; - let scriptFiles = _.filter(localToDevicePaths, localToDevicePath => _.endsWith(localToDevicePath.getDevicePath(), ".js")); + const scriptFiles = _.filter(localToDevicePaths, localToDevicePath => _.endsWith(localToDevicePath.getDevicePath(), ".js")); constants.LIVESYNC_EXCLUDED_FILE_PATTERNS.forEach(pattern => scriptRelatedFiles = _.concat(scriptRelatedFiles, localToDevicePaths.filter(file => minimatch(file.getDevicePath(), pattern, { nocase: true })))); - let otherFiles = _.difference(localToDevicePaths, _.concat(scriptFiles, scriptRelatedFiles)); - let shouldRestart = _.some(otherFiles, (localToDevicePath: Mobile.ILocalToDevicePathData) => !this.$liveSyncProvider.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, deviceAppData.platform)); + const otherFiles = _.difference(localToDevicePaths, _.concat(scriptFiles, scriptRelatedFiles)); + const shouldRestart = _.some(otherFiles, (localToDevicePath: Mobile.ILocalToDevicePathData) => !this.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, deviceAppData.platform)); - if (shouldRestart || (!this.$options.liveEdit && scriptFiles.length)) { + if (shouldRestart || (!liveSyncInfo.useLiveEdit && scriptFiles.length)) { await this.restartApplication(deviceAppData, projectData.projectName); return; } @@ -175,4 +168,3 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { } } } -$injector.register("iosLiveSyncServiceLocator", { factory: IOSLiveSyncService }); diff --git a/lib/services/livesync/ios-livesync-service.ts b/lib/services/livesync/ios-livesync-service.ts new file mode 100644 index 0000000000..3efc0bb3e1 --- /dev/null +++ b/lib/services/livesync/ios-livesync-service.ts @@ -0,0 +1,75 @@ +import * as path from "path"; +import * as temp from "temp"; + +import { IOSDeviceLiveSyncService } from "./ios-device-livesync-service"; +import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base"; +import { APP_FOLDER_NAME, TNS_MODULES_FOLDER_NAME } from "../../constants"; + +export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { + constructor(protected $fs: IFileSystem, + protected $platformsData: IPlatformsData, + protected $projectFilesManager: IProjectFilesManager, + private $injector: IInjector, + $devicePathProvider: IDevicePathProvider, + $logger: ILogger, + $projectFilesProvider: IProjectFilesProvider, + ) { + super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider); + } + + public async fullSync(syncInfo: IFullSyncInfo): Promise { + const device = syncInfo.device; + + if (device.isEmulator) { + return super.fullSync(syncInfo); + } + + const projectData = syncInfo.projectData; + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const deviceAppData = await this.getAppData(syncInfo); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + + temp.track(); + const tempZip = temp.path({ prefix: "sync", suffix: ".zip" }); + const tempApp = temp.mkdirSync("app"); + this.$logger.trace("Creating zip file: " + tempZip); + this.$fs.copyFile(path.join(path.dirname(projectFilesPath), `${APP_FOLDER_NAME}/*`), tempApp); + + if (!syncInfo.syncAllFiles) { + this.$logger.info("Skipping node_modules folder! Use the syncAllFiles option to sync files from this folder."); + this.$fs.deleteDirectory(path.join(tempApp, TNS_MODULES_FOLDER_NAME)); + } + + await this.$fs.zipFiles(tempZip, this.$fs.enumerateFilesInDirectorySync(tempApp), (res) => { + return path.join(APP_FOLDER_NAME, path.relative(tempApp, res)); + }); + + await device.fileSystem.transferFiles(deviceAppData, [{ + getLocalPath: () => tempZip, + getDevicePath: () => deviceAppData.deviceSyncZipPath, + getRelativeToProjectBasePath: () => "../sync.zip", + deviceProjectRootPath: await deviceAppData.getDeviceProjectRootPath() + }]); + + return { + deviceAppData, + isFullSync: true, + modifiedFilesData: [] + }; + } + + public liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise { + if (liveSyncInfo.isRebuilt) { + // In this case we should execute fullsync because iOS Runtime requires the full content of app dir to be extracted in the root of sync dir. + return this.fullSync({ projectData: liveSyncInfo.projectData, device, syncAllFiles: liveSyncInfo.syncAllFiles, watch: true }); + } else { + return super.liveSyncWatchAction(device, liveSyncInfo); + } + } + + protected _getDeviceLiveSyncService(device: Mobile.IDevice): INativeScriptDeviceLiveSyncService { + const service = this.$injector.resolve(IOSDeviceLiveSyncService, { _device: device }); + return service; + } +} +$injector.register("iOSLiveSyncService", IOSLiveSyncService); diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 7225477b47..f5f84f0692 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -1,136 +1,447 @@ -import * as constants from "../../constants"; -import * as helpers from "../../common/helpers"; import * as path from "path"; +import * as choki from "chokidar"; +import { EventEmitter } from "events"; +import { hook } from "../../common/helpers"; +import { APP_FOLDER_NAME, PACKAGE_JSON_FILE_NAME, LiveSyncTrackActionNames } from "../../constants"; +import { FileExtensions, DeviceTypes } from "../../common/constants"; +const deviceDescriptorPrimaryKey = "identifier"; -let choki = require("chokidar"); +const LiveSyncEvents = { + liveSyncStopped: "liveSyncStopped", + // In case we name it error, EventEmitter expects instance of Error to be raised and will also raise uncaught exception in case there's no handler + liveSyncError: "liveSyncError", + liveSyncExecuted: "liveSyncExecuted", + liveSyncStarted: "liveSyncStarted", + liveSyncNotification: "notify" +}; -class LiveSyncService implements ILiveSyncService { - private _isInitialized = false; +export class LiveSyncService extends EventEmitter implements ILiveSyncService { + // key is projectDir + private liveSyncProcessesInfo: IDictionary = {}; - constructor(private $errors: IErrors, - private $platformsData: IPlatformsData, - private $platformService: IPlatformService, - private $injector: IInjector, - private $devicesService: Mobile.IDevicesService, - private $options: IOptions, - private $logger: ILogger, - private $dispatcher: IFutureDispatcher, - private $hooksService: IHooksService, + constructor(protected $platformService: IPlatformService, + private $projectDataService: IProjectDataService, + protected $devicesService: Mobile.IDevicesService, + private $mobileHelper: Mobile.IMobileHelper, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, + protected $logger: ILogger, private $processService: IProcessService, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder) { } - - public get isInitialized(): boolean { // This function is used from https://github.com/NativeScript/nativescript-dev-typescript/blob/master/lib/before-prepare.js#L4 - return this._isInitialized; + private $hooksService: IHooksService, + protected $injector: IInjector) { + super(); } - public async liveSync(platform: string, projectData: IProjectData, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise { - if (this.$options.justlaunch) { - this.$options.watch = false; + @hook("liveSync") + public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], + liveSyncData: ILiveSyncInfo): Promise { + const projectData = this.$projectDataService.getProjectData(liveSyncData.projectDir); + // In case liveSync is called for a second time for the same projectDir. + const isAlreadyLiveSyncing = this.liveSyncProcessesInfo[projectData.projectDir] && !this.liveSyncProcessesInfo[projectData.projectDir].isStopped; + this.setLiveSyncProcessInfo(liveSyncData.projectDir, deviceDescriptors); + + const deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, this.liveSyncProcessesInfo[projectData.projectDir].deviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors; + + await this.initialSync(projectData, deviceDescriptorsForInitialSync, liveSyncData); + + if (!liveSyncData.skipWatcher && deviceDescriptors && deviceDescriptors.length) { + // Should be set after prepare + this.$injector.resolve("usbLiveSyncService").isInitialized = true; + + await this.startWatcher(projectData, liveSyncData); } - let liveSyncData: ILiveSyncData[] = []; - - if (platform) { - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); - liveSyncData.push(await this.prepareLiveSyncData(platform, projectData)); - } else if (this.$options.device) { - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); - platform = this.$devicesService.getDeviceByIdentifier(this.$options.device).deviceInfo.platform; - liveSyncData.push(await this.prepareLiveSyncData(platform, projectData)); - } else { - await this.$devicesService.initialize({ skipInferPlatform: true, skipDeviceDetectionInterval: true }); - - for (let installedPlatform of this.$platformService.getInstalledPlatforms(projectData)) { - if (this.$devicesService.getDevicesForPlatform(installedPlatform).length === 0) { - await this.$devicesService.startEmulator(installedPlatform); + } + + public async stopLiveSync(projectDir: string, deviceIdentifiers?: string[]): Promise { + const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectDir]; + + if (liveSyncProcessInfo) { + _.each(deviceIdentifiers, deviceId => { + _.remove(liveSyncProcessInfo.deviceDescriptors, descriptor => { + const shouldRemove = descriptor.identifier === deviceId; + if (shouldRemove) { + this.emit(LiveSyncEvents.liveSyncStopped, { projectDir, deviceIdentifier: descriptor.identifier }); + } + + return shouldRemove; + }); + }); + + // In case deviceIdentifiers are not passed, we should stop the whole LiveSync. + if (!deviceIdentifiers || !deviceIdentifiers.length || !liveSyncProcessInfo.deviceDescriptors || !liveSyncProcessInfo.deviceDescriptors.length) { + if (liveSyncProcessInfo.timer) { + clearTimeout(liveSyncProcessInfo.timer); + } + + if (liveSyncProcessInfo.watcherInfo && liveSyncProcessInfo.watcherInfo.watcher) { + liveSyncProcessInfo.watcherInfo.watcher.close(); } - liveSyncData.push(await this.prepareLiveSyncData(installedPlatform, projectData)); + liveSyncProcessInfo.watcherInfo = null; + + if (liveSyncProcessInfo.actionsChain) { + await liveSyncProcessInfo.actionsChain; + } + + liveSyncProcessInfo.isStopped = true; + liveSyncProcessInfo.deviceDescriptors = []; + + // Kill typescript watcher + const projectData = this.$projectDataService.getProjectData(projectDir); + await this.$hooksService.executeAfterHooks('watch', { + hookArgs: { + projectData + } + }); + + this.emit(LiveSyncEvents.liveSyncStopped, { projectDir }); } } + } - if (liveSyncData.length === 0) { - this.$errors.fail("There are no platforms installed in this project. Please specify platform or install one by using `tns platform add` command!"); + protected async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo): Promise { + const platformLiveSyncService = this.getLiveSyncService(liveSyncResultInfo.deviceAppData.platform); + try { + await platformLiveSyncService.refreshApplication(projectData, liveSyncResultInfo); + } catch (err) { + this.$logger.info(`Error while trying to start application ${projectData.projectId} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Error is: ${err.message || err}`); + const msg = `Unable to start application ${projectData.projectId} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Try starting it manually.`; + this.$logger.warn(msg); + this.emit(LiveSyncEvents.liveSyncNotification, { + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId, + deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, + notification: msg + }); } - this._isInitialized = true; // If we want before-prepare hooks to work properly, this should be set after preparePlatform function + this.emit(LiveSyncEvents.liveSyncExecuted, { + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId, + syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), + deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier + }); - await this.liveSyncCore(liveSyncData, applicationReloadAction, projectData); + this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); } - private async prepareLiveSyncData(platform: string, projectData: IProjectData): Promise { - platform = platform || this.$devicesService.platform; - let platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); + private setLiveSyncProcessInfo(projectDir: string, deviceDescriptors: ILiveSyncDeviceInfo[]): void { + this.liveSyncProcessesInfo[projectDir] = this.liveSyncProcessesInfo[projectDir] || Object.create(null); + this.liveSyncProcessesInfo[projectDir].actionsChain = this.liveSyncProcessesInfo[projectDir].actionsChain || Promise.resolve(); + this.liveSyncProcessesInfo[projectDir].isStopped = false; - let liveSyncData: ILiveSyncData = { - platform: platform, - appIdentifier: projectData.projectId, - projectFilesPath: path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME), - syncWorkingDirectory: projectData.projectDir, - excludedProjectDirsAndFiles: this.$options.release ? constants.LIVESYNC_EXCLUDED_FILE_PATTERNS : [] - }; + const currentDeviceDescriptors = this.liveSyncProcessesInfo[projectDir].deviceDescriptors || []; + // Prevent cases where liveSync is called consecutive times with the same device, for example [ A, B, C ] and then [ A, B, D ] - we want to execute initialSync only for D. + this.liveSyncProcessesInfo[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), deviceDescriptorPrimaryKey); + } + + private getLiveSyncService(platform: string): IPlatformLiveSyncService { + if (this.$mobileHelper.isiOSPlatform(platform)) { + return this.$injector.resolve("iOSLiveSyncService"); + } else if (this.$mobileHelper.isAndroidPlatform(platform)) { + return this.$injector.resolve("androidLiveSyncService"); + } - return liveSyncData; + throw new Error(`Invalid platform ${platform}. Supported platforms are: ${this.$mobileHelper.platformNames.join(", ")}`); } - @helpers.hook('livesync') - private async liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise { - await this.$platformService.trackProjectType(projectData); + private async ensureLatestAppPackageIsInstalledOnDevice(options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { + const platform = options.device.deviceInfo.platform; + if (options.preparedPlatforms.indexOf(platform) === -1) { + options.preparedPlatforms.push(platform); + // TODO: Pass provision and sdk as a fifth argument here + await this.$platformService.preparePlatform(platform, { + bundle: false, + release: false, + }, null, options.projectData, {}, options.modifiedFiles); + } - let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise)[] = []; + const rebuildInfo = _.find(options.rebuiltInformation, info => info.isEmulator === options.device.isEmulator && info.platform === platform); - for (let dataItem of liveSyncData) { - let service: IPlatformLiveSyncService = this.$injector.resolve("platformLiveSyncService", { _liveSyncData: dataItem }); - watchForChangeActions.push((event: string, filePath: string, dispatcher: IFutureDispatcher) => - service.partialSync(event, filePath, dispatcher, applicationReloadAction, projectData)); + if (rebuildInfo) { + // Case where we have three devices attached, a change that requires build is found, + // we'll rebuild the app only for the first device, but we should install new package on all three devices. + await this.$platformService.installApplication(options.device, { release: false }, options.projectData, rebuildInfo.pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + return; + } - await service.fullSync(projectData, applicationReloadAction); + // TODO: Pass provision and sdk as a fifth argument here + const shouldBuild = await this.$platformService.shouldBuild(platform, options.projectData, { buildForDevice: !options.device.isEmulator, clean: options.liveSyncData && options.liveSyncData.clean }, options.deviceBuildInfoDescriptor.outputPath); + let pathToBuildItem = null; + let action = LiveSyncTrackActionNames.LIVESYNC_OPERATION; + if (shouldBuild) { + pathToBuildItem = await options.deviceBuildInfoDescriptor.buildAction(); + // Is it possible to return shouldBuild for two devices? What about android device and android emulator? + options.rebuiltInformation.push({ isEmulator: options.device.isEmulator, platform, pathToBuildItem }); + action = LiveSyncTrackActionNames.LIVESYNC_OPERATION_BUILD; } - if (this.$options.watch && !this.$options.justlaunch) { - await this.$hooksService.executeBeforeHooks('watch'); - await this.partialSync(liveSyncData[0].syncWorkingDirectory, watchForChangeActions, projectData); + if (!options.settings[platform][options.device.deviceInfo.type]) { + let isForDevice = !options.device.isEmulator; + options.settings[platform][options.device.deviceInfo.type] = true; + if (this.$mobileHelper.isAndroidPlatform(platform)) { + options.settings[platform][DeviceTypes.Emulator] = true; + options.settings[platform][DeviceTypes.Device] = true; + isForDevice = null; + } + + await this.$platformService.trackActionForPlatform({ action, platform, isForDevice }); } + + await this.$platformService.trackActionForPlatform({ action: LiveSyncTrackActionNames.DEVICE_INFO, platform, isForDevice: !options.device.isEmulator, deviceOsVersion: options.device.deviceInfo.version }); + + const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options.deviceBuildInfoDescriptor.outputPath); + if (shouldInstall) { + await this.$platformService.installApplication(options.device, { release: false }, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + } + } + + private async initialSync(projectData: IProjectData, deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { + const preparedPlatforms: string[] = []; + const rebuiltInformation: ILiveSyncBuildInfo[] = []; + + const settings = this.getDefaultLatestAppPackageInstalledSettings(); + // Now fullSync + const deviceAction = async (device: Mobile.IDevice): Promise => { + try { + this.emit(LiveSyncEvents.liveSyncStarted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectId + }); + + const platform = device.deviceInfo.platform; + const deviceBuildInfoDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); + + await this.ensureLatestAppPackageIsInstalledOnDevice({ + device, + preparedPlatforms, + rebuiltInformation, + projectData, + deviceBuildInfoDescriptor, + liveSyncData, + settings + }); + + const liveSyncResultInfo = await this.getLiveSyncService(platform).fullSync({ + projectData, device, + syncAllFiles: liveSyncData.watchAllFiles, + useLiveEdit: liveSyncData.useLiveEdit, + watch: !liveSyncData.skipWatcher + }); + await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); + await this.refreshApplication(projectData, liveSyncResultInfo); + } catch (err) { + this.$logger.warn(`Unable to apply changes on device: ${device.deviceInfo.identifier}. Error is: ${err.message}.`); + + this.emit(LiveSyncEvents.liveSyncError, { + error: err, + deviceIdentifier: device.deviceInfo.identifier, + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId + }); + + await this.stopLiveSync(projectData.projectDir, [device.deviceInfo.identifier]); + } + }; + + // Execute the action only on the deviceDescriptors passed to initialSync. + // In case where we add deviceDescriptors to already running application, we've already executed initialSync for them. + await this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device: Mobile.IDevice) => _.some(deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier))); + } + + private getDefaultLatestAppPackageInstalledSettings(): ILatestAppPackageInstalledSettings { + return { + [this.$devicePlatformsConstants.Android]: { + [DeviceTypes.Device]: false, + [DeviceTypes.Emulator]: false + }, + [this.$devicePlatformsConstants.iOS]: { + [DeviceTypes.Device]: false, + [DeviceTypes.Emulator]: false + } + }; } - private partialSync(syncWorkingDirectory: string, onChangedActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise)[], projectData: IProjectData): void { - let that = this; - let productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); - let pattern = ["app"]; + private async startWatcher(projectData: IProjectData, liveSyncData: ILiveSyncInfo): Promise { + let pattern = [APP_FOLDER_NAME]; - if (this.$options.syncAllFiles) { - pattern.push("package.json"); + if (liveSyncData.watchAllFiles) { + const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); + pattern.push(PACKAGE_JSON_FILE_NAME); // watch only production node_module/packages same one prepare uses for (let index in productionDependencies) { - pattern.push("node_modules/" + productionDependencies[index].name); + pattern.push(productionDependencies[index].directory); } } - let watcher = choki.watch(pattern, { ignoreInitial: true, cwd: syncWorkingDirectory, - awaitWriteFinish: { - stabilityThreshold: 500, - pollInterval: 100 - }, }).on("all", (event: string, filePath: string) => { - that.$dispatcher.dispatch(async () => { - try { - filePath = path.join(syncWorkingDirectory, filePath); - for (let i = 0; i < onChangedActions.length; i++) { - that.$logger.trace(`Event '${event}' triggered for path: '${filePath}'`); - await onChangedActions[i](event, filePath, that.$dispatcher); - } - } catch (err) { - that.$logger.info(`Unable to sync file ${filePath}. Error is:${err.message}`.red.bold); - that.$logger.info("Try saving it again or restart the livesync operation."); + const currentWatcherInfo = this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo; + + if (!currentWatcherInfo || currentWatcherInfo.pattern !== pattern) { + if (currentWatcherInfo) { + currentWatcherInfo.watcher.close(); + } + + let filesToSync: string[] = [], + filesToRemove: string[] = []; + let timeoutTimer: NodeJS.Timer; + + const startTimeout = () => { + timeoutTimer = setTimeout(async () => { + // Push actions to the queue, do not start them simultaneously + await this.addActionToChain(projectData.projectDir, async () => { + if (filesToSync.length || filesToRemove.length) { + try { + let currentFilesToSync = _.cloneDeep(filesToSync); + filesToSync = []; + + let currentFilesToRemove = _.cloneDeep(filesToRemove); + filesToRemove = []; + + const allModifiedFiles = [].concat(currentFilesToSync).concat(currentFilesToRemove); + const preparedPlatforms: string[] = []; + const rebuiltInformation: ILiveSyncBuildInfo[] = []; + + const latestAppPackageInstalledSettings = this.getDefaultLatestAppPackageInstalledSettings(); + + await this.$devicesService.execute(async (device: Mobile.IDevice) => { + const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir]; + const deviceBuildInfoDescriptor = _.find(liveSyncProcessInfo.deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); + + await this.ensureLatestAppPackageIsInstalledOnDevice({ + device, + preparedPlatforms, + rebuiltInformation, + projectData, + deviceBuildInfoDescriptor, + settings: latestAppPackageInstalledSettings, + modifiedFiles: allModifiedFiles + }); + + const service = this.getLiveSyncService(device.deviceInfo.platform); + const settings: ILiveSyncWatchInfo = { + projectData, + filesToRemove: currentFilesToRemove, + filesToSync: currentFilesToSync, + isRebuilt: !!_.find(rebuiltInformation, info => info.isEmulator === device.isEmulator && info.platform === device.deviceInfo.platform), + syncAllFiles: liveSyncData.watchAllFiles, + useLiveEdit: liveSyncData.useLiveEdit + }; + + const liveSyncResultInfo = await service.liveSyncWatchAction(device, settings); + await this.refreshApplication(projectData, liveSyncResultInfo); + }, + (device: Mobile.IDevice) => { + const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir]; + return liveSyncProcessInfo && _.some(liveSyncProcessInfo.deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier); + } + ); + } catch (err) { + const allErrors = (err).allErrors; + + if (allErrors && _.isArray(allErrors)) { + for (let deviceError of allErrors) { + this.$logger.warn(`Unable to apply changes for device: ${deviceError.deviceIdentifier}. Error is: ${deviceError.message}.`); + + this.emit(LiveSyncEvents.liveSyncError, { + error: deviceError, + deviceIdentifier: deviceError.deviceIdentifier, + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId + }); + + await this.stopLiveSync(projectData.projectDir, [deviceError.deviceIdentifier]); + } + } + } + } + }); + }, 250); + + this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer; + }; + + await this.$hooksService.executeBeforeHooks('watch', { + hookArgs: { + projectData } }); - }); - this.$processService.attachToProcessExitSignals(this, () => { - watcher.close(pattern); - }); + const watcherOptions: choki.WatchOptions = { + ignoreInitial: true, + cwd: liveSyncData.projectDir, + awaitWriteFinish: { + pollInterval: 100, + stabilityThreshold: 500 + }, + ignored: ["**/.*", ".*"] // hidden files + }; + + const watcher = choki.watch(pattern, watcherOptions) + .on("all", async (event: string, filePath: string) => { + clearTimeout(timeoutTimer); + + filePath = path.join(liveSyncData.projectDir, filePath); + + this.$logger.trace(`Chokidar raised event ${event} for ${filePath}.`); + + if (event === "add" || event === "addDir" || event === "change" /* <--- what to do when change event is raised ? */) { + filesToSync.push(filePath); + } else if (event === "unlink" || event === "unlinkDir") { + filesToRemove.push(filePath); + } - this.$dispatcher.run(); + // Do not sync typescript files directly - wait for javascript changes to occur in order to restart the app only once + if (path.extname(filePath) !== FileExtensions.TYPESCRIPT_FILE) { + startTimeout(); + } + }); + + this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo = { watcher, pattern }; + this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer; + + this.$processService.attachToProcessExitSignals(this, () => { + _.keys(this.liveSyncProcessesInfo).forEach(projectDir => { + // Do not await here, we are in process exit's handler. + this.stopLiveSync(projectDir); + }); + }); + + this.$devicesService.on("deviceLost", async (device: Mobile.IDevice) => { + await this.stopLiveSync(projectData.projectDir, [device.deviceInfo.identifier]); + }); + } } + + private async addActionToChain(projectDir: string, action: () => Promise): Promise { + const liveSyncInfo = this.liveSyncProcessesInfo[projectDir]; + if (liveSyncInfo) { + liveSyncInfo.actionsChain = liveSyncInfo.actionsChain.then(async () => { + if (!liveSyncInfo.isStopped) { + const res = await action(); + return res; + } + }); + + const result = await liveSyncInfo.actionsChain; + return result; + } + } + +} + +$injector.register("liveSyncService", LiveSyncService); + +/** + * This class is used only for old versions of nativescript-dev-typescript plugin. + * It should be replaced with liveSyncService.isInitalized. + * Consider adding get and set methods for isInitialized, + * so whenever someone tries to access the value of isInitialized, + * they'll get a warning to update the plugins (like nativescript-dev-typescript). + */ +export class DeprecatedUsbLiveSyncService { + public isInitialized = false; } -$injector.register("usbLiveSyncService", LiveSyncService); +$injector.register("usbLiveSyncService", DeprecatedUsbLiveSyncService); diff --git a/lib/services/livesync/platform-livesync-service-base.ts b/lib/services/livesync/platform-livesync-service-base.ts new file mode 100644 index 0000000000..04a3883f20 --- /dev/null +++ b/lib/services/livesync/platform-livesync-service-base.ts @@ -0,0 +1,136 @@ +import * as path from "path"; +import * as util from "util"; +import { APP_FOLDER_NAME } from "../../constants"; + +export abstract class PlatformLiveSyncServiceBase { + private _deviceLiveSyncServicesCache: IDictionary = {}; + + constructor(protected $fs: IFileSystem, + protected $logger: ILogger, + protected $platformsData: IPlatformsData, + protected $projectFilesManager: IProjectFilesManager, + private $devicePathProvider: IDevicePathProvider, + private $projectFilesProvider: IProjectFilesProvider) { } + + public getDeviceLiveSyncService(device: Mobile.IDevice, applicationIdentifier: string): INativeScriptDeviceLiveSyncService { + const key = device.deviceInfo.identifier + applicationIdentifier; + if (!this._deviceLiveSyncServicesCache[key]) { + this._deviceLiveSyncServicesCache[key] = this._getDeviceLiveSyncService(device); + } + + return this._deviceLiveSyncServicesCache[key]; + } + + protected abstract _getDeviceLiveSyncService(device: Mobile.IDevice): INativeScriptDeviceLiveSyncService; + + public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { + if (liveSyncInfo.isFullSync || liveSyncInfo.modifiedFilesData.length) { + const deviceLiveSyncService = this.getDeviceLiveSyncService(liveSyncInfo.deviceAppData.device, projectData.projectId); + this.$logger.info("Refreshing application..."); + await deviceLiveSyncService.refreshApplication(projectData, liveSyncInfo); + } + } + + public async fullSync(syncInfo: IFullSyncInfo): Promise { + const projectData = syncInfo.projectData; + const device = syncInfo.device; + const deviceLiveSyncService = this.getDeviceLiveSyncService(device, syncInfo.projectData.projectId); + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const deviceAppData = await this.getAppData(syncInfo); + + if (deviceLiveSyncService.beforeLiveSyncAction) { + await deviceLiveSyncService.beforeLiveSyncAction(deviceAppData); + } + + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, []); + await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, true); + + return { + modifiedFilesData: localToDevicePaths, + isFullSync: true, + deviceAppData + }; + } + + public async liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise { + const projectData = liveSyncInfo.projectData; + const syncInfo = _.merge({ device, watch: true }, liveSyncInfo); + const deviceAppData = await this.getAppData(syncInfo); + + let modifiedLocalToDevicePaths: Mobile.ILocalToDevicePathData[] = []; + if (liveSyncInfo.filesToSync.length) { + const filesToSync = liveSyncInfo.filesToSync; + const mappedFiles = _.map(filesToSync, filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)); + + // Some plugins modify platforms dir on afterPrepare (check nativescript-dev-sass) - we want to sync only existing file. + const existingFiles = mappedFiles.filter(m => this.$fs.exists(m)); + this.$logger.trace("Will execute livesync for files: ", existingFiles); + const skippedFiles = _.difference(mappedFiles, existingFiles); + if (skippedFiles.length) { + this.$logger.trace("The following files will not be synced as they do not exist:", skippedFiles); + } + + if (existingFiles.length) { + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, + projectFilesPath, mappedFiles, []); + modifiedLocalToDevicePaths.push(...localToDevicePaths); + await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, false); + } + } + + if (liveSyncInfo.filesToRemove.length) { + const filePaths = liveSyncInfo.filesToRemove; + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + + const mappedFiles = _.map(filePaths, filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, mappedFiles, []); + modifiedLocalToDevicePaths.push(...localToDevicePaths); + + const deviceLiveSyncService = this.getDeviceLiveSyncService(device, projectData.projectId); + deviceLiveSyncService.removeFiles(deviceAppData, localToDevicePaths); + } + + return { + modifiedFilesData: modifiedLocalToDevicePaths, + isFullSync: liveSyncInfo.isRebuilt, + deviceAppData + }; + } + + protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { + if (isFullSync) { + await deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); + } else { + await deviceAppData.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); + } + + this.logFilesSyncInformation(localToDevicePaths, "Successfully transferred %s.", this.$logger.info); + } + + protected async getAppData(syncInfo: IFullSyncInfo): Promise { + const deviceProjectRootOptions: IDeviceProjectRootOptions = _.assign({ appIdentifier: syncInfo.projectData.projectId }, syncInfo); + return { + appIdentifier: syncInfo.projectData.projectId, + device: syncInfo.device, + platform: syncInfo.device.deviceInfo.platform, + getDeviceProjectRootPath: () => this.$devicePathProvider.getDeviceProjectRootPath(syncInfo.device, deviceProjectRootOptions), + deviceSyncZipPath: this.$devicePathProvider.getDeviceSyncZipPath(syncInfo.device), + isLiveSyncSupported: async () => true + }; + } + + private logFilesSyncInformation(localToDevicePaths: Mobile.ILocalToDevicePathData[], message: string, action: Function): void { + if (localToDevicePaths && localToDevicePaths.length < 10) { + _.each(localToDevicePaths, (file: Mobile.ILocalToDevicePathData) => { + action.call(this.$logger, util.format(message, path.basename(file.getLocalPath()).yellow)); + }); + } else { + action.call(this.$logger, util.format(message, "all files")); + } + } + +} diff --git a/lib/services/livesync/platform-livesync-service.ts b/lib/services/livesync/platform-livesync-service.ts deleted file mode 100644 index 4b926dfa77..0000000000 --- a/lib/services/livesync/platform-livesync-service.ts +++ /dev/null @@ -1,263 +0,0 @@ -import syncBatchLib = require("../../common/services/livesync/sync-batch"); -import * as path from "path"; -import * as minimatch from "minimatch"; -import * as util from "util"; -import * as helpers from "../../common/helpers"; - -const livesyncInfoFileName = ".nslivesyncinfo"; - -export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { - private batch: IDictionary = Object.create(null); - private livesyncData: IDictionary = Object.create(null); - - protected liveSyncData: ILiveSyncData; - - constructor(_liveSyncData: ILiveSyncData, - private $devicesService: Mobile.IDevicesService, - private $mobileHelper: Mobile.IMobileHelper, - private $logger: ILogger, - private $options: IOptions, - private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, - private $injector: IInjector, - private $projectFilesManager: IProjectFilesManager, - private $projectFilesProvider: IProjectFilesProvider, - private $platformService: IPlatformService, - private $projectChangesService: IProjectChangesService, - private $liveSyncProvider: ILiveSyncProvider, - private $fs: IFileSystem) { - this.liveSyncData = _liveSyncData; - } - - public async fullSync(projectData: IProjectData, postAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise { - let appIdentifier = this.liveSyncData.appIdentifier; - let platform = this.liveSyncData.platform; - let projectFilesPath = this.liveSyncData.projectFilesPath; - let canExecute = this.getCanExecuteAction(platform, appIdentifier); - let action = async (device: Mobile.IDevice): Promise => { - await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); - - let deviceAppData = this.$deviceAppDataFactory.create(appIdentifier, this.$mobileHelper.normalizePlatformName(platform), device); - let localToDevicePaths: Mobile.ILocalToDevicePathData[] = null; - if (await this.shouldTransferAllFiles(platform, deviceAppData, projectData)) { - localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, this.liveSyncData.excludedProjectDirsAndFiles); - await this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, true); - await device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(platform, projectData), await this.getLiveSyncInfoFilePath(deviceAppData), appIdentifier); - } - - if (postAction) { - await this.finishLivesync(deviceAppData); - await postAction(deviceAppData); - return; - } - - await this.refreshApplication(deviceAppData, localToDevicePaths, true, projectData); - await this.finishLivesync(deviceAppData); - }; - await this.$devicesService.execute(action, canExecute); - } - - public async partialSync(event: string, filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise { - if (this.isFileExcluded(filePath, this.liveSyncData.excludedProjectDirsAndFiles)) { - this.$logger.trace(`Skipping livesync for changed file ${filePath} as it is excluded in the patterns: ${this.liveSyncData.excludedProjectDirsAndFiles.join(", ")}`); - return; - } - - if (event === "add" || event === "addDir" || event === "change") { - this.batchSync(filePath, dispatcher, afterFileSyncAction, projectData); - } else if (event === "unlink" || event === "unlinkDir") { - await this.syncRemovedFile(filePath, afterFileSyncAction, projectData); - } - } - - protected getCanExecuteAction(platform: string, appIdentifier: string): (dev: Mobile.IDevice) => boolean { - let isTheSamePlatformAction = ((device: Mobile.IDevice) => device.deviceInfo.platform.toLowerCase() === platform.toLowerCase()); - if (this.$options.device) { - return (device: Mobile.IDevice): boolean => isTheSamePlatformAction(device) && device.deviceInfo.identifier === this.$devicesService.getDeviceByDeviceOption().deviceInfo.identifier; - } - return isTheSamePlatformAction; - } - - public async refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], isFullSync: boolean, projectData: IProjectData): Promise { - let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(deviceAppData.device.deviceInfo.platform, deviceAppData.device); - this.$logger.info("Refreshing application..."); - await deviceLiveSyncService.refreshApplication(deviceAppData, localToDevicePaths, isFullSync, projectData); - } - - protected async finishLivesync(deviceAppData: Mobile.IDeviceAppData): Promise { - // This message is important because it signals Visual Studio Code that livesync has finished and debugger can be attached. - this.$logger.info(`Successfully synced application ${deviceAppData.appIdentifier} on device ${deviceAppData.device.deviceInfo.identifier}.\n`); - } - - protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { - this.$logger.info("Transferring project files..."); - let canTransferDirectory = isFullSync && (this.$devicesService.isAndroidDevice(deviceAppData.device) || this.$devicesService.isiOSSimulator(deviceAppData.device)); - if (canTransferDirectory) { - await deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); - } else { - await this.$liveSyncProvider.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync); - } - this.logFilesSyncInformation(localToDevicePaths, "Successfully transferred %s.", this.$logger.info); - } - - protected resolveDeviceSpecificLiveSyncService(platform: string, device: Mobile.IDevice): INativeScriptDeviceLiveSyncService { - return this.$injector.resolve(this.$liveSyncProvider.deviceSpecificLiveSyncServices[platform.toLowerCase()], { _device: device }); - } - - private isFileExcluded(filePath: string, excludedPatterns: string[]): boolean { - let isFileExcluded = false; - _.each(excludedPatterns, pattern => { - if (minimatch(filePath, pattern, { nocase: true })) { - isFileExcluded = true; - return false; - } - }); - - // skip hidden files, to prevent reload of the app for hidden files - // created temporarily by the IDEs - if (this.isUnixHiddenPath(filePath)) { - isFileExcluded = true; - } - - return isFileExcluded; - } - - private isUnixHiddenPath(filePath: string): boolean { - return (/(^|\/)\.[^\/\.]/g).test(filePath); - } - - private batchSync(filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): void { - let platformBatch: ISyncBatch = this.batch[this.liveSyncData.platform]; - if (!platformBatch || !platformBatch.syncPending) { - let done = async () => { - dispatcher.dispatch(async () => { - try { - for (let platform in this.batch) { - let batch = this.batch[platform]; - await batch.syncFiles(async (filesToSync: string[]) => { - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - await this.$platformService.preparePlatform(this.liveSyncData.platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, this.$options, filesToSync); - let canExecute = this.getCanExecuteAction(this.liveSyncData.platform, this.liveSyncData.appIdentifier); - let deviceFileAction = (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, !filePath); - let action = this.getSyncAction(filesToSync, deviceFileAction, afterFileSyncAction, projectData); - await this.$devicesService.execute(action, canExecute); - }); - } - } catch (err) { - this.$logger.warn(`Unable to sync files. Error is:`, err.message); - } - }); - }; - - this.batch[this.liveSyncData.platform] = this.$injector.resolve(syncBatchLib.SyncBatch, { done: done }); - this.livesyncData[this.liveSyncData.platform] = this.liveSyncData; - } - - this.batch[this.liveSyncData.platform].addFile(filePath); - } - - private async syncRemovedFile(filePath: string, - afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise { - let deviceFilesAction = (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => { - let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(this.liveSyncData.platform, deviceAppData.device); - return deviceLiveSyncService.removeFiles(this.liveSyncData.appIdentifier, localToDevicePaths, projectData.projectId); - }; - let canExecute = this.getCanExecuteAction(this.liveSyncData.platform, this.liveSyncData.appIdentifier); - let action = this.getSyncAction([filePath], deviceFilesAction, afterFileSyncAction, projectData); - await this.$devicesService.execute(action, canExecute); - } - - private getSyncAction( - filesToSync: string[], - fileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, - afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, - projectData: IProjectData): (device: Mobile.IDevice) => Promise { - let action = async (device: Mobile.IDevice): Promise => { - let deviceAppData: Mobile.IDeviceAppData = null; - let localToDevicePaths: Mobile.ILocalToDevicePathData[] = null; - let isFullSync = false; - - if (this.$projectChangesService.currentChanges.changesRequireBuild) { - let buildConfig: IBuildConfig = { - buildForDevice: !device.isEmulator, - projectDir: this.$options.path, - release: this.$options.release, - teamId: this.$options.teamId, - device: this.$options.device, - provision: this.$options.provision, - }; - let platform = device.deviceInfo.platform; - if (this.$platformService.shouldBuild(platform, projectData, buildConfig)) { - await this.$platformService.buildPlatform(platform, buildConfig, projectData); - } - - await this.$platformService.installApplication(device, buildConfig, projectData); - deviceAppData = this.$deviceAppDataFactory.create(this.liveSyncData.appIdentifier, this.$mobileHelper.normalizePlatformName(this.liveSyncData.platform), device); - isFullSync = true; - } else { - deviceAppData = this.$deviceAppDataFactory.create(this.liveSyncData.appIdentifier, this.$mobileHelper.normalizePlatformName(this.liveSyncData.platform), device); - const mappedFiles = filesToSync.map((file: string) => this.$projectFilesProvider.mapFilePath(file, device.deviceInfo.platform, projectData)); - - // Some plugins modify platforms dir on afterPrepare (check nativescript-dev-sass) - we want to sync only existing file. - const existingFiles = mappedFiles.filter(m => this.$fs.exists(m)); - - this.$logger.trace("Will execute livesync for files: ", existingFiles); - - const skippedFiles = _.difference(mappedFiles, existingFiles); - - if (skippedFiles.length) { - this.$logger.trace("The following files will not be synced as they do not exist:", skippedFiles); - } - - localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, this.liveSyncData.projectFilesPath, mappedFiles, this.liveSyncData.excludedProjectDirsAndFiles); - - await fileSyncAction(deviceAppData, localToDevicePaths); - } - - if (!afterFileSyncAction) { - await this.refreshApplication(deviceAppData, localToDevicePaths, isFullSync, projectData); - } - - await device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(device.deviceInfo.platform, projectData), await this.getLiveSyncInfoFilePath(deviceAppData), this.liveSyncData.appIdentifier); - - await this.finishLivesync(deviceAppData); - - if (afterFileSyncAction) { - await afterFileSyncAction(deviceAppData, localToDevicePaths); - } - }; - - return action; - } - - private async shouldTransferAllFiles(platform: string, deviceAppData: Mobile.IDeviceAppData, projectData: IProjectData): Promise { - try { - if (this.$options.clean) { - return false; - } - let fileText = await this.$platformService.readFile(deviceAppData.device, await this.getLiveSyncInfoFilePath(deviceAppData), projectData); - let remoteLivesyncInfo: IPrepareInfo = JSON.parse(fileText); - let localPrepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - return remoteLivesyncInfo.time !== localPrepareInfo.time; - } catch (e) { - return true; - } - } - - private async getLiveSyncInfoFilePath(deviceAppData: Mobile.IDeviceAppData): Promise { - let deviceRootPath = path.dirname(await deviceAppData.getDeviceProjectRootPath()); - let deviceFilePath = helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, livesyncInfoFileName)); - return deviceFilePath; - } - - private logFilesSyncInformation(localToDevicePaths: Mobile.ILocalToDevicePathData[], message: string, action: Function): void { - if (localToDevicePaths && localToDevicePaths.length < 10) { - _.each(localToDevicePaths, (file: Mobile.ILocalToDevicePathData) => { - action.call(this.$logger, util.format(message, path.basename(file.getLocalPath()).yellow)); - }); - } else { - action.call(this.$logger, util.format(message, "all files")); - } - } -} - -$injector.register("platformLiveSyncService", PlatformLiveSyncServiceBase); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 4694896eca..6851c120fd 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -34,10 +34,10 @@ export class PlatformService extends EventEmitter implements IPlatformService { private $projectFilesManager: IProjectFilesManager, private $mobileHelper: Mobile.IMobileHelper, private $hostInfo: IHostInfo, + private $devicePathProvider: IDevicePathProvider, private $xmlValidator: IXmlValidator, private $npm: INodePackageManager, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, private $projectChangesService: IProjectChangesService, private $analyticsService: IAnalyticsService) { super(); @@ -369,32 +369,38 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - public async shouldBuild(platform: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + public async shouldBuild(platform: string, projectData: IProjectData, buildConfig: IBuildConfig, outputPath?: string): Promise { if (this.$projectChangesService.currentChanges.changesRequireBuild) { return true; } + let platformData = this.$platformsData.getPlatformData(platform, projectData); let forDevice = !buildConfig || buildConfig.buildForDevice; - let outputPath = forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath; + outputPath = outputPath || (forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath); if (!this.$fs.exists(outputPath)) { return true; } + let packageNames = platformData.getValidPackageNames({ isForDevice: forDevice }); let packages = this.getApplicationPackages(outputPath, packageNames); if (packages.length === 0) { return true; } + let prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - let buildInfo = this.getBuildInfo(platform, platformData, buildConfig); + let buildInfo = this.getBuildInfo(platform, platformData, buildConfig, outputPath); if (!prepareInfo || !buildInfo) { return true; } + if (buildConfig.clean) { return true; } + if (prepareInfo.time === buildInfo.prepareTime) { return false; } + return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime; } @@ -438,36 +444,45 @@ export class PlatformService extends EventEmitter implements IPlatformService { await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, platformData.platformProjectService, handler, platformData.platformProjectService.buildProject(platformData.projectRoot, projectData, buildConfig)); - let prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - let buildInfoFilePath = this.getBuildOutputPath(platform, platformData, buildConfig); - let buildInfoFile = path.join(buildInfoFilePath, buildInfoFileName); - let buildInfo: IBuildInfo = { + const buildInfoFilePath = this.getBuildOutputPath(platform, platformData, buildConfig); + this.saveBuildInfoFile(platform, projectData.projectDir, buildInfoFilePath); + + this.$logger.out("Project successfully built."); + } + + public saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void { + let buildInfoFile = path.join(buildInfoFileDirname, buildInfoFileName); + + let prepareInfo = this.$projectChangesService.getPrepareInfo(platform, this.$projectDataService.getProjectData(projectDir)); + let buildInfo = { prepareTime: prepareInfo.changesRequireBuildTime, buildTime: new Date().toString() }; + this.$fs.writeJson(buildInfoFile, buildInfo); - this.$logger.out("Project successfully built."); } - public async shouldInstall(device: Mobile.IDevice, projectData: IProjectData): Promise { + public async shouldInstall(device: Mobile.IDevice, projectData: IProjectData, outputPath?: string): Promise { let platform = device.deviceInfo.platform; - let platformData = this.$platformsData.getPlatformData(platform, projectData); if (!(await device.applicationManager.isApplicationInstalled(projectData.projectId))) { return true; } + + let platformData = this.$platformsData.getPlatformData(platform, projectData); let deviceBuildInfo: IBuildInfo = await this.getDeviceBuildInfo(device, projectData); - let localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator }); + let localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator }, outputPath); return !localBuildInfo || !deviceBuildInfo || deviceBuildInfo.buildTime !== localBuildInfo.buildTime; } - public async installApplication(device: Mobile.IDevice, buildConfig: IBuildConfig, projectData: IProjectData): Promise { + public async installApplication(device: Mobile.IDevice, buildConfig: IBuildConfig, projectData: IProjectData, packageFile?: string, outputFilePath?: string): Promise { this.$logger.out("Installing..."); let platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); - let packageFile = ""; - if (this.$devicesService.isiOSSimulator(device)) { - packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; - } else { - packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; + if (!packageFile) { + if (this.$devicesService.isiOSSimulator(device)) { + packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig, outputFilePath).packageName; + } else { + packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig, outputFilePath).packageName; + } } await platformData.platformProjectService.cleanDeviceTempFolder(device.deviceInfo.identifier, projectData); @@ -476,7 +491,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { if (!buildConfig.release) { let deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); - let buildInfoFilePath = this.getBuildOutputPath(device.deviceInfo.platform, platformData, { buildForDevice: !device.isEmulator }); + let buildInfoFilePath = outputFilePath || this.getBuildOutputPath(device.deviceInfo.platform, platformData, { buildForDevice: !device.isEmulator }); let appIdentifier = projectData.projectId; await device.fileSystem.putFile(path.join(buildInfoFilePath, buildInfoFileName), deviceFilePath, appIdentifier); @@ -545,8 +560,10 @@ export class PlatformService extends EventEmitter implements IPlatformService { } private async getDeviceBuildInfoFilePath(device: Mobile.IDevice, projectData: IProjectData): Promise { - let deviceAppData = this.$deviceAppDataFactory.create(projectData.projectId, device.deviceInfo.platform, device); - let deviceRootPath = path.dirname(await deviceAppData.getDeviceProjectRootPath()); + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(device, { + appIdentifier: projectData.projectId, + getDirname: true + }); return helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, buildInfoFileName)); } @@ -559,9 +576,9 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - private getBuildInfo(platform: string, platformData: IPlatformData, options: IBuildForDevice): IBuildInfo { - let buildInfoFilePath = this.getBuildOutputPath(platform, platformData, options); - let buildInfoFile = path.join(buildInfoFilePath, buildInfoFileName); + private getBuildInfo(platform: string, platformData: IPlatformData, options: IBuildForDevice, buildOutputPath?: string): IBuildInfo { + buildOutputPath = buildOutputPath || this.getBuildOutputPath(platform, platformData, options); + let buildInfoFile = path.join(buildOutputPath, buildInfoFileName); if (this.$fs.exists(buildInfoFile)) { try { let buildInfoTime = this.$fs.readJson(buildInfoFile); @@ -584,13 +601,13 @@ export class PlatformService extends EventEmitter implements IPlatformService { appUpdater.cleanDestinationApp(); } - public lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): string { + public lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData, outputPath?: string): string { let packageFile: string; let platformData = this.$platformsData.getPlatformData(platform, projectData); if (buildConfig.buildForDevice) { - packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; + packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig, outputPath).packageName; } else { - packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; + packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig, outputPath).packageName; } if (!packageFile || !this.$fs.exists(packageFile)) { this.$errors.failWithoutHelp("Unable to find built application. Try 'tns build %s'.", platform); @@ -677,10 +694,6 @@ export class PlatformService extends EventEmitter implements IPlatformService { if (!this.isValidPlatform(platform, projectData)) { this.$errors.fail("Invalid platform %s. Valid platforms are %s.", platform, helpers.formatListOfNames(this.$platformsData.platformsNames)); } - - if (!this.isPlatformSupportedForOS(platform, projectData)) { - this.$errors.fail("Applications for platform %s can not be built on this OS - %s", platform, process.platform); - } } public validatePlatformInstalled(platform: string, projectData: IProjectData): void { @@ -705,7 +718,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { return this.$platformsData.getPlatformData(platform, projectData); } - private isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { + public isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { let targetedOS = this.$platformsData.getPlatformData(platform, projectData).targetedOS; let res = !targetedOS || targetedOS.indexOf("*") >= 0 || targetedOS.indexOf(process.platform) >= 0; return res; @@ -745,12 +758,12 @@ export class PlatformService extends EventEmitter implements IPlatformService { return packages[0]; } - public getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage { - return this.getLatestApplicationPackage(platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: true, isReleaseBuild: buildConfig.release })); + public getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage { + return this.getLatestApplicationPackage(outputPath || platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: true, isReleaseBuild: buildConfig.release })); } - public getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage { - return this.getLatestApplicationPackage(platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: false, isReleaseBuild: buildConfig.release })); + public getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage { + return this.getLatestApplicationPackage(outputPath || platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: false, isReleaseBuild: buildConfig.release })); } private async updatePlatform(platform: string, version: string, platformTemplate: string, projectData: IProjectData, config: IAddPlatformCoreOptions): Promise { @@ -794,6 +807,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out("Successfully updated to version ", updateOptions.newVersion); } + // TODO: Remove this method from here. It has nothing to do with platform public async readFile(device: Mobile.IDevice, deviceFilePath: string, projectData: IProjectData): Promise { temp.track(); let uniqueFilePath = temp.path({ suffix: ".tmp" }); diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 58b48e84a8..29f1f964c7 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import { ProjectData } from "../project-data"; interface IProjectFileData { projectData: any; @@ -10,7 +11,8 @@ export class ProjectDataService implements IProjectDataService { constructor(private $fs: IFileSystem, private $staticConfig: IStaticConfig, - private $logger: ILogger) { + private $logger: ILogger, + private $injector: IInjector) { } public getNSValue(projectDir: string, propertyName: string): any { @@ -31,6 +33,14 @@ export class ProjectDataService implements IProjectDataService { this.$fs.writeJson(projectFileInfo.projectFilePath, projectFileInfo.projectData); } + // TODO: Add tests + // TODO: Remove $projectData and replace it with $projectDataService.getProjectData + public getProjectData(projectDir: string): IProjectData { + const projectDataInstance = this.$injector.resolve(ProjectData); + projectDataInstance.initializeProjectData(projectDir); + return projectDataInstance; + } + private getValue(projectDir: string, propertyName: string): any { const projectData = this.getProjectFileData(projectDir).projectData; diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 838fab565a..f6bcc41a45 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -15,7 +15,7 @@ class TestExecutionService implements ITestExecutionService { constructor(private $injector: IInjector, private $platformService: IPlatformService, private $platformsData: IPlatformsData, - private $usbLiveSyncService: ILiveSyncService, + private $liveSyncService: ILiveSyncService, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $debugDataService: IDebugDataService, private $httpClient: Server.IHttpClient, @@ -41,7 +41,12 @@ class TestExecutionService implements ITestExecutionService { try { let platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); let projectDir = projectData.projectDir; - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); + await this.$devicesService.initialize({ + platform: platform, + deviceId: this.$options.device, + emulator: this.$options.emulator + }); + await this.$devicesService.detectCurrentlyAttachedDevices(); let projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); let configOptions: IKarmaConfigOptions = JSON.parse(launcherConfig); @@ -53,25 +58,63 @@ class TestExecutionService implements ITestExecutionService { let socketIoJsUrl = `http://localhost:${this.$options.port}/socket.io/socket.io.js`; let socketIoJs = (await this.$httpClient.httpRequest(socketIoJsUrl)).body; this.$fs.writeFile(path.join(projectDir, TestExecutionService.SOCKETIO_JS_FILE_NAME), socketIoJs); - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; + if (!await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, this.$options)) { this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation."); } + this.detourEntryPoint(projectFilesPath); const deployOptions: IDeployPlatformOptions = { clean: this.$options.clean, device: this.$options.device, - projectDir: this.$options.path, emulator: this.$options.emulator, + projectDir: this.$options.path, platformTemplate: this.$options.platformTemplate, release: this.$options.release, provision: this.$options.provision, teamId: this.$options.teamId }; - await this.$platformService.deployPlatform(platform, appFilesUpdaterOptions, deployOptions, projectData, this.$options); - await this.$usbLiveSyncService.liveSync(platform, projectData); + + if (this.$options.bundle) { + this.$options.watch = false; + } + + const devices = this.$devicesService.getDeviceInstances(); + // Now let's take data for each device: + const platformLowerCase = this.platform && this.platform.toLowerCase(); + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !platformLowerCase || d.deviceInfo.platform.toLowerCase() === platformLowerCase) + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, // this.$options.forDevice, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, projectData); + const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, projectData); + return pathToBuildResult; + } + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { projectDir: projectData.projectDir, skipWatcher: !this.$options.watch || this.$options.justlaunch, watchAllFiles: this.$options.syncAllFiles }; + + await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); if (this.$options.debugBrk) { this.$logger.info('Starting debugger...'); @@ -102,7 +145,11 @@ class TestExecutionService implements ITestExecutionService { await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); let projectDir = projectData.projectDir; - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); + await this.$devicesService.initialize({ + platform: platform, + deviceId: this.$options.device, + emulator: this.$options.emulator + }); let karmaConfig = this.getKarmaConfiguration(platform, projectData), karmaRunner = this.$childProcess.fork(path.join(__dirname, "karma-execution.js")), @@ -145,8 +192,39 @@ class TestExecutionService implements ITestExecutionService { const debugData = this.getDebugData(platform, projectData, deployOptions); await debugService.debug(debugData, this.$options); } else { - await this.$platformService.deployPlatform(platform, appFilesUpdaterOptions, deployOptions, projectData, this.$options); - await this.$usbLiveSyncService.liveSync(platform, projectData); + const devices = this.$devicesService.getDeviceInstances(); + // Now let's take data for each device: + const platformLowerCase = this.platform && this.platform.toLowerCase(); + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !platformLowerCase || d.deviceInfo.platform.toLowerCase() === platformLowerCase) + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, projectData); + const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, projectData); + return pathToBuildResult; + } + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { projectDir: projectData.projectDir, skipWatcher: !this.$options.watch || this.$options.justlaunch, watchAllFiles: this.$options.syncAllFiles }; + await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); } }; diff --git a/package.json b/package.json index 2565baf463..4b1438302a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "bufferpack": "0.0.6", "byline": "4.2.1", "chalk": "1.1.0", - "chokidar": "^1.6.1", + "chokidar": "1.7.0", "cli-table": "https://github.com/telerik/cli-table/tarball/v0.3.1.2", "clui": "0.3.1", "colors": "1.1.2", @@ -42,7 +42,7 @@ "glob": "^7.0.3", "iconv-lite": "0.4.11", "inquirer": "0.9.0", - "ios-device-lib": "0.4.3", + "ios-device-lib": "0.4.4", "ios-mobileprovision-finder": "1.0.9", "ios-sim-portable": "~3.0.0", "lockfile": "1.0.1", @@ -66,6 +66,7 @@ "request": "2.81.0", "semver": "5.3.0", "shelljs": "0.7.6", + "simple-plist": "0.2.1", "source-map": "0.5.6", "tabtab": "https://github.com/Icenium/node-tabtab/tarball/master", "temp": "0.8.3", @@ -82,6 +83,7 @@ "devDependencies": { "@types/chai": "3.4.34", "@types/chai-as-promised": "0.0.29", + "@types/chokidar": "1.6.0", "@types/lodash": "4.14.50", "@types/node": "6.0.61", "@types/qr-image": "3.2.0", diff --git a/test/debug.ts b/test/debug.ts index dc7de422e9..4b1aae6a89 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -26,11 +26,17 @@ function createTestInjector(): IInjector { testInjector.register('errors', stubs.ErrorsStub); testInjector.register('hostInfo', {}); testInjector.register("analyticsService", { - trackException: async () => undefined, - checkConsent: async () => undefined, - trackFeature: async () => undefined + trackException: async (): Promise => undefined, + checkConsent: async (): Promise => undefined, + trackFeature: async (): Promise => undefined }); - testInjector.register("usbLiveSyncService", stubs.LiveSyncServiceStub); + testInjector.register('devicesService', { + initialize: async () => { /* Intentionally left blank */ }, + detectCurrentlyAttachedDevices: async () => { /* Intentionally left blank */ }, + getDeviceInstances: (): any[] => { return []; }, + execute: async (): Promise => ({}) + }); + testInjector.register("debugLiveSyncService", stubs.LiveSyncServiceStub); testInjector.register("androidProjectService", AndroidProjectService); testInjector.register("androidToolsInfo", stubs.AndroidToolsInfoStub); testInjector.register("hostInfo", {}); diff --git a/test/nativescript-cli-lib.ts b/test/nativescript-cli-lib.ts index 653d440dec..3bcc81522c 100644 --- a/test/nativescript-cli-lib.ts +++ b/test/nativescript-cli-lib.ts @@ -20,6 +20,7 @@ describe("nativescript-cli-lib", () => { deviceLogProvider: null, npm: ["install", "uninstall", "view", "search"], extensibilityService: ["loadExtensions", "loadExtension", "getInstalledExtensions", "installExtension", "uninstallExtension"], + liveSyncService: ["liveSync", "stopLiveSync"], analyticsService: ["startEqatecMonitor"], debugService: ["debug"] }; diff --git a/test/npm-support.ts b/test/npm-support.ts index 3b36faa958..3e72a558bb 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -20,7 +20,6 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; @@ -74,7 +73,6 @@ function createTestInjector(): IInjector { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); @@ -87,6 +85,8 @@ function createTestInjector(): IInjector { testInjector.register("messages", Messages); testInjector.register("nodeModulesDependenciesBuilder", NodeModulesDependenciesBuilder); + testInjector.register("devicePathProvider", {}); + return testInjector; } diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 202f0d8c45..31a2350f60 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -15,7 +15,6 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; @@ -131,7 +130,6 @@ function createTestInjector() { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); @@ -143,6 +141,7 @@ function createTestInjector() { track: async () => undefined }); testInjector.register("messages", Messages); + testInjector.register("devicePathProvider", {}); return testInjector; } diff --git a/test/platform-service.ts b/test/platform-service.ts index a1d95ee1b3..d8898f0b26 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -13,7 +13,6 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; @@ -69,7 +68,6 @@ function createTestInjector() { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); @@ -85,6 +83,7 @@ function createTestInjector() { track: async () => undefined }); testInjector.register("messages", Messages); + testInjector.register("devicePathProvider", {}); return testInjector; } diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 2ad19b077a..fa21aa9b98 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -27,7 +27,6 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; @@ -90,7 +89,6 @@ function createTestInjector() { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("projectTemplatesService", { diff --git a/test/services/debug-service.ts b/test/services/debug-service.ts index ca0f5d086c..ea5224621c 100644 --- a/test/services/debug-service.ts +++ b/test/services/debug-service.ts @@ -86,6 +86,8 @@ describe("debugService", () => { testInjector.register("hostInfo", testData.hostInfo); + testInjector.register("logger", stubs.LoggerStub); + return testInjector; }; @@ -100,7 +102,7 @@ describe("debugService", () => { describe("rejects the result promise when", () => { const assertIsRejected = async (testData: IDebugTestData, expectedError: string, userSpecifiedOptions?: IDebugOptions): Promise => { const testInjector = getTestInjectorForTestConfiguration(testData); - const debugService = testInjector.resolve(DebugService); + const debugService = testInjector.resolve(DebugService); const debugData = getDebugData(); await assert.isRejected(debugService.debug(debugData, userSpecifiedOptions), expectedError); @@ -161,7 +163,7 @@ describe("debugService", () => { throw new Error(expectedErrorMessage); }; - const debugService = testInjector.resolve(DebugService); + const debugService = testInjector.resolve(DebugService); const debugData = getDebugData(); await assert.isRejected(debugService.debug(debugData, null), expectedErrorMessage); @@ -192,7 +194,7 @@ describe("debugService", () => { return []; }; - const debugService = testInjector.resolve(DebugService); + const debugService = testInjector.resolve(DebugService); const debugData = getDebugData(); await assert.isFulfilled(debugService.debug(debugData, userSpecifiedOptions)); @@ -255,7 +257,7 @@ describe("debugService", () => { testData.deviceInformation.deviceInfo.platform = platform; const testInjector = getTestInjectorForTestConfiguration(testData); - const debugService = testInjector.resolve(DebugService); + const debugService = testInjector.resolve(DebugService); let dataRaisedForConnectionError: any = null; debugService.on(CONNECTION_ERROR_EVENT_NAME, (data: any) => { dataRaisedForConnectionError = data; @@ -279,7 +281,7 @@ describe("debugService", () => { testData.deviceInformation.deviceInfo.platform = platform; const testInjector = getTestInjectorForTestConfiguration(testData); - const debugService = testInjector.resolve(DebugService); + const debugService = testInjector.resolve(DebugService); const debugData = getDebugData(); const url = await debugService.debug(debugData, null); diff --git a/test/services/ios-log-filter.ts b/test/services/ios-log-filter.ts index 8b4f317f9f..c8e2588275 100644 --- a/test/services/ios-log-filter.ts +++ b/test/services/ios-log-filter.ts @@ -43,7 +43,7 @@ describe("iOSLogFilter", () => { null, null, "CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3472:32: EXCEPTION: Uncaught (in promise): Error: CUSTOM EXCEPTION", - null + "" ] }, { @@ -84,7 +84,7 @@ describe("iOSLogFilter", () => { null, null, null, - null + "" ] } ]; diff --git a/test/services/project-data-service.ts b/test/services/project-data-service.ts index f68c29d449..71f08eff31 100644 --- a/test/services/project-data-service.ts +++ b/test/services/project-data-service.ts @@ -53,6 +53,8 @@ const createTestInjector = (readTextData?: string): IInjector => { testInjector.register("projectDataService", ProjectDataService); + testInjector.register("injector", testInjector); + return testInjector; }; diff --git a/test/stubs.ts b/test/stubs.ts index 2799642aec..d96520608e 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -376,6 +376,8 @@ export class ProjectDataService implements IProjectDataService { removeNSProperty(propertyName: string): void { } removeDependency(dependencyName: string): void { } + + getProjectData(projectDir: string): IProjectData { return null; } } export class ProjectHelperStub implements IProjectHelper { @@ -495,7 +497,11 @@ export class DebugServiceStub extends EventEmitter implements IPlatformDebugServ } export class LiveSyncServiceStub implements ILiveSyncService { - public async liveSync(platform: string, projectData: IProjectData, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise { + public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { + return; + } + + public async stopLiveSync(projectDir: string): Promise { return; } } @@ -617,6 +623,10 @@ export class PlatformServiceStub extends EventEmitter implements IPlatformServic return []; } + public saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void { + return; + } + public async removePlatforms(platforms: string[]): Promise { } @@ -665,6 +675,10 @@ export class PlatformServiceStub extends EventEmitter implements IPlatformServic } + isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { + return true; + } + public getLatestApplicationPackageForDevice(platformData: IPlatformData): IApplicationPackage { return null; }