I’ve been playing lately with the technologies pointed out in the post title, and it took me a considerable amount of time to be able to debug it with ease. I went through several pages tackling these technologies together, but none of them was specific enough for me to get the setup working; hence I’ve decided to describe the steps I took, in case this is helpful to anyone fighting against technology.
Through this post I’ll describe the different pieces and introducing some snippets of code / configuration, but in case you prefer to get the whole picture you can clone the sample project that describes the setup.
I want to debug a Node.JS application that is:
exportstatements) instead of CommonJS
requirespec, so the code is written using a similar structure as web code.
transpiled using Babel: current versions of Node do not support natively ES6 modules (only through experimental flags), hence it’s required to transpile the source code to code that can be executed in Node (10.15) runtime.
executed as a Docker container: my usual preference for running applications.
My current IDE of choice is VS Code, so the expectation is to be able to define a break point in the IDE, connect the debugger to a running container and intercept any request going through the break point.
When using Babel, you need to compile the source code before executing your application. In the sample project
there are a couple of npm scripts to launch the application (or just run
make run in the root folder):
- build: executes babel to compile the source code.
- serve: launch the application using the compiled code generated by Babel (dist folder).
Babel functionality is based on composition, so the developer can choose which functionalities are required for the project and install the specific NPM packages (instead of integraging a huge framework). In the sample project this is the bare minimum list of Babel dependencies:
- @babel/core: basic functionality, which depends on the project configuration defined in the .babelrc file.
- @babel/cli: used to compile from command line instead of loading a library.
- @babel/preset-env: defines automatically the required plugins/polyfills based on the runtime defined.
Nevertheless, when you are under development you want your changes to be refreshed as soon as possible, either for running unit tests or for validating the changes manually. Here is where babel-node package appears: it works similar to Node.js command line, but has a pre-step that executes the Babel compilation. This simplifies a lot the hot-reloading process.
However, as a developer you want to define a breakpoint while debugging in the original source code, as it’s loaded in your IDE (VS Code in this case).
This is where a source map comes into the scene, as it maps the compiled code to the original one, creating a way to reconstruct the source code.
Babel can be configured to generate source maps both from the CLI (–source-maps flag) or in the configuration file (.babelrc). Keep in mind though that its support in config file is limited.
Upon launching the VS Code debugger, it will open a connection to the machine where the application is running. By default the port where the Node application listens is 9229, hence it’s important to map this port from the container to localhost so VS Code can connect to it.
Nodemon is a utility that monitors for any changes in the predefined paths (the source code path) and automatically restarts the application. It’s installed as a development dependency via NPM.
Nodemon will run inside the Docker container, so for Nodemon to detect changes in the source code it’s important to mount a volume in the container to map the source code from the host to the container.
VSCode provides several ways to debug a program. In this case, we’re interested in attaching to a process that is running in a remote host (a Docker container) through a local binding (port 9229, as described before). Upon adding a debug configuration, file .vscode/launch.json will be updated with the new entry. It’s important to configure the attributes that defines the local and remote source code root path, as well as the source maps location.
Defining properly the VS Code launch configuration was only of the hardest points; if you’re having issues check my configuration in the sample project.
The sample project includes any configuration required for the setup.
These are the big parts:
- babel: several modules installed via NPM (package.json). It’s configuration relies on .babelrc file.
- babel-node: it transpiles and executes the code. Any CLI argument provided to babel-node is forwarded to Node, so it’s important to include the flag –inspect to activate the inspector.
- nodemon: used in NPM script start for hot-reloading the application.
- docker: important to expose the debugging port (9229) and mount the source code folder from the host.
- Makefile: execute the target debug for running the application listening in the debug port and ready for hot-reloading.
- VS Code: Debug configuration in .vscode/launch.json file.
Once you have defined a breakpoint in your code in VS Code:
- launch the Docker container executing
- launch VS Code debugger.
- You should see a Debugger attached log in the Docker logs stdout logs:
- Launch a request that goes through your breakpoint.
- VS Code stops in your breakpoint.