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