notes 5 min builds
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 #
{
"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
sourcesContentis 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:
-
No map at all. No
sourceMappingURLcomment, nothing to fetch. - 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.
-
A comment pointing at a map that serves. Whether that matters depends entirely on
sourcesContent.
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.
// 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_modulesthat survived bundling. -
The shape of the team.
packages/admin-sharedin 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
sourcesthat 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