Webpack from Nothing

Third Party Libraries

This should be easy! We have JavaScript code that we're going to write, but we want to use some existing libraries to help us do that.

In the World Before WebPack™, you'd often add the library's CDN-hosted URL to a <script> tag and be on your way.

That would work because the library would dump itself into the global namespace. That's not what we want to do for a few reasons.

First, this means that every library everywhere has to agree on a unique set of names so as to not squash each other. That is difficult.

Secondly, it means that we have no control over when or how these libraries are loaded, which becomes important for writing tests.

Thirdly, this isn't how third-party JavaScript libraries are built - they mostly assume you are using a module bundler like Webpack.

Point is, dumping into global namespace bad. I promise not to get on my soapbox about this again, so let's get to it. Let's add a library to our project!

But wait, we don't really have a project, yet. Let's make one real quick.

A Real Project: Markdown Previewing

Despite how awesome our console.log'ing supersystem is, we should work on something more interesting. The simplest thing I could think of is a markdown previewer. There's a markdown module we can use, so that will be our third-party library!

First, we'll add it to our package.json file using yarn add:

> yarn add markdown
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
└─ markdown@0.5.0
info All dependencies
├─ markdown@0.5.0
└─ nopt@2.1.2
Done in 2.97s.
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"

This will also run yarn install which will download the markdown package to node_modules.

Let's create our HTML first by replacing dist/index.html with the following:

<!DOCTYPE html>
<html>
  <head>
    <script src="bundle.js"></script>
  </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>

The app will work by listening to a submit event on that form and, when the button is pressed, render a preview of the markdown. Rather than put this all in line, we'll make our own module that constructs this function.

The function will use the markdown library to do the actual rendering, so the module we need to write should take some inputs—where to find the source and where to render the preview, and produce a side-effect, namely the formatted markdown gets written to the part of the DOM where the preview should go.

Our entry point is js/index.js, so let's create that like so:

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
};

markdownPreviewer is a file we'll create in a moment. Hopefully you know enough JavaScript to see what's going on (we're passing in document so that our code doesn't have to depend on global state, which will help write tests later).

Now, let's create js/markdownPreviewer.js, which does all the work.

We need to import our markdown library, however it's in node_modules, so how does that work?

Actually Importing Third-party Libraries

The answer is: finding code in node_modules Just Works™. This is one of the scant defaults Webpack provides.

Now, before we write import markdown from "markdown"; we need to learn a bit more about how import works.

The markdown package exports a structure like so;

{
  "markdown": {
    "toHTML": function() { ... }
  }
}

Meaning, if we use import the way we've seen before, we'd need to write markdown.markdown.toHTML. Yuck. Fortunately, import allows you to pluck symbols out of the exported object by using curly braces:

import { markdown } from "markdown";

This allows us to write markdown.toHTML, which is better.

With that said, here's how js/markdownPreviewer.js should look:

import { markdown } from "markdown";

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

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

export default {
  attachPreviewer: attachPreviewer
}

What's up with export default? Writing import attachPreviewer from "./markdownPreviewer"; is asking Webpack (or whoever) to import the default exported thing. Using export default sets that thing, which is handy when you are just exporting one function (though, let's be honest, it's not handy, because it's another special form to have to understand and remember to do, all to save a few scant keystrokes and obscure intent).

Let's package everything up and see if it works.

> yarn webpack
yarn run v1.21.1
$ webpack --config webpack.config.js --display-error-details
Hash: 8f740e9980f30a8938d5
Version: webpack 4.41.5
Time: 173ms
Built at: 01/21/2020 5:45:34 PM
    Asset      Size  Chunks             Chunk Names
bundle.js  80.3 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./js/index.js 334 bytes {0} [built]
[1] ./js/markdownPreviewer.js 381 bytes {0} [built]
    + 6 hidden modules
Done in 0.62s.

If we open up dist/index.html, we should see our UI:

Our App's main UI

And, if you type in some markdown and hit the submit button, voila it's rendered inline by our markdown library:

Our app rendering markdown

Who would've thought it takes 1,000 words to talk about using third party libraries, but this is JavaScript and we should be thankful Webpack exists to make up for the language's deficiencies.

We mentioned earlier that we passed document into our attachPreviewer function to facilitate testing. We should probably look into testing at this point because a) I get nervous writing untested code and b) things we'll learn later, like CSS and ES6, will definitely break things and we want to make sure we can continue to run tests as we use more sophisticated features of Webpack.