Webpack from Nothing

Stack Traces and Line Numbers that Make Sense

We're getting a better and better setup for developing and deploying our applications. We can organize our JavaScript and CSS, we can use third party libraries for both, can deploy to production in a proper way, and run tests. But we're still missing something every other programming language has: stack traces.

Stack traces tell us where in our code errors are happening. In most programming languages, when an uncaught error happens, we see some information about what line of code raised the error, as well as the path through the code that led to that error. Even in a language like C, we can do this. Not so in JavaScript.

The reason is that the file we're editing is not the file that's being executed. Webpack is compiling all of our files together, as well as minifying them and including our third party libraries.

Let's see the problem in action.

Add a throw to the function in markdownPreviewer.js, after event.preventDefault().

Re-run Webpack and open up dev/index.html, then open the JavaScript console, and then click the “Preview” button:

Image of a useless stack trace

The error came from line 1 of our bundle. This is technically true, since our bundle is minified and all the code is on one line. Since we aren't editing this file directly, we have no idea where this error came from in our original source code.

The solution to this is a feature that most browsers support called sourcemaps.

Sourcemaps

If a sourcemap file exists, browsers know to look at it when giving you a stack trace (though you may need to enable this feature in your browser). The sourcemap lets the browser tell us that the problem wasn't in line 1 of our bundle, but on line 10 of markdownPreviewer.js.

Webpack can produce sourcemaps. The configuration option is, of course, not called something intuitive like sourceMap, but instead is called devtool.

The possible values for devtool are many, and poorly documented. Since we have different configurations for production and development, we can use different source map strategies. For production, we'll use source-map as that contains the most information and is designed for production.

In webpack/production.js:

const path         = require('path');
const Merge        = require('webpack-merge');
const CommonConfig = require('./common.js');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = Merge(CommonConfig, {
  output: {
    path: path.join(__dirname, '../production'),
    filename: '[chunkhash]-bundle.js'
  },
/* start new code */
  devtool: "source-map",
/* end new code */
  plugins: [
    new ExtractTextPlugin('[chunkhash]-styles.css')
  ]
});

For development, we want the fastest thing possible that shows the most information. I think that's inline-source-map, but the docs are unclear. It works, so we'll use it:

const path         = require('path');
const Merge        = require('webpack-merge');
const CommonConfig = require('./common.js');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = Merge(CommonConfig, {
  output: {
    path: path.join(__dirname, '../dev'),
    filename: 'bundle.js'
  },
/* start new code */
  devtool: "inline-source-map",
/* end new code */
  plugins: [
    new ExtractTextPlugin('styles.css')
  ]
});

Run Webpack:

> yarn webpack
yarn run v1.3.2
$ webpack $npm_package_config_webpack_args
Hash: c22b619c51baaecacbea
Version: webpack 3.8.1
Time: 1896ms
     Asset       Size  Chunks             Chunk Names
 bundle.js     205 kB       0  [emitted]  main
styles.css     125 kB       0  [emitted]  main
index.html  833 bytes          [emitted]  
   [0] ./js/index.js 421 bytes {0} [built]
   [2] ./css/styles.css 41 bytes {0} [built]
   [3] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [7] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 8 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 1.12 kB {0} [built]
       [2] (webpack)/buildin/global.js 488 bytes {0} [built]
       [3] (webpack)/buildin/module.js 495 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js!css/styles.css:
       [0] ./node_modules/css-loader!./css/styles.css 234 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js!node_modules/tachyons/css/tachyons.css:
       2 modules
Done in 2.81s.

And, if we follow the same steps in the browser, we'll see that we can now see the line number where our error is coming from!

Image of a useful stack trace

If you are in Chrome and click the stack, it even shows you the code where the error came from!

Image of code where the error originated

Nice!

Let's check our production configuration to make sure there's no issue:

> yarn prod
yarn run v1.3.2
$ webpack  $npm_package_config_webpack_args -p --env=production
Hash: 2502cd8db1d8fec17b2f
Version: webpack 3.8.1
Time: 4101ms
                              Asset       Size  Chunks             Chunk Names
     ac44ec53be3404a61b9a-bundle.js    25.9 kB       0  [emitted]  main
    ac44ec53be3404a61b9a-styles.css    81.7 kB       0  [emitted]  main
 ac44ec53be3404a61b9a-bundle.js.map     199 kB       0  [emitted]  main
ac44ec53be3404a61b9a-styles.css.map  108 bytes       0  [emitted]  main
                         index.html  875 bytes          [emitted]  
   [0] ./js/index.js 421 bytes {0} [built]
   [2] ./css/styles.css 41 bytes {0} [built]
   [3] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [7] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 8 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 1.12 kB {0} [built]
       [2] (webpack)/buildin/global.js 488 bytes {0} [built]
       [3] (webpack)/buildin/module.js 495 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js!css/styles.css:
       [0] ./node_modules/css-loader!./css/styles.css 220 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js!node_modules/tachyons/css/tachyons.css:
       2 modules
Done in 5.01s.

What about our CSS? Sometimes it's nice to know where certain styles are defined or where they came from.

Sourcemaps for CSS

To see what I mean, open up your app, and inspect an element. Your browser should show you the CSS of the element in question and should show you where those classes are defined. They will all be line 1 of your .css bundle.

Making this work is slightly tricky, because we have to expand the configuration given to ExtractTextPlugin. The way it's designed is that it takes the exact same options as use:, which are currently:

{
  use: "css-loader"
}

We need to pass sourceMap: true (not devtool—go figure) as an option, but there is no place to add options. The syntax we are using is a short-form of this syntax:

{
  use: {
    loader: "css-loader",
    options: {}
  }
}

And there is a place to add options. We want to pass a structure like that to ExtractTextPlugin, so the full change in webpack/common.js is:

const HtmlPlugin     = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: ExtractTextPlugin.extract({
/* start new code */
          use: {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          }
/* end new code */
        })
      }
    ]
  },
  plugins: [
    new HtmlPlugin({
      template: "./html/index.html"
    })
  ],
  entry: './js/index.js'
};

If you run Webpack:

> yarn webpack
yarn run v1.3.2
$ webpack $npm_package_config_webpack_args
Hash: 0b352f59b1ba34cf0be2
Version: webpack 3.8.1
Time: 1808ms
     Asset       Size  Chunks                    Chunk Names
 bundle.js     205 kB       0  [emitted]         main
styles.css     379 kB       0  [emitted]  [big]  main
index.html  833 bytes          [emitted]         
   [0] ./js/index.js 421 bytes {0} [built]
   [2] ./css/styles.css 41 bytes {0} [built]
   [3] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [7] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 8 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 1.12 kB {0} [built]
       [2] (webpack)/buildin/global.js 488 bytes {0} [built]
       [3] (webpack)/buildin/module.js 495 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--0-1!css/styles.css:
       [0] ./node_modules/css-loader?{"sourceMap":true}!./css/styles.css 507 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--0-1!node_modules/tachyons/css/tachyons.css:
       2 modules
Done in 2.58s.

And repeat the steps to inspect an element, you'll now see the correct line numbers in the files where the classes are defined.

Don't forget to try with production:

> yarn prod
yarn run v1.3.2
$ webpack  $npm_package_config_webpack_args -p --env=production
Hash: 307996ef61def9a21a1f
Version: webpack 3.8.1
Time: 4283ms
                              Asset       Size  Chunks             Chunk Names
     ac44ec53be3404a61b9a-bundle.js    25.9 kB       0  [emitted]  main
    ac44ec53be3404a61b9a-styles.css    81.7 kB       0  [emitted]  main
 ac44ec53be3404a61b9a-bundle.js.map     199 kB       0  [emitted]  main
ac44ec53be3404a61b9a-styles.css.map     171 kB       0  [emitted]  main
                         index.html  875 bytes          [emitted]  
   [0] ./js/index.js 421 bytes {0} [built]
   [2] ./css/styles.css 41 bytes {0} [built]
   [3] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [7] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 8 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 1.12 kB {0} [built]
       [2] (webpack)/buildin/global.js 488 bytes {0} [built]
       [3] (webpack)/buildin/module.js 495 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--0-1!css/styles.css:
       [0] ./node_modules/css-loader?{"sourceMap":true}!./css/styles.css 493 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--0-1!node_modules/tachyons/css/tachyons.css:
       2 modules
Done in 5.26s.

Nice!

What about tests?

Stack Traces in Tests

If we introduce a failure into our tests, we'll see a stack trace, but the line number is useless, as before.

First, remove the throw you added before from js/markdownPreviewer.js. Next, let's introduce a test failure in our test.

Here's our entire test file with a failure introduced:

import markdownPreviewer from "../js/markdownPreviewer"

var event = {
  preventDefaultCalled: false,
  preventDefault: function() { this.preventDefaultCalled = true; }
};
var source = {
  value: "This is _some markdown_"
};
var preview = {
  innerHTML: ""
};

var document = {
  getElementById: function(id) {
    if (id === "source") {
      return source;
    }
    else if (id === "preview") {
      return preview;
    }
    else {
      throw "Don't know how to get " + id;
    }
  }
}

describe("markdownPreviewer", function() {
  describe("attachPreviewer", function() {
    it("renders markdown to the preview element", function() {
      var submitHandler = markdownPreviewer.attachPreviewer(document,
                                                            "source",
                                                            "preview");
      source.value = "This is _some markdown_";

      submitHandler(event);

/* start new code */
      expect(preview.innerHTML).toBe("<p>This is <i>some markdown</em></p>");
      // --FAILURE--------------------------------^^^
      //
/* end new code */
/* start new code */

/* end new code */
      expect(event.preventDefaultCalled).toBe(true);
    });
  });
});
> yarn karma
yarn run v1.3.2
$ karma start spec/karma.conf.js --single-run --no-color
Hash: e0bdc8cc97632b01d813
Version: webpack 3.8.1
Time: 81ms
webpack: Compiled successfully.
webpack: Compiling...
webpack: wait until bundle finished: 
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 162ms
                  Asset     Size  Chunks             Chunk Names
         canary.spec.js   2.6 kB       0  [emitted]  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  [emitted]  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0} [built]
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1} [built]
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1} [built]
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1} [built]
   [5] ./node_modules/util/util.js 15.6 kB {1} [built]
   [6] (webpack)/buildin/global.js 488 bytes {1} [built]
   [7] ./node_modules/process/browser.js 5.42 kB {1} [built]
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1} [built]
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1} [built]
webpack: Compiled successfully.
07 11 2017 19:37:26.048:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
07 11 2017 19:37:26.050:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
07 11 2017 19:37:26.062:INFO [launcher]: Starting browser PhantomJS
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 13ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 12ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 8ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 7ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 5ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 11ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 9ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 7ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 6ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 7ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 6ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0}
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1}
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1}
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1}
   [5] ./node_modules/util/util.js 15.6 kB {1}
   [6] (webpack)/buildin/global.js 488 bytes {1}
   [7] ./node_modules/process/browser.js 5.42 kB {1}
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1}
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1}
webpack: Compiled successfully.
webpack: Compiling...
07 11 2017 19:37:28.537:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket VEn2vweThXmKW5g_AAAA with id 74534795
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 0 of 2 SUCCESS (0 secs / 0 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 1 of 2 SUCCESS (0 secs / 0.002 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0) markdownPreviewer attachPreviewer renders markdown to the preview element FAILED
    Expected '<p>This is <em>some markdown</em></p>' to be '<p>This is <i>some markdown</em></p>'.
    markdownPreview.spec.js:112:37
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 (1 FAILED) (0 secs / 0.006 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 (1 FAILED) (0.007 secs / 0.006 secs)
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
error Command failed with exit code 1.

Although it seems like our Webpack configuration should include the sourcemaps, even for the test code, for whatever reason it doesn't, and we have to set up an additional preprocessor for Karma to include them.

That preprocessor is karma-sourcemap-loader, which we can install thusly:

> yarn add karma-sourcemap-loader -D
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
└─ karma-sourcemap-loader@0.3.7
Done in 3.16s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

We configure it after the Webpack preprocessor, like so:

module.exports = function(config) {
  config.set({
    frameworks: ['jasmine'],
    files: [
      '**/*.spec.js'
    ],
    preprocessors: {
/* start new code */
      '**/*.spec.js': [ 'webpack', 'sourcemap' ]
/* end new code */
    },
    webpack: require('../webpack.config.js'),
    browsers: ['PhantomJS']
  })
}

Now, when we run karma, we should see the stack trace reference a line in our test file:

> yarn karma
yarn run v1.3.2
$ karma start spec/karma.conf.js --single-run --no-color
Hash: e0bdc8cc97632b01d813
Version: webpack 3.8.1
Time: 48ms
webpack: Compiled successfully.
webpack: Compiling...
webpack: wait until bundle finished: 
Hash: 689cf36bad6d3ecf4c27
Version: webpack 3.8.1
Time: 160ms
                  Asset     Size  Chunks             Chunk Names
         canary.spec.js   2.6 kB       0  [emitted]  canary.spec.js
markdownPreview.spec.js  78.7 kB       1  [emitted]  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0} [built]
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1} [built]
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1} [built]
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1} [built]
   [5] ./node_modules/util/util.js 15.6 kB {1} [built]
   [6] (webpack)/buildin/global.js 488 bytes {1} [built]
   [7] ./node_modules/process/browser.js 5.42 kB {1} [built]
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1} [built]
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1} [built]
webpack: Compiled successfully.
07 11 2017 19:37:34.047:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
07 11 2017 19:37:34.049:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
07 11 2017 19:37:34.054:INFO [launcher]: Starting browser PhantomJS
07 11 2017 19:37:35.241:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket IJZjfQDtus_KiS28AAAA with id 90813839
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 0 of 2 SUCCESS (0 secs / 0 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 1 of 2 SUCCESS (0 secs / 0.001 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0) markdownPreviewer attachPreviewer renders markdown to the preview element FAILED
    Expected '<p>This is <em>some markdown</em></p>' to be '<p>This is <i>some markdown</em></p>'.
    markdownPreview.spec.js:112:37
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 (1 FAILED) (0 secs / 0.005 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 (1 FAILED) (0.007 secs / 0.005 secs)
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
error Command failed with exit code 1.

It does reference a line, which is great, but it's not the correct one, which is not great.

As of this writing, PhantomJS does not properly read the sourcemap and reports the wrong line number. This sucks, but all is not lost!

If you recall, we've been using the command line switch --single-run to run our tests. If we omit that, Karma will run our tests and then sit there, waiting.

> $(yarn bin)/karma start spec/karma.conf.js

If you look at the output, it will show something like this:

22 04 2017 14:36:17.997:INFO [karma]: Karma v1.6.0 server started at http://0.0.0.0:9876/

If you navigate to that url and port in your web browser, Karma will run your tests in that browser! If we do this in Chrome, the stack trace is correct:

22 04 2017 14:37:02.819:INFO [Chrome 57.0.2987 (Mac OS X 10.12.4)]: Connected on socket 7nP6V7W0YsH5C0f4AAAB with id manual-9961
PhantomJS 2.1.1 (Mac OS X 0.0.0) markdownPreviewer attachPreviewer renders markdown to the preview element FAILED
    Expected '<p>This is <em>some markdown</em></p>' to be '<p>This is <i>some markdown</em></p>'.
    webpack:///spec/markdownPreview.spec.js:40:0 <- markdownPreview.spec.js:1:77552
    loaded@http://localhost:9876/context.js:162:17
Chrome 57.0.2987 (Mac OS X 10.12.4) markdownPreviewer attachPreviewer renders markdown to the preview element FAILED
    Expected '<p>This is <em>some markdown</em></p>' to be '<p>This is <i>some markdown</em></p>'.
        at Object.<anonymous> (http://0.0.0.0:9876webpack:///spec/markdownPreview.spec.js:40:0 <- markdownPreview.spec.js:1:26111)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 3 of 3 (1 FAILED) (0.005 secs / 0.006 secs)
Chrome 57.0.2987 (Mac OS X 10.12.4): Executed 3 of 3 (1 FAILED) (0.045 secs / 0.008 secs)
TOTAL: 2 FAILED, 4 SUCCESS

It's hard to make out, but you can see that the Chrome run of the test is pointing to line 40, which is where the failing expectation is.

It's not ideal, but it is a way to get stack traces for your tests.

You can hit Ctrl-C to exit Karma.

This isn't the greatest ending to our desire to have stack traces, but at least it's something, and at least it's possible.

We can also start to see the tension between monolithic everything-is-included systems like Webpack, and its attempts at modularity and flexibility. Because neither Webpack nor Karma were designed to work together, and because each tool has a completely proprietary plugin/extension mechanism, we have to jump through a lot of hoops to get them to cooperate. I'll touch on this later toward the end of our journey, but suffice it to say, the design of these tools seems to have the worst of being monolithic and being modular.

So, what's next?

Our setup is pretty good so far, and we haven't written that much configuration—less than 100 lines!

With what we have now, we can get really far, but let's add one more tweak to our dev environment, and configure auto-reloading of code and tests as we make changes.