NX - How to Write a Generator

Angular CLI allows for creating schematics to template code in a command line. nx extends that ability through generators abstracting or overriding Angular and NestJs schematics to support the nx workspace.

In large teams, generators can alleviate developers creating their own patterns. Generators help enforce agreed patterns and practices for developers to follow.

Love Web Development?

Angular, Google Cloud, C#, Node, NestJs?

Goals

1. Benefits of Using a Generator

2. Creating a workspace generator and schema.json

3. The Workspace Tree, File Manipulation - Templates and ts-morph

Benefits of Using a Generator

Enforcing Patterns

Architects opine over code organization and patterns. Teams working within the monorepository shouldn't reinvent or introduce new patterns on which the contributing developer community agreed. When the core architecture exists, generators help enforce these standards creating templates for developers to add their customizations.

Agreeing to a set of standards early will help drive the direction of generators and templatize patterns.

Speed

As these patterns emerge, the generator scaffolds the otherwise mundane tasks of creating a number of files.

For instance, developers complain about the amount of boiler plate code for NgRx. While schematics do exist for NgRx, say the team chose Firebase Firestore as the database. Querying, deleting, and patching different actions to Firebase could be scaffolded with a few variables from the generator.

Or say the team requires a full stack application that includes several NestJs libraries, a NestJs application, a frontend Angular App, and Angular library features. With a combination of generators, the team runs a schematic that generates this code and makes appropriate changes to existing files.

Using a library such as ts-morph utilizes TypeScript's ability to analyze files and make changes to a file. This makes it possible to add modules, components, and service files by querying certain nodes, making updates automated through a generator.

Consistency

Creating several projects in the monorepository for full-stack development leaves itself vulnerable for cross platform issues. In my experience, NestJs application modules crossed accidentally into Angular and vice versa.

nx provides a linting rule that makes use of tagging and the ability to customize boundaries crossing. In the generator, defining these tags up mitigates from discrepancies between the tags.

Creating the Generator

With any project, keep the end in mind. For instance, to support Firebase and NgRx, I'm creating a NgRx Store Module in order to communicate with Firestore. In the current structure, my project includes a data access library, and a common library that houses all my domain models. So my generator needs to generate and update a number of things.

1. Create the boiler plate NgRx Code

2. Update the data access module with the NgRx Firestore collections Module

3. Create a model in the common library

4. Export the model from a barreled file in the common library.

Clarifying each project for your reference, the data access library scoped to Angular for accessing backend services. The common library shares models or utilities for both Angular and Nest Applications.

I'm also operating under the assumption that AngularFire package exists as a dependency. Otherwise, add dependencies via the addDependenciesToPackageJson(tree, dependencies, devDependencies) in the generator. If building an nx-plugin, make sure to add the necessary dependencies.

@angular/fire
The official Angular library for Firebase.. Latest version: 7.2.1, last published: 3 days ago. Start using @angular/fire in your project by running `npm i @angular/fire`. There are 152 other projects in the npm registry using @angular/fire.

To generate the necessary files for the generator run the command:

Generator Input and Running the Generator

The generated files ran in the previous command exist in the tools/generators/ngrx-firebase folder. nx reads the schema.json file for inputs when the generator executes.

Below lists the description of each of the important properties.

Property Name Description
$id This is the name of the generator. You'll use the value from $id to run the generator. e.g. nx workspace-generator firebase-ngrx-store
properties This is a collection of inputs the generator will read and use in the generator code. name, and domain are the properties that will be included as parameters read in by the generator.
name This will be the name of the collection in Firebase for this example. Configured as a string in this instance, the x-prompt value asks for the developers input if they're unaware of the order in which the properties or the names of properties are required for the generator
domain This parameter will allow us to create a grouping folder
required An array of required parameters to run the generator

To better utilize the schema, create an interface that supports the inputs from the schema.json.

The Workspace Tree, Templating Files, and ts-morph

The workspace tree represents the existing files for the workspace. It keeps the in memory changes as files are added, updated, and deleted. Different utilities help check if files exist, retrieve all files in a directory, write to files, and provide all the changes to the tree.

Templating Files

If there are a common set of files that need to be generated at once with a few variances, creating template files eases the pain.

For this use case, there's a number of files to create:

  1. NgRx - effects, actions, selectors, state, and module files.
  2. Model - A model file for the collection created
File for firebase-ngrx-store generator

Make sure these files exist under the files directory as shown. If you want the template files to exist elsewhere, update the tsconfig.tools.json exclude files property so the TypeScript compiler does not compile these files. By default, it globs all files under the files directory for the generator.

Templating Files

Ever write Classic ASP? The templating should look familiar. Like any templating file, variable escaping uses the following pattern <%= variableName %>. Template conditionals and looping logic also exist.

<% for (let i = 0; i < 10; i++) { 
    if (i % 2 === 0) {%>
      Show Even: <%= i %>
<%  }
} %>

WARNING: Eyes may go buggy looking at this templating. Above the template would render Show Even: 0 for all even numbers.

Also templating the folder names are escaped through a double underscore, variable name, and another double underscore.

___variableName__

From the folder structure above, a fileName injects into the template. For instance, the collectionName supplied to the generate was member, the two files above that would be generated with a unique name would be member.model.ts and member-store.module.ts. A unique directory also would be created member-store containing all the files listed underneath.

Injecting variables into the template for this instance is fairly easy. @nrwl/devkit provides helper utilities to transform variable names like className, fileName, and dasherized names utilizing the names method exposed.

generateFiles is another method exposed from @nrwl/devkit. The last parameter of generateFiles injects the properties of the object into the templated files supplied in the second argument joinPathFragments(__dirname, './files/store').

Updating Files

Other files would require manual updates if not done in the generator. First for my instance, a data access module exists in the domain. Second, the model created will be exported via a barreled file in a common project.

For this purpose, the ts-morph package exposes methods to read TypeScript source file and query the syntax tree. Knowing the type of node, ts-morph wraps the difficult updates through helper functions. It also exposes a Project class that works much like Tree.

ts-morph
TypeScript compiler wrapper for static analysis and code manipulation.. Latest version: 13.0.3, last published: 22 days ago. Start using ts-morph in your project by running `npm i ts-morph`. There are 709 other projects in the npm registry using ts-morph.

First, create a helper method around ts-morph for generating a temporary Project and file updates. The parameter for doing these updates is of type Record<string, (sourceFile: SourceFile) => void>. The string will be the path of the file for updates, and the method updates the file. For ease of readability, these export as specific types shown below.

Setting the parameter useInMemoryFileSystem to true on line 19 in ts-morph-helper.ts, keeps changes outside of the file system. For all the changes made in the fileUpdates, those are written to the tree once all changes have been on line 27.

Running the workspace generator, can be done by:

nx workspace-generator firebase-ngrx-store

# The following will dry run and won't make changes
# nx workspace-generator firebase-ngrx-store --dry-run 

This prompts the developer with the questions defined in our schema.json file via the x-prompt value.

Conclusion

Using nx workspace generators saves time, provides developer workspace consistency for tagging, project / code organization.

The schema.json contains the configuration for the inputs, name of the generator.

Templating files is similar to Classic ASP templating using the <% %> escaping variables, or double underscores around a variable name for directory / file names.

For more complex creations that required updates to current files, use ts-morph for ease of adding imports, modules, and exporting files automatically.

ts-morph - https://ts-morph.com/

ast-viewer - https://ts-ast-viewer.com/