1import groovy.json.JsonSlurper
2import java.nio.file.Paths
3
4// Object representing a module.
5class ExpoModule {
6  // Name of the JavaScript package
7  String name
8
9  // Version of the package, loaded from `package.json`
10  String version
11
12  // Name of the Android project
13  String projectName
14
15  // Path to the folder with Android project
16  String sourceDir
17
18  ExpoModule(Object data) {
19    this.name = data.packageName
20    this.version = data.packageVersion
21    this.projectName = data.projectName
22    this.sourceDir = data.sourceDir
23  }
24}
25
26class ExpoAutolinkingManager {
27  private File projectDir
28  private Map options
29  private Object cachedResolvingResults
30
31  static String generatedPackageListNamespace = 'expo.modules'
32  static String generatedPackageListFilename = 'ExpoModulesPackageList.java'
33  static String generatedFilesSrcDir = 'generated/expo/src/main/java'
34
35  ExpoAutolinkingManager(File projectDir, Map options = [:]) {
36    this.projectDir = projectDir
37    this.options = options
38  }
39
40  Object resolve() {
41    if (cachedResolvingResults) {
42      return cachedResolvingResults
43    }
44    String[] args = convertOptionsToCommandArgs('resolve', this.options)
45    args += ['--json']
46
47    String output = exec(args)
48    Object json = new JsonSlurper().parseText(output)
49
50    cachedResolvingResults = json
51    return json
52  }
53
54  boolean shouldUseAAR() {
55    return options?.useAAR == true
56  }
57
58  ExpoModule[] getModules() {
59    Object json = resolve()
60    return json.modules.collect { new ExpoModule(it) }
61  }
62
63  static void generatePackageList(Project project, Map options) {
64    String[] args = convertOptionsToCommandArgs('generate-package-list', options)
65
66    // Construct absolute path to generated package list.
67    def generatedFilePath = Paths.get(
68      project.buildDir.toString(),
69      generatedFilesSrcDir,
70      generatedPackageListNamespace.replace('.', '/'),
71      generatedPackageListFilename
72    )
73
74    args += [
75      '--namespace',
76      generatedPackageListNamespace,
77      '--target',
78      generatedFilePath.toString()
79    ]
80
81    if (options == null) {
82      // Options are provided only when settings.gradle was configured.
83      // If not or opted-out from autolinking, the generated list should be empty.
84      args += '--empty'
85    }
86
87    exec(args)
88  }
89
90  static String exec(String[] commandArgs) {
91    Process proc = commandArgs.execute()
92    StringBuffer outputStream = new StringBuffer()
93    proc.waitForProcessOutput(outputStream, System.err)
94    return outputStream.toString()
95  }
96
97  static private String[] convertOptionsToCommandArgs(String command, Map options) {
98    String[] args = [
99      'node',
100      '--eval',
101      'require(\'expo-modules-autolinking\')(process.argv.slice(1))',
102      '--',
103      command,
104      '--platform',
105      'android'
106    ]
107
108    def searchPaths = options?.get("searchPaths", options?.get("modulesPaths", null))
109    if (searchPaths) {
110      args += searchPaths
111    }
112
113    if (options?.ignorePaths) {
114      args += '--ignore-paths'
115      args += options.ignorePaths
116    }
117
118    if (options?.exclude) {
119      args += '--exclude'
120      args += options.exclude
121    }
122
123    return args
124  }
125}
126
127class Colors {
128  static final String GREEN = "\u001B[32m"
129  static final String RESET = "\u001B[0m"
130}
131
132// Here we split the implementation, depending on Gradle context.
133// `rootProject` is a `ProjectDescriptor` if this file is imported in `settings.gradle` context,
134// otherwise we can assume it is imported in `build.gradle`.
135if (rootProject instanceof ProjectDescriptor) {
136  // Method to be used in `settings.gradle`. Options passed here will have an effect in `build.gradle` context as well,
137  // i.e. adding the dependencies and generating the package list.
138  ext.useExpoModules = { Map options = [:] ->
139    ExpoAutolinkingManager manager = new ExpoAutolinkingManager(rootProject.projectDir, options)
140    ExpoModule[] modules = manager.getModules()
141
142    for (module in modules) {
143      include(":${module.projectName}")
144      project(":${module.projectName}").projectDir = new File(module.sourceDir)
145    }
146
147    // Save the manager in the shared context, so that we can later use it in `build.gradle`.
148    gradle.ext.expoAutolinkingManager = manager
149  }
150} else {
151  def addModule = { DependencyHandler handler, String projectName, Boolean useAAR ->
152    Project dependency = rootProject.project(":${projectName}")
153
154    if (useAAR) {
155      handler.add('api', "${dependency.group}:${projectName}:${dependency.version}")
156    } else {
157      handler.add('api', dependency)
158    }
159  }
160
161  def addDependencies = { DependencyHandler handler, Project project ->
162    ExpoAutolinkingManager manager = gradle.ext.expoAutolinkingManager
163    def modules = manager.getModules()
164
165    if (!modules.length) {
166      return
167    }
168
169    println 'Using expo modules'
170
171    for (module in modules) {
172      // Don't link itself
173      if (module.name == project.name) {
174        continue
175      }
176
177      addModule(handler, module.projectName, manager.shouldUseAAR())
178
179      // Can remove this once we move all the interfaces into the core.
180      if (module.name.endsWith('-interface')) {
181        continue
182      }
183
184      println "— ${Colors.GREEN}${module.name}${Colors.RESET} (${module.version})"
185    }
186  }
187
188  // Adding dependencies
189  ext.addExpoModulesDependencies = { DependencyHandler handler, Project project ->
190    // Return early if `useExpoModules` was not called in `settings.gradle`
191    if (!gradle.ext.has('expoAutolinkingManager')) {
192      logger.error('Error: Autolinking is not set up in `settings.gradle`: expo modules won\'t be autolinked.')
193      return
194    }
195
196    ExpoAutolinkingManager manager = gradle.ext.expoAutolinkingManager
197
198    if (rootProject.findProject(':expo-modules-core')) {
199      // `expo` requires `expo-modules-core` as a dependency, even if autolinking is turned off.
200      addModule(handler, 'expo-modules-core', manager.shouldUseAAR())
201    } else {
202      logger.error('Error: `expo-modules-core` project is not included by autolinking.')
203    }
204
205    // If opted-in not to autolink modules as dependencies
206    if (manager.options == null) {
207      return
208    }
209
210    addDependencies(handler, project)
211  }
212
213  // Generating the package list
214  ext.generatedFilesSrcDir = ExpoAutolinkingManager.generatedFilesSrcDir
215
216  ext.generateExpoModulesPackageList = {
217    // Get options used in `settings.gradle` or null if it wasn't set up.
218    Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null
219
220    if (options == null) {
221      // TODO(@tsapeta): Temporarily muted this error — uncomment it once we start migrating from autolinking v1 to v2
222      // logger.error('Autolinking is not set up in `settings.gradle`: generated package list with expo modules will be empty.')
223    }
224    ExpoAutolinkingManager.generatePackageList(project, options)
225  }
226
227  ext.ensureDependeciesWereEvaluated = { Project project ->
228    if (!gradle.ext.has('expoAutolinkingManager')) {
229      return
230    }
231
232    def modules = gradle.ext.expoAutolinkingManager.getModules()
233    for (module in modules) {
234      def dependency = project.findProject(":${module.projectName}")
235      if (dependency == null) {
236        logger.warn("Coudn't find project ${module.projectName}. Please, make sure that `useExpoModules` was called in `settings.gradle`.")
237        continue
238      }
239
240      // Prevent circular dependencies
241      if (module.projectName == project.name) {
242        continue
243      }
244
245      project.evaluationDependsOn(":${module.projectName}")
246    }
247  }
248}
249