A source map is a claim about two files: that this position in the built output came from that position in a source file. I had never thought of it as a claim that could be false until a stack trace sent me to a line that had nothing to do with the crash.
The build had gained a step. Something small, a licence banner prepended at the end of the pipeline, after the bundler had already written the map. The bundler was right. The banner was right. The map was now a careful description of a file that no longer existed.
What made it expensive was that nothing anywhere said so. A broken map is still well formed JSON with all the right keys. It parses. It loads. The debugger opens it and shows you a file and a line, with exactly the same confidence it shows you a correct one. There is no error, no warning, no degraded mode. You just get an answer that is wrong, and you spend an afternoon doubting your own reading of the code.
So I went looking for a way to ask a map whether it was still true, and found that three things can be checked without knowing anything at all about the transform that produced it.
A mapping never points past the end of a line. If a map says position 1:26 and line 1 is eighteen characters long, it is describing text that is not there.
A mapping never lands strictly inside an identifier. Generators emit positions at token boundaries, because the two halves of a name are not separately addressable in either file.
And when a mapping carries a name, the original source at that position begins with that name. The map is asserting an identity. Either the identifier is there or the map is wrong, and there is no third answer.
The first thing I did was run those three checks over output I had no reason to doubt, because a checker that flags everything passes every soundness test ever written and is worth nothing. Across bundles from esbuild and files from tsc: 371 mappings, of which 26 carried a name, and not one violation. That number is the reason I trusted anything that came after it.
Then I took the same bundle and the same map, and prepended three lines.
$ npx sourcemap-truth --demo
dist/bundle.min.js 93 mappings, 2 sources
generated side 5 of 93 land on a token, 88 do not
original side 93 of 93 land on a token, 0 do not
names 26 of 26 are at the position the map gives
1:6 inside an identifier
1:22 past the end of that line
1:26 past the end of that line
The map is not true of this file. Something changed dist/bundle.min.js
after the map was written.
Eighty-eight of ninety-three. Nothing about the file on disk hints at that, and the debugger will not mention it.
The shape of the failure tells you where to look, which turned out to be the part I use most. In the run above the original side is perfect and the generated side collapsed, which means the output moved after the map was written. Shift the sources instead and it inverts: every position still checks out and all 26 names point at the wrong identifier. Those are different bugs in different steps, and you can tell which one you have before you start bisecting the pipeline.
There is one thing it deliberately does not do, and it took a wrong turn to get there. My first version counted a mapping that landed on whitespace as a fault. That felt reasonable until I ran it against tsc, which deliberately maps the space after a keyword: 14 times in 122 mappings on one small file. Correct output scored 108 of 122. A checker that calls the compiler wrong is not measuring the compiler, it is measuring itself, so whitespace targets are not faults and the tool says nothing about them.
The other thing it refuses to do is guess. A map whose sources carry no content cannot be shown true or untrue, and the report says unchecked rather than passing, with its own exit code. Keeping "this disagreed" apart from "nobody could tell" is the single decision I would keep if I had to throw the rest away. Collapsing them is how a checker ends up certifying everything it is handed.
It is one function if you want it in CI, and one command if you just want to know:
npx sourcemap-truth dist/index.js
Zero for a map that is true, one for a map that is not, two for one nobody could check.
The same question turns up in a lot of places once you start looking for it. It is roughly how I ended up writing schema-parity a while back, for whether a generated JSON Schema still accepts what the validator it came from accepts. Different artifacts, same shape of doubt: two things that are supposed to describe each other, and nothing that ever checks.
Top comments (0)