Real-World Angular Ivy Upgrade (v9-next)
I recently published an article about the compatibility opt-in preview version of Ivy in version 8 and what I experienced attempting to upgrade. This is a follow-on to show what the current upgrade path looks like using Angular v9.0.0-next.6 pre-beta, which contains optimizations for bundle size and performance as well as some bug fixes that were not present in v8’s preview.
As a reminder, Ivy is not ready for production. This is still a compatibility opt-in preview. Use at your own risk. Your mileage may vary.
I’ll be upgrading a large commercial application that leverages many third-party dependencies and a wide gamut of the Angular framework.
The initial step is to upgrade Angular:
ng update @angular/cli@next @angular/core@next
On first attempt I ran into a dependency that specifies an Angular version < 9:
Incompatible peer dependencies found.
Peer dependency warnings when installing dependencies means that those dependencies might not work correctly together.
You can use the '--force' option to ignore incompatible peer dependencies and instead address these warnings later.
To get around this we can use the --force
flag:
ng update @angular/cli@next @angular/core@next --force
This seemed to compile, but scrolling through the output I saw this:
This migration uses the Angular compiler internally and therefore projects that no longer build successfully after the update cannot run the migration. Please ensure there are no AOT compilation errors and rerun the migration.. The following project failed: src/tsconfig.app.json
Error: error TS100: Couldn't resolve resource ../../assets/scss/common/component.common from /.../src/app/common/app-header.component.scssMigration can be rerun with: "ng update @angular/core --from 8.0.0 --to 9.0.0 --migrate-only"
Successfully migrated all found undecorated classes
that use dependency injection.
This is a bug that has been fixed since writing this article. If you’re curious about it, keep reading. If not, skip to the next section.
The problem was with an SCSS @import
statement:
@import '../../assets/scss/common/component.common';
The new compiler is much more strict. The actual file name is needed. If your files have an underscore at the beginning you may have been not including those either. In all of my SCSS imports I had to explicitly use the correct file name, in this case component.common.scss
. I had to run the command over and over, fixing imports until it finally continued past this point.
Now, before I continue, I’m going to upgrade everything I logically can in my package.json
to be sure all of my dependencies are as up to date as I can get them. I use a Visual Studio Code Extension called Version Lens to help manage my package.json
.
Version Lens annotates each item in package.json
with the current version specified and the latest available on npm.
Clicking on the “latest” link will update my package.json
to that version. I updated pretty much everything to latest, with the exception of @types/node
since I want that to match my version of node. Do not upgrade typescript beyond 3.5.x. If you do you won’t be able to compile with a cryptic “ERROR in TypeError: Cannot read property ‘kind’ of undefined” error. Angular doesn’t yet support TypeScript 3.6.
npm install
So far, so good.
ng serve
Uh, oh
ERROR in The ngcc compiler has changed since the last ngcc build.
Please completely remove the "node_modules" folder containing "/Users/jyoutsey/src/MyMedstudy/ng/node_modules/hammerjs" and try again.
This isn’t actually an error, per se. Third party npm modules are not compiled to be compatible with Ivy. So, we either have to A) run ivy-ngcc
against node_modules
, which will compile them for compatibility, or B) delete node_modules
and do another npm install, then ng build
, as ng build
and ng serve
will both execute the build target which will run ivy-ngcc
for you. I prefer B for the simple reason that I think deleting node_modules
now and then is a good thing. If you wish to do A then feel free. You’ll have to do B if you’re switching back and forth between Ivy-disabled branches and Ivy-enabled branches.
delete node_modules
npm install
ng serve
Here we start getting into some very detailed error messages:
ERROR in app/common/global-loading-indicator.component.ts:12:3 - error TS2554: Expected 2 arguments, but got 1.12 @ViewChild('inner') inner;
~~~~~~~~~~~~~~~~~~../node_modules/@angular/core/core.d.ts:7929:47
7929 (selector: Type<any> | Function | string, opts: {
~~~~~~~
7930 read?: any;
~~~~~~~~~~~~~~~~~~~
7931 static: boolean;
~~~~~~~~~~~~~~~~~~~~~~~~
7932 }): any;
~~~~~
An argument for 'opts' was not provided.
Now, this application is already on Angular version 8. And it’s building constantly without these errors. When we initially went from version 7 to version 8 we had to update some static flags, but only if it were static: true. Now, the opts is required and we must fill in the static: false. Depending on when you migrated from 7 to 8 you may not have to do this because at some point this became mandatory for both true and false.
The fix for these is fairly simple, but you’ll have to work through each one and provide the second argument:
@ViewChild('selector', { static: true/false })
As much as I appreciate that these error messages are very clear and helpful, they are not “linked” so that I can Cmd/Ctrl + Click to navigate to the offending file.
The simple answer to “should static be true or false?” is “if the item being queried for has or is in an *ngIf
or *ngFor
, then static should be false.”
<div *ngIf="...">
<!-- If querying for this div or anything contained in this div
then { static: false } -->
</div><div *ngFor="...">
<!-- If querying for this div or anything contained in this div
then { static: false } -->
</div>
You’ll have to assess your code and template to determine on a case-by-case basis which is correct. All but one of mine turned out to be false.
Here is the migration guide officially discussing this issue.
ng serve
Now I have some errors that I also saw in the version 8 preview.
ERROR in app/common/searchable-select.component.ts:27:4 - error TS1117: An object literal cannot have multiple properties with the same name in strict mode.27 [disabled]="disabled"
~~~~~~~~~~~~~~~~~~~~~
This is a case where you cannot have a direct class binding and an attribute binding with the same name, i.e.:
<app-some-component
[class.disabled]="value"
[disabled]="value">
</app-some-component>
I have it on authority that this is a bug, but I won’t name names. That said, the fix is really simple.
<app-some-component
[ngClass]="{ disabled: value }"
[disabled]="value">
</app-some-component>
ng serve
Uh oh… Now I’ve got a good one. And I plead innocence. I inherited this code base and haven’t been through it 100% over the past year. But we have a base @Directive
that @Component
s are derived from. My initial hunch is that the problem is that the @Directive
just needs converted to an @Component
. Here is the error:
ERROR in app/features/.../derived.component.ts:34:4 - error TS8002: 'stepNumber' is not a valid property of <app-derived>.34 [stepNumber]="getAdjustedStepNumber(2)"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For more information on base classes for components and directives and how Ivy will handle migration as of right now, please reference https://next.angular.io/guide/migration-undecorated-classes. As of writing this article, Ivy did not migrate this correctly for me.
@Input() stepNumber
is defined on the base component, not the derived, so Ivy is unhappy. Sure enough, changing the @Directive()
to the following will fix the compilation error. (But, generally speaking, favor composition over inheritance. This pattern would not be best practice, in my opinion.)
@Component({
selector: `app-base`,
template: ``
})
ng serveERROR in ./src/polyfills.ts
Module not found: Error: Can't resolve 'core-js/es7/array' in '/myApp/src'
I have to support IE which means I use conditional polyfills. However, one polyfill that seems to be missing is for Array. I can’t say I completely understand why I need to include this one and not others. But with the latest version of core-js
the version is not present in the path:
Edit polyfills.ts
to remove the version:
import 'core-js/es/array';
At this point, my app compiles! Celebration time!
Now that you have it building, you should re-run the migration. Run ng update
again to ensure that your migration is complete. I was so caught up in the details at this point that I forgot to do this and ended up dealing with some of these things manually. Be aware of that as you continue reading…
I do have some warnings left over:
WARNING in /myApp/src/app/common/interfaces/contentSpecialty.ts is part of the TypeScript compilation but it's unused.
Add only entry points to the 'files' or 'include' properties in your tsconfig.
This appears to be a bug and I’ve opened an issue with the Angular team. It is just a warning, so it’s not a show-stopper. The source of this warning is that the compiler is identifying an unused interface. Except, this interface is used. It is part of a data structure received through an API call. So, it’s referenced in a parent interface that is used in the project. But this interface is never assigned to or leveraged by the Angular code directly. One way to solve this error would be to move this interface into the same file as the parent interface, or to just in-line the interface into the parent interface.
I’m going to take the easier approach and just ignore it.
A quick test through my application and things seem to be working as expected. Of course, I would want to do a full regression test to ensure nothing odd is broken.
But what about bundle sizes and performance?
Well, the news isn’t great. For example, the main-es2015 bundle is 891kB in Angular version 8. But in v9 we have 2.03MB! Overall the Ivy build was larger by 1.45MB. By the writing of this article the CLI has advanced to 9.0.0-next.9, which is starting to add more of the optimizations to improve bundle sizes. Remember, this is an opt-in preview, not final shipping code. The Angular Team is still hard at work in this area.
As for performance, as a human being, I didn’t notice Ivy being any more/less performant. I’m sure I could profile it, but the reality was, I didn’t notice a difference. Each app will have different requirements on this front, so you’ll have to test your own application’s performance under v9 and Ivy.
As a reminder, Ivy is still not ready for production, and neither is Angular v9 yet. But now you have a feel for how much work there really is to get up and going with Ivy. Of course, you may have other third-party dependencies that just won’t work, or parts of the application that are leveraging something that we are not, so you may have some different experiences.
Please, open issues when you run into problems trying out Ivy. The Angular team is working hard to make the version 9.0.0 update a smooth one.
Be careful out there!
EnterpriseNG is coming November 4th & 5th, 2021.
Come hear top community speakers, experts, leaders, and the Angular team present for 2 stacked days on everything you need to make the most of Angular in your enterprise applications.
Topics will be focused on the following four areas:
• Monorepos
• Micro frontends
• Performance & Scalability
• Maintainability & Quality
Learn more here >> https://enterprise.ng-conf.org/