diff --git a/.vscode/launch.json b/.vscode/launch.json index a361c5e51d..acf4559e49 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ { "type": "node", "request": "launch", - "cwd": "${workspaceRoot}/scratch", + "cwd": "${workspaceRoot}", "sourceMaps": true, "name": "Launch CLI (Node 6+)", "program": "${workspaceRoot}/lib/nativescript-cli.js", diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 8881700a7a..9c2dc19f67 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -104,10 +104,9 @@ $injector.requireCommand("platform|clean", "./commands/platform-clean"); $injector.requireCommand("livesync", "./commands/livesync"); $injector.require("usbLiveSyncService", "./services/livesync/livesync-service"); // The name is used in https://github.com/NativeScript/nativescript-dev-typescript -$injector.require("iosPlatformLiveSyncServiceLocator", "./services/livesync/ios-platform-livesync-service"); $injector.require("iosLiveSyncServiceLocator", "./services/livesync/ios-device-livesync-service"); -$injector.require("androidPlatformLiveSyncServiceLocator", "./services/livesync/android-platform-livesync-service"); $injector.require("androidLiveSyncServiceLocator", "./services/livesync/android-device-livesync-service"); +$injector.require("platformLiveSyncService", "./services/livesync/platform-livesync-service"); $injector.require("sysInfo", "./sys-info"); @@ -123,3 +122,4 @@ $injector.requireCommand("post-install-cli", "./commands/post-install"); $injector.requireCommand("update", "./commands/update"); $injector.require("iOSLogFilter", "./services/ios-log-filter"); +$injector.require("projectChangesService", "./services/project-changes-service"); diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index 079b6b7448..1f8f061193 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -65,11 +65,12 @@ export class PublishIOS implements ICommand { }; this.$logger.info("Building .ipa with the selected mobile provision and/or certificate."); // This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here. - this.$platformService.buildPlatform(platform, iOSBuildConfig, true).wait(); + this.$platformService.preparePlatform(platform).wait(); + this.$platformService.buildPlatform(platform, iOSBuildConfig).wait(); ipaFilePath = this.$platformService.lastOutputPath(platform, { isForDevice: iOSBuildConfig.buildForDevice }); } else { this.$logger.info("No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission."); - this.$platformService.preparePlatform(platform, true).wait(); + this.$platformService.preparePlatform(platform).wait(); let platformData = this.$platformsData.getPlatformData(platform); let iOSProjectService = platformData.platformProjectService; diff --git a/lib/commands/build.ts b/lib/commands/build.ts index 29b430e847..def9affbca 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -5,7 +5,9 @@ export class BuildCommandBase { executeCore(args: string[]): IFuture { return (() => { let platform = args[0].toLowerCase(); - this.$platformService.buildPlatform(platform, null, true).wait(); + this.$platformService.preparePlatform(platform).wait(); + this.$options.clean = true; + this.$platformService.buildPlatform(platform).wait(); if(this.$options.copyTo) { this.$platformService.copyLastOutput(platform, this.$options.copyTo, {isForDevice: this.$options.forDevice}); } diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index f38ee4548f..8b6639f6cb 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -7,35 +7,32 @@ private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $config: IConfiguration, private $usbLiveSyncService: ILiveSyncService, + private $platformService: IPlatformService, protected $options: IOptions) { } execute(args: string[]): IFuture { - - if (this.$options.watch) { - this.$options.rebuild = false; + if (this.$options.start) { + return this.debugService.debug(); } - if (!this.$options.rebuild && !this.$options.start) { - this.$config.debugLivesync = true; - let applicationReloadAction = (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): IFuture => { - return (() => { - let projectData: IProjectData = this.$injector.resolve("projectData"); - - this.debugService.debugStop().wait(); + this.$platformService.deployPlatform(this.$devicesService.platform).wait(); + this.$config.debugLivesync = true; + let applicationReloadAction = (deviceAppData: Mobile.IDeviceAppData): IFuture => { + return (() => { + let projectData: IProjectData = this.$injector.resolve("projectData"); - let applicationId = deviceAppData.appIdentifier; - if (deviceAppData.device.isEmulator && deviceAppData.platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) { - applicationId = projectData.projectName; - } - deviceAppData.device.applicationManager.stopApplication(applicationId).wait(); + this.debugService.debugStop().wait(); - this.debugService.debug().wait(); - }).future()(); - }; + let applicationId = deviceAppData.appIdentifier; + if (deviceAppData.device.isEmulator && deviceAppData.platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) { + applicationId = projectData.projectName; + } + deviceAppData.device.applicationManager.stopApplication(applicationId).wait(); - return this.$usbLiveSyncService.liveSync(this.$devicesService.platform, applicationReloadAction); - } - return this.debugService.debug(); + this.debugService.debug().wait(); + }).future()(); + }; + return this.$usbLiveSyncService.liveSync(this.$devicesService.platform, applicationReloadAction); } allowedParameters: ICommandParameter[] = []; @@ -69,8 +66,9 @@ export class DebugIOSCommand extends DebugPlatformCommand { $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $config: IConfiguration, $usbLiveSyncService: ILiveSyncService, + $platformService: IPlatformService, $options: IOptions) { - super($iOSDebugService, $devicesService, $injector, $logger, $childProcess, $devicePlatformsConstants, $config, $usbLiveSyncService, $options); + super($iOSDebugService, $devicesService, $injector, $logger, $childProcess, $devicePlatformsConstants, $config, $usbLiveSyncService, $platformService, $options); } } $injector.registerCommand("debug|ios", DebugIOSCommand); @@ -84,8 +82,9 @@ export class DebugAndroidCommand extends DebugPlatformCommand { $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $config: IConfiguration, $usbLiveSyncService: ILiveSyncService, + $platformService: IPlatformService, $options: IOptions) { - super($androidDebugService, $devicesService, $injector, $logger, $childProcess, $devicePlatformsConstants, $config, $usbLiveSyncService, $options); + super($androidDebugService, $devicesService, $injector, $logger, $childProcess, $devicePlatformsConstants, $config, $usbLiveSyncService, $platformService, $options); } } $injector.registerCommand("debug|android", DebugAndroidCommand); diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index 1681771df8..adf9aad7f0 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -6,7 +6,7 @@ export class DeployOnDeviceCommand implements ICommand { private $mobileHelper: Mobile.IMobileHelper) { } execute(args: string[]): IFuture { - return this.$platformService.deployPlatform(args[0]); + return this.$platformService.deployPlatform(args[0], true); } public canExecute(args: string[]): IFuture { diff --git a/lib/commands/livesync.ts b/lib/commands/livesync.ts index 53b3d8be19..073a5e6d0f 100644 --- a/lib/commands/livesync.ts +++ b/lib/commands/livesync.ts @@ -3,9 +3,11 @@ export class LivesyncCommand implements ICommand { private $usbLiveSyncService: ILiveSyncService, private $mobileHelper: Mobile.IMobileHelper, private $options: IOptions, + private $platformService: IPlatformService, private $errors: IErrors) { } public execute(args: string[]): IFuture { + this.$platformService.deployPlatform(args[0]).wait(); return this.$usbLiveSyncService.liveSync(args[0]); } diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index 913b5a502f..660803f796 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -5,7 +5,7 @@ export class PrepareCommand implements ICommand { execute(args: string[]): IFuture { return (() => { - this.$platformService.preparePlatform(args[0], true).wait(); + this.$platformService.preparePlatform(args[0]).wait(); }).future()(); } diff --git a/lib/commands/run.ts b/lib/commands/run.ts index dfdd8a12a5..5dd2f0b810 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -4,12 +4,11 @@ export class RunCommandBase { protected $options: IOptions) { } public executeCore(args: string[]): IFuture { - if (this.$options.watch) { - this.$platformService.deployPlatform(args[0]).wait(); - return this.$usbLiveSyncService.liveSync(args[0]); - } else { + this.$platformService.deployPlatform(args[0]).wait(); + if (this.$options.release) { return this.$platformService.runPlatform(args[0]); } + return this.$usbLiveSyncService.liveSync(args[0]); } } diff --git a/lib/declarations.ts b/lib/declarations.ts index 1cfc46eb39..6ea862903b 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -51,14 +51,13 @@ interface IOpener { } interface ILiveSyncService { - liveSync(platform: string, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture; - forceExecuteFullSync: boolean; + liveSync(platform: string, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => IFuture): IFuture; } interface IPlatformLiveSyncService { fullSync(postAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture; partialSync(event: string, filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): void; - refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): IFuture; + refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], isFullSync: boolean): IFuture; } interface IOptions extends ICommonOptions { @@ -93,10 +92,10 @@ interface IOptions extends ICommonOptions { sdk: string; tnsModulesVersion: string; teamId: string; - rebuild: boolean; syncAllFiles: boolean; liveEdit: boolean; chrome: boolean; + clean: boolean; } interface IInitService { diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 145afb0dc2..3db446dc6c 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -27,11 +27,78 @@ interface IPlatformService { removePlatforms(platforms: string[]): void; updatePlatforms(platforms: string[]): IFuture; - preparePlatform(platform: string, force?: boolean, skipModulesAndResources?: boolean): IFuture; - buildPlatform(platform: string, buildConfig?: IBuildConfig, forceBuild?: boolean): IFuture; - deployPlatform(platform: string): IFuture; + + /** + * Ensures that the specified platform and its dependencies are installed. + * When there are changes to be prepared, it prepares the native project for the specified platform. + * When finishes, prepare saves the .nsprepareinfo file in platform folder. + * This file contains information about current project configuration and allows skipping unnecessary build, deploy and livesync steps. + * @param {string} platform The platform to be prepared. + * @returns {boolean} true indicates that the platform was prepared. + */ + preparePlatform(platform: string, changesInfo?: IProjectChangesInfo): IFuture; + + /** + * Determines whether a build is necessary. A build is necessary when one of the following is true: + * - there is no previous build. + * - the .nsbuildinfo file in product folder points to an old prepare. + * @param {string} platform The platform to build. + * @param {IBuildConfig} buildConfig Indicates whether the build is for device or emulator. + * @returns {boolean} true indicates that the platform should be build. + */ + shouldBuild(platform: string, buildConfig?: IBuildConfig): IFuture; + + /** + * Builds the native project for the specified platform for device or emulator. + * When finishes, build saves the .nsbuildinfo file in platform product folder. + * This file points to the prepare that was used to build the project and allows skipping unnecessary builds and deploys. + * @param {string} platform The platform to build. + * @param {IBuildConfig} buildConfig Indicates whether the build is for device or emulator. + * @returns {void} + */ + buildPlatform(platform: string, buildConfig?: IBuildConfig): IFuture; + + /** + * Determines whether installation is necessary. It is necessary when one of the following is true: + * - the application is not installed. + * - 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. + * @returns {boolean} true indicates that the application should be installed. + */ + shouldInstall(device: Mobile.IDevice): boolean; + + /** + * Installs the application on specified device. + * When finishes, saves .nsbuildinfo in application root folder to indicate the prepare that was used to build the app. + * * .nsbuildinfo is not persisted when building for release. + * @param {Mobile.IDevice} device The device where the application should be installed. + * @returns {void} + */ + installApplication(device: Mobile.IDevice): IFuture; + + /** + * Executes prepare, build and installOnPlatform when necessary to ensure that the latest version of the app is installed on specified platform. + * - When --clean option is specified it builds the app on every change. If not, build is executed only when there are native changes. + * @param {string} platform The platform to deploy. + * @param {boolean} forceInstall When true, installs the application unconditionally. + * @returns {void} + */ + deployPlatform(platform: string, forceInstall?: boolean): IFuture; + + /** + * Runs the application on specified platform. Assumes that the application is already build and installed. Fails if this is not true. + * @param {string} platform The platform where to start the application. + * @returns {void} + */ runPlatform(platform: string): IFuture; + + /** + * The emulate command. In addition to `run --emulator` command, it handles the `--available-devices` option to show the available devices. + * @param {string} platform The platform to emulate. + * @returns {void} + */ emulatePlatform(platform: string): IFuture; + cleanDestinationApp(platform: string): IFuture; validatePlatformInstalled(platform: string): void; validatePlatform(platform: string): void; @@ -60,7 +127,14 @@ interface IPlatformService { copyLastOutput(platform: string, targetPath: string, settings: {isForDevice: boolean}): void; lastOutputPath(platform: string, settings: { isForDevice: boolean }): string; - ensurePlatformInstalled(platform: string): IFuture; + + /** + * Reads contents of a file on device. + * @param {Mobile.IDevice} device The device to read from. + * @param {string} deviceFilePath The file path. + * @returns {string} The contents of the file or null when there is no such file. + */ + readFile(device: Mobile.IDevice, deviceFilePath: string): IFuture; } interface IPlatformData { @@ -97,4 +171,9 @@ interface INodeModulesBuilder { interface INodeModulesDependenciesBuilder { getProductionDependencies(projectPath: string): void; +} + +interface IBuildInfo { + prepareTime: string; + buildTime: string; } \ No newline at end of file diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts new file mode 100644 index 0000000000..dca107afd0 --- /dev/null +++ b/lib/definitions/project-changes.d.ts @@ -0,0 +1,26 @@ +interface IPrepareInfo { + time: string; + bundle: boolean; + release: boolean; + changesRequireBuild: boolean; + changesRequireBuildTime: string; +} + +interface IProjectChangesInfo { + appFilesChanged: boolean; + appResourcesChanged: boolean; + modulesChanged: boolean; + configChanged: boolean; + packageChanged: boolean; + nativeChanged: boolean; + hasChanges: boolean; + changesRequireBuild: boolean; +} + +interface IProjectChangesService { + checkForChanges(platform: string): IProjectChangesInfo; + getPrepareInfo(platform: string): IPrepareInfo; + savePrepareInfo(platform: string): void; + getPrepareInfoFilePath(platform: string): string; + currentChanges: IProjectChangesInfo; +} \ No newline at end of file diff --git a/lib/options.ts b/lib/options.ts index 37dece04bf..8035c78414 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -38,10 +38,11 @@ export class Options extends commonOptionsLibPath.OptionsBase { bundle: { type: OptionType.Boolean }, all: { type: OptionType.Boolean }, teamId: { type: OptionType.String }, - rebuild: { type: OptionType.Boolean, default: true }, syncAllFiles: { type: OptionType.Boolean }, liveEdit: { type: OptionType.Boolean }, - chrome: { type: OptionType.Boolean } + chrome: { type: OptionType.Boolean }, + clean: { type: OptionType.Boolean }, + watch: { type: OptionType.Boolean, default: true } }, path.join($hostInfo.isWindows ? process.env.AppData : path.join(osenv.home(), ".local/share"), ".nativescript-cli"), $errors, $staticConfig); diff --git a/lib/providers/livesync-provider.ts b/lib/providers/livesync-provider.ts index 0e9ec4dfd5..2210b16105 100644 --- a/lib/providers/livesync-provider.ts +++ b/lib/providers/livesync-provider.ts @@ -3,9 +3,7 @@ import * as temp from "temp"; export class LiveSyncProvider implements ILiveSyncProvider { constructor(private $androidLiveSyncServiceLocator: {factory: Function}, - private $androidPlatformLiveSyncServiceLocator: {factory: Function}, private $iosLiveSyncServiceLocator: {factory: Function}, - private $iosPlatformLiveSyncServiceLocator: {factory: Function}, private $platformService: IPlatformService, private $platformsData: IPlatformsData, private $logger: ILogger, @@ -35,26 +33,6 @@ export class LiveSyncProvider implements ILiveSyncProvider { }; } - private platformSpecificLiveSyncServicesCache: IDictionary = {}; - public get platformSpecificLiveSyncServices(): IDictionary { - return { - android: (_liveSyncData: ILiveSyncData, $injector: IInjector) => { - if(!this.platformSpecificLiveSyncServicesCache[this.$devicePlatformsConstants.Android]) { - this.platformSpecificLiveSyncServicesCache[this.$devicePlatformsConstants.Android] = $injector.resolve(this.$androidPlatformLiveSyncServiceLocator.factory, { _liveSyncData: _liveSyncData }); - } - - return this.platformSpecificLiveSyncServicesCache[this.$devicePlatformsConstants.Android]; - }, - ios: (_liveSyncData: ILiveSyncData, $injector: IInjector) => { - if(!this.platformSpecificLiveSyncServicesCache[this.$devicePlatformsConstants.iOS]) { - this.platformSpecificLiveSyncServicesCache[this.$devicePlatformsConstants.iOS] = $injector.resolve(this.$iosPlatformLiveSyncServiceLocator.factory, { _liveSyncData: _liveSyncData }); - } - - return this.platformSpecificLiveSyncServicesCache[this.$devicePlatformsConstants.iOS]; - } - }; - } - public buildForDevice(device: Mobile.IDevice): IFuture { return (() => { this.$platformService.buildPlatform(device.deviceInfo.platform, {buildForDevice: !device.isEmulator}).wait(); @@ -91,7 +69,8 @@ export class LiveSyncProvider implements ILiveSyncProvider { if (this.$options.syncAllFiles) { this.$childProcess.spawnFromEvent("zip", [ "-r", "-0", tempZip, "app" ], "close", { cwd: path.dirname(projectFilesPath) }).wait(); } else { - this.$childProcess.spawnFromEvent("zip", [ "-r", "-0", tempZip, "app", "-x", "app/tns_modules/*" ], "close", { cwd: path.dirname(projectFilesPath) }).wait(); + this.$logger.info("Skipping node_modules folder! Use the syncAllFiles option to sync files from this folder."); + this.$childProcess.spawnFromEvent("zip", [ "-r", "-0", tempZip, "app", "-x", "app/tns_modules/*" ], "close", { cwd: path.dirname(projectFilesPath) }).wait(); } deviceAppData.device.fileSystem.transferFiles(deviceAppData, [{ diff --git a/lib/providers/project-files-provider.ts b/lib/providers/project-files-provider.ts index 8355499f1b..410295b761 100644 --- a/lib/providers/project-files-provider.ts +++ b/lib/providers/project-files-provider.ts @@ -15,9 +15,14 @@ export class ProjectFilesProvider extends ProjectFilesProviderBase { public mapFilePath(filePath: string, platform: string): string { let platformData = this.$platformsData.getPlatformData(platform.toLowerCase()); - let projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); let parsedFilePath = this.getPreparedFilePath(filePath); - let mappedFilePath = path.join(projectFilesPath, path.relative(path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), parsedFilePath)); + let mappedFilePath = ""; + if (parsedFilePath.indexOf(constants.NODE_MODULES_FOLDER_NAME) > -1) { + let relativePath = path.relative(path.join(this.$projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME), parsedFilePath); + mappedFilePath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, constants.TNS_MODULES_FOLDER_NAME, relativePath); + } else { + mappedFilePath = path.join(platformData.appDestinationDirectoryPath, path.relative(this.$projectData.projectDir, parsedFilePath)); + } let appResourcesDirectoryPath = path.join(constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME); let platformSpecificAppResourcesDirectoryPath = path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName); diff --git a/lib/services/android-debug-service.ts b/lib/services/android-debug-service.ts index 42e6ecf262..9d5a667224 100644 --- a/lib/services/android-debug-service.ts +++ b/lib/services/android-debug-service.ts @@ -39,7 +39,6 @@ class AndroidDebugService implements IDebugService { private debugOnEmulator(): IFuture { return (() => { - this.$platformService.deployPlatform(this.platform).wait(); // Assure we've detected the emulator as device // For example in case deployOnEmulator had stated new emulator instance // we need some time to detect it. Let's force detection. @@ -109,9 +108,6 @@ class AndroidDebugService implements IDebugService { if (!this.$options.start && !this.$options.emulator && !this.$options.getPort) { let cachedDeviceOption = this.$options.forDevice; this.$options.forDevice = true; - if (this.$options.rebuild) { - this.$platformService.buildPlatform(this.platform).wait(); - } this.$options.forDevice = !!cachedDeviceOption; let platformData = this.$platformsData.getPlatformData(this.platform); diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 778a9d472b..ef188a30eb 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -5,7 +5,6 @@ import * as constants from "../constants"; import * as semver from "semver"; import * as projectServiceBaseLib from "./platform-project-service-base"; import { DeviceAndroidDebugBridge } from "../common/mobile/android/device-android-debug-bridge"; -import { AndroidDeviceHashService } from "../common/mobile/android/android-device-hash-service"; import { EOL } from "os"; export class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { @@ -424,17 +423,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return (() => { let adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier }); let deviceRootPath = `/data/local/tmp/${this.$projectData.projectId}`; - adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, "fullsync"), - this.$mobileHelper.buildDevicePath(deviceRootPath, "sync"), - this.$mobileHelper.buildDevicePath(deviceRootPath, "removedsync")]).wait(); - - let projectFilesManager = this.$injector.resolve("projectFilesManager"); // We need to resolve projectFilesManager here due to cyclic dependency - let devicesService: Mobile.IDevicesService = this.$injector.resolve("devicesService"); - let device = _.find(devicesService.getDevicesForPlatform(this.platformData.normalizedPlatformName), d => d.deviceInfo.identifier === deviceIdentifier); - let deviceAppData = this.$deviceAppDataFactory.create(this.$projectData.projectId, this.platformData.normalizedPlatformName, device); - let localToDevicePaths = projectFilesManager.createLocalToDevicePaths(deviceAppData, path.join(this.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); - let deviceHashService = this.$injector.resolve(AndroidDeviceHashService, { adb: adb, appIdentifier: this.$projectData.projectId }); - deviceHashService.uploadHashFileToDevice(localToDevicePaths).wait(); + adb.executeShellCommand(["rm", "-rf", deviceRootPath]).wait(); + adb.executeShellCommand(["mkdir", deviceRootPath]); }).future()(); } diff --git a/lib/services/ios-debug-service.ts b/lib/services/ios-debug-service.ts index 693fb95bb6..0bcb558102 100644 --- a/lib/services/ios-debug-service.ts +++ b/lib/services/ios-debug-service.ts @@ -105,16 +105,13 @@ class IOSDebugService implements IDebugService { private emulatorDebugBrk(shouldBreak?: boolean): IFuture { return (() => { let platformData = this.$platformsData.getPlatformData(this.platform); - if (this.$options.rebuild) { - this.$platformService.buildPlatform(this.platform).wait(); - } let emulatorPackage = this.$platformService.getLatestApplicationPackageForEmulator(platformData); let args = shouldBreak ? "--nativescript-debug-brk" : "--nativescript-debug-start"; let child_process = this.$iOSEmulatorServices.runApplicationOnEmulator(emulatorPackage.packageName, { waitForDebugger: true, captureStdin: true, args: args, appId: this.$projectData.projectId, - skipInstall: this.$config.debugLivesync + skipInstall: true }).wait(); let lineStream = byline(child_process.stdout); this._childProcess = child_process; @@ -157,12 +154,7 @@ class IOSDebugService implements IDebugService { return this.emulatorDebugBrk(shouldBreak).wait(); } // we intentionally do not wait on this here, because if we did, we'd miss the AppLaunching notification - let action: IFuture; - if (this.$config.debugLivesync) { - action = this.$platformService.runPlatform(this.platform); - } else { - action = this.$platformService.deployPlatform(this.platform); - } + let action = this.$platformService.runPlatform(this.platform); this.debugBrkCore(device, shouldBreak).wait(); action.wait(); }).future()()).wait(); diff --git a/lib/services/livesync/android-platform-livesync-service.ts b/lib/services/livesync/android-platform-livesync-service.ts deleted file mode 100644 index 8f87dc5d0e..0000000000 --- a/lib/services/livesync/android-platform-livesync-service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {PlatformLiveSyncServiceBase} from "./platform-livesync-service-base"; - -class AndroidPlatformLiveSyncService extends PlatformLiveSyncServiceBase { - constructor(_liveSyncData: ILiveSyncData, - protected $devicesService: Mobile.IDevicesService, - protected $mobileHelper: Mobile.IMobileHelper, - protected $logger: ILogger, - protected $options: IOptions, - protected $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, - protected $fs: IFileSystem, - protected $injector: IInjector, - protected $projectFilesManager: IProjectFilesManager, - protected $projectFilesProvider: IProjectFilesProvider, - protected $platformService: IPlatformService, - protected $liveSyncProvider: ILiveSyncProvider) { - super(_liveSyncData, $devicesService, $mobileHelper, $logger, $options, $deviceAppDataFactory, $fs, $injector, $projectFilesManager, $projectFilesProvider, $platformService, $liveSyncProvider); - } - - public fullSync(postAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture { - return (() => { - let appIdentifier = this.liveSyncData.appIdentifier; - let platform = this.liveSyncData.platform; - let projectFilesPath = this.liveSyncData.projectFilesPath; - let canExecute = this.getCanExecuteAction(platform, appIdentifier); - let action = (device: Mobile.IDevice): IFuture => { - return (() => { - let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(platform, device); - let deviceAppData = this.$deviceAppDataFactory.create(appIdentifier, this.$mobileHelper.normalizePlatformName(platform), device); - - deviceLiveSyncService.beforeLiveSyncAction(deviceAppData).wait();; - - let installed = this.tryInstallApplication(device, deviceAppData).wait(); - let localToDevicePaths = this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, this.liveSyncData.excludedProjectDirsAndFiles); - let afterSyncAction: () => IFuture; - - if (installed) { - deviceLiveSyncService.afterInstallApplicationAction(deviceAppData, localToDevicePaths).wait(); - afterSyncAction = () => device.applicationManager.tryStartApplication(deviceAppData.appIdentifier); - } else { - this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, true).wait(); - afterSyncAction = () => this.refreshApplication(deviceAppData, localToDevicePaths); - } - - if (postAction) { - this.finishLivesync(deviceAppData).wait(); - return postAction(deviceAppData, localToDevicePaths).wait(); - } - - afterSyncAction().wait(); - this.finishLivesync(deviceAppData).wait(); - }).future()(); - }; - this.$devicesService.execute(action, canExecute).wait(); - }).future()(); - } -} - -$injector.register("androidPlatformLiveSyncServiceLocator", {factory: AndroidPlatformLiveSyncService}); diff --git a/lib/services/livesync/ios-platform-livesync-service.ts b/lib/services/livesync/ios-platform-livesync-service.ts deleted file mode 100644 index 65ac784869..0000000000 --- a/lib/services/livesync/ios-platform-livesync-service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {PlatformLiveSyncServiceBase} from "./platform-livesync-service-base"; - -class IOSPlatformLiveSyncService extends PlatformLiveSyncServiceBase { - constructor(_liveSyncData: ILiveSyncData, - protected $devicesService: Mobile.IDevicesService, - protected $mobileHelper: Mobile.IMobileHelper, - protected $logger: ILogger, - protected $options: IOptions, - protected $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, - protected $fs: IFileSystem, - protected $injector: IInjector, - protected $projectFilesManager: IProjectFilesManager, - protected $projectFilesProvider: IProjectFilesProvider, - protected $platformService: IPlatformService, - protected $liveSyncProvider: ILiveSyncProvider) { - super(_liveSyncData, $devicesService, $mobileHelper, $logger, $options, $deviceAppDataFactory, $fs, $injector, $projectFilesManager, $projectFilesProvider, $platformService, $liveSyncProvider); - } - - public fullSync(postAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture { - return (() => { - let appIdentifier = this.liveSyncData.appIdentifier; - let platform = this.liveSyncData.platform; - let projectFilesPath = this.liveSyncData.projectFilesPath; - let canExecute = this.getCanExecuteAction(platform, appIdentifier); - - let action = (device: Mobile.IDevice): IFuture => { - return (() => { - let deviceAppData = this.$deviceAppDataFactory.create(appIdentifier, this.$mobileHelper.normalizePlatformName(platform), device); - let installed = this.tryInstallApplication(device, deviceAppData).wait(); - let localToDevicePaths = this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, this.liveSyncData.excludedProjectDirsAndFiles); - let afterSyncAction: () => IFuture; - - this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, true).wait(); - - if(installed) { - afterSyncAction = () => device.applicationManager.tryStartApplication(deviceAppData.appIdentifier); - } else { - afterSyncAction = () => this.refreshApplication(deviceAppData, localToDevicePaths); - } - - if (postAction) { - this.finishLivesync(deviceAppData).wait(); - return postAction(deviceAppData, localToDevicePaths).wait(); - } - - afterSyncAction().wait(); - this.finishLivesync(deviceAppData).wait(); - }).future()(); - }; - this.$devicesService.execute(action, canExecute).wait(); - }).future()(); - } -} - -$injector.register("iosPlatformLiveSyncServiceLocator", {factory: IOSPlatformLiveSyncService}); diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index d2d14216ca..66dcdee55a 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -3,11 +3,9 @@ import * as helpers from "../../common/helpers"; import * as path from "path"; import * as semver from "semver"; import * as fiberBootstrap from "../../common/fiber-bootstrap"; - -let gaze = require("gaze"); +let choki = require("chokidar"); class LiveSyncService implements ILiveSyncService { - public forceExecuteFullSync = false; private _isInitialized = false; constructor(private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, @@ -49,7 +47,7 @@ class LiveSyncService implements ILiveSyncService { return this._isInitialized; } - public liveSync(platform: string, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture { + public liveSync(platform: string, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => IFuture): IFuture { return (() => { let liveSyncData: ILiveSyncData[] = []; if (platform) { @@ -80,7 +78,6 @@ class LiveSyncService implements ILiveSyncService { private prepareLiveSyncData(platform: string): ILiveSyncData { platform = platform || this.$devicesService.platform; - this.$platformService.preparePlatform(platform.toLowerCase()).wait(); let platformData = this.$platformsData.getPlatformData(platform.toLowerCase()); if (this.$mobileHelper.isAndroidPlatform(platform)) { this.ensureAndroidFrameworkVersion(platformData).wait(); @@ -89,31 +86,26 @@ class LiveSyncService implements ILiveSyncService { platform: platform, appIdentifier: this.$projectData.projectId, projectFilesPath: path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME), - syncWorkingDirectory: path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), - excludedProjectDirsAndFiles: this.$options.release ? constants.LIVESYNC_EXCLUDED_FILE_PATTERNS : [], - forceExecuteFullSync: this.forceExecuteFullSync + syncWorkingDirectory: this.$projectData.projectDir, + excludedProjectDirsAndFiles: this.$options.release ? constants.LIVESYNC_EXCLUDED_FILE_PATTERNS : [] }; return liveSyncData; } - private resolvePlatformLiveSyncBaseService(platform: string, liveSyncData: ILiveSyncData): IPlatformLiveSyncService { - return this.$injector.resolve(this.$liveSyncProvider.platformSpecificLiveSyncServices[platform.toLowerCase()], { _liveSyncData: liveSyncData }); - } - @helpers.hook('livesync') private liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture { return (() => { let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => void)[] = []; _.each(liveSyncData, (dataItem) => { - let service = this.resolvePlatformLiveSyncBaseService(dataItem.platform, dataItem); + let service: IPlatformLiveSyncService = this.$injector.resolve("platformLiveSyncService", { _liveSyncData: dataItem }); watchForChangeActions.push((event: string, filePath: string, dispatcher: IFutureDispatcher) => { service.partialSync(event, filePath, dispatcher, applicationReloadAction); }); service.fullSync(applicationReloadAction).wait(); }); - if(this.$options.watch) { + if(this.$options.watch && !this.$options.justlaunch) { this.$hooksService.executeBeforeHooks('watch').wait(); this.partialSync(liveSyncData[0].syncWorkingDirectory, watchForChangeActions); } @@ -122,25 +114,26 @@ class LiveSyncService implements ILiveSyncService { private partialSync(syncWorkingDirectory: string, onChangedActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => void )[]): void { let that = this; - - let gazeWatcher = gaze("**/*", { cwd: syncWorkingDirectory, follow: true }, function (err: any, watcher: any) { - this.on('all', (event: string, filePath: string) => { - fiberBootstrap.run(() => { - that.$dispatcher.dispatch(() => (() => { - try { - for (let i = 0; i < onChangedActions.length; i++) { - 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."); + let pattern = ["app", "package.json", "node_modules"]; + let watcher = choki.watch(pattern, { ignoreInitial: true, cwd: syncWorkingDirectory }).on("all", (event: string, filePath: string) => { + fiberBootstrap.run(() => { + that.$dispatcher.dispatch(() => (() => { + try { + filePath = path.join(syncWorkingDirectory, filePath); + for (let i = 0; i < onChangedActions.length; i++) { + onChangedActions[i](event, filePath, that.$dispatcher); } - }).future()()); - }); + } 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."); + } + }).future()()); }); }); - this.$processService.attachToProcessExitSignals(this, () => gazeWatcher.close()); + this.$processService.attachToProcessExitSignals(this, () => { + watcher.close(pattern); + }); this.$dispatcher.run(); } } diff --git a/lib/services/livesync/platform-livesync-service-base.ts b/lib/services/livesync/platform-livesync-service.ts similarity index 53% rename from lib/services/livesync/platform-livesync-service-base.ts rename to lib/services/livesync/platform-livesync-service.ts index 401f48c593..f8e79852ec 100644 --- a/lib/services/livesync/platform-livesync-service-base.ts +++ b/lib/services/livesync/platform-livesync-service.ts @@ -1,68 +1,74 @@ import syncBatchLib = require("../../common/services/livesync/sync-batch"); -import * as shell from "shelljs"; import * as path from "path"; -import * as temp from "temp"; import * as minimatch from "minimatch"; -import * as constants from "../../common/constants"; import * as util from "util"; +const livesyncInfoFileName = ".nslivesyncinfo"; + export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { - private showFullLiveSyncInformation: boolean = false; - private fileHashes: IDictionary; private batch: IDictionary = Object.create(null); private livesyncData: IDictionary = Object.create(null); protected liveSyncData: ILiveSyncData; constructor(_liveSyncData: ILiveSyncData, - protected $devicesService: Mobile.IDevicesService, - protected $mobileHelper: Mobile.IMobileHelper, - protected $logger: ILogger, - protected $options: IOptions, - protected $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, - protected $fs: IFileSystem, - protected $injector: IInjector, - protected $projectFilesManager: IProjectFilesManager, - protected $projectFilesProvider: IProjectFilesProvider, - protected $platformService: IPlatformService, - protected $liveSyncProvider: ILiveSyncProvider) { + private $devicesService: Mobile.IDevicesService, + private $mobileHelper: Mobile.IMobileHelper, + private $logger: ILogger, + private $options: IOptions, + private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, + private $fs: IFileSystem, + private $injector: IInjector, + private $projectFilesManager: IProjectFilesManager, + private $projectFilesProvider: IProjectFilesProvider, + private $platformService: IPlatformService, + private $platformsData: IPlatformsData, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $projectData: IProjectData, + private $projectChangesService: IProjectChangesService, + private $liveSyncProvider: ILiveSyncProvider) { this.liveSyncData = _liveSyncData; - this.fileHashes = Object.create(null); } - public abstract fullSync(postAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture; + public fullSync(postAction?: (deviceAppData: Mobile.IDeviceAppData) => IFuture): IFuture { + return (() => { + let appIdentifier = this.liveSyncData.appIdentifier; + let platform = this.liveSyncData.platform; + let projectFilesPath = this.liveSyncData.projectFilesPath; + let canExecute = this.getCanExecuteAction(platform, appIdentifier); + let action = (device: Mobile.IDevice): IFuture => { + return (() => { + let deviceAppData = this.$deviceAppDataFactory.create(appIdentifier, this.$mobileHelper.normalizePlatformName(platform), device); + let localToDevicePaths: Mobile.ILocalToDevicePathData[] = null; + if (this.shouldTransferAllFiles(platform, deviceAppData)) { + localToDevicePaths = this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, this.liveSyncData.excludedProjectDirsAndFiles); + this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, true).wait(); + device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(platform), this.getLiveSyncInfoFilePath(deviceAppData)).wait(); + } + + if (postAction) { + this.finishLivesync(deviceAppData).wait(); + return postAction(deviceAppData).wait(); + } + + this.refreshApplication(deviceAppData, localToDevicePaths, true).wait(); + this.finishLivesync(deviceAppData).wait(); + }).future()(); + }; + this.$devicesService.execute(action, canExecute).wait(); + }).future()(); + } public partialSync(event: string, filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): void { - if (filePath.indexOf(constants.APP_RESOURCES_FOLDER_NAME) !== -1) { - this.$logger.warn(`Skipping livesync for changed file ${filePath}. This change requires a full build to update your application. `.yellow.bold); - return; - } - - let fileHash = this.$fs.exists(filePath) && this.$fs.getFsStats(filePath).isFile() ? this.$fs.getFileShasum(filePath).wait() : ""; - if (fileHash === this.fileHashes[filePath]) { - this.$logger.trace(`Skipping livesync for ${filePath} file with ${fileHash} hash.`); - return; - } - - this.$logger.trace(`Adding ${filePath} file with ${fileHash} hash.`); - this.fileHashes[filePath] = fileHash; - 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; } - let mappedFilePath = this.$projectFilesProvider.mapFilePath(filePath, this.liveSyncData.platform); - this.$logger.trace(`Syncing filePath ${filePath}, mappedFilePath is ${mappedFilePath}`); - if (!mappedFilePath) { - this.$logger.warn(`Unable to sync ${filePath}.`); - return; - } - if (event === "added" || event === "changed" || event === "renamed") { - this.batchSync(mappedFilePath, dispatcher, afterFileSyncAction); - } else if (event === "deleted") { - this.fileHashes = (_.omit(this.fileHashes, filePath)); - this.syncRemovedFile(mappedFilePath, afterFileSyncAction).wait(); + if (event === "add" || event === "change") { + this.batchSync(filePath, dispatcher, afterFileSyncAction); + } else if (event === "unlink") { + this.syncRemovedFile(filePath, afterFileSyncAction).wait(); } } @@ -74,54 +80,30 @@ export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncSe return isTheSamePlatformAction; } - protected tryInstallApplication(device: Mobile.IDevice, deviceAppData: Mobile.IDeviceAppData): IFuture { - return (() => { - device.applicationManager.checkForApplicationUpdates().wait(); - - let appIdentifier = this.liveSyncData.appIdentifier; - if (!device.applicationManager.isApplicationInstalled(appIdentifier).wait()) { - this.$logger.warn(`The application with id "${appIdentifier}" is not installed on device with identifier ${device.deviceInfo.identifier}.`); - - let packageFilePath = this.$liveSyncProvider.buildForDevice(device).wait(); - device.applicationManager.installApplication(packageFilePath).wait(); - - return true; - } - - return false; - }).future()(); - } - - public refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): IFuture { + public refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], isFullSync: boolean): IFuture { return (() => { let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(deviceAppData.device.deviceInfo.platform, deviceAppData.device); - this.$logger.info("Applying changes..."); - deviceLiveSyncService.refreshApplication(deviceAppData, localToDevicePaths, this.liveSyncData.forceExecuteFullSync).wait(); + this.$logger.info("Refreshing application..."); + deviceLiveSyncService.refreshApplication(deviceAppData, localToDevicePaths, isFullSync).wait(); }).future()(); } protected finishLivesync(deviceAppData: Mobile.IDeviceAppData): IFuture { return (() => { // 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}.`); + this.$logger.info(`Successfully synced application ${deviceAppData.appIdentifier} on device ${deviceAppData.device.deviceInfo.identifier}.\n`); }).future()(); } protected transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): IFuture { return (() => { this.$logger.info("Transferring project files..."); - this.logFilesSyncInformation(localToDevicePaths, "Transferring %s.", this.$logger.trace); - let canTransferDirectory = isFullSync && (this.$devicesService.isAndroidDevice(deviceAppData.device) || this.$devicesService.isiOSSimulator(deviceAppData.device)); if (canTransferDirectory) { - let tempDir = temp.mkdirSync("tempDir"); - shell.cp("-Rf", path.join(projectFilesPath, "*"), tempDir); - this.$projectFilesManager.processPlatformSpecificFiles(tempDir, deviceAppData.platform); - deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, tempDir).wait(); + deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath).wait(); } else { this.$liveSyncProvider.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync).wait(); } - this.logFilesSyncInformation(localToDevicePaths, "Successfully transferred %s.", this.$logger.info); }).future()(); } @@ -149,14 +131,14 @@ export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncSe return (() => { dispatcher.dispatch(() => (() => { try { - for (let platformName in this.batch) { - let batch = this.batch[platformName]; + for (let platform in this.batch) { + let batch = this.batch[platform]; batch.syncFiles(((filesToSync:string[]) => { - this.$platformService.preparePlatform(platformName, false, !this.$options.syncAllFiles).wait(); + this.$platformService.preparePlatform(this.liveSyncData.platform).wait(); 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); - this.$devicesService.execute(action, canExecute); + this.$devicesService.execute(action, canExecute).wait(); }).future()).wait(); } } catch (err) { @@ -172,7 +154,8 @@ export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncSe this.batch[this.liveSyncData.platform].addFile(filePath); } - private syncRemovedFile(filePath: string, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture { + private syncRemovedFile(filePath: string, + afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture { return (() => { let deviceFilesAction = (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => { let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(this.liveSyncData.platform, deviceAppData.device); @@ -184,30 +167,68 @@ export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncSe }).future()(); } - private getSyncAction(filesToSync: string[], + private getSyncAction( + filesToSync: string[], fileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): (device: Mobile.IDevice) => IFuture { let action = (device: Mobile.IDevice): IFuture => { return (() => { - let deviceAppData = this.$deviceAppDataFactory.create(this.liveSyncData.appIdentifier, this.$mobileHelper.normalizePlatformName(this.liveSyncData.platform), device); - let localToDevicePaths = this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, this.liveSyncData.projectFilesPath, filesToSync, this.liveSyncData.excludedProjectDirsAndFiles); - - fileSyncAction(deviceAppData, localToDevicePaths).wait(); + let deviceAppData:Mobile.IDeviceAppData = null; + let localToDevicePaths:Mobile.ILocalToDevicePathData[] = null; + let isFullSync = false; + if (this.$options.clean || this.$projectChangesService.currentChanges.changesRequireBuild) { + let buildConfig: IBuildConfig = { buildForDevice: !device.isEmulator }; + let platform = device.deviceInfo.platform; + if (this.$platformService.shouldBuild(platform, buildConfig)) { + this.$platformService.buildPlatform(platform, buildConfig).wait(); + } + this.$platformService.installApplication(device).wait(); + 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); + let mappedFiles = filesToSync.map((file: string) => this.$projectFilesProvider.mapFilePath(file, device.deviceInfo.platform)); + localToDevicePaths = this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, this.liveSyncData.projectFilesPath, mappedFiles, this.liveSyncData.excludedProjectDirsAndFiles); + fileSyncAction(deviceAppData, localToDevicePaths).wait(); + } if (!afterFileSyncAction) { - this.refreshApplication(deviceAppData, localToDevicePaths).wait(); + this.refreshApplication(deviceAppData, localToDevicePaths, isFullSync).wait(); } + device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(device.deviceInfo.platform), this.getLiveSyncInfoFilePath(deviceAppData)).wait(); this.finishLivesync(deviceAppData).wait(); if (afterFileSyncAction) { afterFileSyncAction(deviceAppData, localToDevicePaths).wait(); } }).future()(); }; - return action; } + private shouldTransferAllFiles(platform: string, deviceAppData: Mobile.IDeviceAppData) { + try { + if (this.$options.clean) { + return false; + } + let fileText = this.$platformService.readFile(deviceAppData.device, this.getLiveSyncInfoFilePath(deviceAppData)).wait(); + let remoteLivesyncInfo: IPrepareInfo = JSON.parse(fileText); + let localPrepareInfo = this.$projectChangesService.getPrepareInfo(platform); + return remoteLivesyncInfo.time !== localPrepareInfo.time; + } catch(e) { + return true; + } + } + + private getLiveSyncInfoFilePath(deviceAppData: Mobile.IDeviceAppData) { + let deviceRootPath = deviceAppData.deviceProjectRootPath; + if (deviceAppData.device.deviceInfo.platform.toLowerCase() === this.$devicePlatformsConstants.Android.toLowerCase()) { + deviceRootPath = path.dirname(deviceRootPath); + } + let deviceFilePath = path.join(deviceRootPath, livesyncInfoFileName); + return deviceFilePath; + } + private logFilesSyncInformation(localToDevicePaths: Mobile.ILocalToDevicePathData[], message: string, action: Function): void { - if (this.showFullLiveSyncInformation) { + if (localToDevicePaths && localToDevicePaths.length < 10) { _.each(localToDevicePaths, (file: Mobile.ILocalToDevicePathData) => { action.call(this.$logger, util.format(message, path.basename(file.getLocalPath()).yellow)); }); @@ -216,3 +237,4 @@ export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncSe } } } +$injector.register("platformLiveSyncService", PlatformLiveSyncServiceBase); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 5bc3d3a69b..adad0a5773 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -5,7 +5,6 @@ import * as helpers from "../common/helpers"; import * as semver from "semver"; import { AppFilesUpdater } from "./app-files-updater"; import * as temp from "temp"; -import { ProjectChangesInfo, IPrepareInfo } from "./project-changes-info"; import Future = require("fibers/future"); temp.track(); let clui = require("clui"); @@ -36,10 +35,10 @@ export class PlatformService implements IPlatformService { private $sysInfo: ISysInfo, private $staticConfig: Config.IStaticConfig, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, + private $projectChangesService: IProjectChangesService, private $childProcess: IChildProcess) { } - private _prepareInfo: IPrepareInfo; - public addPlatforms(platforms: string[]): IFuture { return (() => { let platformsDir = this.$projectData.platformsDir; @@ -192,7 +191,7 @@ export class PlatformService implements IPlatformService { return _.filter(this.$platformsData.platformsNames, p => { return this.isPlatformPrepared(p); }); } - public preparePlatform(platform: string, force?: boolean, skipModulesAndResources?: boolean): IFuture { + public preparePlatform(platform: string): IFuture { return (() => { this.validatePlatform(platform); @@ -215,46 +214,45 @@ export class PlatformService implements IPlatformService { } this.ensurePlatformInstalled(platform).wait(); - - let changeInfo: ProjectChangesInfo = new ProjectChangesInfo(platform, force, skipModulesAndResources, this.$platformsData, this.$projectData, this.$devicePlatformsConstants, this.$options, this.$fs); - this._prepareInfo = changeInfo.prepareInfo; - if (!this.isPlatformPrepared(platform) || changeInfo.hasChanges) { - this.preparePlatformCore(platform, changeInfo).wait(); - return true; + let changesInfo = this.$projectChangesService.checkForChanges(platform); + if (changesInfo.hasChanges) { + this.preparePlatformCore(platform, changesInfo).wait(); + this.$projectChangesService.savePrepareInfo(platform); + } else { + this.$logger.out("Skipping prepare."); } - return false; + + return true; }).future()(); } @helpers.hook('prepare') - private preparePlatformCore(platform: string, changeInfo: ProjectChangesInfo): IFuture { + private preparePlatformCore(platform: string, changesInfo?: IProjectChangesInfo): IFuture { return (() => { + this.$logger.out("Preparing project..."); let platformData = this.$platformsData.getPlatformData(platform); - if (changeInfo.appFilesChanged) { + if (!changesInfo || changesInfo.appFilesChanged) { this.copyAppFiles(platform).wait(); } - - if (changeInfo.appResourcesChanged) { + if (!changesInfo || changesInfo.appResourcesChanged) { this.copyAppResources(platform); platformData.platformProjectService.prepareProject(); } - - if (changeInfo.modulesChanged) { + if (!changesInfo || changesInfo.modulesChanged) { this.copyTnsModules(platform).wait(); } let directoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); let excludedDirs = [constants.APP_RESOURCES_FOLDER_NAME]; - - if (!changeInfo.modulesChanged) { + if (!changesInfo || !changesInfo.modulesChanged) { excludedDirs.push(constants.TNS_MODULES_FOLDER_NAME); } this.$projectFilesManager.processPlatformSpecificFiles(directoryPath, platform, excludedDirs); - if (changeInfo.configChanged || changeInfo.modulesChanged) { + if (!changesInfo || changesInfo.configChanged || changesInfo.modulesChanged) { this.applyBaseConfigOption(platformData); platformData.platformProjectService.processConfigurationFilesFromAppResources().wait(); } @@ -313,55 +311,190 @@ export class PlatformService implements IPlatformService { }).future()(); } - public cleanDestinationApp(platform: string): IFuture { + public shouldBuild(platform: string, buildConfig?: IBuildConfig): IFuture { return (() => { - this.ensurePlatformInstalled(platform).wait(); - - const appSourceDirectoryPath = path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME); + if (this.$options.release || this.$projectChangesService.currentChanges.changesRequireBuild) { + return true; + } let platformData = this.$platformsData.getPlatformData(platform); - let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - const appUpdater = new AppFilesUpdater(appSourceDirectoryPath, appDestinationDirectoryPath, this.$options, this.$fs); - appUpdater.cleanDestinationApp(); - }).future()(); + let forDevice = !buildConfig || buildConfig.buildForDevice; + let outputPath = forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath; + if (!this.$fs.exists(outputPath)) { + return true; + } + let packageNames = forDevice ? platformData.validPackageNamesForDevice : platformData.validPackageNamesForEmulator; + let packages = this.getApplicationPackages(outputPath, packageNames); + if (packages.length === 0) { + return true; + } + let prepareInfo = this.$projectChangesService.getPrepareInfo(platform); + let buildInfo = this.getBuildInfo(platform, platformData, buildConfig); + if (!prepareInfo || !buildInfo) { + return true; + } + if (this.$options.clean) { + return prepareInfo.time !== buildInfo.prepareTime; + } + if (prepareInfo.time === buildInfo.prepareTime) { + return false; + } + return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime; + }).future()(); } - public buildPlatform(platform: string, buildConfig?: IBuildConfig, forceBuild?: boolean): IFuture { + public buildPlatform(platform: string, buildConfig?: IBuildConfig): IFuture { return (() => { - let shouldBuild = this.preparePlatform(platform, false).wait(); + this.$logger.out("Building project..."); let platformData = this.$platformsData.getPlatformData(platform); + platformData.platformProjectService.buildProject(platformData.projectRoot, buildConfig).wait(); + let prepareInfo = this.$projectChangesService.getPrepareInfo(platform); let buildInfoFilePath = this.getBuildOutputPath(platform, platformData, buildConfig); let buildInfoFile = path.join(buildInfoFilePath, buildInfoFileName); - if (!shouldBuild) { - if (this.$fs.exists(buildInfoFile)) { - let buildInfoText = this.$fs.readText(buildInfoFile); - shouldBuild = this._prepareInfo.time !== buildInfoText; - } else { - shouldBuild = true; - } + let buildInfo: IBuildInfo = { + prepareTime: prepareInfo.changesRequireBuildTime, + buildTime: new Date().toString() + }; + this.$fs.writeJson(buildInfoFile, buildInfo); + this.$logger.out("Project successfully built."); + }).future()(); + } + + public shouldInstall(device: Mobile.IDevice): boolean { + let platform = device.deviceInfo.platform; + let platformData = this.$platformsData.getPlatformData(platform); + if (!device.applicationManager.isApplicationInstalled(this.$projectData.projectId).wait()) { + return true; + } + let deviceBuildInfo: IBuildInfo = this.getDeviceBuildInfo(device).wait(); + let localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator }); + return !localBuildInfo || !deviceBuildInfo || deviceBuildInfo.buildTime !== localBuildInfo.buildTime; + } + + public installApplication(device: Mobile.IDevice): IFuture { + return (() => { + this.$logger.out("Installing..."); + let platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform); + let packageFile = ""; + if (this.$devicesService.isiOSSimulator(device)) { + packageFile = this.getLatestApplicationPackageForEmulator(platformData).packageName; + } else { + packageFile = this.getLatestApplicationPackageForDevice(platformData).packageName; } - if (shouldBuild || forceBuild) { - this.buildPlatformCore(platform, buildConfig).wait(); - this.$fs.writeFile(buildInfoFile, this._prepareInfo.time); + platformData.platformProjectService.deploy(device.deviceInfo.identifier).wait(); + device.applicationManager.reinstallApplication(this.$projectData.projectId, packageFile).wait(); + if (!this.$options.release) { + let deviceFilePath = this.getDeviceBuildInfoFilePath(device); + let buildInfoFilePath = this.getBuildOutputPath(device.deviceInfo.platform, platformData, { buildForDevice: !device.isEmulator }); + device.fileSystem.putFile(path.join(buildInfoFilePath, buildInfoFileName), deviceFilePath).wait(); } + this.$logger.out(`Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`); }).future()(); } - private buildPlatformCore(platform: string, buildConfig?: IBuildConfig) { + public deployPlatform(platform: string, forceInstall?: boolean): IFuture { return (() => { - let platformData = this.$platformsData.getPlatformData(platform); - platformData.platformProjectService.buildProject(platformData.projectRoot, buildConfig).wait(); - this.$logger.out("Project successfully built."); + this.preparePlatform(platform).wait(); + this.$logger.out("Searching for devices..."); + this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait(); + let action = (device: Mobile.IDevice): IFuture => { + return (() => { + let buildConfig: IBuildConfig = { buildForDevice: !this.$devicesService.isiOSSimulator(device) }; + let shouldBuild = this.shouldBuild(platform, buildConfig).wait(); + if (shouldBuild) { + this.buildPlatform(platform, buildConfig).wait(); + } else { + this.$logger.out("Skipping package build. No changes detected on the native side. This will be fast!"); + } + if (forceInstall || shouldBuild || this.shouldInstall(device)) { + this.installApplication(device).wait(); + } else { + this.$logger.out("Skipping install."); + } + }).future()(); + }; + this.$devicesService.execute(action, this.getCanExecuteAction(platform)).wait(); }).future()(); } + public runPlatform(platform: string): IFuture { + return (() => { + this.$logger.out("Starting..."); + let action = (device: Mobile.IDevice) => { + return (() => { + device.applicationManager.startApplication(this.$projectData.projectId).wait(); + this.$logger.out(`Successfully started on device with identifier '${device.deviceInfo.identifier}'.`); + }).future()(); + }; + this.$devicesService.execute(action, this.getCanExecuteAction(platform)).wait(); + }).future()(); + } + + public emulatePlatform(platform: string): IFuture { + if (this.$options.avd) { + this.$logger.warn(`Option --avd is no longer supported. Please use --device instead!`); + return Future.fromResult(); + } + if (this.$options.availableDevices) { + return $injector.resolveCommand("device").execute([platform]); + } + this.$options.emulator = true; + return this.runPlatform(platform); + } + private getBuildOutputPath(platform: string, platformData: IPlatformData, buildConfig?: IBuildConfig): string { let buildForDevice = buildConfig ? buildConfig.buildForDevice : this.$options.forDevice; - if (platform === this.$devicePlatformsConstants.iOS.toLowerCase()) { + if (platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) { return buildForDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath; } return platformData.deviceBuildOutputPath; } + private getDeviceBuildInfoFilePath(device: Mobile.IDevice): string { + let deviceAppData = this.$deviceAppDataFactory.create(this.$projectData.projectId, device.deviceInfo.platform, device); + let deviceRootPath = deviceAppData.deviceProjectRootPath; + if (device.deviceInfo.platform.toLowerCase() === this.$devicePlatformsConstants.Android.toLowerCase()) { + deviceRootPath = path.dirname(deviceRootPath); + } + return path.join(deviceRootPath, buildInfoFileName); + } + + private getDeviceBuildInfo(device: Mobile.IDevice): IFuture { + return (() => { + let deviceFilePath = this.getDeviceBuildInfoFilePath(device); + try { + return JSON.parse(this.readFile(device, deviceFilePath).wait()); + } catch(e) { + return null; + }; + }).future()(); + } + + private getBuildInfo(platform: string, platformData: IPlatformData, buildConfig: IBuildConfig): IBuildInfo { + let buildInfoFilePath = this.getBuildOutputPath(platform, platformData, buildConfig); + let buildInfoFile = path.join(buildInfoFilePath, buildInfoFileName); + if (this.$fs.exists(buildInfoFile)) { + try { + let buildInfoTime = this.$fs.readJson(buildInfoFile); + return buildInfoTime; + } catch(e) { + return null; + } + } + return null; + } + + public cleanDestinationApp(platform: string): IFuture { + return (() => { + this.ensurePlatformInstalled(platform).wait(); + + const appSourceDirectoryPath = path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME); + let platformData = this.$platformsData.getPlatformData(platform); + let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + const appUpdater = new AppFilesUpdater(appSourceDirectoryPath, appDestinationDirectoryPath, this.$options, this.$fs); + appUpdater.cleanDestinationApp(); + }).future()(); + } + public lastOutputPath(platform: string, settings: { isForDevice: boolean }): string { let packageFile: string; let platformData = this.$platformsData.getPlatformData(platform); @@ -424,65 +557,6 @@ export class PlatformService implements IPlatformService { }).future()(); } - public deployPlatform(platform: string): IFuture { - return (() => { - let platformData = this.$platformsData.getPlatformData(platform); - this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait(); - let packageFileDict: IStringDictionary = {}; - let action = (device: Mobile.IDevice): IFuture => { - return (() => { - let packageFileKey = this.getPackageFileKey(device); - let packageFile = packageFileDict[packageFileKey]; - if (!packageFile) { - let buildConfig: IBuildConfig = {}; - let isSimulator = this.$devicesService.isiOSSimulator(device); - buildConfig.buildForDevice = !isSimulator; - this.buildPlatform(platform, buildConfig, false).wait(); - if (isSimulator) { - packageFile = this.getLatestApplicationPackageForEmulator(platformData).packageName; - } else { - packageFile = this.getLatestApplicationPackageForDevice(platformData).packageName; - } - } - - platformData.platformProjectService.deploy(device.deviceInfo.identifier).wait(); - device.applicationManager.reinstallApplication(this.$projectData.projectId, packageFile).wait(); - this.$logger.info(`Successfully deployed on device with identifier '${device.deviceInfo.identifier}'.`); - - packageFileDict[packageFileKey] = packageFile; - }).future()(); - }; - this.$devicesService.execute(action, this.getCanExecuteAction(platform)).wait(); - }).future()(); - } - - public runPlatform(platform: string): IFuture { - if (this.$options.avd) { - this.$logger.warn(`Option --avd is no longer supported. Please use --device instead!`); - return Future.fromResult(); - } - return (() => { - this.deployPlatform(platform).wait(); - let action = (device: Mobile.IDevice) => device.applicationManager.startApplication(this.$projectData.projectId); - this.$devicesService.execute(action, this.getCanExecuteAction(platform)).wait(); - }).future()(); - } - - public emulatePlatform(platform: string): IFuture { - this.$options.emulator = true; - if (this.$options.availableDevices) { - return $injector.resolveCommand("device").execute([platform]); - } - return this.runPlatform(platform); - } - - private getPackageFileKey(device: Mobile.IDevice): string { - if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { - return device.deviceInfo.platform.toLowerCase(); - } - return device.deviceInfo.platform.toLowerCase() + device.deviceInfo.type; - } - private getCanExecuteAction(platform: string): any { let canExecute = (currentDevice: Mobile.IDevice): boolean => { if (this.$options.device && currentDevice && currentDevice.deviceInfo) { @@ -647,5 +721,23 @@ export class PlatformService implements IPlatformService { this.$fs.copyFile(newConfigFile, platformData.configurationFilePath); } } + + public readFile(device: Mobile.IDevice, deviceFilePath: string): IFuture { + return (() => { + temp.track(); + let uniqueFilePath = temp.path({ suffix: ".tmp" }); + try { + device.fileSystem.getFile(deviceFilePath, uniqueFilePath).wait(); + } catch (e) { + return null; + } + if (this.$fs.exists(uniqueFilePath)) { + let text = this.$fs.readText(uniqueFilePath); + shell.rm(uniqueFilePath); + return text; + } + return null; + }).future()(); + } } $injector.register("platformService", PlatformService); diff --git a/lib/services/project-changes-info.ts b/lib/services/project-changes-info.ts deleted file mode 100644 index 1cdb6a9649..0000000000 --- a/lib/services/project-changes-info.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as path from "path"; - -const prepareInfoFileName = ".nsprepareinfo"; - -export interface IPrepareInfo { - time: string; - bundle: boolean; - release: boolean; -} - -export class ProjectChangesInfo { - - public get hasChanges(): boolean { - return this.appFilesChanged || this.appResourcesChanged || this.modulesChanged || this.configChanged; - } - - public appFilesChanged: boolean = false; - public appResourcesChanged: boolean = false; - public modulesChanged: boolean = false; - public configChanged: boolean = false; - public prepareInfo: IPrepareInfo; - - constructor(platform: string, - private force: boolean, - private skipModulesAndResources: boolean, - private $platformsData: IPlatformsData, - private $projectData: IProjectData, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $options: IOptions, - private $fs: IFileSystem) { - - let platformData = this.$platformsData.getPlatformData(platform); - let buildInfoFile = path.join(platformData.projectRoot, prepareInfoFileName); - - if (force || !this.$fs.exists(buildInfoFile)) { - this.appFilesChanged = true; - this.appResourcesChanged = true; - this.modulesChanged = true; - this.configChanged = true; - this.prepareInfo = { time: "", bundle: $options.bundle, release: $options.release }; - } else { - let outputProjectMtime = this.$fs.getFsStats(buildInfoFile).mtime.getTime(); - this.prepareInfo = this.$fs.readJson(buildInfoFile); - this.appFilesChanged = this.containsNewerFiles(this.$projectData.appDirectoryPath, this.$projectData.appResourcesDirectoryPath, outputProjectMtime); - if (!skipModulesAndResources) { - this.appResourcesChanged = this.containsNewerFiles(this.$projectData.appResourcesDirectoryPath, null, outputProjectMtime); - this.modulesChanged = this.containsNewerFiles(path.join(this.$projectData.projectDir, "node_modules"), path.join(this.$projectData.projectDir, "node_modules", "tns-ios-inspector")/*done because currently all node_modules are traversed, a possible improvement could be traversing only production dependencies*/, outputProjectMtime); - let platformResourcesDir = path.join(this.$projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName); - if (platform === this.$devicePlatformsConstants.iOS.toLowerCase()) { - this.configChanged = this.filesChanged([ - this.$options.baseConfig || path.join(platformResourcesDir, platformData.configurationFileName), - path.join(platformResourcesDir, "LaunchScreen.storyboard"), - path.join(platformResourcesDir, "build.xcconfig") - ], outputProjectMtime); - } else { - this.configChanged = this.filesChanged([ - path.join(platformResourcesDir, platformData.configurationFileName), - path.join(platformResourcesDir, "app.gradle") - ], outputProjectMtime); - } - } - - if (this.$options.bundle !== this.prepareInfo.bundle || this.$options.release !== this.prepareInfo.release) { - this.appFilesChanged = true; - this.appResourcesChanged = true; - this.modulesChanged = true; - this.configChanged = true; - this.prepareInfo.release = this.$options.release; - this.prepareInfo.bundle = this.$options.bundle; - } - if (this.modulesChanged || this.appResourcesChanged) { - this.configChanged = true; - } - } - - if (this.hasChanges) { - this.prepareInfo.time = new Date().toString(); - this.$fs.writeJson(buildInfoFile, this.prepareInfo); - } - } - - private filesChanged(files: string[], mtime: number): boolean { - for (let file of files) { - if (this.$fs.exists(file)) { - let fileStats = this.$fs.getFsStats(file); - if (fileStats.mtime.getTime() > mtime) { - return true; - } - } - } - return false; - } - - private containsNewerFiles(dir: string, skipDir: string, mtime: number): boolean { - let files = this.$fs.readDirectory(dir); - for (let file of files) { - let filePath = path.join(dir, file); - if (filePath === skipDir) { - continue; - } - let fileStats = this.$fs.getFsStats(filePath); - if (fileStats.mtime.getTime() > mtime) { - return true; - } - let lFileStats = this.$fs.getLsStats(filePath); - if (lFileStats.mtime.getTime() > mtime) { - return true; - } - if (fileStats.isDirectory()) { - if (this.containsNewerFiles(filePath, skipDir, mtime)) { - return true; - } - } - } - return false; - } -} diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts new file mode 100644 index 0000000000..6a484a3ee2 --- /dev/null +++ b/lib/services/project-changes-service.ts @@ -0,0 +1,219 @@ +import * as path from "path"; +import {NODE_MODULES_FOLDER_NAME} from "../constants"; + +const prepareInfoFileName = ".nsprepareinfo"; + +class ProjectChangesInfo implements IProjectChangesInfo { + + public appFilesChanged: boolean; + public appResourcesChanged: boolean; + public modulesChanged: boolean; + public configChanged: boolean; + public packageChanged: boolean; + public nativeChanged: boolean; + + public get hasChanges(): boolean { + return this.packageChanged || + this.appFilesChanged || + this.appResourcesChanged || + this.modulesChanged || + this.configChanged; + } + + public get changesRequireBuild(): boolean { + return this.packageChanged || + this.appResourcesChanged || + this.nativeChanged; + } +} + +export class ProjectChangesService implements IProjectChangesService { + + private _changesInfo: IProjectChangesInfo; + private _prepareInfo: IPrepareInfo; + private _newFiles: number = 0; + private _outputProjectMtime: number; + + constructor( + private $platformsData: IPlatformsData, + private $projectData: IProjectData, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $options: IOptions, + private $fs: IFileSystem) { + } + + public get currentChanges(): IProjectChangesInfo { + return this._changesInfo; + } + + public checkForChanges(platform: string): IProjectChangesInfo { + let platformData = this.$platformsData.getPlatformData(platform); + this._changesInfo = new ProjectChangesInfo(); + if (!this.ensurePrepareInfo(platform)) { + this._newFiles = 0; + this._changesInfo.appFilesChanged = this.containsNewerFiles(this.$projectData.appDirectoryPath, this.$projectData.appResourcesDirectoryPath); + this._changesInfo.packageChanged = this.filesChanged([path.join(this.$projectData.projectDir, "package.json")]); + this._changesInfo.appResourcesChanged = this.containsNewerFiles(this.$projectData.appResourcesDirectoryPath, null); + /*done because currently all node_modules are traversed, a possible improvement could be traversing only the production dependencies*/ + this._changesInfo.nativeChanged = this.containsNewerFiles( + path.join(this.$projectData.projectDir, NODE_MODULES_FOLDER_NAME), + path.join(this.$projectData.projectDir, NODE_MODULES_FOLDER_NAME, "tns-ios-inspector"), + this.fileChangeRequiresBuild); + if (this._newFiles > 0) { + this._changesInfo.modulesChanged = true; + } + let platformResourcesDir = path.join(this.$projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName); + if (platform === this.$devicePlatformsConstants.iOS.toLowerCase()) { + this._changesInfo.configChanged = this.filesChanged([ + this.$options.baseConfig || path.join(platformResourcesDir, platformData.configurationFileName), + path.join(platformResourcesDir, "LaunchScreen.storyboard"), + path.join(platformResourcesDir, "build.xcconfig") + ]); + } else { + this._changesInfo.configChanged = this.filesChanged([ + path.join(platformResourcesDir, platformData.configurationFileName), + path.join(platformResourcesDir, "app.gradle") + ]); + } + } + if (this.$options.bundle !== this._prepareInfo.bundle || this.$options.release !== this._prepareInfo.release) { + this._changesInfo.appFilesChanged = true; + this._changesInfo.appResourcesChanged = true; + this._changesInfo.modulesChanged = true; + this._changesInfo.configChanged = true; + this._prepareInfo.release = this.$options.release; + this._prepareInfo.bundle = this.$options.bundle; + } + if (this._changesInfo.packageChanged) { + this._changesInfo.modulesChanged = true; + } + if (this._changesInfo.modulesChanged || this._changesInfo.appResourcesChanged) { + this._changesInfo.configChanged = true; + } + if (this._changesInfo.hasChanges) { + this._prepareInfo.changesRequireBuild = this._changesInfo.changesRequireBuild; + this._prepareInfo.time = new Date().toString(); + if (this._prepareInfo.changesRequireBuild) { + this._prepareInfo.changesRequireBuildTime = this._prepareInfo.time; + } + } + return this._changesInfo; + } + + public getPrepareInfoFilePath(platform: string): string { + let platformData = this.$platformsData.getPlatformData(platform); + let prepareInfoFilePath = path.join(platformData.projectRoot, prepareInfoFileName); + return prepareInfoFilePath; + } + + public getPrepareInfo(platform: string): IPrepareInfo { + let prepareInfoFilePath = this.getPrepareInfoFilePath(platform); + let prepareInfo: IPrepareInfo = null; + if (this.$fs.exists(prepareInfoFilePath)) { + try { + prepareInfo = this.$fs.readJson(prepareInfoFilePath); + } catch (e) { + prepareInfo = null; + } + } + return prepareInfo; + } + + public savePrepareInfo(platform: string): void { + let prepareInfoFilePath = this.getPrepareInfoFilePath(platform); + this.$fs.writeJson(prepareInfoFilePath, this._prepareInfo); + } + + private ensurePrepareInfo(platform: string): boolean { + this._prepareInfo = this.getPrepareInfo(platform); + if (this._prepareInfo) { + let platformData = this.$platformsData.getPlatformData(platform); + let prepareInfoFile = path.join(platformData.projectRoot, prepareInfoFileName); + this._outputProjectMtime = this.$fs.getFsStats(prepareInfoFile).mtime.getTime(); + return false; + } + this._prepareInfo = { + time: "", + bundle: this.$options.bundle, + release: this.$options.release, + changesRequireBuild: true, + changesRequireBuildTime: null + }; + this._outputProjectMtime = 0; + this._changesInfo.appFilesChanged = true; + this._changesInfo.appResourcesChanged = true; + this._changesInfo.modulesChanged = true; + this._changesInfo.configChanged = true; + return true; + } + + private filesChanged(files: string[]): boolean { + for (let file of files) { + if (this.$fs.exists(file)) { + let fileStats = this.$fs.getFsStats(file); + if (fileStats.mtime.getTime() > this._outputProjectMtime) { + return true; + } + } + } + return false; + } + + private containsNewerFiles(dir: string, skipDir: string, processFunc?: (filePath: string) => boolean): boolean { + let files = this.$fs.readDirectory(dir); + for (let file of files) { + let filePath = path.join(dir, file); + if (filePath === skipDir) { + continue; + } + let fileStats = this.$fs.getFsStats(filePath); + let changed = fileStats.mtime.getTime() > this._outputProjectMtime; + if (!changed) { + let lFileStats = this.$fs.getLsStats(filePath); + changed = lFileStats.mtime.getTime() > this._outputProjectMtime; + } + if (changed) { + if (processFunc) { + this._newFiles ++; + let filePathRelative = path.relative(this.$projectData.projectDir, filePath); + if (processFunc.call(this, filePathRelative)) { + return true; + } + } else { + return true; + } + } + if (fileStats.isDirectory()) { + if (this.containsNewerFiles(filePath, skipDir, processFunc)) { + return true; + } + } + } + return false; + } + + private fileChangeRequiresBuild(file: string) { + if (path.basename(file) === "package.json") { + return true; + } + let projectDir = this.$projectData.projectDir; + if (_.startsWith(path.join(projectDir, file), this.$projectData.appResourcesDirectoryPath)) { + return true; + } + if (_.startsWith(file, NODE_MODULES_FOLDER_NAME)) { + let filePath = file; + while(filePath !== NODE_MODULES_FOLDER_NAME) { + filePath = path.dirname(filePath); + let fullFilePath = path.join(projectDir, path.join(filePath, "package.json")); + if (this.$fs.exists(fullFilePath)) { + let json = this.$fs.readJson(fullFilePath); + if (json["nativescript"] && _.startsWith(file, path.join(filePath, "platforms"))) { + return true; + } + } + } + } + return false; + } +} +$injector.register("projectChangesService", ProjectChangesService); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 354b5c7db8..4fb01eea36 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -232,7 +232,6 @@ class TestExecutionService implements ITestExecutionService { platform: platform, appIdentifier: this.$projectData.projectId, projectFilesPath: projectFilesPath, - forceExecuteFullSync: true, // Always restart the application when change is detected, so tests will be rerun. syncWorkingDirectory: path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), excludedProjectDirsAndFiles: this.$options.release ? constants.LIVESYNC_EXCLUDED_FILE_PATTERNS : [] }; diff --git a/package.json b/package.json index b089ee3e44..75a590da06 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "bufferutil": "https://github.com/telerik/bufferutil/tarball/v1.0.1.4", "byline": "4.2.1", "chalk": "1.1.0", + "chokidar": "^1.6.1", "cli-table": "https://github.com/telerik/cli-table/tarball/v0.3.1.2", "clui": "0.3.1", "colors": "1.1.2", diff --git a/test/debug.ts b/test/debug.ts index 1fa6d1f4b0..b41732c113 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -47,6 +47,7 @@ function createTestInjector(): IInjector { testInjector.register("androidEmulatorServices", {}); testInjector.register("adb", AndroidDebugBridge); testInjector.register("androidDebugBridgeResultHandler", AndroidDebugBridgeResultHandler); + testInjector.register("platformService", stubs.PlatformServiceStub); return testInjector; } @@ -65,7 +66,6 @@ describe("Debugger tests", () => { debugCommand.execute(["android","--watch"]).wait(); let config:IConfiguration = testInjector.resolve("config"); assert.isTrue(config.debugLivesync); - assert.isFalse(options.rebuild); }); it("Ensures that beforePrepareAllPlugins will not call gradle when livesyncing", () => { diff --git a/test/npm-support.ts b/test/npm-support.ts index 634155a3f8..7c766cbce7 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -26,6 +26,7 @@ import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms- import { XmlValidator } from "../lib/xml-validator"; import { LockFile } from "../lib/lockfile"; import Future = require("fibers/future"); +import ProjectChangesLib = require("../lib/services/project-changes-service"); import path = require("path"); import temp = require("temp"); @@ -77,6 +78,7 @@ function createTestInjector(): IInjector { testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); testInjector.register("config", StaticConfigLib.Configuration); + testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); return testInjector; } diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 5cb9deb279..e3c9d2e7fb 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -20,6 +20,7 @@ import {DevicePlatformsConstants} from "../lib/common/mobile/device-platforms-co import { XmlValidator } from "../lib/xml-validator"; import * as ChildProcessLib from "../lib/common/child-process"; import {CleanCommand} from "../lib/commands/platform-clean"; +import ProjectChangesLib = require("../lib/services/project-changes-service"); let isCommandExecuted = true; @@ -139,6 +140,7 @@ function createTestInjector() { testInjector.register("xmlValidator", XmlValidator); testInjector.register("npm", {}); testInjector.register("childProcess", ChildProcessLib.ChildProcess); + testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); return testInjector; } diff --git a/test/platform-service.ts b/test/platform-service.ts index fb82413e8c..bd12fb0f35 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -18,6 +18,7 @@ import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilitie import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; import * as ChildProcessLib from "../lib/common/child-process"; +import ProjectChangesLib = require("../lib/services/project-changes-service"); require("should"); let temp = require("temp"); @@ -78,6 +79,7 @@ function createTestInjector() { } }); testInjector.register("childProcess", ChildProcessLib.ChildProcess); + testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); return testInjector; } diff --git a/test/stubs.ts b/test/stubs.ts index 5c535f62bb..3bbdeece57 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -482,8 +482,6 @@ export class LiveSyncServiceStub implements ILiveSyncService { public liveSync(platform: string, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture): IFuture { return Future.fromResult(); } - - public forceExecuteFullSync: boolean; } export class AndroidToolsInfoStub implements IAndroidToolsInfo { @@ -534,3 +532,131 @@ export class ChildProcessStub { return Future.fromResult(null); } } + +export class ProjectChangesService implements IProjectChangesService { + public checkForChanges(platform: string): IProjectChangesInfo { + return {}; + } + + public getPrepareInfo(platform: string): IPrepareInfo { + return null; + } + + public savePrepareInfo(platform: string): void { + } + + public getPrepareInfoFilePath(platform: string): string { + return ""; + } + + public get currentChanges(): IProjectChangesInfo { + return {}; + } +} + +export class CommandsService implements ICommandsService { + public allCommands(opts: {includeDevCommands: boolean}): string[] { + return []; + } + + public tryExecuteCommand(commandName: string, commandArguments: string[]): IFuture { + return Future.fromResult(); + } + public executeCommandUnchecked(commandName: string, commandArguments: string[]): IFuture { + return Future.fromResult(true); + } + + public completeCommand(): IFuture { + return Future.fromResult(true); + } +} + +export class PlatformServiceStub implements IPlatformService { + + public addPlatforms(platforms: string[]): IFuture { + return Future.fromResult(); + } + + public getInstalledPlatforms(): string[] { + return []; + } + + public getAvailablePlatforms(): string[] { + return []; + } + + public getPreparedPlatforms(): string[] { + return []; + } + + public removePlatforms(platforms: string[]): void { + + } + + public updatePlatforms(platforms: string[]): IFuture { + return Future.fromResult(); + } + + public preparePlatform(platform: string, changesInfo?: IProjectChangesInfo): IFuture { + return Future.fromResult(true); + } + + public shouldBuild(platform: string, buildConfig?: IBuildConfig): IFuture { + return Future.fromResult(true); + } + + public buildPlatform(platform: string, buildConfig?: IBuildConfig): IFuture { + return Future.fromResult(); + } + + public shouldInstall(device: Mobile.IDevice): boolean { + return true; + } + + public installApplication(device: Mobile.IDevice): IFuture { + return Future.fromResult(); + } + + public deployPlatform(platform: string, forceInstall?: boolean): IFuture { + return Future.fromResult(); + } + + public runPlatform(platform: string): IFuture { + return Future.fromResult(); + } + + public emulatePlatform(platform: string): IFuture { + return Future.fromResult(); + } + + public cleanDestinationApp(platform: string): IFuture { + return Future.fromResult(); + } + + public validatePlatformInstalled(platform: string): void { + + } + + public validatePlatform(platform: string): void { + + } + + public getLatestApplicationPackageForDevice(platformData: IPlatformData): IApplicationPackage { + return null; + } + + public getLatestApplicationPackageForEmulator(platformData: IPlatformData): IApplicationPackage { + return null; + } + + public copyLastOutput(platform: string, targetPath: string, settings: {isForDevice: boolean}): void { + } + + public lastOutputPath(platform: string, settings: { isForDevice: boolean }): string { + return ""; + } + + public readFile(device: Mobile.IDevice, deviceFilePath: string): IFuture { + return Future.fromResult(""); + } +}