Writing a custom debugger for ReactNative

ReactNative enables us to build mobile apps that have the elegance of a native use interface while taking advantage of a fast, web like development process. The creative use of Chrome devtools to debug the JavaScript code is definitely a big plus in the workflow of a developer. While I love Chrome for debugging, I still prefer to set breakpoints or watch variable right from within my editor. This way, I still benefit from editor features like syntax highlighting and autocomplete, support for my backend system and simply having lesser windows cluttering my desktop.
Over the past few weeks, I was experimenting with ways to add debugging to my editor, and this post is an explanation of how to add custom debuggers to ReactNative. Our team is planning to add debugging to a bunch of other features that we plan to release as an extension for VSCode.

ReactNative Debugger today

Before writing a custom debugger, it is useful to appreciate how the existing setup works. I found an article that has an excellent explanation, though it is for an older version. The biggest change from the article is the use of a web worker in order to provide an isolated sandbox for the running scripts.
I created an "old-style" UML sequence diagram, hoping to capture most of the concepts without going too deep into the details.

The full SVG file may be easier to read.  Most of the messages have a direct correspondence to methods in the source code.

Path to a Custom Debugger

When trying to implement a custom debugger, I considered the following approaches
  1. Attaching a Javascript debugger directly to the Javascript VM packaged with the app on the device. This is probably the most accurate debugger since you are debugging the code running in its real environment. I believe that the NativeScript debugger uses this approach, but it was a little hard to implement.
  2. Create a parallel JSDebuggerWebSocketClient class to send messages to a process that I write, instead of sending it to the packager. While my process would have all the necessary debug hooks, I would still need to get source files and source maps from the packager.
  3. Simply attach a debugger to the running Chrome process. This seemed like the simplest case, but I was not a fan of having Chrome open and using it to just execute Javascript.
I finally settled on an variation of the third approach where instead of opening Chrome, I open a headless Node process and attach a debugger to that. Instead of launching Chrome, my node process would simply need to open a web socket connection to the packager, and the debug process would now be redirected to the new Node process. Most editors already have excellent support for debugging Node.

Refining the debugger

Since the packager now proxies to the Node process instead of Chrome, some improvements are needed in the Node process
  • In case of the Chrome debugger, ReactNative modules are loaded using the webworker construct - "importScripts". A Node process does not have a simple way to load scripts from a web server. Thus, we had to implement a way to download the code, and "require" it using runInNewContext. The sandboxed context also allows code isolation that the Webworker provides.
  • Sourcemaps also have to be downloaded and changed so that they point to source files on the local system. 
  • For websockets capability in the node process, we could use the websocket npm module that provides an excellent, w3c compliant interface which could be used as a drop in replacement.
  • Instead of requiring the user to shake the phone to enter into the debug mode, we could run adb shell am broadcast -a "com.rnapp.RELOAD_APP_ACTION" --ez jsproxy=true to enable proxy mode on the app. 
However, we still suffer from one problem. ReactNative hardcodes the fact that Chrome needs to be launched when debugging starts. If Chrome connects to the packager's websocket fast enough, our Node process will not work. 
Here is a pull request that looks at an environment variable and then launches a custom process, instead of defaulting to Chrome. This is similar to the way custom editors can be launched from ReactNative. I hope that the pull request is merged soon, so that custom debuggers can be added. 

The final product

Putting all of this together, a demo video of the capabilities is up on youtube. We plan to release it as a part of VSCode+ReactNative extension. In addition to debugging, you would also have support for Javascript and JSX syntax highlighting, autocomple, and ways to call ReactNative commands from within VSCode.
You can also signup for a preview. If you have additional feature requests or ideas that you think we should implement, please ping me and our team would love to talk to you.