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