DEV Community

Cover image for Better Code Generation in Angular CLI workspaces with Nx Devkit
Brandon Roberts for Nx

Posted on • Originally published at blog.nrwl.io

Better Code Generation in Angular CLI workspaces with Nx Devkit

Schematics in Angular provide a way to automate common tasks, such as generating files, reading, and updating configuration files, and modifying your codebase. Angular Schematics are built using the Angular Devkit, which consists of many lower-level APIs, along with a few concepts to learn such as the Virtual Filesystem, Trees, Rules, Actions, Context and more. Learning these concepts and APIs can be challenging for developers looking to write schematics for Angular applications and libraries.

The Nx Devkit is an underlying part of the Nx build framework that provides utilities for building generators and executors. The Nx Devkit provides a way to build generators with a more straightforward approach using only JavaScript language primitives and immutable objects (the tree being the only exception). Combined with many higher-level utility functions, generators can be built with a few lines of code. Generators are similar to schematics in that they automate making file changes.

You may think that the Nx Devkit is only for Nx workspaces, but the Nx Devkit can also be used in a regular Angular CLI workspace. This guide walks you through adding the Nx Devkit to your Angular CLI workspace, using it to build an Nx Generator, and converting it to an Angular Schematic.

Creating an Angular CLI workspace

For this example, you’ll create a new workspace, and add a library.

First, create the Angular CLI workspace

npx @angular/cli@latest new nx-devkit-schematics
Enter fullscreen mode Exit fullscreen mode

Next, generate a library within the workspace.

ng g my-lib
Enter fullscreen mode Exit fullscreen mode

Adding Nx Devkit

To add the Nx Devkit, install the @nrwl/devkit package as a dev dependency,

npm install @nrwl/devkit --save-dev
Enter fullscreen mode Exit fullscreen mode

OR

yarn add @nrwl/devkit --dev
Enter fullscreen mode Exit fullscreen mode

Setting up the metadata and build

To create a generator, follow the same process as creating a schematic. Create a my-lib/schematics folder and define a collection.json with an entry for my-schematic. This metadata is how Nx Generators provide data on how they are to be processed.

{
 "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
 "schematics": {
   "my-schematic": {
     "description": "Generate a service in the project.",
     "factory": "./my-schematic/index#mySchematic",
     "schema": "./my-schematic/schema.json"
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

Add a schematics property to the package.json with a path to the collection, and build steps for compiling the TypeScript and copying assets to the dist directory.

{
 "name": "my-lib",
 "version": "0.0.1",
 "scripts": {
   "prebuild": "../../node_modules/.bin/rimraf dist/my-lib && mkdir -p ../../dist/my-lib/schematics && cp -R schematics/ ../../dist/my-lib/schematics/",
   "build": "../../node_modules/.bin/tsc -p tsconfig.schematics.json",
   "postbuild": "../../node_modules/.bin/rimraf --glob ../../dist/my-lib/schematics/**/*.ts"
 },
 "peerDependencies": {
   "@angular/common": "^12.0.0",
   "@angular/core": "^12.0.0"
 },
 "schematics": "./schematics/collection.json"
}
Enter fullscreen mode Exit fullscreen mode

Also define a tsconfig.schematics.json in the my-lib folder for compiling the generators.

{
 "compilerOptions": {
   "baseUrl": ".",
   "lib": [
     "es2018",
     "dom"
   ],
   "declaration": true,
   "module": "commonjs",
   "moduleResolution": "node",
   "noEmitOnError": true,
   "noFallthroughCasesInSwitch": true,
   "noImplicitAny": true,
   "noImplicitThis": true,
   "noUnusedParameters": true,
   "noUnusedLocals": true,
   "rootDir": "schematics",
   "outDir": "../../dist/my-lib/schematics",
   "skipDefaultLibCheck": true,
   "skipLibCheck": true,
   "sourceMap": true,
   "strictNullChecks": true,
   "target": "es6",
   "types": [
     "node"
   ]
 },
 "include": [
   "schematics/**/*"
 ],
 "exclude": [
   "schematics/*/files/**/*"
 ]
}
Enter fullscreen mode Exit fullscreen mode

The scripts in the package.json and configuration in the tsconfig.schematics.json file are relative to the library, and are based on the library name and path.

Creating a simple generator

To create a generator, first define a schema. This has some information about the name of the generator, and properties for accepting arguments.

Create a schema.json in the projects/my-lib/schematics/my-schematic folder.

{
 "$schema": "http://json-schema.org/schema",
 "id": "SchematicsMySchematic",
 "title": "My Schematic Schema",
 "type": "object",
 "properties": {
  },
 "required": []
}
Enter fullscreen mode Exit fullscreen mode

This schematic doesn’t define any properties or have any required fields.

Next, define the index.ts file inside the my-schematic folder with a very simple generator that logs out the provided options. A generator is just a function, with some metadata to define its inputs to produce outputs.

import { Tree, convertNxGenerator } from '@nrwl/devkit';

export function mySchematicGenerator(_tree: Tree, opts: any) {
 console.log('options', opts);
}
Enter fullscreen mode Exit fullscreen mode

The Tree represents the virtual filesystem that gives you the same access to stage changes before actually applying them. Nx generator functions can also handle side effects, such as installing packages, formatting files, and more.

To convert this to an Angular Schematic, use the convertNxGenerator utility function.

export const mySchematic = convertNxGenerator(mySchematicGenerator);
Enter fullscreen mode Exit fullscreen mode

This generator now functions the same as an Angular Schematic.

Building and Running the schematic

To build the schematic, first build the library

ng build my-lib
Enter fullscreen mode Exit fullscreen mode

Then run the build script from the my-lib directory

npm run build --prefix projects/my-lib
Enter fullscreen mode Exit fullscreen mode

Next, run the schematic from the command line

ng g ./dist/my-lib/schematics/collection.json:my-schematic
Enter fullscreen mode Exit fullscreen mode

If you were to publish this library, it would work just the same as any other schematic.

ng g my-lib:my-schematic
Enter fullscreen mode Exit fullscreen mode

You’ll see the options logged to the console when running the schematic.

Generating files

Another common task when using schematics is to generate files using templates. This can also be with a generator and a utility function provided by the Nx Devkit.

Let’s update the schema for the schematic to accept some options, such as the service name, the path to create the service, and the project.

{
 "$schema": "http://json-schema.org/schema",
 "id": "SchematicsMySchematic",
 "title": "My Schematic Schema",
 "type": "object",
 "properties": {
   "name": {
     "type": "string",
     "description": "Service name",
     "$default": {
       "$source": "argv",
       "index": 0
     }
   },
   "path": {
     "type": "string",
     "format": "path",
     "description": "The path to create the service.",
     "visible": false
   },
   "project": {
     "type": "string",
     "description": "The name of the project.",
     "$default": {
       "$source": "projectName"
     }
   }
 },
 "required": [
   "name"
 ]
}
Enter fullscreen mode Exit fullscreen mode

Next, add a TypeScript interface for the JSON schema.

export interface Schema {
 // The name of the service.
 name: string;

 // The path to create the service.
 path?: string;

 // The name of the project.
 project?: string;
}
Enter fullscreen mode Exit fullscreen mode

The name, path, and project are options the schematic accepts for the data service file.

Define a template for the generated file service named __name__.service__tpl__ in the projects/my-lib/schematics/my-schematic/files directory. The __tpl__ denotes that the file is a template, and prevents the TypeScript code from being parsed. The double underscores also denote that these are template variables that are replaced when the file is generated.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class <%= className %>Service {
  constructor(private http: HttpClient) { }
}
Enter fullscreen mode Exit fullscreen mode

Update the generator to read the angular.json file, build a path to where the files are placed, and scaffold the files using the generateFiles function to replace the template placeholder variables.

import { Tree, convertNxGenerator, generateFiles, joinPathFragments, names, readJson } from '@nrwl/devkit';

export function mySchematicGenerator(tree: Tree, opts: any) {
 const workspace = readJson(tree, 'angular.json');

 if (!opts.project) {
   opts.project = workspace.defaultProject;
 }

 const project = workspace.projects[opts.project];
 const projectType = project.projectType === 'application' ? 'app' : 'lib';

 if (opts.path === undefined) {
   opts.path = `${project.sourceRoot}/${projectType}`;
 }

 const { className, fileName } = names(opts.name);

 generateFiles(tree, joinPathFragments(__dirname, './files'), joinPathFragments(opts.path), {
   className,
   name: fileName,
   tpl: ''
 });
}

export const mySchematic = convertNxGenerator(mySchematicGenerator);
Enter fullscreen mode Exit fullscreen mode

After building the schematics again, run the schematic to generate a service.

ng g ./dist/my-lib/schematics/collection.json:my-schematic my-data
Enter fullscreen mode Exit fullscreen mode

Alt Text


The Nx Devkit contains all the functionality you need for writing Angular Schematics with a cleaner, high-level API. The Nx Devkit also has additional utilities for reading workspace and project configuration files, doing string replacements, and working with ASTs. By using language primitives, it makes authoringAngular Schematics easier to write, debug, and maintain.

See the full example here, and more documentation about the Nx Devkit can be found in the Nx Devkit guide.

Top comments (0)