Skip to content
Branch: master
Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
..
Failed to load latest commit information.
Chrome.png
Firefox.png
README.md
helloworld.coffee
helloworld.js
helloworld.js.map
index.html

README.md

Using vlq.js with source maps

This library doesn't include any special magic for dealing with source maps, just the low-level encoding/decoding. But it's actually fairly straightforward to convert an incomprehensible-looking string like this...

AAAA;AAAA,EAAA,OAAO,CAAC,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA

...into an array of mappings. Suppose we had some CoffeeScript code:

helloworld.coffee

console.log 'hello world'

It would get transpiled into this:

helloworld.js

(function() {
  console.log('hello world');

}).call(this);

And CoffeeScript would (if you asked it to) generate a sourcemap like this:

helloworld.js.map

{
  "version": 3,
  "file": "helloworld.js",
  "sources": [
    "helloworld.coffee"
  ],
  "names": [],
  "mappings": "AAAA;AAAA,EAAA,OAAO,CAAC,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA"
}

(A source map simply a JSON object that adheres to a particular specification, which you can find here.)

Each line in the generated JavaScript (helloworld.js) is represented as a series of VLQ-encoded segments, separated by the , character. The lines themselves are separated by ; characters. So we could represent the mapping like so:

mappings = 'AAAA;AAAA,EAAA,OAAO,CAAC,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA';
vlqs = mappings.split( ';' ).map( function ( line ) {
	return line.split( ',' );
});

[
	// line 0 of helloworld.js (everything is zero-based)
	[ 'AAAA' ],

	// line 1
	[ 'AAAA', 'EAAA', 'OAAO', 'CAAC', 'GAAR', 'CAAY', 'aAAZ', 'CAAA', 'CAAA' ],

	// line 2
	[ 'AAAA' ]
]

Using vlq.js to decode each segment, we can convert that into the following:

decoded = vlqs.map( function ( line ) {
	return line.map( vlq.decode );
});

[
  // line 0
  [ [ 0, 0, 0, 0 ] ],

  // line 1
  [
    [ 0, 0, 0, 0 ],
    [ 2, 0, 0, 0 ],
    [ 7, 0, 0, 7 ],
    [ 1, 0, 0, 1 ],
    [ 3, 0, 0, -8 ],
    [ 1, 0, 0, 12 ],
    [ 13, 0, 0, -12 ],
    [ 1, 0, 0, 0 ],
    [ 1, 0, 0, 0 ]
  ],

  // line 2
  [ [ 0, 0, 0, 0 ] ]
]

Each segment has 4 fields in this case, though the spec allows segments to have either 1, 4 or 5 fields (in other words, 2, 3, 4 and 5 below are optional - in our CoffeeScript example, the fifth field is never used). They are:

  1. The zero-based starting column of the current line. If this is the first segment of the line, it's absolute, otherwise it's relative to the same field in the previous segment.
  2. The zero-based index of the original source file, as listed in the source map object's sources array (since the generated code may be the result of combining several files), relative to the previous value.
  3. The zero-based starting line in the original source code that this segment corresponds to, relative to the previous value.
  4. The zero-based starting column in the original source code that this segment corresponds to, relative to the previous value.
  5. The zero-based index of the name, as listed in the source map object's names array, that this mapping corresponds to, relative to the previous value. (This isn't used here because no names are changed, but it's useful when minifying JavaScript, since myVeryLongVarName will get changed to a or similar.)

We can now decode our mappings a bit further:

var sourceFileIndex = 0,   // second field
    sourceCodeLine = 0,    // third field
    sourceCodeColumn = 0,  // fourth field
    nameIndex = 0;         // fifth field

decoded = decoded.map( function ( line ) {
  var generatedCodeColumn = 0; // first field - reset each time

  return line.map( function ( segment ) {
    var result;

    generatedCodeColumn += segment[0];

    result = [ generatedCodeColumn ];

    if ( segment.length === 1 ) {
      // only one field!
      return result;
    }

    sourceFileIndex  += segment[1];
    sourceCodeLine   += segment[2];
    sourceCodeColumn += segment[3];

    result.push( sourceFileIndex, sourceCodeLine, sourceCodeColumn );

    if ( segment.length === 5 ) {
      nameIndex += segment[4];
      result.push( nameIndex );
    }

    return result;
  });
});

[
  // line 0
  [ [ 0, 0, 0, 0 ] ],

  // line 1
  [
    [ 0, 0, 0, 0 ],
    [ 2, 0, 0, 0 ],
    [ 9, 0, 0, 7 ],
    [ 10, 0, 0, 8 ],
    [ 13, 0, 0, 0 ],
    [ 14, 0, 0, 12 ],
    [ 27, 0, 0, 0 ],
    [ 28, 0, 0, 0 ],
    [ 29, 0, 0, 0 ]
  ],

  // line 2
  [ [ 0, 0, 0, 0 ] ]
]

The first and third lines don't really contain any interesting information. But the second line does. Let's take the first three segments:

// line 1 (the second line - still zero-based, remember)
[
  // Column 0 of line 1 corresponds to source file 0, line 0, column 0
  [ 0, 0, 0, 0 ],

  // Column 2 of line 1 also corresponds to 0, 0, 0! In other words, the
  // two spaces before `console` in helloworld.js don't correspond to
  // anything in helloworld.coffee
  [ 2, 0, 0, 0 ],

  // Column 9 of line 1 corresponds to 0, 0, 7. Taken together with the
  // previous segment, this means that columns 2-9 of line 1 in the
  // generated helloworld.js file correspond to columns 0-7 of line 0
  // in the original helloworld.coffee
  [ 9, 0, 0, 7 ],

  ...
]

It's through this fairly convoluted process that your browser (assuming it's one of the good ones) is able to read helloworld.js and an accompanying source map (typically helloworld.js.map) and do this:

Chrome

Firefox

You can try this for yourself by cloning this repo and opening the sourcemaps/index.html file.

You can’t perform that action at this time.