Table of Contents
- What Are Angular Schematics?
- Why Build Custom Schematics?
- Setting Up Your Schematic Project
- Understanding Schematic Structure
- Creating Your First Custom Schematic
- Testing the Schematic
- Integrating the Schematic into Your Workflow
- Advanced Schematic Concepts
- Conclusion
- References
What Are Angular Schematics?
Angular Schematics are part of the Angular DevKit, a set of libraries for building Angular tools. At their core, schematics are code generators and transformers that operate on a virtual file system (the Tree). They can:
- Generate new files (e.g., components, modules).
- Modify existing files (e.g., update imports, add routes).
- Enforce project conventions (e.g., naming standards, folder structures).
Key Concepts:
- Tree: A read-only virtual file system that tracks changes (additions, deletions, modifications).
- Rule: A function that takes a
Treeand returns a newTreewith changes. Schematics are composed of rules. - Source: A provider of files (e.g., local templates, external schematics).
- SchematicContext: Contains metadata like the schematic collection, logger, and options.
Why Custom Schematics?
While Angular CLI provides built-in schematics (e.g., component, module), custom schematics offer team-specific automation:
- Consistency: Enforce project standards (e.g., “all feature modules must include a service and routing”).
- Time Savings: Eliminate manual steps (e.g., generating a feature module, then adding a component, service, and tests).
- Scalability: Adapt to your project’s unique architecture (e.g., Nx monorepos, micro-frontends).
- Integration: Compose with existing schematics to extend functionality (e.g., “run the Angular module schematic, then add custom logging”).
Setting Up Your Schematic Project
To build a custom schematic, you’ll need the Angular DevKit Schematics CLI. Let’s set up a project:
Step 1: Install Dependencies
First, install the schematics CLI globally:
npm install -g @angular-devkit/schematics-cli
Step 2: Create a New Schematic
Generate a blank schematic project (replace my-schematics with your project name):
schematics blank --name=my-schematics
cd my-schematics
Step 3: Project Structure
Your project will look like this:
my-schematics/
├── src/
│ └── my-schematics/ # Schematic logic
│ ├── files/ # Template files (optional)
│ ├── index.ts # Main schematic logic
│ └── schema.json # Defines schematic options
├── collection.json # Schematic collection config
├── package.json # Dependencies and scripts
└── tsconfig.json # TypeScript config
Step 4: Install Dependencies
Install required packages for development:
npm install @angular-devkit/core @angular-devkit/schematics typescript @types/node @types/jasmine --save-dev
Understanding Schematic Structure
Let’s break down the key files in your schematic:
1. collection.json
Defines your schematic collection (a group of schematics). It maps schematic names to their implementations:
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"my-schematics": { # Schematic name (used in `ng generate my-schematics:my-schematics`)
"description": "A blank schematic.",
"factory": "./my-schematics/index#mySchematics", # Path to the schematic function
"schema": "./my-schematics/schema.json" # Path to options schema
}
}
}
2. schema.json
Defines the options users can pass to your schematic (e.g., --name=user). Example:
{
"$schema": "http://json-schema.org/schema",
"id": "MySchematic",
"title": "My Schematic",
"type": "object",
"properties": {
"name": { # Option: --name
"type": "string",
"description": "The name of the feature."
},
"path": { # Option: --path (where to generate files)
"type": "string",
"default": "src/app",
"description": "The path to generate the feature."
}
},
"required": ["name"] # `name` is mandatory
}
3. index.ts
The main logic file. It exports a function that returns a Rule (the transformation to apply). Example:
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
export function mySchematics(_options: any): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.info(`Generating feature: ${_options.name}`);
// Add logic here (e.g., generate files, modify existing ones)
return tree;
};
}
4. files/ Directory (Optional)
Contains template files (e.g., .ts, .html) used to generate code. Templates use EJS-like syntax (e.g., <%= name %> for variables).
Creating Your First Custom Schematic
Let’s build a practical schematic: feature—generates a feature module with:
- A feature module (
user.module.ts). - A component (
user.component.ts). - A service (
user.service.ts). - Optional routing.
Step 1: Update collection.json
Add the feature schematic to collection.json:
{
"schematics": {
"feature": { # New schematic name: `ng generate my-schematics:feature`
"description": "Generates a feature module with component, service, and routing.",
"factory": "./feature/index#feature",
"schema": "./feature/schema.json"
}
}
}
Step 2: Create the feature Schematic
Create a src/feature directory with schema.json, index.ts, and files/:
mkdir -p src/feature/files
touch src/feature/schema.json src/feature/index.ts
Step 3: Define Options in schema.json
Update src/feature/schema.json to accept options:
{
"$schema": "http://json-schema.org/schema",
"id": "FeatureSchema",
"title": "Feature Options",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the feature (e.g., 'user').",
"$default": { "$source": "argv", "index": 0 } # Default to first CLI argument
},
"path": {
"type": "string",
"default": "src/app/features",
"description": "Path to generate the feature."
},
"hasRouting": {
"type": "boolean",
"default": true,
"description": "Whether to include routing."
}
},
"required": ["name"]
}
Step 4: Write the Schematic Logic (index.ts)
Use Angular’s built-in module schematic to generate the module, then add custom files. Update src/feature/index.ts:
import { Rule, SchematicContext, Tree, apply, url, template, move, chain, externalSchematic } from '@angular-devkit/schematics';
import { strings } from '@angular-devkit/core';
// Interface for options (matches schema.json)
interface FeatureOptions {
name: string;
path: string;
hasRouting: boolean;
}
export function feature(options: FeatureOptions): Rule {
const { name, path, hasRouting } = options;
const dasherizedName = strings.dasherize(name); // Convert "UserProfile" to "user-profile"
const classifiedName = strings.classify(name); // Convert "user-profile" to "UserProfile"
// Rule 1: Generate Angular module (using built-in `module` schematic
const moduleRule = externalSchematic('@schematics/angular', 'module', {
name: dasherizedName,
path,
routing: hasRouting,
flat: false, // Create a subdirectory for the module
});
// Rule 2: Generate component, service, and other files from templates
const templateRule = apply(url('./files'), [
// Replace template variables (e.g., <%= name %>) with options
template({
...strings, // Provide string utilities (dasherize, classify, etc.)
name: dasherizedName,
classifiedName,
}),
// Move files to the target path (e.g., src/app/features/user)
move(`${path}/${dasherizedName}`),
]);
// Run both rules sequentially
return chain([moduleRule, templateRule]);
}
Step 5: Create Template Files
Add templates to src/feature/files/ to generate the component and service.
__name@dasherize__.component.ts.template
import { Component } from '@angular/core';
@Component({
selector: 'app-<%= name %>',
template: `
<h2><%= classifiedName %> Component</h2>
`,
})
export class <%= classifiedName %>Component {}
__name@dasherize__.service.ts.template
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class <%= classifiedName %>Service {
constructor() {
console.log('<%= classifiedName %>Service initialized');
}
}
Step 6: Build the Schematic
Compile TypeScript to JavaScript:
npm run build # Runs `tsc` (defined in package.json)
Testing the Schematic
To test locally:
Step 1: Link the Schematic
Link your schematic to npm so it’s available globally:
npm link
Step 2: Generate the Feature in an Angular App
Create a test Angular app and use your schematic:
ng new test-app
cd test-app
npm link my-schematics # Link to your schematic project
# Generate the feature (e.g., name=user, path=src/app/features)
ng generate my-schematics:feature --name=user --path=src/app/features
Expected Output
You’ll see:
src/app/features/user/
├── user.module.ts
├── user.component.ts
├── user.service.ts
└── user-routing.module.ts (if --hasRouting=true)
Integrating the Schematic into Your Workflow
Option 1: Local Development
Use npm link to test schematics locally (as shown above).
Option 2: Publish to npm
Share with your team by publishing to npm:
npm publish # Ensure you’re logged in with `npm login`
Then install in projects:
npm install my-schematics --save-dev
ng generate my-schematics:feature --name=user
Option 3: Integrate with Angular CLI
Add your schematic collection to angular.json for easier access:
{
"cli": {
"schematicCollections": ["my-schematics", "@schematics/angular"]
}
}
Now you can run:
ng generate feature --name=user # No need for `my-schematics:` prefix
Advanced Schematic Concepts
Composing Schematics
Use chain() to run multiple rules (e.g., generate a module, then add a route to app-routing.module.ts):
import { chain, Rule } from '@angular-devkit/schematics';
function addRouteToAppRouting(options: any): Rule {
return (tree: Tree) => {
// Modify src/app/app-routing.module.ts to add a route
const routeFile = tree.read('src/app/app-routing.module.ts')!.toString();
const newRoute = `{ path: '${options.name}', loadChildren: () => import('./features/${options.name}/${options.name}.module').then(m => m.${options.classifiedName}Module) },`;
const updatedContent = routeFile.replace('const routes: Routes = [', `const routes: Routes = [\n ${newRoute}`);
tree.overwrite('src/app/app-routing.module.ts', updatedContent);
return tree;
};
}
export function feature(options: any): Rule {
return chain([moduleRule, templateRule, addRouteToAppRouting(options)]);
}
Validating Options
Use schema.json to enforce validation (e.g., name must be lowercase):
{
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z-]+$", # Enforce lowercase with dashes
"description": "Name must be lowercase with dashes (e.g., 'user-profile')."
}
}
}
Migrating Code
Use schematics to automate code updates (e.g., replace deprecated APIs):
import { visitTemplateFiles } from '@angular-devkit/schematics';
export function migrate(): Rule {
return visitTemplateFiles((content, path) => {
// Replace `oldApi()` with `newApi()` in all .ts files
return content.replace(/oldApi\(\)/g, 'newApi()');
});
}
Conclusion
Custom Angular Schematics are a game-changer for automating repetitive tasks and enforcing consistency. By composing existing schematics and adding custom templates, you can tailor your workflow to your project’s needs. Start small (e.g., a feature generator) and expand to migrations or complex scaffolding—your team will thank you!