* docs(devkit): updated the astexplorer link to point to the tsquery demo. The docs were pointing to the AST Explorer demo but it mentioned using the TSQuery npm package. So I changed the AST Explorer link to the TSQuery Playground. * docs(misc): updated AST playground to TSQuery The docs were linking to the AST Explorer, which is not the npm package referenced in the generator documentation (as AST Explorer does not support parsing TypeScript). * docs(misc): updated AST playground to TSQuery The docs were linking to the AST Explorer, which is not the npm package referenced in the generator documentation (as AST Explorer does not support parsing TypeScript). * docs(misc): updated AST playground to TSQuery The docs were linking to the AST Explorer, which is not the npm package referenced in the generator documentation (as AST Explorer does not support parsing TypeScript).
133 lines
5.3 KiB
Markdown
133 lines
5.3 KiB
Markdown
# 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](/generators/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.
|
|
|
|
```typescript
|
|
import { updateJson } from '@nrwl/devkit';
|
|
|
|
export default async function (tree: Tree, schema: any) {
|
|
updateJson(tree, 'package.json', (pkgJson) => {
|
|
// if scripts is undefined, set it to an empty object
|
|
pkgJson.scripts = pkgJson.scripts ?? {};
|
|
// add greet script
|
|
pkgJson.scripts.greet = 'echo "Hello!"';
|
|
// return modified JSON object
|
|
return pkgJson;
|
|
});
|
|
}
|
|
```
|
|
|
|
## 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.
|
|
|
|
```typescript
|
|
export default async function (tree: Tree, schema: any) {
|
|
const filePath = `path/to/index.ts`;
|
|
const contents = tree.read(filePath);
|
|
contents.replace('thomasEdison', 'nikolaTesla');
|
|
tree.write(filePath, contents);
|
|
}
|
|
```
|
|
|
|
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.)
|
|
|
|
```typescript
|
|
export default async function (tree: Tree, schema: any) {
|
|
const filePath = `path/to/index.ts`;
|
|
const contents = tree.read(filePath);
|
|
contents.replace(/thomasEdison/g, 'nikolaTesla');
|
|
tree.write(filePath, contents);
|
|
}
|
|
```
|
|
|
|
## 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 [TSQuery Playground](https://tsquery-playground.firebaseapp.com/) 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 [TSQuery Playground](https://tsquery-playground.firebaseapp.com/) and paste in a snippet of code that contains the input and desired output of our generator.
|
|
|
|
```typescript
|
|
// input
|
|
const arr: Array<string> = [];
|
|
|
|
// desired output
|
|
const arr: string[] = [];
|
|
```
|
|
|
|
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:
|
|
|
|
```typescript
|
|
{ // TypeReference
|
|
typeName: { // Identifier
|
|
escapedText: "Array"
|
|
},
|
|
typeArguments: [/* this is where the generic type parameter is specified */]
|
|
}
|
|
```
|
|
|
|
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:
|
|
|
|
```typescript
|
|
import { readProjectConfiguration, Tree } from '@nrwl/devkit';
|
|
import { tsquery } from '@phenomnomnominal/tsquery';
|
|
import { TypeReferenceNode } from 'typescript';
|
|
|
|
/**
|
|
* Run the callback on all files inside the specified path
|
|
*/
|
|
function visitAllFiles(
|
|
tree: Tree,
|
|
path: string,
|
|
callback: (filePath: string) => void
|
|
) {
|
|
tree.children(path).forEach((fileName) => {
|
|
const filePath = `${path}/${fileName}`;
|
|
if (!tree.isFile(filePath)) {
|
|
visitAllFiles(tree, filePath, callback);
|
|
} else {
|
|
callback(filePath);
|
|
}
|
|
});
|
|
}
|
|
|
|
export default function (tree: Tree, schema: any) {
|
|
const sourceRoot = readProjectConfiguration(tree, schema.name).sourceRoot;
|
|
visitAllFiles(tree, sourceRoot, (filePath) => {
|
|
const fileEntry = tree.read(filePath);
|
|
const contents = fileEntry.toString();
|
|
|
|
// Check each `TypeReference` node to see if we need to replace it
|
|
const newContents = tsquery.replace(contents, 'TypeReference', (node) => {
|
|
const trNode = node as TypeReferenceNode;
|
|
if (trNode.typeName.getText() === 'Array') {
|
|
const typeArgument = trNode.typeArguments[0];
|
|
return `${typeArgument.getText()}[]`;
|
|
}
|
|
// return undefined does not replace anything
|
|
});
|
|
|
|
// only write the file if something has changed
|
|
if (newContents !== contents) {
|
|
tree.write(filePath, newContents);
|
|
}
|
|
});
|
|
}
|
|
```
|