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