Tim Deschryver

ng update: the setup

With the Angular 6 release, already a couple weeks ago, the CLI package @angular/cli also received an update (also version 6). You will probably already have heard of it or even used it, but this update of the CLI introduced a new command ng update. In short this updates your application and its dependencies by taking a look if any of the dependencies in your package.json has a new version available. For more details about Angular 6 release you can take a look at the resources at the bottom of this post.

If you want to automatically let the CLI update your own library when a user runs ng update you’ll have to plug into the ng-update hook. As an example we’re going to use frontal— a selectbox/dropdown component based on downshift - which is currently on version 1.0.0 and after running ng update we want to have version 2.0.0 beta installed. There are also some popular packages where you can take a peek: RxJs, angular/material2 and recently the NgRx packages.

Enough of the what, let’s take a look at the how!

The first step is to setup the schematics by creating a migrations.json file, the name isn’t set in stone so you can name it everything you want (RxJS named theirs collection.json).

{
  "schematics": {
    "frontal-migration-01": {
      "description": "Upgrade to version 2",
      "version": "1.0.0",
      "factory": "./2_0_0/index"
    }
  }
}

In the schematics value there is a property for each migration, the property name itself isn’t used (I think) but must be unique. The important part here are the version and the factory values. version is used to match the current installed version to run the update against, in the example the update is going to run when version 1.0.0 (or lower) is installed. factory is used to point to a factory function where all the magic happens, in the example it will call the default function inside ./2_0_0/index. It’s also possible to specify a function, with the following syntax "factory": "./update-6_0_0/index#rxjsV6MigrationSchematic".

This is the function which is called during ng update. In the implementation below, the dependency version will be updated in the package.json. Note that it’s also possible to do more than just upgrading the version number, for instance if we take a look at angular/material2 it also runs some linter rules.

import { Rule, SchematicContext, Tree, SchematicsException } from '@angular-devkit/schematics'

export default function(): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const pkgPath = '/package.json'
    const buffer = tree.read(pkgPath)
    if (buffer == null) {
      throw new SchematicsException('Could not read package.json')
    }
    const content = buffer.toString()
    const pkg = JSON.parse(content)

    if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) {
      throw new SchematicsException('Error reading package.json')
    }

    if (!pkg.dependencies) {
      pkg.dependencies = {}
    }

    if (pkg.dependencies['frontal']) {
      pkg.dependencies['frontal'] = `2.0.0-beta.1`
      tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2))
    }

    return tree
  }
}

Wouldn’t it be 🍌 🍌 🍌 if all libraries would automatically fix (breaking) changes during this step, or at least let you know which steps you have to make in order to have a successful update?

Last, an ng-update entry must be added to the package.json. This is necessary to let the angular CLI know where to look for the migrations.

"ng-update": {
  "migrations": "./migrations/migration.json"
}

It is possible to test the factory function by creating a UnitTestTree, having a dependency to the library with the old version number. Next the migration is is run with runSchematic, expecting that the version number has been changed to the correct version.

import { Tree } from '@angular-devkit/schematics'
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'
import * as path from 'path'

const packagePath = '/package.json'
const collectionPath = path.join(__dirname, '../../migrations/migration.json')

function setup() {
  const tree = Tree.empty() as UnitTestTree
  tree.create(
    packagePath,
    `{
        "dependencies": {
          "frontal": "1.0.0"
        }
      }`,
  )

  return {
    tree,
    runner: new SchematicTestRunner('schematics', collectionPath),
  }
}

test(`installs version 2.0.0`, () => {
  const { runner, tree } = setup()
  const updatedTree = runner.runSchematic('frontal-migration-01', {}, tree)
  const pkg = JSON.parse(updatedTree.readContent(packagePath))
  expect(pkg.dependencies['frontal']).toBe(`2.0.0-beta.1`)
})

To test the ng update command locally before publishing to npm you can use verdaccio, a lightweight private npm proxy registry. There is a resource at the bottom, which addresses this point in detail.

As result we can see that a new version is available after running ng update. I’m using the next flag to also include next tags because version 2 hasn’t been released yet.

we run ng update frontal — next and see that a new version is available

After running the update command we can see the version number is changed in the package.json and we can start using the version in our project 🎉.

the version number is updated to the new version after ng update frontal — next has ran

Version 6 of Angular Now Available

Seamlessly Updating your Angular Libraries with the CLI, Schematics and ng update

angular/devkit