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