The Complete Guide to Custom Angular Schematics

Schematics are great! They enable us to achieve more in shorter amount of time! But most importantly, we can think less about mundane stuff which leaves our limited attention span to focus on solving real challenges!

This is a pretty long article which gets into every detail of implementing custom Angular schematics…

This article will teach you how to create tailor made schematics specific to the needs of your project or organization! It will empower you and your colleagues to become much more productive!

What are we going to learn?

  1. Create custom schematics project workspace
  2. Concepts that describe schematics implementation (Factory, Rule, Tree…)
  3. Build schematics (and setup watch build for convenience)
  4. Run schematics (package vs relative path, debug mode, …)
  5. Implement simple schematics (generate a file, …)
  6. Parametrize schematics with schema and options
  7. Use advanced schematics templates (and string helper functions)
  8. Integrate custom schematics in Angular CLI worksapce
  9. Test custom schematics
  10. Build and publish custom schematics package
  11. Implement schematics as a part of Angular library project
  12. Add ng-add support
  13. Add ng-update support

Preparation

Before we start, we have to install @angular-devkit/schematics-cli package to be able to to use schematics command in our terminal. This command is similar to well-known ng generate but the main advantage is that we can run it anywhere because it’s completely independent from the Angular CLI.

This enables us to use schematics command and especially the blank schematics to generate new schematics project where we can start implementing our custom schematics.

1. Create project

Let’s generate new schematics project using schematics blank hello. What we will get is a folder with the following structure.

Files generated by the “schematics blank hello” command

The project was generated using schematics blank command because it is available in the out of the box available package @schematics/schematics which gets installed together with @angular-devkit/schematics-cli which we installed in the beginning…

Let’s have a closer look on what happened here and what are the most important files and content.

First of all, the collection.json file is the main definition file of the whole schematics library and contains definitions of all schematics available in that library.

If we looked into @schematics/angular package which is provided by default in every Angular CLI project we would see that its collection.json file contains entries like component or service. In other words, everything which we can normally generate using Angular CLI.

Our initial collection.json file will look like this…

This is image title

Content of our generated collection.json file

We can see that our library contains one schematic with the name hello which points to the ./hello/index file and more specially to the #hello function in that file.

We could also export hello function as default export and in that case we could just point factory to ./hello/index file without the need to specify function name…

Besides that we can specify also description and some other fields which we will add later. The description will be displayed when we use our hello schematic in Angular CLI workspace together with the --help flag but more on that later

Now, let’s have a look into the ./hello/index.ts file.

This is image title

Initially generated Angular Schematics factory stub with additional comments for clarity

2. Understand schematics concepts

Our schematic consists of multiple parts playing nicely together. Let’s learn more about these individual parts and their relationship!

The schematic factory

First of all, our file contains the hello function which is a factory function which returns us a Rule.

We might be curious why do we need a factory function and don’t implement just the rule. The thing is, we want schematic to be useful under variety of different circumstances so we need to be able to adjust them accordingly. That’s why the hello function accepts the _options argument.

That way we can parametrize our schematic rule (returned by the factory) so that it behaves accordingly.

The Rule

The second main component of the schematic implementation is the rule itself. Rule is called with a Tree and a SchematicContext. Once called, rule is supposed to make adjustments to the tree and return it back for the further processing.

The rule can call additional already implemented rules in its body because it posses everything that is needed for a rule execution. That is the tree and the context. We will see this in action later when we will use some utility rules (eg for template processing) inside of our main rule…

The Tree

We learned that the rule receives tree and makes changes to that tree but what does the tree represent?

Schematics are all about code generation and changing of existing files!

The tree is then virtual representation of every file in the workspace. Using a virtual tree instead of manipulating files directly comes with a couple of big advantages.

  • we only commit changes to the tree if EVERY schematics ran successfully
  • we can preview changes without actually making them ( with --dry-run flag)
  • the whole process is faster as the I/O operations ( writing files to disk ) happens only at the end of the processing

In a way, Angular Schematics use concept which is similar to what React does with the virtual DOM. The only difference being that with Schematics we are working with the file system…

Let’s summarize what we learned until now…

The schematic consist of a main file which exports a factory function. This function is called with the schematics options. The factory returns us a rule which is called with the virtual representation of the file system, aka the tree and the schematics context.

3. Build schematics

Our schematics project is implemented with Typescript but we run it as a standard node application. Because of that, running our hello schematic would result in error if we didn’t compile all .ts files beforehand. To do that we can simply run npm run build.

Running build manually after every change would be a tedious task so let’s implement build:watch npm script instead!

Add following line to the package.json file and run it using npm run build:watch

4. Run schematic

Cool we have our generated hello schematic so let’s try to run it. Previously we have been running schematics inside of the Angular CLI workspaces using ng generate but our schematics project doesn’t contain Angular CLI…

So what can we do?

Luckily, the schematics command allows us to specify package containing schematics. The command looks like this schematics : [...options].

The package name can be for example @schematics/angular and the schematic name component. The schematics command will by default try to find package inside of the node_modules folder in the directory where it was executed.

Our hello schematic was not yet packaged though…

In this case we can use relative path instead of the package name and we will get command which looks like schematics .:hello


*
Executing schematics from the local project using relative path (the “.” stands for current folder)*

Our hello schematic just returns the tree and hence no changes are performed and Nothing to be done message is displayed in the console…

Running schematics with relative path outside of the current folder

What if we wanted to run our hello schematic outside of the folder where it was implemented? It is possible but the relative path has to point exactly to the collection.json file instead of just the schematics project folder.

Running schematics using relative path from some other folder (remember to point exactly to the collection.json file)

5. Implement simple schematic

OK, we have a schematic implementation stub and we know how to build it and run it!

Now is the time to start creating something useful!

Let’s adjust our hello schematic rule so that it create a hello.js file with a console.log('Hello World!'); content. To do that we have to use .create() method which is available on the tree object.

Let’s run this adjusted schematic using schematics .:hello and see what happens!

We ran our hello schematic which created hello.js file but we can’t see it when we list the folder content, why?

As we can see, the schematics output says that the file /hello.js was created but when we list content of the folder the file is nowhere to be found.

The reason for this is that we specified our schematics package as a relative path (the .) and in that case the schematics are executed in the debug mode. The debug mode results in the same behavior as if running schematics with the --dry-run flag so no changes get committed to the file system.

We have two options to fix this situation. We can either run our command with --debug=false or --dry-run=false flag.

I personally usually go with --debug=false because it is easier to write

Our hello.js file gets generated and we can run it using node hello.js

As a side note, if we tried to run this command again we would encounter error saying that the Path "/hello.js" already exist.

This is usually solved by adding --force flag to the command execution but it would not help in our case. For now, we have to delete the file manually.

The --force flag does work in most situation when schematic is implemented using templates which we will explore later.

6. Parametrize schematics with schema and options

Nice, our hello schematic is now generating hello.js file which logs Hello World! string into console when called. Let’s make this more useful by allowing us to define who we want to greet!

Our schematic is called with the _options object which will contain all the flags that we pass when executing our schematic in the command line. For our purposes we can execute schematics .:hello --name=Tomas --debug=false

This is image title

Retrieve name from the options object and use it to parametrize our Hello string…

This will work and schematics will by default pass every specified flag to the _options object but we can do even better!

What we can do is to create Schema which describes what kind of options we can pass to our schematic. That way schematics will perform validation of the passed options and give hints to what we have to do to get desired results!

First of all, we have to create schema.json file with the following content inside of our ./src/hello folder.

Example of a minimal schema.json file which specifies name positional option

The file is pretty self-explanatory. The properties object contains definitions of all supported options. Options can have various types including string, boolean or even enum which validates passed value against the list of valid values.

In our file we specified one option, the name which is a positional option so that we can use it without specifying the flag itself (without --name).

Now we have to reference our newly generated schema.json file in the collection.json file to use it to validate command line option flags.

Add reference to schema.json file in the collection.json file

If we now tried to execute our hello schematic with unsupported option, let’s say name1, we would encounter following Error: Schematic input does not validate against the Schema: { "name1": "Tomas" }. This is great because it means our options validation is working!

Now we can create also schema.d.ts which will provide us with type checking and code completion in our schematic implementation.

And use it in our hello schematics…

The schema.d.ts can also be generated automatically by using packages like dtsgenerator. We can try navigating to ./src/hello (the folder which contains schema.json ) and running npx -p dtsgenerator dtsgen schema.json -o schema.d.ts which will generate our .d.ts file automatically!

Enhancing options with prompts

Specifying options enhances developer experience because we can provide them with helpful, well described error messages which will help them understand what they have to do to make the schematic work.

We can do even better by adding prompts!

Prompts are the way to provide schematic options without the need to know about them before hand. To use them we have to add x-prompt property to our schema.json file in the definition of the name option.

And this is how it will look like once executed with schematics .:hello --debug=false (note, we didn’t provide --name or positional name argument).

The prompt will provide developer with correct input type based on the option type. This is great because we will get yes/no for boolean and selection list for the enum option types!

7. Use schematic templates

Until now we only used basic Javascript template string to create the content of our file. This works but is very basic and doesn’t provide us with any support in terms of where the file should be created. Luckily, Angular Schematics templates are here for the rescue!

To use templates we can start by creating ./files/ folder in our ./hello/ folder.

We have to use ./files/ folder because it will be excluded from the Typescript compilation by default (see tsconfig.json in the root of the project). The folder has to be excluded because we don’t want to compile the templates!

If we wanted to use ./templates/ folder we would have to adjust tsconfig.json accordingly!

Let’s add one more nested folder inside of the ./files/ folder with the name hello-__name@dasherize__. This looks pretty funny on the first sight so let’s see what is going on.

The __ (double underscore) is a delimiter which separates name variable from rest of the normal string. The dasherize is a helper function which will receive value of the name variable and convert it to kebab case string and the @ is a way to apply variable to a helper function.

This means that if we provided name like AwesomeWrap the resulting folder name would be hello-awesome-wrap. Yummy !

Now we will create file with similar name inside of our last folder with the name hello-__name@dasherize__.ts. This works the same way as with the folder described previously.

Now, for the content of our newly created file we have to add this…

The Angular Schematics template syntax consists of `` closing tags to print value of some variable. It also supports function calls like dasherize or classify to adjust variable value inside of the template.

Example of how to use helper function in the Angular Schematics templates

Our template is ready but we still have to wire things up inside of the schematics rule.

This is image title

Adjustment we made to our original Angular Schematics rule

With these adjustments in place we can try it by running schematics .:hello 'Tomas Trajan' --debug=false

Notice, we put name into quotes so that we can include space between the first and the last name to demonstrate how well it will be handled by the dasherize helper function which produces correct hello-tomas-trajan.ts file.

More on the template helpers

The template helpers like dasherize or classify are available because we’re spreading strings object into the options object we’re passing into the template…

The thing is we could actually pass in any function. Let’s say we would like to add exclamation to the name. What we can do is to create addExclamation function and pass it in too!

Any function can be passed and used in the template which is great for abstracting away reusable parts of template creation logic!

Once we start using templates we can also use --force flag which will overwrite previously generated folders and files with same name which comes very handy when iterating on template content during development.

Note that --force flag does NOT work when creating files with tree.create() method…

Useful schematics

Cool now we have a simple working schematic which uses templates!

The hello schematic in itself is not that exciting but the possibilities are endless!

Imagine we wanted to generated something like a CRUD resource service for our Angular application. The schematic itself could be exactly the same, the only difference would be more complex template…

Note we are using dasherize, classify and camelize helper functions in this template to accommodate for all of our needs

Such a template would generate following file if called with the name 'product code'

File generated for previous template and name ‘product code’

Conditional templates

Let’s say that some of our CrudResourceService instances should give data back as it is received from backend but some have to transform it to better suit our frontend needs.

In that case we could add new boolean option to our schema.json (and .d.ts ) files with the name transform.

Add transform option to schema.json file

Add transform option to schema.d.ts file

Then we could use that variable also in the template to implement if / else conditional parts like this…

This is image title

We can use if / else (and even loops) in our Angular Schematics templates!

Please notice that the if / else blocks are delimited using <% instead of <%= which is used to print value of a variable.

Great! We’re generating something useful. Now is the time to put it all together and integrate it with Angular CLI ng generate command!

Additional template language features

Angular Schematics templates seem to support every JavaScript language feature. Let’s say we would also like to pass an array of frameworks to our template.

In that case we can use standard for loop to write these items into the template in any form, be it html or functions…

How to use for loop inside Angular Schematics templates

8. Integrate custom schematics with Angular CLI

Angular CLI comes with a bit specific environment which needs ti be handled so that our custom schematics integrate seamlessly.

Every Angular CLI workspace consists of zero to possibly many applications and libraries. This brings us to the concept of project.

Every time we run ng generate component we’re generating it in some project. The angular.json file contains definitions of all workspace projects together with the defaultProject property. It will determine the location of our component. We can override it by specifying --project some-app flag in the ng generate command explicitly!

Such a command then looks like ng generate service some-feature/some-sub-feature/some-service --project some-other-app.

Good, we know we have to add support for project so let’s get to it!

Make it work!

First we have to extend our schema.json (and .d.ts ) files…

Add project property to our schema.json file

Add project property to our Schema interface

Now we have to make adjustments to the implementation of our schematics rule.

It might look like a lot but in the end it’s all about resolving correct name and the location of our newly generated file!

  1. We have to retrieve workspace configuration from angular.json file. The config gives us access to the defaultProject and the projects object itself.
  2. We will use them to retrieve specific project configuration
  3. We will use project configuration to resolve project default path (eg src/app or projects/some-app/src/app in standard Angular CLI workspaces)
  4. We will parse name together with default project path to get hold of final path and the name of the created file.
  5. We will pass final name into the template and use path to move created file in the appropriate location in the virtual tree

This is image title

Angular Schematics rule adjusted for support of the Angular CLI workspaces

Once we have this adjustment in place we can try to run our schematics in context of Angular CLI workspace.

The main difference is that we can use ng generate command instead of previously used schematics command!

Boom! Our schematics is useful from the Angular CLI workspace, integration is complete!

Improved help support

The ng generate comes also with additional huge benefit of support for the --help flag which will now correctly print info which we provided in the schema.json file.

This unfortunately doesn’t work when used with basic schematics command!

Angular CLI workspace enables us to run Angular Schematics using ng g command instead of schematics command which is great because it brings full support for the — help flag!

9. Testing schematics

Angular Schematics are very easy to test because of the nature of the virtual Tree representation of the project files. Being able to --dry-run schematics is a perfect fit for the testing.

That way we can always run given schematic as if for real and check if the desired changes really happened in the tree!

In general, the test setup will usually consist of creating of an empty Angular CLI workspace and generating of an app (or lib) on which we then can apply the tested schematic.

This is image title

Excerpt from the Angular Schematics test setup

Then we can add the tests themselves…

This is image title

Simple Angular Schematics test which checks if the file was generated in the correct location…

Check out real world test implementation as a part of the @angular-extensions/model library which comes with built in schematics with full test coverage!

10. Build and publish custom schematics

Until now we have been building our schematics project using npm run build:watch which we added in the beginning.

  1. Let’s stop that process and run npm rum build command instead
  2. Also let’s have a look into package.json file and change the name of the package to @schematics/hello and version to 1.0.0.
  3. Besides that, we have to remove *.ts line from .npmignore file because it would exclude our template from the final package
  4. Now we could run npm publish but let’s run npm pack instead which will give us schematics-hello-1.0.0.tgz file which we can copy to some Angular CLI workspace project.
  5. Then, in the target Angular CLI workspace we can run npm i --no-save schematics-hello-1.0.0-tgz which will install our package into that project.
  6. The last step is to run schematics by referencing package name instead of the path to local schematics project. We can run ng g @schematics/hello:hello Tomas.

✔️ Notice that we don’ have to use --debug=false flag!

Boom that’s it, we have our first working custom Angular Schematics npm package!

11. Implement schematics as a part of library project

Previously we have been focusing on creating schematics as a stand alone npm package. This can be a great approach for a big collection of schematics dedicated for some project similarly to how @schematics/angular is the default standalone collection of Angular CLI.

On the other hand, it can also make sense to have inline smaller collection of schematics as a part of a library itself where it provides stuff specific to that library.

In that case we have to integrate the build of schematics next to the library itself. The exact implementation depends on whether we are implementing our lib as a part of Angular CLI workspace or a custom setup.

It the end it boils down to having separate tsconfig.schematics.json which compiles only the schematics project and a way to copy every other schematics asset to the schematics dist folder.

Assets can be copied using a library like cpx as a part of schematics build in npm scripts, for example something like this "schematics:build:copy": "cpx schematics/**/{collection.json,schema.json,files/**} dist/schematics".

Real-life example of this setup can be seen in this [package.json](https://github.com/angular-extensions/model/blob/master/package.json#L18) file and [tsconfig.schematics.json](https://github.com/angular-extensions/model/blob/master/tsconfig.schematics.json) file.

12. Add ng-add support

The ng-add is this cool thing that enables us to run ng add @angular/material instead of doing npm i @angular/material and then performing additional hundred manual setup steps (I am joking, partially…).

It’s extremely cool because it enables us (or consumers of our library) to get started in the matter of seconds instead of spending tens of minutes following some kind of documentation to be able to setup the library properly.

As always, it depends… Some libs require little to none configuration to get started so they would not benefit much by implementing ng-add support…

Let’s say we have a library with some nontrivial setup. For example we may have implemented websocket based library for our organization and setup includes selection of connected services. Such a setup can be automated using ng-add.

The ng-add is a schematic that is implemented in a same way as any other schematic so all the steps we disused above are still valid! More so, ng-add schematics usually live side by side other supported schematics in the collection.

The only difference between ng-add and other schematics is that they get executed by calling ng add @my-org/schematics instead of ng g @my-org/schematics:ng-add.

The Angular CLI will npm install the mentioned package and execute the ng-add package automatically!

13. Add ng-update support

The ng-update enable us to automate work needed to upgrade to a next version of our library.

Let’s say we have an upcoming major version of our library with a breaking change to API of one of the main services. For example, the service API was 5 arguments but we want to change it to an options object.

Such a “mechanical” change should be possible to automate using some search and replace (or some more advanced AST manipulation).

I haven’t implemented any ng-update schematics yet so the following content is based solely on the reverse engineering of the @angular/material library…

First of all, the ng-update uses a special dedicated ng-update property in the package.json which references new migration.json file.

Add ng-update property to the package.json

The migration.json file then contains all the migration schematics per version…

Example of migration.json file content

Then the content of the index.ts file would follow in line of standard schematics implementation…

Check out real life example of ng-update implementation of @angular/material library.

We’re finally done, this shit was crazy!

I hope you enjoyed this epic ride and will now be able to implement and use tailor made Angular Schematics to make you and your team even more productive!

Thank for reading !

#Angular #Angularjs #JavaScript #Typescript #Frontend

The Complete Guide to Custom Angular Schematics
3 Likes27.40 GEEK