Angular Schematics for Libraries: Keeping Your Projects Up-to-Date

Viktor Slavov / Monday, April 27, 2020

Angular schematics have been around for some time now, and I think most of us have gotten used to working with them and enjoying their many benefits. From running a simple ng new to creating complex schematics that automate workflow and project setup, the schematics engine is a big part of any Angular application's lifecycle.

But what about an Angular library?

At Infragistics, we take great pride in developing and maintaining our open source Angular component library. But with the library gaining more and more users, and the ever-evolving nature of Angular, we needed to provide an easy way to keep projects up-to- date. This is where, as with most things Angular, the schematics come into play.

ng add, ng update and more

In this blog, we’ll show you how to set up your library so that it takes full advantage of Angular CLI’s ng add and ng update commands. We will do this by defining schematic collections in a specific way.

We’ll be covering the following steps:

  1. ng add and ng update hooks
  2. Setting up ng add schematics
  3. Setting up ng update schematics (migrations)
  4. Bundling and running your schematics

In order to take full advantage of this post, ideally you should already be familiar with the basics of Angular schematics, and you might be interested in some of the other great posts on Medium.

To borrow from a recent angular.io article:

 A schematic is a template-based code generator that supports complex logic. It is a set of instructions for transforming a software project by generating or modifying code. Schematics are packaged into collections and installed with npm.

Schematic entry points

Aside from calling an Angular schematic with the usual 

ng g my-custom-library:schematics [args]

Angular’s CLI also provides two other entry points for your custom library workflows - ng add and ng update. If you are not familiar with these ng commands, you can learn more about them here and here. In an Angular application that's using your library, these function similarly to npm's postinstall hook. They are called automatically by the Angular CLI - when calling

ng add my-custom-library

Or

ng update my-custom-library

respectively.

Migration Schematics

One of the main benefits in using Angular’s CLI is updating your package dependencies while minimizing the need to manually workaround breaking changes or deprecations. All of this can be automated by defining migration schematics for your library!

A migration schematic is run each time a newer version of your library is added to the consuming application via ng update your-custom-library. In a migration schematic, you can not only define what changes your migration schematics should perform, but also specify the scope of the migration (which version it affects).

Adding migration schematics

Defining migration schematics is done in a similar way to defining a normal schematic  —  you have to create a function that returns a Rule. That rule can manipulate the work tree, log stuff and/or anything else that you can do with Javascript. The more specific part is making sure that the Angular CLI calls the schematic on the ng update hook.

Defining the schematics collection

First, you need to define a schematic collection.json file. The name doesn't matter. In our library’s case, we've named it migration-collection.json, as we have multiple schematic collections and this makes it easier to tell which is which.

The structure of your collection.json should look something like this:

{
	"schematics": {
		"migration-01": { //  Migration name, no strict naming, must be unique
			"version": "2.0.0", // The target version
			"description": "Updates my-custom-library from v1 to v2", // A short description, not mandatory
			"factory": "./update-2" // The update schematic factory
		},
		"migration-02": {
			"version": "3.0.0",
			"description": "Updates my-custom-library from v2 to v3",
			"factory": "./update-3"
		},

       ...
	}
}

The schematics object houses all of your update schematics definitions. Each definition is under a named property (which has to be unique) and for each one, you have to specify the version in which the "changes" are introduced and the factory for the schematic that will be run on the working tree. The version attribute is used to determine when you schematic needs to be run.

For example, let's say you have an app consuming my-custom-library@1.0.0. With the above schematics definition, if you would run ng update my-custom-library@2.0.0, only the defined "version": "6.0.0"schematic would be run. When running

ng update my-custom-library@3.0.0

both 2.0.0 and 3.0.0 migration schematics would be run.

The actual definition of the migration schematics (for v2.0.0, for example) is in ./update-2/index.ts can be like any other schematic. A very basic implementation would be this one:

export default function(): Rule {

  return (host: Tree, context: SchematicContext) => {

    const version = `1.0.0` // You can get this dynamically from the package.json

    context.logger.info(`Applying migration for custom library to version ${version}`);

    return host;

  };

}

You can find the definition for one of the latest migration schematics in igniteui-angular here.

Disclaimer: In our library’s schematic implementation we are making heavy use of the TypeScript language service for manipulating files. I’m sorry for not going into details, but that would warrant a post of its own. If you have any questions, feel free to reach out in the comments and I’ll try to answer as best as I can.

Attaching schematics collection to ng-update hook

Once the migration collection.json is created and the migration schematics are defined, all that's left is to properly attach them to the ng-update hook. All you need to do for that is to go over to your library's package.json and add the following “ng-update” property:

{
	"name": "my-custom-library",
	"version": "2.0.0",
    ...
    "ng-update": {
		"migrations": "./migrations/migration-collection.json"
	}
}
 

That’s it! Now every time someone runs ng update my-custom-library, the Angular CLI and the schematics that you’ve carefully crafted will take care of any pesky breaking changes or deprecations that a user might otherwise miss!

ng-add

The other helpful entry point that the Angular CLI provides is the ng add hook. When you define an ng add schematic, it is run automatically as soon as a user adds your library to their consuming project by running

ng add [custom-library]

You can think of it as something akin to npm's postinstall hook.

Defining an ng add schematic for your library is the best way to make sure that its users will have an easy first-time experience with it. Through the schematics, you can automate some mandatory initial setup, add/update packages or simply log a big "thank you!" to all your users! :)

In our library, we've added an ng add schematic that installs our IgniteUI CLI to the project workspace so users can take full advantage of our components suite via our CLI.
We make some initial transformations to the workspace (adding a custom .json file needed for our CLI to run), add some styles and packages (for our components to properly render) and even call some schematics (schematiception!) from our CLI, where we've offloaded some of the Angular CLI specific logic.

All this (and more!) can be done automatically, simply by calling ng add - that, in my opinion, is the Angular CLI's true charm.

Create ng-add schematic

To add an ng add schematic to your library, you first need to define it. As with any other schematic, it needs to be part of a collection. If your project already exposes schematics, you can add it to the existing collection under the name ng-add. If not, you can create the collection from scratch:

In your library’s root folder:

mkdir schematics && cd schematics

echo blank > collection.json

mkdir ng-add

Open the newly created collection.json, modify it to point to the definition of the ng-add schematic

{
	"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
	"schematics": {
		"ng-add": {
			"description": "Installs the needed dependencies onto the host application.",
			"factory": "./ng-add/index",
			"schema": "./ng-add/schema.json"
		}
	}
}

Under schematics/ng-add/index.ts you can create the implementation of the ng-add schematic just as you would with any other schematic. For example, to display a simple "Thank you!" message to your users, you can do the following:

export default function (options: Options): Rule {
	return (_tree: Tree, context: SchematicContext) => {
		context.logger.info(`Thank you for using our custom library! You're great!`);
	};
}

The implementation of IgniteUI Angular's ng-add schematic can be found here.

Include in package.json

Once you’re done with the implementation of the schematic, you have to include the collection definition in your library’s package.json. If your library already exposes a schematics collection, you've got this covered! If not, you just need to add a "schematics" property, pointing to the location of your newly created collection.json:

{
	"project": "my-custom-library",
	"version": "2.0.0",
	...
	"schematics": "./schematics/collection.json"
}

You can find this part of the setup reflected on our library repo.

Bundling your schematics

You have to make sure that the schematics you've defined (both migrations and ng-add collections) are properly shipped with your library package. To do this, you will need to define a separate build task for your schematics (executed after the main build task has run), with outDir specified to point to the proper location in your project's dist folder (so the collection.json files have the same paths, relative to the package.json). Finally, you will need to explicitly copy any files that are excluded by the compiler (for example, any .json files!).

Assuming that the output directory for your compiled projects is ../dist (so your project’s bundled files would be under ../dist/my-custom-lib/) your schematic’s compilation output will have to be ../dist/schematics (for the default schematic collection, containing ng-add) and ../dist/migrations (for the migration schematic collection)

To do this, you must first define a tsconfig.json file for your schematics collections (located in ./schematics/ or ./migrations, respectively). For example, the content of the file for the core schematics collection (./schematics) would be something like this:

{
	"compilerOptions": {
		"target": "es6",
		"module": "commonjs",
		"sourceMap": false,
		"composite": true,
		"declaration": true,
		"outDir": "../../../dist/my-custom-lib/schematics/"
	}
}

Then, in your project’s package.json, define a script that builds your schematics and another one that copies the files:

{
	"name": "my-custom-library",
	...
	"scripts": {
		"build:schematics": "tsc --project migrations/tsconfig.json",
		"copy:schematics:collection": "cp --parents migrations/*/collection.json ../../dist/my-custom-lib/"
	},
	...
}

The same goes for the migrations - a separate script for TypeScript compile and for copying the files. 

For the Ignite UI for Angular repo, you can check the links to see the implementation of the default schematics collection and the migrations. (Since we’re using gulp as a build tool, we’ve defined separate tasks there for copying the collection.json files)

Running ng-add and ng-update schematics locally

Before you publish your library package with your new and shiny ng update and ng add functionality, you might want to verify that the schematics are performing the proper workplace transformations. Since, in the end, both of these are just basic schematic implementations, you can run them as you would any other - by using ng g!

After building your library files and schematics + migrations, you can go into your specified output directory (where your bundle’s package.json is located) and pack it

npm pack

Running ng-add

To check how your ng-add is command is performing inside of a new project, you can do the following:

Create a new project using the Angular CLI

ng g my-test-project --routing=true

Once the project is created, you can go into the project folder and install your package manually from the .tgz file:

npm i my-custom-library.tgz

Then, simply run

ng g my-custom-library:ng-add

and review the workplace change once it's finished - that's what your package's ng-add will do!

Running migrations

For testing migrations, you can follow the same steps as the above, though you’ll have to specify the path to the migration schematics collection.json (as it's different from the default schematics collection of the project).

Let’s say your latest migration schematic which you want to test is named update-1. Once you've manually installed your .tgz file, simply run:

ng g "./node_modules/my-custom-library/migration-collection.json:update-1"

This will apply all of the workplace changes that your latest migration schematic would apply.

Closing thoughts

Hopefully, this post has been helpful to you and has cleared up some things about Angular CLI’s ng add and ng update functionality. Angular's schematics are now a solid part of their projects' ecosystem and will surely continue improving. With more and more developers using them and raising awareness of them, now is a great time for libraries to start adopting schematics. Why not automate both you and your users worries away? :)

If you have any questions, feel free to reach out in the comments.

Thanks for reading!