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!
ng-add
supportng-update
supportBefore 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.
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 itscollection.json
file contains entries likecomponent
orservice
. In other words, everything which we can normally generate using Angular CLI.
Our initial collection.json
file will look like this…
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.
Initially generated Angular Schematics factory stub with additional comments for clarity
Our schematic consists of multiple parts playing nicely together. Let’s learn more about these individual parts and their relationship!
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 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…
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.
--dry-run
flag)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.
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
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 andNothing to be done
message is displayed in the console…
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)
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.
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
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 sayname1
, we would encounter followingError: 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 likedtsgenerator
. We can try navigating to./src/hello
(the folder which containsschema.json
) and runningnpx -p dtsgenerator dtsgen schema.json -o schema.d.ts
which will generate our.d.ts
file automatically!
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
forboolean
and selection list for theenum
option types!
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 (seetsconfig.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 adjusttsconfig.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 separatesname
variable from rest of the normal string. Thedasherize
is a helper function which will receive value of thename
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.
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 correcthello-tomas-trajan.ts
file.
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 withtree.create()
method…
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’
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…
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!
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
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!
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!
angular.json
file. The config gives us access to the defaultProject
and the projects
object itself.src/app
or projects/some-app/src/app
in standard Angular CLI workspaces)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 usedschematics command!
Boom! Our schematics is useful from the Angular CLI workspace, integration is complete!
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 theschema.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!
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.
Excerpt from the Angular Schematics test setup
Then we can add the tests themselves…
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!
Until now we have been building our schematics project using npm run build:watch
which we added in the beginning.
npm rum build
command insteadpackage.json
file and change the name of the package to @schematics/hello
and version to 1.0.0
.*.ts
line from .npmignore
file because it would exclude our template from the final packagenpm 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.npm i --no-save schematics-hello-1.0.0-tgz
which will install our package into that project.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!
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.
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!
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.
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