Why Run Schematics From Within Schematics?

A schematic acts as a code generator that follows predefined instructions to modify or extend a project. Schematics collections are valuable for generating consistent code, enforcing architecture, and automating repetitive tasks in Angular projects.

There are scenarios where you need to invoke other schematics—either from the same collection or from an external one. Let's examine some common cases.

Common Scenarios

Here are typical situations where calling other schematics becomes necessary:

  1. Reusing boilerplate patterns
    This approach enforces team-wide consistency by building upon existing templates. @maciej_wwojcik covered this topic in detail: Extend Angular Schematics to customize your development process – Angular inDepth
  2. Combining features from other schematics
    Your schematic might need to incorporate functionality already available elsewhere. For instance, building a universal library generator that works with both Angular and NX could leverage their respective library generator schematics.
  3. Splitting instructions for flexibility
    When a single schematic handles multiple tasks, you might want to let users run only specific parts. You can create separate schematics for individual tasks and refactor your main schematic to call them.

Now that we understand the motivation, let's explore how to instruct a schematic to execute another.

Ways to Execute Other Schematics

There are two primary approaches to run a schematic from within the current one:

  1. Create a rule – A Rule is a function that takes a Tree, applies changes, and returns an updated Tree. The schematic's main file, index.ts, defines rules that implement its logic.
  2. Add a task to the context – Each schematic runs within a SchematicContext. Adding tasks here is helpful for operations on the generated tree, such as installing packages or running linters.

Schematics can originate from two sources:

  1. The current collection
  2. An external collection

This gives us four combinations to explore:

  1. Rule to run a schematic from the same collection
  2. Rule to run a schematic from an external collection
  3. Context task to run a schematic from the same collection
  4. Context task to run a schematic from an external collection

Setting Up the Schematics Project

Start by creating a new schematics project called run-schematics:

npm install -g @angular-devkit/schematics-cli
schematics blank --name=run-schematics
cd ./run-schematics

This command generates a collection named run-schematics along with a blank schematic of the same name. The resulting project structure looks like this:

Project structure after initial run

Initial project structure

Now add another schematic to this collection:

schematics blank --name=child-schematic

This adds a schematic named child-schematic.

Let's update child-schematic with new content:

// src/child-schematic/index.ts

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

export function childSchematic(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    _context.logger.info('Hi from child-schematic');
    return tree;
  };
}

Next, build the run-schematics collection:

npm run build

Remember to rebuild the collection after every change to the schematics.

Link the collection for use in other projects:

npm link

At this point, the project directory appears as follows:

Project structure after adding child-schematic

Project structure with child-schematic added

Now move to your Angular project, link the schematics, and run it:

cd ./path/to/angular/project
npm link run-schematics

ng g run-schematics:run-schematics
# Nothing to be done

ng g run-schematics:child-schematic
# Hi from child-schematic
# Nothing to be done

Using Rules

The schematic and externalSchematic functions from @angular-devkit/schematics are used to create rules.

Rule for schematics in the same collection

Modify run-schematics as follows:

// src/run-schematics/index.ts

export function runSchematics(_options: any): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    const rule = schematic("child-schematic", _options);
    return rule;
  };
}

The schematic function returns a Rule and accepts two arguments:

  • schematicName – Name of the schematic to execute
  • options – Options passed to the RuleFactory

Run the schematic to see it in action:

ng g run-schematics:run-schematics
# Hi from child-schematic
# Nothing to be done

Rule for schematics from an external collection

// src/run-schematics/index.ts

export function runSchematics(_options: any): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    const rule1 = schematic("child-schematic", _options);

    const rule2 = externalSchematic(
      "@schematics/angular",
      "component",
      _options
    );

    return chain([rule1, rule2]);
  };
}

The externalSchematic function also returns a Rule but takes three arguments:

  • collectionName – Name of the collection containing the target schematic
  • The remaining two arguments match those of the schematic function

Execute it to verify:

ng g run-schematics:run-schematics
            Hi from child-schematic
? What name would you like to use for the component? hero
CREATE src/app/hero/hero.component.ts (259 bytes)
UPDATE src/app/app.module.ts (738 bytes)

Using Context Tasks

To add tasks within the SchematicContext, use its addTask method, which accepts a TaskConfigurationGenerator.

There are four task classes that implement this interface:

  1. NodePackageInstallTask
  2. NodePackageLinkTask
  3. RepositoryInitializerTask
  4. RunSchematicTask

The RunSchematicTask class is relevant to our needs, with two constructor overloads:

  1. constructor(schemaName: string, options: T) – Runs a schematic from the same collection
  2. constructor(collectionName: string, schemaName: string, options: T) – Runs a schematic from an external collection

Our objective is to create a sub-application and then lint it. Here's the plan:

  1. Build lint-schematic – handles linting for the newly created app
  2. Build lint-caller-schematic – creates the sub-app and invokes lint-schematic via a context task

First, create lint-schematic:

schematics blank --name=lint-schematic

Project structure after adding lint-schematic

Project structure with lint-schematic

Replace its contents with the following:

// src/lint-schematic/index.ts

import { Rule, SchematicContext, Tree } from "@angular-devkit/schematics";
import { execSync } from "child_process";

export function lintSchematic(_options: { name: string }): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    _context.logger.info(`Executing: npm run lint -- --fix ${_options.name}`);
    execSync("npm run lint -- --fix " + _options.name);
  };
}

Next, create lint-caller-schematic:

schematics blank --name=lint-caller-schematic

Project structure after adding lint-caller-schematic

Project structure with lint-caller-schematic

Update its content with the code below:

// src/lint-caller-schematic/index.ts

import {
  externalSchematic,
  Rule,
  SchematicContext,
  Tree,
} from "@angular-devkit/schematics";
import { RunSchematicTask } from "@angular-devkit/schematics/tasks";

export function lintCallerSchematic(_options: any): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    const rule = externalSchematic(
      "@schematics/angular",
      "application",
      _options
    );

    _context.addTask(new RunSchematicTask("lint-schematic", _options));

    return rule;
  };
}

Run the schematic:

ng g run-schematics:lint-caller-schematic --name=sub-app --defaults

This schematic creates a sub-app in your workspace and executes npm run lint --fix sub-app after creation.

To run an external schematic via a context task, use the same RunSchematicTask class but include the collection name as an additional argument:

_context.addTask(new RunSchematicTask("@schematics/angular", "service", _options));

Summary

We've covered several scenarios where calling other schematics proves beneficial. This knowledge will help you build more advanced library schematics.

Here's a quick recap of the four approaches.

Running a schematic from the same collection

  1. Create a rule
rule = schematic(schemaName, options)

2. Add a context task

context.addTask(new RunSchematicTask(schemaName, options))

Running a schematic from an external collection

  1. Create a rule
rule = externalSchematic(collectionName, schemaName, options)

2. Add a context task

context.addTask(new RunSchematicTask(collectionName, schemaName, options))

The complete source for these schematics is available on GitHub.


Acknowledgments

Special thanks to @kasparovairina for creating the banner for this article.