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.1.0
$ webpack $npm_package_config_webpack_args
Hash: 2cb9d977b46c26f51aaa
Version: webpack 3.6.0
Time: 1220ms
     Asset       Size  Chunks             Chunk Names
 bundle.js     205 kB       0  [emitted]  main
styles.css     123 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 509 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 509 bytes {0} [built]
       [3] (webpack)/buildin/module.js 517 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 1.85s.

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.1.0
$ webpack  $npm_package_config_webpack_args -p --env=production
Hash: 1525636a98a259e8fdee
Version: webpack 3.6.0
Time: 2496ms
                              Asset       Size  Chunks             Chunk Names
     fc0c74a272313cf6426c-bundle.js    25.9 kB       0  [emitted]  main
    fc0c74a272313cf6426c-styles.css    80.8 kB       0  [emitted]  main
 fc0c74a272313cf6426c-bundle.js.map     199 kB       0  [emitted]  main
fc0c74a272313cf6426c-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 509 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 509 bytes {0} [built]
       [3] (webpack)/buildin/module.js 517 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 3.18s.

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.1.0
$ webpack $npm_package_config_webpack_args
Hash: 429ba09cdf1f253a3f19
Version: webpack 3.6.0
Time: 1575ms
     Asset       Size  Chunks                    Chunk Names
 bundle.js     205 kB       0  [emitted]         main
styles.css     375 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 509 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 509 bytes {0} [built]
       [3] (webpack)/buildin/module.js 517 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.15s.

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.1.0
$ webpack  $npm_package_config_webpack_args -p --env=production
Hash: a86c0ea9f00eb1152a1c
Version: webpack 3.6.0
Time: 2953ms
                              Asset       Size  Chunks             Chunk Names
     fc0c74a272313cf6426c-bundle.js    25.9 kB       0  [emitted]  main
    fc0c74a272313cf6426c-styles.css    80.8 kB       0  [emitted]  main
 fc0c74a272313cf6426c-bundle.js.map     199 kB       0  [emitted]  main
fc0c74a272313cf6426c-styles.css.map     169 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 509 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 509 bytes {0} [built]
       [3] (webpack)/buildin/module.js 517 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 3.59s.

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.1.0
$ karma start spec/karma.conf.js --single-run --no-color
Hash: e0bdc8cc97632b01d813
Version: webpack 3.6.0
Time: 33ms
webpack: Compiled successfully.
webpack: Compiling...
webpack: wait until bundle finished: 
Hash: 19c86568b5ae446a05d0
Version: webpack 3.6.0
Time: 133ms
                  Asset     Size  Chunks             Chunk Names
         canary.spec.js   2.6 kB       0  [emitted]  canary.spec.js
markdownPreview.spec.js  78.8 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 509 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.
08 10 2017 12:19:47.228:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
08 10 2017 12:19:47.230:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
08 10 2017 12:19:47.240:INFO [launcher]: Starting browser PhantomJS
webpack: Compiling...
Hash: 19c86568b5ae446a05d0
Version: webpack 3.6.0
Time: 7ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.8 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 509 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: 19c86568b5ae446a05d0
Version: webpack 3.6.0
Time: 4ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.8 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 509 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: 19c86568b5ae446a05d0
Version: webpack 3.6.0
Time: 3ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.8 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 509 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: 19c86568b5ae446a05d0
Version: webpack 3.6.0
Time: 4ms
                  Asset     Size  Chunks  Chunk Names
         canary.spec.js   2.6 kB       0  canary.spec.js
markdownPreview.spec.js  78.8 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 509 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...
08 10 2017 12:19:48.194:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket BYoYqEfkqZYx1n2MAAAA with id 4535561
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.003 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 (1 FAILED) (0.001 secs / 0.003 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.1.0
[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 5.36s.
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.1.0
$ karma start spec/karma.conf.js --single-run --no-color
Hash: e0bdc8cc97632b01d813
Version: webpack 3.6.0
Time: 33ms
webpack: Compiled successfully.
webpack: Compiling...
webpack: wait until bundle finished: 
Hash: 19c86568b5ae446a05d0
Version: webpack 3.6.0
Time: 106ms
                  Asset     Size  Chunks             Chunk Names
         canary.spec.js   2.6 kB       0  [emitted]  canary.spec.js
markdownPreview.spec.js  78.8 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 509 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.
08 10 2017 12:19:55.181:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
08 10 2017 12:19:55.183:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
08 10 2017 12:19:55.188:INFO [launcher]: Starting browser PhantomJS
08 10 2017 12:19:56.011:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket Gy_Mzwg-Cbl22RMoAAAA with id 87566568
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.006 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.

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.