DIY source maps

May 8th, 2014. Tagged: JavaScript

In today's world of always having some sort of code transformation before your JS/CSS/HTML reaches the user, e.g. minification, concatenation, es6-to-es3 transpilation, it's nice to be able to go back to the source before the transformation. And when that happens in the comfort and the immediacy of the browser's dev tools, even better!

Enter source maps. (Intro, another). As the name suggests it's a file that describes the mapping between the "before" and "after" a transform. Source maps work just fine today in Firefox and Chrome and are supported by many tools.

In this post I want to demonstrate how to roll your own using the simplest of transformations - concatenating several files into one (in order to reduce HTTP requests).

In

Say you have two JS "modules" neatly seating in two files: src/dom.js and src/event.js

src/dom.js

var dom = {
  $: function(what) {
    return document.getElementById(what);
  },
  setContent: function(el, content) {
    this.$(el).textContent = content;
  }
};

src/event.js

var event = {
  addListener: function(el, event, fn) {
    el.addEventListener(event, fn);
  }
};

The app

The amazing app that will use these libraries is a simple HTML. But for performance reasons it doesn't include dom.js and event.js, but rather build/release.js - a concatenated version of the two. Here's the "app":

<script src="build/release.js"></script>
<script>
event.addListener(dom.$('butt'), 'click', function () {
  dom.setContent('hi', 'bonjour');
});
</script>

The transform

How to go about transforming them sources? The easiest transform is a one-line concatenation:

$ cat src/dom.js src/event.js > release/build.js

But there are no source maps here! So let's write a small script to do the concatenation and also take care of the source maps.

To help with the source maps there's this handy source-map library from Mozilla.

Using source-map

A barebone example of using the library:

var SourceMapGenerator = require('source-map').SourceMapGenerator;
var map = new SourceMapGenerator({file: 'result.js'});
map.addMapping({
  source: 'source.js',
  original: {line: 1, column: 1},
  generated: {line: 1, column: 1}
});
map.toString();

As you see you simply map locations from one file to another. This really shines in more complex code transforms, while for the purposes of the concatenation all you need is to keep track of the line number in the concatenated file, the lines in the source files are always 1 and so are the columns in both files.

The build script

Let's call it build/build.js call it like

$ node build/build.js

This script writes two files: the map and the build/release.js, here goes:

var SourceMapGenerator = require('source-map').SourceMapGenerator;
var read = require('fs').readFileSync;
var write = require('fs').writeFileSync;
 
var sources = ['src/dom.js', 'src/event.js'];
 
 
var map = new SourceMapGenerator({file: "release.js"});
var concatenated = '';
var line = 1;
 
sources.forEach(function(file) {
  map.addMapping({
    source: '../' + file,
    original: {
      line: 1,
      column: 1
    },
    generated: {
      line: line,
      column: 1
    }
  });
  
  var content = read(file, 'utf8');
  line += content.split('\n').length;
  
  concatenated += content;
 
});
 
concatenated += '\n//# sourceMappingURL=release.map';
 
write('build/release.map', map.toString());
write('build/release.js', concatenated);
 
console.log('Built: build/release.js.')
console.log('All yours, feel free to include in an html of your choosing');

The results

The result of running the script are the concatenated file which also includes a link to the map at the end:

//# sourceMappingURL=release.map

... and the map, which goes like:

{
  "version":3,
  "file":"release.js",
  "sources":["../src/dom.js","../src/event.js"],
  "names":[],
  "mappings":"CAAC;;;;;;;;;;CCAA"
}

In action

You can try for yourself the humble "app" or just browse the directories

Here's what you can expect to see...

Firefox - turn on the viewing of the sources:

ff-on

Firefox - network panel rightly shows the release.js as this is what's downloaded and run:

But switching to debugging panel reveals the source files!

In Chrome you see both:

Happy mapping

And thanks for reading!

Tell your friends about this post on Facebook and Twitter

Sorry, comments disabled and hidden due to excessive spam.

Meanwhile, hit me up on twitter @stoyanstefanov