In my research for an integration of React with Rails, I have encountered a very annoying error on the server side when pre-rendering some React components. It was even more frustrating that I didn't know how to debug my script when it was executed on the server to find out what the problem was.
Running in debug mode
The solution was of course to execute this server-side script in debug mode, but how one is supposed to do that?
ExecJS isn't designed for or intended to be used as a debugging environment. - @josh, on a Github issue
ExecJS is meant to execute JS by providing a common ground for all those runtimes, and the authors/maintainers are not looking for implementing high-level features such as debugging support. So how do we get to hook into our script?
ExecJS provides a very simple way to interface with external runtimes (ie. executing a binary instead of linking with a library) like Node.js. All you have to do is to declare an ExecJS::ExternalRuntime object with a command name and ExecJS will be able to launch this runtime with the script file to execute. Here's how the default Node.js runtime is defined:
module ExecJS module Runtimes # ... Node = ExternalRuntime.new( name: "Node.js (V8)", command: ["nodejs", "node"], runner_path: ExecJS.root + "/support/node_runner.js", encoding: 'UTF-8' ) #... end end
Note the :runner_path option: ExecJS will use the script referenced here to run your own code within a wrapper that will make sure the output conforms to what ExecJS is expecting.
So here's our solution: create a new external runtime instance with the correct command-line arguments to start a debug session and have ExecJS use it.
The simplest way I have found to do this is by using Node.js: its --debug-brk option will start a debug session and stop at the very first instruction of a script, waiting for a debugger to connect. As a bonus, because ExecJS already offers support for Node.js, we don't have to create our own runner script!
Here's our new external runtime definition:
if ENV['EXECJS_DEBUG'] ExecJS.runtime = ExecJS::ExternalRuntime.new( name: 'Node.js Debug (V8)', command: ['node --debug-brk'], runner_path: ExecJS.root + '/support/node_runner.js', encoding: 'UTF-8' ) end
Put this code into an initializer file in your Rails project (eg. config/initializers/execjs_debug.rb) and you have it: ExecJS will execute your server-side scripts using Node.js in debug mode!
You will of course need to install Node.js first and to run your Rails server with the EXECJS_DEBUG environment variable:
EXECJS_DEBUG= rails s
Yup, you're going to need one!
Fortunately, there is a great tool available in the Node.js ecosystem that will make you feel at home if you're used to the Chrome Developer Tools: node-inspector.
Just install it:
npm install -g node-inspector
Then run it from your project root:
It will output an URL to open in your browser, that will load the developer tools. Note that you will need to use a Webkit-based browser (Chrome, Safari, Opera) to make it work.
Once it's ready, load the page of your website from which the script you want to debug will be run: the request will hang, waiting for the debugger to connect.
Reload node-inspector's page, and voilà!
The execjs20150612–1mcoe90js file that you can see opened in the tool is the one ExecJS has started. In the case of using Rails, React and Browserify, this script contains all my JS code and its dependencies.
It may not be the simplest to work with but everything's here and you'll be able to set breakpoints, inspect variables, etc.
Two final notes on this:
- When the execution flows exits the script and ExecJS checks its result, it will crash trying to interpret it as a JSON string because Node.js outputs the string "debugger listening on port 5858". I haven't found a way to prevent this output, so it would need some tinkering to avoid this error.
This is a somewhat minor issue though, as it will happen only when you have finished stepping inside your script and leave the debugger.
- The files listed in the left sidebar, at least in my case, won't be run except for the execjs one I mentioned. Do not set breakpoint in any other file, they won't be hit!
This could possibly be fixed using sourcemaps but I still have to figure out this point.