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