Skip to content

Commit 03ea822

Browse files
feat: add support for metadata filtering
The new versions of runtimes (6.4.x and newer) will support filtering of metadata generation. To achieve this they need input from CLI - two files in `<project dir>/platforms/<platform>` directory. Add new service in CLI to generate the required files. Whenever the service is called, the old files (from previous execution) will be cleaned and full generation will be triggered.
1 parent 25a4046 commit 03ea822

7 files changed

+323
-1
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,4 @@ $injector.require("npmConfigService", "./services/npm-config-service");
234234
$injector.require("ipService", "./services/ip-service");
235235
$injector.require("jsonFileSettingsService", "./common/services/json-file-settings-service");
236236
$injector.require("markingModeService", "./services/marking-mode-service");
237+
$injector.require("metadataFilteringService", "./services/metadata-filtering-service");

lib/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export const APPLICATION_RESPONSE_TIMEOUT_SECONDS = 60;
6464
export const NATIVE_EXTENSION_FOLDER = "extensions";
6565
export const IOS_WATCHAPP_FOLDER = "watchapp";
6666
export const IOS_WATCHAPP_EXTENSION_FOLDER = "watchextension";
67+
export class MetadataFilteringConstants {
68+
static NATIVE_API_USAGE_FILE_NAME = "native-api-usage.json";
69+
static WHITELIST_FILE_NAME = "whitelist.mdg";
70+
static BLACKLIST_FILE_NAME = "blacklist.mdg";
71+
}
6772

6873
export class PackageVersion {
6974
static NEXT = "next";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Describes the content of the native-api-usage.json file located in `<path to project's App_Resources>/<platform> directory.
3+
*/
4+
interface INativeApiUsageConfiguartion {
5+
/**
6+
* Defines if the content of plugins' native-api-usage files will be used and included in the whitelist content.
7+
*/
8+
["whitelist-plugins-usages"]: boolean;
9+
10+
/**
11+
* Defines APIs which will be inlcuded in the metadata.
12+
*/
13+
whitelist: string[];
14+
15+
/**
16+
* Defines APIs which will be excluded from the metadata.
17+
*/
18+
blacklist: string[];
19+
}
20+
21+
/**
22+
* Describes the content of plugin's native-api-usage.json file located in `<path to plugin>/platforms/<platform> directory.
23+
*/
24+
interface INativeApiUsagePluginConfiguration {
25+
/**
26+
* Defines APIs which are used by the plugin and which should be whitelisted by the application using this plugin.
27+
*/
28+
uses: string[];
29+
}
30+
31+
/**
32+
* Describes service used to generate neccesary files to filter the metadata generation.
33+
*/
34+
interface IMetadataFilteringService {
35+
/**
36+
* Cleans old metadata filters and creates new ones for the current project and platform.
37+
* @param {IProjectData} projectData Information about the current project.
38+
* @param {string} platform The platform for which metadata should be generated.
39+
* @returns {void}
40+
*/
41+
generateMetadataFilters(projectData: IProjectData, platform: string): void;
42+
}
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as path from "path";
2+
import * as os from "os";
3+
import { MetadataFilteringConstants } from "../constants";
4+
5+
export class MetadataFilteringService implements IMetadataFilteringService {
6+
constructor(private $fs: IFileSystem,
7+
private $pluginsService: IPluginsService,
8+
private $mobileHelper: Mobile.IMobileHelper,
9+
private $platformsDataService: IPlatformsDataService,
10+
private $logger: ILogger) { }
11+
12+
public generateMetadataFilters(projectData: IProjectData, platform: string): void {
13+
this.generateWhitelist(projectData, platform);
14+
this.generateBlacklist(projectData, platform);
15+
}
16+
17+
private generateWhitelist(projectData: IProjectData, platform: string): void {
18+
const platformsDirPath = this.getPlatformsDirPath(projectData, platform);
19+
const pathToWhitelistFile = path.join(platformsDirPath, MetadataFilteringConstants.WHITELIST_FILE_NAME);
20+
this.$fs.deleteFile(pathToWhitelistFile);
21+
22+
const nativeApiConfiguration = this.getNativeApiConfigurationForPlatform(projectData, platform);
23+
if (nativeApiConfiguration) {
24+
const whitelistedItems: string[] = [];
25+
if (nativeApiConfiguration["whitelist-plugins-usages"]) {
26+
const plugins = this.$pluginsService.getAllProductionPlugins(projectData);
27+
for (const pluginData of plugins) {
28+
const pathToPlatformsDir = pluginData.pluginPlatformsFolderPath(platform);
29+
const pathToPluginsMetadataConfig = path.join(pathToPlatformsDir, MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME);
30+
if (this.$fs.exists(pathToPluginsMetadataConfig)) {
31+
const pluginConfig: INativeApiUsagePluginConfiguration = this.$fs.readJson(pathToPluginsMetadataConfig) || {};
32+
this.$logger.trace(`Adding content of ${pathToPluginsMetadataConfig} to whitelisted items of metadata filtering.`);
33+
whitelistedItems.push(...(pluginConfig.uses || []));
34+
}
35+
}
36+
}
37+
38+
whitelistedItems.push(...(nativeApiConfiguration.whitelist || []));
39+
40+
if (whitelistedItems.length) {
41+
this.$fs.writeFile(pathToWhitelistFile, _.uniq(whitelistedItems).sort().join(os.EOL));
42+
}
43+
}
44+
}
45+
46+
private generateBlacklist(projectData: IProjectData, platform: string): void {
47+
const platformsDirPath = this.getPlatformsDirPath(projectData, platform);
48+
const pathToBlacklistFile = path.join(platformsDirPath, MetadataFilteringConstants.BLACKLIST_FILE_NAME);
49+
this.$fs.deleteFile(pathToBlacklistFile);
50+
51+
const nativeApiConfiguration = this.getNativeApiConfigurationForPlatform(projectData, platform);
52+
if (nativeApiConfiguration) {
53+
const blacklistedItems: string[] = nativeApiConfiguration.blacklist || [];
54+
55+
if (blacklistedItems.length) {
56+
this.$fs.writeFile(pathToBlacklistFile, _.uniq(blacklistedItems).sort().join(os.EOL));
57+
}
58+
} else {
59+
this.$logger.trace(`There's no application configuration for metadata filtering for platform ${platform}. Full metadata will be generated.`);
60+
}
61+
}
62+
63+
private getNativeApiConfigurationForPlatform(projectData: IProjectData, platform: string): INativeApiUsageConfiguartion {
64+
let config: INativeApiUsageConfiguartion = null;
65+
const pathToApplicationConfigurationFile = this.getPathToApplicationConfigurationForPlatform(projectData, platform);
66+
if (this.$fs.exists(pathToApplicationConfigurationFile)) {
67+
config = this.$fs.readJson(pathToApplicationConfigurationFile);
68+
}
69+
70+
return config;
71+
}
72+
73+
private getPlatformsDirPath(projectData: IProjectData, platform: string): string {
74+
const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
75+
return platformData.projectRoot;
76+
}
77+
78+
private getPathToApplicationConfigurationForPlatform(projectData: IProjectData, platform: string): string {
79+
return path.join(projectData.appResourcesDirectoryPath, this.$mobileHelper.normalizePlatformName(platform), MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME);
80+
}
81+
}
82+
83+
$injector.register("metadataFilteringService", MetadataFilteringService);

lib/services/platform/prepare-native-platform-service.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class PrepareNativePlatformService implements IPrepareNativePlatformServi
99
public $hooksService: IHooksService,
1010
private $nodeModulesBuilder: INodeModulesBuilder,
1111
private $projectChangesService: IProjectChangesService,
12+
private $metadataFilteringService: IMetadataFilteringService
1213
) { }
1314

1415
@performanceLog()
@@ -37,12 +38,13 @@ export class PrepareNativePlatformService implements IPrepareNativePlatformServi
3738
}
3839

3940
if (hasNativeModulesChange) {
40-
await this.$nodeModulesBuilder.prepareNodeModules({platformData, projectData});
41+
await this.$nodeModulesBuilder.prepareNodeModules({ platformData, projectData });
4142
}
4243

4344
if (hasNativeModulesChange || hasConfigChange) {
4445
await platformData.platformProjectService.processConfigurationFilesFromAppResources(projectData, { release });
4546
await platformData.platformProjectService.handleNativeDependenciesChange(projectData, { release });
47+
this.$metadataFilteringService.generateMetadataFilters(projectData, platformData.platformNameLowerCase);
4648
}
4749

4850
platformData.platformProjectService.interpolateConfigurationFile(projectData);
+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { MetadataFilteringService } from "../../lib/services/metadata-filtering-service";
2+
import { Yok } from "../../lib/common/yok";
3+
import { LoggerStub, FileSystemStub } from "../stubs";
4+
import { assert } from "chai";
5+
import * as path from "path";
6+
import { MetadataFilteringConstants } from "../../lib/constants";
7+
import { EOL } from "os";
8+
9+
describe("metadataFilteringService", () => {
10+
const platform = "platform";
11+
const projectDir = "projectDir";
12+
const projectRoot = path.join(projectDir, "platforms", platform);
13+
const projectData: any = {
14+
appResourcesDirectoryPath: path.join(projectDir, "App_Resources")
15+
};
16+
const blacklistArray: string[] = ["blacklisted1", "blacklisted2"];
17+
const whitelistArray: string[] = ["whitelisted1", "whitelisted2"];
18+
const appResourcesNativeApiUsageFilePath = path.join(projectData.appResourcesDirectoryPath, platform, MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME);
19+
const pluginPlatformsDir = path.join("pluginDir", platform);
20+
const pluginNativeApiUsageFilePath = path.join(pluginPlatformsDir, MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME);
21+
const pluginsUses: string[] = ["whitelisted1", "pluginUses1", "pluginUses2"];
22+
23+
const createTestInjector = (input?: { hasPlugins: boolean }): IInjector => {
24+
const testInjector = new Yok();
25+
testInjector.register("logger", LoggerStub);
26+
testInjector.register("fs", FileSystemStub);
27+
testInjector.register("pluginsService", {
28+
getAllProductionPlugins: (prjData: IProjectData, dependencies?: IDependencyData[]): IPluginData[] => {
29+
const plugins = !!(input && input.hasPlugins) ? [
30+
<any>{
31+
pluginPlatformsFolderPath: (pl: string) => pluginPlatformsDir
32+
}
33+
] : [];
34+
35+
return plugins;
36+
}
37+
});
38+
testInjector.register("mobileHelper", {
39+
normalizePlatformName: (pl: string) => pl
40+
});
41+
testInjector.register("platformsDataService", {
42+
getPlatformData: (pl: string, prjData: IProjectData): IPlatformData => (<any>{ projectRoot })
43+
});
44+
return testInjector;
45+
};
46+
47+
describe("generateMetadataFilters", () => {
48+
const mockFs = (input: { testInjector: IInjector, readJsonData?: any, writeFileAction?: (filePath: string, data: string) => void, existingFiles?: any[] }): { fs: FileSystemStub, dataWritten: IDictionary<any> } => {
49+
const fs = input.testInjector.resolve<FileSystemStub>("fs");
50+
const dataWritten: IDictionary<any> = {};
51+
52+
if (input.writeFileAction) {
53+
fs.writeFile = (filePath: string, data: string) => input.writeFileAction(filePath, data);
54+
} else {
55+
fs.writeFile = (filePath: string, data: string) => dataWritten[filePath] = data;
56+
}
57+
58+
if (input.readJsonData) {
59+
fs.readJson = (filePath: string) => input.readJsonData[filePath];
60+
}
61+
62+
if (input.existingFiles) {
63+
fs.exists = (filePath: string) => input.existingFiles.indexOf(filePath) !== -1;
64+
}
65+
66+
return { fs, dataWritten };
67+
};
68+
69+
it("deletes previously generated files for metadata filtering", () => {
70+
const testInjector = createTestInjector();
71+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
72+
const { fs } = mockFs({
73+
testInjector, writeFileAction: (filePath: string, data: string) => {
74+
throw new Error(`No data should be written when the ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} does not exist in App_Resource/<platform>`);
75+
}
76+
});
77+
78+
metadataFilteringService.generateMetadataFilters(projectData, platform);
79+
80+
const expectedDeletedFiles = [
81+
path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME),
82+
path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)
83+
];
84+
assert.deepEqual(fs.deletedFiles, expectedDeletedFiles);
85+
});
86+
87+
it(`generates ${MetadataFilteringConstants.BLACKLIST_FILE_NAME} when the file ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} exists in App_Resources/<platform>`, () => {
88+
const testInjector = createTestInjector();
89+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
90+
const { dataWritten } = mockFs({
91+
testInjector,
92+
existingFiles: [appResourcesNativeApiUsageFilePath],
93+
readJsonData: { [`${appResourcesNativeApiUsageFilePath}`]: { blacklist: blacklistArray } }
94+
});
95+
96+
metadataFilteringService.generateMetadataFilters(projectData, platform);
97+
98+
assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL) });
99+
});
100+
101+
it(`generates ${MetadataFilteringConstants.WHITELIST_FILE_NAME} when the file ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} exists in App_Resources/<platform>`, () => {
102+
const testInjector = createTestInjector();
103+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
104+
const { dataWritten } = mockFs({
105+
testInjector,
106+
existingFiles: [appResourcesNativeApiUsageFilePath],
107+
readJsonData: { [`${appResourcesNativeApiUsageFilePath}`]: { whitelist: whitelistArray } },
108+
});
109+
110+
metadataFilteringService.generateMetadataFilters(projectData, platform);
111+
assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: whitelistArray.join(EOL) });
112+
});
113+
114+
it(`generates ${MetadataFilteringConstants.WHITELIST_FILE_NAME} with content from plugins when the file ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} exists in App_Resources/<platform> and whitelist-plugins-usages is true`, () => {
115+
const testInjector = createTestInjector({ hasPlugins: true });
116+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
117+
const { dataWritten } = mockFs({
118+
testInjector,
119+
existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath],
120+
readJsonData: {
121+
[`${appResourcesNativeApiUsageFilePath}`]: { ["whitelist-plugins-usages"]: true },
122+
[`${pluginNativeApiUsageFilePath}`]: { uses: whitelistArray }
123+
},
124+
});
125+
126+
metadataFilteringService.generateMetadataFilters(projectData, platform);
127+
assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: whitelistArray.join(EOL) });
128+
});
129+
130+
it(`generates all files when both plugins and applications filters are included`, () => {
131+
const testInjector = createTestInjector({ hasPlugins: true });
132+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
133+
const { dataWritten } = mockFs({
134+
testInjector,
135+
existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath],
136+
readJsonData: {
137+
[`${appResourcesNativeApiUsageFilePath}`]: {
138+
whitelist: whitelistArray,
139+
blacklist: blacklistArray,
140+
["whitelist-plugins-usages"]: true
141+
},
142+
[`${pluginNativeApiUsageFilePath}`]: { uses: pluginsUses }
143+
},
144+
});
145+
146+
metadataFilteringService.generateMetadataFilters(projectData, platform);
147+
const expectedWhitelist = [
148+
"pluginUses1",
149+
"pluginUses2",
150+
...whitelistArray
151+
].join(EOL);
152+
153+
assert.deepEqual(dataWritten, {
154+
[path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: expectedWhitelist,
155+
[path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL)
156+
});
157+
});
158+
159+
it(`skips plugins ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} files when whitelist-plugins-usages in App_Resources is false`, () => {
160+
const testInjector = createTestInjector({ hasPlugins: true });
161+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
162+
const { dataWritten } = mockFs({
163+
testInjector,
164+
existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath],
165+
readJsonData: {
166+
[`${appResourcesNativeApiUsageFilePath}`]: {
167+
whitelist: whitelistArray,
168+
blacklist: blacklistArray,
169+
["whitelist-plugins-usages"]: false
170+
},
171+
[`${pluginNativeApiUsageFilePath}`]: { uses: pluginsUses }
172+
},
173+
});
174+
175+
metadataFilteringService.generateMetadataFilters(projectData, "platform");
176+
const expectedWhitelist = [
177+
...whitelistArray
178+
].join(EOL);
179+
180+
assert.deepEqual(dataWritten, {
181+
[path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: expectedWhitelist,
182+
[path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL)
183+
});
184+
});
185+
});
186+
});

test/stubs.ts

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export class LoggerStub implements ILogger {
4646

4747
export class FileSystemStub implements IFileSystem {
4848
public fsStatCache: IDictionary<IFsStats> = {};
49+
public deletedFiles: string[] = [];
4950
deleteDirectorySafe(directory: string): void {
5051
return this.deleteDirectory(directory);
5152
}
@@ -62,10 +63,12 @@ export class FileSystemStub implements IFileSystem {
6263
}
6364

6465
deleteFile(path: string): void {
66+
this.deletedFiles.push(path);
6567
return undefined;
6668
}
6769

6870
deleteDirectory(directory: string): void {
71+
this.deletedFiles.push(directory);
6972
return undefined;
7073
}
7174

0 commit comments

Comments
 (0)