Learn how to create Angular schematics.
What are Schematics?
Schematics are generators that transform an existing filesystem.
With schematics we can:
- Create files,
- Refactor existing files, or
- Move files around.
What can Schematics do?
In general, schematics:
- Add libraries to an Angular project,
- Update libraries in an Angular project, and
- Generate code.
The possibilities for using schematics in your own projects or at your organization is endless. A few examples of how you or your organization can benefit from creating a schematics collection include:
- Generating commonly used UI patterns in an application.
- Generating organization specific components using predefined templates or layouts.
- Enforcing organizational architecture.
Angular Specific?
Currently, yes, schematics are part of the Angular ecosystem.
CLI Integration?
Yes, schematics are tightly integrated with the Angular CLI. You use schematics with the following CLI commands (to name a few):
- ng add
- ng generate
- ng update
What is a Collection?
A collection is a list of schematics. We’ll define the metadata for each schematic in our project’s collection.json file.
Installation
Ok, let’s dive in. 🏄 🏄 First, install the schematics CLI via npm or yarn:
$ npm install -g @angular-devkit/schematics-cli
$ yarn add -g @angular-devkit/schematics-cli
Getting Started
To get started, I would recommend that you check out the sample schematics and read through the code and associated comments. You can generate the sample schematics via:
$ schematics schematic --name demo
Hello World Demo
Check out the repository to follow along as we build a simple Hello World schematic:
Hello World
A good place to start is with a simple “Hello World” schematic. Create a new schematics blank project using the schematics CLI:
$ schematics blank --name=hello-world
$ cd hello-world
Open the src/collection.json file:
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"hello-world": {
"description": "A blank schematic.",
"factory": "./hello-world/index#helloWorld"
}
}
}
Let’s review the collection.json configuration:
- The
$schema
property specifies the schema for validation. - The
schematics
property is an object where each key is thename
of a schematic. - Each schematic has a
description
property that, you guessed it, provides a description of the schematic. - Each schematic also has a
factory
property that is astring
that specifies the file location of our schematic’s entry point, followed by a hash symbol, followed by the function that will be invoked; in this case we’ll invoke thehelloWorld()
function in the hello-world/index.js file.
We can also specify some additional properties for a schematic:
- The optional
aliases
property is anarray
that we can use to specify one or more aliases for our schematic. For example, an alias for the “generate” schematic that ships with the Angular cli is “g”. This enables us to execute the generate command via$ ng g
. - The optional
schema
property can be used to specify a schema for each individual schematic and all of the command line options that are available for the schematic.
It’s also important to note that our project’s package.json file contains a new schematics
property that points to the src/collections.json file:
{
"name": "hello-world",
"version": "0.0.0",
"description": "A blank schematics",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && jasmine src/**/*_spec.js"
},
"keywords": [
"schematics"
],
"author": "",
"license": "MIT",
"schematics": "./src/collection.json",
"dependencies": {
"@angular-devkit/core": "^7.0.2",
"@angular-devkit/schematics": "^7.0.2",
"@types/jasmine": "^2.6.0",
"@types/node": "^8.0.31",
"jasmine": "^2.8.0",
"typescript": "^2.5.2"
}
}
Entry Function
Let’s open the src/hello-world/index.ts file and examine the contents:
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
export function helloWorld(options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
return tree;
};
}
- We export the
helloWorld
function that will be invoked as the “entry function”. - The function accepts an options argument, which is an object of command line argument key/value pairs.
- Our function is a higher order function, meaning that it either accepts or returns a function reference. In this case, our function returns a function that accepts the
Tree
and theSchematicContext
objects.
What is a Tree
?
According to the documenation, a Tree
is:
A staging area for changes, containing the original file system, and a list of changes to apply to it.
A few of things we can use the tree to accomplish include:
read(path: string): Buffer | null;
- read the path specifiedexists(path: string): boolean;
- determine if a path existscreate(path: string, content: Buffer | string): void;
- creates a new file at the specified path with the specified contentbeginUpdate(path: string): UpdateRecorder;
- returns a newUpdateRecorder
instance for the file at the specified pathcommitUpdate(record: UpdateRecorder): void;
- commits the actions of anUpdateRecorder
What is a Rule
?
A Rule
is a function that applies actions to a Tree
given the SchematicContext
:
declare type Rule =
(tree: Tree, context: SchematicContext) =>
Tree | Observable<Tree> | Rule | void;
In the hello world example code above, note that the helloWorld()
“entry” function returns a Rule
.
Build and Execute
Let’s go ahead and build and execute our schematics:
$ yarn build
$ schematics .:hello-world --foo=bar
A few things to note:
- We are executing the schematics using the schematics CLI, not the Angular CLI.
- The first option is to specify the collection name. In this instance we are pointing to the current directory,
.
, as the collection. - The collection name optional. If we omit the collection name the internal collection is used. We used the internal collection previously when we executed the “blank” schematic.
- If we specify a collection name, we use the colon ( : ) to separate the collection name followed by the schematic name.
- In this example we are executing the “hello-world” schematic.
- Finally, we also specify the “foo” option to our schematic, which has the value “bar”.
Let’s start by adding a log statement to the function that we return from the helloWorld
function:
export function helloWorld(options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
console.log(options);
return tree;
};
}
Build and execute the schematic and you should now see the logging of the options that you specify to the schematic:
$ yarn build
$ schematics .:hello-world --foo=bar
{ foo: 'bar' }
Nothing to be done.
Generate a Hello World Template
Our schematic doesn’t no anything right now other than log out the user specified options.
Let’s update our Rule
function to use the tree.create()
method to create a new file:
export function helloWorld(options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
tree.create("hello-world.html", `<h1>Hello ${options.name} 👋</h1>`);
return tree;
};
}
As we learned previously, the create()
method accepts the path
to the file we are creating along with the content
for the file.
In this example, we’re simply using the name
value that the user specified to populate a hello-world.html template with the string “Hello” followed by the name.
When we build and execute our updated schematic we should see:
$ yarn build
$ schematics .:hello-world --name="Angular Community" --dry-run
CREATE /hello-world.html (37 bytes)
Note:
- I specified the
name
option when executing the schematic - I also specified the
dry-run
option to avoid the actual file creation.
Removing the dry-run
option should create the hello-world.html template in your current working directory.
Unit Tests
Angular schematics include a SchematicTestRunner
for building a suite of unit tests to ensure the quality of your schematics collection.
Let’s test our newly created hello-world schematic:
const collectionPath = path.join(__dirname, '../collection.json');
describe('hello-world', () => {
it('works', () => {
const runner = new SchematicTestRunner('schematics', collectionPath);
const tree = runner.runSchematic('hello-world', {}, Tree.empty());
expect(tree.files.length).toEqual(1);
});
});
- The test is executed using Jasmine, which should feel very comfortable to Angular developers.
- We new-up the
SchematicTestRunner
class, specifying thecollectionName
followed by thecollectionPath
. - Using the test runner we invoke
runSchematic()
, specifying theschematicName
, the options object and the sourceTree
. - Finally, we assert that the tree’s file count has incremented to
1
.
Conclusion
I hope this is a start to your learning journey with Angular Schematics.
For some further resources, check out: