06 June 2018

Denis Sumbaev

Web-developer

Upgrading a project without CLI to Angular 6

In the following article, I’m going to describe the challenging process of updating an Angular application with custom Webpack configuration, which our team had to pull through 3 weeks ago. I guess our experience would be useful for those who use Angular with acustom Webpack config. For others, it is an illustration of where modern front-end could lead us and how to live with that.
Our team works on BILLmanager 6 interface. I would like to let you know that the number of files inside the project before the update exceeded 67 thousand so that you have a general idea about the size of it. Structurally, I would highlight two sub-projects: registration module and primary user interface. In terms of technologies, the project is based on components, directives and Angular modules written in TypeScript. There are a few Web components. We use SASS/SCSS to style and CSS variables to theme the app without having to recompile.

Prerequisite

Everything has its reason and our current difficulties started a year and a half ago. Angular 2 beta had been just released back then. Programmers in our company had experience in Angular 1 applications development, ReactJS and our own relatively small framework development. At that time Angular 2 had the advantages of both: its first version and ReactJS. So we chose it as the one with the most potential (and it’s Google after all); and also because of the success of Angular 1 and the formalization that TypeScript provides. We do not create small SPA websites, which could be delivered to customers and forgotten, our applications work for a long time and need constant support. BILLmanager is used by providers to sell hosting services and work with clients. Therefore it should be constantly supported and evolving, as well as other ISPsystem products. By and large the Angular 2 team had already mentioned multiple times that from now on there would be just Angular and development of the framework would be evolutionary, which is suitable for our internal processes.
As I’ve mentioned, our projects are large and long-lasting. They have complicated configurations with flexible settings for a particular build. Webpack is kind of a long-term standard for small and large project builds in the frontend world, so it was the clear choice.
As a result, the main part of project config looked like this: webpack.config.common.js.
This looks very similar to the Angular documentation angular.io/guide/webpack. The most interesting part of it is about the compilation of .ts files.
{
  test: /\.ts$/,
  loaders: [{
      loader: 'awesome-typescript-loader',
      options: {
        transpileOnly: process.env.NODE_ENV !== 'production'
      }
    },
    'angular2-template-loader',
    'angular2-router-loader'

  ],
  exclude: [/\.(spec|e2e)\.ts$/],
},
{
  test: /\.ts$/,
  include: [/\.(spec|e2e)\.ts$/],
  loaders: ['awesome-typescript-loader', 'angular2-template-loader']
},
As you can see we use loaders angular2-template-loader and angular2-router-loader for generating our Angular components. The same is written in the official documentation. And that is quite odd because both of the loaders weren’t created by the Angular team and they are also stored in user repositories on GitHub. One of the reasons to choose Angular as the main framework was exactly the fact that it works completely — you have got everything «out of the box», unlike ReactJS as an example. But here we see that the tool to build our project doesn’t come «out of the box» at all.
Anyway, this config has been working from version two to version five, there were no reasons to worry. Although there was one. During ng-conf 2017 Brad Green told about the attempt to build Angular apps with Bazel and Closure. Those who worked with big Angular projects would agree — builds take a lot of time. Our first build in development mode on the fifth Angular version with the second webpack takes more than 4 minutes. So the framework developers’ intention to make builds faster is quite justified. However, there is an alternative point of view. As my colleague said:
«Why on earth to create a slow building framework and then begin to speed it up?»

Updating Angular to the 6th version

In our company, it is well-known what happens if you don’t update the development tools. We know where this could lead in the end. Revolutions are not made painless and are better to be prevented by evolving evolutionary. Therefore as soon as the news about a stable Angular 6 version came out, we decided to update our project. But it wasn’t painless.
We visited the website with update guide update.angular.io, the same way we did during the last update update.angular.io. There was the first surprise awaiting for us.
If you don’t specify “I use ngUpgrade”, the guide will suggest running ng update @angular/core command anyway.
During the update of previous versions that wasn’t the case. But now it seems we are not able to update without CLI. Or aren’t we? Probably this is a bug, that will be fixed, but today and a week ago we couldn’t find the clear description of the steps to update without CLI.
If you are like us, and still do want to continue updating your project, this is where our tricky journey begins. For a start, we should decide on our direction:
  1. Install CLI and update according to the steps described in the guide.
  2. Update packages separately and edit configs yourself.
The first option seemed easier and that what we chose. But after the installation, CLI update and running command ng update @angular/core we were disappointed.
$ ng update @angular/core
Package "@angular/compiler-cli" has an incompatible peer dependency to "typescript" (requires ">=2.7.2 <2.8", would install "2.8.3")
Invalid range: ">=2.3.0 <3.0.0||>=4.0.0"
In issues on GitHub, you can find github.com/angular/angular-cli/issues/10621. As for today, this error was probably fixed (according to github.com/angular/devkit/pull/901), but at that moment we decided not to dig down into the update utility and instead we decided to update the packages manually.
After the packages update the project run failed, which was quite predictable. Angular 6 uses Webpack 4 (you are able to see that if installed with CLI). Therefore on the next step, we updated the Webpack and related packages to the latest versions. The story about the Webpack update is a topic for another separate article. So here I would only mention that if you use extract-text-webpack-plugin, change it to mini-css-extract-plugin. That will save you time and energy. Text about the advantages of the fourth Webpack is available here, and of course in article on migration.
Besides updating Angular and Webpack, we also needed to upgrade RxJS to the sixth version, otherwise, the project just wouldn’t run. This required step is not so difficult to make, you just need to follow the migration documentation. There shouldn’t be any significant difficulties, RxJS provides a utility which adds necessary changes to the project.
Meanwhile, let’s go return to our update to Angular 6. The project still doesn’t run and gives out a lot of unintelligible errors. This is the time to have a look at the loader, which processes .ts files. We use angular2-template-loader and angular2-router-loader together. If you take a look at angular2-template-loader repository, it is clear that there weren’t any updates for a year (it is quite odd that official documentation still suggests to use it).
It looks like the problem is in the way the loader processes our code. We started to look for an alternative and found a plugin for Ahead-of-Time (AoT) compilation of @ngtools/webpack. This is not an equal exchange, as we are used to JIT-compilation. On the other hand, the Angular team has long been talking about plans to make AoT-compilation default. @ngtools/webpack — the official tool from Angular DevKit, is constantly updating and was altered for the sixth version of the framework. To be fair, I have to note that it is possible to build an Angular 6 project with angular2-template-loader and angular2-router-loader plugins. Both of these plugins might be suitable for development, but not for production builds, as there is a lack of additional code validation. This is exactly what prevented us from immediately making all the necessary changes for the transfer to the sixth version.
AOT — compilation is mostly different because of template compilation at the moment of the app build, but not after the opening in the user’s browser. On one hand, it speeds up the app and allows us to reduce its size, on the other — it takes more resources during compilation and requires more precision in writing components.
Transition to AoT-compilation is a separate large task. To perform it, we have to rewrite most of the project, as AoT has stricter requirements for the code. If you didn’t follow them from the start, it’s going to be tough. But there is a way out. It’s possible to use plugin @ngtools/webpack with JIT-compilation. To do that, you need to add parameter @ngtools/webpack in plugin settings.
I’d like to note the main problems which we had to fix during the transition to @ngtools/webpack plugin:
  1. Change all private variables to the public in templates.
  2. It is preferable not to inherit one component from another (as well as directives). In general, this is logical, but angular2-template-loader skipped the modules, which were created in the wrong way, and the @ngtools/webpack gave errors.
  3. If you neglect the above recommendation, you can get an error in a component constructor using simple variable types. This is probably the most unusual error. The component looks like this:
@Component({
 selector: '[form-component]',
 template: ''
})
export class FormComponent extends BaseComponent implements OnInit {
 constructor(
   public formService: FormService,
   public formFunc: string,
   public formParams: Array<any> = []
 ) {
   super();
 }
...
In the logs we can see the following:
ERROR in : Can't resolve all parameters for FormComponent in form.component.ts: ( [object Object], ?, ?)
I strongly recommend following the second rule. But if for some reason this doesn’t work out, you can make a small hack https://stackoverflow.com/a/48748942/4778628 by changing the code above to:
@Component({
 selector: '[form-component]',
 template: ''
})
export class FormComponent extends BaseComponent implements OnInit {
 constructor(
   public formService: FormService,
   @Inject('') public formFunc: string,
   @Inject('') public formParams: Array<any> = []
 ) {
   super();
 }
...
Unfortunately, the errors didn’t stop there and we got another one in the Angular compilation plugin itself:
At first, we thought that we give compiler packages from node_modules and it is not able to process them, but adding exceptions didn’t affect the error. There were no alternatives and it was too late to go back, therefore we created a small PR into @ngtools/webpack. These changes were included in version 6.0.1 of the package. After that, the build was successful and the project was finally up running!
But! It occurred that all the modules except for main one were skipped. Let’s have a look at @ngtools/webpack plugin settings.
new AngularCompilerPlugin({
     platform: 0,
     sourceMap: true,
     tsConfigPath: path.join(PATHS.root, 'tsconfig.json'),
     skipCodeGeneration: true,
   })
At first sight, everything matches the documentation. Please note, that the parameter entryModule is marked as optional. Through tests and re-tries, we found out that if the parameter wasn’t specified, the build didn’t go further than the main module, and therefore we got a non-working application. To fix the problem we just had to add entryModule .
new AngularCompilerPlugin({
     platform: 0,
     entryModule: path.join(PATHS.src, 'apps/client/app/app.module#AppModule'),
     sourceMap: true,
     tsConfigPath: path.join(PATHS.root, 'tsconfig.json'),
     skipCodeGeneration: true,
   })
At the beginning of the post, I mentioned that we have two sub-projects. But in entryModule, it is possible to specify only one. Here we were quite lucky, as the second application doesn’t contain inner modules. If your situation is different: multiple complicated projects inside one, you have to create a separate config for each subproject or wait until this PR is accepted into the Angular DevKit repository.
In the end, the main part of config in the project started to look like this.

Conclusion

After all the manipulations described above, we got a working Angular 6 application and our own Webpack config. During this work, 180 files of the project were edited and it took us about one week.
Angular is a solid framework and as the sixth version came out it becomes even more noticeable. At the moment we have not only extra tools, such as router or HTTP-requests library, but also build tools that come «out of the box». And it is better not to touch them, not to add any changes that weren’t suggested by developers of Angular. Only, in this case, you would be able to update the project and probably won’t face the need to edit hundreds of files after the update. Otherwise, there would be a tough journey in front of you and you would have to give likes to negative comments on the new version in the sea of positivity and overall joy of others.
This doesn’t mean that Angular is bad or good, it just requires to be treated in a special way and won’t suit everybody. If you work through CLI, build projects with ng utility, test and create modules and components with it, that is a win. Our team would be happy to do the same, but unfortunately, we are not able to get rid of our Webpack config, as a lot depends on it. The proverb says: «Danger foreseen is half avoided», but we didn’t see this danger coming. Only a year ago CLI wasn’t a required tool for Angular projects, but today the documentation doesn’t even say how to update without it.

Denis Sumbaev

Web-developer