diff --git a/lib/constants.ts b/lib/constants.ts index 42e724c28c..5865093dad 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -219,3 +219,4 @@ export class AddPlaformErrors { } export const PLUGIN_BUILD_DATA_FILENAME = "plugin-data.json"; +export const PLUGINS_BUILD_DATA_FILENAME = ".ns-plugins-build-data.json"; diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 8277f74e5e..b08b9f0bc3 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -36,6 +36,7 @@ export class PluginsService implements IPluginsService { private $options: IOptions, private $logger: ILogger, private $errors: IErrors, + private $filesHashService: IFilesHashService, private $injector: IInjector) { } public async add(plugin: string, projectData: IProjectData): Promise { @@ -107,7 +108,7 @@ export class PluginsService implements IPluginsService { return await platformData.platformProjectService.validatePlugins(projectData); } - public async prepare(dependencyData: IDependencyData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { + public async prepare(dependencyData: IDependencyData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { platform = platform.toLowerCase(); const platformData = this.$platformsData.getPlatformData(platform, projectData); const pluginData = this.convertToPluginData(dependencyData, projectData.projectDir); @@ -141,9 +142,26 @@ export class PluginsService implements IPluginsService { public async preparePluginNativeCode(pluginData: IPluginData, platform: string, projectData: IProjectData): Promise { const platformData = this.$platformsData.getPlatformData(platform, projectData); - pluginData.pluginPlatformsFolderPath = (_platform: string) => path.join(pluginData.fullPath, "platforms", _platform); - await platformData.platformProjectService.preparePluginNativeCode(pluginData, projectData); + + const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(platform); + if (this.$fs.exists(pluginPlatformsFolderPath)) { + const pathToPluginsBuildFile = path.join(platformData.projectRoot, constants.PLUGINS_BUILD_DATA_FILENAME); + + const allPluginsNativeHashes = this.getAllPluginsNativeHashes(pathToPluginsBuildFile); + const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name]; + const currentPluginNativeHashes = await this.getPluginNativeHashes(pluginPlatformsFolderPath); + + if (!oldPluginNativeHashes || this.$filesHashService.hasChangesInShasums(oldPluginNativeHashes, currentPluginNativeHashes)) { + await platformData.platformProjectService.preparePluginNativeCode(pluginData, projectData); + this.setPluginNativeHashes({ + pathToPluginsBuildFile, + pluginData, + currentPluginNativeHashes, + allPluginsNativeHashes + }); + } + } } public async ensureAllDependenciesAreInstalled(projectData: IProjectData): Promise { @@ -307,6 +325,30 @@ export class PluginsService implements IPluginsService { return isValid; } + + private async getPluginNativeHashes(pluginPlatformsDir: string): Promise { + let data: IStringDictionary = {}; + if (this.$fs.exists(pluginPlatformsDir)) { + const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginPlatformsDir); + data = await this.$filesHashService.generateHashes(pluginNativeDataFiles); + } + + return data; + } + + private getAllPluginsNativeHashes(pathToPluginsBuildFile: string): IDictionary { + let data: IDictionary = {}; + if (this.$fs.exists(pathToPluginsBuildFile)) { + data = this.$fs.readJson(pathToPluginsBuildFile); + } + + return data; + } + + private setPluginNativeHashes(opts: { pathToPluginsBuildFile: string, pluginData: IPluginData, currentPluginNativeHashes: IStringDictionary, allPluginsNativeHashes: IDictionary }): void { + opts.allPluginsNativeHashes[opts.pluginData.name] = opts.currentPluginNativeHashes; + this.$fs.writeJson(opts.pathToPluginsBuildFile, opts.allPluginsNativeHashes); + } } $injector.register("pluginsService", PluginsService); diff --git a/test/plugins-service.ts b/test/plugins-service.ts index d49dbebaef..2da6707195 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -34,6 +34,7 @@ import { SettingsService } from "../lib/common/test/unit-tests/stubs"; import StaticConfigLib = require("../lib/config"); import * as path from "path"; import * as temp from "temp"; +import { PLUGINS_BUILD_DATA_FILENAME } from '../lib/constants'; temp.track(); let isErrorThrown = false; @@ -119,6 +120,10 @@ function createTestInjector() { testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub); testInjector.register("platformEnvironmentRequirements", {}); + testInjector.register("filesHashService", { + hasChangesInShasums: (oldPluginNativeHashes: IStringDictionary, currentPluginNativeHashes: IStringDictionary) => true, + generateHashes: async (files: string[]): Promise => ({}) + }); return testInjector; } @@ -541,4 +546,90 @@ describe("Plugins service", () => { await pluginsService.prepare(pluginJsonData, "android", projectData, {}); }); }); + + describe("preparePluginNativeCode", () => { + const setupTest = (opts: { hasChangesInShasums?: boolean, newPluginHashes?: IStringDictionary, buildDataFileExists?: boolean, hasPluginPlatformsDir?: boolean }): any => { + const testData: any = { + pluginsService: null, + isPreparePluginNativeCodeCalled: false, + dataPassedToWriteJson: null + }; + + const unitTestsInjector = new Yok(); + unitTestsInjector.register("platformsData", { + getPlatformData: (platform: string, projectData: IProjectData) => ({ + projectRoot: "projectRoot", + platformProjectService: { + preparePluginNativeCode: async (pluginData: IPluginData, projData: IProjectData) => { + testData.isPreparePluginNativeCodeCalled = true; + } + } + }) + }); + + const pluginHashes = opts.newPluginHashes || { "file1": "hash1" }; + const pluginData: IPluginData = { + fullPath: "plugin_full_path", + name: "plugin_name" + }; + + unitTestsInjector.register("filesHashService", { + hasChangesInShasums: (oldPluginNativeHashes: IStringDictionary, currentPluginNativeHashes: IStringDictionary) => !!opts.hasChangesInShasums, + generateHashes: async (files: string[]): Promise => pluginHashes + }); + + unitTestsInjector.register("fs", { + exists: (file: string) => { + if (file.indexOf(PLUGINS_BUILD_DATA_FILENAME) !== -1) { + return !!opts.buildDataFileExists; + } + + if (file.indexOf("platforms") !== -1) { + return !!opts.hasPluginPlatformsDir; + } + + return true; + }, + readJson: (file: string) => ({ + [pluginData.name]: pluginHashes + }), + writeJson: (file: string, json: any) => { testData.dataPassedToWriteJson = json; }, + enumerateFilesInDirectorySync: (): string[] => ["some_file"] + }); + + unitTestsInjector.register("npm", {}); + unitTestsInjector.register("options", {}); + unitTestsInjector.register("logger", {}); + unitTestsInjector.register("errors", {}); + unitTestsInjector.register("injector", unitTestsInjector); + + const pluginsService: PluginsService = unitTestsInjector.resolve(PluginsService); + testData.pluginsService = pluginsService; + testData.pluginData = pluginData; + return testData; + }; + + const platform = "platform"; + const projectData: IProjectData = {}; + + it("does not prepare the files when plugin does not have platforms dir", async () => { + const testData = setupTest({ hasPluginPlatformsDir: false }); + await testData.pluginsService.preparePluginNativeCode(testData.pluginData, platform, projectData); + assert.isFalse(testData.isPreparePluginNativeCodeCalled); + }); + + it("prepares the files when plugin has platforms dir and has not been built before", async () => { + const newPluginHashes = { "file": "hash" }; + const testData = setupTest({ newPluginHashes, hasPluginPlatformsDir: true }); + await testData.pluginsService.preparePluginNativeCode(testData.pluginData, platform, projectData); + assert.isTrue(testData.isPreparePluginNativeCodeCalled); + assert.deepEqual(testData.dataPassedToWriteJson, { [testData.pluginData.name]: newPluginHashes }); + }); + + it("does not prepare the files when plugin has platforms dir and has files has not changed since then", async () => { + const testData = setupTest({ hasChangesInShasums: false, buildDataFileExists: true, hasPluginPlatformsDir: true }); + await testData.pluginsService.preparePluginNativeCode(testData.pluginData, platform, projectData); + assert.isFalse(testData.isPreparePluginNativeCodeCalled); + }); + }); });