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, but being able to rely on our code working in lots of browsers that don't support it.

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 -D
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
info This module is OPTIONAL, you can safely ignore this error
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ @babel/core@7.8.3
info All dependencies
└─ @babel/core@7.8.3
Done in 8.26s.
warning Pattern ["@babel/core@^7.8.3"] is trying to unpack in the same destination "/Users/davec/Library/Caches/Yarn/v6/npm-@babel-core-7.8.3-30b0ebb4dd1585de6923a0b4d179e0b9f5d82941-integrity/node_modules/@babel/core" as pattern ["@babel/core@^7.1.0","@babel/core@^7.1.0"]. This could result in non-deterministic behavior, skipping.
warning Error running install script for optional dependency: "/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments: 
Directory: /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
Output:
gyp info it worked if it ends with ok
gyp info using node-gyp@3.8.0
gyp info using node@12.3.0 | darwin | x64
gyp info spawn /usr/bin/python
gyp info spawn args [
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.node-gyp/12.3.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/davec/.node-gyp/12.3.0',
gyp info spawn args   '-Dnode_gyp_dir=/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=/Users/davec/.node-gyp/12.3.0/<(target_arch)/node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents',
gyp info spawn args   '-Dnode_engine=v8',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.'
gyp info spawn args ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
  SOLINK_MODULE(target) Release/.node
  CXX(target) Release/obj.target/fse/fsevents.o
In file included from ../fsevents.cc:6:
In file included from ../../nan/nan.h:54:
/Users/davec/.node-gyp/12.3.0/include/node/node.h:107:12: fatal error: 'util-inl.h' file not found
#  include <util-inl.h>
           ^~~~~~~~~~~~
1 error generated.
make: *** [Release/obj.target/fse/fsevents.o] Error 1
gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:200:13)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Darwin 18.7.0
gyp ERR! command \"/Users/davec/.asdf/installs/nodejs/12.3.0/bin/node\" \"/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\"
gyp ERR! cwd /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
gyp ERR! node -v v12.3.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok"

Yes, that is an @ sign which is a scoped package and more and more big frameworks are using it to group their submodules.

We'll also need the Babel loader for Webpack:

> yarn add babel-loader -D
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
info This module is OPTIONAL, you can safely ignore this error
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ babel-loader@8.0.6
info All dependencies
└─ babel-loader@8.0.6
Done in 4.38s.
warning Error running install script for optional dependency: "/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments: 
Directory: /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
Output:
gyp info it worked if it ends with ok
gyp info using node-gyp@3.8.0
gyp info using node@12.3.0 | darwin | x64
gyp info spawn /usr/bin/python
gyp info spawn args [
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.node-gyp/12.3.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/davec/.node-gyp/12.3.0',
gyp info spawn args   '-Dnode_gyp_dir=/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=/Users/davec/.node-gyp/12.3.0/<(target_arch)/node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents',
gyp info spawn args   '-Dnode_engine=v8',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.'
gyp info spawn args ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
  SOLINK_MODULE(target) Release/.node
  CXX(target) Release/obj.target/fse/fsevents.o
In file included from ../fsevents.cc:6:
In file included from ../../nan/nan.h:54:
/Users/davec/.node-gyp/12.3.0/include/node/node.h:107:12: fatal error: 'util-inl.h' file not found
#  include <util-inl.h>
           ^~~~~~~~~~~~
1 error generated.
make: *** [Release/obj.target/fse/fsevents.o] Error 1
gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:200:13)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Darwin 18.7.0
gyp ERR! command \"/Users/davec/.asdf/installs/nodejs/12.3.0/bin/node\" \"/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\"
gyp ERR! cwd /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
gyp ERR! node -v v12.3.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok"

Babel should process all JavaScript, so we'll configure the loader in config/webpack.common.config.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 MiniCssExtractPlugin = require('mini-css-extract-plugin');

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

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 -D
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
info This module is OPTIONAL, you can safely ignore this error
success Saved lockfile.
success Saved 64 new dependencies.
info Direct dependencies
└─ @babel/preset-env@7.8.3
info All dependencies
├─ @babel/compat-data@7.8.1
├─ @babel/helper-builder-binary-assignment-operator-visitor@7.8.3
├─ @babel/helper-call-delegate@7.8.3
├─ @babel/helper-compilation-targets@7.8.3
├─ @babel/helper-define-map@7.8.3
├─ @babel/helper-explode-assignable-expression@7.8.3
├─ @babel/helper-member-expression-to-functions@7.8.3
├─ @babel/helper-wrap-function@7.8.3
├─ @babel/plugin-proposal-async-generator-functions@7.8.3
├─ @babel/plugin-proposal-dynamic-import@7.8.3
├─ @babel/plugin-proposal-json-strings@7.8.3
├─ @babel/plugin-proposal-nullish-coalescing-operator@7.8.3
├─ @babel/plugin-proposal-object-rest-spread@7.8.3
├─ @babel/plugin-proposal-optional-catch-binding@7.8.3
├─ @babel/plugin-proposal-optional-chaining@7.8.3
├─ @babel/plugin-proposal-unicode-property-regex@7.8.3
├─ @babel/plugin-syntax-top-level-await@7.8.3
├─ @babel/plugin-transform-arrow-functions@7.8.3
├─ @babel/plugin-transform-async-to-generator@7.8.3
├─ @babel/plugin-transform-block-scoped-functions@7.8.3
├─ @babel/plugin-transform-block-scoping@7.8.3
├─ @babel/plugin-transform-classes@7.8.3
├─ @babel/plugin-transform-computed-properties@7.8.3
├─ @babel/plugin-transform-destructuring@7.8.3
├─ @babel/plugin-transform-dotall-regex@7.8.3
├─ @babel/plugin-transform-duplicate-keys@7.8.3
├─ @babel/plugin-transform-exponentiation-operator@7.8.3
├─ @babel/plugin-transform-for-of@7.8.3
├─ @babel/plugin-transform-function-name@7.8.3
├─ @babel/plugin-transform-literals@7.8.3
├─ @babel/plugin-transform-member-expression-literals@7.8.3
├─ @babel/plugin-transform-modules-amd@7.8.3
├─ @babel/plugin-transform-modules-commonjs@7.8.3
├─ @babel/plugin-transform-modules-systemjs@7.8.3
├─ @babel/plugin-transform-modules-umd@7.8.3
├─ @babel/plugin-transform-named-capturing-groups-regex@7.8.3
├─ @babel/plugin-transform-new-target@7.8.3
├─ @babel/plugin-transform-object-super@7.8.3
├─ @babel/plugin-transform-parameters@7.8.3
├─ @babel/plugin-transform-property-literals@7.8.3
├─ @babel/plugin-transform-regenerator@7.8.3
├─ @babel/plugin-transform-reserved-words@7.8.3
├─ @babel/plugin-transform-shorthand-properties@7.8.3
├─ @babel/plugin-transform-spread@7.8.3
├─ @babel/plugin-transform-sticky-regex@7.8.3
├─ @babel/plugin-transform-template-literals@7.8.3
├─ @babel/plugin-transform-typeof-symbol@7.8.3
├─ @babel/plugin-transform-unicode-regex@7.8.3
├─ @babel/preset-env@7.8.3
├─ browserslist@4.8.5
├─ caniuse-lite@1.0.30001022
├─ core-js-compat@3.6.4
├─ electron-to-chromium@1.3.338
├─ node-releases@1.1.47
├─ private@0.1.8
├─ regenerate-unicode-properties@8.1.0
├─ regenerator-transform@0.14.1
├─ regexpu-core@4.6.0
├─ regjsgen@0.5.1
├─ regjsparser@0.6.2
├─ unicode-canonical-property-names-ecmascript@1.0.4
├─ unicode-match-property-ecmascript@1.0.4
├─ unicode-match-property-value-ecmascript@1.1.0
└─ unicode-property-aliases-ecmascript@1.0.5
Done in 5.68s.
warning Error running install script for optional dependency: "/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments: 
Directory: /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
Output:
gyp info it worked if it ends with ok
gyp info using node-gyp@3.8.0
gyp info using node@12.3.0 | darwin | x64
gyp info spawn /usr/bin/python
gyp info spawn args [
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.node-gyp/12.3.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/davec/.node-gyp/12.3.0',
gyp info spawn args   '-Dnode_gyp_dir=/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=/Users/davec/.node-gyp/12.3.0/<(target_arch)/node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents',
gyp info spawn args   '-Dnode_engine=v8',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.'
gyp info spawn args ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
  SOLINK_MODULE(target) Release/.node
  CXX(target) Release/obj.target/fse/fsevents.o
In file included from ../fsevents.cc:6:
In file included from ../../nan/nan.h:54:
/Users/davec/.node-gyp/12.3.0/include/node/node.h:107:12: fatal error: 'util-inl.h' file not found
#  include <util-inl.h>
           ^~~~~~~~~~~~
1 error generated.
make: *** [Release/obj.target/fse/fsevents.o] Error 1
gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:200:13)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Darwin 18.7.0
gyp ERR! command \"/Users/davec/.asdf/installs/nodejs/12.3.0/bin/node\" \"/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\"
gyp ERR! cwd /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
gyp ERR! node -v v12.3.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok"

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": ["@babel/preset-env"]
}

And now, re-run Webpack:

> yarn webpack
yarn run v1.21.1
$ webpack $npm_package_config_webpack_args
Hash: 2f1c8af0de453464338e
Version: webpack 4.41.5
Time: 1018ms
Built at: 01/21/2020 6:16:32 PM
     Asset       Size  Chunks             Chunk Names
 bundle.js    210 KiB       0  [emitted]  main
index.html  833 bytes          [emitted]  
styles.css    327 KiB       0  [emitted]  main
Entrypoint main = styles.css bundle.js
[0] ./js/index.js 382 bytes {0} [built]
[2] ./css/styles.css 39 bytes {0} [built]
[3] ./js/markdownPreviewer.js 397 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 303 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.67s.

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

Our test files are pretty basic, so there's nothing exciting about using arrows as those will work with the version of Node we are using. Instead, let's use an experimental feature called optional chaining. As of this writing, it's not supported by many browser or by Node. It will also require adding a plugin to Babel to support, so this should be a good learning.

The way optional chaining works is to allow you to safely dereference a deep object without worrying about undefined:

const o = {
  foo: {
    bar: "blah"
   }
};

console.log(o?.foo?.bar); # => "blah"

We'll add this code as a test:

import markdownPreviewer from "../js/markdownPreviewer"

describe("markdownPreviewer", () => {
  it("should exist", () => {
    expect(markdownPreviewer).toBeDefined();
  });
  it("should allow deep references", () => {
    const o = {
      foo: {
        bar: "blah"
       }
    };

    expect((o?.foo?.bar)).toBe("blah");
  });
});

This should fail with a huge stack about the syntax error we created:

> yarn test
yarn run v1.21.1
$ yarn webpack:test && yarn jest
$ webpack $npm_package_config_webpack_args --env=test
Hash: 3bbc80361111b21c0f35
Version: webpack 4.41.5
Time: 927ms
Built at: 01/21/2020 6:16:35 PM
         Asset       Size  Chunks             Chunk Names
bundle.test.js    382 KiB       0  [emitted]  main
    index.html  797 bytes          [emitted]  
Entrypoint main = bundle.test.js
 [0] multi ./node_modules/gensync/test/index.test.js ./test/canary.test.js ./test/markdownPreviewer.test.js 52 bytes {0} [built]
[17] (webpack)/buildin/global.js 472 bytes {0} [built]
[52] ./test/canary.test.js 108 bytes {0} [built]
[53] ./test/markdownPreviewer.test.js 459 bytes {0} [built]
[54] ./js/markdownPreviewer.js 397 bytes {0} [built]
    + 52 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
$ jest test/bundle.test.js
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
FAIL test/bundle.test.js
  ● Test suite failed to run

    ReferenceError: regeneratorRuntime is not defined

      at buildOperation (test/webpack:/node_modules/gensync/index.js:201:1)
      at Object.<anonymous> (test/webpack:/node_modules/gensync/index.js:26:1)
      at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
      at Object.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:4:17)
      at Object.<anonymous> (test/bundle.test.js:1441:6)
      at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
      at Object.<anonymous> (test/bundle.test.js:275:3)
      at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
      at test/webpack:/webpack/bootstrap:83:1
      at Object.<anonymous> (test/bundle.test.js:266:3)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.17s, estimated 2s
Ran all test suites matching /test\/bundle.test.js/i.
error Command failed with exit code 1.
error Command failed with exit code 1.

To allow using this new feature, we'll add the babel plugin @babel/plugin-proposal-optional-chaining to our project:

> yarn add @babel/plugin-proposal-optional-chaining -D
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
info This module is OPTIONAL, you can safely ignore this error
success Saved 1 new dependency.
info Direct dependencies
└─ @babel/plugin-proposal-optional-chaining@7.8.3
info All dependencies
└─ @babel/plugin-proposal-optional-chaining@7.8.3
Done in 4.33s.
warning Error running install script for optional dependency: "/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments: 
Directory: /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
Output:
gyp info it worked if it ends with ok
gyp info using node-gyp@3.8.0
gyp info using node@12.3.0 | darwin | x64
gyp info spawn /usr/bin/python
gyp info spawn args [
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.node-gyp/12.3.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/davec/.node-gyp/12.3.0',
gyp info spawn args   '-Dnode_gyp_dir=/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=/Users/davec/.node-gyp/12.3.0/<(target_arch)/node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents',
gyp info spawn args   '-Dnode_engine=v8',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.'
gyp info spawn args ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
  SOLINK_MODULE(target) Release/.node
  CXX(target) Release/obj.target/fse/fsevents.o
In file included from ../fsevents.cc:6:
In file included from ../../nan/nan.h:54:
/Users/davec/.node-gyp/12.3.0/include/node/node.h:107:12: fatal error: 'util-inl.h' file not found
#  include <util-inl.h>
           ^~~~~~~~~~~~
1 error generated.
make: *** [Release/obj.target/fse/fsevents.o] Error 1
gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:200:13)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Darwin 18.7.0
gyp ERR! command \"/Users/davec/.asdf/installs/nodejs/12.3.0/bin/node\" \"/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\"
gyp ERR! cwd /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
gyp ERR! node -v v12.3.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok"

To use this, we'll add the plugins: key in our .babelrc file:

{
  "presets": ["@babel/preset-env"],
  "plugins": ["@babel/plugin-proposal-optional-chaining"]
}

Now, when we re-run our tests, they…still don't pass.

> yarn test
yarn run v1.21.1
$ yarn webpack:test && yarn jest
$ webpack $npm_package_config_webpack_args --env=test
Hash: 3bbc80361111b21c0f35
Version: webpack 4.41.5
Time: 934ms
Built at: 01/21/2020 6:16:44 PM
         Asset       Size  Chunks             Chunk Names
bundle.test.js    382 KiB       0  [emitted]  main
    index.html  797 bytes          [emitted]  
Entrypoint main = bundle.test.js
 [0] multi ./node_modules/gensync/test/index.test.js ./test/canary.test.js ./test/markdownPreviewer.test.js 52 bytes {0} [built]
[17] (webpack)/buildin/global.js 472 bytes {0} [built]
[52] ./test/canary.test.js 108 bytes {0} [built]
[53] ./test/markdownPreviewer.test.js 459 bytes {0} [built]
[54] ./js/markdownPreviewer.js 397 bytes {0} [built]
    + 52 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
$ jest test/bundle.test.js
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
FAIL test/bundle.test.js
  ● Test suite failed to run

    ReferenceError: regeneratorRuntime is not defined

      at buildOperation (test/webpack:/node_modules/gensync/index.js:201:1)
      at Object.<anonymous> (test/webpack:/node_modules/gensync/index.js:26:1)
      at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
      at Object.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:4:17)
      at Object.<anonymous> (test/bundle.test.js:1441:6)
      at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
      at Object.<anonymous> (test/bundle.test.js:275:3)
      at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
      at test/webpack:/webpack/bootstrap:83:1
      at Object.<anonymous> (test/bundle.test.js:266:3)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.166s
Ran all test suites matching /test\/bundle.test.js/i.
error Command failed with exit code 1.
error Command failed with exit code 1.

Babel 7.4 includes a breaking change, because of course it does. To be honest, I don't know what the problem actually is, because the way Babel functions is entirely opaque and unobservable. There's somethig going on with polyfills, but when we dig deeper we find that the way we are organizing tests is executing tests in other packages in node_modules. UGH.

Let's just get through this. Our .babelrc actually requires a bunch of configuration to the preset, thus making it not very pre-set.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": { version: 3, proposals: true }
      }
    ]
  ],
  "plugins": ["@babel/plugin-proposal-optional-chaining"]
}

We also need to now explicitly add core-js as well as a package called rengerator-runtime:

> yarn add -D core-js@3 regenerator-runtime
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
info This module is OPTIONAL, you can safely ignore this error
success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
├─ core-js@3.6.4
└─ regenerator-runtime@0.13.3
info All dependencies
├─ core-js@3.6.4
└─ regenerator-runtime@0.13.3
Done in 5.12s.
warning Error running install script for optional dependency: "/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments: 
Directory: /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
Output:
gyp info it worked if it ends with ok
gyp info using node-gyp@3.8.0
gyp info using node@12.3.0 | darwin | x64
gyp info spawn /usr/bin/python
gyp info spawn args [
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/davec/.node-gyp/12.3.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/davec/.node-gyp/12.3.0',
gyp info spawn args   '-Dnode_gyp_dir=/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=/Users/davec/.node-gyp/12.3.0/<(target_arch)/node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents',
gyp info spawn args   '-Dnode_engine=v8',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.'
gyp info spawn args ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
  SOLINK_MODULE(target) Release/.node
  CXX(target) Release/obj.target/fse/fsevents.o
In file included from ../fsevents.cc:6:
In file included from ../../nan/nan.h:54:
/Users/davec/.node-gyp/12.3.0/include/node/node.h:107:12: fatal error: 'util-inl.h' file not found
#  include <util-inl.h>
           ^~~~~~~~~~~~
1 error generated.
make: *** [Release/obj.target/fse/fsevents.o] Error 1
gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:200:13)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Darwin 18.7.0
gyp ERR! command \"/Users/davec/.asdf/installs/nodejs/12.3.0/bin/node\" \"/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\"
gyp ERR! cwd /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
gyp ERR! node -v v12.3.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok"

And now yarn test works, and inadvertently runs a bunch of other tests. But it supports the chaining syntax at least!

> yarn test
yarn run v1.21.1
$ yarn webpack:test && yarn jest
$ webpack $npm_package_config_webpack_args --env=test
Hash: 1eb40dfc4408d0db1852
Version: webpack 4.41.5
Time: 1008ms
Built at: 01/21/2020 6:16:55 PM
         Asset       Size  Chunks             Chunk Names
bundle.test.js    454 KiB       0  [emitted]  main
    index.html  797 bytes          [emitted]  
Entrypoint main = bundle.test.js
  [0] multi ./node_modules/core-js/modules/es.regexp.test.js ./node_modules/gensync/test/index.test.js ./test/canary.test.js ./test/markdownPreviewer.test.js 64 bytes {0} [built]
  [5] (webpack)/buildin/global.js 472 bytes {0} [built]
[100] ./test/canary.test.js 108 bytes {0} [built]
[101] ./test/markdownPreviewer.test.js 459 bytes {0} [built]
[102] ./js/markdownPreviewer.js 397 bytes {0} [built]
    + 100 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
$ jest test/bundle.test.js
  console.log node_modules/jest-jasmine2/build/jasmine/Env.js:502
      ● Test suite failed to run

        Returning a Promise from "describe" is not supported. Tests must be defined synchronously.
        Returning a value from "describe" will fail the test in a future version of Jest.

          2814 |   // implementation from standard node.js 'util' module
          2815 |   module.exports = function inherits(ctor, superCtor) {
        > 2816 |     ctor.super_ = superCtor
               |      ^
          2817 |     ctor.prototype = Object.create(superCtor.prototype, {
          2818 |       constructor: {
          2819 |         value: ctor,

          at addSpecsToSuite (node_modules/jest-jasmine2/build/jasmine/Env.js:504:17)
          at Suite.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:201:1)
          at Object.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:62:1)
          at Object.<anonymous> (test/bundle.test.js:2816:6)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at Object.<anonymous> (test/bundle.test.js:523:3)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at test/webpack:/webpack/bootstrap:83:1
          at Object.<anonymous> (test/bundle.test.js:512:3)


  console.log node_modules/jest-jasmine2/build/jasmine/Env.js:502
      ● Test suite failed to run

        Returning a Promise from "describe" is not supported. Tests must be defined synchronously.
        Returning a value from "describe" will fail the test in a future version of Jest.

          2814 |   // implementation from standard node.js 'util' module
          2815 |   module.exports = function inherits(ctor, superCtor) {
        > 2816 |     ctor.super_ = superCtor
               |      ^
          2817 |     ctor.prototype = Object.create(superCtor.prototype, {
          2818 |       constructor: {
          2819 |         value: ctor,

          at addSpecsToSuite (node_modules/jest-jasmine2/build/jasmine/Env.js:504:17)
          at Suite.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:221:1)
          at Object.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:62:1)
          at Object.<anonymous> (test/bundle.test.js:2816:6)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at Object.<anonymous> (test/bundle.test.js:523:3)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at test/webpack:/webpack/bootstrap:83:1
          at Object.<anonymous> (test/bundle.test.js:512:3)


  console.log node_modules/jest-jasmine2/build/jasmine/Env.js:502
      ● Test suite failed to run

        Returning a Promise from "describe" is not supported. Tests must be defined synchronously.
        Returning a value from "describe" will fail the test in a future version of Jest.

          2814 |   // implementation from standard node.js 'util' module
          2815 |   module.exports = function inherits(ctor, superCtor) {
        > 2816 |     ctor.super_ = superCtor
               |      ^
          2817 |     ctor.prototype = Object.create(superCtor.prototype, {
          2818 |       constructor: {
          2819 |         value: ctor,

          at addSpecsToSuite (node_modules/jest-jasmine2/build/jasmine/Env.js:504:17)
          at Suite.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:241:1)
          at Object.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:62:1)
          at Object.<anonymous> (test/bundle.test.js:2816:6)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at Object.<anonymous> (test/bundle.test.js:523:3)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at test/webpack:/webpack/bootstrap:83:1
          at Object.<anonymous> (test/bundle.test.js:512:3)


  console.log node_modules/jest-jasmine2/build/jasmine/Env.js:502
      ● Test suite failed to run

        Returning a Promise from "describe" is not supported. Tests must be defined synchronously.
        Returning a value from "describe" will fail the test in a future version of Jest.

          2814 |   // implementation from standard node.js 'util' module
          2815 |   module.exports = function inherits(ctor, superCtor) {
        > 2816 |     ctor.super_ = superCtor
               |      ^
          2817 |     ctor.prototype = Object.create(superCtor.prototype, {
          2818 |       constructor: {
          2819 |         value: ctor,

          at addSpecsToSuite (node_modules/jest-jasmine2/build/jasmine/Env.js:504:17)
          at Suite.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:261:1)
          at Object.<anonymous> (test/webpack:/node_modules/gensync/test/index.test.js:62:1)
          at Object.<anonymous> (test/bundle.test.js:2816:6)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at Object.<anonymous> (test/bundle.test.js:523:3)
          at __webpack_require__ (test/webpack:/webpack/bootstrap:19:1)
          at test/webpack:/webpack/bootstrap:83:1
          at Object.<anonymous> (test/bundle.test.js:512:3)


Done in 4.79s.
PASS test/bundle.test.js
  gensync({})
    option validation
      ✓ disallow async and errback handler together (2ms)
      ✓ disallow missing sync handler (1ms)
      ✓ errback callback required
    generator function metadata
      ✓ automatic naming (2ms)
      ✓ explicit naming
      ✓ default arity
      ✓ explicit arity
    'sync' handler
      ✓ success (2ms)
      ✓ failure (2ms)
    'async' handler
      ✓ success (2ms)
      ✓ failure (1ms)
    'errback' sync handler
      ✓ success (1ms)
      ✓ failure (1ms)
    'errback' async handler
      ✓ success (7ms)
      ✓ failure (5ms)
  gensync(function* () {})
    ✓ sync throw before body (2ms)
    ✓ sync throw inside body (1ms)
    ✓ async throw inside body (5ms)
    ✓ error inside body (2ms)
    ✓ successful return value (1ms)
    ✓ successful final value
    ✓ yield unexpected object (1ms)
    ✓ yield suspend yield (1ms)
    ✓ yield suspend return
  gensync.all()
    ✓ success (3ms)
    ✓ error first
    ✓ error last (1ms)
  gensync.race()
    ✓ success (1ms)
    ✓ error (1ms)
  canary
    ✓ can run a test
  markdownPreviewer
    ✓ should exist (1ms)
    ✓ should allow deep references

Test Suites: 1 passed, 1 total
Tests:       32 passed, 32 total
Snapshots:   0 total
Time:        1.555s
Ran all test suites matching /test\/bundle.test.js/i.

From here, you can configure Babel in a ton of ways. Notably, you will need to use Babel if you want to use React and write JSX files.

Where We Are

We started with nothing, and we gradually changed our configuration to solve real problems. In the end, what we have isn't bad! We can write modular JavaScript, use third party libraries and CSS, get useful stack traces, run unit tests, and bundle for production. We can also use bleeding edge features of JavaScript while ensuring browser compatibility. That's pretty good!

And, we didn't have to write a ton of configuration:

> wc -l webpack.config.js config/webpack.*
6 webpack.config.js
      30 config/webpack.common.config.js
      17 config/webpack.dev.config.js
      21 config/webpack.production.config.js
      21 config/webpack.test.config.js
      95 total

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

My hope is you have taken a few things away from this. First, I hope you understand Webpack a bit better and can navigate it's basic features and learn what works and why. Second, and more important, I hope you feel confident working with tools that just aren't very well designed for your needs. I hope you feel validated when you feel frustrated by cryptic error messages that it's not just you. I hope you feel like running to Google or browsing GitHub issues to find out how to use your tools is perfectly normal.

Webpack clearly values moving fast and delivering features over ergonomics and predictability. There's nothing inherently wrong with that tradeoff, but it means you have to shoulder the burden when things dont' work the first time, and they rarely do. Hopefully, you've seen that by taking small steps each time and changing very little in each step will allow you to understand what has broken this time and how you might fix it.

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.