UNPKG

13.3 kBPlain TextView Raw
1import groovy.json.JsonSlurper
2import org.gradle.initialization.DefaultSettings
3import org.apache.tools.ant.taskdefs.condition.Os
4
5def generatedFileName = "PackageList.java"
6def generatedFilePackage = "com.facebook.react"
7def generatedFileContentsTemplate = """
8package $generatedFilePackage;
9
10import android.app.Application;
11import android.content.Context;
12import android.content.res.Resources;
13
14import com.facebook.react.ReactPackage;
15import com.facebook.react.shell.MainPackageConfig;
16import com.facebook.react.shell.MainReactPackage;
17import java.util.Arrays;
18import java.util.ArrayList;
19
20{{ packageImports }}
21
22public class PackageList {
23 private Application application;
24 private ReactNativeHost reactNativeHost;
25 private MainPackageConfig mConfig;
26
27 public PackageList(ReactNativeHost reactNativeHost) {
28 this(reactNativeHost, null);
29 }
30
31 public PackageList(Application application) {
32 this(application, null);
33 }
34
35 public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
36 this.reactNativeHost = reactNativeHost;
37 mConfig = config;
38 }
39
40 public PackageList(Application application, MainPackageConfig config) {
41 this.reactNativeHost = null;
42 this.application = application;
43 mConfig = config;
44 }
45
46 private ReactNativeHost getReactNativeHost() {
47 return this.reactNativeHost;
48 }
49
50 private Resources getResources() {
51 return this.getApplication().getResources();
52 }
53
54 private Application getApplication() {
55 if (this.reactNativeHost == null) return this.application;
56 return this.reactNativeHost.getApplication();
57 }
58
59 private Context getApplicationContext() {
60 return this.getApplication().getApplicationContext();
61 }
62
63 public ArrayList<ReactPackage> getPackages() {
64 return new ArrayList<>(Arrays.<ReactPackage>asList(
65 new MainReactPackage(mConfig){{ packageClassInstances }}
66 ));
67 }
68}
69"""
70
71class ReactNativeModules {
72 private Logger logger
73 private String packageName
74 private File root
75 private ArrayList<HashMap<String, String>> reactNativeModules
76 private HashMap<String, ArrayList> reactNativeModulesBuildVariants
77
78 private static String LOG_PREFIX = ":ReactNative:"
79
80 ReactNativeModules(Logger logger, File root) {
81 this.logger = logger
82 this.root = root
83
84 def (nativeModules, reactNativeModulesBuildVariants, packageName) = this.getReactNativeConfig()
85 this.reactNativeModules = nativeModules
86 this.reactNativeModulesBuildVariants = reactNativeModulesBuildVariants
87 this.packageName = packageName
88 }
89
90 /**
91 * Include the react native modules android projects and specify their project directory
92 */
93 void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
94 reactNativeModules.forEach { reactNativeModule ->
95 String nameCleansed = reactNativeModule["nameCleansed"]
96 String androidSourceDir = reactNativeModule["androidSourceDir"]
97 defaultSettings.include(":${nameCleansed}")
98 defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
99 }
100 }
101
102 /**
103 * Adds the react native modules as dependencies to the users `app` project
104 */
105 void addReactNativeModuleDependencies(Project appProject) {
106 reactNativeModules.forEach { reactNativeModule ->
107 def nameCleansed = reactNativeModule["nameCleansed"]
108 def dependencyConfiguration = reactNativeModule["dependencyConfiguration"]
109 appProject.dependencies {
110 if (reactNativeModulesBuildVariants.containsKey(nameCleansed)) {
111 reactNativeModulesBuildVariants
112 .get(nameCleansed)
113 .forEach { buildVariant ->
114 if(dependencyConfiguration != null) {
115 "${buildVariant}${dependencyConfiguration}"
116 } else {
117 "${buildVariant}Implementation" project(path: ":${nameCleansed}")
118 }
119 }
120 } else {
121 if(dependencyConfiguration != null) {
122 "${dependencyConfiguration}"
123 } else {
124 implementation project(path: ":${nameCleansed}")
125 }
126 }
127 }
128 }
129 }
130
131 /**
132 * Code-gen a java file with all the detected ReactNativePackage instances automatically added
133 *
134 * @param outputDir
135 * @param generatedFileName
136 * @param generatedFileContentsTemplate
137 */
138 void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
139 ArrayList<HashMap<String, String>> packages = this.reactNativeModules
140 String packageName = this.packageName
141
142 String packageImports = ""
143 String packageClassInstances = ""
144
145 if (packages.size() > 0) {
146 def interpolateDynamicValues = {
147 it
148 // Before adding the package replacement mechanism,
149 // BuildConfig and R classes were imported automatically
150 // into the scope of the file. We want to replace all
151 // non-FQDN references to those classes with the package name
152 // of the MainApplication.
153 //
154 // We want to match "R" or "BuildConfig":
155 // - new Package(R.string…),
156 // - Module.configure(BuildConfig);
157 // ^ hence including (BuildConfig|R)
158 // but we don't want to match "R":
159 // - new Package(getResources…),
160 // - new PackageR…,
161 // - new Royal…,
162 // ^ hence excluding \w before and after matches
163 // and "BuildConfig" that has FQDN reference:
164 // - Module.configure(com.acme.BuildConfig);
165 // ^ hence excluding . before the match.
166 .replaceAll(~/([^.\w])(BuildConfig|R)([^\w])/, {
167 wholeString, prefix, className, suffix ->
168 "${prefix}${packageName}.${className}${suffix}"
169 })
170 }
171 packageImports = packages.collect {
172 "// ${it.name}\n${interpolateDynamicValues(it.packageImportPath)}"
173 }.join('\n')
174 packageClassInstances = ",\n " + packages.collect {
175 interpolateDynamicValues(it.packageInstance)
176 }.join(",\n ")
177 }
178
179 String generatedFileContents = generatedFileContentsTemplate
180 .replace("{{ packageImports }}", packageImports)
181 .replace("{{ packageClassInstances }}", packageClassInstances)
182
183 outputDir.mkdirs()
184 final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
185 treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
186 w << generatedFileContents
187 }
188 }
189
190 /**
191 * Runs a specified command using Runtime exec() in a specified directory.
192 * Throws when the command result is empty.
193 */
194 String getCommandOutput(String[] command, File directory) {
195 try {
196 def output = ""
197 def cmdProcess = Runtime.getRuntime().exec(command, null, directory)
198 def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream()))
199 def buff = ""
200 def readBuffer = new StringBuffer()
201 while ((buff = bufferedReader.readLine()) != null) {
202 readBuffer.append(buff)
203 }
204 output = readBuffer.toString()
205 if (!output) {
206 this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command.")
207 def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
208 def errBuff = ""
209 def readErrorBuffer = new StringBuffer()
210 while ((errBuff = bufferedErrorReader.readLine()) != null) {
211 readErrorBuffer.append(errBuff)
212 }
213 throw new Exception(readErrorBuffer.toString())
214 }
215 return output
216 } catch (Exception exception) {
217 this.logger.error("${LOG_PREFIX}Running '${command}' command failed.")
218 throw exception
219 }
220 }
221
222 /**
223 * Runs a process to call the React Native CLI Config command and parses the output
224 */
225 ArrayList<HashMap<String, String>> getReactNativeConfig() {
226 if (this.reactNativeModules != null) return this.reactNativeModules
227
228 ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
229 HashMap<String, ArrayList> reactNativeModulesBuildVariants = new HashMap<String, ArrayList>()
230
231 /**
232 * Resolve the CLI location from Gradle file
233 *
234 * @todo: Sometimes Gradle can be called outside of the JavaScript hierarchy (-p flag) which
235 * will fail to resolve the script and the dependencies. We should resolve this soon.
236 *
237 * @todo: `fastlane` has been reported to not work too.
238 */
239 def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}"
240 String[] nodeCommand = ["node", "-e", cliResolveScript]
241 def cliPath = this.getCommandOutput(nodeCommand, this.root)
242
243 String[] reactNativeConfigCommand = ["node", cliPath, "config"]
244 def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)
245
246 def json
247 try {
248 json = new JsonSlurper().parseText(reactNativeConfigOutput)
249 } catch (Exception exception) {
250 throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
251 }
252 def dependencies = json["dependencies"]
253 def project = json["project"]["android"]
254
255 if (project == null) {
256 throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
257 }
258
259 def engine = new groovy.text.SimpleTemplateEngine()
260
261 dependencies.each { name, value ->
262 def platformsConfig = value["platforms"];
263 def androidConfig = platformsConfig["android"]
264
265 if (androidConfig != null && androidConfig["sourceDir"] != null) {
266 this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
267
268 HashMap reactNativeModuleConfig = new HashMap<String, String>()
269 def nameCleansed = name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_')
270 reactNativeModuleConfig.put("name", name)
271 reactNativeModuleConfig.put("nameCleansed", nameCleansed)
272 reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
273 reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
274 reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
275 if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) {
276 reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"])
277 }
278 if(androidConfig.containsKey("dependencyConfiguration")) {
279 reactNativeModuleConfig.put("dependencyConfiguration", androidConfig["dependencyConfiguration"])
280 } else if (project.containsKey("dependencyConfiguration")) {
281 def bindings = ["dependencyName": nameCleansed]
282 def template = engine.createTemplate(project["dependencyConfiguration"]).make(bindings)
283
284 reactNativeModuleConfig.put("dependencyConfiguration", template.toString())
285 }
286
287 this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
288
289 reactNativeModules.add(reactNativeModuleConfig)
290 } else {
291 this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
292 }
293 }
294
295 return [reactNativeModules, reactNativeModulesBuildVariants, json["project"]["android"]["packageName"]];
296 }
297}
298
299
300/*
301 * Sometimes Gradle can be called outside of JavaScript hierarchy. Detect the directory
302 * where build files of an active project are located.
303 */
304def projectRoot = rootProject.projectDir
305
306def autoModules = new ReactNativeModules(logger, projectRoot)
307
308/** -----------------------
309 * Exported Extensions
310 * ------------------------ */
311
312ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null ->
313 if (root != null) {
314 logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now.");
315 logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesSettingsGradle`.");
316 }
317 autoModules.addReactNativeModuleProjects(defaultSettings)
318}
319
320ext.applyNativeModulesAppBuildGradle = { Project project, String root = null ->
321 if (root != null) {
322 logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now");
323 logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesAppBuildGradle`.");
324 }
325 autoModules.addReactNativeModuleDependencies(project)
326
327 def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java")
328 def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))
329
330 task generatePackageList {
331 doLast {
332 autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
333 }
334 }
335
336 preBuild.dependsOn generatePackageList
337
338 android {
339 sourceSets {
340 main {
341 java {
342 srcDirs += generatedSrcDir
343 }
344 }
345 }
346 }
347}