Application upgrade
Angular Material SPA Typescript Visual Studio Code

How to upgrade an Angular Application from version 8.x to 9.x

Welcome to today’s post.

In today’s post I will be discussing how I converted an Angular application from version 8 to version 9. I will not be showing the only way to upgrade an application, as each application has its own structures, configurations, and third-party packages. I will be upgrading an application of moderate complexity that will introduce some challenges along the way.

The application I will be upgrading consists of the following components and artifacts:

  1. Use of the Angular Material Library.
  2. Use of at least one third-party (non-core) NPM package.
  3. Use of lazy-loaded modules that include components and custom UI forms.
  4. Use of shared modules containing imported libraries.

A note about version 9 and the Angular Material dependencies

As you would be aware, version 9 of Angular supports an improved compiler known as Ivy, which is intended to produce faster builds and optimized size. Even though this is a good improvement to have it requires the application maintainer to jump through numerous hoops, and in many cases, unnecessary hacks, and workarounds to get an application upgraded and tested. Whilst I believe in improving software and continual improvements, forcing developers to break a software code base that is in many cases used in live production environments should never be enforced. With this said, I decided not to enable Ivy for version 9 as there is a requirement to use versions of Angular Material packages that are Ivy optimized. This would require making unnecessary changes to the project to improve loading and build performance. The priority when upgrading should be to ensure the software is upgraded, then working as expected without having to endure breakages caused by features that are not necessary.

In the sections within this post, I will be showing how to prepare the current libraries for the upgrade, then show how to prepare the code base for the upgrade. I will then cover the following tasks for the upgrade:

  1. Upgrading the project code base.
  2. Upgrading third-party library components.
  3. Upgrading unit tests.
  4. Resolving errors during builds.
  5. Resolving errors during application executions.

Preparing to upgrade

The first task before upgrading is to check if the version of Node.js you have is high enough to support an upgrade to Angular version 9. Do this by running the command below:

node -v

My version shows up as:

v10.16.0

The minimum version of Node.js required to support Angular version 9 is v12.14.

I will explain how I determined the minimum version later.

To get Node.js and install it, we follow the following tasks:

  1. Open https://nodejs.org/
  • Open https://nodejs.org/en/download/
  • Locate the version we want to upgrade to:

Node.js 12.14.1 Erbium  2020-01-07          7.7.299.13            6.13.4    72

  • Install Node.js 12.14.1

Download the MSI package.

Install the MSI package. This will take several minutes.

Once installed, move onto the next task.

Preparing the code base to support version 9

The first task before making any changes to an Angular project that is due to be upgraded is to make sure you have committed the current version into git or whatever VCS you are using. You can then create a new branch for the upgraded project and make upgrade changes from there.

One of the changes that you will need to make to your Angular code is within Angular routing, and that is to support dynamic loading of children.

This is done by locating routes containing paths with strings as follows:

const routes: Routes = [
    ...
    { path: 'dashboards', loadChildren:   
        '../app/dashboards/dashboards.module#DashboardsModule' 
    },
    ...
];

Then change the paths to dynamic paths as follows:

const routes: Routes = [
    ...
    { path: 'dashboards', loadChildren: () => 
        import('../app/dashboards/dashboards.module')
            .then(m => m.DashboardsModule) 
    },
    ...
];

If you are using AOT compilation, then there are significantly more workarounds and compiler errors to fix with components that depend on the Angular Material library.

My first recommendation is to stick to the current Material packages 8.2.x and NOT upgrade to Material 9.x. If you do this, you will be in a world of pain jumping through hoops trying the convert your project to use Angular Material Ivy or restructure your project dependencies to use Angular Material 9.x. With Material 8.x you can still using module deep loading like this:

import { MatInputModule, MatFormFieldModule, MatTabsModule, MatSidenavModule, 
         MatToolbarModule, MatIconModule, MatButtonModule, MatListModule, 
         MatMenuModule, MatSelectModule, MatButtonToggleModule,
         MatDatepickerModule, MatNativeDateModule, MatCardModule,
         MatDialogModule, MatPaginatorModule, MatSortModule, MatGridListModule, 
         MatTableModule, MatSnackBarModule, 
         MatBadgeModule, MAT_DATE_LOCALE, MatCheckboxModule, MatChipsModule, 
         MatProgressSpinner } from '@angular/material';

With Material 9.x and Angular 9.x, you will have to individually import each library component like this:

import { MatSnackBar } from '@angular/material';

With the latter approach I have seen more problems with compatibility issues between Material 9.x and Angular 9.x.

To return to the compilation that uses the View Engine, which is more stable you can set the following in your tsconfig.app.json:

"angularCompilerOptions": {
    "enableIvy": false
}

In your Angular.json change the following AOT compilation setting as shown:

"projects": {
    "angular-api-azure-app": {
        "projectType": "application",
        …
    }
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
    "build": {
        "builder": "@angular-devkit/build-angular:browser",
        "options": {
            …
            "aot": false,
            ... 

Upgrading the Angular project to version 9

The first step is to update the core Angular 8 libraries to the latest minor versions. This is done as follows:

ng update @angular/core@8 @angular/cli@8

After running the core libraries upgrade, you will get the following output confirming the application of library upgrades (most output removed for brevity):

Using package manager: 'npm'
Collecting installed dependencies...
Found 40 dependencies.
Fetching dependency metadata from registry...
    Updating package.json with dependency @angular/cli @ "8.3.29" (was "8.1.3")...
    Updating package.json with dependency @angular/core @ "8.2.14" (was "8.1.3")...
    Updating package.json with dependency @angular/language-service @ "8.2.14" (was "8.1.3")...
 	…
    Updating package.json with dependency rxjs @ "6.6.7" (was "6.4.0")...
    Updating package.json with dependency typescript @ "3.5.3" (was "3.4.5")...
UPDATE package.json (2135 bytes)
npm WARN deprecated fsevents@1.2.13: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.
	…

To determine the minimum version of node that is required, we will attempt to upgrade to version 9. First open the Angular project.

In the terminal run the following command:

ng update @angular/core@9 @angular/cli@9

What the above command does is load any dependent packages from Node.js and then determine if an upgrade is possible. If possible, then the upgrade will be applied to your project.

If, however, you have tried to upgrade from a version of Node.js which is too early to support an upgrade to version 9, then the following error will show:

The installed Angular CLI version is older than the latest stable version.
Installing a temporary version to perform the update.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
Node.js version v10.16.0 detected.
The Angular CLI requires a minimum Node.js version of either v12.14 or v14.15.

If your Node.js version does support the upgrade, the following output is produced (with most output omitted for brevity):

The installed Angular CLI version is older than the latest stable version.
Installing a temporary version to perform the update.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
Using package manager: 'npm'
Collecting installed dependencies...
Found 40 dependencies.
Fetching dependency metadata from registry...
    Updating package.json with dependency @angular-devkit/build-angular @ "0.901.15" (was "0.803.29")...
    Updating package.json with dependency @angular/cli @ "9.1.15" (was "8.3.29")...
…
  UPDATE package.json (2136 bytes)
⠧ Installing packages...
Packages successfully installed.
** Executing migrations of package '@angular/cli' **
…

A successful build using the View Engine compiler and ng build would look something like this (excess output omitted):

>ng build
Generating ES5 bundles for differential loading...
ES5 bundle generation complete.

chunk {runtime} runtime-es2015.js, runtime-es2015.js.map (runtime) 9.14 kB [entry] [rendered]
chunk {runtime} runtime-es5.js, runtime-es5.js.map (runtime) 9.13 kB [entry] [rendered]
…
chunk {app-dashboards-dashboards-module} app-dashboards-dashboards-module-es2015.js, app-dashboards-dashboards-module-es2015.js.map (app-dashboards-dashboards-module) 851 kB  [rendered]
chunk {app-dashboards-dashboards-module} app-dashboards-dashboards-module-es5.js, app-dashboards-dashboards-module-es5.js.map (app-dashboards-dashboards-module) 900 kB  [rendered]
chunk {main} main-es2015.js, main-es2015.js.map (main) 128 kB [initial] [rendered]
chunk {main} main-es5.js, main-es5.js.map (main) 148 kB [initial] [rendered]
chunk {styles} styles-es2015.js, styles-es2015.js.map (styles) 834 kB [initial] [rendered]
chunk {styles} styles-es5.js, styles-es5.js.map (styles) 836 kB [initial] [rendered]
chunk {polyfills-es5} polyfills-es5.js, polyfills-es5.js.map (polyfills-es5) 703 kB [initial] [rendered]
chunk {vendor} vendor-es2015.js, vendor-es2015.js.map (vendor) 9.53 MB [initial] [rendered]
chunk {vendor} vendor-es5.js, vendor-es5.js.map (vendor) 10.8 MB [initial] [rendered]
Date: 2021-06-17T13:02:02.697Z - Hash: 1388946abe7eee8af341 - Time: 243558ms

Third party NPM packages

With third-party NPM packages, to upgrade these libraries you would be advised to check the NPM site and locate the Versions menu. From there you can locate the builds that would be candidates to install into your package.json. Start off with the next version above your current version, build and re-test your project and observe any compatibility issues at run-time. If the build fails on a higher version, then rollback to a lower version and re-test.

Configuring production builds using AOT and build optimization

When a project is built with AOT (Ahead Of Time) compilation switched on, the build optimizer must be set. This requires all source and libraries to be compliant with the recommended optimizations. For a production build (that outputs into the distribution folder /dist) using:

ng build --prod

The setting within Angular.json is as follows:

"configurations": {
    "production": {
	    …
        "aot": true,
        "buildOptimizer": true,
        …

If your code and libraries are not compliant with the recommended optimizations, then you can create a production build with the non-optimized compilation as shown:

"configurations": {
    "production": {
        …
        "aot": false,
        "buildOptimizer": false,
        …

Sample non-optimized build output is shown below:

>ng build --prod
Generating ES5 bundles for differential loading...
ES5 bundle generation complete.

chunk {2} polyfills-es2015.35d1eebbe698f3fe4a83.js (polyfills) 61.9 kB [initial] [rendered]
chunk {0} runtime-es2015.b1d435d1196c1cf6e9e3.js (runtime) 2.29 kB [entry] [rendered]
chunk {0} runtime-es5.b1d435d1196c1cf6e9e3.js (runtime) 2.29 kB [entry] [rendered]
chunk {7} 7-es2015.bf30eff9333d972bd37c.js () 13.9 kB  [rendered]
chunk {7} 7-es5.bf30eff9333d972bd37c.js () 16 kB  [rendered]
chunk {6} 6-es2015.3a37c394d9b81c7ef898.js () 24.9 kB  [rendered]
chunk {6} 6-es5.3a37c394d9b81c7ef898.js () 27.7 kB  [rendered]
chunk {5} 5-es2015.213e08d7f407b0109bbd.js () 82.6 kB  [rendered]
chunk {5} 5-es5.213e08d7f407b0109bbd.js () 88.2 kB  [rendered]
chunk {3} polyfills-es5.e8474720721bed3dde5b.js (polyfills-es5) 137 kB [initial] [rendered]
chunk {1} main-es2015.1ba243daaf5a36a7ee58.js (main) 2.06 MB [initial] [rendered]
chunk {1} main-es5.1ba243daaf5a36a7ee58.js (main) 2.32 MB [initial] [rendered]
chunk {4} styles.ba895ca16ef0715b7a0d.css (styles) 205 kB [initial] [rendered]
Date: 2021-06-18T13:13:25.660Z - Hash: c2b076a138e7987ae393 - Time: 383880ms

WARNING in budgets: Exceeded maximum budget for initial-es2015. Budget 2 MB was not met by 330 kB with a total of 2.32 MB.

WARNING in budgets: Exceeded maximum budget for initial-es5. Budget 2 MB was not met by 672 kB with a total of 2.66 MB.

Without the optimizations, the build will succeed but display a warning on the build output exceeding the recommended limit set:

WARNING in budgets: Exceeded maximum budget for initial-es2015. Budget 2 MB was not met by 330 kB with a total of 2.32 MB.

WARNING in budgets: Exceeded maximum budget for initial-es5. Budget 2 MB was not met by 672 kB with a total of 2.66 MB.

The build size limits are set within the budget configuration:

"budgets": [
{
    "type": "initial",
    "maximumWarning": "2mb",
    "maximumError": "5mb"
},

Projects that contain Unit Tests and E2E Tests (such as Protractor)

With a project that contains unit tests, the build will attempt to compile all the .spec.ts test spec files and these will give you many compiler errors. If your project already has unit tests, these will break the build. During the upgrade, tsconfig.app.json will be modified to remove the exclude option. You can re-instate it as follows:

"exclude": [
    "src/test.ts",
    "src/**/*.spec.ts"
],

Then rebuild.

Errors that occur during builds and suggested resolutions

If you get errors like this within Material library components:

ERROR in Failed to compile entry-point @angular/material/sidenav (es2015 as esm2015) due to compilation errors:
node_modules/@angular/material/fesm2015/sidenav.js:1513:26 - error NG1010: Value at position 3 in the NgModule.imports 
of MatSidenavModule is not a reference: [object Object]

Then remove this entry from package.json:

"postinstall": "ngcc"

Another error related to module exports like the one below:

Error: Failed to compile entry-point @angular/material/tooltip (es2015 as esm2015) due to compilation errors:

node_modules/@angular/material/fesm2015/tooltip.js:1129:26 - error NG1010: Value at position 3 in the NgModule.exports of MatTooltipModule is not a reference: [object Object]
1129                 exports: [MatTooltip, TooltipComponent, MatCommonModule, CdkScrollableModule],

When I get this error, it is caused by using a version of Material at 9.0 or above. I do not just get one, I see numerous errors, all similar, so instead of continually attempting workarounds or restructuring my imports, I would rollback the Material version to 8.2.x and the build will be more successful.

Resolving errors when running the application after successful build

After a successful build, you may get errors while the application is running that are difficult to resolve from the code base.

A common error that occurs after running ng serve is shown below:

core.js:36282 Uncaught TypeError: Cannot read property 'id' of undefined
    at registerNgModuleType (core.js:36282)
    at core.js:36300
    at Array.forEach (<anonymous>)
    at registerNgModuleType (core.js:36296)
    at core.js:36300
    at Array.forEach (<anonymous>)
    at registerNgModuleType (core.js:36296)
    at new NgModuleFactory$1 (core.js:36461)
    at compileNgModuleFactory__POST_R3__ (core.js:42350)
    at PlatformRef.bootstrapModule (core.js:42717)

This error is caused by a component that uses a reference that cannot be resolved to a module. To be able to see detailed errors, in Angular.json, enable AOT compilation:

"aot": true,

Next, run a production build:

ng build --prod

You will then see the cause of the error. A typical error would be as shown:

src/app/services/notification-service.ts:10:47 - error TS2339: Property 'baseLoanApiUrl' does not exist on type '{ production: boolean; baseApiUrl: string; }'.
10     private baseLoanAPI: string = environment.baseLoanApiUrl;

After correcting the source issues re-build, run and re-test the application.

After upgrading ensure that the application is thoroughly tested and all components that are dependent on upgraded libraries are tested and pass any unit tests and/or E2E for the project. A discussion on the compilation issues and compatibility of Angular Material Components and the Angular Ivy compiler is in the following discussion here at the Angular component github site.

In a future post I will show how to upgrade an Angular application from version 9 to version 10.

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial