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