diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 9f4bb2094d..0c162f69c4 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -165,3 +165,5 @@ $injector.require("nativeScriptCloudExtensionService", "./services/nativescript- $injector.requireCommand("resources|generate|icons", "./commands/generate-assets"); $injector.requireCommand("resources|generate|splashes", "./commands/generate-assets"); $injector.requirePublic("assetsGenerationService", "./services/assets-generation/assets-generation-service"); + +$injector.require("filesHashService", "./services/files-hash-service"); diff --git a/lib/definitions/files-hash-service.d.ts b/lib/definitions/files-hash-service.d.ts new file mode 100644 index 0000000000..cd6b3f2b5f --- /dev/null +++ b/lib/definitions/files-hash-service.d.ts @@ -0,0 +1,4 @@ +interface IFilesHashService { + generateHashes(files: string[]): Promise; + getChanges(files: string[], oldHashes: IStringDictionary): Promise; +} \ No newline at end of file diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 4f098575eb..b8d2a32cc2 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -88,7 +88,7 @@ interface IPlatformService extends IBuildPlatformAction, NodeJS.EventEmitter { * @param {IShouldPrepareInfo} shouldPrepareInfo Options needed to decide whether to prepare. * @returns {Promise} true indicates that the project should be prepared. */ - shouldPrepare(shouldPrepareInfo: IShouldPrepareInfo): Promise + shouldPrepare(shouldPrepareInfo: IShouldPrepareInfo): Promise; /** * Installs the application on specified device. @@ -213,7 +213,7 @@ interface IPlatformService extends IBuildPlatformAction, NodeJS.EventEmitter { * @param {string} buildInfoFileDirname The directory where the build file should be written to. * @returns {void} */ - saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void + saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void; } interface IPlatformOptions extends IPlatformSpecificData, ICreateProjectOptions { } diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index f1178d11b8..1704a44ed9 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -1,4 +1,8 @@ -interface IPrepareInfo extends IAddedNativePlatform { +interface IAppFilesHashes { + appFilesHashes: IStringDictionary; +} + +interface IPrepareInfo extends IAddedNativePlatform, IAppFilesHashes { time: string; bundle: boolean; release: boolean; diff --git a/lib/services/files-hash-service.ts b/lib/services/files-hash-service.ts new file mode 100644 index 0000000000..06d844ea04 --- /dev/null +++ b/lib/services/files-hash-service.ts @@ -0,0 +1,26 @@ +import { executeActionByChunks } from "../common/helpers"; +import { DEFAULT_CHUNK_SIZE } from "../common/constants"; + +export class FilesHashService implements IFilesHashService { + constructor(private $fs: IFileSystem) { } + + public async generateHashes(files: string[]): Promise { + const result: IStringDictionary = {}; + + const action = async (file: string) => { + if (this.$fs.getFsStats(file).isFile()) { + result[file] = await this.$fs.getFileShasum(file); + } + }; + + await executeActionByChunks(files, DEFAULT_CHUNK_SIZE, action); + + return result; + } + + public async getChanges(files: string[], oldHashes: IStringDictionary): Promise { + const newHashes = await this.generateHashes(files); + return _.omitBy(newHashes, (hash: string, pathToFile: string) => !!_.find(oldHashes, (oldHash: string, oldPath: string) => pathToFile === oldPath && hash === oldHash)); + } +} +$injector.register("filesHashService", FilesHashService); diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index cdf003f603..d59bd34069 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -41,8 +41,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi super(); } - public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], - liveSyncData: ILiveSyncInfo): Promise { + public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { const projectData = this.$projectDataService.getProjectData(liveSyncData.projectDir); await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); await this.liveSyncOperation(deviceDescriptors, liveSyncData, projectData); @@ -318,27 +317,19 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi } @hook("liveSync") - private async liveSyncOperation(deviceDescriptors: ILiveSyncDeviceInfo[], - liveSyncData: ILiveSyncInfo, projectData: IProjectData): Promise { + private async liveSyncOperation(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo, projectData: IProjectData): Promise { // In case liveSync is called for a second time for the same projectDir. const isAlreadyLiveSyncing = this.liveSyncProcessesInfo[projectData.projectDir] && !this.liveSyncProcessesInfo[projectData.projectDir].isStopped; - // 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. - const currentlyRunningDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectData.projectDir); - const deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentlyRunningDeviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors; this.setLiveSyncProcessInfo(liveSyncData.projectDir, deviceDescriptors); - await this.initialSync(projectData, deviceDescriptorsForInitialSync, liveSyncData); - if (!liveSyncData.skipWatcher && this.liveSyncProcessesInfo[projectData.projectDir].deviceDescriptors.length) { // Should be set after prepare this.$usbLiveSyncService.isInitialized = true; - - const devicesIds = deviceDescriptors.map(dd => dd.identifier); - const devices = _.filter(this.$devicesService.getDeviceInstances(), device => _.includes(devicesIds, device.deviceInfo.identifier)); - const platforms = _(devices).map(device => device.deviceInfo.platform).uniq().value(); - await this.startWatcher(projectData, liveSyncData, platforms); + await this.startWatcher(projectData, liveSyncData, deviceDescriptors, { isAlreadyLiveSyncing }); } + + await this.initialSync(projectData, liveSyncData, deviceDescriptors, { isAlreadyLiveSyncing }); } private setLiveSyncProcessInfo(projectDir: string, deviceDescriptors: ILiveSyncDeviceInfo[]): void { @@ -351,6 +342,13 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi this.liveSyncProcessesInfo[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), deviceDescriptorPrimaryKey); } + private async initialSync(projectData: IProjectData, liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[], options: { isAlreadyLiveSyncing: boolean }): Promise { + // 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. + const currentlyRunningDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectData.projectDir); + const deviceDescriptorsForInitialSync = options.isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentlyRunningDeviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors; + await this.initialSyncCore(projectData, deviceDescriptorsForInitialSync, liveSyncData); + } + private getLiveSyncService(platform: string): IPlatformLiveSyncService { if (this.$mobileHelper.isiOSPlatform(platform)) { return this.$injector.resolve("iOSLiveSyncService"); @@ -452,7 +450,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi return null; } - private async initialSync(projectData: IProjectData, deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { + private async initialSyncCore(projectData: IProjectData, deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { const preparedPlatforms: string[] = []; const rebuiltInformation: ILiveSyncBuildInfo[] = []; @@ -483,6 +481,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi 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, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath); @@ -525,7 +524,10 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi }; } - private async startWatcher(projectData: IProjectData, liveSyncData: ILiveSyncInfo, platforms: string[]): Promise { + private async startWatcher(projectData: IProjectData, liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[], options: { isAlreadyLiveSyncing: boolean }): Promise { + const devicesIds = deviceDescriptors.map(dd => dd.identifier); + const devices = _.filter(this.$devicesService.getDeviceInstances(), device => _.includes(devicesIds, device.deviceInfo.identifier)); + const platforms = _(devices).map(device => device.deviceInfo.platform).uniq().value(); const patterns = await this.getWatcherPatterns(liveSyncData, projectData, platforms); if (liveSyncData.watchAllFiles) { diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index 6af8e7e5c0..4237859f47 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { NODE_MODULES_FOLDER_NAME, NativePlatformStatus, PACKAGE_JSON_FILE_NAME, APP_GRADLE_FILE_NAME, BUILD_XCCONFIG_FILE_NAME } from "../constants"; +import { NODE_MODULES_FOLDER_NAME, NativePlatformStatus, PACKAGE_JSON_FILE_NAME, APP_GRADLE_FILE_NAME, BUILD_XCCONFIG_FILE_NAME, APP_RESOURCES_FOLDER_NAME } from "../constants"; import { getHash } from "../common/helpers"; const prepareInfoFileName = ".nsprepareinfo"; @@ -48,7 +48,8 @@ export class ProjectChangesService implements IProjectChangesService { constructor( private $platformsData: IPlatformsData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $fs: IFileSystem) { + private $fs: IFileSystem, + private $filesHashService: IFilesHashService) { } public get currentChanges(): IProjectChangesInfo { @@ -58,9 +59,12 @@ export class ProjectChangesService implements IProjectChangesService { public async checkForChanges(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise { const platformData = this.$platformsData.getPlatformData(platform, projectData); this._changesInfo = new ProjectChangesInfo(); - if (!this.ensurePrepareInfo(platform, projectData, projectChangesOptions)) { + const isPrepareInfoEnsured = await this.ensurePrepareInfo(platform, projectData, projectChangesOptions); + if (!isPrepareInfoEnsured) { this._newFiles = 0; - this._changesInfo.appFilesChanged = this.containsNewerFiles(projectData.appDirectoryPath, projectData.appResourcesDirectoryPath, projectData); + + this._changesInfo.appFilesChanged = await this.hasChangedAppFiles(projectData); + this._changesInfo.packageChanged = this.isProjectFileChanged(projectData, platform); this._changesInfo.appResourcesChanged = this.containsNewerFiles(projectData.appResourcesDirectoryPath, null, projectData); /*done because currently all node_modules are traversed, a possible improvement could be traversing only the production dependencies*/ @@ -152,7 +156,7 @@ export class ProjectChangesService implements IProjectChangesService { } } - private ensurePrepareInfo(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): boolean { + private async ensurePrepareInfo(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise { this._prepareInfo = this.getPrepareInfo(platform, projectData); if (this._prepareInfo) { this._prepareInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus && this._prepareInfo.nativePlatformStatus < projectChangesOptions.nativePlatformStatus ? @@ -173,7 +177,8 @@ export class ProjectChangesService implements IProjectChangesService { release: projectChangesOptions.release, changesRequireBuild: true, projectFileHash: this.getProjectFileStrippedHash(projectData, platform), - changesRequireBuildTime: null + changesRequireBuildTime: null, + appFilesHashes: await this.$filesHashService.generateHashes(this.getAppFiles(projectData.appDirectoryPath)) }; this._outputProjectMtime = 0; @@ -300,5 +305,20 @@ export class ProjectChangesService implements IProjectChangesService { } return false; } + + private getAppFiles(appDirectoryPath: string): string[] { + return this.$fs.enumerateFilesInDirectorySync(appDirectoryPath, (filePath: string, stat: IFsStats) => filePath.indexOf(APP_RESOURCES_FOLDER_NAME) === -1); + } + + private async hasChangedAppFiles(projectData: IProjectData): Promise { + const files = this.getAppFiles(projectData.appDirectoryPath); + const changedFiles = await this.$filesHashService.getChanges(files, this._prepareInfo.appFilesHashes || {}); + const hasChanges = changedFiles && _.keys(changedFiles).length > 0; + if (hasChanges) { + this._prepareInfo.appFilesHashes = await this.$filesHashService.generateHashes(files); + } + + return hasChanges; + } } $injector.register("projectChangesService", ProjectChangesService); diff --git a/test/npm-support.ts b/test/npm-support.ts index d2715b0f94..1252996825 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -98,6 +98,10 @@ function createTestInjector(): IInjector { }); testInjector.register("httpClient", {}); testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub); + testInjector.register("filesHashService", { + getChanges: () => Promise.resolve({}), + generateHashes: () => Promise.resolve() + }); return testInjector; } diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 6c96874772..c8d988f3d4 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -164,6 +164,7 @@ function createTestInjector() { testInjector.register("analyticsSettingsService", { getPlaygroundInfo: () => Promise.resolve(null) }); + testInjector.register("filesHashService", {}); return testInjector; } diff --git a/test/platform-service.ts b/test/platform-service.ts index f765d84d69..3c969262b9 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -106,6 +106,10 @@ function createTestInjector() { }) }); testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub); + testInjector.register("filesHashService", { + generateHashes: () => Promise.resolve(), + getChanges: () => Promise.resolve({test: "testHash"}) + }); return testInjector; } diff --git a/test/project-changes-service.ts b/test/project-changes-service.ts index 47e8451bdd..60b36ffa69 100644 --- a/test/project-changes-service.ts +++ b/test/project-changes-service.ts @@ -30,6 +30,12 @@ class ProjectChangesServiceTest extends BaseServiceTest { this.injector.register("devicePlatformsConstants", {}); this.injector.register("devicePlatformsConstants", {}); this.injector.register("projectChangesService", ProjectChangesService); + this.injector.register("filesHashService", { + generateHashes: () => Promise.resolve({}) + }); + this.injector.register("logger", { + warn: () => ({}) + }); const fs = this.injector.resolve("fs"); fs.writeJson(path.join(this.projectDir, Constants.PACKAGE_JSON_FILE_NAME), { @@ -127,7 +133,8 @@ describe("Project Changes Service Tests", () => { changesRequireBuildTime: new Date().toString(), iOSProvisioningProfileUUID: "provisioning_profile_test", projectFileHash: "", - nativePlatformStatus: Constants.NativePlatformStatus.requiresPlatformAdd + nativePlatformStatus: Constants.NativePlatformStatus.requiresPlatformAdd, + appFilesHashes: {} }; fs.writeJson(prepareInfoPath, expectedPrepareInfo);