Rebecca Murphey

Last updated

Notes on setting up a JS project, circa 2016: Webpack

I'm working on a project that may or may not see the light of day: a collection of simple games written in JavaScript. The goal of the project is to help JavaScript learners understand how to break moderately complex problems into their constituent parts; in the process, the project will also show the use of modern JavaScript tooling. A secondary goal for me is to write about the process of working on the project; this post is an attempt at that.

The project is going to consist of both code and content, so, to start, I created a content/ directory and a client/ directory. I know that one of the games I want to show will be a simple number-guessing game, so I made a number-guessing/ directory. I also ran npm init to generate a package.json file. When I was done with this initial setup, this is what my files looked like:

/js-games
  /client
    /number-guessing
  /content
  package.json

I knew that I wanted to use Webpack, and I wanted to make sure I got it set up before I got too far with anything else. To verify that Webpack was working, I'd need some basic JS and HTML. I created a file client/number-guessing/index.js, and put a simple console.log('it works') inside; I also created a file client/number-guessing/index.html, and added the following:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Number Guessing</title>
</head>
<body>
<script src="./number-guessing.js"></script>
</body>
</html>

With these pieces in place, I was ready to take a stab at configuring Webpack.

Setting up Webpack #

First, I needed to install the Webpack npm module, along with the Webpack development server:

npm install --save-dev webpack webpack-dev-server

Next, I needed to create my Webpack config. Traditionally, this file goes in the root directory of a project, alongside the package.json; however, for a project like this, I felt like it made more sense for the Webpack configuration file to be with the client files, so I created client/webpack.config.js.

/js-games
  /client
    /number-guessing
      index.html
      index.js
    webpack.config.js
  /content
  package.json

I knew that I would want to create a separate bundle for each game, so I would need multiple "entry points" in my Webpack config, one for each game's index.js file. With that in mind, this was my first Webpack config:

var path = require('path');

module.exports = {
entry : {
'number-guessing' : './number-guessing/index'
},
output : {
path : path.join(__dirname, 'dist'),
filename : "[name].js"
}
};

Before I went any farther, I wanted to try it out. First, I ran cd client to move into the client/ directory. Then, from the client directory, I ran:

../node_modules/webpack/bin/webpack.js

This was the output:

-> % ../node_modules/webpack/bin/webpack.js
Hash: 3dec0922a524080dea35
Version: webpack 1.12.11
Time: 41ms
             Asset     Size  Chunks             Chunk Names
number-guessing.js  1.41 kB       0  [emitted]  number-guessing
   [0] ./number-guessing/index.js 25 bytes {0} [built]

Seems good. I opened the file client/dist/number-guessing.js and inspected it; after the Webpack loader code, at the very end of the file I saw my console.log statement. My simple file had been built as expected; later, I could use that file to load other modules, and configure Webpack to Uglify the output, transpile ES6 code to ES5, and more.

Setting up the Webpack dev server #

Next, I wanted to set up the Webpack development server. This server serves static files from your filesystem, and also watches your JS files, updating the build whenever there are changes. I wanted to use it to serve the HTML file, which would in turn load the built version of my JS.

Still in the client directory, I ran:

../node_modules/webpack-dev-server/bin/webpack-dev-server.js

This was the output:

-> % ../node_modules/webpack-dev-server/bin/webpack-dev-server.js
http://localhost:8080/webpack-dev-server/
webpack result is served from /
content is served from /Users/rmurphey/personal/js-games/client
Hash: 3dec0922a524080dea35
Version: webpack 1.12.11
Time: 71ms
             Asset     Size  Chunks             Chunk Names
number-guessing.js  1.41 kB       0  [emitted]  number-guessing
chunk    {0} number-guessing.js (number-guessing) 25 bytes [rendered]
    [0] ./number-guessing/index.js 25 bytes {0} [built]
webpack: bundle is now VALID.

I opened the URL from the output (http://localhost:8080/webpack-dev-server/), which presented me with a simple UI provided by the Webpack server. I clicked on the number-guessing link, and when the page loaded, I saw my message in the console. However, the URL was still http://localhost:8080/webpack-dev-server/, and my actual HTML wasn't being loaded.

I wanted to be able to access my test page directly, so I tried navigating to http://localhost:8080/number-guessing/. It loaded, but there was now an error in the console: the page was trying to access http://localhost:8080/number-guessing/number-guessing.js, and was getting a 404 in response. It seemed that if I wanted to be able to access my test page directly, I was going to need to change the reference to my script in the HTML.

Looking back at the output from when I started the Webpack server, I saw webpack result is served from / -- this means that my "built" JavaScript was served from the server root. I tried loading http://localhost:8080/number-guessing.js and, indeed, it worked. I changed the script tag in my HTML to reflect the actual location of my built JavaScript bundle, one level up from the HTML file:

<script src="../number-guessing.js"></script>

With this change, the number-guessing HTML now worked whether I accessed it via the http://localhost:8080/number-guessing/ URL or by clicking on the link in the Webpack server UI.

Adding an npm script #

So far, I had been running Webpack commands directly. This project may eventually need a tool like gulp or grunt for task automation, but for now I figured I would just use an npm script as a shortcut for the command to start the server.

I edited my project's package.json to add a new "serve" entry to the "scripts" object:

"serve": "webpack-dev-server --config ./client/webpack.config.js",

In an npm script, I can skip providing the full path to an executable; npm knows to look in the right places to find webpack-dev-server. Since my config was in a non-standard location, I had to pass its location to the command.

When I ran npm run serve, this was the output:

-> % npm run serve

> [email protected] serve /Users/rmurphey/personal/js-games
> webpack-dev-server --config ./client/webpack.config.js

Hash: b74b27e56bc0a032a890
Version: webpack 1.12.11
Time: 28ms

ERROR in Entry module not found: Error: Cannot resolve 'file' or 'directory' ./number-guessing/index in /Users/rmurphey/personal/js-games
webpack: bundle is now VALID.
http://localhost:8080/webpack-dev-server/
webpack result is served from /
content is served from /Users/rmurphey/personal/js-games

Though Webpack said my bundle was "VALID", the error on the line before made clear that something was wrong: Webpack was looking for the client/number-guessing/index.js file in the wrong place: in number-guessing/index.js instead. Even though my webpack.config.js file was in the client/ directory, it seemed Webpack was looking for the file one level up, where package.json was. Even running npm run serve from inside the client/ directory didn't change this.

To address this, I needed to tell Webpack where to start its search, using the context configuration option. I modified my webpack.config.js to add a context:

var path = require('path');

module.exports = {
context : __dirname,
entry : {
'number-guessing' : './number-guessing/index'
},
output : {
path : path.join(__dirname, 'dist'),
filename : "[name].js"
}
};

With this change, the build was working, but now when I opened the server in the browser, I saw that my project's root directory was being served -- I only wanted the content/ directory to be served. Fixing this required another change to my Webpack config, to configure the dev server to use a different directory as its "content base":

var path = require('path');

module.exports = {
context : __dirname,
entry : {
'number-guessing' : './number-guessing/index'
},
output : {
path : path.join(__dirname, 'dist'),
filename : "[name].js"
},
devServer : {
contentBase : __dirname
}
};

With this change, my npm script was now showing the same thing I was seeing earlier, when I ran the command to start the dev server directly.

I made three more small changes: adding the --hot option, to enable hot module replacement; adding the --open option, to automatically open a browser to the dev server whenever I run it; and adding the --inline option to automatically reload the page when I make changes to the JavaScript:

"serve": "webpack-dev-server --hot --open --inline --config ./client/webpack.config.js"

Lastly, I added a script to generate a build:

"build": "webpack --config ./client/webpack.config.js"

Closing Thoughts #

Setting this all up took less time than writing this post, but that's due largely to the fact that I've done this kind of setup several times before. I knew what I needed, and past experience gave me good instincts when something wasn't working quite like I hoped. The Webpack docs are pretty good if you already know what you're doing, but I can imagine they're painfully opaque if you don't.

If you don't want to use one of the many boilerplates that exist, then my main advice would be: "baby steps." Get a tiny thing working, and commit that; then move on to the next tiny thing. It's the process I followed in the setup I outlined above, and it helped me have confidence each step of the way.

You can see the code as of this post here.

Read more posts in the archive.