diff --git a/React/DevSupport/RCTDevMenu.m b/React/DevSupport/RCTDevMenu.m index 782396f82f45ee..69abab6d5201d6 100644 --- a/React/DevSupport/RCTDevMenu.m +++ b/React/DevSupport/RCTDevMenu.m @@ -17,6 +17,10 @@ #if RCT_DEV +#if RCT_ENABLE_INSPECTOR +#import "RCTInspectorDevServerHelper.h" +#endif + NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification"; @implementation UIWindow (RCTDevMenu) @@ -199,6 +203,14 @@ - (void)addItem:(RCTDevMenuItem *)item [bridge reload]; }]]; + if (devSettings.isNuclideDebuggingAvailable) { + [items addObject:[RCTDevMenuItem buttonItemWithTitle:[NSString stringWithFormat:@"Debug JS in Nuclide %@", @"\U0001F4AF"] handler:^{ +#if RCT_ENABLE_INSPECTOR + [RCTInspectorDevServerHelper attachDebugger:@"ReactNative" withBundleURL:bridge.bundleURL withView: RCTPresentedViewController()]; +#endif + }]]; + } + if (!devSettings.isRemoteDebuggingAvailable) { [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Remote JS Debugger Unavailable" handler:^{ UIAlertController *alertController = [UIAlertController @@ -209,7 +221,12 @@ - (void)addItem:(RCTDevMenuItem *)item }]]; } else { [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{ - return devSettings.isDebuggingRemotely ? @"Stop Remote JS Debugging" : @"Debug JS Remotely"; + NSString *title = devSettings.isDebuggingRemotely ? @"Stop Remote JS Debugging" : @"Debug JS Remotely"; + if (devSettings.isNuclideDebuggingAvailable) { + return [NSString stringWithFormat:@"%@ %@", title, @"\U0001F645"]; + } else { + return title; + } } handler:^{ devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely; }]]; diff --git a/React/DevSupport/RCTInspectorDevServerHelper.h b/React/DevSupport/RCTInspectorDevServerHelper.h index b107dbdc6190fa..1a1317ee8e16b4 100644 --- a/React/DevSupport/RCTInspectorDevServerHelper.h +++ b/React/DevSupport/RCTInspectorDevServerHelper.h @@ -2,6 +2,8 @@ #import #import +#import + #import #if RCT_DEV @@ -11,6 +13,9 @@ + (void)connectForContext:(JSGlobalContextRef)context withBundleURL:(NSURL *)bundleURL; + (void)disableDebugger; ++ (void)attachDebugger:(NSString *)owner + withBundleURL:(NSURL *)bundleURL + withView:(UIViewController *)view; @end #endif diff --git a/React/DevSupport/RCTInspectorDevServerHelper.mm b/React/DevSupport/RCTInspectorDevServerHelper.mm index 4f1357fa3671c0..dc80a3a48613aa 100644 --- a/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -4,6 +4,7 @@ #import #import +#import #import "RCTDefines.h" #import "RCTInspectorPackagerConnection.h" @@ -12,16 +13,13 @@ static NSString *const kDebuggerMsgDisable = @"{ \"id\":1,\"method\":\"Debugger.disable\" }"; -static NSString *getDebugServerHost(NSURL *bundleURL) +static NSString *getServerHost(NSURL *bundleURL, NSNumber *port) { NSString *host = [bundleURL host]; if (!host) { host = @"localhost"; } - // Inspector Proxy is run on a separate port (from packager). - NSNumber *port = @8082; - // this is consistent with the Android implementation, where http:// is the // hardcoded implicit scheme for the debug server. Note, packagerURL // technically looks like it could handle schemes/protocols other than HTTP, @@ -32,14 +30,26 @@ static NSURL *getInspectorDeviceUrl(NSURL *bundleURL) { + NSNumber *inspectorProxyPort = @8082; NSString *escapedDeviceName = [[[UIDevice currentDevice] name] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSString *escapedAppName = [[[NSBundle mainBundle] bundleIdentifier] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@", - getDebugServerHost(bundleURL), + getServerHost(bundleURL, inspectorProxyPort), escapedDeviceName, escapedAppName]]; } +static NSURL *getAttachDeviceUrl(NSURL *bundleURL, NSString *title) +{ + NSNumber *metroBundlerPort = @8081; + NSString *escapedDeviceName = [[[UIDevice currentDevice] name] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSString *escapedAppName = [[[NSBundle mainBundle] bundleIdentifier] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/attach-debugger-nuclide?title=%@&device=%@&app=%@", + getServerHost(bundleURL, metroBundlerPort), + title, + escapedDeviceName, + escapedAppName]]; +} @implementation RCTInspectorDevServerHelper @@ -54,6 +64,41 @@ static void sendEventToAllConnections(NSString *event) } } +static void displayErrorAlert(UIViewController *view, NSString *message) { + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:nil + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [view presentViewController:alert animated:YES completion:nil]; + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2.5), + dispatch_get_main_queue(), + ^{ + [alert dismissViewControllerAnimated:YES completion:nil]; + }); +} + ++ (void)attachDebugger:(NSString *)owner + withBundleURL:(NSURL *)bundleURL + withView:(UIViewController *)view +{ + NSURL *url = getAttachDeviceUrl(bundleURL, owner); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setHTTPMethod:@"GET"]; + + __weak UIViewController *viewCapture = view; + [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler: + ^(NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + UIViewController *viewCaptureStrong = viewCapture; + if (error != nullptr && viewCaptureStrong != nullptr) { + displayErrorAlert(viewCaptureStrong, @"The request to attach Nuclide couldn't reach Metro Bundler!"); + } + }] resume]; +} + + (void)disableDebugger { sendEventToAllConnections(kDebuggerMsgDisable); diff --git a/React/Modules/RCTDevSettings.h b/React/Modules/RCTDevSettings.h index e3b2a8b152b4cf..e4aa72a065be2a 100644 --- a/React/Modules/RCTDevSettings.h +++ b/React/Modules/RCTDevSettings.h @@ -38,6 +38,7 @@ @property (nonatomic, readonly) BOOL isHotLoadingAvailable; @property (nonatomic, readonly) BOOL isLiveReloadAvailable; @property (nonatomic, readonly) BOOL isRemoteDebuggingAvailable; +@property (nonatomic, readonly) BOOL isNuclideDebuggingAvailable; @property (nonatomic, readonly) BOOL isJSCSamplingProfilerAvailable; /** diff --git a/React/Modules/RCTDevSettings.mm b/React/Modules/RCTDevSettings.mm index f8e79f84fad0fc..7ec3f6b9fce29b 100644 --- a/React/Modules/RCTDevSettings.mm +++ b/React/Modules/RCTDevSettings.mm @@ -252,6 +252,15 @@ - (id)settingForKey:(NSString *)key return [_dataSource settingForKey:key]; } +- (BOOL)isNuclideDebuggingAvailable +{ +#if RCT_ENABLE_INSPECTOR + return true; +#else + return false; +#endif //RCT_ENABLE_INSPECTOR +} + - (BOOL)isRemoteDebuggingAvailable { Class jsDebuggingExecutorClass = objc_lookUpClass("RCTWebSocketExecutor");