1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types'; 2082815dcSEvan Baconimport Debug from 'debug'; 3082815dcSEvan Baconimport fs from 'fs'; 4082815dcSEvan Baconimport { sync as globSync } from 'glob'; 5082815dcSEvan Baconimport path from 'path'; 6082815dcSEvan Bacon 7*8a424bebSJames Ideimport { getAppBuildGradleFilePath, getProjectFilePath } from './Paths'; 8082815dcSEvan Baconimport { ConfigPlugin } from '../Plugin.types'; 93216ce43SKudo Chienimport { withAppBuildGradle } from '../plugins/android-plugins'; 10082815dcSEvan Baconimport { withDangerousMod } from '../plugins/withDangerousMod'; 11082815dcSEvan Baconimport { directoryExistsAsync } from '../utils/modules'; 12082815dcSEvan Baconimport { addWarningAndroid } from '../utils/warnings'; 13082815dcSEvan Bacon 14082815dcSEvan Baconconst debug = Debug('expo:config-plugins:android:package'); 15082815dcSEvan Bacon 16082815dcSEvan Baconexport const withPackageGradle: ConfigPlugin = (config) => { 17082815dcSEvan Bacon return withAppBuildGradle(config, (config) => { 18082815dcSEvan Bacon if (config.modResults.language === 'groovy') { 19082815dcSEvan Bacon config.modResults.contents = setPackageInBuildGradle(config, config.modResults.contents); 20082815dcSEvan Bacon } else { 21082815dcSEvan Bacon addWarningAndroid( 22082815dcSEvan Bacon 'android.package', 23082815dcSEvan Bacon `Cannot automatically configure app build.gradle if it's not groovy` 24082815dcSEvan Bacon ); 25082815dcSEvan Bacon } 26082815dcSEvan Bacon return config; 27082815dcSEvan Bacon }); 28082815dcSEvan Bacon}; 29082815dcSEvan Bacon 30082815dcSEvan Baconexport const withPackageRefactor: ConfigPlugin = (config) => { 31082815dcSEvan Bacon return withDangerousMod(config, [ 32082815dcSEvan Bacon 'android', 33082815dcSEvan Bacon async (config) => { 34082815dcSEvan Bacon await renamePackageOnDisk(config, config.modRequest.projectRoot); 35082815dcSEvan Bacon return config; 36082815dcSEvan Bacon }, 37082815dcSEvan Bacon ]); 38082815dcSEvan Bacon}; 39082815dcSEvan Bacon 40082815dcSEvan Baconexport function getPackage(config: Pick<ExpoConfig, 'android'>) { 41082815dcSEvan Bacon return config.android?.package ?? null; 42082815dcSEvan Bacon} 43082815dcSEvan Bacon 44082815dcSEvan Baconfunction getPackageRoot(projectRoot: string, type: 'main' | 'debug') { 45082815dcSEvan Bacon return path.join(projectRoot, 'android', 'app', 'src', type, 'java'); 46082815dcSEvan Bacon} 47082815dcSEvan Bacon 48082815dcSEvan Baconfunction getCurrentPackageName(projectRoot: string, packageRoot: string) { 49082815dcSEvan Bacon const mainApplication = getProjectFilePath(projectRoot, 'MainApplication'); 50082815dcSEvan Bacon const packagePath = path.dirname(mainApplication); 51082815dcSEvan Bacon const packagePathParts = path.relative(packageRoot, packagePath).split(path.sep).filter(Boolean); 52082815dcSEvan Bacon 53082815dcSEvan Bacon return packagePathParts.join('.'); 54082815dcSEvan Bacon} 55082815dcSEvan Bacon 56082815dcSEvan Baconfunction getCurrentPackageForProjectFile( 57082815dcSEvan Bacon projectRoot: string, 58082815dcSEvan Bacon packageRoot: string, 59082815dcSEvan Bacon fileName: string, 60082815dcSEvan Bacon type: string 61082815dcSEvan Bacon) { 62082815dcSEvan Bacon const filePath = globSync( 63082815dcSEvan Bacon path.join(projectRoot, `android/app/src/${type}/java/**/${fileName}.@(java|kt)`) 64082815dcSEvan Bacon )[0]; 65082815dcSEvan Bacon 66082815dcSEvan Bacon if (!filePath) { 67082815dcSEvan Bacon return null; 68082815dcSEvan Bacon } 69082815dcSEvan Bacon 70082815dcSEvan Bacon const packagePath = path.dirname(filePath); 71082815dcSEvan Bacon const packagePathParts = path.relative(packageRoot, packagePath).split(path.sep).filter(Boolean); 72082815dcSEvan Bacon 73082815dcSEvan Bacon return packagePathParts.join('.'); 74082815dcSEvan Bacon} 75082815dcSEvan Bacon 76082815dcSEvan Baconfunction getCurrentPackageNameForType(projectRoot: string, type: string): string | null { 77082815dcSEvan Bacon const packageRoot = getPackageRoot(projectRoot, type as any); 78082815dcSEvan Bacon 79082815dcSEvan Bacon if (type === 'main') { 80082815dcSEvan Bacon return getCurrentPackageName(projectRoot, packageRoot); 81082815dcSEvan Bacon } 82082815dcSEvan Bacon // debug, etc.. 83082815dcSEvan Bacon return getCurrentPackageForProjectFile(projectRoot, packageRoot, '*', type); 84082815dcSEvan Bacon} 85082815dcSEvan Bacon 86082815dcSEvan Bacon// NOTE(brentvatne): this assumes that our MainApplication.java file is in the root of the package 87082815dcSEvan Bacon// this makes sense for standard react-native projects but may not apply in customized projects, so if 88082815dcSEvan Bacon// we want this to be runnable in any app we need to handle other possibilities 89082815dcSEvan Baconexport async function renamePackageOnDisk( 90082815dcSEvan Bacon config: Pick<ExpoConfig, 'android'>, 91082815dcSEvan Bacon projectRoot: string 92082815dcSEvan Bacon) { 93082815dcSEvan Bacon const newPackageName = getPackage(config); 94082815dcSEvan Bacon if (newPackageName === null) { 95082815dcSEvan Bacon return; 96082815dcSEvan Bacon } 97082815dcSEvan Bacon 9884f418d7SKudo Chien for (const type of ['debug', 'main', 'release']) { 99082815dcSEvan Bacon await renameJniOnDiskForType({ projectRoot, type, packageName: newPackageName }); 100082815dcSEvan Bacon await renamePackageOnDiskForType({ projectRoot, type, packageName: newPackageName }); 101082815dcSEvan Bacon } 102082815dcSEvan Bacon} 103082815dcSEvan Bacon 104082815dcSEvan Baconexport async function renameJniOnDiskForType({ 105082815dcSEvan Bacon projectRoot, 106082815dcSEvan Bacon type, 107082815dcSEvan Bacon packageName, 108082815dcSEvan Bacon}: { 109082815dcSEvan Bacon projectRoot: string; 110082815dcSEvan Bacon type: string; 111082815dcSEvan Bacon packageName: string; 112082815dcSEvan Bacon}) { 113082815dcSEvan Bacon if (!packageName) { 114082815dcSEvan Bacon return; 115082815dcSEvan Bacon } 116082815dcSEvan Bacon 117082815dcSEvan Bacon const currentPackageName = getCurrentPackageNameForType(projectRoot, type); 118082815dcSEvan Bacon if (!currentPackageName || !packageName || currentPackageName === packageName) { 119082815dcSEvan Bacon return; 120082815dcSEvan Bacon } 121082815dcSEvan Bacon 122082815dcSEvan Bacon const jniRoot = path.join(projectRoot, 'android', 'app', 'src', type, 'jni'); 123082815dcSEvan Bacon const filesToUpdate = [...globSync('**/*', { cwd: jniRoot, absolute: true })]; 124082815dcSEvan Bacon // Replace all occurrences of the path in the project 125082815dcSEvan Bacon filesToUpdate.forEach((filepath: string) => { 126082815dcSEvan Bacon try { 127082815dcSEvan Bacon if (fs.lstatSync(filepath).isFile() && ['.h', '.cpp'].includes(path.extname(filepath))) { 128082815dcSEvan Bacon let contents = fs.readFileSync(filepath).toString(); 129082815dcSEvan Bacon contents = contents.replace( 130082815dcSEvan Bacon new RegExp(transformJavaClassDescriptor(currentPackageName).replace(/\//g, '\\/'), 'g'), 131082815dcSEvan Bacon transformJavaClassDescriptor(packageName) 132082815dcSEvan Bacon ); 133082815dcSEvan Bacon fs.writeFileSync(filepath, contents); 134082815dcSEvan Bacon } 135082815dcSEvan Bacon } catch { 136082815dcSEvan Bacon debug(`Error updating "${filepath}" for type "${type}"`); 137082815dcSEvan Bacon } 138082815dcSEvan Bacon }); 139082815dcSEvan Bacon} 140082815dcSEvan Bacon 141082815dcSEvan Baconexport async function renamePackageOnDiskForType({ 142082815dcSEvan Bacon projectRoot, 143082815dcSEvan Bacon type, 144082815dcSEvan Bacon packageName, 145082815dcSEvan Bacon}: { 146082815dcSEvan Bacon projectRoot: string; 147082815dcSEvan Bacon type: string; 148082815dcSEvan Bacon packageName: string; 149082815dcSEvan Bacon}) { 150082815dcSEvan Bacon if (!packageName) { 151082815dcSEvan Bacon return; 152082815dcSEvan Bacon } 153082815dcSEvan Bacon 154082815dcSEvan Bacon const currentPackageName = getCurrentPackageNameForType(projectRoot, type); 155082815dcSEvan Bacon debug(`Found package "${currentPackageName}" for type "${type}"`); 156082815dcSEvan Bacon if (!currentPackageName || currentPackageName === packageName) { 157082815dcSEvan Bacon return; 158082815dcSEvan Bacon } 159082815dcSEvan Bacon debug(`Refactor "${currentPackageName}" to "${packageName}" for type "${type}"`); 160082815dcSEvan Bacon const packageRoot = getPackageRoot(projectRoot, type as any); 161082815dcSEvan Bacon // Set up our paths 162082815dcSEvan Bacon if (!(await directoryExistsAsync(packageRoot))) { 163082815dcSEvan Bacon debug(`- skipping refactor of missing directory: ${packageRoot}`); 164082815dcSEvan Bacon return; 165082815dcSEvan Bacon } 166082815dcSEvan Bacon 167082815dcSEvan Bacon const currentPackagePath = path.join(packageRoot, ...currentPackageName.split('.')); 168082815dcSEvan Bacon const newPackagePath = path.join(packageRoot, ...packageName.split('.')); 169082815dcSEvan Bacon 170082815dcSEvan Bacon // Create the new directory 171082815dcSEvan Bacon fs.mkdirSync(newPackagePath, { recursive: true }); 172082815dcSEvan Bacon 173082815dcSEvan Bacon // Move everything from the old directory over 174082815dcSEvan Bacon globSync('**/*', { cwd: currentPackagePath }).forEach((relativePath) => { 175082815dcSEvan Bacon const filepath = path.join(currentPackagePath, relativePath); 176082815dcSEvan Bacon if (fs.lstatSync(filepath).isFile()) { 177082815dcSEvan Bacon moveFileSync(filepath, path.join(newPackagePath, relativePath)); 178082815dcSEvan Bacon } else { 179082815dcSEvan Bacon fs.mkdirSync(filepath, { recursive: true }); 180082815dcSEvan Bacon } 181082815dcSEvan Bacon }); 182082815dcSEvan Bacon 183082815dcSEvan Bacon // Remove the old directory recursively from com/old/package to com/old and com, 184082815dcSEvan Bacon // as long as the directories are empty 185082815dcSEvan Bacon const oldPathParts = currentPackageName.split('.'); 186082815dcSEvan Bacon while (oldPathParts.length) { 187082815dcSEvan Bacon const pathToCheck = path.join(packageRoot, ...oldPathParts); 188082815dcSEvan Bacon try { 189082815dcSEvan Bacon const files = fs.readdirSync(pathToCheck); 190082815dcSEvan Bacon if (files.length === 0) { 191082815dcSEvan Bacon fs.rmdirSync(pathToCheck); 192082815dcSEvan Bacon } 193082815dcSEvan Bacon } finally { 194082815dcSEvan Bacon oldPathParts.pop(); 195082815dcSEvan Bacon } 196082815dcSEvan Bacon } 197082815dcSEvan Bacon 198082815dcSEvan Bacon const filesToUpdate = [...globSync('**/*', { cwd: newPackagePath, absolute: true })]; 199082815dcSEvan Bacon // Only update the BUCK file to match the main package name 200082815dcSEvan Bacon if (type === 'main') { 201ed3bd27bSEvan Bacon // NOTE(EvanBacon): We dropped this file in SDK 48 but other templates may still use it. 202082815dcSEvan Bacon filesToUpdate.push(path.join(projectRoot, 'android', 'app', 'BUCK')); 203082815dcSEvan Bacon } 204082815dcSEvan Bacon // Replace all occurrences of the path in the project 205082815dcSEvan Bacon filesToUpdate.forEach((filepath: string) => { 206082815dcSEvan Bacon try { 207082815dcSEvan Bacon if (fs.lstatSync(filepath).isFile()) { 208082815dcSEvan Bacon let contents = fs.readFileSync(filepath).toString(); 209082815dcSEvan Bacon contents = contents.replace(new RegExp(currentPackageName!, 'g'), packageName); 210082815dcSEvan Bacon if (['.h', '.cpp'].includes(path.extname(filepath))) { 211082815dcSEvan Bacon contents = contents.replace( 212082815dcSEvan Bacon new RegExp(transformJavaClassDescriptor(currentPackageName).replace(/\//g, '\\'), 'g'), 213082815dcSEvan Bacon transformJavaClassDescriptor(packageName) 214082815dcSEvan Bacon ); 215082815dcSEvan Bacon } 216082815dcSEvan Bacon fs.writeFileSync(filepath, contents); 217082815dcSEvan Bacon } 218082815dcSEvan Bacon } catch { 219082815dcSEvan Bacon debug(`Error updating "${filepath}" for type "${type}"`); 220082815dcSEvan Bacon } 221082815dcSEvan Bacon }); 222082815dcSEvan Bacon} 223082815dcSEvan Bacon 224082815dcSEvan Baconfunction moveFileSync(src: string, dest: string) { 225082815dcSEvan Bacon fs.mkdirSync(path.dirname(dest), { recursive: true }); 226082815dcSEvan Bacon fs.renameSync(src, dest); 227082815dcSEvan Bacon} 228082815dcSEvan Bacon 229082815dcSEvan Baconexport function setPackageInBuildGradle(config: Pick<ExpoConfig, 'android'>, buildGradle: string) { 230082815dcSEvan Bacon const packageName = getPackage(config); 231082815dcSEvan Bacon if (packageName === null) { 232082815dcSEvan Bacon return buildGradle; 233082815dcSEvan Bacon } 234082815dcSEvan Bacon 23584f418d7SKudo Chien const pattern = new RegExp(`(applicationId|namespace) ['"].*['"]`, 'g'); 23684f418d7SKudo Chien return buildGradle.replace(pattern, `$1 '${packageName}'`); 237082815dcSEvan Bacon} 238082815dcSEvan Bacon 239082815dcSEvan Baconexport async function getApplicationIdAsync(projectRoot: string): Promise<string | null> { 240082815dcSEvan Bacon const buildGradlePath = getAppBuildGradleFilePath(projectRoot); 241082815dcSEvan Bacon if (!fs.existsSync(buildGradlePath)) { 242082815dcSEvan Bacon return null; 243082815dcSEvan Bacon } 244082815dcSEvan Bacon const buildGradle = await fs.promises.readFile(buildGradlePath, 'utf8'); 245082815dcSEvan Bacon const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/); 246082815dcSEvan Bacon // TODO add fallback for legacy cases to read from AndroidManifest.xml 247082815dcSEvan Bacon return matchResult?.[1] ?? null; 248082815dcSEvan Bacon} 249082815dcSEvan Bacon 250082815dcSEvan Bacon/** 251082815dcSEvan Bacon * Transform a java package name to java class descriptor, 252082815dcSEvan Bacon * e.g. `com.helloworld` -> `Lcom/helloworld`. 253082815dcSEvan Bacon */ 254082815dcSEvan Baconfunction transformJavaClassDescriptor(packageName: string) { 255082815dcSEvan Bacon return `L${packageName.replace(/\./g, '/')}`; 256082815dcSEvan Bacon} 257