Gaurab Paul

Polyglot software developer & consultant passionate about web development, distributed systems and open source technologies

Support my blog and open-source work

Tags

On Webpack and Source Map integration
Posted  7 years ago

This post has not been updated in quite some time and the content here may be out of date or not reflect my current my recommedation in the matter.

Webpack is the forerunner in the javascript module bundling space. One of the important aspects that plays a key role in debuggability of javascript applications is source map integration - so we can debug the original source code after transpilation.

Webpack has a devtool option which (just like everything in webpack) supports a variety of choices which can be beffudling even though the official documentation has a dedicated section on it.

Our primary focus would be on a babel transpiled project, though sourcemaps can be effectively used with many other compile-to-js languages like dart, coffeescript, opal(ruby) etc, as well as languages which target css.


Before we delve into different supported devtool options, let us briefly look into how a webpack generated file typically looks like.

Webpack bootstrapper

A typical bundle generated by webpack file is essentially an IIFE that is passed an array of compiled modules.

(function(modules) {
  ... bootstrapper goes here
})([
  ... modules go here
])

The bootstrapper basically defines a minimal dependency loader __webpack_require__ which is injected into each module . The entry module (modules[0]) is invoked first and uses __webpack_require__ to load other dependencies (if any). Irrespective of whether the original source code used commonjs/amd/ES6 imports, they all are transpiled to use __webpack_require__.

Different devtool options:

The documentation mentions following options as valid values for the devtool config, and outlines their characteristics in a helpful table:

devtool build speed rebuild speed production supported quality
eval +++ +++ no generated code
cheap-eval-source-map + ++ no transformed code (lines only)
cheap-source-map + o yes transformed code (lines only)
cheap-module-eval-source-map o ++ no original source (lines only)
cheap-module-source-map o - yes original source (lines only)
eval-source-map + no original source
source-map yes original source

The different dev-tools option provides different levels of ease of debuggability. We start with the most basic one:

eval

| Each module is executed with eval and //@ sourceURL.

With eval each module entry in the modules array looks something like this:

eval("'use strict';...generated code of index.js...//# sourceURL=webpack:///./index.js?");

In this mode the only mapping we have is at file level so exceptions point to the right file in the source code, but not to the right line number of original source code. But while debugging we are not really debugging the original code that we wrote.

It is not really a great choice for compiled code. It is possible to deal with it for languages which are semantically similar to ES2015 like coffeescript or basic babel compilation without async/await transforms (which result in significant restructuring of code) because it is easy to mentally map the compiled source to original code, but it is advisable that we look into the other options below for easier debuggability.

It is to be noted that the line numbers in exception stack traces not only don't match with original code before babel transpilation, it might also not match with the babel transpiled code because of webpack specific transformations that happen before bundle generation. So we can not reliable depend on our editor plugins' babel transpile output to get the line numbers from exceptions.

However, there is one advantage: During debugging, it is easy to copy a part of the source code from the sources panel at any point, tweak it a bit and evaluate it in the javascript console. While this feature is often taken for granted by developers habituated to vanilla javascript, we loose this in most of the other alternatives below.

Because of its simplicity the generation time is quite fast (in fact fastest among all the options below) and does not significantly add to the size of generated bundle.

However, it should not be used in production because eval potentially disables multiple performance optimizations that javascript engines perform.

cheap-eval-source-map

The above table mentions the quality as "transformed code (lines code)". What this means is that the code visible in browser devtools is the code after being transformed by babel (or other loaders) but before final stage webpack specific transformations, which transforms the require statements to use __webpack_require__, injects errors for missing modules etc.

In this mode each module entry in the modules array looks something like this:

eval("'use strict';...generated code of index.js ...//# sourceMappingURL=data:application/json;charset=utf-8;base64,...base 64 encoded string of sourcemap...");

The last part is a base64 encoded string of a JSON source map similar to the following:

{
  "version": 3,
  "file": "0.js",
  "sources": [
    "webpack:///./index.js?8920"
  ],
  "sourcesContent": [
    "'use strict';\n\nvar _react = require('react');\n\nvar _react2 = _interopRequireDefault(_react);\n\nvar _reactDom = require('react-dom');\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\ndebugger;\n\n(0, _reactDom.render)(_react2.default.createElement(\n  'div',\n  null,\n  'Hello world'\n), document.getElementById('root'));\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./index.js\n// module id = 0\n// module chunks = 0"
  ],
  "mappings": "AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA",
  "sourceRoot": ""
}

The code in sourcesContent is the source code after babel transformation, so we can use editor plugin's babel transformation preview (or for that matter babel cli) to get lines reported in exceptions.

Besides the above, the advantages and disadvantages are mostly the same as eval and is basically only useful if the code being webpacked is vanilla javascript which is loaded through webpack. For debugging original source code before babel transpilation, other options are outlined below.

This should also not be used in production because of not only the eval related issues highlighted above, but also because generated code is separately embedded in the code which increases the size of bundle significantly. For production usage it is desirable that the source map be saved as separate files which are loaded on demand during debugging sessions.

While typically line numbers would not match with the original source code if loaders (like babel) are used for transformation, however babel has a option retainLines which will result in output structured in such a way that line numbers of transpiled code match original source line numbers. This obviously does not retain the column numbers.

In the example below we see a simple example of original and babel-transpiled code with retainLines=true

import React from 'react';
import {render} from 'react-dom';

render(
  <div>
    Hello world
  </div>,
  document.getElementById('root')
);
'use strict';var _react = require('react');var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}

(0, _reactDom.render)(
_react2.default.createElement('div', null, 'Hello world'),


document.getElementById('root'));

cheap-source-map

In this case eval is no longer used. And a single source map file is generated which is referred in the generated bundle through a line in the end like:

//# sourceMappingURL=bundle.js.map

The code visible in browser dev-tools is still babel-transpiled code, like the case with cheap-eval-source-map.

cheap-module-eval-source-map

As mentioned in the name this uses eval but the source is the actual source code before babel transpilation. Though we get line level mapping, individual expressions are not mapped to expressions in the original source.

However because of the same reasons as cheap-eval-source-map (eval usage, source map embedded in generated bundle leading to increased size) it is not suitable for production usage.

cheap-module-source-map

This is an evolution over the above in that we get rid of eval and move source maps to separate files making it sutable for production usage.

eval-source-map

This maps to not only lines of original source code, but also columns. It does, however, use eval making it suitable only for development.

The detailed source map generation results in an execution time penalty during bundling as well as larger source maps.

source-map

This has column level mapping to original source code, does not use eval and is suitable for production. This is the best option for production usage, even though build times are slower than above options.


For the simple example above the sizes of different bundles and source maps are summarized below:

File Size Time taken for initial bundling
bundle.eval.js 760K 1603ms
bundle.cheap-eval-source-map.js 1.9M 1888ms
bundle.cheap-source-map.js 723K 1724ms
bundle.cheap-source-map.js.map 841K
bundle.cheap-module-eval-source-map.js 1.9M 1870ms
bundle.cheap-module-source-map.js 723K 1756ms
bundle.cheap-module-source-map.js.map 841K
bundle.eval-source-map.js 1.9M 2127ms
bundle.source-map.js 725K 2197ms
bundle.source-map.js.map 844K

Given this is a small file with very few dependencies, the webpack bundling times are not representative of real world use cases.

It is recommended to use separate webpack configurations for production and development. In development, when dealing with mostly well formatted source code cheap-module-eval-source-map is usually adequate where as in production it is good to have source-map as the devtool config.

Selective source map generation

Webpack provides a SourceMapDevToolPlugin for greater control over which files source maps should be generated for.

For instance, using this plugin we can specify a regular expression for filtering all the files for which source maps should be generated.

About pragma styles

The devtool configuration string can be prefixed with # or @ to force a specific a pragma style. The latter is now deprecated by browser vendors. The reasoning behind this has been summarized by html5rocks:

Previously the comment pragma was //@ but due to some issues with that and IE conditional compilation comments the decision was made to change it to //#. Currently Chrome Canary, WebKit Nightly and Firefox 24+ support the new comment pragma. This syntax change also affects sourceURL.

Changing the prefix to @ can help with debugging in older browsers