Skip to content

Setting Up A Test-Driven React Project From Scratch - Part 1: webpack

Posted on:July 22, 2015

You will learn how to scaffold a React project from scratch with Karma and webpack.

I’ve tried to make it as unopinionated as possible. Hopefully, once you understand the rationale behind each of the moving parts, you can roll your own or use one of the many templates available elsewhere.

At the end of this part, you would have set up webpack with:

Let’s create a simple project that allows users to create reviews of beer and populate a list. We’ll call it Beerist.

Let’s create a project directory:

mkdir beerist && cd beerist

Initialize a package.json by filling in the fields as appropriate:

npm init
{
  "name": "beerist",
  "version": "1.0.0",
  "description": "A beer review site",
  "main": "index.js",
  "dependencies": {},
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Lau Siaw Young",
  "license": "ISC"
}

Create a src directory that will hold all our source files. Within src, create a js directory and a index.js that will act as an entry point for the app. Our directory now looks like this:

├── package.json
├── src
│   └── js
│       └── index.js
└── webpack.config.js

In index.js, let’s just write a simple one-liner for the purposes of testing:

console.log("index.js is loaded!");

webpack

webpack was born as a agnostic module loader. Its Motivation page introduces itself quite nicely.

Since its inception, webpack has grown to encompass some elements of build tooling as well, which greatly reduces the need for Gulp or Grunt.

We’ll install webpack1 first so that we can write our React files in ES6 (or ES2015, or ES6+, or ES7, or whatever they call it nowadays) from the get-go and have webpack run the files through Babel to transpile our ES6 down to ES5.

We’ll also install webpack’s dev server so that we can serve the files locally using a built-in Express server and sockets (for hotloading):

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

This creates a node_modules directory if it hasn’t previously been created, and downloads the latest version of all the modules specified from npm. --save-dev saves it as a development dependency entry within your package.json2:

"devDependencies": {
  "node-libs-browser": "^0.5.2",
  "webpack": "^1.10.1",
  "webpack-dev-server": "^1.10.1"
}

Create a file called webpack.config.js, which tells webpack what to do:

var webpack = require("webpack");
var path = require("path");
 
module.exports = {
  entry: [path.resolve(__dirname, "./src/js/index.js")],
 
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
};

The config starts off by requiring webpack and path, which is a built-in Node library that provides utility methods for manipulating file path strings. In particular, we use path.resolve together with the “global” variable __dirname to resolve relative file paths to absolute ones.

Now how do we run webpack and/or its dev server? If you’ve installed webpack and/or webpack-dev-server globally, you can simply run webpack or webpack-dev-server on the command line to fire it up. Else, you can call it from an npm script inside package.json3:

"scripts": {
  "start": "webpack-dev-server",
  "build": "webpack"
},

Now, we can run npm start or npm run build (check out the difference between the two for yourself). Our directory should look like this:

├── dist
│   └── bundle.js
├── package.json
├── src
│   └── js
│       └── index.js
└── webpack.config.js

If you open bundle.js, you’ll see some crazy stuff that webpack is doing to tie all your dependencies together. Of course, you’ll see your source code - all the way at the bottom:

/***/ function(module, exports) {
 
  console.log("index.js works");
 
/***/ }
/******/ ]);

To see it in action in the browser though, we’ll need a HTML entry point that loads our bundle.js.

Plugins

Introducing: webpack plugins, which provide additional functionality to the webpack building and bundling process. We’ll start by installing 2 of them:

npm install babel-loader html-webpack-plugin --save-dev

html-webpack-plugin is a plugin that we’re using here to generate a HTML file that includes all our webpack bundle(s).

Loaders are a type of plugin which transforms files from one state to another. babel-loader, as mentioned earlier, transforms ES6 code into ES5 code so that current browsers can run our app.

Our webpack.config.js is changed to accommodate these new plugins.

Adding a new plugin is as simple as requiring it, and adding a new instance of it in the plugins array.

Adding a loader is only slightly more complicated - the additional thing we need to specify is test, which tells the loader which files are eligible to go through the loader for transformation. Here, we specify with a regular expression that only files ending with .js or .jsx are eligible to be transformed by babel-loader.

I’ve demarcated diffed lines with // for single line changes and //- for block changes.

var webpack = require("webpack");
var path = require("path");
var HtmlWebpackPlugin = require("html-webpack-plugin"); //
 
module.exports = {
  entry: [path.resolve(__dirname, "./src/js/index.js")],
 
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
 
  module: {
    //-
    loaders: [
      {
        test: /\.jsx?$/,
        loaders: ["babel-loader"],
      },
    ],
  },
 
  plugins: [new HtmlWebpackPlugin()], //-
};

To confirm that babel-loader is doing its job, let’s write some ES6 code in index.js:

console.log("index.js works");
 
let x = 5; // wow so ES6
console.log(x);

Run webpack-dev-server - you should see an index.html in the dist folder now:

├── dist
│   ├── bundle.js
│   └── index.html
├── package.json
├── src
│   └── js
│       └── index.js
└── webpack.config.js

Navigate to http://localhost:8080 (I’m using Chrome) and open up your console:

index.js works
5

To demonstrate the agnostic aspect of webpack’s module loading, let’s write two throwaway files:

├── dist
│   ├── bundle.js
│   └── index.html
├── package.json
├── src
│   └── js
│       ├── index.js
│       ├── test1.js
│       └── test2.js
└── webpack.config.js

test1.js and test2.js contains just one line each: console.log("test1/2.js is imported").

We’ll then import these two files in two different ways in index.js - test1.js using ES6 module syntax, and test2.js using CommonJS/browserify syntax4:

console.log("index.js works");
 
let something = 5;
console.log(something);
 
import Test1 from "./test1.js";
require("./test2.js");

Refreshing the browser yields:

test1.js is imported
index.js works
5
test2.js is imported

Immediately, you’ll see that ES6 module imports are hoisted to the top of the file, whereas CommonJS imports are run in-place (this is a simplistic treatment of the issue, but it is generally true).

Source Maps

You’ll also notice the line number on the right says something like bundle.js(59), even for console.logs emanating from different source files. This could potentially make your debugging very difficult in the future when you have many source files and all you have is bundle.js(38593) or some such to work backwards with.

Source maps allow the browser to map bundled JS files back to their unbundled state, so that things like errors’ and console.logs’ line numbers correspond back to their sources.

The simplest way to enable source maps is by indicating the -d flag5. In the package.json:

"scripts": {
  "start": "webpack-dev-server -d"
}

The -d flag is actually short for --debug --devtool source-map --output-pathinfo. Each of these options are explained in further detail here.

In the next part, we’ll look at hot reloading with hot-loader, linting with eslint and eslint-react, as well as a couple of other useful webpack plugins.

Footnotes

  1. We’re installing it locally for now, but you can also install it globally by indicating a -g flag. This will allow you access to the webpack command on the command line.

  2. In case you were wondering, the ^ in "^0.5.2" means that the package.json will update you to the latest release within a specified major version, which is indicated the first number in the version. In this case, ^0.5.2 is valid for all versions that match 0.x.x.

  3. The reason we’re doing this is because running npm scripts specified using package.json will add node_modules/.bin to the path, thus making node_modules/.bin/webpack and node_modules/.bin/webpack-dev-server available to the script to use (open up the node_modules folder to see for yourself, its there).

  4. Regardless of ES6 module synatax or CommonJS require syntax, please put all your imports at the top of the file.

  5. One can also enable source maps for other assets using the built-in source maps plugin:

    var webpack = require("webpack");
    var path = require("path");
    var HtmlWebpackPlugin = require("html-webpack-plugin");
     
    module.exports = {
      entry: [path.resolve(__dirname, "./src/js/index.js")],
     
      output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js",
        sourceMapFilename: "bundle.js.map",
      },
     
      module: {
        loaders: [
          {
            test: /\.jsx?$/,
            loaders: ["babel-loader"],
            include: path.resolve(__dirname, "src/js"),
          },
        ],
      },
     
      plugins: [
        new HtmlWebpackPlugin(),
        new webpack.SourceMapDevToolPlugin(), //
      ],
    };

    The default behavior of the plugin is to map only JavaScript files, and to inline the source map. These, along with other options, can be changed.