However, problems arise when you try to debug the compiled code in the browser’s debugger. The code in the browser isn’t the code that you wrote, so it’s hard to understand what’s going on.
Source maps solve this problem. They describe how the compiled code is related to your original source code. The debugger can then show the original code you wrote, while actually running the compiled code.
This article will look at what source maps actually look like and what their limitations are.
What does a source map look like?
Let’s compile this ES 2015 code with Babel:
And this is the source map content inside “test.js.map”:
As you can see the file contains JSON data. Most of the values are straightfoward.
- Version: This is the source map standard revision that was used.
- File: Filename of the compiled code.
- Content: Content of the files specified in Sources.
However, the mapping data isn’t easy to make sense of. It uses a format called Base 64 VLQ.
Base 64 VLQ
VLQ stands for variable-length quantity, and it’s used to store a number in a more space-efficient way than storing its digits as a string.
The image below shows how to count in Base 64 VLQ.
mappings entries, like “AAAA” and “GAAG”, are called “segments”. They each represent an array of numbers. For example “AAAA” stands for
[0,0,0,0] and “GAAG” stands for
You can use the vlq package on NPM to encode and decode the segments.
Interpreting the mappings entries
Once we’ve got the array representation of a mapping it becomes a little easier to make sense of the source map. Each value in the array is an index that refers to a position either in the compiled source code or in the original source code.
- : Column index in the compiled file
- : What original source file the location in the compiled source maps to
- : Row index in the original source file (i.e. the line number)
- : Column index in the original source file
All positions are relative to the previous position, rather than absolute column indices.
So how does the debugger know what row/line an entry refers to in the compiled code? The mappings are separated by semi-colons, and each mapping describes one line.
You can see that the
mappings value in the example start with “;;”. That’s because
"use strict" and an empty line are added by Babel, even though these lines don’t relate to anything in the original file.
Interpreting the mappings value in our example
Let’s see if we can understand some more bits from the mappings value.
Why are the middle values always “A”?
You can notice that the two middle characters are always “A”, representing the value 0. This has a simple explanation.
We only have one original source file. The debugger always takes index 0 of the
Sources array above, which is “test.es6.js”.
The content of “test.es6.js” is
const square = (x) => x * x;. It’s only one line, so the line index in the original file is also always 0.
Why does it mean that the first segment is “AAAA”?
This means that the compiled source column and the column in the original source are both 0. In both files the assignment starts at the beginning of the line.
Why does IAAM mean?
“IAAM” represents the array
[4,0,0,6]. The column indices are relative to the column from the previous segment, but because we started at column 0 the absolute column indices are also 4 and 6.
This refers to the beginning of the function name in the compiled source and in the original source code.
Visualizing the source map
This source map visualization tool by Tobias Koppers can help you get a better understanding. See the image below for an example.
Limitations of source maps
There are at least two limitations that make working with source maps harder than working directly with the original files.
- Source maps map positions in the code, but they don’t map how two variable names relate to one another.
- Optimizations can make the compiled source code not match the behavior you’d expect from the code you wrote.
Mapping relationships between variable names
If the compiler you used made changes to variable names source maps allow the debugger to show you the original names in the code panel.
However the debugger won’t give you access to the original variable names in the console, watch expressions, or the list of scope variables. Here you need to use the variable name that’s was generated by the compiler.
Likewise, hovering over a variable to see its value won’t work.
Minifiers are a common type of compiler that will change variable names. The screenshot below shows the original Underscore.js source code, but the scopes pane is showing the minified single-letter variable names.
Another example of variable renaming is using the
let keyword with Babel. Babel has to rename it to get the desired scoping behavior:
If you look at the source map example above you might think the
names property would allow you to map the variable names to one another.
However, most tools don’t use it and it doesn’t actually solve this problem.
the intention was to both deobfuscate stack traces and provide a way for de-minimization of source code if the mapping was semi-direct.
[…It] was left in primarily because it have any cost associated with it. But yes, the intent was to provide to link to the original names used for a variable.
Compiled code behaving differently in the debugger than the original code
Source maps give you the illusion that you’re debugging the code you wrote. However, the truth is that the debugger is running different code and then trying to map its behavior back to your original source code.
Sometimes makes your code behave differently from what you would expect. For example, you might try to set a breakpoint on one line but Chrome sets the breakpoint a few lines further down.
Has this ever happened to you?
While it looks like we’re stepping through three separate variable assignments the Uglify minifier has actually merged them into one.
Chrome isn’t able to set a breakpoint in-between them anymore.
Something similar would happen if an optimizer removed an unnecessary line from your code. It’s impossible for Chrome to set a breakpoint on a line that doesn’t exist in the compiled code.
Learning more about source maps
There appears to be some interest in moving the source maps format forward and adding a way to map names to each other.