314 lines
11 KiB
Groovy
314 lines
11 KiB
Groovy
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|