1const { withDangerousMod, IOSConfig } = require('@expo/config-plugins');
2const fs = require('fs-extra');
3
4const withDevMenu = config => {
5  return withDangerousMod(config, [
6    'ios',
7    async config => {
8      const fileInfo = IOSConfig.Paths.getAppDelegate(config.modRequest.projectRoot);
9      let contents = await fs.readFile(fileInfo.path, 'utf-8');
10      if (fileInfo.language === 'objc') {
11        // Add DevMenu imports
12        if (!contents.includes('#import <React/RCTDevMenu.h>')) {
13          contents = contents.replace(
14            /#import "AppDelegate.h"/g,
15            `#import "AppDelegate.h"
16#import <React/RCTDevMenu.h>`
17          );
18        }
19        if (!contents.includes('#import <React/RCTUtils.h>')) {
20          contents = contents.replace(
21            /#import "AppDelegate.h"/g,
22            `#import "AppDelegate.h"
23#import <React/RCTUtils.h>`
24          );
25        }
26
27        // Make the extraModules mutable
28        const modulesRegex = /NSArray<id<RCTBridgeModule>>\s?\*extraModules\s?=\s?\[_moduleRegistryAdapter extraModulesForBridge:bridge\];/;
29        if (contents.match(modulesRegex)) {
30          contents = contents.replace(
31            modulesRegex,
32            'NSMutableArray<id<RCTBridgeModule>> *extraModules = [NSMutableArray arrayWithArray:[_moduleRegistryAdapter extraModulesForBridge:bridge]];'
33          );
34        }
35
36        // Add DevMenu back
37        if (
38          !contents.includes(
39            '[extraModules addObject:(id<RCTBridgeModule>)[[RCTDevMenu alloc] init]];'
40          )
41        ) {
42          contents = contents.replace(
43            /return extraModules;/g,
44            `[extraModules addObject:(id<RCTBridgeModule>)[[RCTDevMenu alloc] init]];
45  return extraModules;`
46          );
47        }
48
49        // Add swizzling invocation
50        if (!contents.includes(swizzleMethodInvocationBlock)) {
51          // self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc]
52          contents = contents.replace(
53            /self\.moduleRegistryAdapter = \[\[UMModuleRegistryAdapter alloc\]/g,
54            `${swizzleMethodInvocationBlock}
55  self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc]`
56          );
57        }
58
59        // Add swizzling method
60        if (!contents.match(/\(void\)\s?ensureReactMethodSwizzlingSetUp/g)) {
61          const sections = contents.split('@end');
62          sections[sections.length - 2] += swizzleMethodBlock;
63          contents = sections.join('@end');
64        }
65      } else {
66        throw new Error(
67          `Cannot append DevMenu module to AppDelegate of language "${fileInfo.language}"`
68        );
69      }
70      await fs.writeFile(fileInfo.path, contents);
71
72      return config;
73    },
74  ]);
75};
76
77const swizzleMethodInvocationBlock = `[self ensureReactMethodSwizzlingSetUp];`;
78
79const swizzleMethodBlock = `
80// Bring back React method swizzling removed from its Pod
81// when integrating with Expo client.
82// https://github.com/expo/react-native/commit/7f2912e8005ea6e81c45935241081153b822b988
83- (void)ensureReactMethodSwizzlingSetUp
84{
85  static dispatch_once_t onceToken;
86  dispatch_once(&onceToken, ^{
87    #pragma clang diagnostic push
88    #pragma clang diagnostic ignored "-Wundeclared-selector"
89    // RCTKeyCommands.m
90    // swizzle UIResponder
91    RCTSwapInstanceMethods([UIResponder class],
92                          @selector(keyCommands),
93                          @selector(RCT_keyCommands));
94
95    // RCTDevMenu.m
96    // We're swizzling here because it's poor form to override methods in a category,
97    // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
98    // no need to call the original implementation.
99    RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
100    #pragma clang diagnostic pop
101  });
102}
103`;
104
105module.exports = withDevMenu;
106