diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index e24cc7a529..fe32ed475b 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -234,3 +234,4 @@ $injector.require("npmConfigService", "./services/npm-config-service"); $injector.require("ipService", "./services/ip-service"); $injector.require("jsonFileSettingsService", "./common/services/json-file-settings-service"); $injector.require("markingModeService", "./services/marking-mode-service"); +$injector.require("metadataFilteringService", "./services/metadata-filtering-service"); diff --git a/lib/constants.ts b/lib/constants.ts index ae5279593e..45009ec50c 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -64,6 +64,11 @@ export const APPLICATION_RESPONSE_TIMEOUT_SECONDS = 60; export const NATIVE_EXTENSION_FOLDER = "extensions"; export const IOS_WATCHAPP_FOLDER = "watchapp"; export const IOS_WATCHAPP_EXTENSION_FOLDER = "watchextension"; +export class MetadataFilteringConstants { + static NATIVE_API_USAGE_FILE_NAME = "native-api-usage.json"; + static WHITELIST_FILE_NAME = "whitelist.mdg"; + static BLACKLIST_FILE_NAME = "blacklist.mdg"; +} export class PackageVersion { static NEXT = "next"; diff --git a/lib/definitions/metadata-filtering-service.ts b/lib/definitions/metadata-filtering-service.ts new file mode 100644 index 0000000000..bacc7d38f1 --- /dev/null +++ b/lib/definitions/metadata-filtering-service.ts @@ -0,0 +1,42 @@ +/** + * Describes service used to generate necessary files to filter the native metadata generation. + */ +interface INativeApiUsageConfiguartion { + /** + * Defines if the content of plugins' native-api-usage files will be used and included in the whitelist content. + */ + ["whitelist-plugins-usages"]: boolean; + + /** + * Defines APIs which will be inlcuded in the metadata. + */ + whitelist: string[]; + + /** + * Defines APIs which will be excluded from the metadata. + */ + blacklist: string[]; +} + +/** + * Describes the content of plugin's native-api-usage.json file located in `/platforms/ directory. + */ +interface INativeApiUsagePluginConfiguration { + /** + * Defines APIs which are used by the plugin and which should be whitelisted by the application using this plugin. + */ + uses: string[]; +} + +/** + * Describes service used to generate neccesary files to filter the metadata generation. + */ +interface IMetadataFilteringService { + /** + * Cleans old metadata filters and creates new ones for the current project and platform. + * @param {IProjectData} projectData Information about the current project. + * @param {string} platform The platform for which metadata should be generated. + * @returns {void} + */ + generateMetadataFilters(projectData: IProjectData, platform: string): void; +} diff --git a/lib/services/metadata-filtering-service.ts b/lib/services/metadata-filtering-service.ts new file mode 100644 index 0000000000..dbc3fb2a61 --- /dev/null +++ b/lib/services/metadata-filtering-service.ts @@ -0,0 +1,95 @@ +import * as path from "path"; +import * as os from "os"; +import { MetadataFilteringConstants } from "../constants"; + +export class MetadataFilteringService implements IMetadataFilteringService { + constructor(private $fs: IFileSystem, + private $pluginsService: IPluginsService, + private $mobileHelper: Mobile.IMobileHelper, + private $platformsDataService: IPlatformsDataService, + private $logger: ILogger) { } + + public generateMetadataFilters(projectData: IProjectData, platform: string): void { + this.generateWhitelist(projectData, platform); + this.generateBlacklist(projectData, platform); + } + + private generateWhitelist(projectData: IProjectData, platform: string): void { + const platformsDirPath = this.getPlatformsDirPath(projectData, platform); + const pathToWhitelistFile = path.join(platformsDirPath, MetadataFilteringConstants.WHITELIST_FILE_NAME); + this.$fs.deleteFile(pathToWhitelistFile); + + const nativeApiConfiguration = this.getNativeApiConfigurationForPlatform(projectData, platform); + if (nativeApiConfiguration) { + const whitelistedItems: string[] = []; + if (nativeApiConfiguration["whitelist-plugins-usages"]) { + const plugins = this.$pluginsService.getAllProductionPlugins(projectData); + for (const pluginData of plugins) { + const pathToPlatformsDir = pluginData.pluginPlatformsFolderPath(platform); + const pathToPluginsMetadataConfig = path.join(pathToPlatformsDir, MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME); + if (this.$fs.exists(pathToPluginsMetadataConfig)) { + const pluginConfig: INativeApiUsagePluginConfiguration = this.$fs.readJson(pathToPluginsMetadataConfig) || {}; + this.$logger.trace(`Adding content of ${pathToPluginsMetadataConfig} to whitelisted items of metadata filtering: ${JSON.stringify(pluginConfig, null, 2)}`); + const itemsToAdd = pluginConfig.uses || []; + if (itemsToAdd.length) { + whitelistedItems.push(`// Added from: ${pathToPluginsMetadataConfig}`); + whitelistedItems.push(...itemsToAdd); + whitelistedItems.push(`// Finished part from ${pathToPluginsMetadataConfig}${os.EOL}`); + } + } + } + } + + const applicationWhitelistedItems = nativeApiConfiguration.whitelist || []; + if (applicationWhitelistedItems.length) { + this.$logger.trace(`Adding content from application to whitelisted items of metadata filtering: ${JSON.stringify(applicationWhitelistedItems, null, 2)}`); + + whitelistedItems.push(`// Added from application`); + whitelistedItems.push(...applicationWhitelistedItems); + whitelistedItems.push(`// Finished part from application${os.EOL}`); + } + + if (whitelistedItems.length) { + this.$fs.writeFile(pathToWhitelistFile, whitelistedItems.join(os.EOL)); + } + } + } + + private generateBlacklist(projectData: IProjectData, platform: string): void { + const platformsDirPath = this.getPlatformsDirPath(projectData, platform); + const pathToBlacklistFile = path.join(platformsDirPath, MetadataFilteringConstants.BLACKLIST_FILE_NAME); + this.$fs.deleteFile(pathToBlacklistFile); + + const nativeApiConfiguration = this.getNativeApiConfigurationForPlatform(projectData, platform); + if (nativeApiConfiguration) { + const blacklistedItems: string[] = nativeApiConfiguration.blacklist || []; + + if (blacklistedItems.length) { + this.$fs.writeFile(pathToBlacklistFile, blacklistedItems.join(os.EOL)); + } + } else { + this.$logger.trace(`There's no application configuration for metadata filtering for platform ${platform}. Full metadata will be generated.`); + } + } + + private getNativeApiConfigurationForPlatform(projectData: IProjectData, platform: string): INativeApiUsageConfiguartion { + let config: INativeApiUsageConfiguartion = null; + const pathToApplicationConfigurationFile = this.getPathToApplicationConfigurationForPlatform(projectData, platform); + if (this.$fs.exists(pathToApplicationConfigurationFile)) { + config = this.$fs.readJson(pathToApplicationConfigurationFile); + } + + return config; + } + + private getPlatformsDirPath(projectData: IProjectData, platform: string): string { + const platformData = this.$platformsDataService.getPlatformData(platform, projectData); + return platformData.projectRoot; + } + + private getPathToApplicationConfigurationForPlatform(projectData: IProjectData, platform: string): string { + return path.join(projectData.appResourcesDirectoryPath, this.$mobileHelper.normalizePlatformName(platform), MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME); + } +} + +$injector.register("metadataFilteringService", MetadataFilteringService); diff --git a/lib/services/platform/prepare-native-platform-service.ts b/lib/services/platform/prepare-native-platform-service.ts index c70a93cd1b..c4af534f17 100644 --- a/lib/services/platform/prepare-native-platform-service.ts +++ b/lib/services/platform/prepare-native-platform-service.ts @@ -9,6 +9,7 @@ export class PrepareNativePlatformService implements IPrepareNativePlatformServi public $hooksService: IHooksService, private $nodeModulesBuilder: INodeModulesBuilder, private $projectChangesService: IProjectChangesService, + private $metadataFilteringService: IMetadataFilteringService ) { } @performanceLog() @@ -37,12 +38,13 @@ export class PrepareNativePlatformService implements IPrepareNativePlatformServi } if (hasNativeModulesChange) { - await this.$nodeModulesBuilder.prepareNodeModules({platformData, projectData}); + await this.$nodeModulesBuilder.prepareNodeModules({ platformData, projectData }); } if (hasNativeModulesChange || hasConfigChange) { await platformData.platformProjectService.processConfigurationFilesFromAppResources(projectData, { release }); await platformData.platformProjectService.handleNativeDependenciesChange(projectData, { release }); + this.$metadataFilteringService.generateMetadataFilters(projectData, platformData.platformNameLowerCase); } platformData.platformProjectService.interpolateConfigurationFile(projectData); diff --git a/test/services/metadata-filtering-service.ts b/test/services/metadata-filtering-service.ts new file mode 100644 index 0000000000..191594ffb9 --- /dev/null +++ b/test/services/metadata-filtering-service.ts @@ -0,0 +1,197 @@ +import { MetadataFilteringService } from "../../lib/services/metadata-filtering-service"; +import { Yok } from "../../lib/common/yok"; +import { LoggerStub, FileSystemStub } from "../stubs"; +import { assert } from "chai"; +import * as path from "path"; +import { MetadataFilteringConstants } from "../../lib/constants"; +import { EOL } from "os"; + +describe("metadataFilteringService", () => { + const platform = "platform"; + const projectDir = "projectDir"; + const projectRoot = path.join(projectDir, "platforms", platform); + const projectData: any = { + appResourcesDirectoryPath: path.join(projectDir, "App_Resources") + }; + const blacklistArray: string[] = ["blacklisted1", "blacklisted2"]; + const whitelistArray: string[] = ["whitelisted1", "whitelisted2"]; + const appResourcesNativeApiUsageFilePath = path.join(projectData.appResourcesDirectoryPath, platform, MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME); + const pluginPlatformsDir = path.join("pluginDir", platform); + const pluginNativeApiUsageFilePath = path.join(pluginPlatformsDir, MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME); + const pluginsUses: string[] = ["pluginUses1", "pluginUses2"]; + + const createTestInjector = (input?: { hasPlugins: boolean }): IInjector => { + const testInjector = new Yok(); + testInjector.register("logger", LoggerStub); + testInjector.register("fs", FileSystemStub); + testInjector.register("pluginsService", { + getAllProductionPlugins: (prjData: IProjectData, dependencies?: IDependencyData[]): IPluginData[] => { + const plugins = !!(input && input.hasPlugins) ? [ + { + pluginPlatformsFolderPath: (pl: string) => pluginPlatformsDir + } + ] : []; + + return plugins; + } + }); + testInjector.register("mobileHelper", { + normalizePlatformName: (pl: string) => pl + }); + testInjector.register("platformsDataService", { + getPlatformData: (pl: string, prjData: IProjectData): IPlatformData => ({ projectRoot }) + }); + return testInjector; + }; + + describe("generateMetadataFilters", () => { + const mockFs = (input: { testInjector: IInjector, readJsonData?: any, writeFileAction?: (filePath: string, data: string) => void, existingFiles?: any[] }): { fs: FileSystemStub, dataWritten: IDictionary } => { + const fs = input.testInjector.resolve("fs"); + const dataWritten: IDictionary = {}; + + if (input.writeFileAction) { + fs.writeFile = (filePath: string, data: string) => input.writeFileAction(filePath, data); + } else { + fs.writeFile = (filePath: string, data: string) => dataWritten[filePath] = data; + } + + if (input.readJsonData) { + fs.readJson = (filePath: string) => input.readJsonData[filePath]; + } + + if (input.existingFiles) { + fs.exists = (filePath: string) => input.existingFiles.indexOf(filePath) !== -1; + } + + return { fs, dataWritten }; + }; + + it("deletes previously generated files for metadata filtering", () => { + const testInjector = createTestInjector(); + const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService); + const { fs } = mockFs({ + testInjector, writeFileAction: (filePath: string, data: string) => { + throw new Error(`No data should be written when the ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} does not exist in App_Resource/`); + } + }); + + metadataFilteringService.generateMetadataFilters(projectData, platform); + + const expectedDeletedFiles = [ + path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME), + path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME) + ]; + assert.deepEqual(fs.deletedFiles, expectedDeletedFiles); + }); + + it(`generates ${MetadataFilteringConstants.BLACKLIST_FILE_NAME} when the file ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} exists in App_Resources/`, () => { + const testInjector = createTestInjector(); + const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService); + const { dataWritten } = mockFs({ + testInjector, + existingFiles: [appResourcesNativeApiUsageFilePath], + readJsonData: { [`${appResourcesNativeApiUsageFilePath}`]: { blacklist: blacklistArray } } + }); + + metadataFilteringService.generateMetadataFilters(projectData, platform); + + assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL) }); + }); + + const getExpectedWhitelistContent = (input: { applicationWhitelist?: string[], pluginWhitelist?: string[] }): string => { + let finalContent = ""; + if (input.pluginWhitelist) { + finalContent += `// Added from: ${pluginNativeApiUsageFilePath}${EOL}${input.pluginWhitelist.join(EOL)}${EOL}// Finished part from ${pluginNativeApiUsageFilePath}${EOL}`; + } + + if (input.applicationWhitelist) { + if (finalContent !== "") { + finalContent += EOL; + } + + finalContent += `// Added from application${EOL}${input.applicationWhitelist.join(EOL)}${EOL}// Finished part from application${EOL}`; + } + + return finalContent; + }; + + it(`generates ${MetadataFilteringConstants.WHITELIST_FILE_NAME} when the file ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} exists in App_Resources/`, () => { + const testInjector = createTestInjector(); + const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService); + const { dataWritten } = mockFs({ + testInjector, + existingFiles: [appResourcesNativeApiUsageFilePath], + readJsonData: { [`${appResourcesNativeApiUsageFilePath}`]: { whitelist: whitelistArray } }, + }); + + metadataFilteringService.generateMetadataFilters(projectData, platform); + assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: getExpectedWhitelistContent({ applicationWhitelist: whitelistArray }) }); + }); + + it(`generates ${MetadataFilteringConstants.WHITELIST_FILE_NAME} with content from plugins when the file ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} exists in App_Resources/ and whitelist-plugins-usages is true`, () => { + const testInjector = createTestInjector({ hasPlugins: true }); + const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService); + const { dataWritten } = mockFs({ + testInjector, + existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath], + readJsonData: { + [`${appResourcesNativeApiUsageFilePath}`]: { ["whitelist-plugins-usages"]: true }, + [`${pluginNativeApiUsageFilePath}`]: { uses: whitelistArray } + }, + }); + + metadataFilteringService.generateMetadataFilters(projectData, platform); + assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: getExpectedWhitelistContent({ pluginWhitelist: whitelistArray }) }); + }); + + it(`generates all files when both plugins and applications filters are included`, () => { + const testInjector = createTestInjector({ hasPlugins: true }); + const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService); + const { dataWritten } = mockFs({ + testInjector, + existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath], + readJsonData: { + [`${appResourcesNativeApiUsageFilePath}`]: { + whitelist: whitelistArray, + blacklist: blacklistArray, + ["whitelist-plugins-usages"]: true + }, + [`${pluginNativeApiUsageFilePath}`]: { uses: pluginsUses } + }, + }); + + metadataFilteringService.generateMetadataFilters(projectData, platform); + const expectedWhitelist = getExpectedWhitelistContent({ pluginWhitelist: pluginsUses, applicationWhitelist: whitelistArray }); + + assert.deepEqual(dataWritten, { + [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: expectedWhitelist, + [path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL) + }); + }); + + it(`skips plugins ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} files when whitelist-plugins-usages in App_Resources is false`, () => { + const testInjector = createTestInjector({ hasPlugins: true }); + const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService); + const { dataWritten } = mockFs({ + testInjector, + existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath], + readJsonData: { + [`${appResourcesNativeApiUsageFilePath}`]: { + whitelist: whitelistArray, + blacklist: blacklistArray, + ["whitelist-plugins-usages"]: false + }, + [`${pluginNativeApiUsageFilePath}`]: { uses: pluginsUses } + }, + }); + + metadataFilteringService.generateMetadataFilters(projectData, "platform"); + const expectedWhitelist = getExpectedWhitelistContent({ applicationWhitelist: whitelistArray }); + + assert.deepEqual(dataWritten, { + [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: expectedWhitelist, + [path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL) + }); + }); + }); +}); diff --git a/test/stubs.ts b/test/stubs.ts index af3d18a719..9dc2649398 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -46,6 +46,7 @@ export class LoggerStub implements ILogger { export class FileSystemStub implements IFileSystem { public fsStatCache: IDictionary = {}; + public deletedFiles: string[] = []; deleteDirectorySafe(directory: string): void { return this.deleteDirectory(directory); } @@ -62,10 +63,12 @@ export class FileSystemStub implements IFileSystem { } deleteFile(path: string): void { + this.deletedFiles.push(path); return undefined; } deleteDirectory(directory: string): void { + this.deletedFiles.push(directory); return undefined; }