An Annotated webpack 4 Config for Frontend Web Development
Tags: webpack, javascript, frontend
Published: 2020-02-21 01:50:12

---
title: An Annotated webpack 4 Config for Frontend Web Development
published: true
date: 2018-10-23 13:40:00 UTC
tags: webpack,javascript,frontend
canonical_url: https://nystudio107.com/blog/an-annotated-webpack-4-config-for-frontend-web-development
---

# An Annotated webpack 4 Config for Frontend Web Development

### As web devel­op­ment becomes more com­plex, we need tool­ing to help us build mod­ern web­sites. Here’s a com­plete real-world pro­duc­tion exam­ple of a sophis­ti­cat­ed web­pack 4 config

Andrew Welch / [nystudio107](https://nystudio107.com)

![Webpack 4 Annotated Config](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/2176/webpack-4-annotated-config.jpg)

Build­ing a mod­ern web­site has become cus­tom appli­ca­tion devel­op­ment. Web­sites are expect­ed to do more than just be mar­ket­ing sites as they take on the func­tion­al­i­ty of tra­di­tion­al apps.

Any time a process becomes com­pli­cat­ed, we break it down into man­age­able com­po­nents, and auto­mate the build process with tool­ing. This is the case in whether we are man­u­fac­tur­ing cars, draft­ing legal doc­u­ments, or build­ing websites. 

<aside>
                                Use the right tool for the job
                            </aside>

Tools like [web­pack](http://webpack.js.org/) have been at the fore­front of mod­ern web devel­op­ment for pre­cise­ly that rea­son: they help us build com­plex things.

web­pack 4 boasts some [amaz­ing improve­ments](http://medium.com/webpack/webpack-4-released-today-6cdb994702d4), the most appeal­ing to me was how much faster it’d become at build­ing. So I decid­ed to adopt it.

![Buckle Up I Want To Try Something](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_100_line/buckle-up-i-want-to-try-something.jpg)

Buck­le up, because this is a long arti­cle filled with tons of information.

## Adopt­ing Webpack

A bit over a year ago, I pub­lished the arti­cle [A Gulp Work­flow for Fron­tend Devel­op­ment Automa­tion](https://dev.to/gaijinity/a-gulp-workflow-for-frontend-development-automation-24ne-temp-slug-6690328) that showed how to use Gulp to accom­plish the same thing. How­ev­er in the inter­ven­ing time, I’ve been doing more and more with fron­tend frame­works like [Vue­JS](http://vuejs.org/) and [GraphQL](http://graphql.org/), as dis­cussed in the [Using Vue­JS + GraphQL to make Prac­ti­cal Mag­ic](https://dev.to/gaijinity/using-vuejs-graphql-to-make-practical-magic-1520-temp-slug-9813575) article.

I have found that web­pack makes it eas­i­er for me to build the types of web­sites and appli­ca­tions that I’m mak­ing these days, and it also allows me to use the most mod­ern tool­chain around.

There are oth­er choices:

- **[Lar­avel Mix](http://laravel.com/docs/5.7/mix)** is a lay­er on top of web­pack. It’s appeal­ing in its sim­plic­i­ty: you can get up and run­ning quick­ly, and it’ll do what you want 90% of the time. But that remain­ing 10% means a drop down into web­pack anyway.
- **[vue-cli](http://cli.vuejs.org/)** is very appeal­ing if you’re build­ing noth­ing but Vue­JS fron­tends. It again is a lay­er on top of web­pack that works great most of the time, and does some amaz­ing things for you. But again, you need to drop down into web­pack when your needs diverge from what it pro­vides. And I’m not always using Vue­JS exclusively.
- **[Neu­tri­no](http://neutrinojs.org/)** is an inter­est­ing lay­er on web­pack that we explored in the [Neu­tri­no: How I Learned to Stop Wor­ry­ing and Love Web­pack](http://devmode.fm/episodes/neutrino-how-i-learned-to-stop-worrying-and-love-webpack) pod­cast. The premise is amaz­ing, build­ing a web­pack con­fig by snap­ping togeth­er pre­fab Lego brick com­po­nents. But learn­ing how it worked seemed almost as much work as learn­ing web­pack itself.

I won’t fault you if you choose any of the above tools (or even some­thing else), but note that there’s a com­mon theme to all of them: they lay­er on top of webpack.

<aside>
                                Under­stand­ing how the lay­ers in your devel­op­ment sys­tem work pays div­i­dends in the end
                            </aside>

Ulti­mate­ly, you just need to decide where in the pyra­mid of fron­tend tech­nolo­gies you want to stand.

At some point, I think it makes sense to under­stand how an impor­tant tool like web­pack works. A while ago, I’d [com­plained to Sean Larkin](http://twitter.com/nystudio107/status/941488105600765952) (one of the web­pack core team mem­bers) that web­pack was like a ​“black box”. His reply was pithy, but quite poignant:

<aside>
                                It’s only black if you haven’t opened it
                            </aside>

He’s right. Time to open the box.

This arti­cle will not teach you all there is to know about web­pack or even how to install it. There are plen­ty of resources avail­able for that — pick the for­mat that you learn best from:

- **[web­pack — the Con­fus­ing Parts](https://medium.com/@rajaraodv/webpack-the-confusing-parts-58712f8fcad9)** — for a con­cise overview of how web­pack works
- **[web­pack doc­u­men­ta­tion](http://webpack.js.org/concepts/)** — if you learn best by reading
- **[web­pack fun­da­men­tals](http://frontendmasters.com/courses/webpack-fundamentals/)** — if you learn best by watching
- **[How to switch from Gulp to web­pack](http://www.valentinog.com/blog/from-gulp-to-webpack-4-tutorial/)** — if that’s where you’re com­ing from

…and there are many, many more. Instead, this arti­cle will anno­tate a full work­ing exam­ple of a fair­ly sophis­ti­cat­ed web­pack 4 set­up. You may use all of it; you may use bits and pieces of it. But hope­ful­ly you’ll learn a thing or two from it.

While on my con­tin­u­ing jour­ney learn­ing web­pack, I found many tuto­r­i­al videos, a bunch of write-ups show­ing how to install it and a basic con­fig, but not a whole lot of real-world pro­duc­tion exam­ples of web­pack con­figs. So here we are.

## What We Get Out of the Box

As I set about learn­ing web­pack by open­ing up the box, I had a list of tech­nolo­gies that I relied upon that I want­ed to be part of the build process. I also took the time to look around to see what else was out there that I could adopt in the process.

As dis­cussed in the [A Pret­ty Web­site Isn’t Enough arti­cle](https://dev.to/gaijinity/a-pretty-website-isn-8217-t-enough-1nbj-temp-slug-3932740), web­site per­for­mance has always been a key con­cern of mine, so it should be no sur­prise that there’s a focus on that in this web­pack con­fig as well.

![Webpack Black Box](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/webpack-black-box.jpg)

So here is my very opin­ion­at­ed list of things that I want­ed web­pack to do for me, and tech­nolo­gies I want­ed to incor­po­rate in my build process:

- **[Devel­op­ment / Pro­duc­tion](http://webpack.js.org/guides/production/)** — in local devel­op­ment, I want fast builds via the in-mem­o­ry [web­pack-dev-serv­er](http://webpack.js.org/configuration/dev-server/), and for pro­duc­tion builds (often done in a Dock­er con­tain­er via [buddy.works](http://buddy.works/)), I want the every pos­si­ble opti­miza­tion. Thus we have sep­a­rate <kbd>dev</kbd> and <kbd>prod</kbd> con­figs & builds.
- **[Hot Mod­ule Replace­ment](http://webpack.js.org/concepts/hot-module-replacement/)** — as I make changes to my JavaScript, CSS, or tem­plates, I want the web­page to seam­less­ly refresh. This speeds devel­op­ment tremen­dous­ly: just say no to the Reload button.
- **[Dynam­ic Code Split­ting](http://webpack.js.org/guides/code-splitting/#dynamic-imports)** — I don’t want to man­u­al­ly have to define JavaScript chunks in a con­fig file, I want web­pack to sort it out for me.
- **[Lazy Load­ing](http://webpack.js.org/guides/lazy-loading/)** — aka async dynam­ic mod­ule load­ing. Load only the code/​resources need­ed, when they are need­ed, with­out ren­der blocking.
- **[Mod­ern & Lega­cy JS Bun­dles](http://philipwalton.com/articles/deploying-es2015-code-in-production-today/)** — I want­ed to deploy mod­ern ES2015+ JavaScript mod­ules to the [75%+ of world­wide browsers](https://caniuse.com/#search=javascript%20modules) that sup­port it, while grace­ful­ly pro­vid­ing a fall­back lega­cy bun­dle for lega­cy browsers (with all of the tran­spiled code and polyfills).
- **[Cache Bust­ing via manifest.json](http://webpack.js.org/guides/caching/)** - this allows us to set a long expiry data for our sta­t­ic assets, while also ensur­ing that they are auto­mat­i­cal­ly cache bust­ed if they change.
- **[Crit­i­cal CSS](http://developers.google.com/speed/docs/insights/OptimizeCSSDelivery)** — as per the [Imple­ment­ing Crit­i­cal CSS on your web­site arti­cle](https://dev.to/gaijinity/implementing-critical-css-on-your-website-37d5-temp-slug-6012917), this is some­thing that makes ini­tial page loads sig­nif­i­cant­ly faster.
- **[Work­box Ser­vice Work­er](http://developers.google.com/web/tools/workbox/)** — we can lever­age Google’s Wor<kbd></kbd>kbox project to gen­er­ate a [Ser­vice Work­er](http://developers.google.com/web/fundamentals/primers/service-workers/) for us that will know about all of our pro­jec­t’s assets. PWA, here we come!
- **[PostC­SS](http://postcss.org/)** — I think of it as the ​“Babel of CSS”, things like SASS and SCSS are built on it, and it lets you use upcom­ing CSS fea­tures now.
- **[Image Opti­miza­tion](http://images.guide/)** — Images are by far the largest thing on most web­pages, so it makes sense to opti­mize them via auto­mat­ed tools like <kbd>mozjpeg</kbd>, <kbd>optipng</kbd>, <kbd>svgo</kbd>, etc.
- **[Auto­mat­ic .webp Cre­ation](http://developers.google.com/speed/webp/)** — Chrome, Edge, and Fire­fox all are sup­port­ing <kbd>.webp</kbd>, a for­mat that is more effi­cient than JPEG.
- **[Vue­JS](http://vuejs.org/)** — Vue­JS is my fron­tend frame­work of choice. I want to be able to use sin­gle file <kbd>.vue</kbd> com­po­nents as a seam­less part of my devel­op­ment process.
- **[Tail­wind CSS](http://tailwindcss.com/docs/what-is-tailwind/)** — Tail­wind is a util­i­ty-first CSS that I use for rapid­ly pro­to­typ­ing in local dev, and then run through [PurgeC­SS](http://github.com/FullHuman/purgecss) for pro­duc­tion, to reduce the size dramatically.
- **[Offline Com­pres­sion of sta­t­ic resources](https://medium.com/@selvaganesh93/how-to-serve-webpack-gzipped-file-in-production-using-nginx-692eadbb9f1c)** — We can pre-com­press our sta­t­ic resources into .gz files that our web­serv­er can auto­mat­i­cal­ly serve up to clients that accept them

Phew, quite an ambi­tious list!

![Ambitious Feature List Webpack](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/ambitious-feature-list-webpack.jpg)

There’s more too, like the auto­mat­ic ugli­fi­ca­tion of JavaScript, mini­fi­ca­tion of CSS, and oth­er stan­dard things we’d expect from a fron­tend build system.

I also want it to work with a devel­op­ment team that may use dis­parate tools for their local dev envi­ron­ment, and to have the con­fig be easy to main­tain and reuse from project to project.

<aside>
                                The impor­tance of main­tain­abil­i­ty and reusabil­i­ty can’t be understated
                            </aside>

Your stack of fron­tend frameworks/​technologies may look dif­fer­ent from mine, but the prin­ci­ples applied will be the same. So read on, regard­less of what you use!

## Project Tree & Organization

To give you an overview of what the set­up looks like, here’s a bare bones project tree:

```

├── example.env
├── package.json
├── postcss.config.js
├── src
│   ├── css
│   │   ├── app.pcss
│   │   ├── components
│   │   │   ├── global.pcss
│   │   │   ├── typography.pcss
│   │   │   └── webfonts.pcss
│   │   ├── pages
│   │   │   └── homepage.pcss
│   │   └── vendor.pcss
│   ├── fonts
│   ├── img
│   │   └── favicon-src.png
│   ├── js
│   │   ├── app.js
│   │   └── workbox-catch-handler.js
│   └── vue
│   └── Confetti.vue
├── tailwind.config.js
├── templates
├── webpack.common.js
├── webpack.dev.js
├── webpack.prod.js
├── webpack.settings.js
└── yarn.lock

```

For the com­plete source code for every­thing pre­sent­ed here, check out the **[anno­tat­ed-web­pack-4-con­fig](http://github.com/nystudio107/annotated-webpack-4-config)** github repo.

So in terms of the core con­fig files, we have:

- <kbd><strong>.env</strong></kbd> — envi­ron­men­tal-spe­cif­ic set­tings for the <kbd>webpack-dev-server</kbd>; this is _nev­er_ checked into git
- <kbd><strong>webpack.settings.js</strong></kbd> — a JSON-ish set­tings file, the only file we need to edit from project to project
- <kbd><strong>webpack.common.js</strong></kbd> — com­mon set­tings for both types of builds
- <kbd><strong>webpack.dev.js</strong></kbd> — set­tings for local devel­op­ment builds
- <kbd><strong>webpack.prod.js</strong></kbd> — set­tings for pro­duc­tion builds

Here’s a dia­gram of how it all fits together:

![Webpack Config Text Solid Sm](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x1315_crop_center-center_100_line/webpack-config-text-solid-sm.png)

The goal is that you need to edit only what is in the gold col­ored round­ed-rec­tan­gles (<kbd>.env</kbd> & <kbd>webpack.settings.js</kbd>) from project to project.

Sep­a­rat­ing things out in this way makes work­ing with the con­fig files quite a bit eas­i­er. Even if you do end up chang­ing the var­i­ous web­pack con­fig files from what I’ve pre­sent­ed here, keep­ing with this method­ol­o­gy will help you main­tain them long-term.

Don’t wor­ry, we’ll get into each file in detail later.

## Anno­tat­ed package.json

Let’s start by break­ing down our <kbd>package.json</kbd> :

```

{
    "name": "example-project",
    "version": "1.1.0",
    "description": "Example Project brand website",
    "keywords": [
        "Example",
        "Keywords"
    ],
    "homepage": "https://github.com/example-developer/example-project",
    "bugs": {
        "email": "someone@example-developer.com",
        "url": "https://github.com/example-developer/example-project/issues"
    },
    "license": "SEE LICENSE IN LICENSE.md",
    "author": {
        "name": "Example Developer",
        "email": "someone@example-developer.com",
        "url": "https://example-developer.com"
    },
    "browser": "/web/index.php",
    "repository": {
        "type": "git",
        "url": "git+https://github.com/example-developer/example-project.git"
    },
    "private": true,

```

Noth­ing par­tic­u­lar­ly inter­est­ing here, just meta infor­ma­tion for our web­site as out­lined in the [package.json spec­i­fi­ca­tion](http://docs.npmjs.com/files/package.json).

```

"scripts": {
    "debug": "webpack-dev-server --config webpack.dev.js",
    "dev": "webpack-dashboard -- webpack-dev-server --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js --progress --hide-modules"
},

```

These are the scripts that rep­re­sent the two major build steps we have for our project:

- **debug**  — used when you need to debug the web­pack build itself; this dis­ables the <kbd>webpack-dashboard</kbd> (see below) to make get­ting at the con­sole out­put easier
- **dev**  — used when­ev­er we’re work­ing on the project, it spins up the <kbd>webpack-dev-server</kbd> to allow for Hot Mod­ule Replace­ment (HMR), in mem­o­ry com­pi­la­tion, and oth­er niceties.
- **build**  — used when we do a pro­duc­tion deploy­ment, it does all of the fan­cy and time con­sum­ing things like Crit­i­cal CSS, ugli­fi­ca­tion of JavaScript, etc. that need to be done for pro­duc­tion deployment.

To run them, we just use the CLI _inside of our devel­op­ment envi­ron­ment_ to do <kbd>yarn dev</kbd> or <kbd>yarn build</kbd> if we’re using [yarn](http://yarnpkg.com/), and <kbd>npm run dev</kbd> or <kbd>npm run build</kbd> if we’re using [npm](http://www.npmjs.com/). These are the only two com­mands you’ll need to use.

Notice that via the <kbd>--config</kbd> flag, we’re also pass­ing in [sep­a­rate con­fig files](http://webpack.js.org/guides/production/). This lets us break down our web­pack con­fig into sep­a­rate log­i­cal files, because we’re going to be doing things very dif­fer­ent­ly for devel­op­ment builds com­pared to pro­duc­tion builds.

Next up we have our **browser­slist** :

```

"browserslist": {
        "production": [
            "> 1%",
            "last 2 versions",
            "Firefox ESR"
        ],
        "legacyBrowsers": [
            "> 1%",
            "last 2 versions",
            "Firefox ESR"
        ],
        "modernBrowsers": [
            "last 2 Chrome versions",
            "not Chrome < 60",
            "last 2 Safari versions",
            "not Safari < 10.1",
            "last 2 iOS versions",
            "not iOS < 10.3",
            "last 2 Firefox versions",
            "not Firefox < 54",
            "last 2 Edge versions",
            "not Edge < 15"
        ]
    },

```

This is a [browser­slist](http://github.com/browserslist/browserslist) that tar­gets spe­cif­ic browsers based on human-read­able con­figs. The [PostC­SS auto­pre­fix­er](http://github.com/postcss/autoprefixer) defaults to using our <kbd>production</kbd> set­tings. We pass in the <kbd>legacyBrowsers</kbd> and <kbd>modernBrowsers</kbd> to Babel to han­dle build­ing both lega­cy and mod­ern JavaScript bun­dles. More on that later!

Next up we have our **devDe­pen­den­cies** , which are all of the npm pack­ages required for our build system:

```

"devDependencies": {
    "@babel/core": "^7.1.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/plugin-transform-runtime": "^7.1.0",
    "@babel/preset-env": "^7.1.0",
    "@babel/register": "^7.0.0",
    "@babel/runtime": "^7.0.0",
    "@gfx/zopfli": "^1.0.11",
    "babel-loader": "^8.0.2",
    "clean-webpack-plugin": "^3.0.0",
    "compression-webpack-plugin": "^2.0.0",
    "copy-webpack-plugin": "^4.5.2",
    "create-symlink-webpack-plugin": "^1.0.0",
    "critical": "^1.3.4",
    "critical-css-webpack-plugin": "^0.2.0",
    "css-loader": "^2.1.0",
    "cssnano": "^4.1.0",
    "dotenv": "^6.1.0",
    "file-loader": "^2.0.0",
    "git-rev-sync": "^1.12.0",
    "glob-all": "^3.1.0",
    "html-webpack-plugin": "^3.2.0",
    "ignore-loader": "^0.1.2",
    "imagemin": "^6.0.0",
    "imagemin-gifsicle": "^6.0.0",
    "imagemin-mozjpeg": "^8.0.0",
    "imagemin-optipng": "^6.0.0",
    "imagemin-svgo": "^7.0.0",
    "imagemin-webp": "^5.0.0",
    "imagemin-webp-webpack-plugin": "^3.1.0",
    "img-loader": "^3.0.1",
    "mini-css-extract-plugin": "^0.4.3",
    "moment": "^2.22.2",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "postcss": "^7.0.2",
    "postcss-import": "^12.0.0",
    "postcss-loader": "^3.0.0",
    "postcss-preset-env": "^6.4.0",
    "purgecss-webpack-plugin": "^1.3.0",
    "purgecss-whitelister": "^2.2.0",
    "resolve-url-loader": "^3.0.0",
    "save-remote-file-webpack-plugin": "^1.0.0",
    "stylelint": "^9.9.0",
    "stylelint-config-recommended": "^2.1.0",
    "style-loader": "^0.23.0",
    "symlink-webpack-plugin": "^0.0.4",
    "terser-webpack-plugin": "^1.1.0",
    "vue-loader": "^15.4.2",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.5.17",
    "webapp-webpack-plugin": "https://github.com/brunocodutra/webapp-webpack-plugin.git",
    "webpack": "^4.19.1",
    "webpack-bundle-analyzer": "^3.0.2",
    "webpack-cli": "^3.1.1",
    "webpack-dashboard": "^3.0.0",
    "webpack-dev-server": "^3.3.0",
    "webpack-manifest-plugin": "^2.0.4",
    "webpack-merge": "^4.1.4",
    "webpack-notifier": "^1.6.0",
    "workbox-webpack-plugin": "^3.6.2"
},

```

Yep, that’s quite a bit of pack­ages. But our build process _does_ quite a bit.

And final­ly, we use the **depen­den­cies** for the pack­ages we use on the fron­tend of our website:

```

"dependencies": {
    "axios": "^0.18.0",
    "core-js": "^3.0.0",
    "regenerator-runtime": "^0.13.2",
    "tailwindcss": "^1.0.0",
    "vue": "^2.5.17",
    "vue-confetti": "^0.4.2"
}

```

Obvi­ous­ly for an actu­al website/​app, there would be more pack­ages in **depen­den­cies** ; but we’re focus­ing on the build process.

## Anno­tat­ed webpack.settings.js

I’m also using a sim­i­lar approach I dis­cussed in the [A Bet­ter package.json for the Fron­tend arti­cle](https://dev.to/gaijinity/a-better-package-json-for-the-frontend-1gal-temp-slug-5184040), which is to cor­don off the con­fig that changes from project to project into a sep­a­rate <kbd>webpack.settings.js</kbd>, and keep the web­pack con­fig itself the same.

<aside>
                                The key con­cept is that the only file we need to edit from project to project is the webpack.settings.js
                            </aside>

Since most projects have a very sim­i­lar set of things that need to be done, we can cre­ate a web­pack con­fig that works for a wide vari­ety of projects. We just need to change the data it oper­ates on.

Thus the sep­a­ra­tion of con­cerns between what is in our <kbd>webpack.settings.js</kbd> file (the data that changes from project to project) and what is in our web­pack con­fig (how that data is manip­u­lat­ed to pro­duce an end result).

```

// webpack.settings.js - webpack settings config

// node modules
require('dotenv').config();

// Webpack settings exports
// noinspection WebpackConfigHighlighting
module.exports = {
    name: "Example Project",
    copyright: "Example Company, Inc.",
    paths: {
        src: {
            base: "./src/",
            css: "./src/css/",
            js: "./src/js/"
        },
        dist: {
            base: "./web/dist/",
            clean: [
                '**/*',
            ]
        },
        templates: "./templates/"
    },
    urls: {
        live: "https://example.com/",
        local: "http://example.test/",
        critical: "http://example.test/",
        publicPath: () => process.env.PUBLIC_PATH || "/dist/",
    },
    vars: {
        cssName: "styles"
    },
    entries: {
        "app": "app.js"
    },
    babelLoaderConfig: {
        exclude: [
            /(node_modules|bower_components)/
        ],
    },
    copyWebpackConfig: [
        {
            from: "./src/js/workbox-catch-handler.js",
            to: "js/[name].[ext]"
        }
    ],
    criticalCssConfig: {
        base: "./web/dist/criticalcss/",
        suffix: "_critical.min.css",
        criticalHeight: 1200,
        criticalWidth: 1200,
        ampPrefix: "amp_",
        ampCriticalHeight: 19200,
        ampCriticalWidth: 600,
        pages: [
            {
                url: "",
                template: "index"
            }
        ]
    },
    devServerConfig: {
        public: () => process.env.DEVSERVER_PUBLIC || "http://localhost:8080",
        host: () => process.env.DEVSERVER_HOST || "localhost",
        poll: () => process.env.DEVSERVER_POLL || false,
        port: () => process.env.DEVSERVER_PORT || 8080,
        https: () => process.env.DEVSERVER_HTTPS || false,
    },
    manifestConfig: {
        basePath: ""
    },
    purgeCssConfig: {
        paths: [
            "./templates/**/*.{twig,html}",
            "./src/vue/**/*.{vue,html}"
        ],
        whitelist: [
            "./src/css/components/**/*.{css}"
        ],
        whitelistPatterns: [],
        extensions: [
            "html",
            "js",
            "twig",
            "vue"
        ]
    },
    saveRemoteFileConfig: [
        {
            url: "https://www.google-analytics.com/analytics.js",
            filepath: "js/analytics.js"
        }
    ],
    createSymlinkConfig: [
        {
            origin: "img/favicons/favicon.ico",
            symlink: "../favicon.ico"
        }
    ],
    webappConfig: {
        logo: "./src/img/favicon-src.png",
        prefix: "img/favicons/"
    },
    workboxConfig: {
        swDest: "../sw.js",
        precacheManifestFilename: "js/precache-manifest.[manifestHash].js",
        importScripts: [
            "/dist/js/workbox-catch-handler.js"
        ],
        exclude: [
            /\.(png|jpe?g|gif|svg|webp)$/i,
            /\.map$/,
            /^manifest.*\\.js(?:on)?$/,
        ],
        globDirectory: "./web/",
        globPatterns: [
            "offline.html",
            "offline.svg"
        ],
        offlineGoogleAnalytics: true,
        runtimeCaching: [
            {
                urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
                handler: "CacheFirst",
                options: {
                    cacheName: "images",
                    expiration: {
                        maxEntries: 20
                    }
                }
            }
        ]
    }
};

```

We’ll cov­er what all of these things are down in the web­pack con­fig sec­tions. The impor­tant thing to note here is that we’ve tak­en things that change from project to project, and bro­ken them out of our web­pack con­fig, and into a sep­a­rate <kbd>webpack.settings.js</kbd> file.

This means we can just define what’s dif­fer­ent in each project in our <kbd>webpack.settings.js</kbd> file, and not have to be wran­gling with the web­pack con­fig itself.

Even though the <kbd>webpack.settings.js</kbd> file is just JavaScript, I tried to keep it as JSON-ish as pos­si­ble, so we’re just chang­ing sim­ple set­tings in it. I did­n’t use JSON as a file for­mat for flex­i­bil­i­ty, and also to allow for com­ments to be added.

## Com­mon Con­ven­tions for web­pack configs

I’ve adopt­ed a few con­ven­tions for the web­pack con­fig files <kbd>webpack.common.js</kbd> & <kbd>webpack.prod.js</kbd> to make things more consistent.

Each con­fig file has two inter­nal configs:

- **lega­cy­Con­fig**  — the con­fig that applies to the lega­cy ES5 build
- **mod­ern­Con­fig**  — the con­fig that applies to the mod­ern ES2015+ build

We do it this way because we have sep­a­rate con­fig­u­ra­tions to cre­ate the lega­cy and mod­ern builds. This keeps them log­i­cal­ly sep­a­rate. The <kbd>webpack.common.js</kbd> also has a  **baseC­on­fig** ; this is pure­ly organizational.

Think of it like Object Ori­ent­ed Pro­gram­ming, where the var­i­ous con­figs inher­it from each oth­er, with the **baseC­on­fig** being the root object.

The <kbd>webpack.dev.js</kbd> con­fig does not have a con­cept of lega­cy & mod­ern builds; if we’re work­ing in local dev with <kbd>webpack-dev-server</kbd>, we can assume a mod­ern build.

Anoth­er con­ven­tion that I’ve adopt­ed to keep the con­fig­u­ra­tion clean and read­able is to have <kbd>configure()</kbd> func­tions for the var­i­ous web­pack plu­g­ins and oth­er pieces of web­pack that need con­fig­ur­ing, rather than putting it all inline.

I did this because some data com­ing from the <kbd>webpack.settings.js</kbd> needs to be trans­formed before it can be used by web­pack, and because of the dual legacy/​modern builds, we need to return a dif­fer­ent con­fig depend­ing on the type of build.

It also makes the con­fig files a bit more read­able as well.

As a gen­er­al web­pack con­cept, under­stand that web­pack itself knows only how to load JavaScript and JSON. To load any­thing else, we need to to use a [loader](http://webpack.js.org/concepts/#loaders). We’ll be using a num­ber of dif­fer­ent load­ers in our web­pack config.

## Anno­tat­ed webpack.common.js

Now let’s have a look at our <kbd>webpack.common.js</kbd> con­fig file that has all of the set­tings that are shared by both the <kbd>dev</kbd> and <kbd>prod</kbd> build types.

```

// webpack.common.js - common webpack config
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const path = require('path');
const merge = require('webpack-merge');

// webpack plugins
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const WebpackNotifierPlugin = require('webpack-notifier');

// config files
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');

```

In the pre­am­ble we pull in the Node pack­ages we need, and the web­pack plu­g­ins we use. We then import our <kbd>webpack.settings.js</kbd> as <kbd>settings</kbd> so we can access the set­tings there, and also import our <kbd>package.json</kbd> as <kbd>pkg</kbd> to access a few set­tings there as well.

**CON­FIG­U­RA­TION FUNCTIONS**

Here’s what the <kbd>configureBabelLoader()</kbd> looks like:

```

// Configure Babel loader
const configureBabelLoader = (browserList) => {
    return {
        test: /\.js$/,
        exclude: settings.babelLoaderConfig.exclude,
        cacheDirectory: true,
        use: {
            loader: 'babel-loader',
            options: {
                cacheDirectory: true,
                sourceType: 'unambiguous',
                presets: [
                    [
                        '@babel/preset-env', {
                            modules: false,
                            corejs: {
                                version: 2,
                                proposals: true
                            },
                            useBuiltIns: 'usage',
                            targets: {
                                browsers: browserList,
                            },
                        }
                    ],
                ],
                plugins: [
                    '@babel/plugin-syntax-dynamic-import',
                    '@babel/plugin-transform-runtime',
                ],
            },
        },
    };
};

```

The <kbd>configureBabelLoader()</kbd> func­tion con­fig­ures the <kbd>babel-loader</kbd> to han­dle the load­ing of all files that end in <kbd>.js</kbd>. It uses [@babel/preset-env](http://babeljs.io/docs/en/babel-preset-env) instead of a <kbd>.babelrc</kbd> file so we can keep every­thing com­part­men­tal­ized in our web­pack config.

Babel can com­pile mod­ern ES2015+ JavaScript (and many oth­er lan­guages like Type­Script or Cof­fee­Script) down to JavaScript that tar­gets a spe­cif­ic set of browsers or stan­dards. We pass in the <kbd>browserList</kbd> as a para­me­ter so that we can build [both mod­ern ES2015+ mod­ules and lega­cy ES5 JavaScript](http://philipwalton.com/articles/deploying-es2015-code-in-production-today/) with poly­fills for lega­cy browsers.

By set­ting <kbd>useBuiltIns</kbd> to <kbd>'usage'</kbd> we are also telling babel to apply indi­vid­ual pol­ly­fills on a per-file basis. This can allow for a much small­er bun­dle size, since it includes only what we use. For more on this, check out the [Work­ing with Babel 7 and Web­pack](https://www.thebasement.be/working-with-babel-7-and-webpack/#a-cleaner-approach) article.

In our HTML, we just do some­thing like this:

```

<!-- Browsers with ES module support load this file. -->
<script type="module" src="main.js"></script>

<!-- Older browsers load this file (and module-supporting -->
<!-- browsers know *not* to load this file). -->
<script nomodule src="main-legacy.js"></script>

```

No poly­fills, no fuss. Old browsers ignore the <kbd>type="module"</kbd> script, and get the <kbd>main-legacy.js</kbd>. Mod­ern browsers load the <kbd>main.js</kbd>, and ignore the <kbd>nomodule</kbd>. It’s bril­liant; I wish I came up with the idea! Lest you think it’s fringe, [vue-cli has adopt­ed this strat­e­gy](http://medium.com/the-vue-point/vue-cli-3-0-is-here-c42bebe28fbb) in ver­sion 3.

The [@babel/plugin-syntax-dynamic-import](http://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import) plu­g­in is what allows us to do dynam­ic imports even before the [ECMAScript dynam­ic import pro­pos­al](http://github.com/tc39/proposal-dynamic-import) is imple­ment­ed by web browsers. This lets us load our JavaScript mod­ules [asyn­chro­nous­ly, and dynam­i­cal­ly](http://webpack.js.org/guides/code-splitting/#dynamic-imports) as needed.

So what does this mean? It means we can do some­thing like this:

```

// App main
const main = async () => {
    // Async load the vue module
    const { default: Vue } = await import(/* webpackChunkName: "vue" */ 'vue');
    // Create our vue instance
    const vm = new Vue({
        el: "#app",
        components: {
            'confetti': () => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
        },
    });
    
    return vm;
};
// Execute async function
main().then( (vm) => {
});
// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
    module.hot.accept();
}

```

This does two pri­ma­ry things:

1. Via the <kbd>/* webpackChunkName: "vue" */</kbd> com­ment, we’ve told web­pack what we want this dynam­i­cal­ly code split chunk to be named
2. Since we’re using <kbd>import()</kbd> in an <kbd>async</kbd> func­tion (“<kbd>main</kbd>”), that func­tion <kbd>await</kbd>s the result of our dynam­i­cal­ly loaded JavaScript import while the rest of our code con­tin­ues on its mer­ry way

We’ve effec­tive­ly told web­pack how we want our chunks split up through code, rather than via con­fig. And through the mag­ic of <kbd>@babel/plugin-syntax-dynamic-import</kbd>, this JavaScript chunk can be loaded asyn­chro­nous­ly, on demand as needed.

Notice we did the same thing with our <kbd>.vue</kbd> sin­gle file com­po­nents, too. Nice.

Instead of using <kbd>await</kbd>, we could also just exe­cute our code after the <kbd>import()</kbd> [Promise](http://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) has returned:

```

// Async load the vue module
import(/* webpackChunkName: "vue" */ 'vue').then(Vue => {
    // Vue has loaded, do something with it
    // Create our vue instance
    const vm = new Vue.default({
        el: "#app",
        components: {
            'confetti': () => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
        },
    });
});

```

Here instead of using <kbd>await</kbd> with [import()](http://webpack.js.org/api/module-methods/#import-) we’re using the Promise, so then we know the dynam­ic import has hap­pened and can hap­pi­ly use <kbd>Vue</kbd>.

If you’re pay­ing atten­tion, you can see that we’ve effec­tive­ly solved JavaScript depen­den­cies via Promis­es. Nice!

We can even do fun things like load cer­tain JavaScript chunks only after the user has clicked on some­thing, scrolled to a cer­tain posi­tion, or sat­is­fied some oth­er con­di­tion. Check out the [Mod­ule Meth­ods import()](http://webpack.js.org/api/module-methods/#import-) for more.

If you’re inter­est­ed in learn­ing more about Babel, check out the [Work­ing with Babel 7 and Web­pack](http://www.thebasement.be/working-with-babel-7-and-webpack/) article.

Next up we have <kbd>configureEntries()</kbd>:

```

// Configure Entries
const configureEntries = () => {
    let entries = {};
    for (const [key, value] of Object.entries(settings.entries)) {
        entries[key] = path.resolve(__dirname, settings.paths.src.js + value);
    }

    return entries;
};

```

Here we pull in the web­pack [Entry Points](http://webpack.js.org/concepts/entry-points/) from our <kbd>webpack.settings.js</kbd> via <kbd>settings.entries</kbd>. For a [Sin­gle Page App](https://medium.com/@pshrmn/demystifying-single-page-applications-3068d0555d46) (SPA) you’ll have just one entry point. For a more tra­di­tion­al web­site, you may have sev­er­al entry points (per­haps one per page template).

Either way, because we’ve defined our entry points in our <kbd>webpack.settings.js</kbd>, it’s easy to con­fig­ure them there. An entry point is real­ly just a <kbd>&lt;script src="app.js"&gt;&lt;/script&gt;</kbd> tag that you’ll include in your HTML to boot­strap the JavaScript.

Since we’re using dynam­i­cal­ly import­ed mod­ules, we typ­i­cal­ly would have only one <kbd>&lt;script&gt;&lt;/script&gt;</kbd> tag on a page; the rest of our JavaScript gets loaded dynam­i­cal­ly as needed.

Next we have the <kbd>configureFontLoader()</kbd> function:

```

// Configure Font loader
const configureFontLoader = () => {
    return {
        test: /\.(ttf|eot|woff2?)$/i,
        use: [
            {
                loader: 'file-loader',
                options: {
                    name: 'fonts/[name].[ext]'
                }
            }
        ]
    };
};

```

Font load­ing is the same for both <kbd>dev</kbd> and <kbd>prod</kbd> builds, so we include it here. For any local fonts that we’re using, we can tell web­pack to load them in our JavaScript:

```

import comicsans from '../fonts/ComicSans.woff2';

```

Next we have the <kbd>configureManifest()</kbd> function:

```

// Configure Manifest
const configureManifest = (fileName) => {
    return {
        fileName: fileName,
        basePath: settings.manifestConfig.basePath,
        map: (file) => {
            file.name = file.name.replace(/(\.[a-f0-9]{32})(\..*)$/, '$2');
            return file;
        },
    };
};

```

This con­fig­ures the [web­pack-man­i­fest-plu­g­in](http://github.com/danethurber/webpack-manifest-plugin) for file­name-based cache bust­ing. In a nut­shell, web­pack knows about all of the JavaScript, CSS, and oth­er resources we need, so it can gen­er­ate a man­i­fest that points to the con­tent-hashed name of the resource, e.g.:

```

{
  "vendors~confetti~vue.js": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js",
  "vendors~confetti~vue.js.map": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js.map",
  "app.js": "/dist/js/app.30334b5124fa6e221464.js",
  "app.js.map": "/dist/js/app.30334b5124fa6e221464.js.map",
  "confetti.js": "/dist/js/confetti.1152197f8c58a1b40b34.js",
  "confetti.js.map": "/dist/js/confetti.1152197f8c58a1b40b34.js.map",
  "js/precache-manifest.js": "/dist/js/precache-manifest.f774c437974257fc8026ca1bc693655c.js",
  "../sw.js": "/dist/../sw.js"
}

```

We pass in a file­name because we cre­ate both a mod­ern <kbd>manifest.json</kbd> and a lega­cy <kbd>manifest-legacy.json</kbd> that have the entry points for our mod­ern ES2015+ mod­ules and lega­cy ES5 mod­ules, respec­tive­ly. The keys in both of the man­i­fests are iden­ti­cal for resources that are built for both mod­ern and lega­cy builds.

Next up we have a pret­ty stan­dard look­ing <kbd>configureVueLoader()</kbd>:

```

// Configure Vue loader
const configureVueLoader = () => {
    return {
        test: /\.vue$/,
        loader: 'vue-loader'
    };
};

```

This just lets us load [Vue Sin­gle File Com­po­nents](http://vuejs.org/v2/guide/single-file-components.html) eas­i­ly. web­pack takes care of extract­ing the appro­pri­ate HTML, CSS, and JavaScript for you.

**BASE CON­FIG**

The <kbd>baseConfig</kbd> gets merged with both the <kbd>modernConfig</kbd> and <kbd>legacyConfig</kbd>:

```

// The base webpack config
const baseConfig = {
    name: pkg.name,
    entry: configureEntries(),
    output: {
        path: path.resolve(__dirname, settings.paths.dist.base),
        publicPath: settings.urls.publicPath()
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    module: {
        rules: [
            configureVueLoader(),
        ],
    },
    plugins: [
        new WebpackNotifierPlugin({title: 'Webpack', excludeWarnings: true, alwaysNotify: true}),
        new VueLoaderPlugin(),
    ]
};

```

Every­thing here is pret­ty stan­dard web­pack fare, but note that we alias <kbd>vue$</kbd> to <kbd>vue/dist/vue.esm.js</kbd> so that we can get the ES2015 mod­ule ver­sion of Vue.

We use the [Web­pac­kNo­ti­fier­Plu­g­in](http://www.npmjs.com/package/webpack-notifier) to let us know the sta­tus of our builds in a friend­ly way.

**LEGA­CY CONFIG**

The <kbd>legacyConfig</kbd> is for build­ing ES5 lega­cy JavaScript with the appro­pri­ate polyfills:

```

// Legacy webpack config
const legacyConfig = {
    module: {
        rules: [
            configureBabelLoader(Object.values(pkg.browserslist.legacyBrowsers)),
        ],
    },
    plugins: [
        new CopyWebpackPlugin(
            settings.copyWebpackConfig
        ),
        new ManifestPlugin(
            configureManifest('manifest-legacy.json')
        ),
    ]
};

```

Note that we pass in <kbd>pkg.browserslist.legacyBrowsers</kbd> to <kbd>configureBabelLoader()</kbd>, and we pass in<kbd> 'manifest-legacy.json'</kbd> to <kbd>configureManifest()</kbd>.

We also include the [Copy­Web­pack­Plu­g­in](http://github.com/webpack-contrib/copy-webpack-plugin) in this build, so that we only copy the files defined in <kbd>settings.copyWebpackConfig</kbd> once.

**MOD­ERN CONFIG**

The <kbd>modernConfig</kbd> is for build­ing mod­ern ES2015 JavaScript mod­ules with­out the cruft:

```

// Modern webpack config
const modernConfig = {
    module: {
        rules: [
            configureBabelLoader(Object.values(pkg.browserslist.modernBrowsers)),
        ],
    },
    plugins: [
        new ManifestPlugin(
            configureManifest('manifest.json')
        ),
    ]
};

```

Note that we pass in <kbd>pkg.browserslist.modernBrowsers</kbd> to <kbd>configureBabelLoader()</kbd>, and we pass in<kbd>'manifest.json'</kbd> to <kbd>configureManifest()</kbd>.

**MODULE.EXPORTS**

Final­ly, the <kbd>module.exports</kbd> uses the [web­pack-merge](http://www.npmjs.com/package/webpack-merge) pack­age to merge the con­figs togeth­er, and returns an object that is used by the <kbd>webpack.dev.js</kbd> and <kbd>webpack.prod.js</kbd>.

```

// Common module exports
// noinspection WebpackConfigHighlighting
module.exports = {
    'legacyConfig': merge.strategy({
        module: 'prepend',
        plugins: 'prepend',
    })(
        baseConfig,
        legacyConfig,
    ),
    'modernConfig': merge.strategy({
        module: 'prepend',
        plugins: 'prepend',
    })(
        baseConfig,
        modernConfig,
    ),
};

```

## Anno­tat­ed webpack.dev.js

Now let’s have a look at our <kbd>webpack.dev.js</kbd> con­fig file that has all of the set­tings that are used for devel­op­men­tal builds while we’re work­ing on the project. It gets merged with the set­tings in <kbd>webpack.common.js</kbd> to form a com­plete web­pack configuration.

```

// webpack.dev.js - developmental builds

// node modules
const merge = require('webpack-merge');
const path = require('path');
const webpack = require('webpack');

// webpack plugins
const DashboardPlugin = require('webpack-dashboard/plugin');

// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');

```

In the <kbd>webpack.dev.js</kbd> con­fig, there isn’t a con­cept of mod­ern & lega­cy builds, because in local dev when we’re using <kbd>webpack-dev-server</kbd>, we can assume a mod­ern build.

In the pre­am­ble we again pull in the Node pack­ages we need, and the web­pack plu­g­ins we use. We then import our <kbd>webpack.settings.js</kbd> as <kbd>settings</kbd> so we can access the set­tings there, and also import our <kbd>package.json</kbd> as <kbd>pkg</kbd> to access a few set­tings there as well.

We also import our <kbd>webpack.common.js</kbd> com­mon web­pack con­fig that we’ll merge our dev set­tings with.

**CON­FIG­U­RA­TION FUNCTIONS**

Here’s what the <kbd>configureDevServer()</kbd> looks like:

```

// Configure the webpack-dev-server
const configureDevServer = () => {
    return {
        public: settings.devServerConfig.public(),
        contentBase: path.resolve(__dirname, settings.paths.templates),
        host: settings.devServerConfig.host(),
        port: settings.devServerConfig.port(),
        https: !!parseInt(settings.devServerConfig.https()),
        disableHostCheck: true,
        hot: true,
        overlay: true,
        watchContentBase: true,
        watchOptions: {
            poll: !!parseInt(settings.devServerConfig.poll()),
            ignored: /node_modules/,
        },
        headers: {
            'Access-Control-Allow-Origin': '*'
        },
    };
};

```

When we do a pro­duc­tion build, web­pack bun­dles up all of our var­i­ous assets and saves them to the file sys­tem. By con­trast, when we’re work­ing on a project in local dev, we use a devel­op­ment build via [web­pack-dev-serv­er](http://github.com/webpack/webpack-dev-server) that:

- Spins up a local [Express](http://expressjs.com/) web serv­er that serves our assets
- Builds our assets in mem­o­ry rather than to the file sys­tem, for speed
- Will rebuild assets like JavaScript, CSS, Vue com­po­nents, etc. as we change them and inject them into the web­page via [Hot Mod­ule Replace­ment (HMR)](http://webpack.js.org/concepts/hot-module-replacement/) with­out a page reload
- Will reload the page when we make changes to our templates

This is akin to a much more sophis­ti­cat­ed vari­ant of [Browser­sync](http://browsersync.io/), and great­ly speeds development.

Note that con­fig for the <kbd>webpack-dev-server</kbd> again comes from our <kbd>webpack.settings.js</kbd> file. The defaults are prob­a­bly okay for many peo­ple, but I use [Lar­avel Home­stead](http://laravel.com/docs/5.7/homestead) for local dev, as dis­cussed in the [Local Devel­op­ment with Vagrant / Home­stead](https://dev.to/gaijinity/local-development-with-vagrant-homestead-5gb0-temp-slug-2094211) arti­cle. This means I run all devel­op­ment tool­ing inside of my Home­stead VM.

So instead of hard-cod­ing the local devel­op­ment envi­ron­ment in my <kbd>webpack.settings.js</kbd> file (since it can vary from per­son to per­son work­ing on a team), the <kbd>webpack.settings.js</kbd> can read from an option­al <kbd>.env</kbd> file for your own par­tic­u­lar <kbd>devServer</kbd> config:

```

# webpack example settings for Homestead/Vagrant
PUBLIC_PATH="/dist/"
DEVSERVER_PUBLIC="http://192.168.10.10:8080"
DEVSERVER_HOST="0.0.0.0"
DEVSERVER_POLL=1
DEVSERVER_PORT=8080
DEVSERVER_HTTPS=0

```

You may use some­thing dif­fer­ent, so change the set­tings as appro­pri­ate in your <kbd>.env</kbd> file as need­ed. The idea behind [dotenv](http://www.npmjs.com/package/dotenv) is that we put any­thing spe­cif­ic to an envi­ron­ment in the <kbd>.env</kbd> file, and we **do not** check it in to our git repo. If the <kbd>.env</kbd> file isn’t present, that’s fine, it just uses default values:

```

devServerConfig: {
    public: () => process.env.DEVSERVER_PUBLIC || "http://localhost:8080",
    host: () => process.env.DEVSERVER_HOST || "localhost",
    poll: () => process.env.DEVSERVER_POLL || false,
    port: () => process.env.DEVSERVER_PORT || 8080,
    https: () => process.env.DEVSERVER_HTTPS || false,
},
urls: {
    live: "https://example.com/",
    local: "http://example.test/",
    critical: "http://example.test/",
    publicPath: () => process.env.PUBLIC_PATH || "/dist/",
},

```

We also use the <kbd>PUBLIC_PATH</kbd> .env vari­able (if present) to allow for per-envi­ron­ment builds of the pro­duc­tion build. This is so that we can do a local pro­duc­tion build, or we can do a dis­tri­b­u­tion pro­duc­tion build in a Dock­er con­tain­er that builds with URLs ready for dis­tri­b­u­tion via a CDN.

Next up is the <kbd>configureImageLoader()</kbd>:

```

// Configure Image loader
const configureImageLoader = () => {
    return {
        test: /\.(png|jpe?g|gif|svg|webp)$/i,
        use: [
            {
                loader: 'file-loader',
                options: {
                    name: 'img/[name].[hash].[ext]'
                }
            }
        ]
    };
};

```

It’s impor­tant to note that this is only for images that are includ­ed in our web­pack build; many oth­er images will be com­ing from else­where (a CMS sys­tem, an asset man­age­ment sys­tem, etc.).

To let web­pack know about an image, you import it into your JavaScript:

```

import Icon from './icon.png';

```

Check out the [Load­ing Images](http://webpack.js.org/guides/asset-management/#loading-images) sec­tion of the web­pack docs for more details on this.

Next up is our <kbd>configurePostcssLoader()</kbd>:

```

// Configure the Postcss loader
const configurePostcssLoader = () => {
    return {
        test: /\.(pcss|css)$/,
        use: [
            {
                loader: 'style-loader',
            },
            {
                loader: 'vue-style-loader',
            },
            {
                loader: 'css-loader',
                options: {
                    url: false,
                    importLoaders: 2,
                    sourceMap: true
                }
            },
            {
                loader: 'resolve-url-loader'
            },
            {
                loader: 'postcss-loader',
                options: {
                    sourceMap: true
                }
            }
        ]
    };
};

```

We use [PostC­SS](http://postcss.org/) to process all of our CSS, includ­ing [Tail­wind CSS](http://tailwindcss.com/docs/what-is-tailwind/). I think of it as the Babel of CSS, in that it com­piles all sorts of advanced CSS func­tion­al­i­ty down to plain old CSS that your browsers can understand.

It’s impor­tant to note that for web­pack load­ers, they are processed in reverse order that they are listed:

- **[postc­ss-loader](http://github.com/postcss/postcss-loader)** — Loads and process­es files as PostCSS
- **[resolve-url-loader](http://www.npmjs.com/package/resolve-url-loader)** — Rewrites any <kbd>url()</kbd>s in our CSS to pub­lic path relative
- **[css-loader](http://github.com/webpack-contrib/css-loader)** — Resolves all of our CSS <kbd>@import</kbd> and <kbd>url()</kbd>s
- **[vue-style-loader](http://github.com/vuejs/vue-style-loader)** — Injects all of our CSS from <kbd>.vue</kbd> Sin­gle File Com­po­nents linline
- **[style-loader](http://github.com/webpack-contrib/style-loader)** — Injects all of our CSS into the doc­u­ment inline in <kbd>&lt;style&gt;&lt;/style&gt;</kbd> tags

Remem­ber, since this is what we do in local devel­op­ment, we don’t need to do any­thing fan­cy in terms of extract­ing all of our CSS out into a min­i­mized file. Instead, we just let the <kbd>style-loader</kbd> inline it all in our document.

The <kbd>webpack-dev-server</kbd> will use Hot Mod­ule Replace­ment (HMR) for our CSS, so any time we change any­thing, it rebuilds our CSS and re-injects it auto­mat­i­cal­ly. It’s some­what magical.

We tell web­pack about our CSS by includ­ing it:

```

import styles from '../css/app.pcss';

```

This is dis­cussed in detail in the [Load­ing CSS](http://webpack.js.org/guides/asset-management/#loading-css) sec­tion of the web­pack docs.

We do this from our <kbd>App.js</kbd> entry point; think of this as the PostC­SS entry point. The <kbd>app.pcss</kbd> file <kbd>@import</kbd>s all of the CSS that our project uses; this will be cov­ered in detail lat­er on. 

**MODULE.EXPORTS**

Final­ly, the <kbd>module.exports</kbd> uses the [web­pack-merge](http://www.npmjs.com/package/webpack-merge) pack­age to merge the <kbd>common.modernConfig</kbd> with our dev config:

```

// Development module exports
module.exports = merge(
    common.modernConfig,
    {
        output: {
            filename: path.join('./js', '[name].[hash].js'),
            publicPath: settings.devServerConfig.public() + '/',
        },
        mode: 'development',
        devtool: 'inline-source-map',
        devServer: configureDevServer(),
        module: {
            rules: [
                configurePostcssLoader(),
                configureImageLoader(),
            ],
        },
        plugins: [
            new webpack.HotModuleReplacementPlugin(),
            new DashboardPlugin(),
        ],
    }
);

```

By set­ting the <kbd>mode</kbd> to <kbd>'development'</kbd> we’re telling web­pack that this is a [devel­op­ment build](http://webpack.js.org/concepts/mode/).

By set­ting <kbd>devtool</kbd> to <kbd>'inline-source-map'</kbd> we’re ask­ing for our <kbd>.map</kbd>s for our CSS/​JavaScript to be [inlined into the files them­selves](http://webpack.js.org/configuration/devtool/). This makes the files huge, but it’s con­ve­nient for debug­ging purposes.

The [webpack.HotModuleReplacementPlugin](http://webpack.js.org/plugins/hot-module-replacement-plugin/) enables sup­port for Hot Mod­ule Replace­ment (HMR) on the web­pack side of things.

The [Dash­board­Plu­g­in](http://github.com/FormidableLabs/webpack-dashboard) plu­g­in lets us feel like an astro­naut with a fan­cy web­pack build HUD:

![Webpack Dashboard Plugin](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x699_crop_center-center_100_line/webpack-dashboard-plugin.png)

I’ve found the Dash­board­Plu­g­in devel­op­ment HUD to be sig­nif­i­cant­ly more use­ful than the default web­pack progress scroll.

If you find that you need to debug the web­pack con­fig itself, you can use <kbd>yarn run debug</kbd> or <kbd>npm run debug</kbd> to run the local devel­op­ment build but bypass the <kbd>webpack-dashboard</kbd>.

And that’s it, we now have a nice devel­op­ment build for our projects; check out the [Hot Mod­ule Replace­ment video](http://www.youtube.com/watch?v=9-n2Ah0aowo) for an exam­ple of this in action:

## Anno­tat­ed webpack.prod.js

Now let’s have a look at our <kbd>webpack.prod.js</kbd> con­fig file that has all of the set­tings that are used for pro­duc­tion builds while we’re work­ing on the project. It gets merged with the set­tings in <kbd>webpack.common.js</kbd> to form a com­plete web­pack configuration.

```

// webpack.prod.js - production builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const git = require('git-rev-sync');
const glob = require('glob-all');
const merge = require('webpack-merge');
const moment = require('moment');
const path = require('path');
const webpack = require('webpack');

// webpack plugins
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CreateSymlinkPlugin = require('create-symlink-webpack-plugin');
const CriticalCssPlugin = require('critical-css-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ImageminWebpWebpackPlugin = require('imagemin-webp-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const SaveRemoteFilePlugin = require('save-remote-file-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const WhitelisterPlugin = require('purgecss-whitelister');
const WorkboxPlugin = require('workbox-webpack-plugin');

// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');

```

In the pre­am­ble we again pull in the Node pack­ages we need, and the web­pack plu­g­ins we use. We then import our <kbd>webpack.settings.js</kbd> as <kbd>settings</kbd> so we can access the set­tings there, and also import our <kbd>package.json</kbd> as <kbd>pkg</kbd> to access a few set­tings there as well. 

We also import our <kbd>webpack.common.js</kbd> com­mon web­pack con­fig that we’ll merge our dev set­tings with.

**TAIL­WIND EXTRACTOR**

This class is a cus­tom [PurgeC­SS](http://www.purgecss.com/) extrac­tor for [Tail­wind CSS](http://tailwindcss.com/docs/what-is-tailwind/) that allows spe­cial char­ac­ters in class names.

```

// Custom PurgeCSS extractor for Tailwind that allows special characters in
// class names.
//
// https://github.com/FullHuman/purgecss#extractor
class TailwindExtractor {
    static extract(content) {
        return content.match(/[A-Za-z0-9-_:\/]+/g) || [];
    }
}

```

This is tak­en from the [Remov­ing unused CSS with PurgeC­SS](http://tailwindcss.com/docs/controlling-file-size/#removing-unused-css-with-purgecss) sec­tion of the Tail­wind CSS docs. See below for details on how this extrac­tor works with PurgeC­SS to mag­i­cal­ly make your CSS svelte and tidy.

**CON­FIG­U­RA­TION FUNCTIONS**

Here’s what the <kbd>configureBanner()</kbd> looks like:

```

// Configure file banner
const configureBanner = () => {
    return {
        banner: [
            '/*!',
            ' * @project ' + settings.name,
            ' * @name ' + '[filebase]',
            ' * @author ' + pkg.author.name,
            ' * @build ' + moment().format('llll') + ' ET',
            ' * @release ' + git.long() + ' [' + git.branch() + ']',
            ' * @copyright Copyright (c) ' + moment().format('YYYY') + ' ' + settings.copyright,
            ' *',
            ' */',
            ''
        ].join('\n'),
        raw: true
    };
};

```

This sim­ply adds a ban­ner with project name, file name, author, and git infor­ma­tion for each file we build.

Next up is the <kbd>configureBundleAnalyzer()</kbd>:

```

// Configure Bundle Analyzer
const configureBundleAnalyzer = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            analyzerMode: 'static',
            reportFilename: 'report-legacy.html',
        };
    }
    if (buildType === MODERN_CONFIG) {
        return {
            analyzerMode: 'static',
            reportFilename: 'report-modern.html',
        };
    }
};

```

This uses the [Web­pack­Bundle­An­a­lyz­er](http://www.npmjs.com/package/webpack-bundle-analyzer) plu­g­in to gen­er­ate a report for both our mod­ern and lega­cy bun­dle builds that results in a self-con­tained inter­ac­tive HTML page that allows you to explore what exact­ly is in the bun­dle that has been gen­er­at­ed by webpack.

![Webpack Bundle Analyzer](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/webpack-bundle-analyzer.jpg)

I’ve found it to be very use­ful to help me keep my bun­dle sizes down, and under­stand exact­ly what web­pack is build­ing, so I’ve made it part of my pro­duc­tion build process.

Next up is the <kbd>configureCriticalCss()</kbd>:

```

// Configure Critical CSS
const configureCriticalCss = () => {
    return (settings.criticalCssConfig.pages.map((row) => {
            const criticalSrc = settings.urls.critical + row.url;
            const criticalDest = settings.criticalCssConfig.base + row.template + settings.criticalCssConfig.suffix;
            let criticalWidth = settings.criticalCssConfig.criticalWidth;
            let criticalHeight = settings.criticalCssConfig.criticalHeight;
            // Handle Google AMP templates
            if (row.template.indexOf(settings.criticalCssConfig.ampPrefix) !== -1) {
                criticalWidth = settings.criticalCssConfig.ampCriticalWidth;
                criticalHeight = settings.criticalCssConfig.ampCriticalHeight;
            }
            console.log("source: " + criticalSrc + " dest: " + criticalDest);
            return new CriticalCssPlugin({
                base: './',
                src: criticalSrc,
                dest: criticalDest,
                extract: false,
                inline: false,
                minify: true,
                width: criticalWidth,
                height: criticalHeight,
            })
        })
    );
};

```

This uses the [Crit­i­calC­ss­Plu­g­in](http://www.npmjs.com/package/critical-css-webpack-plugin) to gen­er­ate Crit­i­calC­SS for our web­site by chunk­ing through the <kbd>settings.criticalCssConfig.pages</kbd> from our <kbd>webpack.settings.js</kbd>.

Note that if the page passed in has <kbd>settings.criticalCssConfig.ampPrefix</kbd> any­where in its name, it gen­er­ates Crit­i­calC­SS for the entire web­page (not just the above the fold con­tent) by pass­ing in a very large height.

I won’t go into too much detail on Crit­i­calC­SS here; check out the [Imple­ment­ing Crit­i­cal CSS on your web­site](https://dev.to/gaijinity/implementing-critical-css-on-your-website-37d5-temp-slug-6012917) arti­cle for more infor­ma­tion on CriticalCSS.

Next up is the <kbd>configureCleanWebpack()</kbd>:

```

// Configure Clean webpack
const configureCleanWebpack = () => {
    return {
        cleanOnceBeforeBuildPatterns: settings.paths.dist.clean,
        verbose: true,
        dry: false
    };
};

```

This just uses the [Clean­Web­pack­Plu­g­in](http://github.com/johnagan/clean-webpack-plugin) to delete the build direc­to­ry in <kbd>settings.paths.dist.base</kbd> from our <kbd>webpack.settings.js</kbd>.

Next up is <kbd>configureCompression()</kbd>:

```

// Configure Compression webpack plugin
const configureCompression = () => {
    return {
        filename: '[path].gz[query]',
        test: /\.(js|css|html|svg)$/,
        threshold: 10240,
        minRatio: 0.8,
        deleteOriginalAssets: false,
        compressionOptions: {
            numiterations: 15,
            level: 9
        },
        algorithm(input, compressionOptions, callback) {
            return zopfli.gzip(input, compressionOptions, callback);
        }
    };
};

```

This uses the [Com­pres­sion­Plu­g­in](http://github.com/webpack-contrib/compression-webpack-plugin) to pre-com­press our sta­t­ic resources into <kbd>.gz</kbd> files so we can [serve them up pre-com­pressed](https://medium.com/@selvaganesh93/how-to-serve-webpack-gzipped-file-in-production-using-nginx-692eadbb9f1c) via a sim­ple [web­serv­er con­fig](http://theartofmachinery.com/2016/06/06/nginx_gzip_static.html).

Next up is <kbd>configureHtml()</kbd>:

```

// Configure Html webpack
const configureHtml = () => {
    return {
        templateContent: '',
        filename: 'webapp.html',
        inject: false,
    };
};

```

This uses the [Html­Web­pack­Plu­g­in](http://webpack.js.org/plugins/html-webpack-plugin/) in con­junc­tion with the [Webap­p­Web­pack­Plu­g­in](http://github.com/brunocodutra/webapp-webpack-plugin) (see below) to gen­er­ate the HTML for our fav­i­cons. Note that we pass in an emp­ty string in <kbd>templateContent</kbd> so that the out­put is just the raw out­put from the WebappWebpackPlugin.

Next up is the <kbd>configureImageLoader()</kbd>:

```

// Configure Image loader
const configureImageLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(png|jpe?g|gif|svg|webp)$/i,
            use: [
                {
                    loader: 'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'
                    }
                }
            ]
        };
    }
    if (buildType === MODERN_CONFIG) {
        return {
            test: /\.(png|jpe?g|gif|svg|webp)$/i,
            use: [
                {
                    loader: 'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'
                    }
                },
                {
                    loader: 'img-loader',
                    options: {
                        plugins: [
                            require('imagemin-gifsicle')({
                                interlaced: true,
                            }),
                            require('imagemin-mozjpeg')({
                                progressive: true,
                                arithmetic: false,
                            }),
                            require('imagemin-optipng')({
                                optimizationLevel: 5,
                            }),
                            require('imagemin-svgo')({
                                plugins: [
                                    {convertPathData: false},
                                ]
                            }),
                        ]
                    }
                }
            ]
        };
    }
};

```

We pass in the <kbd>buildType</kbd> so that we can return dif­fer­ent results depend­ing on whether it is a lega­cy or mod­ern build. In this case, we run images through a vari­ety of image opti­miza­tions via [img-loader](http://www.npmjs.com/package/img-loader) for the mod­ern build.

We only do this for the mod­ern build, because there’s no sense in spend­ing the time to opti­mize the images for both the mod­ern and the lega­cy builds (the images are the same for both).

It’s impor­tant to note that this is only for images that are includ­ed in our web­pack build; many oth­er images will be com­ing from else­where (a CMS sys­tem, an asset man­age­ment sys­tem, etc.).

To let web­pack know about an image, you import it into your JavaScript:

```

import Icon from './icon.png';

```

Check out the [Load­ing Images](http://webpack.js.org/guides/asset-management/#loading-images) sec­tion of the web­pack docs for more details on this.

Next up is our <kbd>configureOptimization()</kbd>:

```

// Configure optimization
const configureOptimization = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            splitChunks: {
                cacheGroups: {
                    default: false,
                    common: false,
                    styles: {
                        name: settings.vars.cssName,
                        test: /\.(pcss|css|vue)$/,
                        chunks: 'all',
                        enforce: true
                    }
                }
            },
            minimizer: [
                new TerserPlugin(
                    configureTerser()
                ),
                new OptimizeCSSAssetsPlugin({
                    cssProcessorOptions: {
                        map: {
                            inline: false,
                            annotation: true,
                        },
                        safe: true,
                        discardComments: true
                    },
                })
            ]
        };
    }
    if (buildType === MODERN_CONFIG) {
        return {
            minimizer: [
                new TerserPlugin(
                    configureTerser()
                ),
            ]
        };
    }
};

```

This is where we con­fig­ure the [web­pack pro­duc­tion opti­miza­tion](http://webpack.js.org/configuration/optimization/). For the lega­cy build only (there’s no sense in doing it twice), we use the [MiniC­s­sEx­tract­Plu­g­in](http://github.com/webpack-contrib/mini-css-extract-plugin) to extract all of the CSS used project-wide into a sin­gle file. If you’ve used web­pack before, you might have used the Extract­TextPlu­g­in to do this in the past; no more.

We then also use the [Opti­mizeC­SSAs­set­sPlu­g­in](http://github.com/NMFR/optimize-css-assets-webpack-plugin) to opti­mize the result­ing CSS by remov­ing dupli­cate rules, and min­i­miz­ing the CSS via <kbd>cssnano</kbd>.

Final­ly, we set the JavaScript min­i­miz­er to be the [Terser­Plu­g­in](http://github.com/webpack-contrib/terser-webpack-plugin); this is because the [Ugli­fyJs­Plu­g­in no longer sup­ports](http://github.com/webpack-contrib/uglifyjs-webpack-plugin/releases/tag/v2.0.0) min­i­miz­ing ES2015+ JavaScript. And since we’re gen­er­at­ing mod­ern ES2015+ bun­dles, we need it.

Next up is the <kbd>configurePostcssLoader()</kbd>:

```

// Configure Postcss loader
const configurePostcssLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(pcss|css)$/,
            use: [
                MiniCssExtractPlugin.loader,
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2,
                        sourceMap: true
                    }
                },
                {
                    loader: 'resolve-url-loader'
                },
                {
                    loader: 'postcss-loader',
                    options: {
                        sourceMap: true
                    }
                }
            ]
        };
    }
    // Don't generate CSS for the modern config in production
    if (buildType === MODERN_CONFIG) {
        return {
            test: /\.(pcss|css)$/,
            loader: 'ignore-loader'
        };
    }
};

```

This looks very sim­i­lar to the dev ver­sion of <kbd>configurePostcssLoader()</kbd>, except that for our final loader, we use the [MiniCssExtractPlugin.loader](http://github.com/webpack-contrib/mini-css-extract-plugin) to extract all of our CSS into a sin­gle file.

We do this only for the lega­cy build, since there’s no sense in doing it for each build (the CSS is the same). We use the [ignore-loader](http://www.npmjs.com/package/ignore-loader) for mod­ern builds, so a loader exists for our .css & .pcss files, but it does nothing.

As men­tioned ear­li­er, we use [PostC­SS](http://postcss.org/) to process all of our CSS, includ­ing [Tail­wind CSS](http://tailwindcss.com/docs/what-is-tailwind/). I think of it as the Babel of CSS, in that it com­piles all sorts of advanced CSS func­tion­al­i­ty down to plain old CSS that your browsers can understand.

Again, it’s impor­tant to note that for web­pack load­ers, they are processed in reverse order that they are listed:

- **[postc­ss-loader](http://github.com/postcss/postcss-loader)** — Loads and process­es files as PostCSS
- **[resolve-url-loader](http://www.npmjs.com/package/resolve-url-loader)** — Rewrites any <kbd>url()</kbd>s in our CSS to pub­lic path relative
- **[css-loader](http://github.com/webpack-contrib/css-loader)** — Resolves all of our CSS <kbd>@import</kbd> and <kbd>url()</kbd>s
- **[MiniCssExtractPlugin.loader](http://github.com/webpack-contrib/mini-css-extract-plugin)** — Extracts all of our pro­duc­tion CSS to a sin­gle file

Since this is a pro­duc­tion build, we pull out all of the CSS used every­where with the <kbd>MiniCssExtractPlugin.loader</kbd>, and save it to a sin­gle <kbd>.css</kbd> file. The CSS also gets min­i­mized, and opti­mized for production.

We tell web­pack about our CSS by includ­ing it:

```

import styles from '../css/app.pcss';

```

This is dis­cussed in detail in the [Load­ing CSS](http://webpack.js.org/guides/asset-management/#loading-css) sec­tion of the web­pack docs.

We do this from our <kbd>App.js</kbd> entry point; think of this as the PostC­SS entry point. The <kbd>app.pcss</kbd> file <kbd>@import</kbd>s all of the CSS that our project uses; this will be cov­ered in detail lat­er on.

Next up is the <kbd>configurePurgeCss()</kbd>:

```

// Configure PurgeCSS
const configurePurgeCss = () => {
    let paths = [];
    // Configure whitelist paths
    for (const [key, value] of Object.entries(settings.purgeCssConfig.paths)) {
        paths.push(path.join(__dirname, value));
    }

    return {
        paths: glob.sync(paths),
        whitelist: WhitelisterPlugin(settings.purgeCssConfig.whitelist),
        whitelistPatterns: settings.purgeCssConfig.whitelistPatterns,
        extractors: [
            {
                extractor: TailwindExtractor,
                extensions: settings.purgeCssConfig.extensions
            }
        ]
    };
};

```

[Tail­wind CSS](http://tailwindcss.com/docs/what-is-tailwind/) is a fan­tas­tic util­i­ty-first CSS frame­work that allows for rapid pro­to­typ­ing because in local devel­op­ment, you rarely have to actu­al­ly write any CSS. Instead, you just use the pro­vid­ed util­i­ty CSS classes.

The down­side is that the result­ing CSS can be a lit­tle large. This is where [PurgeC­SS](http://www.purgecss.com/) comes in. It will parse through all of your HTML/​template/​Vue/​whatever files, and strip out any unused CSS.

The sav­ings can be dra­mat­ic; Tail­wind CSS and PurgeC­SS are a match made in heav­en. We talked about this in depth on the [Tail­wind CSS util­i­ty-first CSS with Adam Wathan](http://devmode.fm/episodes/tailwind-css-utility-first-css-with-adam-watham) podcast.

It iter­ates through all of the path globs in <kbd>settings.purgeCssConfig.paths</kbd> look­ing for CSS rules to keep; any CSS rules not found get stripped out of our result­ing CSS build.

We also use the [Whitelis­ter­Plu­g­in](http://www.npmjs.com/package/purgecss-whitelister) to make it easy to whitelist entire files or even globs when we know we don’t want cer­tain CSS stripped. The CSS rules in all of the files that match our <kbd>settings.purgeCssConfig.whitelist</kbd> are whitelist­ed, and nev­er stripped from the result­ing build.

Next up is <kbd>configureTerser()</kbd>:

```

// Configure terser
const configureTerser = () => {
    return {
        cache: true,
        parallel: true,
        sourceMap: true
    };
};

```

This just con­fig­ures some set­tings used by the [Terser­Plu­g­in](http://github.com/webpack-contrib/terser-webpack-plugin) that min­i­mizes both our lega­cy and mod­ern JavaScript code.

Next up is the <kbd>configureWebApp()</kbd>:

```

// Configure Webapp webpack
const configureWebapp = () => {
    return {
        logo: settings.webappConfig.logo,
        prefix: settings.webappConfig.prefix,
        cache: false,
        inject: 'force',
        favicons: {
            appName: pkg.name,
            appDescription: pkg.description,
            developerName: pkg.author.name,
            developerURL: pkg.author.url,
            path: settings.paths.dist.base,
        }
    };
};

```

This uses the [Webap­p­Web­pack­Plu­g­in](http://www.npmjs.com/package/webapp-webpack-plugin) to gen­er­ate all of our site fav­i­cons in a myr­i­ad of for­mats, as well as our webapp <kbd>manifest.json</kbd> and oth­er PWA niceties.

It works in con­junc­tion with the [Html­Web­pack­Plu­g­in](http://webpack.js.org/plugins/html-webpack-plugin/) to also out­put a <kbd>webapp.html</kbd> file that con­tains links to all of the gen­er­at­ed fav­i­cons and asso­ci­at­ed files, for inclu­sion in our HTML page’s <kbd>&lt;head&gt;&lt;/head&gt;</kbd>.

Next up is the <kbd>configureWorkbox()</kbd>:

```

// Configure Workbox service worker
const configureWorkbox = () => {
    let config = settings.workboxConfig;

    return config;
};

```

We use Google’s [Work­boxWeb­pack­Plu­g­in](http://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin) to gen­er­ate a [Ser­vice Work­er](http://developers.google.com/web/fundamentals/primers/service-workers/) for our web­site. It’s beyond the scope of this arti­cle explain what a Ser­vice Work­er is, but you can check out the [Going Offline: Ser­vice Work­ers with Jere­my Kei­th](http://devmode.fm/episodes/going-offline-service-workers-with-jeremy-keith) pod­cast for a primer.

The con­fig­u­ra­tion all comes from the <kbd>settings.workboxConfig</kbd> object in our <kbd>webpack.settings.js</kbd>. In addi­tion to pre-caching all of the assets in our mod­ern build <kbd>manifest.json</kbd>, we also include a <kbd>workbox-catch-handler.js</kbd> to con­fig­ure it to use a [fall­back response catch-all route](http://developers.google.com/web/tools/workbox/guides/advanced-recipes#provide_a_fallback_response_to_a_route).

```

// fallback URLs
const FALLBACK_HTML_URL = '/offline.html';
const FALLBACK_IMAGE_URL = '/offline.svg';

// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
// https://developers.google.com/web/tools/workbox/guides/advanced-recipes#provide_a_fallback_response_to_a_route
workbox.routing.setCatchHandler(({event, request, url}) => {
    // Use event, request, and url to figure out how to respond.
    // One approach would be to use request.destination, see
    // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
    switch (request.destination) {
        case 'document':
            return caches.match(FALLBACK_HTML_URL);
            break;

        case 'image':
            return caches.match(FALLBACK_IMAGE_URL);
            break;

        default:
            // If we don't have a fallback, just return an error response.
            return Response.error();
    }
});

// Use a stale-while-revalidate strategy for all other requests.
workbox.routing.setDefaultHandler(
    workbox.strategies.staleWhileRevalidate()
);

```

**MODULE.EXPORTS**

Final­ly, the <kbd>module.exports</kbd> uses the [web­pack-merge](http://www.npmjs.com/package/webpack-merge) to merge the <kbd>common.legacyConfig</kbd> from the <kbd>webpack.common.js</kbd> with our pro­duc­tion lega­cy con­fig, and the <kbd>common.modernConfig</kbd> with our pro­duc­tion mod­ern config:

```

// Production module exports
module.exports = [
    merge(
        common.legacyConfig,
        {
            output: {
                filename: path.join('./js', '[name]-legacy.[chunkhash].js'),
            },
            mode: 'production',
            devtool: 'source-map',
            optimization: configureOptimization(LEGACY_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(LEGACY_CONFIG),
                    configureImageLoader(LEGACY_CONFIG),
                ],
            },
            plugins: [
                new MiniCssExtractPlugin({
                    path: path.resolve(__dirname, settings.paths.dist.base),
                    filename: path.join('./css', '[name].[chunkhash].css'),
                }),
                new PurgecssPlugin(
                    configurePurgeCss()
                ),
                new webpack.BannerPlugin(
                    configureBanner()
                ),
                new HtmlWebpackPlugin(
                    configureHtml()
                ),
                new WebappWebpackPlugin(
                    configureWebapp()
                ),
                new CreateSymlinkPlugin(
                    settings.createSymlinkConfig,
                    true
                ),
                new SaveRemoteFilePlugin(
                    settings.saveRemoteFileConfig
                ),
                new BundleAnalyzerPlugin(
                    configureBundleAnalyzer(LEGACY_CONFIG),
                ),
            ].concat(
                configureCriticalCss()
            )
        }
    ),
    merge(
        common.modernConfig,
        {
            output: {
                filename: path.join('./js', '[name].[chunkhash].js'),
            },
            mode: 'production',
            devtool: 'source-map',
            optimization: configureOptimization(MODERN_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(MODERN_CONFIG),
                    configureImageLoader(MODERN_CONFIG),
                ],
            },
            plugins: [
                new CleanWebpackPlugin(
                    configureCleanWebpack()
                ),
                new webpack.BannerPlugin(
                    configureBanner()
                ),
                new ImageminWebpWebpackPlugin(),
                new WorkboxPlugin.GenerateSW(
                    configureWorkbox()
                ),
                new BundleAnalyzerPlugin(
                    configureBundleAnalyzer(MODERN_CONFIG),
                ),
            ]
        }
    ),
];

```

By return­ing an array in our <kbd>module.exports</kbd>, we’re telling web­pack that we have more than one com­pile that needs to be done: one for our lega­cy build, and anoth­er for our mod­ern build.

Note that for the lega­cy build, we out­put processed JavaScript as <kbd>[name]-legacy.[hash].js</kbd>, where­as the mod­ern build out­puts it as <kbd>[name].[hash].js</kbd>.

By set­ting the <kbd>mode</kbd> to <kbd>'production'</kbd> we’re telling web­pack that this is a [pro­duc­tion build](http://webpack.js.org/concepts/mode/). This enables a num­ber of set­tings appro­pri­ate for a pro­duc­tion build.

By set­ting <kbd>devtool</kbd> to <kbd>'source-map'</kbd> we’re ask­ing for our <kbd>.map</kbd>s for our CSS/​JavaScript to be [gen­er­at­ed as sep­a­rate .map files](http://webpack.js.org/configuration/devtool/). This makes it eas­i­er for us to debug live pro­duc­tion web­sites with­out adding the file size of our assets.

There are a cou­ple of web­pack plu­g­ins used here that we haven’t cov­ered already:

- **[Cre­ateSym­linkPlu­g­in](http://www.npmjs.com/package/create-symlink-webpack-plugin)** — this is a plu­g­in I cre­at­ed to allow for sym­link cre­ation as part of the build process. I use it to sym­link the gen­er­at­ed <kbd>favicon.ico</kbd> to <kbd>/favicon.ico</kbd> because many web browsers look for in the web root.
- **[SaveR­e­mote­File­Plu­g­in](http://www.npmjs.com/package/save-remote-file-webpack-plugin)** — this is a plu­g­in I cre­at­ed to down­load remote files and emit them as part of the web­pack build process. I use this for down­load­ing and serv­ing up Google’s <kbd>analytics.js</kbd> locally.
- **[Imagem­inWebp­Web­pack­Plu­g­in](http://www.npmjs.com/package/imagemin-webp-webpack-plugin)** — this plu­g­in cre­ates <kbd>.webp</kbd> vari­ants of all of the JPEG and PNG files that your project imports

And that’s it, we now have a nice pro­duc­tion build for our projects with all of the bells & whistles.

## Tail­wind CSS & PostC­SS Config

To make web­pack build Tail­wind CSS and the rest of our CSS prop­er­ly, we need to do a lit­tle set­up. Cred­it to my part­ner in crime, Jonathan Melville, for work­ing this aspect of the build out. First we need a <kbd>postcss.config.js</kbd> file:

```

module.exports = {
    plugins: [
        require('postcss-import')({
            plugins: [
                require('stylelint')
            ]
        }),
        require('tailwindcss')('./tailwind.config.js'),
        require('postcss-preset-env')({
            autoprefixer: { grid: true },
            features: {
                'nesting-rules': true
            }
        })
    ]
};

```

This can be stored in the project root; PostC­SS will look for it auto­mat­i­cal­ly as part of the build process, and apply the PostC­SS plu­g­ins we’ve spec­i­fied. Note this is where we include the <kbd>tailwind.config.js</kbd> file to make it part of the build process.

Final­ly, our CSS entry point <kbd>app.pcss</kbd> looks some­thing like this:

```

/**
 * app.css
 *
 * The entry point for the css.
 *
 */

/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 *
 * You can see the styles here:
 * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css
 */
 @import "tailwindcss/preflight";

/**
 * This injects any component classes registered by plugins.
 *
 */
@import 'tailwindcss/components';

/**
 * Here we add custom component classes; stuff we want loaded
 * *before* the utilities so that the utilities can still
 * override them.
 *
 */
@import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss';

/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 */
@import 'tailwindcss/utilities';

/**
 * Include styles for individual pages
 *
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 *
 */
 @import 'vendor.pcss';

```

Obvi­ous­ly, tai­lor it to include what­ev­er components/​pages that you use for your cus­tom CSS.

## Post-Build Project Tree

Here’s what our project tree looks like post-build:

```

├── example.env
├── package.json
├── postcss.config.js
├── src
│   ├── css
│   │   ├── app.pcss
│   │   ├── components
│   │   │   ├── global.pcss
│   │   │   ├── typography.pcss
│   │   │   └── webfonts.pcss
│   │   ├── pages
│   │   │   └── homepage.pcss
│   │   └── vendor.pcss
│   ├── fonts
│   ├── img
│   │   └── favicon-src.png
│   ├── js
│   │   ├── app.js
│   │   └── workbox-catch-handler.js
│   └── vue
│   └── Confetti.vue
├── tailwind.config.js
├── templates
├── web
│   ├── dist
│   │   ├── criticalcss
│   │   │   └── index_critical.min.css
│   │   ├── css
│   │   │   ├── styles.d833997e3e3f91af64e7.css
│   │   │   └── styles.d833997e3e3f91af64e7.css.map
│   │   ├── img
│   │   │   └── favicons
│   │   │   ├── android-chrome-144x144.png
│   │   │   ├── android-chrome-192x192.png
│   │   │   ├── android-chrome-256x256.png
│   │   │   ├── android-chrome-36x36.png
│   │   │   ├── android-chrome-384x384.png
│   │   │   ├── android-chrome-48x48.png
│   │   │   ├── android-chrome-512x512.png
│   │   │   ├── android-chrome-72x72.png
│   │   │   ├── android-chrome-96x96.png
│   │   │   ├── apple-touch-icon-114x114.png
│   │   │   ├── apple-touch-icon-120x120.png
│   │   │   ├── apple-touch-icon-144x144.png
│   │   │   ├── apple-touch-icon-152x152.png
│   │   │   ├── apple-touch-icon-167x167.png
│   │   │   ├── apple-touch-icon-180x180.png
│   │   │   ├── apple-touch-icon-57x57.png
│   │   │   ├── apple-touch-icon-60x60.png
│   │   │   ├── apple-touch-icon-72x72.png
│   │   │   ├── apple-touch-icon-76x76.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── apple-touch-icon-precomposed.png
│   │   │   ├── apple-touch-startup-image-1182x2208.png
│   │   │   ├── apple-touch-startup-image-1242x2148.png
│   │   │   ├── apple-touch-startup-image-1496x2048.png
│   │   │   ├── apple-touch-startup-image-1536x2008.png
│   │   │   ├── apple-touch-startup-image-320x460.png
│   │   │   ├── apple-touch-startup-image-640x1096.png
│   │   │   ├── apple-touch-startup-image-640x920.png
│   │   │   ├── apple-touch-startup-image-748x1024.png
│   │   │   ├── apple-touch-startup-image-750x1294.png
│   │   │   ├── apple-touch-startup-image-768x1004.png
│   │   │   ├── browserconfig.xml
│   │   │   ├── coast-228x228.png
│   │   │   ├── favicon-16x16.png
│   │   │   ├── favicon-32x32.png
│   │   │   ├── favicon.ico
│   │   │   ├── firefox_app_128x128.png
│   │   │   ├── firefox_app_512x512.png
│   │   │   ├── firefox_app_60x60.png
│   │   │   ├── manifest.json
│   │   │   ├── manifest.webapp
│   │   │   ├── mstile-144x144.png
│   │   │   ├── mstile-150x150.png
│   │   │   ├── mstile-310x150.png
│   │   │   ├── mstile-310x310.png
│   │   │   ├── mstile-70x70.png
│   │   │   ├── yandex-browser-50x50.png
│   │   │   └── yandex-browser-manifest.json
│   │   ├── js
│   │   │   ├── analytics.45eff9ff7d6c7c1e3c3d4184fdbbed90.js
│   │   │   ├── app.30334b5124fa6e221464.js
│   │   │   ├── app.30334b5124fa6e221464.js.map
│   │   │   ├── app-legacy.560ef247e6649c0c24d0.js
│   │   │   ├── app-legacy.560ef247e6649c0c24d0.js.map
│   │   │   ├── confetti.1152197f8c58a1b40b34.js
│   │   │   ├── confetti.1152197f8c58a1b40b34.js.map
│   │   │   ├── confetti-legacy.8e9093b414ea8aed46e5.js
│   │   │   ├── confetti-legacy.8e9093b414ea8aed46e5.js.map
│   │   │   ├── precache-manifest.f774c437974257fc8026ca1bc693655c.js
│   │   │   ├── styles-legacy.d833997e3e3f91af64e7.js
│   │   │   ├── styles-legacy.d833997e3e3f91af64e7.js.map
│   │   │   ├── vendors~confetti~vue.03b9213ce186db5518ea.js
│   │   │   ├── vendors~confetti~vue.03b9213ce186db5518ea.js.map
│   │   │   ├── vendors~confetti~vue-legacy.e31223849ab7fea17bb8.js
│   │   │   ├── vendors~confetti~vue-legacy.e31223849ab7fea17bb8.js.map
│   │   │   └── workbox-catch-handler.js
│   │   ├── manifest.json
│   │   ├── manifest-legacy.json
│   │   ├── report-legacy.html
│   │   ├── report-modern.html
│   │   ├── webapp.html
│   │   └── workbox-catch-handler.js
│   ├── favicon.ico -> dist/img/favicons/favicon.ico
│   ├── index.php
│   ├── offline.html
│   ├── offline.svg
│   └── sw.js
├── webpack.common.js
├── webpack.dev.js
├── webpack.prod.js
├── webpack.settings.js
└── yarn.lock

```

## Inject­ing script & CSS tags in your HTML

With the web­pack con­fig shown here, <kbd>&lt;script&gt;</kbd> and <kbd>&lt;style&gt;</kbd> tags do not get inject­ed into your HTML as part of the pro­duc­tion build. The set­up uses [Craft CMS](http://craftcms.com/), which has a tem­plat­ing sys­tem, and we inject the tags using the [Twig­pack plu­g­in](http://github.com/nystudio107/craft-twigpack).

If you’re not using Craft CMS or a sys­tem that has a tem­plat­ing engine, and want these tags inject­ed into your HTML, you’ll want to use the [Html­Web­pack­Plu­g­in](http://github.com/jantimon/html-webpack-plugin) to do that for you. This plu­g­in is already includ­ed, you’d just need to add a lit­tle con­fig to tell it to inject the tags into your HTML.

## Craft CMS 3 Inte­gra­tion with the Twig­pack plugin

If you’re not using [Craft CMS 3,](http://craftcms.com/) you can safe­ly skip this sec­tion. It just pro­vides some use­ful inte­gra­tion information.

![Twigpack Plugin Logo Lg](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/twigpack-plugin-logo-lg.jpg)

I wrote a free plu­g­in called [Twig­pack](http://github.com/nystudio107/craft-twigpack) that makes it easy to inte­grate our fan­cy web­pack build set­up with Craft CMS 3.

It han­dles access­ing the <kbd>manifest.json</kbd> files to inject entry points into your Twig tem­plates, and it even han­dles pat­terns for doing the legacy/​modern mod­ule injec­tion, asyn­chro­nous CSS load­ing, and a whole lot more.

It’ll make work­ing with the web­pack 4 con­fig pre­sent­ed here very simple.

To include the CSS, I do:

```

<!--# if expr="$HTTP_COOKIE=/critical\-css\=1/" -->
    {{ craft.twigpack.includeCssModule("styles.css", false) }}
<!--# else -->
    <script>
        Cookie.set("critical-css", '1', { expires: "7D", secure: true });
    </script>
    {{ craft.twigpack.includeCriticalCssTags() }}

    {{ craft.twigpack.includeCssModule("styles.css", true) }}
    {{ craft.twigpack.includeCssRelPreloadPolyfill() }}
<!--# endif -->

```

The <kbd>&lt;!--# --&gt;</kbd> HTML com­ments are [Nginx Serv­er Side Includes](http://nginx.org/en/docs/http/ngx_http_ssi_module.html) direc­tives. The pat­tern is that if the <kbd>critical-css</kbd> cook­ie is set, the user has already vis­it­ed our web­site in the last 7 days, so their brows­er should have the site CSS cached, and we just serve up the site CSS normally.

If the <kbd>critical-css</kbd> cook­ie is not set, we set the cook­ie via [Tiny Cook­ie](http://www.npmjs.com/package/tiny-cookie), include our Crit­i­cal CSS, and load the site CSS asyn­chro­nous­ly. See the [Imple­ment­ing Crit­i­cal CSS on your web­site](https://dev.to/gaijinity/implementing-critical-css-on-your-website-37d5-temp-slug-6012917) arti­cle for details on Crit­i­cal CSS.

To serve up our JavaScript, we just do:

```

{{ craft.twigpack.includeSafariNomoduleFix() }}
{{ craft.twigpack.includeJsModule("app.js", true) }}

```

The sec­ond true para­me­ter tells it to load the JavaScript async as a mod­ule, so the result­ing HTML looks like this:

```

<script>
!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();
</script>
<script type="module" src="http://example.test/dist/js/app.273e88e73566fecf20de.js"></script>
<script nomodule src="http://example.test/dist/js/app-legacy.95d36ead9190c0571578.js"></script>

```

See the [Twig­pack doc­u­men­ta­tion](http://github.com/nystudio107/craft-twigpack) for details

Here’s my full <kbd>config/twigpack.php file</kbd> that I use; note that it has local set­tings for run­ning inside of my Home­stead VM. Your set­tings may differ:

```

return [
    // Global settings
    '*' => [
        // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading)
        'useDevServer' => false,
        // The JavaScript entry from the manifest.json to inject on Twig error pages
        'errorEntry' => '',
        // Manifest file names
        'manifest' => [
            'legacy' => 'manifest-legacy.json',
            'modern' => 'manifest.json',
        ],
        // Public server config
        'server' => [
            'manifestPath' => '/dist/',
            'publicPath' => '/',
        ],
        // webpack-dev-server config
        'devServer' => [
            'manifestPath' => 'http://localhost:8080/',
            'publicPath' => 'http://localhost:8080/',
        ],
        // Local files config
        'localFiles' => [
            'basePath' => '@webroot/',
            'criticalPrefix' => 'dist/criticalcss/',
            'criticalSuffix' => '_critical.min.css',
        ],
    ],
    // Live (production) environment
    'live' => [
    ],
    // Staging (pre-production) environment
    'staging' => [
    ],
    // Local (development) environment
    'local' => [
        // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading)
        'useDevServer' => true,
        // The JavaScript entry from the manifest.json to inject on Twig error pages
        'errorEntry' => 'app.js',
        // webpack-dev-server config
        'devServer' => [
            'manifestPath' => 'http://localhost:8080/',
            'publicPath' => 'http://192.168.10.10:8080/',
        ],
    ],
];

```

## Wrap­ping up!

Well, that was quite a deep dive! When I first start­ed delv­ing into web­pack, I soon real­ized that it’s a tremen­dous­ly pow­er­ful tool, with very deep func­tion­al­i­ty. How deep you go depends on how far you want to dive.

![Webpack Deep Dive](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/webpack-deep-dive.jpg)

For the com­plete source code for every­thing pre­sent­ed here, check out the **[anno­tat­ed-web­pack-4-con­fig](http://github.com/nystudio107/annotated-webpack-4-config)** github repo.

Hope­ful­ly this was help­ful to you, enjoy your jour­ney, and go build some­thing awesome!

## Further Reading

_If you want to be notified about new articles, follow [nystudio107](https://twitter.com/nystudio107) on Twitter._

<small>Copyright ©2020 nystudio107. Designed by nystudio107</small>