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.13.0
$ webpack $npm_package_config_webpack_args
Hash: 07a0a07cee934bceec64
Version: webpack 4.29.6
Time: 349ms
Built at: 2019-03-30 10:25:00
     Asset       Size  Chunks             Chunk Names
 bundle.js   80.3 KiB       0  [emitted]  main
index.html  520 bytes          [emitted]  
Entrypoint main = bundle.js
[0] ./js/index.js 334 bytes {0} [built]
[1] ./js/markdownPreviewer.js 381 bytes {0} [built]
    + 6 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 678 bytes {0} [built]
    [2] (webpack)/buildin/global.js 472 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 1 hidden module
Done in 1.00s.

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 -D css-loader
yarn add v1.13.0
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 11 new dependencies.
info Direct dependencies
└─ css-loader@2.1.1
info All dependencies
├─ css-loader@2.1.1
├─ cssesc@3.0.0
├─ icss-replace-symbols@1.1.0
├─ icss-utils@4.1.0
├─ indexes-of@1.0.1
├─ postcss-modules-extract-imports@2.0.0
├─ postcss-modules-local-by-default@2.0.6
├─ postcss-modules-scope@2.1.0
├─ postcss-modules-values@2.0.0
├─ postcss-value-parser@3.3.1
└─ uniq@1.0.1
Done in 2.87s.
warning "jest > jest-cli > @jest/core > jest-resolve-dependencies@24.5.0" has unmet peer dependency "jest-resolve@^24.1.0".
warning "jest > jest-cli > jest-config > jest-resolve@24.5.0" has unmet peer dependency "jest-haste-map@^24.0.0".

(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 config/webpack.common.config.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',
  mode: 'none'
};

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.13.0
$ webpack $npm_package_config_webpack_args
Hash: f9891ce78f281c2ed105
Version: webpack 4.29.6
Time: 754ms
Built at: 2019-03-30 10:25:10
     Asset       Size  Chunks             Chunk Names
 bundle.js   83.4 KiB       0  [emitted]  main
index.html  520 bytes          [emitted]  
Entrypoint main = bundle.js
[0] ./js/index.js 402 bytes {0} [built]
[1] ./css/styles.css 321 bytes {0} [built]
[3] ./js/markdownPreviewer.js 381 bytes {0} [built]
    + 7 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 678 bytes {0} [built]
    [2] (webpack)/buildin/global.js 472 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 1 hidden module
Done in 1.42s.

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 MiniCssExtractPlugin.

> yarn add -D mini-css-extract-plugin
yarn add v1.13.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.
info Direct dependencies
└─ mini-css-extract-plugin@0.5.0
info All dependencies
└─ mini-css-extract-plugin@0.5.0
Done in 2.40s.
warning "jest > jest-cli > @jest/core > jest-resolve-dependencies@24.5.0" has unmet peer dependency "jest-resolve@^24.1.0".
warning "jest > jest-cli > jest-config > jest-resolve@24.5.0" has unmet peer dependency "jest-haste-map@^24.0.0".

To use this plugin requires two bits of configuration. The first is to configure it as a loader by calling extract on it. The next is to configure it as a plugin by creating a new instance of it. It supports the filename configuration format, so we can put [contenthash] in the filename to get hashing.

First, we'll use the loader, which is set up in our common configuration:

const HtmlPlugin = require("html-webpack-plugin");
/* start new code */
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
/* end new code */

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

Now, to configure the plugin. We need different configurations for production and development.

Here's what config/webpack.dev.config.js will look like

const path         = require('path');
const Merge        = require('webpack-merge');
const CommonConfig = require('./webpack.common.config.js');
/* start new code */
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
/* end new code */

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

And, for config/webpack.production.config.js:

const path         = require('path');
const Merge        = require('webpack-merge');
const CommonConfig = require('./webpack.common.config.js');
/* start new code */
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
/* end new code */

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

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.13.0
$ webpack $npm_package_config_webpack_args
Hash: 54434fd57aea8b3c2a7a
Version: webpack 4.29.6
Time: 463ms
Built at: 2019-03-30 10:25:14
     Asset       Size  Chunks             Chunk Names
 bundle.js   80.7 KiB       0  [emitted]  main
index.html  560 bytes          [emitted]  
styles.css  173 bytes       0  [emitted]  main
Entrypoint main = styles.css bundle.js
[0] ./js/index.js 402 bytes {0} [built]
[1] ./css/styles.css 39 bytes {0} [built]
[2] ./js/markdownPreviewer.js 381 bytes {0} [built]
    + 7 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 677 bytes {0} [built]
    [2] (webpack)/buildin/global.js 472 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 1 hidden module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!css/styles.css:
    Entrypoint mini-css-extract-plugin = *
    [0] ./node_modules/css-loader/dist/cjs.js!./css/styles.css 321 bytes {0} [built]
        + 1 hidden module
Done in 1.15s.
> 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 webpack:production
yarn run v1.13.0
$ webpack $npm_package_config_webpack_args --env=production
Hash: b5bec5220d1e44987e9b
Version: webpack 4.29.6
Time: 1102ms
Built at: 2019-03-30 10:25:17
                          Asset       Size  Chunks             Chunk Names
 bundle-4470b9e28595fbc946fb.js   26.8 KiB       0  [emitted]  main
                     index.html  602 bytes          [emitted]  
styles-e0fa49d1a1e7760289be.css  173 bytes       0  [emitted]  main
Entrypoint main = styles-e0fa49d1a1e7760289be.css bundle-4470b9e28595fbc946fb.js
[0] ./js/index.js 402 bytes {0} [built]
[1] ./css/styles.css 39 bytes {0} [built]
[2] ./js/markdownPreviewer.js 381 bytes {0} [built]
    + 7 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 677 bytes {0} [built]
    [2] (webpack)/buildin/global.js 472 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 1 hidden module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!css/styles.css:
    Entrypoint mini-css-extract-plugin = *
    [0] ./node_modules/css-loader/dist/cjs.js!./css/styles.css 321 bytes {0} [built]
        + 1 hidden module
Done in 1.92s.
> ls production/*.css
production/styles-e0fa49d1a1e7760289be.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="styles-e0fa49d1a1e7760289be.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-4470b9e28595fbc946fb.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!

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

Third Party Modules that Have CSS

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.13.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.
info Direct dependencies
└─ tachyons@4.11.1
info All dependencies
└─ tachyons@4.11.1
Done in 2.55s.
warning "jest > jest-cli > @jest/core > jest-resolve-dependencies@24.5.0" has unmet peer dependency "jest-resolve@^24.1.0".
warning "jest > jest-cli > jest-config > jest-resolve@24.5.0" has unmet peer dependency "jest-haste-map@^24.0.0".

(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.13.0
$ webpack $npm_package_config_webpack_args
Hash: 3df951cd5a52cedbabb2
Version: webpack 4.29.6
Time: 567ms
Built at: 2019-03-30 10:25:26
     Asset       Size  Chunks             Chunk Names
 bundle.js     81 KiB       0  [emitted]  main
index.html  833 bytes          [emitted]  
styles.css    112 KiB       0  [emitted]  main
Entrypoint main = styles.css bundle.js
[0] ./js/index.js 421 bytes {0} [built]
[2] ./css/styles.css 39 bytes {0} [built]
[3] ./js/markdownPreviewer.js 381 bytes {0} [built]
    + 9 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 961 bytes {0} [built]
    [2] (webpack)/buildin/global.js 472 bytes {0} [built]
    [3] (webpack)/buildin/module.js 497 bytes {0} [built]
        + 1 hidden module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!css/styles.css:
    Entrypoint mini-css-extract-plugin = *
    [0] ./node_modules/css-loader/dist/cjs.js!./css/styles.css 210 bytes {0} [built]
        + 1 hidden module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/tachyons/css/tachyons.css:
    Entrypoint mini-css-extract-plugin = *
       2 modules
Done in 1.25s.

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.