Set Up Your ngUpgrade Project for Production Without Crying Yourself to Sleep Tonight

We all love the Angular CLI, but when you’re working through a big ngUpgrade project, you just can’t always use it. You’ve got a big, messy repo with years’ worth of history. You can’t just up and move it to a clean new repository.

So now you’re faced with hand-rolling a Webpack build that works for your legacy AngularJS code and your new Angular code, all while ngUpgrade bridges the gap.

On top of that, unless you’ve got untold hours of time on your hands, you’re going to have to do this while continuing bug fixes and feature work. You’ve gotta be methodical and iterative and always have an app you can deploy to production.

That’s what this guide is for.

The Setup

Here are some assumptions we’re going to make right now:

  • The latest version of AngularJS (at least 1.5)
  • Component architecture and other best practices (no $scope and such)
  • A Webpack 3 build (until Webpack 4 is out and stable) using TypeScript set up for multiple environments using webpack-merge
  • Angular set up with ngUpgrade (we’re going to use Angular 4 in this guide for now since it has LTS support, but I’ll add notes about Angular 5 as time goes on)

(If you’re lost on how to set up any of that, don’t worry - we cover all of those topics step-by-step in our course.)

Go ahead and clone our course sample project on GitHub and I’ll reference different commits throughout the guide so you can see examples of everything we cover here. Checkout this commit to see the set-up and starting point.

The Problem

If you were to look at the production build in this project as it is now with Webpack bundle analyzer (one of the best tools ever), you’d see this for the vendor bundle:

Our vendor file is a whopping 5.3mb, but it’s dropped down to about 1.43mb after uglification. That’s still way too big for a single vendor file,though. You can see that Angular is about half of the vendor bundle: 2 megabytes unminified and still about 700k when it’s uglified and minified. We can’t possibly take that to production. Luckily, the bundle analyzer is telling us something major here. See this giant box here for the compiler?

That compiler is taking up about 350k of the minified file. That’s pretty massive.

(P.S. Did you also catch that we’re importing all of RxJS here? We won’t get into that here, but we do cover that fix in the course.)

So, what’s going on there? Well, it turns out that Angular can run in two different modes. The first one is called JIT, or just in time, and the second one is called AOT, or ahead of time. Remember when you were bootstrapping your Angular module, and you used something called platformBrowserDynamic in main.ts? That’s the just in time compilation process. Angular is actually compiling our components and our templates and all that metadata that we use in our decorators on the fly in the browser. So, it’s shipping the entire Angular compiler in the code so it can compile live in the browser.

That’s totally fine for our development process, but we really don’t want to do that in production. So, the other mode is called AOT, or ahead of time compiling. This is when Angular generates all of the compiled code ahead of time and then bundles all that up and sends static files to the browser. That’s when we use platformBrowser instead of platformBrowserDynamic to bootstrap our application. This way, Angular doesn’t ship the compiler code to the browser.

I’m not going to lie to you: setting up and configuring AOT in an upgrade project can be a bit of a bear. Luckily, I’ve done a lot work for you and figured out all the steps you need to take. It turns out there are three parts:

  1. Prep your Angular code for AOT.
  2. Completely separate your AngularJS code from your Angular code.
  3. Set up Webpack and TypeScript for AOT.

Let’s go for it!

Part 1: Prep Your Angular Code for AOT

Before we actually set up the tooling around AOT, we’re going to have to lay some groundwork to get our application ready for the AOT compiler. The AOT compiler is heavily opinionated and requires some conventions be followed in order for it to work. It can be a bit of a mystery deducing what those conventions are though, so I’m going to walk you through a few of them.

Use TemplateURLs for Your Angular Components, not Inline Templates

Recently, when using AngularJS or Angular with Webpack, one best practice has been to require templates in order to inline them using the Webpack raw loader, like this:

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

This is still the case, but it turns out that the AOT compiler wants to do this for us. It’s expecting templateUrl, so when it builds, it changes those template URLs to inline strings.

It’s a little confusing, and this may change in the future, but for now we need to go through our Angular components — not our AngularJS ones — and change these templates back to templateUrl:

In just a bit, we’ll be changing our Webpack build so that it can handle the inline templates in our AngularJS code and the template URLs in our Angular code for both development and production, so hang tight.

Change Private Template Variables

It turns out the AOT compiler doesn’t like private variables on your templates. In this sample project, we’ve only done that one time in the orders component:

We can just get rid of that private keyword before sortReverse.

Note that the AOT compiler does not care about our private keywords in our constructors. These are different, as they’re for dependency injection. Why the AOT compiler doesn’t like template variables that are set to private is complicated and has to do with the way that it generates the metadata and compiles down from TypeScript into JavaScript. You really don’t need to worry about that. Just know that for the AOT compiler, it’s a no-go.

Ditch Any Instances of Export Default

The AOT compiler does not like default exports. In our sample project, we currently have two upgraded AngularJS services that use export default instead of plain old exports: the product service and the address service. It was pretty standard practice to use export default ProductService in AngularJS. Since Angular needs access to this service via ngUpgrade, though, it must obey AOT conventions. Our other AngularJS services don’t need to change.

So, in both our product service and our address service, we’ll need to change the export default to just export:

export default productService;

becomes:

export class ProductService { //...the service }

Then, we’ll need to update all the places that import those two services, from:

import productService from './products/productService';

To:

import { ProductService } from './products/productService';

There are four different places this happens: our AngularJS module (app.module.ajs.ts), our upgraded providers file (ajs.upgradedproviders.ts), the create order component (which uses the product service), and the customer detail component (which uses the address service).

Our Code is Ready

Now we’ve got our application set up in a way that complies with the AOT compiler’s specifications, which means we’re ready to start getting our dependencies in order and installing what we need to add AOT compiling to our production build process.

By the way, these are only three of the AOT compiler’s preferences. There are many more, and by far the best resource out there for this is this guide from Range.io:

Part 2: Separate Your Angular and AngularJS Code

Getting the AOT compiler to work requires we have a hard separation between our AngularJS code and our Angular code. This requires two things from us:

  1. Moving all of our entity registrations into the AngularJS module file
  2. Adding a separate Webpack entry point for AngularJS

Move Component and Service Registrations into the AngularJS Module

In non-production examples of ngUpgrade, it’s common to mingle your AngularJS code with your Angular code by registering downgraded components and services in the same file as your new Angular class, like this:

In fact, this is still done in the official ngUpgrade docs. This method works great for development, and it would actually be fine if we didn’t need to use the AOT compiler.

It turns out that Angular’s AOT compiler, when used in Webpack outside of the CLI, flat out won’t work with this approach. We need to split out our AngularJS code from our Angular code.

So, the first thing we’re going to do is we’re going to need to take all of our instances of downgradeComponent and downgradeInjectable and move them into the AngularJS module, right alongside our remaining AngularJS items.

This is a tedious process, but in the end you’re going to end up with this:

app.module.ajs.ts

You can see the whole file here.

One other note: reflect-metadata

There’s one other random thing that I ran into when setting up AOT with Angular 4. For some reason, I had to import the reflect-metadata library in my AngularJS module:

import 'reflect-metadata';

It wasn’t enough that it was imported into my Angular module. I’m pretty sure this quirk gets resolved in Angular 5, but just keep it in mind if you run into a weird problem.

Change the AngularJS Entry Point in Webpack

We’ve successfully split our app between our AngularJS parts and our Angular parts so that we have two different modules controlling the apps. None of the pure Angular components or services know anything about AngularJS or or the ngUpgrade methods. They only know what we tell them.

Now, to complete this, we need to change our entry in our Webpack common config. This is easy to do because we’ve already set up our Webpack process to be compose-able with the amazing tool webpack-merge. I can just open up our Webpack common config and add a line before our app bundle in our entry in module.exports:

webpack.common.js

I’m going to call this entry “ajs”, for AngularJS. The path I’m going to use here is ./source/app.module.ajs.ts, which is just the path for our AngularJS module. We don’t need to modify anything else, because we’re already using the Webpack CommonsChunkPlugin in our plugins (this will be going away in Webpack 4, just a heads up).

That’s going to take care of outputting our different bundles in the correct name along with the output of our configuration. This sets us up for another step later on in the process where we’ll need to have a different entry point for production when we use our AOT compiler, but that’s all we need to do our Webpack config right now.

Quick Aside Before You Continue: Update Your Version of Angular

We’ve made a few changes to our application in order to get it ready for the AOT compiler. Before we actually install the AOT compiler Webpack plugin and a couple of other things, we need to make sure our version of Angular is up to date. This is because we always want the ngc compiler version to match our Angular version. For example, if we’re running Angular 4.4.5, we want our compiler to be the same problem.

The easiest way to accomplish is this to make sure your entries for Angular in package.json use the caret to indicate that we want every update for a major version. This looks like this: "@angular/core": "^4.0.0". That way, you can just run npm install again and automatically get bumped to the latest version. You may also want to run npm prune before your run npm install to get rid of any unused packages.

If you run into problems trying to install ngc, try wiping out your node modules folder and installing again.

Part 3: Set Up Webpack and TypeScript for AOT

Install AOT Tools

Okay, now that we’re using the latest version of Angular, we’re ready to install some tools that we need to use to set up AOT compiling with our project. Jump to this commit in the sample project to follow along.

There are three of these tools, and we’re going to do one npm install command to get them in our project:

npm install --save-dev @angular/compiler-cli@4.4.6 @ngtools/webpack angular2-template-loader

The first is the Angular compiler CLI. The compiler CLI lets us run compile commands with something called ngc. ngc is a replacement for the TypeScript compiler that Angular uses to compile down our code. It’s very important you include the version here that matches your version of Angular core (hence the @4.4.6, but swap out with your version). The CLI for it is, as you might guess, a command line tool set that lets you run NGC.

The next tool is called ngTools/webpack. This is a piece of the Angular CLI that’s been extracted for use with Webpack outside of the CLI. This comes with both a loader and a plug-in for using the Angular compiler.

And then lastly, we need a Webpack loader called angular2-template-loader. Remember when we switched our inline templates back to templateUrls? Well, this is a handy tool written by the great Sean Larkin that can reverse that. I know that sounds a little crazy, but I’ll explain in a sec. We’re going to use it with our development build.

Add a New TypeScript Config for AOT

So now we’ve got the tools we need, and we’re ready to set them up. In order to use the Angular compiler for AOT, we need to have a separate TypeScript config for our production build that uses ngc instead of the regular TypeScript compiler.

We need to make a new file called tsconfig.aot.json with these options:

Most of the options that we have in here are similar to what we had in the regular TypeScript config file, but there are a couple of changes. First, you can see that we’re using an ES2016 library instead of ES2015. That’s because the CoreJS library needs this.

Next, we’ve added a typeRoots object, which is the types folder and our node_modules folder. After this, we’re telling the TypeScript config to exclude both our node_modules folder and also our current main.ts file, which is where we call platformBrowserDynamic. This is because we’re going to make a separate main.aot.ts file that will be used specifically as the entry point for our production build for AOT. This file will use platformBrowser instead of platformBrowserDynamic.

Finally, we’ve included a section called angularCompilerOptions, which includes where we specify the directory in which the compiler should produce the AOT files. Now, most of these files will just be held in memory, but we are going to produce a new directory at the same level as our public and dist folders.

This is still an evolving ecosystem and an evolving process, so you may see people with other options in this config file, and that’s okay. It’s still kind of a work in progress. For example, some people use AMD modules instead of CommonJS modules. This is the configuration that I found works best for me in this context of using Webpack and having multiple configurations for different environments.

Now we’re ready to make our main.aot.ts file at the root of our application.

Adding a New main.ts File for AOT

We’re set up and ready to get started on actually using the AOT compiler. Let’s make our main file that we’ll use for the AOT build. So in our source folder, I’m going to make a new file called main.aot.ts. It’s going to look like this when we’re done:

Let’s break this down.

Of course, the first thing I need to do is import a whole bunch of stuff, starting with zone.js and reflect-metadata. Then, we’ll import enableProdMode from Angular core. We’ll also need setAngularLib from angular/upgrade/static (and the static is important here since we’re using AOT). Now I’ll import platformBrowser from angular/platform browser.

I also need to import AngularJS to use with that setAngularLib function. I’ve seen some people ignore using setAngularLib, but to be honest, I’ve never gotten my production build working properly without it. As I’ve said, this is an evolving ecosystem, so things may be different for your specific situation.

The final import is what will eventually be what’s called app.module.ngfactory. Now this file does not exist yet, and that’s because the AOT compiler is going to generate this ngfactory from our app module. We’ll be setting that up in the Webpack config, but actually this file won’t exist after the build process. It’s only going to exist in memory during the build process. So we’re getting the red squiggle because the file doesn’t exist, but there’s really nothing we can do about it right now.

After the imports, we can actually do what we need to do in this file. So the first thing I need to do is call setAngularLib and pass in AngularJS. Then we call enableProdMode, which does things like suppress errors. Finally, I’m going to call platformBrowser.bootstrapModuleFactory and pass in our AOT compiled AppModuleNgFactory.

Essentially, this file acts the same as our regular main.ts file, there are just some key differences. The only things that change are the references to the ngfactory and using platform browser instead of platform browser dynamic.

Update the Prod Config

We’re now ready to set up Webpack with AOT. We’ll start with our production build. In our production Webpack configuration (inside of our webpack-configs folder), we need to first change our entry point to our new main.aot.ts file, and then we need to use the ngTools/webpack library as both a loader and a plugin.

First, let’s change our entry point for our production configuration. The webpack-merge tool makes this really easy. All we need to do is add a line in our module.exports for our entry point:

entry: { app: './src/main.aot.ts' }

Now we’re ready to use the ngTools/webpack library. We need to add it as both a loader for our TypeScript and as a plugin. Before we actually start using it, we need to import both the path library and ngTools/webpack up at the top of our file:

const path = require('path');
const ngToolsWeback = require('@ngtools/webpack');

Now, in our loader rules, we’re going to add a new rule for TypeScript:

The test will just be a regular expression for typescript, and then we’ll use the ‘ngtools/webpack’ loader. Then we’re going to need to exclude both our node_modules and our other main.ts file. We don’t want the Angular compiler touching that file at all. This loader rule will tell Webpack to use the ngc compiler instead of the normal tsc compiler.

And lastly, we need to add an instance of the ngTools AOT plugin:

This plugin has an options object. The first option is our tsconfig path for the new file that we made, and the second option is our entry module. This tells the AOT compiler the root module of our project.

Important: this usage has changed in Angular 5. Check out the docs here for a modification of how to set up this plugin.

And now, our Webpack production config is set up for our AOT compiling.

You can see our finished prod config right here.

Update Dev Config

We just have one more step in this process. We’ve told the production build to do something different with our TypeScript compiler, but we currently also have a TypeScript rule in our common config, and these will conflict. So, we need to change where our TypeScript loader rule is and move it into our dev config.

So first, we need to cut the TypeScript loader rule out of webpack.common.js. We can then open our dev config, create our module object and rules array, and paste it there. It looks like this so far:

webpack.dev.js

We need to make two adjustments to this rule though. First, we need to exclude our main.aot typescript file, in addition to our node modules. In order to do this, we need to require path up at the top of our file. Then, I’ll wrap node modules in an array, and move it to the next line. And now I can add our main.aot file. If we had left our TypeScript loader rule in our common config, our TS loader would have run in addition to ngc, and it would have caused some problems. This way we’ve separated our production TS build from our dev TS build.

We need to do one more thing with this though. Remember how we installed a package called angular2-template-loader? This is where we’re going to use that. We had changed our Angular components to use templateUrl instead of template. What’s weird about this is that ngc is expecting templateUrls, and then wants to inline them itself. Unfortunately, the route of Angular is different than the route of webpack-dev-server. If we were to try to load our Angular components with template URLs that currently say things like, “./customers,” the Webpack dev server isn’t going to be able to find it.

This is weird and annoying, I know, but luckily the development community has come to the rescue. In fact, it was Sean Larkin himself that wrote this loader. This loader takes template URLs and swaps them out with the template require statement that we were doing before. To set this up, all we need to do is change our use option here to an array and add angular2-template-loader after ts-loader:

use: ['ts-loader', 'angular2-template-loader']

Remember: Webpack loaders are run right to left, so the angular2-template-loader will run before the ts-loader.

I know this seems crazy, but this is the current state of the ecosystem, and hey, it works. We just need to do one final thing to ensure that our AOT build doesn’t conflict with our dev build. We need to add that same exclude array to our prod TypeScript loader, but add .aot to our main file.

exclude: [                                           
/node_modules/,
path.resolve(__dirname, './src/main.aot.ts') ]

Now the regular TypeScript compiler won’t try to also compile the “main.aot” file.

You can see the finished dev config right here.

Our Finished Builds

We’ve finally got TypeScript and Webpack set up for AOT, and now it’s the moment of truth. If you switch to this commit, you can run both the development and production builds. You can also just uncomment the bundle analyzer plugin to see the difference.

If we run our new production build (npm run build:prod), we have a new, shiny bundle:

Our Glorious Finished Bundle

Our bundle size has dropped significantly, it’s down to under a meg when it’s uglified, and look at Angular! Angular is half the size, and you no longer see the compiler. Boom!

Wrap Up

Alright everybody. I hope you’ve enjoyed this painfully detailed guide to setting up your ngUpgrade project for AOT. This is the #1 point of frustration for folks migrating to Angular using ngUpgrade. It’s an ever-changing subject, but hopefully will just get easier as more people make this switch and contribute to docs and tools.

If you love this guide, we’ve got 200+ detailed videos, quiz questions, and more for you in our comprehensive course UpgradingAngularJS.com. It’s the best ngUpgrade resource on the planet. Head on over and sign up for our email list and get yourself a free Upgrade Roadmap Checklist. And, while you’re there, check out our full demo.

Like what you read? Give Upgrading AngularJS a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.