From bc7375b506e7c78c90bfb3b42066a49a5995b512 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 18 Aug 2026 22:21:22 +0200 Subject: [PATCH] feat(android): per-plugin build options, with aarSuffix to break name clashes The name of the `.aar` built for a plugin comes from `getShortPluginName`, which drops the npm scope. `@foo/plugin-x` and `@bar/plugin-x` therefore both build a `plugin_x.aar` into their own platforms folder, and the one that gradle picks up depends on which was built last. A project can now give one of them a suffix: ```js export default { android: { plugins: { "@bar/plugin-x": { aarSuffix: "-bar" }, }, }, } satisfies NativeScriptConfig; ``` `android.plugins` is a map keyed by npm package name, spread into the options `buildAar` receives, so it is the place to put future per-plugin build settings too. The suffix is appended to the plugin name before it is shortened, and the resulting name is used consistently - for the temp build directory, the produced `.aar` and the namespace fallback, which `setupGradle` used to recompute without it. Co-Authored-By: Claude Opus 5 --- lib/definitions/android-plugin-migrator.d.ts | 7 ++++ lib/definitions/project.d.ts | 16 ++++++++ lib/services/android-plugin-build-service.ts | 10 +++-- lib/services/android-project-service.ts | 3 ++ test/services/android-plugin-build-service.ts | 37 +++++++++++++++++-- 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/lib/definitions/android-plugin-migrator.d.ts b/lib/definitions/android-plugin-migrator.d.ts index f5ad873307..c5b81a3213 100644 --- a/lib/definitions/android-plugin-migrator.d.ts +++ b/lib/definitions/android-plugin-migrator.d.ts @@ -12,6 +12,13 @@ interface IAndroidBuildOptions { tempPluginDirPath: string; gradlePath?: string; gradleArgs?: string; + /** + * Appended to the plugin name before it is shortened into the name of the + * produced `.aar`. The npm scope is dropped when shortening, so two plugins + * from different scopes can end up with the same `.aar` - a suffix tells + * them apart. + */ + aarSuffix?: string; } interface IAndroidPluginBuildService { diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 7ab75c5b0f..82794dded0 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -8,6 +8,7 @@ import { import { ICheckEnvironmentRequirementsOutput, IPlatformData } from "./platform"; import { IPluginData, IBasePluginData } from "./plugins"; import { + IDictionary, IStringDictionary, IProjectDir, IDeviceIdentifier, @@ -179,6 +180,21 @@ interface INsConfigAndroid extends INsConfigPlaform { * Custom runtime package name */ runtimePackageName?: string; + + /** + * Per plugin build options, keyed by the plugin's npm package name. + */ + plugins?: IDictionary; +} + +interface INsConfigAndroidPlugin { + /** + * Appended to the plugin name before it is shortened into the name of the + * produced `.aar`. The npm scope is dropped when shortening, so + * `@foo/plugin` and `@bar/plugin` both build a `plugin.aar` and overwrite + * each other - a suffix tells them apart. + */ + aarSuffix?: string; } interface INsConfigHooks { diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 88098d55b0..34bd48a919 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -226,7 +226,11 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { const androidSourceDirectories = this.getAndroidSourceDirectories( options.platformsAndroidDirPath, ); - const shortPluginName = getShortPluginName(options.pluginName); + // the npm scope is dropped when shortening, so an optional suffix is what + // keeps two same-named plugins from overwriting each other's `.aar` + const shortPluginName = getShortPluginName( + `${options.pluginName}${options.aarSuffix || ""}`, + ); const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName); const pluginSourceFileHashesInfo = await this.getSourceFilesHashes( options.platformsAndroidDirPath, @@ -260,6 +264,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { options.platformsAndroidDirPath, options.projectDir, options.pluginName, + shortPluginName, ); await this.buildPlugin({ gradlePath: options.gradlePath, @@ -401,6 +406,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { platformsAndroidDirPath: string, projectDir: string, pluginName: string, + shortPluginName: string, ): Promise { const gradleTemplatePath = path.resolve( path.join(__dirname, "../../vendor/gradle-plugin"), @@ -425,8 +431,6 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.replaceFileContent(settingsGradlePath, "{{pluginName}}", pluginName); // gets the package from the AndroidManifest to use as the namespace or fallback to the `org.nativescript.${shortPluginName}` - const shortPluginName = getShortPluginName(pluginName); - const manifestPath = path.join( pluginTempDir, "src", diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index b0e53a53e5..b07a0d55e9 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -688,6 +688,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject AndroidProjectService.ANDROID_PLATFORM_NAME ); if (this.$fs.exists(pluginPlatformsFolderPath)) { + const pluginConfig = + (projectData.nsConfig?.android?.plugins || {})[pluginData.name] || {}; const options: IPluginBuildOptions = { gradlePath: this.$options.gradlePath, gradleArgs: this.$options.gradleArgs, @@ -696,6 +698,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject platformsAndroidDirPath: pluginPlatformsFolderPath, aarOutputDir: pluginPlatformsFolderPath, tempPluginDirPath: path.join(projectData.platformsDir, "tempPlugin"), + ...pluginConfig, }; if (await this.$androidPluginBuildService.buildAar(options)) { diff --git a/test/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index c2107cf4c1..16621473c1 100644 --- a/test/services/android-plugin-build-service.ts +++ b/test/services/android-plugin-build-service.ts @@ -24,6 +24,7 @@ describe("androidPluginBuildService", () => { const pluginName = "my-plugin"; const shortPluginName = getShortPluginName(pluginName); let spawnFromEventCalled = false; + let builtPluginDirName: string = null; let fs: IFileSystem; let androidBuildPluginService: AndroidPluginBuildService; let tempFolder: string; @@ -46,6 +47,7 @@ describe("androidPluginBuildService", () => { }): IPluginBuildOptions { options = options || {}; spawnFromEventCalled = false; + builtPluginDirName = null; tempFolder = mkdtempSync( path.join(tmpdir(), "androidPluginBuildService-temp-"), ); @@ -75,11 +77,16 @@ describe("androidPluginBuildService", () => { const testInjector: IInjector = new stubs.InjectorStub(); testInjector.register("fs", FsLib.FileSystem); testInjector.register("childProcess", { - spawnFromEvent: async (command: string): Promise => { - const finalAarName = `${shortPluginName}-release.aar`; + spawnFromEvent: async ( + command: string, + args: string[], + ): Promise => { + // the plugin dir gradle was pointed at is what names the built aar + const pluginDir = args[args.indexOf("-p") + 1]; + builtPluginDirName = path.basename(pluginDir); + const finalAarName = `${builtPluginDirName}-release.aar`; const aar = path.join( - tempFolder, - shortPluginName, + pluginDir, "build", "outputs", "aar", @@ -269,6 +276,28 @@ dependencies { assert.isTrue(spawnFromEventCalled); }); + it("builds an aar named after the plugin", async () => { + const config: IPluginBuildOptions = setup({ addManifest: true }); + + await androidBuildPluginService.buildAar(config); + + assert.deepStrictEqual(builtPluginDirName, shortPluginName); + assert.isTrue( + fs.exists(path.join(pluginFolder, `${shortPluginName}.aar`)), + ); + }); + + it("appends aarSuffix to the name of the built aar", async () => { + const config: IPluginBuildOptions = setup({ addManifest: true }); + config.aarSuffix = "-v2"; + + await androidBuildPluginService.buildAar(config); + + const expectedName = getShortPluginName(`${pluginName}-v2`); + assert.deepStrictEqual(builtPluginDirName, expectedName); + assert.isTrue(fs.exists(path.join(pluginFolder, `${expectedName}.aar`))); + }); + it("does not build aar when there are no supported files in the plugin", async () => { const config: IPluginBuildOptions = setup();