Skip to content

Repository files navigation

sourcemap-fidelity

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.

The problem

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.

Install

npm install --save-dev sourcemap-fidelity

Node >= 20.6. No runtime dependencies. No config file — one file in, one report out.

Use

npx sourcemap-fidelity dist/app.js

Real 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 1

Programmatically:

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}%`);
}

What it checks

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.

Options

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.

What it does not do

  • It does not catch a mapping that points to the wrong identifier of the same kind. If a mapping should point at total but instead points at a different, adjacent count — 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. 2000 evenly-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 --all for 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 sourceMappingURL or sources entry that's a URL rather than a local path is out of scope.

Tested against

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 inlined sourcesContent; the original .ts is read from disk instead).
  • esbuild-bundle/ — esbuild --bundle --sourcemap --sources-content=true over the tsc output, chained straight back to the original .ts files.
  • esbuild-bundle-min/ — the same bundle with --minify, which is what produced the synthetic names[] entries discussed above.
  • esbuild-chained/ — esbuild bundling the tsc output with its sourceMappingURL comments stripped first, so the resulting map's sources point at the intermediate .js files instead of the original .ts — a real, reproducible instance of the "chained build" case.
  • broken-truncated-and-shifted/ — the esbuild-bundle map above, with one source's sourcesContent truncated 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.

Develop

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 22

License

MIT

About

Check that source map mappings actually point where they claim — token-level verification, not just valid JSON.

Topics

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages