You need to understand what sourcemaps are and why you should care about them. I'll go into more depth explaining how they work for those interested. Next, I'll explain the part that I found challenging: understanding how VSCode deals with sourcemaps. I found the available explanations I came across lacking in helping bridge the gap I needed to make sure a build process would support no only
F5 style debugging in VSCode, but also attaching to a running process on my laptop as well as attaching to the Node.js process running within a local Docker container.
The Skinny on Sourcemaps
Why should you care about these things called sourcemaps? If you're familiar with debugging symbols (the
First, for client-side applications, it's common to not only concatenate multiple script files into one to reduce the number of round trips to the server (which is slower than pulling big file). In this case, you need to have a way to map the line of code you want to inspect back to the line of code in a specific file.
Sourcemap Implementation Options
There are a few different ways sourcemaps are implemented; I'll focus on the two most common options: external files & inline.
One option is for the sourcemap to live in it's own file beside the generated file. This is where you commonly see a file named
index.js.map next to the generated
sourceMappingURL= reference. This points to the mapping file. It might be beside the generated file, or it might be located somewhere remote like in a CDN.
Inline in the Generated File's Footer
The other option (and the one I prefer) is to have the entire sourcemap located at the end of the file. Just like in the above description, there's a
sourceMappingURL= reference, but the value is set to
data:application/json;base64, followed by a Base64 encoded JSON structure describing the sourcemap. I like this option because it's less stuff to manage and you never get things out of sync. When deploying a client-side application to production I don't build sourcemaps so the filesize isn't a concern.
Sourcemaps are encoded as Base64 strings. Once you decode this string you can see it's actually a big JSON object that is pretty easy to read.
I like to use either the Encode Decode or vscode-base64 extensions for VSCode to quickly decode & encode strings, including Base64 strings like those used in sourcemaps when getting things setup. Just don't forget to encode the sourcemap after decoding it because otherwise, it won't work.
- version: The sourcemap schema version; this should be equal to
- file: The filename of the generated code; for instance if your source code is in
index.ts, the value of this field will be
index.jsin most cases.
- sources: An array of files that were used to create the generated file; there may be multiple in the case where you have concatenated multiple files into a single file (common in client-side apps).
- sourcesRoot: This field lets you prepend the sources field with a folder structure, such as a remote URL or folder.
mappings: This field contains a CSV string of Base64 VLQ values. In layman's terms, these are encoded values that map a line in the generated file to a line in the source file. This also depends on the order of files in the sources field... it's complex how they are built & really doesn't matter.
If you want to learn more about how this works, here are two good references:
sourcesContent: This field might be empty, but one thing you can do is include the entire source within the sourcemap. This is something that isn't required, but it's recommended as a good fallback. You'll see how this can help in a moment.
So now you know what sourcemaps are, how to view the structure and how the structure works. So how are they used?
How VSCode Uses Sourcemaps
So you know how sourcemaps work, but how does VSCode use them? There's nothing special, but when you're having trouble getting your project configured so you can debug it in VSCode, it helps to understand how it works to resolve the issues.
First, keep in mind that when you want to debug a Node.js application, you have to launch the node process in debug mode. This is done by adding
--debug argument to the node process. Node will then expose a port on the local machine, by default port 5858, that editors can attach to. Using this port, the editors can catch breakpoints, get values on watch expressions, inspect the call stack, etc.
When VSCode attaches to the Node.js process, it will use information published over port 5858 to figure out what the running code is and how it maps to other code using sourcemaps. There is a three-step fallback that VSCode uses to make this mapping work. The goal is to get the first one working, but when it isn't working, it can really frustrate you as things don't seem to be working as you'd expect. For instance, if it isn't setup correctly while debugging works, you'll notice that breakpoints set prior to the debugging session won't get hit... instead, they show up not with the expected red circle, but rather with a hollow gray circle:
Figure 01 - Missed Breakpoint in VSCode
server.js) and set a breakpoint. When Node.js hit that line, VSCode opened a new tab
server.ts and stopped where I set the breakpoint. But where's the red circle... and you think "wait, if it saw the TypeScript file, then why can't I set a breakpoint in it?" Ah... that's because it's not really your TypeScript file... it's the code being streamed back from the Node.js process that's in the sourcemap. You can tell by hovering over the filename:
Figure 02 - Inlined Sourcecode from Node.js in VSCode
While this isn't what you want, it explains why you can't set your own breakpoints in TypeScript. The other scenario is when VSCode basically throws up its arms and says "I can't find anything that maps to this code... so here's the generated stuff" which is what this picture shows:
Figure 03 - Executing Source from Node.js in VSCode
Armed with this understanding, you can figure out your build process and makes sure everything is setup correctly.
In a previous post, I explained how I like to structure my projects: Node.js, TypeScript & Building to Different Folders. In a nutshell, I like to have all my source in
/src and build it to
My build process is set so that it will always include the source TypeScript within the sourcemap and include sourcemaps inline. Let me show you what one of these tasks looks like... for some context, you can see the full source of the build task in this gist build.ts which references a barrel for all plugins I have in my project (gulpPlugins.ts).
My use of gulp involves creating tasks as TypeScript classes and having them dynamically loaded at runtime instead of one big
gulpfile.jslike most demos show. If you want to learn more about it, check out this post: Dynamically Loading Gulp Tasks For Simplified Reuse and Maintenance.
Within the Gulp task, I first create a TypeScript project by loading the
Next, configure how you're going to handle sourcemaps:
The trick here was getting the
sourceRoot property on the sourcemaps correct. Looking at the code I came up with, it amazes me that I spent so much time on what now is a single return statement... say it with me... "I love my job, I love my job" :)
Configuring VSCode for Debugging
I needed a single task mapped to my gulp build task so that when I hit
First things first, I need a task. Within my
tasks.json file within the
.vscode folder, that will run my gulp task build. I also pass the
--app argument on the command line as this task can build my project TypeScript (gulp tasks, etc) or the actual application code:
Next setup is to create three different launch configurations:
- Launch LOCAL Node.js: This will start the Node.js process in debug mode, after running the build task. Take note of the fields
sourceMaps. These were a bit tricky to get setup correctly, but they were required for mysetup.
- Attach to LOCAL Node.js: This will attach to a currently running Node.js process running on my laptop. It assumes the process was started in debug mode.
- Attach to DOCKER Node.js: This one is the same as above, but what it does is connect to a Node.js process running in Docker. It doesn't specifically connect to a Docker container, but just to port 5858. When I run the Docker container, it exposes port 5858. Take note of the
remoteRootfield. That's the absolute path to the location where the application is running within the Docker container (here's the Dockerfile for reference on how the container was created that this project uses).
To use one of these configurations, jump to the debug pane in VSCode and select the launch configuration desired. Pressing