Skip to content

Commit 39cc606

Browse files
Merge pull request #5224 from NativeScript/vladimirov/metadata-filtering
feat: add support for metadata filtering
2 parents 25a4046 + 46f5b91 commit 39cc606

File tree

7 files changed

+346
-1
lines changed

7 files changed

+346
-1
lines changed

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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";
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Describes service used to generate necessary files to filter the native metadata generation.
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+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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: ${JSON.stringify(pluginConfig, null, 2)}`);
33+
const itemsToAdd = pluginConfig.uses || [];
34+
if (itemsToAdd.length) {
35+
whitelistedItems.push(`// Added from: ${pathToPluginsMetadataConfig}`);
36+
whitelistedItems.push(...itemsToAdd);
37+
whitelistedItems.push(`// Finished part from ${pathToPluginsMetadataConfig}${os.EOL}`);
38+
}
39+
}
40+
}
41+
}
42+
43+
const applicationWhitelistedItems = nativeApiConfiguration.whitelist || [];
44+
if (applicationWhitelistedItems.length) {
45+
this.$logger.trace(`Adding content from application to whitelisted items of metadata filtering: ${JSON.stringify(applicationWhitelistedItems, null, 2)}`);
46+
47+
whitelistedItems.push(`// Added from application`);
48+
whitelistedItems.push(...applicationWhitelistedItems);
49+
whitelistedItems.push(`// Finished part from application${os.EOL}`);
50+
}
51+
52+
if (whitelistedItems.length) {
53+
this.$fs.writeFile(pathToWhitelistFile, whitelistedItems.join(os.EOL));
54+
}
55+
}
56+
}
57+
58+
private generateBlacklist(projectData: IProjectData, platform: string): void {
59+
const platformsDirPath = this.getPlatformsDirPath(projectData, platform);
60+
const pathToBlacklistFile = path.join(platformsDirPath, MetadataFilteringConstants.BLACKLIST_FILE_NAME);
61+
this.$fs.deleteFile(pathToBlacklistFile);
62+
63+
const nativeApiConfiguration = this.getNativeApiConfigurationForPlatform(projectData, platform);
64+
if (nativeApiConfiguration) {
65+
const blacklistedItems: string[] = nativeApiConfiguration.blacklist || [];
66+
67+
if (blacklistedItems.length) {
68+
this.$fs.writeFile(pathToBlacklistFile, blacklistedItems.join(os.EOL));
69+
}
70+
} else {
71+
this.$logger.trace(`There's no application configuration for metadata filtering for platform ${platform}. Full metadata will be generated.`);
72+
}
73+
}
74+
75+
private getNativeApiConfigurationForPlatform(projectData: IProjectData, platform: string): INativeApiUsageConfiguartion {
76+
let config: INativeApiUsageConfiguartion = null;
77+
const pathToApplicationConfigurationFile = this.getPathToApplicationConfigurationForPlatform(projectData, platform);
78+
if (this.$fs.exists(pathToApplicationConfigurationFile)) {
79+
config = this.$fs.readJson(pathToApplicationConfigurationFile);
80+
}
81+
82+
return config;
83+
}
84+
85+
private getPlatformsDirPath(projectData: IProjectData, platform: string): string {
86+
const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
87+
return platformData.projectRoot;
88+
}
89+
90+
private getPathToApplicationConfigurationForPlatform(projectData: IProjectData, platform: string): string {
91+
return path.join(projectData.appResourcesDirectoryPath, this.$mobileHelper.normalizePlatformName(platform), MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME);
92+
}
93+
}
94+
95+
$injector.register("metadataFilteringService", MetadataFilteringService);

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

Lines changed: 3 additions & 1 deletion
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);
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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[] = ["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+
const getExpectedWhitelistContent = (input: { applicationWhitelist?: string[], pluginWhitelist?: string[] }): string => {
102+
let finalContent = "";
103+
if (input.pluginWhitelist) {
104+
finalContent += `// Added from: ${pluginNativeApiUsageFilePath}${EOL}${input.pluginWhitelist.join(EOL)}${EOL}// Finished part from ${pluginNativeApiUsageFilePath}${EOL}`;
105+
}
106+
107+
if (input.applicationWhitelist) {
108+
if (finalContent !== "") {
109+
finalContent += EOL;
110+
}
111+
112+
finalContent += `// Added from application${EOL}${input.applicationWhitelist.join(EOL)}${EOL}// Finished part from application${EOL}`;
113+
}
114+
115+
return finalContent;
116+
};
117+
118+
it(`generates ${MetadataFilteringConstants.WHITELIST_FILE_NAME} when the file ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} exists in App_Resources/<platform>`, () => {
119+
const testInjector = createTestInjector();
120+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
121+
const { dataWritten } = mockFs({
122+
testInjector,
123+
existingFiles: [appResourcesNativeApiUsageFilePath],
124+
readJsonData: { [`${appResourcesNativeApiUsageFilePath}`]: { whitelist: whitelistArray } },
125+
});
126+
127+
metadataFilteringService.generateMetadataFilters(projectData, platform);
128+
assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: getExpectedWhitelistContent({ applicationWhitelist: whitelistArray }) });
129+
});
130+
131+
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`, () => {
132+
const testInjector = createTestInjector({ hasPlugins: true });
133+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
134+
const { dataWritten } = mockFs({
135+
testInjector,
136+
existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath],
137+
readJsonData: {
138+
[`${appResourcesNativeApiUsageFilePath}`]: { ["whitelist-plugins-usages"]: true },
139+
[`${pluginNativeApiUsageFilePath}`]: { uses: whitelistArray }
140+
},
141+
});
142+
143+
metadataFilteringService.generateMetadataFilters(projectData, platform);
144+
assert.deepEqual(dataWritten, { [path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: getExpectedWhitelistContent({ pluginWhitelist: whitelistArray }) });
145+
});
146+
147+
it(`generates all files when both plugins and applications filters are included`, () => {
148+
const testInjector = createTestInjector({ hasPlugins: true });
149+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
150+
const { dataWritten } = mockFs({
151+
testInjector,
152+
existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath],
153+
readJsonData: {
154+
[`${appResourcesNativeApiUsageFilePath}`]: {
155+
whitelist: whitelistArray,
156+
blacklist: blacklistArray,
157+
["whitelist-plugins-usages"]: true
158+
},
159+
[`${pluginNativeApiUsageFilePath}`]: { uses: pluginsUses }
160+
},
161+
});
162+
163+
metadataFilteringService.generateMetadataFilters(projectData, platform);
164+
const expectedWhitelist = getExpectedWhitelistContent({ pluginWhitelist: pluginsUses, applicationWhitelist: whitelistArray });
165+
166+
assert.deepEqual(dataWritten, {
167+
[path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: expectedWhitelist,
168+
[path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL)
169+
});
170+
});
171+
172+
it(`skips plugins ${MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME} files when whitelist-plugins-usages in App_Resources is false`, () => {
173+
const testInjector = createTestInjector({ hasPlugins: true });
174+
const metadataFilteringService: IMetadataFilteringService = testInjector.resolve(MetadataFilteringService);
175+
const { dataWritten } = mockFs({
176+
testInjector,
177+
existingFiles: [appResourcesNativeApiUsageFilePath, pluginNativeApiUsageFilePath],
178+
readJsonData: {
179+
[`${appResourcesNativeApiUsageFilePath}`]: {
180+
whitelist: whitelistArray,
181+
blacklist: blacklistArray,
182+
["whitelist-plugins-usages"]: false
183+
},
184+
[`${pluginNativeApiUsageFilePath}`]: { uses: pluginsUses }
185+
},
186+
});
187+
188+
metadataFilteringService.generateMetadataFilters(projectData, "platform");
189+
const expectedWhitelist = getExpectedWhitelistContent({ applicationWhitelist: whitelistArray });
190+
191+
assert.deepEqual(dataWritten, {
192+
[path.join(projectRoot, MetadataFilteringConstants.WHITELIST_FILE_NAME)]: expectedWhitelist,
193+
[path.join(projectRoot, MetadataFilteringConstants.BLACKLIST_FILE_NAME)]: blacklistArray.join(EOL)
194+
});
195+
});
196+
});
197+
});

test/stubs.ts

Lines changed: 3 additions & 0 deletions
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)