123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- 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<ReactPackage> getPackages() {
- return new ArrayList<>(Arrays.<ReactPackage>asList(
- new MainReactPackage(mConfig){{ packageClassInstances }}
- ));
- }
- }
- """
- class ReactNativeModules {
- private Logger logger
- private String packageName
- private File root
- private ArrayList<HashMap<String, String>> reactNativeModules
- private static String LOG_PREFIX = ":ReactNative:"
- ReactNativeModules(Logger logger, File root) {
- this.logger = logger
- this.root = root
- def (nativeModules, packageName) = this.getReactNativeConfig()
- this.reactNativeModules = nativeModules
- this.packageName = packageName
- }
- /**
- * 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"]
- appProject.dependencies {
- // TODO(salakar): are other dependency scope methods such as `api` required?
- 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<HashMap<String, String>>[] 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
- }
- }
- /**
- * 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<HashMap<String, String>> getReactNativeConfig() {
- if (this.reactNativeModules != null) return this.reactNativeModules
- ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
- /**
- * 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 = "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()}")
- }
- 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<String, String>()
- reactNativeModuleConfig.put("name", name)
- reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_'))
- reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
- reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
- reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
- this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
- reactNativeModules.add(reactNativeModuleConfig)
- } else {
- this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
- }
- }
- return [reactNativeModules, json["project"]["android"]["packageName"]];
- }
- }
- /*
- * 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('.', '/'))
- task generatePackageList {
- doLast {
- autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
- }
- }
- preBuild.dependsOn generatePackageList
- android {
- sourceSets {
- main {
- java {
- srcDirs += generatedSrcDir
- }
- }
- }
- }
- }
|