From 4c8a9f0d00cfb452a03077d914e6accb94a9769b Mon Sep 17 00:00:00 2001 From: digeff Date: Thu, 7 Apr 2016 13:14:39 -0700 Subject: [PATCH] Added support for JavaScript third-party debuggers Summary:* Add ability to configure the app that should open when starting debugging axemclion discussed this feature with tadeuzagallo and martinbigio on: https://github.com/facebook/react-native/issues/5051 Closes https://github.com/facebook/react-native/pull/5683 Reviewed By: martinbigio Differential Revision: D2971497 Pulled By: mkonicek fb-gh-sync-id: 91c3ce68feed989658124bb96cb61d03dd032599 fbshipit-source-id: 91c3ce68feed989658124bb96cb61d03dd032599 --- Libraries/WebSocket/RCTWebSocketExecutor.m | 4 +- React/Modules/RCTDevMenu.m | 14 ++--- .../facebook/react/bridge/JSBundleLoader.java | 2 +- .../react/devsupport/DevServerHelper.java | 14 ++--- .../devsupport/DevSupportManagerImpl.java | 4 +- .../main/res/devsupport/values/strings.xml | 4 +- docs/Debugging.md | 5 +- .../middleware/getDevToolsMiddleware.js | 55 +++++++++++++++---- 8 files changed, 68 insertions(+), 34 deletions(-) diff --git a/Libraries/WebSocket/RCTWebSocketExecutor.m b/Libraries/WebSocket/RCTWebSocketExecutor.m index 7b78d2942..14f61cc86 100644 --- a/Libraries/WebSocket/RCTWebSocketExecutor.m +++ b/Libraries/WebSocket/RCTWebSocketExecutor.m @@ -62,7 +62,7 @@ RCT_EXPORT_MODULE() _injectedObjects = [NSMutableDictionary new]; [_socket setDelegateDispatchQueue:_jsQueue]; - NSURL *startDevToolsURL = [NSURL URLWithString:@"/launch-chrome-devtools" relativeToURL:_url]; + NSURL *startDevToolsURL = [NSURL URLWithString:@"/launch-js-devtools" relativeToURL:_url]; [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:startDevToolsURL] delegate:nil]; if (![self connectToProxy]) { @@ -82,7 +82,7 @@ RCT_EXPORT_MODULE() if (!runtimeIsReady) { RCTLogError(@"Runtime is not ready for debugging.\n " "- Make sure Packager server is running.\n" - "- Make sure Chrome is running and not paused on a breakpoint or exception and try reloading again."); + "- Make sure the JavaScript Debugger is running and not paused on a breakpoint or exception and try reloading again."); [self invalidate]; return; } diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index 58cf5e531..fcb789a46 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -185,7 +185,7 @@ RCT_EXPORT_MODULE() [weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; }]]; - _webSocketExecutorName = [_defaults objectForKey:@"websocket-executor-name"] ?: @"Chrome"; + _webSocketExecutorName = [_defaults objectForKey:@"websocket-executor-name"] ?: @"JS Remotely"; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -443,8 +443,8 @@ RCT_EXPORT_MODULE() [weakSelf reload]; }]]; - Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); - if (!chromeExecutorClass) { + Class jsDebuggingExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); + if (!jsDebuggingExecutorClass) { [items addObject:[RCTDevMenuItem buttonItemWithTitle:[NSString stringWithFormat:@"%@ Debugger Unavailable", _webSocketExecutorName] handler:^{ UIAlertView *alert = RCTAlertView( [NSString stringWithFormat:@"%@ Debugger Unavailable", _webSocketExecutorName], @@ -455,10 +455,10 @@ RCT_EXPORT_MODULE() [alert show]; }]]; } else { - BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; - NSString *debugTitleChrome = isDebuggingInChrome ? [NSString stringWithFormat:@"Disable %@ Debugging", _webSocketExecutorName] : [NSString stringWithFormat:@"Debug in %@", _webSocketExecutorName]; - [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleChrome handler:^{ - weakSelf.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; + BOOL isDebuggingJS = _executorClass && _executorClass == jsDebuggingExecutorClass; + NSString *debugTitleJS = isDebuggingJS ? [NSString stringWithFormat:@"Disable %@ Debugging", _webSocketExecutorName] : [NSString stringWithFormat:@"Debug %@", _webSocketExecutorName]; + [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleJS handler:^{ + weakSelf.executorClass = isDebuggingJS ? Nil : jsDebuggingExecutorClass; }]]; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java index 799863b08..6722cb83c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSBundleLoader.java @@ -69,7 +69,7 @@ public abstract class JSBundleLoader { * This loader is used when proxy debugging is enabled. In that case there is no point in fetching * the bundle from device as remote executor will have to do it anyway. * - * @param proxySourceURL the URL to load the JS bundle from in Chrome + * @param proxySourceURL the URL to load the JS bundle from in the JavaScript proxy * @param realSourceURL the URL to report as the source URL, e.g. for asset loading */ public static JSBundleLoader createRemoteDebuggerBundleLoader( diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index b48d8f632..ffd53e5bd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -54,8 +54,8 @@ public class DevServerHelper { "http://%s/%s.bundle?platform=android&dev=%s&hot=%s&minify=%s"; private static final String SOURCE_MAP_URL_FORMAT = BUNDLE_URL_FORMAT.replaceFirst("\\.bundle", ".map"); - private static final String LAUNCH_CHROME_DEVTOOLS_COMMAND_URL_FORMAT = - "http://%s/launch-chrome-devtools"; + private static final String LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT = + "http://%s/launch-js-devtools"; private static final String ONCHANGE_ENDPOINT_URL_FORMAT = "http://%s/onchange"; private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy?role=client"; @@ -366,13 +366,13 @@ public class DevServerHelper { return String.format(Locale.US, ONCHANGE_ENDPOINT_URL_FORMAT, getDebugServerHost()); } - private String createLaunchChromeDevtoolsCommandUrl() { - return String.format(LAUNCH_CHROME_DEVTOOLS_COMMAND_URL_FORMAT, getDebugServerHost()); + private String createLaunchJSDevtoolsCommandUrl() { + return String.format(LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT, getDebugServerHost()); } - public void launchChromeDevtools() { + public void launchJSDevtools() { Request request = new Request.Builder() - .url(createLaunchChromeDevtoolsCommandUrl()) + .url(createLaunchJSDevtoolsCommandUrl()) .build(); mClient.newCall(request).enqueue(new Callback() { @Override @@ -398,7 +398,7 @@ public class DevServerHelper { public String getJSBundleURLForRemoteDebugging(String mainModuleName) { // The host IP we use when connecting to the JS bundle server from the emulator is not the - // same as the one needed to connect to the same server from the Chrome proxy running on the + // same as the one needed to connect to the same server from the JavaScript proxy running on the // host itself. return createBundleURL(getHostForJSProxy(), mainModuleName, getDevMode(), getHMR(), getJSMinifyMode()); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 75d7282a7..cd85ad64c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -139,7 +139,7 @@ public class DevSupportManagerImpl implements DevSupportManager { if (DevServerHelper.getReloadAppAction(context).equals(action)) { if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { mIsUsingJSProxy = true; - mDevServerHelper.launchChromeDevtools(); + mDevServerHelper.launchJSDevtools(); } else { mIsUsingJSProxy = false; } @@ -584,7 +584,7 @@ public class DevSupportManagerImpl implements DevSupportManager { private void reloadJSInProxyMode(final ProgressDialog progressDialog) { // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that // anyway - mDevServerHelper.launchChromeDevtools(); + mDevServerHelper.launchJSDevtools(); JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() { @Override diff --git a/ReactAndroid/src/main/res/devsupport/values/strings.xml b/ReactAndroid/src/main/res/devsupport/values/strings.xml index 647a4c373..ea86d0ab9 100644 --- a/ReactAndroid/src/main/res/devsupport/values/strings.xml +++ b/ReactAndroid/src/main/res/devsupport/values/strings.xml @@ -1,8 +1,8 @@ Reload JS - Debug in Chrome - Stop Chrome Debugging + Debug JS Remotely + Stop JS Remotely Debugging Enable Hot Reloading Disable Hot Reloading Enable Live Reload diff --git a/docs/Debugging.md b/docs/Debugging.md index 841aa2830..d5373b79b 100644 --- a/docs/Debugging.md +++ b/docs/Debugging.md @@ -34,7 +34,7 @@ You can use `console.error` to display a full screen error on a red background. These boxes only appear when you're running your app in dev mode. ### Chrome Developer Tools -To debug the JavaScript code in Chrome, select `Debug in Chrome` from the developer menu. This will open a new tab at [http://localhost:8081/debugger-ui](http://localhost:8081/debugger-ui). +To debug the JavaScript code in Chrome, select `Debug JS Remotely` from the developer menu. This will open a new tab at [http://localhost:8081/debugger-ui](http://localhost:8081/debugger-ui). In Chrome, press `⌘ + option + i` or select `View` → `Developer` → `Developer Tools` to toggle the developer tools console. Enable [Pause On Caught Exceptions](http://stackoverflow.com/questions/2233339/javascript-is-there-a-way-to-get-chrome-to-break-on-all-errors/17324511#17324511) for a better debugging experience. @@ -43,6 +43,9 @@ To debug on a real device: 1. On iOS - open the file `RCTWebSocketExecutor.m` and change `localhost` to the IP address of your computer. Shake the device to open the development menu with the option to start debugging. 2. On Android, if you're running Android 5.0+ device connected via USB you can use `adb` command line tool to setup port forwarding from the device to your computer. For that run: `adb reverse tcp:8081 tcp:8081` (see [this link](http://developer.android.com/tools/help/adb.html) for help on `adb` command). Alternatively, you can [open dev menu](#debugging-react-native-apps) on the device and select `Dev Settings`, then update `Debug server host for device` setting to the IP address of your computer. +### Custom JavaScript debugger +To use a custom JavaScript debugger define the `REACT_DEBUGGER` environment variable to a command that will start your custom debugger. That variable will be read from the Packager process. If that environment variable is set, selecting `Debug JS Remotely` from the developer menu will execute that command instead of opening Chrome. The exact command to be executed is the contents of the REACT_DEBUGGER environment variable followed by the space separated paths of all project roots (e.g. If you set REACT_DEBUGGER="node /path/to/launchDebugger.js --port 2345 --type ReactNative" then the command "node /path/to/launchDebugger.js --port 2345 --type ReactNative /path/to/reactNative/app" will end up being executed). Custom debugger commands executed this way should be short-lived processes, and they shouldn't produce more than 200 kilobytes of output. + ### Live Reload This option allows for your JS changes to trigger automatic reload on the connected device/emulator. To enable this option: diff --git a/local-cli/server/middleware/getDevToolsMiddleware.js b/local-cli/server/middleware/getDevToolsMiddleware.js index 53c73d8d6..6c4f7ab82 100644 --- a/local-cli/server/middleware/getDevToolsMiddleware.js +++ b/local-cli/server/middleware/getDevToolsMiddleware.js @@ -8,6 +8,7 @@ */ 'use strict'; +var child_process = require('child_process'); var execFile = require('child_process').execFile; var fs = require('fs'); var opn = require('opn'); @@ -24,7 +25,39 @@ function getChromeAppName() { } } -module.exports = function(options, isDebuggerConnected) { +function launchChromeDevTools(port) { + var debuggerURL = 'http://localhost:' + port + '/debugger-ui'; + console.log('Launching Dev Tools...'); + opn(debuggerURL, {app: [getChromeAppName()]}, function(err) { + if (err) { + console.error('Google Chrome exited with error:', err); + } + }); +} + +function escapePath(path) { + return '"' + path + '"'; // " Can escape paths with spaces in OS X, Windows, and *nix +} + +function launchDevTools(options, isChromeConnected) { + // Explicit config always wins + var customDebugger = process.env.REACT_DEBUGGER; + if (customDebugger) { + var projects = options.projectRoots.map(escapePath).join(' '); + var command = customDebugger + ' ' + projects; + console.log('Starting custom debugger by executing: ' + command); + child_process.exec(command, function (error, stdout, stderr) { + if (error !== null) { + console.log('Error while starting custom debugger: ' + error); + } + }); + } else if (!isChromeConnected()) { + // Dev tools are not yet open; we need to open a session + launchChromeDevTools(options.port); + } +} + +module.exports = function(options, isChromeConnected) { return function(req, res, next) { if (req.url === '/debugger-ui') { var debuggerPath = path.join(__dirname, '..', 'util', 'debugger.html'); @@ -41,18 +74,16 @@ module.exports = function(options, isDebuggerConnected) { 'If you still need this, please let us know.' ); } else if (req.url === '/launch-chrome-devtools') { - if (isDebuggerConnected()) { - // Dev tools are already open; no need to open another session + // TODO: Remove this case in the future + console.log( + 'The method /launch-chrome-devtools is deprecated. You are ' + + ' probably using an application created with an older CLI with the ' + + ' packager of a newer CLI. Please upgrade your application: ' + + 'https://facebook.github.io/react-native/docs/upgrading.html'); + launchDevTools(options, isChromeConnected); res.end('OK'); - return; - } - var debuggerURL = 'http://localhost:' + options.port + '/debugger-ui'; - console.log('Launching Dev Tools...'); - opn(debuggerURL, {app: [getChromeAppName()]}, function(err) { - if (err) { - console.error('Google Chrome exited with error:', err); - } - }); + } else if (req.url === '/launch-js-devtools') { + launchDevTools(options, isChromeConnected); res.end('OK'); } else { next();