Checks that a source map's mappings actually point where they claim to — not just that the JSON is structurally valid.
$ node examples/broken-build-demo.ts
A correct build — sourcesContent matches what is on disk:
fidelity score: 100% ok: true
The same map, but sourcesContent still holds the *original* wording —
the map was never regenerated after the on-disk file was edited.
It is still perfectly valid JSON, so a structural check sees nothing wrong:
fidelity score: 100% ok: false
stale sourcesContent: embedded sourcesContent (4 lines) differs from the file on disk (4 lines), first at line 2
The same map, but the "return" mapping was shifted one original line down
(a stray inserted line during a hand patch, or a bad merge):
fidelity score: 50% ok: false
[original-out-of-bounds] generated 2:3 -> greet.js:4:3
mapping points past the end of greet.js
That's node examples/broken-build-demo.ts, real output. Both broken maps are
100% valid JSON with the right shape — every field a structural validator
checks is present and well-typed. Neither defect is visible without decoding
mappings and looking at what's actually at each end of it.
sourcemap-validator gets
404,322 weekly downloads (npm downloads API,
week of 2026-09-15) and was last published on 2019-09-10 (npm registry) —
six years of ecosystem drift with no update. It checks that a map is valid
JSON, that version is 3, that sources/names/mappings have the right
shapes, and that mappings decodes without throwing. It never decodes a
mapping and asks whether the position it names is right.
sorcery (435,590 weekly downloads,
last published 2024-06-12) flattens a chain of maps into one, which is a
different job — it doesn't verify accuracy either, at any link in the chain.
That gap matters because of what a source map is for: turning a production
stack trace's minified app.min.js:1:48221 back into the line a human wrote.
A structurally valid map with a wrong mapping does that silently — it points
confidently at the wrong line, or at a blank line, or at a } three
functions away — and nothing about the file tells you. You find out mid
incident, staring at a stack trace that makes no sense, with no way to tell
whether the bug is in your code or in the map.
Sourcemaps break silently in a few specific, common ways:
- A stale inlined
sourcesContent. The source changed after the last build; the map wasn't regenerated. The map is still valid JSON pointing at real line/column numbers — they're just numbers into a version of the file that no longer exists. - A shifted mapping. A hand patch, a bad merge, or a bug in a build step nudges a line count by one somewhere in the pipeline. Most mappings still land on real code; a few land in whitespace, in a comment, or past the end of the file.
- A broken chain. A TS → bundler → minifier pipeline composes three maps into one. If any link in that chain drops or misaligns its input map, the final map points at the bundler's intermediate output instead of the original source — still "a real file," just not the one anyone can read.
None of these change the map's shape. They only change whether the numbers in it are true, which is exactly what this tool checks.
npm install --save-dev sourcemap-fidelityNode >= 20.6. No runtime dependencies. No config file — one file in, one report out.
npx sourcemap-fidelity dist/app.jsReal output, run against this package's own esbuild-built test fixture
(node dist/cli.js test/fixtures/esbuild-bundle/app.js):
test/fixtures/esbuild-bundle/app.js
map: .../test/fixtures/esbuild-bundle/app.js.map
fidelity score: 100% (115/115 sampled mappings OK)
total mappings in map: 115
OK
On a real bundle, total mappings easily reaches the tens of thousands;
sample (default 2000, evenly spaced) keeps the check fast without reading
every one — see Options.
In CI:
npx sourcemap-fidelity dist/app.js --json > sourcemap-report.json
npx sourcemap-fidelity dist/app.js || exit 1Programmatically:
import { analyze } from 'sourcemap-fidelity';
const report = analyze('dist/app.js', { sample: 5000 });
if (!report.ok) {
console.error(`${report.badMappings.length} bad mapping(s), score ${report.score}%`);
}Mapping accuracy. For a sample of mappings, it extracts the token at the generated position and the token at the claimed original position and asks one narrow question: is there plausibly real code at the original end of this mapping, given that there's real code at the generated end?
It deliberately does not compare the two tokens' text for equality.
Minifiers rename everything — function handleClick becoming function a is
the normal case, not a defect, and only names[] (see below) claims to
record the pre-rename identity. Comparing text would make this tool cry wolf
on every correctly-minified file that exists. Instead it compares categories
(identifier, number, string, regex, punctuation, comment, whitespace,
past-end-of-file) and flags exactly these as defects:
| Generated side | Original side | Verdict |
|---|---|---|
| real code (any kind) | real code (any kind, even a different kind) | OK — could be a legitimate rewrite |
| real code | whitespace, with no real token within ~64 characters on the same line | defect: points into dead space |
| real code | inside a comment | defect: points into a comment |
| real code | past the end of the file or line | defect: out of bounds |
| anything | the position doesn't exist in the generated file either | defect: map doesn't match this build |
The whitespace check has a deliberate, bounded tolerance: real bundlers (esbuild, in every fixture this package tested against) routinely map a statement to a column that lands a few characters of leading indentation before the token it names — "the start of this statement, trivia included" is a real, common convention, not a bug. A mapping is only flagged once nothing resembling code turns up nearby.
names[], honestly. The spec's third field on a mapping names the
original identifier a renamed one came from. It looks tempting to verify
names[i] against the text at the original position — but real bundlers
routinely emit synthetic names with no original-source counterpart
(esbuild's init_*, __esmMin, and similar internal helper names showed up
in every minified fixture this package tested against). Treating a mismatch
there as a defect produces constant false positives against correct output.
So names[] agreement is reported as a plain count — names: { total, matched } — informational only, and never affects the score.
sourcesContent freshness. Where a map embeds sourcesContent and the
referenced source file exists on disk, the two are compared (line-ending
normalized). A mismatch is reported as a stale/rebuild-needed defect. Where
only one of the two exists, there's nothing to compare, so nothing is flagged
— that source is instead counted separately as unverifiable.
ignoreList well-formedness. Checks ignoreList (falling back to the
legacy x_google_ignoreList) is an array of in-range, non-duplicate integer
indices into sources, and that the two fields agree when both are present.
Chained builds. Flags a source as likely itself a build artifact when
either (a) its content contains its own //# sourceMappingURL= comment, or
(b) its path sits under a directory segment that conventionally names build
output (dist, build, .next, and similar). This is reported for
awareness — a chained source means the map may be one hop short of the file
anyone actually wrote — and never fails the run by default (--strict opts
in to failing on it), because the mappings into that intermediate file can
still be perfectly accurate on their own.
analyze() option |
CLI flag | Default | What it does |
|---|---|---|---|
sample |
--sample <n> / --all |
2000 |
Max mappings checked, evenly spaced across the file. Infinity (--all) checks every one. |
checkSourcesOnDisk |
--no-source-check |
true |
Compare sourcesContent against the file on disk. |
CLI-only flags: --json (machine-readable report), --fail-under <n> (score
threshold, default 100), --strict (also fail on a detected chained
build), -h/--help.
Exit codes: 0 clean, 1 a defect was found or the score is below
--fail-under, 2 usage error or the file/map couldn't be read.
- It does not catch a mapping that points to the wrong identifier of the
same kind. If a mapping should point at
totalbut instead points at a different, adjacentcount— both real identifiers, both plausible — this tool has no way to tell, short of a full parse and semantic diff against the pre-build source. That would be a different, much heavier tool. - It is not a parser. Token classification is a single-pass lexical scan:
real strings, templates, line/block comments, and a standard (imperfect)
regex-vs-division heuristic. It does not resolve
${...}interpolation as further code, and non-ASCII identifiers aren't recognized as identifiers. - It samples by default.
2000evenly-spaced mappings is fast on multi-megabyte bundles and catches a defect anywhere with a proportional chance of hitting it; it is not a guarantee every mapping was checked. Use--allfor that. - It does not resolve a chain beyond one hop. A chained-build source is flagged, not recursively re-verified against whatever produced it.
- It does not fetch remote sources. A
sourceMappingURLorsourcesentry that's a URL rather than a local path is out of scope.
Hand-written fixtures can only prove this tool agrees with its own
assumptions about the format. test/fixtures/ instead holds the real,
unmodified output of compiling three small TypeScript files
(test/fixtures/src/) with actual tools, captured once and committed:
tsc-single-hop/—tsc --sourceMap(no inlinedsourcesContent; the original.tsis read from disk instead).esbuild-bundle/—esbuild --bundle --sourcemap --sources-content=trueover thetscoutput, chained straight back to the original.tsfiles.esbuild-bundle-min/— the same bundle with--minify, which is what produced the syntheticnames[]entries discussed above.esbuild-chained/—esbuildbundling thetscoutput with itssourceMappingURLcomments stripped first, so the resulting map'ssourcespoint at the intermediate.jsfiles instead of the original.ts— a real, reproducible instance of the "chained build" case.broken-truncated-and-shifted/— theesbuild-bundlemap above, with one source'ssourcesContenttruncated and another's shifted by one blank line, to confirm both known-failure modes are actually caught.
The test suite (npm test) runs analyze() against all five and asserts on
the real scores and defects shown above — see test/analyze.test.ts.
npm install
npx tsc --noEmit # typecheck
node --test "test/*.test.ts" # full suite, needs node 24+ for type stripping
node examples/broken-build-demo.ts
npm run build && npm run test:dist # what CI runs against node 20 and 22MIT