Skip to content
install

notes 5 min builds

note

What a source map says about the build that shipped it

A source map is not a leak by itself. It is a decision, and on most sites nobody made it - the bundler defaulted, the flag was never read, and the original source went out with the minified file.

Minified JavaScript is unreadable by design. A source map is the index that reverses it: a JSON file mapping every position in the shipped bundle back to a position in a file that was never shipped - and, optionally, carrying the contents of those files inside itself.

That optional part is the whole subject. A map without it is a set of names and offsets. A map with it is your repository, served as a static asset.

The anatomy, and the field that matters #

a source map, with the interesting fields kept
{
  "version": 3,
  "file": "main.a1b2c3.js",
  "sources": [
    "../src/lib/billing/stripeWebhook.ts",
    "../src/lib/internal/featureFlags.ts",
    "../../packages/admin-shared/src/roles.ts"
  ],
  "sourcesContent": [ "import Stripe from …",],
  "names": ["chargeCustomer", "isStaffOverride"],
  "mappings": "AAAA,SAASA,…"
}
sources
The original paths, relative. Names your folder structure, your internal package names, and often the shape of a monorepo you have never published.
sourcesContent
The files themselves. Comments, TODOs, commented-out code, the variable names you picked. This is the field that turns a map into a publication.
names
Original identifiers. Enough to reconstruct intent even when sourcesContent is absent.
mappings
The offsets. Useless alone, and the only part strictly required to symbolicate a stack trace.

A map is not automatically a vulnerability. It exposes no secret a bundle did not already contain - an API key in client code was public the moment it was bundled. What it exposes is structure and intent, which is a disclosure decision rather than a security bug, and it deserves to be made rather than defaulted.

Checking what a site actually publishes #

Three states, and they are commonly confused with each other:

  1. No map at all. No sourceMappingURL comment, nothing to fetch.
  2. A comment pointing at a map that 404s. Generated, then not deployed - or deployed and later removed. Common, and the one people mistake for the first.
  3. A comment pointing at a map that serves. Whether that matters depends entirely on sourcesContent.
which of the three, for every script on a page
for (const s of document.scripts) {
  if (!s.src) continue;
  const body = await (await fetch(s.src)).text();
  const ref = body.match(/sourceMappingURL=(\S+)/)?.[1];
  if (!ref) continue;

  const map = new URL(ref, s.src).href;
  const res = await fetch(map);
  console.log(s.src, res.status, res.ok
    ? ((await res.json()).sourcesContent ? 'full source' : 'names only')
    : 'referenced, not served');
}

Worth running on your own build after a config change. A bundler upgrade is the usual way this flips, in either direction, and nothing in the deploy output mentions it.

The three configurations that are defensible #

Ship nothing
No map, no comment. Stack traces from production are unreadable and you accept that. Honest, and increasingly rare because error monitoring made the alternative cheap.
Build it, upload it, delete it
The one most teams want. The map is generated, sent to the error tracker at deploy time, then removed from the output directory so it is never served. Traces symbolicate; nothing is public.
Ship it deliberately
A defensible choice for an open codebase, a developer tool, or a team that considers its frontend source uninteresting. The point is that somebody decided.
the middle option, which is the one to copy
// vite.config.ts
export default { build: { sourcemap: true } };

// deploy step, in this order
// 1. build
// 2. upload dist/**/*.map to the error tracker
// 3. rm dist/**/*.map

// Deleting before uploading is the mistake to avoid,
// and it fails silently: traces stay minified and
// nothing in the pipeline reports a problem.

If you take the third option, take it completely. A half-published map - sources listed, contents stripped - gives an attacker your architecture and gives your own team nothing. It is the only configuration with no upside.

What it tells you about somebody else's build #

Read from the outside, a map is one of the highest-signal artefacts a site publishes - not because of the source, but because of the sources array:

  • The framework and its version, from the paths inside node_modules that survived bundling.
  • The shape of the team. packages/admin-shared in a public bundle says there is an admin surface and that it shares code with this one.
  • What was tree-shaken and what was not. A dependency in sources that no feature on the page uses is dead weight somebody is still shipping.
  • Whether the build is what the site says it is. A page advertising one framework, mapping back to files from another, is a migration nobody finished.

None of that needs the source contents, and all of it survives sourcesContent being stripped. Which is the argument for the middle option one more time: the map you did not serve tells nobody anything.

Written by Ján Turský

Building LoupeKit and other browser tools out of Bratislava, under Apptiary.

tools in this note