Skip to content

Commit 61051bf

Browse files
committed
feat(metadata): apply native-api-usage.json rules in metadata trim
Declared rules are merged with what the bundle analysis finds, following the same whitelist-plugins-usages semantics as ns prepare. Projects are read automatically for iOS and Android; other hosts pass --native-api-usage.
1 parent 14dbfc8 commit 61051bf

10 files changed

Lines changed: 372 additions & 70 deletions

File tree

‎docs/man_pages/project/configuration/metadata-trim.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ position: 26
99

1010
Reduces the native API metadata an app ships to what its JavaScript uses. The app's emitted bundle is analyzed for the native symbols it references, and the generator keeps those plus the types they depend on.
1111

12+
Rules in `native-api-usage.json` still apply. Entries from `whitelist` and from plugins' `uses` are kept in addition to what the analysis finds, and `blacklist` entries are removed. A blacklisted type that a kept API's signature needs can't be removed: on Apple platforms it stays in the metadata, and on Android the build fails and names it. In a NativeScript project these files are found automatically for iOS and Android; other hosts pass them with `--native-api-usage`.
13+
1214
If the bundle reads globals dynamically (an alias of `globalThis`, for example), its usage cannot be determined and nothing is trimmed.
1315

1416
### Commands
@@ -27,6 +29,7 @@ Write the whitelist for an Android project | `$ ns metadata trim android <Bundle
2729

2830
* `--out` - Output directory. Defaults to `metadata-trim`.
2931
* `--keep` - A `Module:Symbol` pattern to keep regardless of the analysis. Repeat for several.
32+
* `--native-api-usage` - A `native-api-usage.json` file to apply, in the app or plugin format. Repeat for several.
3033
* `--analyzer` - `ns-metadata-symbols` on Apple platforms; overrides the runtime's `seed.js` on Android.
3134
* `--generator` - `objc-metadata-generator`. Use the one from the runtime release the app ships.
3235
* `--framework` - A `NativeScript.framework` to copy into the output directory with the trimmed metadata embedded.

‎lib/commands/metadata-trim.ts‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@ import {
66
stringOption,
77
} from "../common/define-command";
88
import { inject } from "../common/di";
9-
import { IMetadataTrimService } from "../definitions/metadata-trim-service";
9+
import { IMetadataFilteringService } from "../definitions/metadata-filtering-service";
10+
import {
11+
IDeclaredMetadataRules,
12+
IMetadataTrimService,
13+
} from "../definitions/metadata-trim-service";
14+
import { IProjectData } from "../definitions/project";
15+
import { IProjectHelper } from "../common/declarations";
16+
import { readDeclaredRules } from "../services/metadata-trim/declared-rules";
1017

1118
const PLATFORMS = ["ios", "macos", "android"];
1219

1320
const metadataTrimOptions = {
1421
out: stringOption(),
1522
keep: arrayOption(),
23+
nativeApiUsage: arrayOption(),
1624
analyzer: stringOption(),
1725
generator: stringOption(),
1826
framework: stringOption(),
@@ -37,9 +45,14 @@ export class MetadataTrimCommand extends Command({
3745
params: "any",
3846
}) {
3947
private $logger = inject<ILogger>("logger");
48+
private $metadataFilteringService = inject<IMetadataFilteringService>(
49+
"metadataFilteringService",
50+
);
4051
private $metadataTrimService = inject<IMetadataTrimService>(
4152
"metadataTrimService",
4253
);
54+
private $projectData = inject<IProjectData>("projectData");
55+
private $projectHelper = inject<IProjectHelper>("projectHelper");
4356

4457
public canExecute(): boolean {
4558
const [platform, ...bundles] = this.args;
@@ -57,11 +70,13 @@ export class MetadataTrimCommand extends Command({
5770
public async run(): Promise<void> {
5871
const [platform, ...bundles] = this.args;
5972
const options = this.options;
73+
const declared = this.declaredRules(platform.toLowerCase());
6074
const result = await this.$metadataTrimService.trim({
6175
platform: platform.toLowerCase(),
6276
bundles: bundles.map((bundle) => path.resolve(bundle)),
6377
outDir: path.resolve(options.out ?? "metadata-trim"),
64-
keep: options.keep ?? [],
78+
keep: [...(options.keep ?? []), ...declared.keep],
79+
blacklist: declared.blacklist,
6580
analyzerPath: resolved(options.analyzer),
6681
generatorPath: resolved(options.generator),
6782
framework: resolved(options.framework),
@@ -71,6 +86,11 @@ export class MetadataTrimCommand extends Command({
7186
minVersion: options.minVersion,
7287
});
7388

89+
if (declared.keep.length || declared.blacklist.length) {
90+
this.$logger.info(
91+
`Applied native-api-usage.json: ${declared.keep.length} kept, ${declared.blacklist.length} removed.`,
92+
);
93+
}
7494
if (result.metadataBytes !== undefined) {
7595
this.$logger.info(
7696
`Generated ${formatBytes(result.metadataBytes)} of metadata: ${result.metadataPath}`,
@@ -86,4 +106,24 @@ export class MetadataTrimCommand extends Command({
86106
this.$logger.info(result.nextStep);
87107
}
88108
}
109+
110+
private declaredRules(platform: string): IDeclaredMetadataRules {
111+
const files = (this.options.nativeApiUsage ?? []).map((file) =>
112+
readDeclaredRules(path.resolve(file)),
113+
);
114+
// A standard project's files only exist for iOS and Android.
115+
if (this.$projectHelper.projectDir && platform !== "macos") {
116+
this.$projectData.initializeProjectData();
117+
files.push(
118+
this.$metadataFilteringService.getDeclaredRules(
119+
this.$projectData,
120+
platform,
121+
),
122+
);
123+
}
124+
return {
125+
keep: files.flatMap((rules) => rules.keep),
126+
blacklist: files.flatMap((rules) => rules.blacklist),
127+
};
128+
}
89129
}

‎lib/definitions/metadata-filtering-service.d.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { IProjectData } from "./project";
2+
import { IDeclaredMetadataRules } from "./metadata-trim-service";
23

34
/**
45
* Describes service used to generate necessary files to filter the native metadata generation.
@@ -45,4 +46,15 @@ interface IMetadataFilteringService {
4546
* @returns {void}
4647
*/
4748
generateMetadataFilters(projectData: IProjectData, platform: string): void;
49+
50+
/**
51+
* Reads the rules the app and its plugins declare in native-api-usage.json.
52+
* @param {IProjectData} projectData Information about the current project.
53+
* @param {string} platform The platform to read rules for.
54+
* @returns {IDeclaredMetadataRules} The patterns to keep and to remove.
55+
*/
56+
getDeclaredRules(
57+
projectData: IProjectData,
58+
platform: string,
59+
): IDeclaredMetadataRules;
4860
}

‎lib/definitions/metadata-trim-service.d.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface IMetadataTrimOptions {
66
outDir: string;
77
/** Filter patterns (`Module:Symbol`) to keep regardless of what is found. */
88
keep: string[];
9+
/** Filter patterns to remove even if the app references them. */
10+
blacklist?: string[];
911
analyzerPath?: string;
1012
generatorPath?: string;
1113
/** A NativeScript.framework to embed the trimmed metadata into. */
@@ -17,6 +19,12 @@ export interface IMetadataTrimOptions {
1719
minVersion?: string;
1820
}
1921

22+
/** What native-api-usage.json files declare, as filter patterns. */
23+
export interface IDeclaredMetadataRules {
24+
keep: string[];
25+
blacklist: string[];
26+
}
27+
2028
/** The native symbols an app's JavaScript can reach. */
2129
export interface IMetadataUsage {
2230
whitelistPath: string;

‎lib/services/metadata-filtering-service.ts‎

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { MetadataFilteringConstants } from "../constants";
44
import { IPluginsService } from "../definitions/plugins";
55
import { IPlatformsDataService } from "../definitions/platform";
66
import { IProjectData } from "../definitions/project";
7+
import { IDeclaredMetadataRules } from "../definitions/metadata-trim-service";
8+
import { mergeDeclaredRules } from "./metadata-trim/declared-rules";
79
import {
810
IMetadataFilteringService,
911
INativeApiUsageConfiguration,
@@ -18,43 +20,66 @@ export class MetadataFilteringService implements IMetadataFilteringService {
1820
private $pluginsService: IPluginsService,
1921
private $mobileHelper: Mobile.IMobileHelper,
2022
private $platformsDataService: IPlatformsDataService,
21-
private $logger: ILogger
23+
private $logger: ILogger,
2224
) {}
2325

2426
public generateMetadataFilters(
2527
projectData: IProjectData,
26-
platform: string
28+
platform: string,
2729
): void {
2830
this.generateWhitelist(projectData, platform);
2931
this.generateBlacklist(projectData, platform);
3032
}
3133

34+
public getDeclaredRules(
35+
projectData: IProjectData,
36+
platform: string,
37+
): IDeclaredMetadataRules {
38+
const pluginConfigurations = this.$pluginsService
39+
.getAllProductionPlugins(projectData, platform)
40+
.map((plugin) =>
41+
path.join(
42+
plugin.pluginPlatformsFolderPath(platform),
43+
MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME,
44+
),
45+
)
46+
.filter((configPath) => this.$fs.exists(configPath))
47+
.map(
48+
(configPath): INativeApiUsagePluginConfiguration =>
49+
this.$fs.readJson(configPath) || {},
50+
);
51+
52+
return mergeDeclaredRules(
53+
this.getNativeApiConfigurationForPlatform(projectData, platform),
54+
pluginConfigurations,
55+
);
56+
}
57+
3258
private generateWhitelist(projectData: IProjectData, platform: string): void {
3359
const platformsDirPath = this.getPlatformsDirPath(projectData, platform);
3460
const pathToWhitelistFile = path.join(
3561
platformsDirPath,
36-
MetadataFilteringConstants.WHITELIST_FILE_NAME
62+
MetadataFilteringConstants.WHITELIST_FILE_NAME,
3763
);
3864
this.$fs.deleteFile(pathToWhitelistFile);
3965

4066
const nativeApiConfiguration = this.getNativeApiConfigurationForPlatform(
4167
projectData,
42-
platform
68+
platform,
4369
);
4470
if (nativeApiConfiguration) {
4571
const whitelistedItems: string[] = [];
4672
if (nativeApiConfiguration["whitelist-plugins-usages"]) {
4773
const plugins = this.$pluginsService.getAllProductionPlugins(
4874
projectData,
49-
platform
75+
platform,
5076
);
5177
for (const pluginData of plugins) {
52-
const pathToPlatformsDir = pluginData.pluginPlatformsFolderPath(
53-
platform
54-
);
78+
const pathToPlatformsDir =
79+
pluginData.pluginPlatformsFolderPath(platform);
5580
const pathToPluginsMetadataConfig = path.join(
5681
pathToPlatformsDir,
57-
MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME
82+
MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME,
5883
);
5984
if (this.$fs.exists(pathToPluginsMetadataConfig)) {
6085
const pluginConfig: INativeApiUsagePluginConfiguration =
@@ -63,17 +88,17 @@ export class MetadataFilteringService implements IMetadataFilteringService {
6388
`Adding content of ${pathToPluginsMetadataConfig} to whitelisted items of metadata filtering: ${JSON.stringify(
6489
pluginConfig,
6590
null,
66-
2
67-
)}`
91+
2,
92+
)}`,
6893
);
6994
const itemsToAdd = pluginConfig.uses || [];
7095
if (itemsToAdd.length) {
7196
whitelistedItems.push(
72-
`// Added from: ${pathToPluginsMetadataConfig}`
97+
`// Added from: ${pathToPluginsMetadataConfig}`,
7398
);
7499
whitelistedItems.push(...itemsToAdd);
75100
whitelistedItems.push(
76-
`// Finished part from ${pathToPluginsMetadataConfig}${os.EOL}`
101+
`// Finished part from ${pathToPluginsMetadataConfig}${os.EOL}`,
77102
);
78103
}
79104
}
@@ -87,8 +112,8 @@ export class MetadataFilteringService implements IMetadataFilteringService {
87112
`Adding content from application to whitelisted items of metadata filtering: ${JSON.stringify(
88113
applicationWhitelistedItems,
89114
null,
90-
2
91-
)}`
115+
2,
116+
)}`,
92117
);
93118

94119
whitelistedItems.push(`// Added from application`);
@@ -106,28 +131,27 @@ export class MetadataFilteringService implements IMetadataFilteringService {
106131
const platformsDirPath = this.getPlatformsDirPath(projectData, platform);
107132
const pathToBlacklistFile = path.join(
108133
platformsDirPath,
109-
MetadataFilteringConstants.BLACKLIST_FILE_NAME
134+
MetadataFilteringConstants.BLACKLIST_FILE_NAME,
110135
);
111136
this.$fs.deleteFile(pathToBlacklistFile);
112137

113138
const nativeApiConfiguration = this.getNativeApiConfigurationForPlatform(
114139
projectData,
115-
platform
140+
platform,
116141
);
117142
if (nativeApiConfiguration) {
118143
const blacklistedItems: string[] = nativeApiConfiguration.blacklist || [];
119144
if (nativeApiConfiguration["whitelist-plugins-usages"]) {
120145
const plugins = this.$pluginsService.getAllProductionPlugins(
121146
projectData,
122-
platform
147+
platform,
123148
);
124149
for (const pluginData of plugins) {
125-
const pathToPlatformsDir = pluginData.pluginPlatformsFolderPath(
126-
platform
127-
);
150+
const pathToPlatformsDir =
151+
pluginData.pluginPlatformsFolderPath(platform);
128152
const pathToPluginsMetadataConfig = path.join(
129153
pathToPlatformsDir,
130-
MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME
154+
MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME,
131155
);
132156
if (this.$fs.exists(pathToPluginsMetadataConfig)) {
133157
const pluginConfig: INativeApiUsagePluginConfiguration =
@@ -136,17 +160,17 @@ export class MetadataFilteringService implements IMetadataFilteringService {
136160
`Adding content of ${pathToPluginsMetadataConfig} to whitelisted items of metadata filtering: ${JSON.stringify(
137161
pluginConfig,
138162
null,
139-
2
140-
)}`
163+
2,
164+
)}`,
141165
);
142166
const itemsToAdd = pluginConfig.blacklist || [];
143167
if (itemsToAdd.length) {
144168
blacklistedItems.push(
145-
`// Added from: ${pathToPluginsMetadataConfig}`
169+
`// Added from: ${pathToPluginsMetadataConfig}`,
146170
);
147171
blacklistedItems.push(...itemsToAdd);
148172
blacklistedItems.push(
149-
`// Finished part from ${pathToPluginsMetadataConfig}${os.EOL}`
173+
`// Finished part from ${pathToPluginsMetadataConfig}${os.EOL}`,
150174
);
151175
}
152176
}
@@ -157,20 +181,18 @@ export class MetadataFilteringService implements IMetadataFilteringService {
157181
}
158182
} else {
159183
this.$logger.trace(
160-
`There's no application configuration for metadata filtering for platform ${platform}. Full metadata will be generated.`
184+
`There's no application configuration for metadata filtering for platform ${platform}. Full metadata will be generated.`,
161185
);
162186
}
163187
}
164188

165189
private getNativeApiConfigurationForPlatform(
166190
projectData: IProjectData,
167-
platform: string
191+
platform: string,
168192
): INativeApiUsageConfiguration {
169193
let config: INativeApiUsageConfiguration = null;
170-
const pathToApplicationConfigurationFile = this.getPathToApplicationConfigurationForPlatform(
171-
projectData,
172-
platform
173-
);
194+
const pathToApplicationConfigurationFile =
195+
this.getPathToApplicationConfigurationForPlatform(projectData, platform);
174196
if (this.$fs.exists(pathToApplicationConfigurationFile)) {
175197
config = this.$fs.readJson(pathToApplicationConfigurationFile);
176198
}
@@ -180,23 +202,23 @@ export class MetadataFilteringService implements IMetadataFilteringService {
180202

181203
private getPlatformsDirPath(
182204
projectData: IProjectData,
183-
platform: string
205+
platform: string,
184206
): string {
185207
const platformData = this.$platformsDataService.getPlatformData(
186208
platform,
187-
projectData
209+
projectData,
188210
);
189211
return platformData.projectRoot;
190212
}
191213

192214
private getPathToApplicationConfigurationForPlatform(
193215
projectData: IProjectData,
194-
platform: string
216+
platform: string,
195217
): string {
196218
return path.join(
197219
projectData.appResourcesDirectoryPath,
198220
this.$mobileHelper.normalizePlatformName(platform),
199-
MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME
221+
MetadataFilteringConstants.NATIVE_API_USAGE_FILE_NAME,
200222
);
201223
}
202224
}

‎lib/services/metadata-trim/android-toolchain.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { readFilterPatterns } from "./apple-toolchain";
1111

1212
const SEED_SCRIPT = path.join("build-tools", "metadata-filter", "seed.js");
1313
const WHITELIST_NAME = "whitelist.mdg";
14+
const BLACKLIST_NAME = "blacklist.mdg";
1415

1516
/** Android's metadata generator runs in Gradle; this produces its whitelist. */
1617
export class AndroidMetadataToolchain implements IMetadataToolchain {
@@ -82,6 +83,12 @@ export class AndroidMetadataToolchain implements IMetadataToolchain {
8283
> {
8384
const installedTo = path.join(options.androidProject, WHITELIST_NAME);
8485
fs.copyFileSync(usage.whitelistPath, installedTo);
86+
if (options.blacklist?.length) {
87+
fs.writeFileSync(
88+
path.join(options.androidProject, BLACKLIST_NAME),
89+
options.blacklist.join("\n") + "\n",
90+
);
91+
}
8592
return {
8693
installedTo,
8794
// Without this flag Gradle filters by the list as written, dropping

0 commit comments

Comments
 (0)