Webpack from Nothing

Wrangling CSS, too

CSS is great for writing term papers, not great for writing web apps, but it's what we've got, and we need to deal with it.

For our simple app, we could inline our CSS into the <head>, but the second we need another template, that ceases to work, and we'd like to manage CSS in separate files, the same as our JavaScript.

Webpack can totally handle this, even though it feels way outside its wheelhouse, since it's not JavaScript. I'm not even sure why Webpack has features to support this, but it does, and it keeps us from having to find another tool to manage CSS.

We talked before about plugins, but we didn't talk about loaders. Loaders are the fourth core concept in Webpack and they have to do with the way in which import behaves on files that aren't JavaScript.

First, let's write some CSS for our app so we have something real to work with.

Styling our App

We're going to use a third-party CSS library later on, so let's use a light touch here. Let's make our text a bit lighter, make the background a bit darker, but keep our <textarea> black on white. We'll also change to one of those sans-serif fonts the kids like so much.

We'll put this in css/styles.css:

html {
  color: #111111;
  background-color: #EEEEEE;
  font-family: avenir next, avenir, helvetica, sans-serif;
}

textarea {
  color: black;
  background-color: white;
}

We can now reference this in html/index.html:

<!DOCTYPE html>
<html>
  <head>
<!-- start new code -->
    <link rel="stylesheet" href="styles.css">
<!-- end new code -->
    <!-- script tag is omitted - Webpack will provide -->
  </head>
  <body>
    <h1>Markdown Preview-o-tron 7000!</h1>
    <form id="editor">
      <textarea id="source" rows="10" cols="80"></textarea>
      <br>
      <input type="submit" value="Preview!">
    </form>
    <hr>
    <section id="preview">
    </section>
  </body>
</html>

Run Webpack:

> yarn webpack
yarn run v1.3.2
$ webpack $npm_package_config_webpack_args
Hash: ad02fc8f1a8143903361
Version: webpack 3.8.1
Time: 480ms
     Asset       Size  Chunks             Chunk Names
 bundle.js    77.8 kB       0  [emitted]  main
index.html  520 bytes          [emitted]  
   [0] ./js/index.js 334 bytes {0} [built]
   [1] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [5] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 6 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 840 bytes {0} [built]
       [2] (webpack)/buildin/global.js 488 bytes {0} [built]
       [3] (webpack)/buildin/module.js 495 bytes {0} [built]
        + 1 hidden module
Done in 1.20s.

And then manually copy our stylesheet into dev/:

> cp css/styles.css dev/

Now, our app looks a bit nicer:

Our app with some styling

This has the same problems with bundle.js. We want it minified and we want it hashed and we generally don't want to deal with it. We know Webpack can do those things for JavaScript, and it can do them for CSS.

Surprisingly, we make this happen by importing our CSS into js/index.js!

Loaders Can Load Anything

When you write an import statement, and Webpack compiles your code, it loads the file referenced in the import. By default, Webpack assumes that file is JavaScript and that you are trying to write modular code. Cool.

But, you can change that default, basically controlling Webpack's internal machinery to do things other than create JavaScript bundles. To do that, you tell Webpack to use a specific loader.

To load CSS, we'll install css-loader:

> yarn add css-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 105 new dependencies.
├─ alphanum-sort@1.0.2
├─ ansi-styles@2.2.1
├─ argparse@1.0.9
├─ autoprefixer@6.7.7
├─ babel-code-frame@6.26.0
├─ balanced-match@0.4.2
├─ browserslist@1.7.7
├─ caniuse-api@1.6.1
├─ caniuse-db@1.0.30000758
├─ chalk@1.1.3
├─ clap@1.2.3
├─ clone@1.0.2
├─ coa@1.0.4
├─ color-convert@1.9.0
├─ color-name@1.1.3
├─ color-string@0.3.0
├─ color@0.11.4
├─ colormin@1.1.2
├─ css-color-names@0.0.4
├─ css-loader@0.28.7
├─ css-selector-tokenizer@0.7.0
├─ cssesc@0.1.0
├─ cssnano@3.10.0
├─ csso@2.3.2
├─ defined@1.0.0
├─ electron-to-chromium@1.3.27
├─ escape-string-regexp@1.0.5
├─ esprima@2.7.3
├─ esutils@2.0.2
├─ fastparse@1.1.1
├─ flatten@1.0.2
├─ function-bind@1.1.1
├─ has-ansi@2.0.0
├─ has@1.0.1
├─ html-comment-regex@1.1.1
├─ icss-replace-symbols@1.1.0
├─ icss-utils@2.1.0
├─ indexes-of@1.0.1
├─ is-absolute-url@2.1.0
├─ is-plain-obj@1.1.0
├─ is-svg@2.1.0
├─ js-base64@2.3.2
├─ js-tokens@3.0.2
├─ js-yaml@3.7.0
├─ jsesc@0.5.0
├─ lodash.camelcase@4.3.0
├─ lodash.memoize@4.1.2
├─ lodash.uniq@4.5.0
├─ macaddress@0.2.8
├─ math-expression-evaluator@1.2.17
├─ normalize-range@0.1.2
├─ normalize-url@1.9.1
├─ num2fraction@1.2.2
├─ postcss-calc@5.3.1
├─ postcss-colormin@2.2.2
├─ postcss-convert-values@2.6.1
├─ postcss-discard-comments@2.0.4
├─ postcss-discard-duplicates@2.1.0
├─ postcss-discard-empty@2.1.0
├─ postcss-discard-overridden@0.1.1
├─ postcss-discard-unused@2.2.3
├─ postcss-filter-plugins@2.0.2
├─ postcss-merge-idents@2.1.7
├─ postcss-merge-longhand@2.0.2
├─ postcss-merge-rules@2.1.2
├─ postcss-message-helpers@2.0.0
├─ postcss-minify-font-values@1.0.5
├─ postcss-minify-gradients@1.0.5
├─ postcss-minify-params@1.2.2
├─ postcss-minify-selectors@2.1.1
├─ postcss-modules-extract-imports@1.1.0
├─ postcss-modules-local-by-default@1.2.0
├─ postcss-modules-scope@1.1.0
├─ postcss-modules-values@1.3.0
├─ postcss-normalize-charset@1.1.1
├─ postcss-normalize-url@3.0.8
├─ postcss-ordered-values@2.2.3
├─ postcss-reduce-idents@2.4.0
├─ postcss-reduce-initial@1.0.1
├─ postcss-reduce-transforms@1.0.4
├─ postcss-selector-parser@2.2.3
├─ postcss-svgo@2.1.6
├─ postcss-unique-selectors@2.0.2
├─ postcss-value-parser@3.3.0
├─ postcss-zindex@2.2.0
├─ postcss@5.2.18
├─ prepend-http@1.0.4
├─ q@1.5.1
├─ query-string@4.3.4
├─ reduce-css-calc@1.3.0
├─ reduce-function-call@1.0.2
├─ regenerate@1.3.3
├─ regexpu-core@1.0.0
├─ regjsgen@0.2.0
├─ regjsparser@0.1.5
├─ sax@1.2.4
├─ sort-keys@1.1.2
├─ sprintf-js@1.0.3
├─ strict-uri-encode@1.1.0
├─ svgo@0.7.2
├─ uniq@1.0.1
├─ uniqid@4.1.1
├─ uniqs@2.0.0
├─ vendors@1.0.1
└─ whet.extend@0.9.9
Done in 5.21s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

(This is an amazing amount of code to do what will ultimately be concatenating some CSS files together)

To use this loader, we'll add a new section to our Webpack configuration, called module. This gives us a peek into how Webpack views itsef. The modules section controls how Webpack treats different types of modules we might import. This statement implies that there are types other than just JavaScript. It's starting to make sense.

Inside module:, we create a rules: array, which will itemize out all the rules for handling modules that aren't JavaScript. Each rule has a test, and a loader to use. This goes in webpack/common.js, since we need it for dev and prod:

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

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

With this in place, we can modify js/index.js to import our CSS:

/* start new code */
import "../css/styles.css";
/* end new code */
import markdownPreviewer from "./markdownPreviewer";

window.onload = function() {
  document.getElementById("editor").addEventListener(
      "submit",
      markdownPreviewer.attachPreviewer(
        document,    // pass in document
        "source",    // id of source textarea
        "preview")); // id of preview DOM element
};

Note that we're importing "../css/styles.css", because imports that have dots in front of them are relative to the directory where the file being processed is located.

Let's clean up after our test so we can be sure what's happening:

> rm dev/*.*

Now, we can run Webpack:

> yarn webpack
yarn run v1.3.2
$ webpack $npm_package_config_webpack_args
Hash: ca35bc01eb00064b139e
Version: webpack 3.8.1
Time: 720ms
     Asset       Size  Chunks             Chunk Names
 bundle.js    80.8 kB       0  [emitted]  main
index.html  520 bytes          [emitted]  
   [0] ./js/index.js 402 bytes {0} [built]
   [1] ./css/styles.css 345 bytes {0} [built]
   [3] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [7] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 7 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 840 bytes {0} [built]
       [2] (webpack)/buildin/global.js 488 bytes {0} [built]
       [3] (webpack)/buildin/module.js 495 bytes {0} [built]
        + 1 hidden module
Done in 1.54s.

If we open up dev/index.html in your browser, the CSS isn't being applied. BUT, if you look inside the bundle .js file, you can see our CSS in there:

> grep textarea dev/bundle.js
"source",    // id of source textarea
exports.push([module.i, "html {\n  color: #111111;\n  background-color: #EEEEEE;\n  font-family: avenir next, avenir, helvetica, sans-serif;\n}\n\ntextarea {\n  color: black;\n  background-color: white;\n}\n", ""]);

There is a loader called the style-loader that would dynamically create a <style> tag in our DOM and put the CSS in there, but that's no good. We want the browser to load CSS separately, so it can download both the CSS and the JS in parallel.

We need to tell Webpack that our CSS that gets loaded should be placed into a separate output file. This can be done with the ExtractTextPlugin. Despite its generic name, it appears created to solve this specific problem.

> yarn add extract-text-webpack-plugin  -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 2 new dependencies.
├─ extract-text-webpack-plugin@3.0.2
└─ schema-utils@0.3.0
Done in 2.91s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

ExtractTextPlugin provides the function extract which will create a custom loader that, when we also use ExtractTextPlugin as a plugin, will write out our CSS to a file. We can even use the magic "[chunkhash]" inside the filename to get the hash in there!

Here's what our common Webpack configuration now looks like:

const HtmlPlugin     = require('html-webpack-plugin');
/* start new code */
const ExtractTextPlugin = require('extract-text-webpack-plugin');
/* end new code */

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

We also need to tell ExtractTextPlugin what name to use. Because we want this file hashed, the same as our JavaScript, we'll need to specify some configuration in webpack/dev.js and webpack/production.js. Here's what webpack/dev.js will look like

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

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

And, for production.js:

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

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

With this in place, we can remove the <link> tag we put in:

<!DOCTYPE html>
<html>
  <head>
<!-- start new code -->
    <!-- css will be inserted by webpack -->
<!-- end new code -->
    <!-- script tag is omitted - Webpack will provide -->
  </head>
  <body>
    <h1>Markdown Preview-o-tron 7000!</h1>
    <form id="editor">
      <textarea id="source" rows="10" cols="80"></textarea>
      <br>
      <input type="submit" value="Preview!">
    </form>
    <hr>
    <section id="preview">
    </section>
  </body>
</html>

Now, when we run Webpack, our CSS file is being created separately and is available in dev/:

> yarn webpack
yarn run v1.3.2
$ webpack $npm_package_config_webpack_args
Hash: 97ee7c34163781bd4562
Version: webpack 3.8.1
Time: 985ms
     Asset       Size  Chunks             Chunk Names
 bundle.js    78.2 kB       0  [emitted]  main
styles.css  172 bytes       0  [emitted]  main
index.html  560 bytes          [emitted]  
   [0] ./js/index.js 402 bytes {0} [built]
   [1] ./css/styles.css 41 bytes {0} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [6] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 7 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 839 bytes {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 345 bytes {0} [built]
        + 1 hidden module
Done in 1.90s.
> ls dev/*.css
dev/styles.css
> cat dev/index.html
<!DOCTYPE html>
<html>
  <head>
<!-- start new code -->
    <!-- css will be inserted by webpack -->
<!-- end new code -->
    <!-- script tag is omitted - Webpack will provide -->
  <link href="styles.css" rel="stylesheet"></head>
  <body>
    <h1>Markdown Preview-o-tron 7000!</h1>
    <form id="editor">
      <textarea id="source" rows="10" cols="80"></textarea>
      <br>
      <input type="submit" value="Preview!">
    </form>
    <hr>
    <section id="preview">
    </section>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

And, when we build for production, it uses the hashed name:

> yarn prod
yarn run v1.3.2
$ webpack  $npm_package_config_webpack_args -p --env=production
Hash: 94b220a3b594a07541d5
Version: webpack 3.8.1
Time: 1976ms
                          Asset       Size  Chunks             Chunk Names
 4c4cd139c109f982db3b-bundle.js    25.8 kB       0  [emitted]  main
4c4cd139c109f982db3b-styles.css  132 bytes       0  [emitted]  main
                     index.html  602 bytes          [emitted]  
   [0] ./js/index.js 402 bytes {0} [built]
   [1] ./css/styles.css 41 bytes {0} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [6] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 7 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 839 bytes {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 295 bytes {0} [built]
        + 1 hidden module
Done in 3.00s.
> ls production/*.css
production/4c4cd139c109f982db3b-styles.css
> cat production/index.html
<!DOCTYPE html>
<html>
  <head>
<!-- start new code -->
    <!-- css will be inserted by webpack -->
<!-- end new code -->
    <!-- script tag is omitted - Webpack will provide -->
  <link href="4c4cd139c109f982db3b-styles.css" rel="stylesheet"></head>
  <body>
    <h1>Markdown Preview-o-tron 7000!</h1>
    <form id="editor">
      <textarea id="source" rows="10" cols="80"></textarea>
      <br>
      <input type="submit" value="Preview!">
    </form>
    <hr>
    <section id="preview">
    </section>
  <script type="text/javascript" src="4c4cd139c109f982db3b-bundle.js"></script></body>
</html>

Sure enough, if we open up either dev/index.html or production/index.html, the CSS is working:

Our app with CSS managed by Webpack

Nice!

And, since we're using -p for our production build, the CSS is being minified automatically.

Let's bring in a third-party CSS library to make sure that works as expected.

Third-party CSS Libraries

I don't like writing CSS. I do like using re-usable/functional CSS and not snowflake/“semantic” CSS, which is why we're going to use Tachyons. If I were to write a “What problem does it solve?” for functional CSS like Tachyons, I'd just point you to this article by Tachyons' author Adam Morse, which explains it.

First, let's bring in tachyons:

> yarn add tachyons
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.
└─ tachyons@4.9.0
Done in 2.98s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

(Refreshingly free of dependencies)

Much of the styling we've added is pretty simple stuff, so we'll let Tachyons handle all that for us. We'll leave the font setting in css/styles.css just to demonstrate that we can merge our styles with Tachyons'. Replace all of css/styles.css with:

html {
  font-family: avenir next, avenir, helvetica, sans-serif;
}

To bring in Tachyons to our CSS bundle we import it just like anything else:

/* start new code */
import "tachyons";
/* end new code */
import "../css/styles.css";
import markdownPreviewer from "./markdownPreviewer";

window.onload = function() {
  document.getElementById("editor").addEventListener(
      "submit",
      markdownPreviewer.attachPreviewer(
        document,    // pass in document
        "source",    // id of source textarea
        "preview")); // id of preview DOM element
};

If you run Webpack now, you'll see the size of our CSS bundle increase, due to the inclusion of Tachyons. But, let's actually use it so we can see it working.

We'll use some of Tachyons' styles on <body> to set the colors, as well as pad the UI a bit (since Tachyons includes a reset):

<body class="dark-gray bg-light-gray ph4">

We'd like our text area to look a bit nicer, so let's set it to fill the width of the body, have a 30% black border, with a slight border radius, and a bit of padding inside:

<textarea 
  id="source" 
  rows="10" 
  cols="80" 
  class="w-100 ba br2 pa2 b--black-30"></textarea>

We'd also like our preview button to be a bit fancier, so let's set that to be in a slightly washed-out green, and give it some padding and borders so it looks like a button. We'll also set it to zoom on hover, so it feels like a real app:

<input 
  type="submit" 
  value="Preview!" 
  class="grow pointer ba br3 bg-washed-green ph3 pv2">

(If you are incredulous at all this “mixing” of presentation and markup, please do read the linked article above. Trust me, this way of writing CSS is soooooo much better than snowflaking every single thing. But, this isn't the point. The point is we are using third party CSS with Webpack.)

All told, our template looks like so:

<!DOCTYPE html>
<html>
  <head>
    <!-- css will be inserted by webpack -->
    <!-- script tag is omitted - Webpack will provide -->
  </head>
<!-- start new code -->
  <body class="dark-gray bg-light-gray ph4">
<!-- end new code -->
    <h1>Markdown Preview-o-tron 7000!</h1>
    <form id="editor">
<!-- start new code -->
      <textarea
        id="source"
        rows="10"
        cols="80"
        class="w-100 ba br2 pa2 b--black-30"></textarea>
<!-- end new code -->
      <br>
<!-- start new code -->
      <input
        type="submit"
        value="Preview!"
        class="grow pointer ba br3 bg-washed-green ph3 pv2">
<!-- end new code -->
    </form>
    <hr>
    <section id="preview">
    </section>
  </body>
</html>

OK, now we can run Webpack:

> rm dev/*.*
> yarn webpack
yarn run v1.3.2
$ webpack $npm_package_config_webpack_args
Hash: c22b619c51baaecacbea
Version: webpack 3.8.1
Time: 1664ms
     Asset       Size  Chunks             Chunk Names
 bundle.js    78.5 kB       0  [emitted]  main
styles.css     124 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.60s.

If we open up dev/index.html, we'll see our nicely styled app, courtesy of Tachyons!

Our app styled by Tachyons

Don't get too wrapped up in a) Tachyons or b) how we've styled our app The point is that we can mix a third-party CSS framework, along with our own CSS, just like we are doing with JavaScript. This demonstrates that Webpack is a full-fledged asset pipeline.

And this meets our needs as web developers.

But, our workflow is kinda slow, and we don't have sophisticated debugging tools available. Let's look at that next.