Browse files

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: #5051
Closes #5683

Reviewed By: martinbigio

Differential Revision: D2971497

Pulled By: mkonicek

fb-gh-sync-id: 91c3ce68feed989658124bb96cb61d03dd032599
fbshipit-source-id: 91c3ce68feed989658124bb96cb61d03dd032599
  • Loading branch information...
digeff authored and Facebook Github Bot 1 committed Apr 7, 2016
1 parent b9396cd commit 4c8a9f0d00cfb452a03077d914e6accb94a9769b
@@ -62,7 +62,7 @@ - (void)setUp
_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 @@ - (void)setUp
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];
@@ -185,7 +185,7 @@ - (instancetype)init
[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 @@ - (void)addItem:(RCTDevMenuItem *)item
[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 @@ - (void)addItem:(RCTDevMenuItem *)item
[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;
@@ -69,7 +69,7 @@ public String getSourceUrl() {
* 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(
@@ -54,8 +54,8 @@
private static final String SOURCE_MAP_URL_FORMAT =
BUNDLE_URL_FORMAT.replaceFirst("\\.bundle", ".map");
private static final String LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT =
private static final String ONCHANGE_ENDPOINT_URL_FORMAT =
private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy?role=client";
@@ -366,13 +366,13 @@ private String createOnChangeEndpointUrl() {
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()
mClient.newCall(request).enqueue(new Callback() {
@@ -398,7 +398,7 @@ public String getSourceUrl(String mainModuleName) {
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());
@@ -139,7 +139,7 @@ public void onReceive(Context context, Intent intent) {
if (DevServerHelper.getReloadAppAction(context).equals(action)) {
if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) {
mIsUsingJSProxy = true;
} else {
mIsUsingJSProxy = false;
@@ -584,7 +584,7 @@ public void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback) {
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
JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() {
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<string name="catalyst_reloadjs" project="catalyst" translatable="false">Reload JS</string>
<string name="catalyst_debugjs" project="catalyst" translatable="false">Debug in Chrome</string>
<string name="catalyst_debugjs_off" project="catalyst" translatable="false">Stop Chrome Debugging</string>
<string name="catalyst_debugjs" project="catalyst" translatable="false">Debug JS Remotely</string>
<string name="catalyst_debugjs_off" project="catalyst" translatable="false">Stop JS Remotely Debugging</string>
<string name="catalyst_hot_module_replacement" project="catalyst" translatable="false">Enable Hot Reloading</string>
<string name="catalyst_hot_module_replacement_off" project="catalyst" translatable="false">Disable Hot Reloading</string>
<string name="catalyst_live_reload" project="catalyst" translatable="false">Enable Live Reload</string>
@@ -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]( 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]( 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:
@@ -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 =' ');
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
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
'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: ' +
launchDevTools(options, isChromeConnected);
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);
} else {

0 comments on commit 4c8a9f0

Please sign in to comment.