Webpack from Nothing

Writing Code in Another Language

JavaScript is not a great programming language. Even with Webpack giving us the ability to write modular code, JavaScript is still a bit weak.

For example, we have to remember to use === everywhere or things get weird. We also have to type the keyword function a lot. Scoping is a mess, we can't make constants, and it would be nice to define a proper class that handles this.

ECMAScript 2015 addresses all of these failures in JavaScript, but we can't run it in a browser. What we can do is translate it to JavaScript, allowing us to write modern code that still runs in a browser.

Write Modern JavaScript, Ship Compatible JavaScript

To do this with Webpack, we'll need to set up Babel, which will do the work. Babel advertises itself as “a JavaScript compiler” which is also kindof what Webpack is. The confusion lies around what is meant by the word “JavaScript”. In Babel's case, it means “a newer version of JavaScript than your browser can produce”, which is sort of what Webpack can do as well. Suffice it to say, Babel handles the newest version of JavaScript most properly, so we do need it to accomplish our goal of writing entirely in ES2015.

Let's write some! Replace js/markdownPreviewer.js with this:

import { markdown } from "markdown";

const attachPreviewer = ($document,sourceId,previewId) => {
  return (event) => {
    const text    = $document.getElementById(sourceId).value,
          preview = $document.getElementById(previewId);

    preview.innerHTML = markdown.toHTML(text);
    event.preventDefault();
  };
}

export default {
  attachPreviewer: attachPreviewer
}

It's fairly similar, because we don't have that much, but note that we've changed from function to using arrows and we've changed our use of var to const, since the variables never get assigned more than once.

Also note that if you run Webpack now, and you are using a modern browser, this code has a good chance of working. But, it won't work for all browsers, including ones we want to support. Let's continue.

First, we'll install babel. Which, sadly, cannot be accomplished via yarn add babel. Instead we must:

> yarn add babel-core
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 23 new dependencies.
├─ babel-core@6.26.0
├─ babel-generator@6.26.0
├─ babel-helpers@6.24.1
├─ babel-messages@6.23.0
├─ babel-register@6.26.0
├─ babel-runtime@6.26.0
├─ babel-template@6.26.0
├─ babel-traverse@6.26.0
├─ babel-types@6.26.0
├─ babylon@6.18.0
├─ convert-source-map@1.5.0
├─ detect-indent@4.0.0
├─ globals@9.18.0
├─ home-or-tmp@2.0.0
├─ invariant@2.2.2
├─ jsesc@1.3.0
├─ loose-envify@1.3.1
├─ private@0.1.7
├─ regenerator-runtime@0.11.0
├─ slash@1.0.0
├─ source-map-support@0.4.18
├─ to-fast-properties@1.0.3
└─ trim-right@1.0.1
Done in 3.12s.
warning "karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

We'll also need the Babel loader for Webpack:

> yarn add babel-loader
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 5 new dependencies.
├─ babel-loader@7.1.2
├─ commondir@1.0.1
├─ find-cache-dir@1.0.0
├─ make-dir@1.0.0
└─ pkg-dir@2.0.0
Done in 2.33s.
warning "karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

Babel should process all JavaScript, so we'll configure the loader in webpack/common.js to handle all .js files, save for those in node_modules, which are already assumed to be ready for distribution to a browser:

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

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

Of course, Babel doesn't automatically do anything, so we need more configuration and more modules. Babel has the concept of presets, but of course, none of them are actually pre-set. We'll use the recommend “env” preset, which means “generally do the right thing without having to configure stuff”, which is a godsend, so we'll take it.

> yarn add babel-preset-env
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 44 new dependencies.
├─ babel-helper-builder-binary-assignment-operator-visitor@6.24.1
├─ babel-helper-call-delegate@6.24.1
├─ babel-helper-define-map@6.26.0
├─ babel-helper-explode-assignable-expression@6.24.1
├─ babel-helper-function-name@6.24.1
├─ babel-helper-get-function-arity@6.24.1
├─ babel-helper-hoist-variables@6.24.1
├─ babel-helper-optimise-call-expression@6.24.1
├─ babel-helper-regex@6.26.0
├─ babel-helper-remap-async-to-generator@6.24.1
├─ babel-helper-replace-supers@6.24.1
├─ babel-plugin-check-es2015-constants@6.22.0
├─ babel-plugin-syntax-async-functions@6.13.0
├─ babel-plugin-syntax-exponentiation-operator@6.13.0
├─ babel-plugin-syntax-trailing-function-commas@6.22.0
├─ babel-plugin-transform-async-to-generator@6.24.1
├─ babel-plugin-transform-es2015-arrow-functions@6.22.0
├─ babel-plugin-transform-es2015-block-scoped-functions@6.22.0
├─ babel-plugin-transform-es2015-block-scoping@6.26.0
├─ babel-plugin-transform-es2015-classes@6.24.1
├─ babel-plugin-transform-es2015-computed-properties@6.24.1
├─ babel-plugin-transform-es2015-destructuring@6.23.0
├─ babel-plugin-transform-es2015-duplicate-keys@6.24.1
├─ babel-plugin-transform-es2015-for-of@6.23.0
├─ babel-plugin-transform-es2015-function-name@6.24.1
├─ babel-plugin-transform-es2015-literals@6.22.0
├─ babel-plugin-transform-es2015-modules-amd@6.24.1
├─ babel-plugin-transform-es2015-modules-commonjs@6.26.0
├─ babel-plugin-transform-es2015-modules-systemjs@6.24.1
├─ babel-plugin-transform-es2015-modules-umd@6.24.1
├─ babel-plugin-transform-es2015-object-super@6.24.1
├─ babel-plugin-transform-es2015-parameters@6.24.1
├─ babel-plugin-transform-es2015-shorthand-properties@6.24.1
├─ babel-plugin-transform-es2015-spread@6.22.0
├─ babel-plugin-transform-es2015-sticky-regex@6.24.1
├─ babel-plugin-transform-es2015-template-literals@6.22.0
├─ babel-plugin-transform-es2015-typeof-symbol@6.23.0
├─ babel-plugin-transform-es2015-unicode-regex@6.24.1
├─ babel-plugin-transform-exponentiation-operator@6.24.1
├─ babel-plugin-transform-regenerator@6.26.0
├─ babel-plugin-transform-strict-mode@6.24.1
├─ babel-preset-env@1.6.0
├─ caniuse-lite@1.0.30000744
└─ regenerator-transform@0.10.1
Done in 3.54s.
warning "karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

Now, create a config file for Babel in the root directory (yup) called .babelrc (note that this file is JSON and not JavaScript, so it's far easier to mess up, and you can be sure you won't get a good error message if you do):

{
  "presets": ["env"]
}

And now, re-run Webpack:

> yarn webpack
yarn run v1.1.0
$ webpack $npm_package_config_webpack_args
Hash: eb5b73d0606a68db59f1
Version: webpack 3.6.0
Time: 1692ms
     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 584 bytes {0} [built]
   [2] ./css/styles.css 41 bytes {0} [built]
   [3] ./js/markdownPreviewer.js 492 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--1-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--1-1!node_modules/tachyons/css/tachyons.css:
       2 modules
Done in 2.29s.

If you inspect dev/bundle.js, you can see that the fancy arrows and use of const is gone, replaced with old school JavaScript.

Now, we can write modern JavaScript and not worry about what browsers actually support it. What about tests?

Using Modern JavaScript to Write Tests

First, let's rewrite our test using ES2015:

import markdownPreviewer from "../js/markdownPreviewer"

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

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

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

      submitHandler(event);

      expect(preview.innerHTML).toBe("<p>This is <em>some markdown</em></p>");
      expect(event.preventDefaultCalled).toBe(true);
    });
  });
});

If you run Karma now, you'll get an error on the new syntax. Although Karma is using Babel to transpile our production code, it's not doing that for the test code. We'll need another preprocessor, karma-babel-preprocessor.

> yarn add karma-babel-preprocessor
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-babel-preprocessor@7.0.0
Done in 2.32s.
warning "karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

Now, configure it in spec/karma.conf.js:

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

Note that the addition of 'babel' to the preprocessors must come at the end or it doesn't work. Why? Who knows?

If your test file still has the error from before, or if you introduce one, you'll get a lovely new surprise - source maps no longer work.

While they do work in our production code, we no longer get the ability to see where in our test something failed. Them's the breaks and it's currently not fixable in this setup. Changing the order of the preprocessors doesn't work, nor does explicitly setting options for babel. Even debugging this is difficult, because of how poorly all these tools are designed and how opaque their interoperability is.

Such a letdown. And it seems like a pretty fitting end to our journey.

Where We Are

It's not all bleak. We started with some basic needs to manage JavaScript and Webpack has met them, and more. We can write modular JavaScript, handle both development and production, run tests, and even use a new language. What's better, the amount of configuration we had to add wasn't that large.

> wc -l webpack.config.js webpack/*.js spec/karma.conf.js
6 webpack.config.js
      33 webpack/common.js
      17 webpack/dev.js
      17 webpack/production.js
      15 spec/karma.conf.js
      88 total

That's less than 100 lines total, and we have a completely workable development environment.

Hopefully, you've learned a bit about why Webpack exists, and what it can (and can't) do. I also hope you've learned to feel confident in your needs as a developer and comfortable pointing out when available tools aren't meeting those needs. It doesn't mean the people that put their blood, sweat, and tears into them are bad people, but designing build tools is hard, and the JavaScript ecosystem has the widest variety of developers ever, so it's hard to please everyone.

That said, I want to spend the last chapter discussing the design decisions that I believe make this entire thing so difficult to deal with and what might make it all work better. These are ways of thinking that help you build any application, even if it's not a build tool.