Bootstrapping our first schematic
Having covered the conceptual groundwork in earlier installments, it's time to get our hands dirty and build something tangible. Before diving in, confirm these packages are installed globally. While local installation is an option in many team settings, we'll opt for global installation to keep our environment consistent throughout this guide.
node v12.8.0
npm v.6.10.2
@angular-cli (core y cli) v.10
@schematics/angular
@schematics/schematics@0.1000.5
prerequisites for following along
With that squared away, let's begin.
Scaffolding an empty schematic
The schematics-cli makes initializing an empty schematic remarkably painless.
Navigate to your preferred working directory and execute the following command:
$ schematics blank --name=indepth-dev-schematic
terminal command for generating an empty schematic
We're essentially calling the schematics tool to scaffold a blank project, supplying the collection name as our sole option.
Looking at the generated directory structure, it's clear this is an npm package. You'll spot a package.json carrying the required dependencies, along with the node_modules directory.
Additionally, there's a tsconfig.json file and a scr folder waiting for our code.
Let's take a closer look at the src folder's contents.
+ src
--- collection.json
--- + indepth-dev-schematic
------ index.ts
------ index_spec.ts
structure of the generated folder
collection.json
This file declares our schematic as the primary entry within the indepth-dev-schematic collection, bearing the same name.
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"indepth-dev-schematic": {
"description": "A blank schematic.",
"factory": "./indepth-dev-schematic/index#indepthDevSchematic"
}
}
}
sample collection.json
Both the schematics-cli and the Angular CLI refer to this specific file whenever they execute a schematic.
Any additional schematics introduced into this package must be registered within this collection file.
index.ts
Serving as the schematic's entry point, this file contains the Rule Factory when initially generated.
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function indepthDevSchematic(_options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
return tree;
};
}
default rule factory entry point
The function adopts a camelized version of the schematic's name as its own. It accepts an options object and returns a Rule. Remember, a Rule is itself a function which takes a tree and a context, ultimately returning a tree.
Key points regarding the entry file:
- It can house a single or multiple rule factories
- A default export is not required
In principle, we could run this as-is, but it would only print `Nothing to be done.` to the console. Let's make it functional by leveraging the create method to generate a readme file.
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function indepthDevSchematic(_options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
tree.create('readme.md', '#This is the read me file');
return tree;
};
}
(method) Tree.create(path: string, content: string | Buffer): void
Invoking your custom schematic via the CLI
Let's return to the terminal and trigger the schematic. Ensure you're in the root directory of your schematic package, where package.json resides.
A crucial step before execution: build your package to transpile the TypeScript into JavaScript. Following a successful compilation, run the command below.
$ schematics .:indepth-dev-schematic
executing a schematic with schematics-cli
Being in the package root, we can skip the collection name and use . followed by a colon : and the schematic's name, which here is `indepth-dev-schematic`. Later on, we'll introduce an alias to make this invocation more concise.
Press enter and observe what happens next.
Dealing with no output from the schematic
Don't worry if nothing seems to change. Schematics operate in a debug mode by default. To apply changes to your filesystem, you'll need to specify the --dry-run=false option.
$ schematics .:indepth-dev-schematic --dry-run=false
running a schematic without dry-run mode
You should now spot the readme.md file on your disk. Nice work!
Supplying options from the command line
Until now, we've hardcoded values for the path and content. To make things more flexible, let's pass these as CLI arguments.
Update the RuleFactory as follows:
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { join } from 'path';
import { capitalize } from '@angular-devkit/core/src/utils/strings';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function indepthDevSchematic(_options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
const name: string = _options.name;
const content: string = _options.content;
const extension: string = _options.extension || '.md';
tree.create(join(name, extension), capitalize(content));
return tree;
};
}
custom schematic generating a markdown file
You can now execute the schematic like this:
$ schematics .:indepth-dev-schematic --name=file --content=hello
custom schematic with options passed from the CLI
Introducing a proper model, moving beyond `any`
Creating an empty schematic initially types options as any since the generator can't predict what's needed. To rectify this, we'll craft a schema model.
Create a schema.ts file alongside your index.ts and fill it with the following:
export interface Schema {
name: string;
content: string;
extension?: string;
}
schema model for typed options
Now, apply this new type to your options parameter:
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { join } from 'path';
import { capitalize } from '@angular-devkit/core/src/utils/strings';
import { Schema } from './schema';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function indepthDevSchematic(_options: Schema): Rule {
return (tree: Tree, _context: SchematicContext) => {
const name: string = _options.name;
const content: string = _options.content;
const extension: string = _options.extension || '.md';
tree.create(join(name, extension), capitalize(content));
return tree;
};
}
custom schematic with explicit type
Implementing a validation schema
As discussed earlier, you can enhance your schematic with a schema.json file, placed next to the entry point. This schema is key for defining default values, marking options as required, enforcing data types, and even setting up interactive prompts.
Populate the schema.json file with this content:
{
"$schema": "http://json-schema.org/schema",
"id": "indepth-dev-schematics",
"title": "A schematic to learn schematics",
"type": "object",
"properties": {
"name": {
"description": "File name, also equivalent to its path",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
}
},
"content": {
"description": "Some content for that file",
"type": "string",
"$default": {
"$source": "argv",
"index": 1
}
},
"extension": {
"description": "An extension for that file. Defaults to markdown",
"type": "string",
"default": ".md"
}
},
"required": [
"name", "content"
]
}
validation schema for a custom schematic
This schema defines three properties within the indepth-dev-schematic id. The name and content fields are positional arguments (indexes 0 and 1) and are mandatory. The third property, extension, holds a default value and isn't strictly required from the user.
This schema takes effect only when the collection explicitly references it. So, update your collection.json file accordingly.
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"indepth-dev-schematic": {
"description": "A blank schematic.",
"factory": "./indepth-dev-schematic/index#indepthDevSchematic",
"schema": "./indepth-dev-schematic/schema.json"
}
}
}
linking schema.json within the collection
Using prompts for user input
Another powerful feature is defining prompts, which create interactive dialogs in the CLI. This greatly enhances usability, sparing developers from sifting through extensive docs to provide the correct input.
Prompts come in three flavors: textual input (string or number), decision (a boolean translating to true/false), and list backed by an enum.
Let's adjust the schema.json to introduce prompts for the mandatory fields.
{
"$schema": "http://json-schema.org/schema",
"id": "indepth-dev-schematics",
"title": "A schematic to learn schematics",
"type": "object",
"properties": {
"name": {
"description": "File name, also equivalent to its path",
"type": "string",
"x-prompt": "What's the file name? (matches path)"
},
"content": {
"description": "Some content for that file",
"type": "string",
"x-prompt": "Enter some content for your file"
},
"extension": {
"description": "An extension for that file. Defaults to markdown",
"type": "string",
"default": ".md"
}
},
"required": [
"name", "content"
]
}
adding prompts to the schema
Creating aliases for convenience
Prior to rebuilding and running, defining a shorter alias is a nice optimization. Typing .:indepth-dev-schematic repeatedly is both tedious and prone to errors.
To add an alias, revisit the collection.json and modify it as shown:
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"indepth-dev-schematic": {
"description": "A blank schematic.",
"factory": "./indepth-dev-schematic/index#indepthDevSchematic",
"schema": "./indepth-dev-schematic/schema.json",
"aliases": ["dive"]
}
}
}
defining aliases within the collection
Keep in mind that thealiases property accepts an array, so you can set multiple aliases if desired.
You can now execute the schematic via the CLI with the following:
$ schematics .:dive
It will present prompts for the name and content. The default extension, .md, will be applied automatically.
Executing the schematic from an Angular project
Running this from schematics-cli is fine, but the real goal is to use it inside an Angular application.
First, create a global link for our package using:
$ npm link
run at the package root.
Next, scaffold a new Angular project via the CLI. Once created, execute the command below from the project root to link the schematic package:
$ npm link indepth-dev-schematic
command run in app root to symlink the package
Before running, let's enhance the entry file with some checks.
import { Rule, SchematicContext, Tree, SchematicsException } from '@angular-devkit/schematics';
import { join } from 'path';
import { capitalize } from '@angular-devkit/core/src/utils/strings';
import { Schema } from './schema';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function indepthDevSchematic(_options: Schema): Rule {
return (tree: Tree, _context: SchematicContext) => {
const name: string = _options.name;
const content: string = _options.content;
const extension: string = _options.extension || '.md';
const path = join(name, extension);
const angularConfig = 'angular.json';
// Let's make sure we're in an angular workspace
if (!tree.exists(angularConfig)) {
throw new SchematicsException('???This is not an Angular worksapce! Try again in an Angular project.');
} else {
if (!tree.exists(path)) {
tree.create(path, capitalize(content));
} else {
throw new SchematicsException('???That file already exists! Try a new name');
}
}
return tree;
};
}
using SchematicException for error handling
These additions ensure two things: the schematic operates from an Angular workspace, and the target file doesn't already exist.
After rebuilding the schematic, execute it from the Angular app directory:
$ ng generate indepth-dev-schematic:dive
invoking the schematic from an angular app
Wrap-up (for now!)
Clearly, this is a basic demonstration. To tackle more compelling scenarios, we need a clear set of objectives. In the next article, we'll identify a practical, real-world problem and build a schematic to solve it.
Until next time.
