import groovy.json.JsonSlurper import org.gradle.initialization.DefaultSettings import org.apache.tools.ant.taskdefs.condition.Os def generatedFileName = "PackageList.java" def generatedFilePackage = "com.facebook.react" def generatedFileContentsTemplate = """ package $generatedFilePackage; import android.app.Application; import android.content.Context; import android.content.res.Resources; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainPackageConfig; import com.facebook.react.shell.MainReactPackage; import java.util.Arrays; import java.util.ArrayList; {{ packageImports }} public class PackageList { private Application application; private ReactNativeHost reactNativeHost; private MainPackageConfig mConfig; public PackageList(ReactNativeHost reactNativeHost) { this(reactNativeHost, null); } public PackageList(Application application) { this(application, null); } public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) { this.reactNativeHost = reactNativeHost; mConfig = config; } public PackageList(Application application, MainPackageConfig config) { this.reactNativeHost = null; this.application = application; mConfig = config; } private ReactNativeHost getReactNativeHost() { return this.reactNativeHost; } private Resources getResources() { return this.getApplication().getResources(); } private Application getApplication() { if (this.reactNativeHost == null) return this.application; return this.reactNativeHost.getApplication(); } private Context getApplicationContext() { return this.getApplication().getApplicationContext(); } public ArrayList getPackages() { return new ArrayList<>(Arrays.asList( new MainReactPackage(mConfig){{ packageClassInstances }} )); } } """ def cmakeTemplate = """# This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli) cmake_minimum_required(VERSION 3.13) set(CMAKE_VERBOSE_MAKEFILE on) {{ libraryIncludes }} set(AUTOLINKED_LIBRARIES {{ libraryModules }} ) """ def rncliCppTemplate = """/** * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli). * * Do not edit this file as changes may cause incorrect behavior and will be lost * once the code is regenerated. * */ #include "rncli.h" {{ rncliCppIncludes }} namespace facebook { namespace react { {{ rncliReactLegacyComponentNames }} std::shared_ptr rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { {{ rncliCppModuleProviders }} return nullptr; } void rncli_registerProviders(std::shared_ptr providerRegistry) { {{ rncliCppComponentDescriptors }} {{ rncliReactLegacyComponentDescriptors }} return; } } // namespace react } // namespace facebook """ def rncliHTemplate = """/** * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli). * * Do not edit this file as changes may cause incorrect behavior and will be lost * once the code is regenerated. * */ #pragma once #include #include #include #include namespace facebook { namespace react { std::shared_ptr rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); void rncli_registerProviders(std::shared_ptr providerRegistry); } // namespace react } // namespace facebook """ class ReactNativeModules { private Logger logger private String packageName private File root private ArrayList> reactNativeModules private ArrayList unstable_reactLegacyComponentNames private HashMap reactNativeModulesBuildVariants private static String LOG_PREFIX = ":ReactNative:" ReactNativeModules(Logger logger, File root) { this.logger = logger this.root = root def (nativeModules, reactNativeModulesBuildVariants, androidProject) = this.getReactNativeConfig() this.reactNativeModules = nativeModules this.reactNativeModulesBuildVariants = reactNativeModulesBuildVariants this.packageName = androidProject["packageName"] this.unstable_reactLegacyComponentNames = androidProject["unstable_reactLegacyComponentNames"] } /** * Include the react native modules android projects and specify their project directory */ void addReactNativeModuleProjects(DefaultSettings defaultSettings) { reactNativeModules.forEach { reactNativeModule -> String nameCleansed = reactNativeModule["nameCleansed"] String androidSourceDir = reactNativeModule["androidSourceDir"] defaultSettings.include(":${nameCleansed}") defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}") } } /** * Adds the react native modules as dependencies to the users `app` project */ void addReactNativeModuleDependencies(Project appProject) { reactNativeModules.forEach { reactNativeModule -> def nameCleansed = reactNativeModule["nameCleansed"] def dependencyConfiguration = reactNativeModule["dependencyConfiguration"] appProject.dependencies { if (reactNativeModulesBuildVariants.containsKey(nameCleansed)) { reactNativeModulesBuildVariants .get(nameCleansed) .forEach { buildVariant -> if(dependencyConfiguration != null) { "${buildVariant}${dependencyConfiguration}" } else { "${buildVariant}Implementation" project(path: ":${nameCleansed}") } } } else { if(dependencyConfiguration != null) { "${dependencyConfiguration}" } else { implementation project(path: ":${nameCleansed}") } } } } } /** * Code-gen a java file with all the detected ReactNativePackage instances automatically added * * @param outputDir * @param generatedFileName * @param generatedFileContentsTemplate */ void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) { ArrayList> packages = this.reactNativeModules String packageName = this.packageName String packageImports = "" String packageClassInstances = "" if (packages.size() > 0) { def interpolateDynamicValues = { it // Before adding the package replacement mechanism, // BuildConfig and R classes were imported automatically // into the scope of the file. We want to replace all // non-FQDN references to those classes with the package name // of the MainApplication. // // We want to match "R" or "BuildConfig": // - new Package(R.string…), // - Module.configure(BuildConfig); // ^ hence including (BuildConfig|R) // but we don't want to match "R": // - new Package(getResources…), // - new PackageR…, // - new Royal…, // ^ hence excluding \w before and after matches // and "BuildConfig" that has FQDN reference: // - Module.configure(com.acme.BuildConfig); // ^ hence excluding . before the match. .replaceAll(~/([^.\w])(BuildConfig|R)([^\w])/, { wholeString, prefix, className, suffix -> "${prefix}${packageName}.${className}${suffix}" }) } packageImports = packages.collect { "// ${it.name}\n${interpolateDynamicValues(it.packageImportPath)}" }.join('\n') packageClassInstances = ",\n " + packages.collect { interpolateDynamicValues(it.packageInstance) }.join(",\n ") } String generatedFileContents = generatedFileContentsTemplate .replace("{{ packageImports }}", packageImports) .replace("{{ packageClassInstances }}", packageClassInstances) outputDir.mkdirs() final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) treeBuilder.file(generatedFileName).newWriter().withWriter { w -> w << generatedFileContents } } void generateCmakeFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) { ArrayList> packages = this.reactNativeModules String packageName = this.packageName String codegenLibPrefix = "react_codegen_" String libraryIncludes = "" String libraryModules = "" if (packages.size() > 0) { libraryIncludes = packages.collect { if (it.libraryName != null && it.cmakeListsPath != null) { // If user provided a custom cmakeListsPath, let's honor it. String nativeFolderPath = it.cmakeListsPath.replace("CMakeLists.txt", "") "add_subdirectory($nativeFolderPath ${it.libraryName}_autolinked_build)" } else { null } }.minus(null).join('\n') libraryModules = packages.collect { it.libraryName ? "${codegenLibPrefix}${it.libraryName}" : null }.minus(null).join('\n ') } String generatedFileContents = generatedFileContentsTemplate .replace("{{ libraryIncludes }}", libraryIncludes) .replace("{{ libraryModules }}", libraryModules) outputDir.mkdirs() final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) treeBuilder.file(generatedFileName).newWriter().withWriter { w -> w << generatedFileContents } } void generateRncliCpp(File outputDir, String generatedFileName, String generatedFileContentsTemplate) { ArrayList> packages = this.reactNativeModules ArrayList unstable_reactLegacyComponentNames = this.unstable_reactLegacyComponentNames String rncliCppIncludes = "" String rncliCppModuleProviders = "" String rncliCppComponentDescriptors = "" String rncliReactLegacyComponentDescriptors = "" String rncliReactLegacyComponentNames = "" String codegenComponentDescriptorsHeaderFile = "ComponentDescriptors.h" String codegenReactComponentsDir = "react/renderer/components" if (packages.size() > 0) { rncliCppIncludes = packages.collect { if (!it.libraryName) { return null } def result = "#include <${it.libraryName}.h>" if (it.componentDescriptors && it.componentDescriptors.size() > 0) { result += "\n#include <${codegenReactComponentsDir}/${it.libraryName}/${codegenComponentDescriptorsHeaderFile}>" } result }.minus(null).join('\n') rncliCppModuleProviders = packages.collect { it.libraryName ? """ auto module_${it.libraryName} = ${it.libraryName}_ModuleProvider(moduleName, params); if (module_${it.libraryName} != nullptr) { return module_${it.libraryName}; }""" : null }.minus(null).join("\n") rncliCppComponentDescriptors = packages.collect { def result = "" if (it.componentDescriptors && it.componentDescriptors.size() > 0) { result += it.componentDescriptors.collect { " providerRegistry->add(concreteComponentDescriptorProvider<${it}>());" }.join('\n') } result }.join("\n") } rncliReactLegacyComponentDescriptors = unstable_reactLegacyComponentNames.collect { " providerRegistry->add(concreteComponentDescriptorProvider>());" }.join("\n") rncliReactLegacyComponentNames = unstable_reactLegacyComponentNames.collect { "extern const char ${it}[] = \"${it}\";" }.join("\n") if (unstable_reactLegacyComponentNames && unstable_reactLegacyComponentNames.size() > 0) { rncliCppIncludes += "\n#include " } String generatedFileContents = generatedFileContentsTemplate .replace("{{ rncliCppIncludes }}", rncliCppIncludes) .replace("{{ rncliCppModuleProviders }}", rncliCppModuleProviders) .replace("{{ rncliCppComponentDescriptors }}", rncliCppComponentDescriptors) .replace("{{ rncliReactLegacyComponentDescriptors }}", rncliReactLegacyComponentDescriptors) .replace("{{ rncliReactLegacyComponentNames }}", rncliReactLegacyComponentNames) outputDir.mkdirs() final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) treeBuilder.file(generatedFileName).newWriter().withWriter { w -> w << generatedFileContents } } void generateRncliH(File outputDir, String generatedFileName, String generatedFileContentsTemplate) { String generatedFileContents = generatedFileContentsTemplate outputDir.mkdirs() final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) treeBuilder.file(generatedFileName).newWriter().withWriter { w -> w << generatedFileContents } } /** * Runs a specified command using Runtime exec() in a specified directory. * Throws when the command result is empty. */ String getCommandOutput(String[] command, File directory) { try { def output = "" def cmdProcess = Runtime.getRuntime().exec(command, null, directory) def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream())) def buff = "" def readBuffer = new StringBuffer() while ((buff = bufferedReader.readLine()) != null) { readBuffer.append(buff) } output = readBuffer.toString() if (!output) { this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command.") def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream())) def errBuff = "" def readErrorBuffer = new StringBuffer() while ((errBuff = bufferedErrorReader.readLine()) != null) { readErrorBuffer.append(errBuff) } throw new Exception(readErrorBuffer.toString()) } return output } catch (Exception exception) { this.logger.error("${LOG_PREFIX}Running '${command}' command failed.") throw exception } } /** * Runs a process to call the React Native CLI Config command and parses the output */ ArrayList> getReactNativeConfig() { if (this.reactNativeModules != null) return this.reactNativeModules ArrayList> reactNativeModules = new ArrayList>() HashMap reactNativeModulesBuildVariants = new HashMap() /** * Resolve the CLI location from Gradle file * * @todo: Sometimes Gradle can be called outside of the JavaScript hierarchy (-p flag) which * will fail to resolve the script and the dependencies. We should resolve this soon. * * @todo: `fastlane` has been reported to not work too. */ def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}" String[] nodeCommand = ["node", "-e", cliResolveScript] def cliPath = this.getCommandOutput(nodeCommand, this.root) String[] reactNativeConfigCommand = ["node", cliPath, "config"] def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root) def json try { json = new JsonSlurper().parseText(reactNativeConfigOutput) } catch (Exception exception) { throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}"); } def dependencies = json["dependencies"] def project = json["project"]["android"] if (project == null) { throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}") } def engine = new groovy.text.SimpleTemplateEngine() dependencies.each { name, value -> def platformsConfig = value["platforms"]; def androidConfig = platformsConfig["android"] if (androidConfig != null && androidConfig["sourceDir"] != null) { this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'") HashMap reactNativeModuleConfig = new HashMap() def nameCleansed = name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_') reactNativeModuleConfig.put("name", name) reactNativeModuleConfig.put("nameCleansed", nameCleansed) reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"]) reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"]) reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"]) reactNativeModuleConfig.put("libraryName", androidConfig["libraryName"]) reactNativeModuleConfig.put("componentDescriptors", androidConfig["componentDescriptors"]) reactNativeModuleConfig.put("cmakeListsPath", androidConfig["cmakeListsPath"]) if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) { reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"]) } if(androidConfig.containsKey("dependencyConfiguration")) { reactNativeModuleConfig.put("dependencyConfiguration", androidConfig["dependencyConfiguration"]) } else if (project.containsKey("dependencyConfiguration")) { def bindings = ["dependencyName": nameCleansed] def template = engine.createTemplate(project["dependencyConfiguration"]).make(bindings) reactNativeModuleConfig.put("dependencyConfiguration", template.toString()) } this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}") reactNativeModules.add(reactNativeModuleConfig) } else { this.logger.info("${LOG_PREFIX}Skipping native module '${name}'") } } return [reactNativeModules, reactNativeModulesBuildVariants, json["project"]["android"]]; } } /* * Sometimes Gradle can be called outside of JavaScript hierarchy. Detect the directory * where build files of an active project are located. */ def projectRoot = rootProject.projectDir def autoModules = new ReactNativeModules(logger, projectRoot) /** ----------------------- * Exported Extensions * ------------------------ */ ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null -> if (root != null) { logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now."); logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesSettingsGradle`."); } autoModules.addReactNativeModuleProjects(defaultSettings) } ext.applyNativeModulesAppBuildGradle = { Project project, String root = null -> if (root != null) { logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now"); logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesAppBuildGradle`."); } autoModules.addReactNativeModuleDependencies(project) def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java") def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/')) def generatedJniDir = new File(buildDir, "generated/rncli/src/main/jni") task generatePackageList { doLast { autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate) } } task generateNewArchitectureFiles { doLast { autoModules.generateCmakeFile(generatedJniDir, "Android-rncli.cmake", cmakeTemplate) autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate) autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate) } } preBuild.dependsOn generatePackageList if (project.hasProperty("newArchEnabled") && project.newArchEnabled == "true") { preBuild.dependsOn generateNewArchitectureFiles } android { sourceSets { main { java { srcDirs += generatedSrcDir } } } } }