Modifying Files with a Generator
Modifying existing files is an order of magnitude harder than creating new files, so care should be taken when trying to automate this process. When the situation merits it, automating a process can lead to tremendous benefits across the organization. Here are some approaches listed from simplest to most complex.
Compose Existing Generators
If you can compose together existing generators to modify the files you need, you should take that approach. See Composing Generators for more information.
Modify JSON Files
JSON files are fairly simple to modify, given their predictable structure.
The following example adds a package.json
script that issues a friendly greeting.
1import { updateJson } from '@nrwl/devkit';
2
3export default async function (tree: Tree, schema: any) {
4 updateJson(tree, 'package.json', (pkgJson) => {
5 // if scripts is undefined, set it to an empty object
6 pkgJson.scripts = pkgJson.scripts ?? {};
7 // add greet script
8 pkgJson.scripts.greet = 'echo "Hello!"';
9 // return modified JSON object
10 return pkgJson;
11 });
12}
13
String Replace
For files that are not as predictable as JSON files (like .ts
, .md
or .css
files), modifying the contents can get tricky. One approach is to do a find and replace on the string contents of the file.
Let's say we want to replace any instance of thomasEdison
with nikolaTesla
in the index.ts
file.
1export default async function (tree: Tree, schema: any) {
2 const filePath = `path/to/index.ts`;
3 const contents = tree.read(filePath);
4 contents.replace('thomasEdison', 'nikolaTesla');
5 tree.write(filePath, contents);
6}
7
This works, but only replaces the first instance of thomasEdison
. To replace them all, you need to use regular expressions. (Regular expressions also give you a lot more flexibility in how you search for a string.)
1export default async function (tree: Tree, schema: any) {
2 const filePath = `path/to/index.ts`;
3 const contents = tree.read(filePath);
4 contents.replace(/thomasEdison/g, 'nikolaTesla');
5 tree.write(filePath, contents);
6}
7
AST Manipulation
ASTs (Abstract Syntax Trees) allow you to understand exactly the code you're modifying. Replacing a string value can accidentally modify text found in a comment rather than changing the name of a variable.
We'll write a generator that replaces all instances of the type Array<something>
with something[]
. To help accomplish this, we'll use the @phenomnomnominal/tsquery
npm package and the AST Explorer site. TSQuery allows you to query and modify ASTs with a syntax similar to CSS selectors. The AST Explorer tool allows you to easily examine the AST for a given snippet of code.
First, go to AST Explorer and paste in a snippet of code that contains the input and desired output of our generator.
1// input
2const arr: Array<string> = [];
3
4// desired output
5const arr: string[] = [];
6
Make sure the parser is set to typescript
. When you place the cursor on the Array
text, the right hand panel highlights the corresponding node of the AST. The AST node we're looking for looks like this:
1{ // TypeReference
2 typeName: { // Identifier
3 escapedText: "Array"
4 },
5 typeArguments: [/* this is where the generic type parameter is specified */]
6}
7
Second, we need to choose a selector to target this node. Just like with CSS selectors, there is an art to choosing a selector that is specific enough to target the correct nodes, but not overly tied to a certain structure. For our simple example, we can use TypeReference
to select the parent node and check to see if it has a typeName
of Array
before we perform the replacement. We'll then use the typeArguments
to get the text inside the <>
characters.
The finished code looks like this:
1import { readProjectConfiguration, Tree } from '@nrwl/devkit';
2import { tsquery } from '@phenomnomnominal/tsquery';
3import { TypeReferenceNode } from 'typescript';
4
5/**
6 * Run the callback on all files inside the specified path
7 */
8function visitAllFiles(
9 tree: Tree,
10 path: string,
11 callback: (filePath: string) => void
12) {
13 tree.children(path).forEach((fileName) => {
14 const filePath = `${path}/${fileName}`;
15 if (!tree.isFile(filePath)) {
16 visitAllFiles(tree, filePath, callback);
17 } else {
18 callback(filePath);
19 }
20 });
21}
22
23export default function (tree: Tree, schema: any) {
24 const sourceRoot = readProjectConfiguration(tree, schema.name).sourceRoot;
25 visitAllFiles(tree, sourceRoot, (filePath) => {
26 const fileEntry = tree.read(filePath);
27 const contents = fileEntry.toString();
28
29 // Check each `TypeReference` node to see if we need to replace it
30 const newContents = tsquery.replace(contents, 'TypeReference', (node) => {
31 const trNode = node as TypeReferenceNode;
32 if (trNode.typeName.getText() === 'Array') {
33 const typeArgument = trNode.typeArguments[0];
34 return `${typeArgument.getText()}[]`;
35 }
36 // return undefined does not replace anything
37 });
38
39 // only write the file if something has changed
40 if (newContents !== contents) {
41 tree.write(filePath, newContents);
42 }
43 });
44}
45