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.
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:
- NgRx - effects, actions, selectors, state, and module files.
- Model - A model file for the collection created
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
.
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/