Total Guide To Custom Angular Schematics

Tomas Trajan
Mar 26, 2019 · 20 min read
Image for post
Image for post
Original 📷 by Sveta Fedarava

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!

Check out video from Angular Meetup Zurich with live coding of custom schematics!

☕☕☕☕☕ 20 minutes read

⚠️ 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!

Feel free to skip straight to the part dealing with the specific topic you are interested in. That way you can fill in the blanks in your pre-existing Angular schematics knowledge!

What are we going to learn?

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


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

Image for post
Image for post
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…

Image for post
Image for post
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.

Image for post
Image for post
Initially generated Angular Schematics factory stub with additional comments for clarity

2. Understand schematics concepts

The schematic factory

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 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

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

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

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

4. Run schematic

So what can we do?

Luckily, the schematics command allows us to specify package containing schematics. The command looks like this schematics <package-name>:<schematic-name> [...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

Image for post
Image for post
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

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

Follow me on Twitter to get notified about the newest Angular blog posts and interesting frontend stuff!🐤

5. Implement simple schematic

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.

Image for post
Image for post

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

Image for post
Image for post
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 😉.

Image for post
Image for post
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.

🤫 Psst! Do you think that NgRx or Redux are overkill for your needs? Looking for something simpler? Check out @angular-extensions/model library!

Image for post
Image for post
Try out @angular-extensions/model library! Check out docs & Github

6. Parametrize schematics with schema and options

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

Image for post
Image for post
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.

Image for post
Image for post
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.

Image for post
Image for post
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.

Image for post
Image for post

And use it in our hello schematics…

Image for post
Image for post

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

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.

Image for post
Image for post

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).

Image for post
Image for post

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

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…

Image for post
Image for post

The Angular Schematics template syntax consists of <%= opening and %> 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.

Image for post
Image for post
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.

Image for post
Image for post
Adjustment we made to our original Angular Schematics rule

Thanks to Alexander K. J. Schmidt for great feedback about the issue he encountered on a mac: “ I had to rm .DS_Store in the directory hello/ sometimes” and also, to make the compiler happy you will have to use second return mergeWith(sourceParametrizedTemplate)(tree, _context) to prevent Typescript compiler from complaining about unused tree variable!

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

Image for post
Image for post

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 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!

Image for post
Image for post
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

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…

Image for post
Image for post
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'

Image for post
Image for post
File generated for previous template and name ‘product code’

Conditional templates

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

Image for post
Image for post
Add transform option to schema.json file
Image for post
Image for post
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…

Image for post
Image for post
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

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

Image for post
Image for post
How to use for loop inside Angular Schematics templates

8. Integrate custom schematics with Angular CLI

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!

Image for post
Image for post
Add project property to our schema.json file
Image for post
Image for post
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
Image for post
Image for post
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!

Image for post
Image for post
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!

Image for post
Image for post
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

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.

Image for post
Image for post
Excerpt from the Angular Schematics test setup

Then we can add the tests themselves…

Image for post
Image for post
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

  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

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 file and tsconfig.schematics.json file.

12. Add ng-add support

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

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.

Image for post
Image for post
Add ng-update property to the package.json

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

Image for post
Image for post
Example of migration.json file content

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

Image for post
Image for post

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!

This is by far the longest and most complicated article I have ever written so your 👏👏👏 would be very appreciated to help it spread to a wider audience 🙏.

Also, don’t hesitate to ping me if you have any questions using the article responses or Twitter DMs @tomastrajan.

And never forget, future is bright

Image for post
Image for post
Obviously the bright future! (📷 by Cyrus Pellet)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store