From b0af38ac88a96aae3411bbd4b2add219fd8a299a Mon Sep 17 00:00:00 2001 From: Marian Date: Fri, 6 Mar 2026 13:09:22 +0200 Subject: [PATCH 1/4] Fix autolinking for libraries with multiple ReactPackage classes `extractFqcnFromImport` used `Regex.find()` which only returned the first FQCN from `packageImportPath`. Libraries registering multiple ReactPackage classes (e.g. react-native-appsflyer with both RNAppsFlyerPackage and PCAppsFlyerPackage) would have only the first class qualified, leaving others as bare class names causing `cannot find symbol` compilation errors in the generated PackageList.java. Additionally, the replacement regex `\bClassName\b` could match inside already-qualified names (since `.` is not a word character), causing double-qualification if packageInstance already contained FQCNs. Changes: - Add `extractAllFqcnsFromImport` that uses `Regex.findAll()` to extract all FQCNs from import statements - Replace bare class names sequentially with a `(? ["com.foo.bar.A", "com.foo.bar.B"] + */ + internal fun extractAllFqcnsFromImport(importStatements: String): List = + Regex("import\\s+([\\w.]+)\\s*;").findAll(importStatements).map { it.groupValues[1] }.toList() + internal fun composePackageInstance( packageName: String, packages: Map, @@ -86,15 +93,17 @@ abstract class GeneratePackageListTask : DefaultTask() { val packageImportPath = dep.packageImportPath val interpolated = interpolateDynamicValues(packageInstance, packageName) - // Use FQCN to avoid class name collisions between different packages - val fqcn = extractFqcnFromImport(interpolateDynamicValues(packageImportPath, packageName)) + // Use FQCNs to avoid class name collisions between different packages. + // A library may register multiple ReactPackage classes (e.g. react-native-appsflyer), + // so we extract all FQCNs and replace each bare class name individually. + val fqcns = + extractAllFqcnsFromImport(interpolateDynamicValues(packageImportPath, packageName)) val fqcnInstance = - if (fqcn != null) { + fqcns.fold(interpolated) { acc, fqcn -> val className = fqcn.substringAfterLast('.') - // Replace the short class name with FQCN in the instance - interpolated.replace(Regex("\\b${Regex.escape(className)}\\b")) { fqcn } - } else { - interpolated + // Negative lookbehind (?() + val result = + task.extractAllFqcnsFromImport( + "import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;" + ) + assertThat(result) + .containsExactly( + "com.appsflyer.reactnative.RNAppsFlyerPackage", + "com.appsflyer.reactnative.PCAppsFlyerPackage" + ) + } + + @Test + fun extractAllFqcnsFromImport_withSingleImport_returnsSingleElement() { + val task = createTestTask() + val result = task.extractAllFqcnsFromImport("import com.facebook.react.APackage;") + assertThat(result).containsExactly("com.facebook.react.APackage") + } + + @Test + fun extractAllFqcnsFromImport_withNoValidImport_returnsEmpty() { + val task = createTestTask() + val result = task.extractAllFqcnsFromImport("not an import statement") + assertThat(result).isEmpty() + } + @Test fun composePackageInstance_withNoPackages_returnsEmpty() { val task = createTestTask() @@ -83,6 +111,68 @@ class GeneratePackageListTaskTest { ) } + @Test + fun composePackageInstance_withMultiplePackagesPerLibrary_qualifiesAll() { + val task = createTestTask() + val packageName = "com.example.app" + + val multiPackageDeps = + mapOf( + "react-native-appsflyer" to + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./node_modules/react-native-appsflyer/android", + packageImportPath = + "import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;", + packageInstance = + "new RNAppsFlyerPackage(),\nnew PCAppsFlyerPackage()", + buildTypes = emptyList(), + ), + ) + + val result = task.composePackageInstance(packageName, multiPackageDeps) + assertThat(result) + .isEqualTo( + """ + , + // react-native-appsflyer + new com.appsflyer.reactnative.RNAppsFlyerPackage(), + new com.appsflyer.reactnative.PCAppsFlyerPackage() + """ + .trimIndent() + ) + } + + @Test + fun composePackageInstance_withPreQualifiedInstances_doesNotDoubleQualify() { + val task = createTestTask() + val packageName = "com.example.app" + + val preQualifiedDeps = + mapOf( + "react-native-appsflyer" to + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./node_modules/react-native-appsflyer/android", + packageImportPath = + "import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;", + packageInstance = + "new com.appsflyer.reactnative.RNAppsFlyerPackage(),\nnew com.appsflyer.reactnative.PCAppsFlyerPackage()", + buildTypes = emptyList(), + ), + ) + + val result = task.composePackageInstance(packageName, preQualifiedDeps) + assertThat(result) + .isEqualTo( + """ + , + // react-native-appsflyer + new com.appsflyer.reactnative.RNAppsFlyerPackage(), + new com.appsflyer.reactnative.PCAppsFlyerPackage() + """ + .trimIndent() + ) + } + @Test fun interpolateDynamicValues_withNoBuildConfigOrROccurrencies_doesNothing() { val packageName = "com.facebook.react" From 13c67a7739c52348c09448a82cb08416efd82a25 Mon Sep 17 00:00:00 2001 From: Marian Date: Thu, 9 Apr 2026 22:05:33 +0300 Subject: [PATCH 2/4] Revert "Fix autolinking for libraries with multiple ReactPackage classes" This reverts commit 71e4936770cb941f2e4bce0f18c432f24c98e02e. --- .../react/tasks/GeneratePackageListTask.kt | 23 ++--- .../tasks/GeneratePackageListTaskTest.kt | 90 ------------------- 2 files changed, 7 insertions(+), 106 deletions(-) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt index 3de368f6c1a8..1156bc280694 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt @@ -69,13 +69,6 @@ abstract class GeneratePackageListTask : DefaultTask() { return match?.groupValues?.get(1) } - /** - * Extracts all fully qualified class names from one or more import statements. E.g., "import - * com.foo.bar.A;\nimport com.foo.bar.B;" -> ["com.foo.bar.A", "com.foo.bar.B"] - */ - internal fun extractAllFqcnsFromImport(importStatements: String): List = - Regex("import\\s+([\\w.]+)\\s*;").findAll(importStatements).map { it.groupValues[1] }.toList() - internal fun composePackageInstance( packageName: String, packages: Map, @@ -93,17 +86,15 @@ abstract class GeneratePackageListTask : DefaultTask() { val packageImportPath = dep.packageImportPath val interpolated = interpolateDynamicValues(packageInstance, packageName) - // Use FQCNs to avoid class name collisions between different packages. - // A library may register multiple ReactPackage classes (e.g. react-native-appsflyer), - // so we extract all FQCNs and replace each bare class name individually. - val fqcns = - extractAllFqcnsFromImport(interpolateDynamicValues(packageImportPath, packageName)) + // Use FQCN to avoid class name collisions between different packages + val fqcn = extractFqcnFromImport(interpolateDynamicValues(packageImportPath, packageName)) val fqcnInstance = - fqcns.fold(interpolated) { acc, fqcn -> + if (fqcn != null) { val className = fqcn.substringAfterLast('.') - // Negative lookbehind (?() - val result = - task.extractAllFqcnsFromImport( - "import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;" - ) - assertThat(result) - .containsExactly( - "com.appsflyer.reactnative.RNAppsFlyerPackage", - "com.appsflyer.reactnative.PCAppsFlyerPackage" - ) - } - - @Test - fun extractAllFqcnsFromImport_withSingleImport_returnsSingleElement() { - val task = createTestTask() - val result = task.extractAllFqcnsFromImport("import com.facebook.react.APackage;") - assertThat(result).containsExactly("com.facebook.react.APackage") - } - - @Test - fun extractAllFqcnsFromImport_withNoValidImport_returnsEmpty() { - val task = createTestTask() - val result = task.extractAllFqcnsFromImport("not an import statement") - assertThat(result).isEmpty() - } - @Test fun composePackageInstance_withNoPackages_returnsEmpty() { val task = createTestTask() @@ -111,68 +83,6 @@ class GeneratePackageListTaskTest { ) } - @Test - fun composePackageInstance_withMultiplePackagesPerLibrary_qualifiesAll() { - val task = createTestTask() - val packageName = "com.example.app" - - val multiPackageDeps = - mapOf( - "react-native-appsflyer" to - ModelAutolinkingDependenciesPlatformAndroidJson( - sourceDir = "./node_modules/react-native-appsflyer/android", - packageImportPath = - "import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;", - packageInstance = - "new RNAppsFlyerPackage(),\nnew PCAppsFlyerPackage()", - buildTypes = emptyList(), - ), - ) - - val result = task.composePackageInstance(packageName, multiPackageDeps) - assertThat(result) - .isEqualTo( - """ - , - // react-native-appsflyer - new com.appsflyer.reactnative.RNAppsFlyerPackage(), - new com.appsflyer.reactnative.PCAppsFlyerPackage() - """ - .trimIndent() - ) - } - - @Test - fun composePackageInstance_withPreQualifiedInstances_doesNotDoubleQualify() { - val task = createTestTask() - val packageName = "com.example.app" - - val preQualifiedDeps = - mapOf( - "react-native-appsflyer" to - ModelAutolinkingDependenciesPlatformAndroidJson( - sourceDir = "./node_modules/react-native-appsflyer/android", - packageImportPath = - "import com.appsflyer.reactnative.RNAppsFlyerPackage;\nimport com.appsflyer.reactnative.PCAppsFlyerPackage;", - packageInstance = - "new com.appsflyer.reactnative.RNAppsFlyerPackage(),\nnew com.appsflyer.reactnative.PCAppsFlyerPackage()", - buildTypes = emptyList(), - ), - ) - - val result = task.composePackageInstance(packageName, preQualifiedDeps) - assertThat(result) - .isEqualTo( - """ - , - // react-native-appsflyer - new com.appsflyer.reactnative.RNAppsFlyerPackage(), - new com.appsflyer.reactnative.PCAppsFlyerPackage() - """ - .trimIndent() - ) - } - @Test fun interpolateDynamicValues_withNoBuildConfigOrROccurrencies_doesNothing() { val packageName = "com.facebook.react" From 5f8dd41a7054711822d63899a15f874cd7c3e9f6 Mon Sep 17 00:00:00 2001 From: Marian Date: Thu, 9 Apr 2026 22:10:00 +0300 Subject: [PATCH 3/4] Add packageImportPaths array field to autolinking config Instead of regex-parsing Java import statements from the packageImportPath string, introduce a new packageImportPaths field that accepts a structured array of FQCNs directly from the CLI config. RNGP prefers packageImportPaths when present and falls back to the legacy extractFqcnFromImport path for backward compatibility with older CLI versions that don't yet emit the new field. Made-with: Cursor --- .../react/tasks/GeneratePackageListTask.kt | 26 +++++-- .../tasks/GeneratePackageListTaskTest.kt | 77 +++++++++++++++++++ ...olinkingDependenciesPlatformAndroidJson.kt | 1 + .../com/facebook/react/utils/JsonUtilsTest.kt | 45 +++++++++++ 4 files changed, 141 insertions(+), 8 deletions(-) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt index 1156bc280694..ea470d0fefa5 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt @@ -83,18 +83,28 @@ abstract class GeneratePackageListTask : DefaultTask() { requireNotNull(dep.packageInstance) { "RNGP - Autolinking: Missing `packageInstance` in `config` for dependency $name. This is required to generate the autolinking package list." } - val packageImportPath = dep.packageImportPath val interpolated = interpolateDynamicValues(packageInstance, packageName) - // Use FQCN to avoid class name collisions between different packages - val fqcn = extractFqcnFromImport(interpolateDynamicValues(packageImportPath, packageName)) + // Use FQCNs to avoid class name collisions between different packages. + val packageImportPaths = dep.packageImportPaths val fqcnInstance = - if (fqcn != null) { - val className = fqcn.substringAfterLast('.') - // Replace the short class name with FQCN in the instance - interpolated.replace(Regex("\\b${Regex.escape(className)}\\b")) { fqcn } + if (packageImportPaths != null) { + packageImportPaths.fold(interpolated) { acc, fqcn -> + val className = fqcn.substringAfterLast('.') + // Negative lookbehind ensures we only replace bare class names, + // not ones already part of a fully qualified name. + acc.replace(Regex("(?() + val packageName = "com.example.app" + + val deps = + mapOf( + "react-native-appsflyer" to + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./node_modules/react-native-appsflyer/android", + packageImportPath = "", + packageImportPaths = + listOf( + "com.appsflyer.reactnative.RNAppsFlyerPackage", + "com.appsflyer.reactnative.PCAppsFlyerPackage", + ), + packageInstance = "new RNAppsFlyerPackage(),\nnew PCAppsFlyerPackage()", + buildTypes = emptyList(), + ), + ) + + val result = task.composePackageInstance(packageName, deps) + assertThat(result) + .isEqualTo( + """ + , + // react-native-appsflyer + new com.appsflyer.reactnative.RNAppsFlyerPackage(), + new com.appsflyer.reactnative.PCAppsFlyerPackage() + """ + .trimIndent() + ) + } + + @Test + fun composePackageInstance_withBothFields_prefersPackageImportPaths() { + val task = createTestTask() + val packageName = "com.example.app" + + val deps = + mapOf( + "my-library" to + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./node_modules/my-library/android", + packageImportPath = "import com.wrong.WrongPackage;", + packageImportPaths = listOf("com.correct.CorrectPackage"), + packageInstance = "new CorrectPackage()", + buildTypes = emptyList(), + ), + ) + + val result = task.composePackageInstance(packageName, deps) + assertThat(result).contains("com.correct.CorrectPackage") + assertThat(result).doesNotContain("com.wrong.WrongPackage") + } + + @Test + fun composePackageInstance_withNullPackageImportPaths_fallsBackToSingleFqcn() { + val task = createTestTask() + val packageName = "com.example.app" + + val deps = + mapOf( + "my-library" to + ModelAutolinkingDependenciesPlatformAndroidJson( + sourceDir = "./node_modules/my-library/android", + packageImportPath = "import com.legacy.LegacyPackage;", + packageImportPaths = null, + packageInstance = "new LegacyPackage()", + buildTypes = emptyList(), + ), + ) + + val result = task.composePackageInstance(packageName, deps) + assertThat(result).contains("com.legacy.LegacyPackage") + } + @Test fun interpolateDynamicValues_withNoBuildConfigOrROccurrencies_doesNothing() { val packageName = "com.facebook.react" diff --git a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt index 947f0f3f2293..ae6fdfc90836 100644 --- a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt +++ b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/model/ModelAutolinkingDependenciesPlatformAndroidJson.kt @@ -10,6 +10,7 @@ package com.facebook.react.model data class ModelAutolinkingDependenciesPlatformAndroidJson( val sourceDir: String, val packageImportPath: String, + val packageImportPaths: List? = null, val packageInstance: String, val buildTypes: List, val libraryName: String? = null, diff --git a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt index 0544a8775470..2c7f11d906de 100644 --- a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt +++ b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/utils/JsonUtilsTest.kt @@ -375,6 +375,51 @@ class JsonUtilsTest { .isPureCxxDependency!! ) .isFalse() + assertThat( + parsed.dependencies!!["@react-native/oss-library-example"]!! + .platforms!! + .android!! + .packageImportPaths + ) + .isNull() + } + + @Test + fun fromAutolinkingConfigJson_withPackageImportPaths_canParseIt() { + val validJson = + createJsonFile( + """ + { + "reactNativeVersion": "1000.0.0", + "dependencies": { + "@react-native/oss-library-example": { + "root": "./node_modules/@react-native/oss-library-example", + "name": "@react-native/oss-library-example", + "platforms": { + "android": { + "sourceDir": "./node_modules/@react-native/oss-library-example/android", + "packageImportPath": "import com.facebook.react.osslibraryexample.OSSLibraryExamplePackage;", + "packageImportPaths": [ + "com.facebook.react.osslibraryexample.OSSLibraryExamplePackage" + ], + "packageInstance": "new OSSLibraryExamplePackage()", + "buildTypes": ["debug", "release"] + } + } + } + } + } + """ + .trimIndent() + ) + val parsed = JsonUtils.fromAutolinkingConfigJson(validJson)!! + + val android = + parsed.dependencies!!["@react-native/oss-library-example"]!!.platforms!!.android!! + assertThat(android.packageImportPaths) + .containsExactly("com.facebook.react.osslibraryexample.OSSLibraryExamplePackage") + assertThat(android.packageImportPath) + .isEqualTo("import com.facebook.react.osslibraryexample.OSSLibraryExamplePackage;") } private fun createJsonFile(@Language("JSON") input: String) = From 0e7a9eaa7114f4a902c1eb84707a67dfe9b382e8 Mon Sep 17 00:00:00 2001 From: Marian Date: Thu, 9 Apr 2026 22:18:40 +0300 Subject: [PATCH 4/4] Fix comment wording: use "libraries" instead of "packages" for clarity Made-with: Cursor --- .../kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt index ea470d0fefa5..cf5f28bfd9a2 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GeneratePackageListTask.kt @@ -85,7 +85,7 @@ abstract class GeneratePackageListTask : DefaultTask() { } val interpolated = interpolateDynamicValues(packageInstance, packageName) - // Use FQCNs to avoid class name collisions between different packages. + // Use FQCNs to avoid class name collisions between different libraries. val packageImportPaths = dep.packageImportPaths val fqcnInstance = if (packageImportPaths != null) {