Auralinna.blogTero Auralinna's blog about intriguing world of web development. Tweaking pixels since the '90s.

I'm a Full Stackish web developer with a strong passion for a beautiful and solid front end. I have mainly focused on front-end development, responsive web design, Content Management Systems, modern web frameworks, DevOps and back-end coding with PHP, Node.js and Java.

webpack

Setting up webpack 4 for a project

02.12.2018

This blog post shows how to setup webpack 4 module bundler for development. This webpack tutorial contains many common examples you might need to configure when doing JS application development with the webpack.

The demo is available at my GitHub repository. The demo includes a couple of example components. These components really don't do anything reasonable. They are there just to prove config changes work when we are adding new things.

I realized that I have never really configured the whole webpack development workflow by myself. It's usually already done when you start using JS framework like Vue.js or Angular. So that's the inspiration for this blog post.

Following tasks are covered

  1. Setup webpack-dev-server and npm build scripts
  2. Add index.html and generated Javascript bundle
  3. Add webpack alias to make it easier to import files
  4. Transform ES6 to ES5 with Babel
  5. Import and inject CSS code
  6. Extracting all CSS into a single file
  7. Handle files by file-loader
  8. Inline SVG elements
  9. Apply CSS vendor prefixes by postcss-loader and autoprefixer
  10. Optimize CSS and Javascript assets by minifying
  11. Use TypeScript with @babel/preset-typescript
  12. Separate dev and prod environments

Prerequisite

Here are minimum configs we are starting to fill. Also, you should have Node.js installed before starting.

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/app',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'app.[contenthash:8].js',
    publicPath: '/'
  },
  resolve: {
    modules: [
      'node_modules',
      path.resolve(__dirname, 'src')
    ],
    extensions: ['.js'],
  }
}

package.json

{
  "name": "webpack-guide",
  "version": "1.0.0",
  "description": "webpack 4 guide",
  "main": "app.js",
  "dependencies": {
  },
  "devDependencies": {
  },
  "author": "John Doe",
  "license": "ISC"
}

Setup webpack-dev-server and npm build scripts

Install webpack-dev-server

$ npm i webpack-dev-server webpack-cli webpack --save-dev

Add following npm scripts to package.json

  "scripts": {
    "build": "rm -rf ./dist/ && webpack --mode production --config webpack.config.js",
    "dev": "webpack-dev-server --mode development --config webpack.config.js"
  }

It's possible to build our app for the first time after we add the ./src/app.js file. App.js is the entry point of our app.

Add index.html and generated Javascript bundle

Though there still is nothing to show in a browser. So let's add index.html file and add generated JS bundle into that file. This can be done with html-webpack-plugin.

Install html-webpack-plugin

$ npm i html-webpack-plugin --save-dev

Create index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title><%= htmlWebpackPlugin.options.title %></title>
  <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

</body>
</html>

Add configuration into the webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

...

  plugins: [
    new HtmlWebpackPlugin({
      title: 'Setting up webpack 4',
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true
      },
    })
  ]

Now we can start the app with npm run dev and navigate to the address http://localhost:8080. We'll see blank page with the title Setting up webpack 4.

Add webpack alias to make it easier to import files

With alias, we don't have to use relative import paths which are annoying most of the time.

As an example we can use import { header } from '@components' instead of using import { header } from '../../components'.

webpack.config.js

  resolve: {
    alias: {
      '@components': path.resolve(__dirname, 'src/components'),
      '@scss': path.resolve(__dirname, 'src/scss'),
      '@img': path.resolve(__dirname, 'src/img'),
      '@': path.resolve(__dirname, 'src')
    }
  }

Transform ES6 to ES5 with Babel

I want to write ES6 instead of older Javascript syntax so let's add Babel config for the transpiling.

Install Babel and babel-loader

$ npm i babel-loader @babel/core @babel/preset-env --save-dev

Add configuration into the webpack.config.js

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }

Import and inject CSS code

To import and use CSS styles we need to add new loaders. Css-loader imports content to a variable and style-loader injects content into the HTML file as an inline tag.

Install

$ npm i style-loader css-loader --save-dev

Add configuration into the webpack.config.js

      {
        test: /\.css$/,
        use: [
          "style-loader", 
          "css-loader"
        ]
      }

To support SCSS as well we will add sass-loader and node-sass.

Install sass-loader and node-sass

$ npm i sass-loader node-sass --save-dev

Add sass-loader into the existing style configuration block

      {
        test: [/.css$|.scss$/],
        use: [
          "style-loader", 
          'css-loader', 
          'sass-loader'
        ]
      }

Extracting all CSS into a single file

Now we are able to style our application. Style-loader styles are injected as an inline. We can extract styles with css-mini-extract-plugin if we want to use an external stylesheet file. This stylesheet will be then injected into the index.html automatically.

Install

$ npm i mini-css-extract-plugin --save-dev

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

...

      {
        test: [/.css$|.scss$/],
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader", 
          "sass-loader"
        ]
      }
      
...

  plugins: [
    ...
    new MiniCssExtractPlugin({
      filename: 'app.[contenthash:8].css',
    }),
    ...
  ]

Import images by file-loader

To include images we need to configure file-loader.

Install file-loader

$ npm i file-loader --save-dev

webpack.config.js

      {
        test: /\.(png|jpg|gif|svg)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[hash:8].[ext]',
              outputPath: 'assets/'
            }
          }
        ]
      }

It's now possible to use images either via import

import nodejsLogo from '@img/nodejs.png'

or CSS rules.

body {
  background: transparent url(../img/webpack-logo.png);
}

Inline SVG elements

In some cases, we might want to inline assets. Here is a configuration for inlining SVG images.

Install svg-url-loader

$ npm i svg-url-loader --save-dev

webpack.config.js

      {
        test: /\.svg$/,
        loader: 'svg-url-loader',
        options: {
          noquotes: true
        }
      },

Also remove svg extension from the file-loader configuration.

Apply CSS vendor prefixes by postcss-loader and autoprefixer

Vendor prefixes can be applied to the styles automatically by postcss-loader and autoprefixer.

Install postcss-loader and autoprefixer

$ npm i postcss-loader autoprefixer --save-dev

Add configuration into the webpack.config.js

      {
        test: [/.css$|.scss$/],
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader', 
          'sass-loader',
          'postcss-loader'
        ]
      }

Create postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

Add targeted browsers into the package.json

  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]

After this change all vendor prefixes are set automatically for the styles which require autoprefixing.

You can tweak browser support via the browserslist property in package.json. Check out supported browsers by different rules at browserl.ist.

Optimize CSS and Javascript assets by minifying

Then let's optimize app by minifying our assets. Actually webpack 4 optimizes JS bundle by default when using production mode. If you want to tweak settings you can provide a plugin by yourself.

Install plugins

$ npm i uglifyjs-webpack-plugin optimize-css-assets-webpack-plugin --save-dev

Add configuration into the webpack.config.js

const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

...

  optimization: {
    minimizer: [
      new UglifyJsPlugin(),
      new OptimizeCSSAssetsPlugin()
    ]
  },

Use TypeScript with @babel/preset-typescript

There is a new approach to use TypeScript with Babel. This blog post "TypeScript With Babel: A Beautiful Marriage" explains the pros and cons well so I don't repeat those here.

Another option is to use ts-loader.

Install TypeScript and @babel/preset-typescript

$ npm i @babel/preset-typescript typescript --save-dev

Modify babel-loader settings in webpack.config.js to include @babel/typescript preset

      {
        test: [/.js$|.ts$/],
        exclude: /(node_modules)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/typescript', 
              '@babel/preset-env'
            ]
          }
        }
      },
        
  ...
  
  resolve: {
    extensions: [".js", ".ts"],
  },

Add custom.d.ts

declare module "*.svg" {
  const content: any;
  export default content;
}

declare module "*.png" {
  const content: any;
  export default content;
}

Add tsconfig.json for TypeScript settings

{
  "compilerOptions": {
    // Target latest version of ECMAScript.
    "target": "esnext",
    // Search under node_modules for non-relative imports.
    "moduleResolution": "node",
    // Process & infer types from .js files.
    "allowJs": true,
    // Don't emit; allow Babel to transform files.
    "noEmit": true,
    // Enable strictest settings like strictNullChecks & noImplicitAny.
    "strict": true,
    // Disallow features that require cross-file information for emit.
    "isolatedModules": true,
    // Import non-ES modules as default imports.
    "esModuleInterop": true,
    "baseUrl": ".",
    "paths": {
      "@components": [ "src/components" ],
      "@scss": [ "src/scss" ],
      "@img": [ "src/img" ],
      "@": [ "src" ],
    }
  },
  "include": [
    "custom.d.ts",
    "src"
  ]
}

Aliases must be added into the tsconfig.json paths also in order that TypeScript can find them.

Add check-types script to package.json

  "scripts": {
    "check-types": "tsc"
  }

Now you have a separate command for type checking.

$ npm run check-types -- --watch

You could also add npm run check-types to your build script to check types when building for the production.

Now you could rename your .js files to .ts and start using TypeScript features. My demo project contains both js and ts files which shouldn't be the case in a real project.

Note that type checking is not part of the development workflow with this approach. You need to check them separately. This might be a good or bad thing depending on how do you want to work.

Separate dev and prod environments

And finally, let's make a few changes to our build system. We'll separate dev and prod build to make development easier and building faster.

Install webpack-merge

$ npm i webpack-merge --save-dev

Create build/webpack.base.config.js

We'll move most of the configuration into this file.

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: './src/app',
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'app.[contenthash:8].js',
    publicPath: '/'
  },
  module: {
    rules: [
      {
        test: [/.js$|.ts$/],
        exclude: /(node_modules)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/typescript', 
              '@babel/preset-env'
            ]
          }
        }
      },
      {
        test: /\.svg$/,
        loader: 'svg-url-loader',
        options: {
          noquotes: true
        }
      },
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[hash:8].[ext]',
              outputPath: 'assets/'
            }
          }
        ]
      },
      {
        test: [/.css$|.scss$/],
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader', 
          'sass-loader',
          'postcss-loader'
        ]
      }
    ]
  },
  resolve: {
    alias: {
      '@components': path.resolve(__dirname, '../src/components'),
      '@scss': path.resolve(__dirname, '../src/scss'),
      '@img': path.resolve(__dirname, '../src/img'),
      '@': path.resolve(__dirname, '../src')
    },
    modules: [
      'node_modules',
      path.resolve(__dirname, '../src')
    ],
    extensions: ['.js', '.ts'],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'app.[contenthash:8].css',
    }),
    new HtmlWebpackPlugin({
      title: 'Setting up webpack 4',
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true
      },
    })
  ]
}

Create build/webpack.dev.config.js

Dev config is currently quite empty but something probably comes up which should be added only to the development environment.

const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base.config.js')

module.exports = merge(webpackBaseConfig, {})

Create build/webpack.prod.config.js

Production file has all optimization tasks which will slow down our dev build. Though remember to test prod build occasionally to ditch early prod config related issues.

const merge = require('webpack-merge')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const webpackBaseConfig = require('./webpack.base.config.js')

module.exports = merge(webpackBaseConfig, {
  optimization: {
    minimizer: [
      new UglifyJsPlugin(),
      new OptimizeCSSAssetsPlugin()
    ]
  }
})

Modify package.json build commands to utilize new configs

  "scripts": {
    "build": "rm -rf ./dist/ && npm run check-types && webpack --mode production --config ./build/webpack.prod.config.js",
    "dev": "webpack-dev-server --mode development --config ./build/webpack.dev.config.js",
    "check-types": "tsc"
  },

After this step, it's possible to remove webpack.config.js from the root of the project. I have kept it in the demo as a reference.


Now our webpack configuration starts to look quite ready and we can focus more on the application logic.

Latest blog posts

Latest CodePens

View all CodePens