1import glob from 'fast-glob'; 2import fs from 'fs-extra'; 3import path from 'path'; 4 5import { ModuleDescriptorAndroid, PackageRevision } from '../types'; 6 7/** 8 * Generates Java file that contains all autolinked packages. 9 */ 10export async function generatePackageListAsync( 11 modules: ModuleDescriptorAndroid[], 12 targetPath: string, 13 namespace: string 14): Promise<void> { 15 const generatedFileContent = await generatePackageListFileContentAsync(modules, namespace); 16 await fs.outputFile(targetPath, generatedFileContent); 17} 18 19async function findGradleFilesAsync(revision: PackageRevision): Promise<string[]> { 20 const configGradlePaths = revision.config?.androidGradlePaths(); 21 if (configGradlePaths && configGradlePaths.length) { 22 return configGradlePaths; 23 } 24 25 const buildGradleFiles = await glob('*/build.gradle', { 26 cwd: revision.path, 27 ignore: ['**/node_modules/**'], 28 }); 29 30 return buildGradleFiles; 31} 32 33export async function resolveModuleAsync( 34 packageName: string, 35 revision: PackageRevision 36): Promise<ModuleDescriptorAndroid | null> { 37 // TODO: Relative source dir should be configurable through the module config. 38 39 // Don't link itself... :D 40 if (packageName === '@unimodules/react-native-adapter') { 41 return null; 42 } 43 44 const buildGradleFiles = await findGradleFilesAsync(revision); 45 // Just in case where the module doesn't have its own `build.gradle`. 46 if (!buildGradleFiles.length) { 47 return null; 48 } 49 50 const projects = buildGradleFiles.map((buildGradleFile) => { 51 const gradleFilePath = path.join(revision.path, buildGradleFile); 52 return { 53 name: convertPackageNameToProjectName( 54 packageName, 55 path.relative(revision.path, gradleFilePath) 56 ), 57 sourceDir: path.dirname(gradleFilePath), 58 }; 59 }); 60 61 const plugins = (revision.config?.androidGradlePlugins() ?? []).map( 62 ({ id, group, sourceDir }) => ({ 63 id, 64 group, 65 sourceDir: path.join(revision.path, sourceDir), 66 }) 67 ); 68 69 return { 70 packageName, 71 projects, 72 ...(plugins.length > 0 ? { plugins } : {}), 73 modules: revision.config?.androidModules() ?? [], 74 }; 75} 76 77/** 78 * Generates the string to put into the generated package list. 79 */ 80async function generatePackageListFileContentAsync( 81 modules: ModuleDescriptorAndroid[], 82 namespace: string 83): Promise<string> { 84 // TODO: Instead of ignoring `expo` here, make the package class paths configurable from `expo-module.config.json`. 85 const packagesClasses = await findAndroidPackagesAsync( 86 modules.filter((module) => module.packageName !== 'expo') 87 ); 88 89 const modulesClasses = await findAndroidModules(modules); 90 91 return `package ${namespace}; 92 93import java.util.Arrays; 94import java.util.List; 95import expo.modules.core.interfaces.Package; 96import expo.modules.kotlin.modules.Module; 97import expo.modules.kotlin.ModulesProvider; 98 99public class ExpoModulesPackageList implements ModulesProvider { 100 private static class LazyHolder { 101 static final List<Package> packagesList = Arrays.<Package>asList( 102${packagesClasses.map((packageClass) => ` new ${packageClass}()`).join(',\n')} 103 ); 104 105 static final List<Class<? extends Module>> modulesList = Arrays.<Class<? extends Module>>asList( 106 ${modulesClasses.map((moduleClass) => ` ${moduleClass}.class`).join(',\n')} 107 ); 108 } 109 110 public static List<Package> getPackageList() { 111 return LazyHolder.packagesList; 112 } 113 114 @Override 115 public List<Class<? extends Module>> getModulesList() { 116 return LazyHolder.modulesList; 117 } 118} 119`; 120} 121 122function findAndroidModules(modules: ModuleDescriptorAndroid[]): string[] { 123 const modulesToProvide = modules.filter((module) => module.modules.length > 0); 124 const classNames = ([] as string[]).concat(...modulesToProvide.map((module) => module.modules)); 125 return classNames; 126} 127 128async function findAndroidPackagesAsync(modules: ModuleDescriptorAndroid[]): Promise<string[]> { 129 const classes: string[] = []; 130 131 const flattenedSourceDirList: string[] = []; 132 for (const module of modules) { 133 for (const project of module.projects) { 134 flattenedSourceDirList.push(project.sourceDir); 135 } 136 } 137 138 await Promise.all( 139 flattenedSourceDirList.map(async (sourceDir) => { 140 const files = await glob('**/*Package.{java,kt}', { 141 cwd: sourceDir, 142 }); 143 144 for (const file of files) { 145 const fileContent = await fs.readFile(path.join(sourceDir, file), 'utf8'); 146 147 const packageRegex = (() => { 148 if (process.env.EXPO_SHOULD_USE_LEGACY_PACKAGE_INTERFACE) { 149 return /\bimport\s+org\.unimodules\.core\.(interfaces\.Package|BasePackage)\b/; 150 } else { 151 return /\bimport\s+expo\.modules\.core\.(interfaces\.Package|BasePackage)\b/; 152 } 153 })(); 154 155 // Very naive check to skip non-expo packages 156 if (!packageRegex.test(fileContent)) { 157 continue; 158 } 159 160 const classPathMatches = fileContent.match(/^package ([\w.]+)\b/m); 161 162 if (classPathMatches) { 163 const basename = path.basename(file, path.extname(file)); 164 classes.push(`${classPathMatches[1]}.${basename}`); 165 } 166 } 167 }) 168 ); 169 return classes.sort(); 170} 171 172/** 173 * Converts the package name and gradle file path to Android's project name. 174 * `$` to indicate subprojects 175 * `/` path will transform as `-` 176 * 177 * Example: `@expo/example` + `android/build.gradle` → `expo-example` 178 * 179 * Example: multiple projects 180 * - `expo-test` + `android/build.gradle` → `react-native-third-party` 181 * - `expo-test` + `subproject/build.gradle` → `react-native-third-party$subproject` 182 */ 183export function convertPackageNameToProjectName( 184 packageName: string, 185 buildGradleFile: string 186): string { 187 const name = packageName.replace(/^@/g, '').replace(/\W+/g, '-'); 188 const baseDir = path.dirname(buildGradleFile).replace(/\//g, '-'); 189 return baseDir === 'android' ? name : `${name}$${baseDir}`; 190} 191