---
title: 'Source Maps'
source: 'https://academia.sh/en/courses/javascript-ecosystem/source-maps'
course: 'Modules, Tooling and the Ecosystem'
language: en
updated: '2026-08-17T18:09:45+00:00'
license: 'CC BY-SA 4.0'
---

# Source Maps

Mapping from a generated position to a source position, the map file's fields, how the mapping string is encoded, hand-building a map, and the decision to publish it in production.

The previous lesson stopped at the point where transpilation separates source from
output. The output's lines may have shifted, its constructs may have changed. When an
error occurs, the line number in the stack trace belongs to the generated file and does
not match the source the developer has open.

A **source map** is a data file that carries the position mapping between these two
files. It is not code and is not executed; it holds only the information that "this
line and column of the generated file correspond to this line and column of the source
file."

## What the Mapping Covers

The mapping works at the level of **position**, not file. Given a generated position,
the corresponding source file, line, and column are found. This level of detail is
necessary: a single generated line can come from more than one source line.

Three operations use the mapping: resolving stack traces back to the source, placing
breakpoints in the debugger at the source file, and writing performance-measurement
durations back to source functions. All three let the tools introduced in the
debugging and profiling lessons of the Asynchronous JavaScript course work against
generated code.

## The Fields of the Map File

The map is an object in JSON form:

| Field | Content |
|---|---|
| `version` | The format's version number |
| `file` | The name of the generated file the map belongs to |
| `sources` | The paths of the source files |
| `sourcesContent` | The content of the source files; optional |
| `names` | The original names of identifiers that were renamed |
| `mappings` | The encoded form of the position mappings |

The `sourcesContent` field lets the source be embedded inside the map file itself. If
it is embedded, a tool does not have to look for the source elsewhere; if it is not,
the tool tries to read from the `sources` paths and cannot display the source if it
fails to find it.

The generated file is linked to its map by a comment on its last line. This comment
tells the runtime and debugging tools where to look for the map.

## The Shape of the Mapping String

The `mappings` field carries every mapping in a single string. The structure has three
levels:

- A semicolon separates **generated lines**. Two semicolons in a row mean that line has
  no mapping at all.
- A comma separates the **segments** on the same line.
- Each segment consists of four or five numbers: generated column, source file index,
  source line, source column, and a name index if one applies.

The numbers are not written directly. Two compressions apply: each number is kept as a
**delta** relative to the previous one, and the deltas are written with **variable-length
quantity** encoding. The encoding splits the number into five-bit groups, puts a sign
bit in the lowest bit, and marks continuing groups with the sixth bit; each group is
written as one letter from a 64-character alphabet.

The result is a string that is small but not human-readable. A thousand-line file's
mapping, written out without compression, would run to hundreds of thousands of
characters; this encoding brings it down to a tenth of that.

## Producing the Map

The shortest way to understand the encoding is to write a working generator. The
transform below is deliberately plain: it adds a three-line banner to the top of the
source. By the end of the transform, every source line has shifted down by three lines,
and that shift cannot be tracked without a map.

```javascript
// file: source/measure.mjs
export function measure(text) {
  if (typeof text !== 'string') {
    throw new TypeError('expected a string');
  }
  return text.split(/\s+/).length;
}

console.log(measure('one two three'));
console.log(measure(42));
```

```javascript
// file: generate.mjs
import { mkdir, readFile, writeFile } from 'node:fs/promises';

const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

// Variable-length quantity encoding: sign bit in the lowest bit, 5-bit groups.
function vlq(number) {
  let value = number < 0 ? (-number << 1) | 1 : number << 1;
  let output = '';
  do {
    let chunk = value & 0b11111;
    value >>>= 5;
    if (value > 0) chunk |= 0b100000;
    output += ALPHABET[chunk];
  } while (value > 0);
  return output;
}

await mkdir('output', { recursive: true });

const sourceName = 'source/measure.mjs';
const source = await readFile(sourceName, 'utf8');
const lines = source.split('\n');

const banner = ['/* generated file */', '/* do not edit by hand */', ''];
const output = [...banner, ...lines];

// Banner lines have no mapping; every generated line looks at its source counterpart.
const segments = banner.map(() => '');
let previousSourceLine = 0;
for (let i = 0; i < lines.length; i += 1) {
  const indent = lines[i].length - lines[i].trimStart().length;
  segments.push([vlq(indent), vlq(0), vlq(i - previousSourceLine), vlq(indent)].join(''));
  previousSourceLine = i;
}

const map = {
  version: 3,
  file: 'measure.mjs',
  sources: ['../source/measure.mjs'],
  sourcesContent: [source],
  names: [],
  mappings: segments.join(';'),
};

await writeFile('output/measure.mjs', output.join('\n') + '\n//# sourceMappingURL=measure.mjs.map\n');
await writeFile('output/measure.mjs.map', JSON.stringify(map));
console.log('mappings:', map.mappings.slice(0, 60));
```

```
$ node generate.mjs
mappings: ;;;AAAA;AACA;EACE;IACI;EACE;EACE;AACA;AACA;AACA;AACA;AACA
```

The three semicolons at the start of the string say the banner lines have no mapping.
The fourth line's segment is `AAAA` — four zeros: generated column 0, source file 0,
source line delta 0, source column 0. The fifth line's segment reads `AACA`: the third
field is no longer zero, a delta showing the source line advanced by one. On indented
lines the first and last fields also move off zero; the segment `EACE` means generated
column 2 and source column 2.

The generated file's first and last lines show how the link is established:

```
$ head -5 output/measure.mjs
/* generated file */
/* do not edit by hand */

// file: source/measure.mjs
export function measure(text) {
$ tail -2 output/measure.mjs

//# sourceMappingURL=measure.mjs.map
```

## Tracing Back

When the generated file is run, the error reports the generated position:

```
$ node output/measure.mjs 2>&1 | grep 'at measure' | sed "s|file://||; s|$(pwd -P)/||"
    at measure (output/measure.mjs:7:11)
```

The same file, run with the runtime's source-map support turned on:

```
$ node --enable-source-maps output/measure.mjs 2>&1 | grep 'at measure' | sed "s|$(pwd -P)/||"
    at measure (source/measure.mjs:4:7)
```

The stack trace now shows the source file and the source line. The line number is
exact; the column number is approximate, because this generator wrote one segment per
line. Real tools write a segment for every token, and the column becomes exact too. The
`sed` calls only shorten the absolute path in the output.

## Chaining and Integrity

A source can go through more than one transform: transpiled first, then bundled, then
minified. Each step produces its own map, and the last one looks back at the one
before it. The tools have to merge this chain into a single map; a step that does not
merge makes the whole chain unusable.

This explains the following observation: a stack trace sometimes lands not on the
source but on an intermediate output. The reason is usually that the map was not
carried through one link of the chain.

## The Map in Production

If the map is published, the entire source is effectively published along with it —
especially if the `sourcesContent` field is filled in. The decision is between three
options.

**Do not publish.** The map is produced but not published alongside the output. The
source stays protected; in exchange, stack traces coming from production have to be
resolved by hand.

**Publish.** The map sits next to the output. Debugging is at its easiest; the source
code is open to everyone.

**Publish somewhere separate.** The map is uploaded to an error-tracking system; it is
not present next to the output. Stack traces are resolved inside that system, the
source is not exposed. This needs extra infrastructure.

The choice depends on how private the source is. If the source is already open, there
is no reason not to publish the map. If it is closed, the third option strikes the
balance between the other two. Whichever option is used, producing the map is not
optional: a map that was never produced cannot be obtained afterward.

## Summary

- A source map is a data file that maps positions in the generated file to positions
  in the source file; it contains no code and is not executed.
- Mappings are held in a single string, separated into lines and segments, with each
  number kept as a delta and compressed with variable-length quantity encoding.
- The generated file is linked to its map by a comment on its last line; the source
  text can be embedded in the map.
- Once the runtime's source-map support is turned on, stack traces show source
  positions; column precision depends on how often segments are written.
- In a multi-step transform, each step has to merge its map with the one before it; a
  step that does not merge breaks the chain.
- Publishing the map means publishing the source too; uploading to a separate
  error-tracking system balances that trade-off.

## Next Step

The tools covered so far transformed code. Another class of tool does not transform
code at all — it only reads it and judges it against a rule set: which constructs are
a sign of a defect, which formatting is inconsistent. The next lesson covers this
check performed without running the code, and why formatting is kept separate from it.
