My current editor of choice for all things related to Javascript and Node is VS Code, which I highly recommend. The other day I needed to hunt down a bug in one of my tests written in ES6, which at time of writing is not fully supported in Node. Shortly after, I found myself down the rabbit hole of debugging in VS Code and realized this isn't as straightforward as I thought initially. This short post summarizes the steps I took to make debugging ES6 in VS Code frictionless.
My first approach was a launch configuration in launch.json
mimicking tape -r babel-register ./path/to/testfile.js
with babel configured to create inline sourcemaps in my package.json
. The debugging started but breakpoints and stepping through the code in VS Code were a complete mess. Apparently, ad-hoc transpilation via babel-require-hook and inline sourcemaps do not work in VS Code. The same result for
attaching (instead of launch) to babel-node --debug-brk ./path/to/testfile.js
with same babel sourcemap configuration as before.
Stepping back from my naive approach, I figured out that transpiling the code beforehand with sourcemaps (set to inline
and both
both work in package.json, true
doesn't), executing the transpiled entry point and then attaching the VS Code debugger worked:
node_modules/.bin/babel src --out-dir .compiled
node --debug-brk .compiled/path/to/testfile.js
The VS Code attach configuration in launch.json
:
{
"name": "Attach",
"type": "node",
"request": "attach",
"port": 5858,
"address": "localhost",
"restart": false,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.compiled",
"localRoot": "${workspaceRoot}",
"remoteRoot": null
}
While this worked, I wasn't satisfied since switching back and forth between VS Code and the command line didn't feel right.
Yes, we can. For that we have to compile the sources immediately before the debug session starts. The launch configuration provides preLaunchTask
as pre-debug-hook for this purpose. First, I've added a script in package.json
to compile the sources:
{
...
"scripts": {
...
"compile": "rm -rf .compiled && babel src --out-dir .compiled/src"
}
...
}
Unfortunately, we cannot call this script directly in our launch.json
since preLaunchTask
references tasks configured in tasks.json
under .vscode
. So let's create a tasks.json
file containing our compile task:
{
"version": "0.1.0",
"command": "npm",
"isShellCommand": true,
"args": ["run"],
"showOutput": "silent",
"tasks": [
{
"taskName": "compile",
"isBuildCommand": false,
"isTestCommand": false,
"showOutput": "silent",
"args": []
}
]
}
This just maps our npm script to a task. We could add other npm scripts here as well. Assumed we have a npm script which runs our test suite, we could configure a corresponding task and run our tests by pressing cmd+shift+T, if we set isTestCommand
to true
.
Finally, we have to configure the launch section in launch.json
accordingly:
{
"name": "debug",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/.compiled/path/to/file.js",
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
"preLaunchTask": "compile",
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development"
},
"externalConsole": false,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.compiled"
}
Now we can press F5 in VS Code and the file configured under program
will be debugged.
It still feels cumbersome to edit launch.json
each time we want to debug a different file. What about debugging the current opened file in VS Code?
To achieve this we somehow have to get the active file into the program
property in launch.json
. Since VS Code supports variable substitution in tasks.config
and launch.config
this is not too hard. So is ${file}
replaced with the current opened file -- exactly what we need here. The only problem is, that we have to execute the opened file's compiled counterpart.
One solution to overcome this obstacle is to introduce a wrapper script which gets executed for the debug session instead of the file to debug. This wrapper takes the current opened file as single argument, derives its compiled counterpart location and finally just requires the compiled file. The corresponding launch.json
section is shown here:
{
"name": "debug current file",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/runcompiled.js",
"stopOnEntry": false,
"args": ["${file}"],
"cwd": "${workspaceRoot}",
"preLaunchTask": "compile",
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development",
"NODE_PATH": "${workspaceRoot}/.compiled/src"
}
Note the changed properties program
and args
. The remaining task is to implement runcompiled.js
. A simple solution taking advantage of the fact that our src
folder is completely mirrored in .compiled
is shown below:
// runcompiled.js
// Takes an uncompiled .js file as argument and executes its pendant in .compiled folder.
// Assumes that source files in cwd are mirrored in folder .compiled.
var path = require('path');
var uncompiledFile = process.argv[2];
var compiledDir = path.resolve(process.cwd(), '.compiled');
if (!uncompiledFile) {
process.stderr.write('filename missing');
process.exit(1);
}
uncompiledFile = path.resolve(uncompiledFile);
if (uncompiledFile.indexOf(compiledDir) === 0) {
process.stderr.write(`file in ${compiledDir} not allowed`);
process.exit(1);
}
var relativePath = path.relative(process.cwd(), uncompiledFile);
var compiledFile = path.join(compiledDir, relativePath);
require(compiledFile);
With those ingredients in place debugging the current opened ES6 file in VS Code is only an F5 away. Enjoy!
I have shown some approaches to debugging ES6 in VS Code from attaching to a node process running the transpiled sources, over automatically transpilation in a pre-debug-hook, and, finally, improving this further to be able to debug the current opened ES6 file with just a keystroke.
Anyway, I still feel that I may have missed something. If that's the case I'm eager hearing about simpler solutions!
๐