Better Code Generation in Angular CLI workspaces with Nx Devkit
Tags: angular, nx, schematics, generators
Published: 2021-05-24 16:31:34

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

```sh
npx @angular/cli@latest new nx-devkit-schematics
```

Next, generate a library within the workspace.

```sh
ng g my-lib
```

### Adding Nx Devkit

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

```sh
npm install @nrwl/devkit --save-dev
```

OR

```sh
yarn add @nrwl/devkit --dev
```

### 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.

```json
{
 "$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"
   }
 }
}
```

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.

```json
{
 "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"
}
```

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

```json
{
 "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/**/*"
 ]
}
```

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.

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

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.

```ts
import { Tree, convertNxGenerator } from '@nrwl/devkit';
 
export function mySchematicGenerator(_tree: Tree, opts: any) {
 console.log('options', opts);
}
```

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.

```ts 
export const mySchematic = convertNxGenerator(mySchematicGenerator);
```

This generator now functions the same as an Angular Schematic.

## Building and Running the schematic

To build the schematic, first build the library

```sh
ng build my-lib
```

Then run the build script from the my-lib directory

```sh
npm run build --prefix projects/my-lib
```

Next, run the schematic from the command line

```sh
ng g ./dist/my-lib/schematics/collection.json:my-schematic
```

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

```
ng g my-lib:my-schematic
```

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.

```json
{
 "$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"
 ]
}
```

Next, add a TypeScript interface for the JSON schema.

```ts
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;
}
```

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.

```ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
 
@Injectable({
  providedIn: 'root'
})
export class <%= className %>Service {
  constructor(private http: HttpClient) { }
}
```

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.

```ts
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);
```

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

```sh
ng g ./dist/my-lib/schematics/collection.json:my-schematic my-data
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k35aue5jhqb3xvl0sko6.gif)

---

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](https://github.com/nrwl/nx-devkit-schematics), and more documentation about the Nx Devkit can be found in the [Nx Devkit guide](https://nx.dev/latest/core-concepts/nx-devkit).