Welcome to the fourth and final part of my series on setting up Webpack for your ngUpgrade project! In the previous parts of these series, we’ve covered:

Now we’re going to learn how to break our configurations out into composeable pieces that can be used for different environments, whether staging, production, or something else. It always annoys me when web tutorials leave out the details of how to actually use something in the real world — we’re not going to do that here.

Here’s what we’ll cover in this guide:

Using the Webpack config function Using webpack-merge to compose environment configs Some examples of what you might do in a prod config for ngUpgrade

Before we jump in, I do have to say that I owe a huge debt of gratitude to Sean Larkin and Juho Vepsäläinen for all of their great resources on advanced Webpack usage, from docs and tutorials where I learned these techniques to the tools themselves. My goal is to spread that knowledge to a new sector of the industry. Thanks, guys!

As with the other guides, if your eyes start to glaze over because of the length, do yourself a favor and check out my super detailed video program Upgrading AngularJS. It includes the best Webpack material out there for specifically the Angular community.

Our Starting Point

As with the previous guides, take a minute and clone our course sample project on GitHub (don’t forget to run npm install in both the public and server folders). I’ll reference different commits throughout this guide so you can see examples of everything we cover here. Checkout this commit to see our starting point:

git checkout 85b2c82d71828f8d8bf039e0687f1b71e8d40e5a

We’ve skipped ahead a bit since the last guide. Here’s where we’re at in this commit:

We’re now using an Express server for our demo API.

The webpack-dev-server has been set up with a proxy option to pass requests to the API to our Express server.

has been set up with a option to pass requests to the API to our Express server. We’re using css-loader and style-loader to process CSS and file-loader to process fonts.

We don’t always want to use the same options for all of our environments, though. For example, we might want to only use source maps in our development and testing environments, but not in staging or production. And the opposite might be true for optimizing our JavaScript or CSS through uglification or minification.

Let’s jump into the code and explore how we can set up Webpack configs for these different environments.

The Webpack Config Function

We’re going to take the first step towards making our Webpack configuration a bit more sophisticated for other environments. You might not know that in addition to exporting an object in webpack.config , you have the option of exporting a function that returns an object. So, in our webpack.config.js file, we could actually do this:

module.exports = () => {

return {

//config options

}

}

…and return our configuration object. That may not look too impressive yet. The really cool thing about this is that Webpack can pass an env object into this function like this:

module.exports = (env) => {

//return config object

}

You can control the definition of this object in the command line using npm scripts. If we go over to the scripts section of package.json , we can pass an --env flag into webpack-dev-server that just integrates right into Webpack.

We can set it directly equal to something, which will get interpreted as a string:

"dev": "webpack-dev-server --env=dev"

Or we can set env equal to an object and set properties of that object using a dot syntax:

"dev": "webpack-dev-server --env.env=dev"

This makes it super easy to set up multiple environment variables if need be. For example:

"dev": "webpack-dev-server --env.env=dev --env.locale=us"

We’ll only need that first option for this guide.

Going back over to the Webpack config now, we can console.log this env object so that you can see the connection between the npm script and the Webpack config:

module.exports = (env) => {

console.log(env);

return {

//webpack config

}

}

Let‘s’ run the npm run dev command so you can see this in action. Just after the beginning of the console, you can see the object there with the env property set to dev.

Console logging the “env” object.

This opens up a whole new world for us because we can actually control parts of our configuration based on these environment parameters. This is really helpful!

Composing Webpack Configs

Installing webpack-merge

Let’s install a tool that’s going to make this process even easier for us. It’s called webpack-merge.

A small but mighty tool, webpack-merge is basically a super-duper array-concatenator and object-merger that lets us layer and merge together config objects. It’s really, really great. Like many other tools throughout my course and articles, you could go really deep into how sophisticated you want to be with webpack-merge . We’re not going to go too crazy; we’re just using it to layer either a development or production config object on top of a base common config.

The first thing we need to do (as you might guess) is use npm to install the webpack-merge package:

npm install --save-dev webpack-merge

Once that’s installed, let’s close the terminal and require webpack-merge at the top of our config file:

const webpackMerge = require (‘webpack-merge’);

Isn’t the node ecosystem really wonderful?

A Simple Merge Example

Let’s try a simple experiment to show you how this works. Let’s separate a common config object from a dev config object and use webpack-merge to put them together. In webpack.config.js, change that return statement to:

const commonConfig = { //config options

Then, pull out the devtool and devServer options into another object:

const devConfig = {

devtool: 'source-map',

devServer: {

contentBase: "./",

port: 9000,

proxy: {

'/api': 'http://localhost:9001'

}

}

};

Now we can use webpack-merge to return a composed config like this:

return webpackMerge(commonConfig, devConfig);

Your Webpack config file should look like this now:

A simple example of webpack-merge

What’s really neat is that we can still run our development server with no problem. Open up the terminal and run npm run dev . You should see that everything is still bundling and running correctly, even with our shiny new composed config. Pretty neat, huh?

Extracting the Common Configs

Let’s extract the devConfig and commonConfig objects into separate files to keep everything more organized. We’ll start with the common config, which will be our base layer for our different environment builds.

Make a new folder inside of the public folder called webpack-configs . Now, cut the commonConfig object from webpack.config.js and paste it into a new file called webpack.common.js in our new folder. You can swap the const declaration with module.exports like this:

// ./webpack-configs/webpack.common.js

module.exports = //commonConfig object follows

Now, back in webpack.config.js , you can require this object up at the top underneath our import of webpack-merge :

const commonConfig = require(‘./webpack-configs/webpack.common’);

Fixing Output with the Path Library

We’ve got our common config extracted out for Webpack. Before we move to our dev config, I want to fix something that I’ve let slide up until now because we were only using the Webpack development server locally and not deploying to a real server.

We really don’t want to have a path in our output’s filename option; we want to use the path option and pass in the path there. What we’ve got right now works because Webpack lets it happen, but what if we needed to deploy to different file systems (think Windows vs. Linux)? We’d much rather build the path to the output file using absolute paths that can take the environment into account.

To do that, we’re going to use the node path module, which handles many of the cross-platform woes we may encounter (here’s a very helpful article for more on this subject). The path module is built in, so we don’t need to install it, we just need to require it as a constant at the top of our webpack.common.js file:

const path = require('path');

In our output, let’s remove ./dist from our filename so that we only have bundle.js . Now we can add a path option and use a function called path.resolve . This function lets us pass in multiple path segments and join them together.

Our first path segment is the the node variable __dirname , which is the variable for our current directory:

path.resolve(__dirname,

Then, our second path segment is going to be ../.. , which is basically saying, “Go up two levels.” We’re currently in the webpack-configs folder and we want to be at the root of our project, which is at the same level as public and server :

path.resolve(__dirname, '../..'

And then our last path segment is going to be our dist folder:

path.resolve(__dirname, '../../', 'dist')

Our final output option should look like this:

output: {

filename: 'bundle.js',

path: path.resolve(__dirname, '../..', 'dist')

}

The last thing we need to change is the path in the script tag in our index.html file:

<script src="./bundle.js"></script>

Now, that may be a little confusing because our index file is now in a different directory than our Webpack output. What’s happening is the development server actually brings the static assets and the Webpack assets into the same root level. So our index.html , whether it’s being run from the dev server or whether it’s inside of our dist folder, is going to be at the same root as bundle.js .

Your finished common config file should look like this:

Our Finished Common Config

Extracting the Dev Config

Now let’s do this same process with the devConfig :

Cut the devConfig object from webpack.config.js Create a new file webpack.dev.js in the webpack-configs folder Paste in the object Replace the const declaration with module.exports

The should look similar to the common config file but with the devtool and devServer options:

webpack.dev.js

Now we’re ready to incorporate our new dev config — don’t import it at the top of webpack.config.js just yet!

Dynamic Webpack Configuration

To include the dev config, I’m going to show you a neat trick that I learned from Sean Larkin. In our webpack.config.js file, we can declare a const called envConfig and use some fancy string interpolation inside of our require statement. It’s really crazy! I didn’t even know you could do this.

Inside of our exported function, we can write:

const envConfig = require(`./webpack-configs/webpack.${env.env}.js`);

Did you catch what’s happening there? We’re accessing the env variable to dynamically build the path to our included config. So, when we pass --env.env=dev to Webpack, we’ll require our webpack.dev.js file. Neat, huh?

Now, let’s change the reference to devConfig in our webpackMerge function to envConfig :

return webpackMerge(commonConfig, envConfig);

Our finished webpack.config.js file will look like this:

Finished webpack.config.js

Look at clean and pretty it is!

One quick note. You may have notice that we’re not checking whether this env.env property is null or undefined. We could do that, but we don’t need to in this simple example. We’re always going to be in control of what scripts we’re writing and what environments for which we’re building. So, we won’t worry about that. In the real world, though, you may not have that kind of control over the build scripts, so you might want to do a quick integrity check on that variable.

Let’s Make Sure Everything Still Works

Before we add our production config, let’s make sure everything is still working. (You can double-check your code against this commit.)

Open up a terminal. You can start the Express server by cd ’ing into the server folder and running npm start . Then, open another terminal, navigate to the public folder, and run npm run dev .

If you head to the browser at localhost:9000 , you can see that our application is still running!

It works!

Take a quick peek at the network tab of your Chrome dev tools (you’ll need to reload if you didn’t already have them open so you can see the requests). First, if you hover over bundle.js , localhost , or any other HTML files, you’ll see that they’re being served at the root. You can also see that any data calls (like /customers ) are coming from our API being routed by proxy to our express server.

Our files are being served at the root.

So this setup is awesome. We can go and make any sort of environment we want and add whatever kind of configuration we want to it totally cleanly and not have to worry about it affecting the other environment configs. Pretty amazing, isn’t it? Thanks, Sean!

Speaking of whatever configuration we want, let’s start working on a production build.

Adding a Basic Production Config

We’re ready to start adding production configuration to our application. There’s two things that we need to do to get started with that.

Add an npm Script

The first thing we need to do is create a new npm script that includes our env option in package.jason . So, above our build script, let’s add a new script and call it build:prod . There’s a neat trick you can do with npm scripts where you can call a different npm script and pass in an additional argument using a double-dash, which works like a pipe operator. So, let’s do this:

//npm scripts

"build:prod": "npm run build -- --env.env=prod",

"build": "webpack --bail --progress", //no changes here

So, the build:prod script calls the build script, but passes env.env=prod to it. This is great, because we can have different scripts for different environments that all build on the same base. If we ever want to change our baseline Webpack command by adding or removing flags, we can do that and not have to change all of the other build scripts for the different environments.

Add Prod Config File

The next thing we need to do is add a config file for production. Create a new file called webpack.prod.js in our webpack-configs folder. We’ll start it by exporting an empty object:

//webpack.prod.js

module.exports = {

};

Let’s run npm run build:prod and see what happens. You should see that Webpack bundles everything successfully. You should also see a dist folder up at the root level (you can now delete the one under public ):

Our new dist folder.

Inside that folder, you’ll see all of our assets. We’ve got our font files and our bundle. Unfortunately, we don’t have our index.html file and that’s kind of a pain. Let’s fix that next.

Handling Our Index with HtmlWebpackPlugin

We’ve got our assets and our bundle.js coming out into a dist folder at the root level of our project. That’s great. The only problem with this is that our index.html file isn’t being moved over with it. As you saw, with the dev server, that doesn’t matter. If we’re bundling for production, though, we can’t serve this static dist folder with something like Express or .NET without that HTML file.

This means we’re ready for our very first Webpack plugin. Isn’t that exciting? It’s called the HtmlWebpackPlugin and the first thing we need to do is install it with npm:

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

With any plugin, we need to require it up at the top of our config file. In our webpack.comon.js file, we’ll do this underneath where we require path :

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

Now, at the bottom of our config, after our resolve section but still within our object, let’s add an array of plugins:

plugins: [

//plugins go here

]

A plugin is created by using the new keyword and the name of the plugin:

plugins: [

new HtmlWebpackPlugin()

]

You can also pass in an object of options. With the HtmlWebpackPlugin, if you don’t pass in any options, it will generate an index.html file for you. However, we want to use our own index.html file as template. To do this, we can say:

plugins: [

new HtmlWebpackPlugin({ template: './index.html' })

]

Remove Script Tag from HTML

HtmlWebpackPlugin also injects our script tag into our HTML. By default, it does that at the bottom in the body tag of our HTML (you can change this with options, but this is good with us). Open up index.html and remove our script tag referencing our bundle. We don’t need it anymore.

Zero script tags!

And look at that! If you look at this file at the very first commit of this project, we had no build process and TONS of script tags — now we have zero! How cool is that?

Let’s see what happens when the plugin runs. Open up the terminal and run npm run build:prod again. Open up the dist folder and checkout our new index.html file. You can see it inserted our script tag for us inside of our body, but it left the rest of our template. How cool is that?

Handling Templates with raw-loader

Okay, so our index.html is getting bundled up into the dist folder.

Let’s fire up our Express server and see what our current status is. It’s set up already to serve our dist folder. Just cd into the server folder and run npm start . If you switch over to the browser and head to localhost:9001 , you’re going to see a bunch of errors like this:

Yikes!

We’ve got our index and our bundle loading, but we don’t have our navigation or our home component templates anywhere to be found. We need to figure out a way to get those files over to our dist folder. However, we don’t really want to actually serve up all those individual HTML files. That could get really messy and kind of defeats the purpose of having a single page application.

So how are we going to get Webpack to load that HTML? Well, I kind of just gave it away. Any time we need to load or bundle a new type of file with Webpack, we want to use a new loader. In this case, we’re going to use something called the raw-loader with Webpack. Of course, the first thing we need to do is install it with npm.

Open a new terminal, move into the public folder, and run:

run npm install — save-dev raw-loader

We need to make a new rule in our rules array of webpack.common.js . Under our file-loader rule, add a new rule object. The test is going to be a regular expression for HTML and we’ll use the raw-loader :

{

test: /\.html$/,

use: 'raw-loader'

}

Now, just installing the raw-loader for using HTML isn’t enough. It doesn’t tell Webpack which HTML files to load. To do this, we’ll need to import our templates into our component files.

Requiring Templates

Webpack now knows what to do with HTML, but it actually doesn’t know which HTML it needs to bundle. We’re going to fix that now.

Open up the home component ( /public/home/home.ts ). At the top of the file, first let’s change the var before the homeComponent object definition to a const . It doesn’t make a huge difference, it’s just kind of best practice to use the const keyword for anything that’s not really going to change.

Now, before line one, let’s require our template:

const template = require('./home.html');

If you’re using Visual Studio Code, you should see a red squiggle. This is TypeScript complaining to us, because it doesn’t know what require is. If you recall, we’ve been using require in our Webpack config, and that’s because Webpack is running on node, not in the browser. Our components are going to be running in the browser, not in the node runtime, and so TypeScript has no idea what this means. This should be a red flag for you (literally) to look for a type definition.

Here, we need the node type definitions. This is one of those things that would probably take you a while to just piece together on your own, so I’m just going to give you the answer. Open up the terminal and run:

npm install --save-dev @types/node

This is one of the best parts of the evolution of the typing system of TypeScript. Early on there were a couple different TypeScript type defnition package managers, and it was super confusing. Now all of that’s just been wrapped up in npm under this types namespace.

Once that’s done installing, if you go back to the home component you should see that the red squiggle has gone away (you might need to open and close the file or delete and retype the semicolon to trigger a refresh).

All that’s left to do is change our templateUrl to template , and replace the URL itself with our template constant. Our homeComponent object now looks like this:

const homeComponent = {

template: template,

bindings: {},

controller: homeComponentController

};

The rest of the file looks like the same.

That’s all it takes to require a template. To get the application running again, you’ll need to do the same thing with the rest of the templates. You can start with the navigation component to just get the front page loading to ensure you understand this process (this commit), then finish out the rest.

Now, let’s skip ahead and look at how we might use our production config.

Example Uses of the Prod Config

We’re going to skip ahead past all of the template updates to talk about our prod config. I’m going to switch it up and tell you a commit to look at and explain what’s going on. (Note that there are step-by-step videos on every step of this process in course 2 of Upgrading AngularJS.)

Check out this one:

git checkout 41f648bda6a53e8bb13003c96ee01ae3ea0a197f

cd public

npm install

Here’s what’s happening here:

We’re using rimraf to clean out our dist folder every time our build runs

folder every time our build runs We’ve split our entry point in webpack.common.js. We’ve now got an app bundle and a vendor bundle

bundle and a bundle We’re using the CommonsChunkPlugin to optimize our vendor bundle so we don’t duplicate dependencies

One quick note: In this sample project, we’re using Webpack 3. Webpack 4 is hot off the presses and switches from the CommonsChunkPlugin to the SplitChunksPlugin. You can read more about that from Tobias Koppers here.

In our prod config:

We’re using [hash] in our bundle file names to help with cache-busting.

in our bundle file names to help with cache-busting. We’re using Webpack’s built-in UglifyJSPlugin to optimize our JavaScript.

Let’s take a minute to talk about that last one — the Webpack UglifyJSPlugin.

UglifyJSPlugin

Prior to dropping in that plugin, our vendor bundle was a whopping 2.6mb, which is way too large for production. To fix that, we went over to the production config file and first required Webpack:

const webpack=require(‘webpack’);

Then inside of our module.exports object, we added a plugins array. Luckily, webpack-merge is smart enough to just layer on top our additional plug-ins. It won’t replace our plugins array in our common configuration with what’s in our production configuration. All we needed to do is create a new instance of UglifyJSplugin:

new webpack.optimize.UglifyJsPlugin(),

As is the case with many plugins, the UglifyJSPlugIn has many, many options that you can use to really tweak the optimization of your bundles. The best thing you can do is read more about the options in the readme of the plugin. That will let you delve deeper for yourself and apply this to your own application

I also want to make a quick point about using minified vendor libraries. In the old world of script tags, or even using Gulp or Grunt, we usually used minified versions of our libraries. There’s an issue with ES6 imports and trying to use the minified versions of your libraries that come inside your node_modules folder. Unfortunately, this becomes a huge pain. You can use some Webpack trickery to resolve aliases to your minified versions, and then use a plug-in to fix the problem with the imports. However, so far in my experience, that’s been a pretty rocky road. I’ve resorted to just importing all of the regular, un-minified, uncompressed code, and then running the UglifyJS plug-in over both my vendor bundle and my app bundle. This situation isn’t quite ideal for now, but it gets the job done.

Let’s Run This

Now we can just open up a terminal, run npm build:prod again, and go look in the browser to check out how big these files came out. (Be sure the Express server is running.)

If you refresh the page, you can now see that our vendor file got reduced down to 683kb and our app bundle is down to a tiny 17kb. That’s a pretty impressive size reduction — and that’s without passing any options at all into the UglifyJS plug-in.

Look at those tiny files!

Where to Go Next

Between the CommonsChunkPlugIn (or SplitChunksPlugin in Webpack 4) and the UglifyJSPlugIn, you can really fine-tune and optimize your production build. We could spend days and days getting into those options (and other things like optimizing CSS), but hopefully you now know where to start in really tweaking your own application using Webpack with the best practices of AngularJS and TypeScript.

Your next step would be to work through your CSS. You might need to set up a pre-processor like LESS or SCSS. You’ll also want to optimize your CSS by removing unused styles. Finally, you also may want to extract your styles into separate files instead of having them bundled up with Webpack using the css-loader and style-loader .

Love this series? Want to learn more?

We’ve covered a ton in this four-part series on Webpack for ngUpgrade. If you love this guide and the others in the series, I’ve got 200+ detailed videos, quiz questions, and more for you in my comprehensive course over at UpgradingAngularJS.com. I created it for everyday, normal developers and it’s the best ngUpgrade resource on the planet. Head on over and sign up for our email list to get yourself a free Upgrade Roadmap Checklist so you don’t lose track of your upgrade prep. And, while you’re there, check out our full demo.

See you next time!