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