Skip to content

Instantly share code, notes, and snippets.

@balvinderg
Last active March 24, 2024 08:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save balvinderg/d7b93e329c84ae81eab7936f9c9b8f54 to your computer and use it in GitHub Desktop.
Save balvinderg/d7b93e329c84ae81eab7936f9c9b8f54 to your computer and use it in GitHub Desktop.
Modified native_modules.gradle (7.0.1)
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 HashMap<String, ArrayList> reactNativeModulesBuildVariants
private static String LOG_PREFIX = ":ReactNative:"
ReactNativeModules(Logger logger, File root) {
this.logger = logger
this.root = root
def (nativeModules, reactNativeModulesBuildVariants, packageName) = this.getReactNativeConfig()
this.reactNativeModules = nativeModules
this.reactNativeModulesBuildVariants = reactNativeModulesBuildVariants
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"]
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<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>>()
HashMap<String, ArrayList> reactNativeModulesBuildVariants = new HashMap<String, ArrayList>()
def packageDotJsonPath = root.absolutePath + "/package.json"
def packageLockDotJsonPath = root.absolutePath + "/package-lock.json"
def cachedPackageDotJsonPath = root.absolutePath + "/cached_package.json"
def cachedPackageLockDotJsonPath = root.absolutePath + "/cached_package-lock.json"
def cachedNodeModuleOutputPath = root.absolutePath + "/node_command_output.json"
def cachedNodeModuleOutputFile = new File(cachedNodeModuleOutputPath)
def cachedPackageFile = new File(cachedPackageDotJsonPath);
def cachedPackageLockFile = new File(cachedPackageLockDotJsonPath);
def packageFileContent = new File(packageDotJsonPath).text
def packageLockFileContent = new File(packageLockDotJsonPath).text
def shouldUseCache = true
if (!cachedPackageFile.exists() || !cachedNodeModuleOutputFile.exists() || !cachedPackageLockFile.exists() || packageFileContent != cachedPackageFile.text || packageLockFileContent != cachedPackageLockFile.text) {
cachedPackageFile.delete()
cachedPackageFile.createNewFile()
cachedPackageFile.write(new File(packageDotJsonPath).text)
cachedPackageLockFile.delete()
cachedPackageLockFile.createNewFile()
cachedPackageLockFile.write(new File(packageLockDotJsonPath).text)
shouldUseCache = false
}
def output = ""
if (shouldUseCache) {
output = cachedNodeModuleOutputFile.text.replaceAll("to_be_replaced_with", root.absolutePath)
} else {
/**
* 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[] configCommand = ["node", cliPath, "config"]
output = this.getCommandOutput(configCommand, this.root)
cachedNodeModuleOutputFile.delete()
cachedNodeModuleOutputFile.createNewFile()
cachedNodeModuleOutputFile.write(output.replace(root.absolutePath, "to_be_replaced_with"))
}
def json
try {
json = new JsonSlurper().parseText(output)
} catch (Exception exception) {
throw new Exception("Calling node command finished with an exception. Error message: ${exception.toString()}. Output: ${ouput}");
}
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<String, String>()
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"])
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"]["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
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment