1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import React
4 
5 @objc
6 class DevMenuAppInstance: DevMenuBaseAppInstance, RCTBridgeDelegate {
7   static private var CloseEventName = "closeDevMenu"
8   static private var OpenEventName = "openDevMenu"
9 
10   private let manager: DevMenuManager
11 
12   var bridge: RCTBridge?
13 
14   init(manager: DevMenuManager) {
15     self.manager = manager
16 
17     super.init()
18 
19     self.bridge = DevMenuRCTBridge.init(delegate: self, launchOptions: nil)
20     fixChromeDevTools()
21   }
22 
23   init(manager: DevMenuManager, bridge: RCTBridge) {
24     self.manager = manager
25 
26     super.init()
27 
28     self.bridge = bridge
29     fixChromeDevTools()
30   }
31 
32   private func fixChromeDevTools() {
33     // Hermes inspector will use latest executed script for Chrome DevTools Protocol.
34     // It will be EXDevMenuApp.ios.js in our case.
35     // To let Hermes aware target bundle, we try to reload here as a workaround solution.
36     // See https://github.com/facebook/react-native/blob/ec614c16b331bf3f793fda5780fa273d181a8492/ReactCommon/hermes/inspector/Inspector.cpp#L291
37     if let appBridge = manager.currentBridge {
38       appBridge.requestReload()
39     }
40   }
41 
42   /**
43    Sends an event to JS triggering the animation that collapses the dev menu.
44    */
45   public func sendCloseEvent() {
46     bridge?.enqueueJSCall("RCTDeviceEventEmitter.emit", args: [DevMenuAppInstance.CloseEventName])
47   }
48 
49   public func sendOpenEvent() {
50     bridge?.enqueueJSCall("RCTDeviceEventEmitter.emit", args: [DevMenuAppInstance.OpenEventName])
51   }
52 
53   // MARK: RCTBridgeDelegate
54 
55   func sourceURL(for bridge: RCTBridge!) -> URL! {
56     #if DEBUG
57     if let packagerHost = jsPackagerHost() {
58       return RCTBundleURLProvider.jsBundleURL(forBundleRoot: "index", packagerHost: packagerHost, enableDev: true, enableMinification: false)
59     }
60     #endif
61     return jsSourceUrl()
62   }
63 
64   func extraModules(for bridge: RCTBridge!) -> [RCTBridgeModule]! {
65     var modules: [RCTBridgeModule] = [DevMenuInternalModule(manager: manager)]
66     modules.append(contentsOf: DevMenuVendoredModulesUtils.vendoredModules())
67     modules.append(DevMenuLoadingView.init())
68     return modules
69   }
70 
71   func bridge(_ bridge: RCTBridge!, didNotFindModule moduleName: String!) -> Bool {
72     return moduleName == "DevMenu"
73   }
74 
75   // MARK: private
76 
77   private func jsSourceUrl() -> URL? {
78     return DevMenuUtils.resourcesBundle()?.url(forResource: "EXDevMenuApp.ios", withExtension: "js")
79   }
80 
81   private func jsPackagerHost() -> String? {
82     // Return `nil` if resource doesn't exist in the bundle.
83     guard let packagerHostPath = DevMenuUtils.resourcesBundle()?.path(forResource: "dev-menu-packager-host", ofType: nil) else {
84       return nil
85     }
86     // Return `nil` if the content is not a valid URL.
87     guard let content = try? String(contentsOfFile: packagerHostPath, encoding: String.Encoding.utf8).trimmingCharacters(in: CharacterSet.newlines),
88       let url = URL(string: content) else {
89       return nil
90     }
91     return url.absoluteString
92   }
93 }
94