1import {
2  ConfigPlugin,
3  createRunOncePlugin,
4  ExportedConfigWithProps,
5  WarningAggregator,
6  withDangerousMod,
7  withMainActivity,
8} from '@expo/config-plugins';
9import { ExpoConfig } from '@expo/config-types';
10import fs from 'fs';
11import path from 'path';
12
13import { withDevLauncherAppDelegate } from './withDevLauncherAppDelegate';
14
15const pkg = require('expo-dev-launcher/package.json');
16
17const DEV_LAUNCHER_ANDROID_IMPORT = 'expo.modules.devlauncher.DevLauncherController';
18const DEV_LAUNCHER_ON_NEW_INTENT = `
19  @Override
20  public void onNewIntent(Intent intent) {
21      if (DevLauncherController.tryToHandleIntent(this, intent)) {
22         return;
23      }
24      super.onNewIntent(intent);
25  }
26`;
27const DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE = `DevLauncherController.wrapReactActivityDelegate(this, () -> $1);`;
28const DEV_LAUNCHER_ANDROID_INIT = 'DevLauncherController.initialize(this, getReactNativeHost());';
29
30const DEV_LAUNCHER_POD_IMPORT =
31  "pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', :configurations => :debug";
32
33async function readFileAsync(path: string): Promise<string> {
34  return fs.promises.readFile(path, 'utf8');
35}
36
37async function saveFileAsync(path: string, content: string): Promise<void> {
38  return fs.promises.writeFile(path, content, 'utf8');
39}
40
41function addLines(content: string, find: string | RegExp, offset: number, toAdd: string[]) {
42  const lines = content.split('\n');
43
44  let lineIndex = lines.findIndex(line => line.match(find));
45
46  for (const newLine of toAdd) {
47    if (!content.includes(newLine)) {
48      lines.splice(lineIndex + offset, 0, newLine);
49      lineIndex++;
50    }
51  }
52
53  return lines.join('\n');
54}
55
56function addJavaImports(javaSource: string, javaImports: string[]): string {
57  const lines = javaSource.split('\n');
58  const lineIndexWithPackageDeclaration = lines.findIndex(line => line.match(/^package .*;$/));
59  for (const javaImport of javaImports) {
60    if (!javaSource.includes(javaImport)) {
61      const importStatement = `import ${javaImport};`;
62      lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement);
63    }
64  }
65  return lines.join('\n');
66}
67
68async function editMainApplication(
69  config: ExportedConfigWithProps,
70  action: (mainApplication: string) => string
71): Promise<void> {
72  const mainApplicationPath = path.join(
73    config.modRequest.platformProjectRoot,
74    'app',
75    'src',
76    'main',
77    'java',
78    ...config.android!.package!.split('.'),
79    'MainApplication.java'
80  );
81
82  try {
83    const mainApplication = action(await readFileAsync(mainApplicationPath));
84    return await saveFileAsync(mainApplicationPath, mainApplication);
85  } catch (e) {
86    WarningAggregator.addWarningIOS(
87      'expo-dev-launcher',
88      `Couldn't modified MainApplication.java - ${e}.`
89    );
90  }
91}
92
93async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) {
94  const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
95  try {
96    const podfile = action(await readFileAsync(podfilePath));
97
98    return await saveFileAsync(podfilePath, podfile);
99  } catch (e) {
100    WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modified AppDelegate.m - ${e}.`);
101  }
102}
103
104const withDevLauncherApplication: ConfigPlugin = config => {
105  return withDangerousMod(config, [
106    'android',
107    async config => {
108      await editMainApplication(config, mainApplication => {
109        mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_ANDROID_IMPORT]);
110
111        mainApplication = addLines(mainApplication, 'super.onCreate()', 1, [
112          `    ${DEV_LAUNCHER_ANDROID_INIT}`,
113        ]);
114
115        return mainApplication;
116      });
117      return config;
118    },
119  ]);
120};
121
122const withDevLauncherActivity: ConfigPlugin = config => {
123  return withMainActivity(config, config => {
124    if (config.modResults.language === 'java') {
125      let content = addJavaImports(config.modResults.contents, [
126        DEV_LAUNCHER_ANDROID_IMPORT,
127        'android.content.Intent',
128      ]);
129
130      if (!content.includes(DEV_LAUNCHER_ON_NEW_INTENT)) {
131        const lines = content.split('\n');
132        const onCreateIndex = lines.findIndex(line => line.includes('public class MainActivity'));
133
134        lines.splice(onCreateIndex + 1, 0, DEV_LAUNCHER_ON_NEW_INTENT);
135
136        content = lines.join('\n');
137      }
138
139      if (!content.includes('DevLauncherController.wrapReactActivityDelegate')) {
140        content = content.replace(
141          /(new ReactActivityDelegate(.*|\s)*});$/m,
142          DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE
143        );
144      }
145
146      config.modResults.contents = content;
147    } else {
148      WarningAggregator.addWarningAndroid(
149        'expo-dev-launcher',
150        `Cannot automatically configure MainActivity if it's not java`
151      );
152    }
153
154    return config;
155  });
156};
157
158const withDevLauncherPodfile: ConfigPlugin = config => {
159  return withDangerousMod(config, [
160    'ios',
161    async config => {
162      await editPodfile(config, podfile => {
163        podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'");
164        // Match both variations of Ruby config:
165        // unknown: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', :configurations => :debug
166        // Rubocop: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', configurations: :debug
167        if (
168          !podfile.match(
169            /pod ['"]expo-dev-launcher['"],\s?path: ['"]\.\.\/node_modules\/expo-dev-launcher['"],\s?:?configurations:?\s(?:=>\s)?:debug/
170          )
171        ) {
172          podfile = addLines(podfile, 'use_react_native', 0, [`  ${DEV_LAUNCHER_POD_IMPORT}`]);
173        }
174        return podfile;
175      });
176      return config;
177    },
178  ]);
179};
180
181const withDevLauncher = (config: ExpoConfig) => {
182  config = withDevLauncherActivity(config);
183  config = withDevLauncherApplication(config);
184  config = withDevLauncherPodfile(config);
185  config = withDevLauncherAppDelegate(config);
186  return config;
187};
188
189export default createRunOncePlugin(withDevLauncher, pkg.name, pkg.version);
190