Update Guide: NX Workspace to Angular 17

Marcell Kiss
7 min readNov 24, 2023

If you are working within an NX workspace and can’t wait to try the latest Angular features, this article is for you.

New features

You might already be familiar with Angular 17 and what it brings to the table. Besides a new logo and an impressive new documentation page - angular.dev - it offers some great new features:

The new control flow and faster builds fundamentally improve the Developer Experience, providing strong motivation to update your project as soon as possible.

Update process

This article presents an example update process, including project-specific details and potential challenges. However, in your own project, with your unique settings, you may encounter different obstacles.

In an NX workspace, the update process should be as simple as executing the following commands:

// Updates your dependencies in package.json 
// and generates a migrations.json file
npx nx migrate latest

// Installs the new dependencies
npm install

// Executes the migration scripts based on migrations.json one-by-one
npx nx migrate --run-migrations

In theory, this sounds simple. In practice, issues often arise.

Migrate latest

Before continuing, let’s examine what npx nx migrate latest does.

This command updates all the @nx/* and @angular/* dependencies in your package.json file to their latest versions. The notable exception I found was @angular/cli, which was updated later during the migration process.

In addition, it also generates a migrations.json file, which contains all the scripts which should be executed on your code to adopt the changes — eg. automatic config changes and similar.

Npm install issues

In my case, npm install was failing with this error:

npm ERR! While resolving: portals@0.0.0
npm ERR! Found: @angular-devkit/build-angular@16.2.1
npm ERR! node_modules/@angular-devkit/build-angular
npm ERR! dev @angular-devkit/build-angular@"17.0.0" from the root project
npm ERR! peer @angular-devkit/build-angular@">= 14.0.0 < 17.0.0" from @nx/angular@16.8.1
npm ERR! node_modules/@nx/angular
npm ERR! @nx/angular@"17.1.1" from the root project
npm ERR! @nx/angular@"16.8.1" from @nrwl/angular@16.8.1
npm ERR! node_modules/@nrwl/angular
npm ERR! @nrwl/angular@"16.8.1" from @nx/angular@16.8.1
npm ERR! 2 more (@storybook/angular, jest-preset-angular)

Let’s focus on this line:

npm ERR!   peer @angular-devkit/build-angular@">= 14.0.0 < 17.0.0" from @nx/angular@16.8.1

Which is saying, that @nx/angular@16.8.1 package still wants to use an earlier version of @angular-devkit/build-angular (<17.0.0), although 17.0.0 is defined in the package.json file.

Ok, that’s fine, but we have @nx/angular@17.1.1 in the package.json after the update… So where does the @nx/angular@16.8.1 come from?

It comes from the previous installation, so let’s get rid of that and start from a clean state by removing the package.lock.json file and the node_modules folder:

rm package-lock.json && rm -rf node_modules

After this npm install should execute smoothly.

If not, you may have other conflicts, for example libraries which were not updated by the migrate command, but they still depend on angular <17.0.0. In this case, you have to update those one-by-one to the proper version.

It’s a good idea in general, to try to introduce as few dependencies as you can. It makes such updates much easier and you can move faster.

Running the migrations

It’s worthwhile to pay attention to the console output when executing npx nx migrate --run-migrations, as it provides step-by-step details of what is being executed.

Ran 17.0.0-move-cache-directory from nx
Updates the default cache directory to .nx/cache

UPDATE .gitignore
UPDATE .prettierignore
---------------------------------------------------------
Ran 17.0.0-use-minimal-config-for-tasks-runner-options from nx
Use minimal config for tasksRunnerOptions

UPDATE package.json
UPDATE nx.json
---------------------------------------------------------
[object Object]
Ran rm-default-collection-npm-scope from nx
Migration for v17.0.0-rc.1

UPDATE nx.json
---------------------------------------------------------
Ran move-options-to-target-defaults from @nx/jest
Move jest executor options to nx.json targetDefaults

UPDATE nx.json
UPDATE apps/sample-app/project.json
// Same repeats for all projects
---------------------------------------------------------
Ran update-angular-cli-version-17-0-0 from @nx/angular
Update the @angular/cli package version to ~17.0.0.

UPDATE package.json
---------------------------------------------------------
Ran rename-browser-target-to-build-target from @nx/angular
Rename 'browserTarget' to 'buildTarget'.

UPDATE apps/sample-app/project.json
// Same repeats for all apps
---------------------------------------------------------
Ran update-17-0-0-rename-to-eslint from @nx/linter
update-17-0-0-rename-to-eslint

UPDATE package.json
UPDATE apps/sample-app/project.json
// Same repeats for all projects
UPDATE migrations.json
---------------------------------------------------------
Ran block-template-entities from @angular/core
Angular v17 introduces a new control flow syntax that uses the @ and } characters. This migration replaces the existing usages with their corresponding HTML entities.

UPDATE libs/../../x.component.html
// Plus all other templates, where we had the '@' sign gets encoded to '&#64;'
---------------------------------------------------------
Skipping migration for project sample-project. Unable to determine 'tsconfig.json' file in workspace config.
// Same repeats for all projects (apps and libs)

✓ Updated Angular Material to version 17

Skipping migration for project sample-project. Unable to determine 'tsconfig.json' file in workspace config.
// Same repeats for all projects (apps and libs)

Summing it up, a new .nx cache folder got introduced in the root folder, the @angular/cli got updated, the @ and { characters have been replaced with their HTML entities in the templates — due to the new control flow syntax — and a bunch of nx.json and project.json config changes were executed, like renaming @nx/linter:eslint to @nx/eslint:lint.

Ok, great, we successfully updated the project, or, didn’t we? When I tried to validate the project (by linting, building, testing), it threw the following error:

Unable to resolve @nrwl/linter:eslint

This is the lint executor in some of the project.json files, seems like it wasn’t replaced in some of the projects to the newer version, so I had to update those project.json files manually:

"lint": {
"executor": "@nx/eslint:lint",
...

During running the linting, it might happen, that you get some errors you didn’t have before, namely:

@typescript-eslint/no-unused-vars

error  'x' is defined but never used       @typescript-eslint/no-unused-vars

and @typescript-eslint/no-explicit-any

error  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any

The dirty quick fix is to switch the errors to warnings — as they were originally — simply by changing your global eslint config:

{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn",
...

So now we can say, that we migrated to the latest version of @nx and @angular successfully. Although we still have the old *ngFor, *ngIf, etc. control flows as there are no auto-update migration scripts provided by nx — to date.

Adopting the new control flow syntax

Fortunately, @angular/core provides support here. Simply run ng g @angular/core:control-flow in the root folder of your NX workspace. Note that this script is in developer preview, so use it cautiously.

ng g @angular/core:control-flow

After we applied the auto-updates and executed the tests, we had a new error message, saying:

@switch block can only contain @case and @default blocks

It turned out, we used an if statement within a switch, but not within a case. This was relatively easy to fix, by simply moving the if statement out of the switch.

Later on, we noticed, that the formatting of the templates is not as pretty as it was before, proper indentation was missing from all the updated files.

If you're using prettier, the simplest solution is to run it on the templates:

npx prettier ./**/*.html --write

Of course, feel free to change the wildcard ./**/*.html path according to your needs. And before you do this, you maybe also want to update to prettier@3.1.0 as it adds extra support for Angular's new control flow syntax

npm i prettier@3.1.0 --save-dev

Note: I also had to restart my VSCode, otherwise the new version of prettier didn't start working in the editor.

Running prettier can still run in to errors on the so-called ICU expressions (which are supported by angular i18n)

SyntaxError: Unexpected closing block. The block may have been closed earlier. If you meant to write the } character

Here you can decide to wait for a prettier fix or simply replace them by calculating the proper string in code.

Adopting the new ESBuild based builder

Ok, great so far, but don’t stop here. Angular 17 is also spoiling us with a great new builder, which is faster than ever before — thanks to esbuild.

To have a better understanding of what changed and how exactly, it’s worth to take a look at the Angular documentation page.

In a nutshell, you have two options, you either go with the

  • browser builder — which is easier to adopt
  • or with the application builder — which is more robust

If you take the browser builder, the only thing you have to change is the name of the builder in your respective project.json file from

@angular-devkit/build-angular:browser

to

@angular-devkit/build-angular:browser-esbuild

So we added an -esbuild suffix, 8 letters, and we have a faster build, great deal, right?

Although, the recommended way is to use the application builder, which prepares you much better for the future, for example by providing SSR or SSG capabilities.

In this case, you go with @angular-devkit/build-angular:application in you angular.json, you have to rename your main option to browser, plus remove a bunch of options which are not relevant anymore: buildOptimizer, resourcesOutputPath, vendorChunk, commonChunk, deployUrl, ngswConfigPath

And that’s it, it’s also not that crazy difficult.

Also take care, that with the new build everything you had under dist/apps/x is going to move to dist/apps/x/browser.

Review

Maybe upgrading from a previous version of nx and angular to v17 is a bit more effort than just simply running 1–2 commands, but it’s definitely worth it.

The Angular Renaissance is living its best, and the new features are improving the DX immensely, so don’t hesitate give it a try!

Note: Did you give it a try? How did it go?

I’m looking forward to read your feedbacks.

--

--

Marcell Kiss

Angular consultant | @AngularHungary organizer | @AngularArchitects trainer | speaking 🇬🇧🇩🇪🇭🇺 | @marcelltech - https://marcell.tech