Jon Preece

Follow DeveloperHandbook.com on TwitterJon Preece
Published on Dec 31, 2018 · Updated on Sep 14, 2019 · 8 minute read.

Writing your own Webpack 4 configuration from absolute scratch is not strictly necessary. You could go online, find a basic Webpack configuration file and fork it/copy it to your project and use that as your starting point.

Whilst using a starter project is good for getting started quickly, it can be better to have a more indepth understanding of the process, which can help with adding/removing functionality (and debugging) later.

We will configure Webpack to do the following;

  • Build our JavaScript, use Babel and various plugins to allow us to use the latest language features whilst maintaining backwards compatibility
  • Enable hot module reloading (HMR)
  • Build other assets such as SCSS and images
  • Alternatively, we will look at how to use TypeScript instead of Babel

Set up a simple Webpack project from scratch

Create a new folder called webpack-4-react-from-scratch and run git init && npm init -y to add Git support and create a package.json file with all the defaults, then open your favourite code editor in the folder just created.

Run the following command in your terminal to install Webpack and associated tools;

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

Add a .gitignore file with the following lines;

dist
node_modules

Then create a Webpack configuration file, called webpack.config.js. We do not need any config yet.

Add the following code;

module.exports = {}

We will utilise Webpack’s defaults where possible. By default, Webpack will look for our code under a folder called src, so here we should have a file called index.js. Go ahead and create that.

Add the following line of JavaScript;

console.log('Hello, World!')

Finally, add the following build NPM script to your package.json;

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

Now run the following command;

npm run build && node ./dist/main.js

Assuming all is working well, you should notice that a new directory is created (called dist) and a new bundle called main.js. The second command runs the compiled code, which writes Hello, World! to the console.

This is our project so far;

Webpack 4 initial setup
Webpack 4 initial setup

Feel free to ignore the yellow warning in the console. Webpack says that it has defaulted to production mode, which is fine for now. We will fix this later.

Let’s move on.

Configure Webpack to compile ES6+ code using Babel

Currently Webpack is not doing much for us. It takes our code and bundles it, minifies it, (because it defaults to production mode), then outputs the compiled code into a single file. The compilation step using Babel is required to maximise cross browser support.

An added benefit of using a compiler like Babel is that we can also configure it to support language features that are not currently standardised or supported by all browsers.

Add the following packages using this command;

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader

What are these packages for?

  • @babel/core is the Babel compiler core.
  • @babel/preset-env is a preset that allows you to use the latest JavaScript without needing to worry about what transformations/polyfills are needed for browser support.
  • @babel/preset-react is a wrapper around several transformations that enable JSX support and more.
  • babel-loader is a plugin for Webpack. We will need to instruct Webpack to use Babel shortly.

Flip back to webpack.config.js and update to the following;

module.exports = {
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  }
}

We added a module rule. There can be many rules for basically all different file extensions. Typically you will have different rules for JavaScript files, TypeScript files, images, SVG’s and so on.

Each rule is ran by Webpack for every file that it encounters in our project. As part of the rule, we have provided a test, which is a regular expression. The regular expression is simple. If the file has a .js or .jsx file extension, it will be processed using the given loader, which here is babel-loader. As all the code in node_modules is not part of our project (and usually pre-compiled) we do not need to do that here, so we exclude that project and tell Webpack not to process any files within.

We need to tell Babel (via babel-loader) to use our presets. Create a new file called .babelrc and add the following code;

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

When Babel runs, it will look for .babelrc and use the presets automatically.

Note: We have added resolve, with an extensions array with JS and JSX file extensions. I have added this here because it will clean up our imports in our JavaScript files, meaning we will not have to include the extension as part of the import. This is strictly not necessary but I always like to have tidy code when possible.

With resolve > extensions, this;

import frank from './frank.js'

becomes

import frank from './frank'

Neater huh?

Update your index.js as follows;

const obj = {
  hello: 'World'
}

const { hello } = obj

console.log(hello)

So much ES6 code here. We have used const and we have also used destructuring.

Run the following command in your console;

npm run build -- --mode development && node ./dist/main.js

This will prevent the bundle from being minifed so we can see it properly. Open main.js (in the dist folder) and observe the code as shown (which should be somewhere near the bottom);

eval(
  'var obj = {\n  hello: "World"\n};\nvar hello = obj.hello;\nconsole.log(hello);\n\n//# sourceURL=webpack:///./src/index.js?'
)

Notice that const and our destructuring have disappeared, and has been rewritten in ES5 compatible language, so we know Babel is working.

Observe the output from the script we just ran, notice that World has been written out to the console!

Configure Webpack to compile ES6+ code using Babel and TypeScript

Babel is fantastic, I love it. Babel is fast, and easy to configure. TypeScript is great for writing code that scales well, is easier to maintain, and is self documenting. More and more projects are moving to using TypeScript, and that is great for the future of web development.

Babel and TypeScript are not mutually exclusive, they are not competing products. You can easily use TypeScript and Babel together, with a simple plugin called @babel/preset-typescript. Let us try it out.

Carrying on from where we left off, we need to make a few changes.

First rename src/index.js to src/index.ts. TypeScript is opt in, and JavaScript and TypeScript code can co-exist safely in the same project. To opt in, changing the file name is all that is required.

Open src/index.ts, delete all existing code, and add the following;

interface User {
  id: number
  username: string
  firstName: string
  lastName: string
}

const newUser: User = {
  id: 123,
  username: 'jpreecedev',
  firstName: 'Jon',
  lastName: 'Preece'
}

console.log(newUser)

Here we have used a built in feature of TypeScript called interfaces. This allows us to define a contract for the shape of an object, in this case, a User object. Any object that implements user must implement all those properties.

Observe the newUser object next. Notice that it implements the User interface and has all the properties associated with that interface. Finally we output the value of newUser.

Before we can see this in action, we need to install and set up @babel/preset-typescript.

Run the following command;

npm install --save-dev @babel/preset-typescript

Next, open .babelrc and all @babel/preset-typescript as shown;

{
-  "presets": ["@babel/preset-env", "@babel/preset-react"]
+  "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}

Then, open webpack.config.js and update the existing rule that currently only handles JavaScript files to include TypeScript files as well;

module.exports = {
  module: {
    rules: [
      {
-        test: /\.jsx?$/,
+        test: /\.(t|j)sx?$/,
        loader: "babel-loader",
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [".js", ".jsx"]
  }
};

If you do not have much experience with regular expressions, this might look a little confusing. All that is going on here is that we are saying to Webpack; Babel Loader can process JS, JSX, TS, and TSX files.

Finally, update resolve > extensions to automatically resolve both .ts and .tsx files, as follows;

resolve: {
-  extensions: [".js", ".jsx"]
+  extensions: [".js", ".jsx", ".ts", ".tsx"]
}

To run the code, run the following command;

npm run build -- --mode development && node ./dist/main.js

You should see the newUser object output to the console;

{
  "id": 123,
  "username": "jpreecedev",
  "firstName": "Jon",
  "lastName": "Preece"
}

Congratulations, you now have support for TypeScript in your codebase. Note that @babel/preset-typescript does not have compile time checking, you will need the full blown TypeScript compiler for that, but this is enough to get off the ground quickly.

Summary

Congratulations, you have written your first, albeit basic, Webpack configuration file. Subsequent posts will discuss how to flesh this out and get all the pieces in place to enable us to build and deploy a serious React single page application (SPA).

Next steps

If you would like to continue learning about Webpack, check out the next post in the Webpack tutorial series, How to create a production ready Webpack config

Webpackwebpack-intro-series

Jon Preece

About the author

I am Jon Preece, an experienced website and software developer from the United Kingdom, based in Manchester.

Throughout my 10+ year professional career I have worked in many sectors, including; e-commerce, financial services, marketing, healthcare, travel and accountancy. Get in touch via Twitter.