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 { InstallationPage } from './constants';
14import { withDevMenuAppDelegate } from './withDevMenuAppDelegate';
15
16const pkg = require('expo-dev-menu/package.json');
17
18const DEV_MENU_ANDROID_IMPORT = 'expo.modules.devmenu.react.DevMenuAwareReactActivity';
19const DEV_MENU_ACTIVITY_CLASS = 'public class MainActivity extends DevMenuAwareReactActivity {';
20
21async function readFileAsync(path: string): Promise<string> {
22  return fs.promises.readFile(path, 'utf8');
23}
24
25async function saveFileAsync(path: string, content: string): Promise<void> {
26  return fs.promises.writeFile(path, content, 'utf8');
27}
28
29function addJavaImports(javaSource: string, javaImports: string[]): string {
30  const lines = javaSource.split('\n');
31  const lineIndexWithPackageDeclaration = lines.findIndex((line) => line.match(/^package .*;$/));
32  for (const javaImport of javaImports) {
33    if (!javaSource.includes(javaImport)) {
34      const importStatement = `import ${javaImport};`;
35      lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement);
36    }
37  }
38  return lines.join('\n');
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
56async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) {
57  const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
58  try {
59    const podfile = action(await readFileAsync(podfilePath));
60
61    return await saveFileAsync(podfilePath, podfile);
62  } catch (e) {
63    WarningAggregator.addWarningIOS(
64      'expo-dev-menu',
65      `Couldn't modified AppDelegate.m - ${e}.
66See the expo-dev-client installation instructions to modify your AppDelegate manually: ${InstallationPage}`
67    );
68  }
69}
70
71const withDevMenuActivity: ConfigPlugin = (config) => {
72  return withMainActivity(config, (config) => {
73    if (config.modResults.language === 'java') {
74      let content = config.modResults.contents;
75      content = addJavaImports(content, [DEV_MENU_ANDROID_IMPORT]);
76      content = content.replace(
77        'public class MainActivity extends ReactActivity {',
78        DEV_MENU_ACTIVITY_CLASS
79      );
80      config.modResults.contents = content;
81    } else {
82      WarningAggregator.addWarningAndroid(
83        'expo-dev-menu',
84        `Cannot automatically configure MainActivity if it's not java.
85See the expo-dev-client installation instructions to modify your MainActivity manually: ${InstallationPage}`
86      );
87    }
88
89    return config;
90  });
91};
92
93const withDevMenuPodfile: ConfigPlugin = (config) => {
94  return withDangerousMod(config, [
95    'ios',
96    async (config) => {
97      await editPodfile(config, (podfile) => {
98        podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'");
99        // Match both variations of Ruby config:
100        // unknown: pod 'expo-dev-menu', path: '../node_modules/expo-dev-menu', :configurations => :debug
101        // Rubocop: pod 'expo-dev-menu', path: '../node_modules/expo-dev-menu', configurations: :debug
102        if (
103          !podfile.match(
104            /pod ['"]expo-dev-menu['"],\s?path: ['"][^'"]*node_modules\/expo-dev-menu['"],\s?:?configurations:?\s(?:=>\s)?:debug/
105          )
106        ) {
107          const packagePath = path.dirname(require.resolve('expo-dev-menu/package.json'));
108          const relativePath = path.relative(config.modRequest.platformProjectRoot, packagePath);
109          podfile = addLines(podfile, 'use_react_native', 0, [
110            `  pod 'expo-dev-menu', path: '${relativePath}', :configurations => :debug`,
111          ]);
112        }
113        return podfile;
114      });
115      return config;
116    },
117  ]);
118};
119
120const withDevMenu = (config: ExpoConfig) => {
121  config = withDevMenuActivity(config);
122  config = withDevMenuPodfile(config);
123  config = withDevMenuAppDelegate(config);
124  return config;
125};
126
127export default createRunOncePlugin(withDevMenu, pkg.name, pkg.version);
128