Skip to content

Commit

Permalink
Added support for JavaScript third-party debuggers
Browse files Browse the repository at this point in the history
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 4c8a9f0
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 34 deletions.
4 changes: 2 additions & 2 deletions Libraries/WebSocket/RCTWebSocketExecutor.m
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand All @@ -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];
return;
}
Expand Down
14 changes: 7 additions & 7 deletions React/Modules/RCTDevMenu.m
Original file line number Diff line number Diff line change
Expand Up @@ -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, ^{
Expand Down Expand Up @@ -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],
Expand All @@ -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;
}]];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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()
.url(createLaunchChromeDevtoolsCommandUrl())
.url(createLaunchJSDevtoolsCommandUrl())
.build();
mClient.newCall(request).enqueue(new Callback() {
@Override
Expand All @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
mDevServerHelper.launchChromeDevtools();
mDevServerHelper.launchJSDevtools();
} else {
mIsUsingJSProxy = false;
}
Expand Down Expand Up @@ -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
mDevServerHelper.launchChromeDevtools();
mDevServerHelper.launchJSDevtools();

JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() {
@Override
Expand Down
4 changes: 2 additions & 2 deletions ReactAndroid/src/main/res/devsupport/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>
Expand Down
5 changes: 4 additions & 1 deletion docs/Debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:

Expand Down
55 changes: 43 additions & 12 deletions local-cli/server/middleware/getDevToolsMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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();
Expand Down

0 comments on commit 4c8a9f0

Please sign in to comment.