Nx generator code modifier toolkit for typescript code using tsquery
npm install nx-code-modsThis library is intended to contain _Code Mods_ (AST Modifiers) for use in generators such as:
- Nx monorepo generators
- Ng (Angular) generators
- Any other generator.
The library includes a number of utility functions which greatly simplify the creation of your own _Code Mods_.
_Code Mods_ are commands that can intelligently update your code by inserting or removing code at specific points in existing code and apply formatting so the change looks native to the code base.
- Chainable APIs
- Insert API
- Full example
- Remove API
- Replace API
In addition the toolkit includes experimental support for:
- Auto-naming
- Automated refactoring
``bash`
Test Suites: 36 passed, 36 total
Tests: 188 passed, 188 total
- Chain API
- Insert API
- Remove API
- Replace API
- Transform API
- chainApi(source: string)
Example
`ts
const applyCodeMods = (source) => {
const chain = chainApi(source);
const { insert, remove } = chain;
chain.setDefaults({
classId: 'myClass',
});
insert
.classDecorator({
code: '@Model()',
})
.classMethodDecorator({
code: '@Post()',
methodId: 'myMethod',
});
remove.fromNamedArray({
varId: 'Routes',
remove: {
index: 'end',
},
});
return chain;
};
const codeModsOnFile = async (filePath: string) => {
const source = readFileIfExisting(filePath);
const chain = applyCodeMods(source);
return await chain.saveFile(filePath);
};
`
#### Sample Nx usage
`ts
import { readFileIfExisting } from '@nrwl/workspace/src/core/file-utils';
import { chainApi, saveAndFormatTree } from 'nx-code-mods';
export async function pageGenerator(tree: Tree, options: GeneratorSchema) {
const normalizedOptions = normalizeOptions(tree, options);
const { classId, projectRoot, relTargetFilePath } = normalizedOptions;
// Read source file to modify
const filePath = path.join(projectRoot, relTargetFilePath);
const source = readFileIfExisting(filePath);
// create Chain API
const chain = chainApi(source);
chain.setTree(tree);
const { insert } = chain;
// Apply Code Mods
insert.classDecorator({
code: '@Model()',
classId,
});
await chain.saveFile(filePath);
}
`
#### Chain API: Load JSON structure
Load a JSON structure that defines the Code Mod operations.
`tsimport { Model } from './models'
[
{
api: 'remove': {
ops: [
{
name: 'imports',
def: {
importFileRef: './legacy-models',
},
},
]
},
{
api: 'insert',
ops: [{
name: 'import',
def: {
code: ,`
},
}, {
name: 'classDecorator',
def: {
code: '@Model()',
classId: 'myClass',
},
],
},
];
Usage Example
`ts`
const chain = chainApi(source);
chain.setTee(tree);
chain.loadChainFromFile(chainDefFilePath);
chain.applyStores();
await chain.saveFile(sourceFilePath);
- insertApi(source: string)
Example
`ts
const insert = insertApi(source);
insert.classDecorator({
code: '@Model()',
classId: 'myClass',
});
`
- removeApi(source: string)
Example
`ts
const remove = removeApi(source);
remove.fromNamedArray({
varId: 'Routes',
remove: {
index: 'end',
},
});
`
- replaceApi(source: string)
Example
`ts
const replace = replaceApi(source);
replace.inNamedObject({
varId: 'Routes',
code: { x: 2 },`
replace: {
index: 'end',
},
});
- async transformInTree(tree, opts)transformInFile(filePath, opts)
- transformInSource(filePath, opts)
-
Example
`ts`
const opts = {
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/app-routing.module.ts',
format: true,
transform: (source) => {
const chain = chainApi(source).setDefaultOpts({ classId: 'myClass' });
const { insert, remove } = chain;
insert
.classDecorator({
code: '@Model()',
})
.classMethodDecorator({
code: '@Post()',
methodId: 'myMethod',
});
return chain.source;
},
};
await transformInTree(tree, opts);
The following is a full example for how to use the Code Mods in a typical Nx Generator. It uses the function insertIntoNamedArrayInTree directly.
For generators with more complex requirements involving use of multiple Code Mode it is advisable to use the Chainable APIs or the Transform API.
Note that with the Remove and Replace APIs you can easily build in "undo" generators for your inserts to reverse previous modifications.
`ts
import {
convertNxGenerator,
formatFiles,
generateFiles,
getWorkspaceLayout,
names,
offsetFromRoot,
Tree,
} from '@nrwl/devkit';
import * as path from 'path';
import { NormalizedSchema, GeneratorSchema } from './schema';
import { insertIntoNamedArrayInTree } from 'nx-code-mods';
function normalizeOptions(
tree: Tree,
options: GeneratorSchema
): NormalizedSchema {
const { appsDir, npmScope } = getWorkspaceLayout(tree);
const projectRoot = ${appsDir}/${options.project};
return {
...options,
projectRoot,
prefix: npmScope,
};
}
function addFiles(tree: Tree, options: NormalizedSchema) {
const templateOptions = {
...options,
...names(options.name),
name: names(options.name).fileName,
offsetFromRoot: offsetFromRoot(options.projectRoot),
template: '',
};
const pageDir = options.directory
? path.join(
options.projectRoot,
/src/app/${options.directory}/${names(options.name).fileName}/src/app/${names(options.name).fileName}
)
: path.join(
options.projectRoot,
);
generateFiles(tree, path.join(__dirname, 'files'), pageDir, templateOptions);
}
export async function pageGenerator(tree: Tree, options: GeneratorSchema) {
const normalizedOptions = normalizeOptions(tree, options);
const { importPath, pageNames } = normalizedOptions
// code to be pre-pended to array
const code = {
path: '${pageNames.fileName}',
loadChildren: () =>
import('${importPath}').then((m) => m.${pageNames.classId}PageModule),
};
insertIntoNamedArrayInTree(tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/app-routing.module.ts',
varId: 'Routes',
code,
insert: {
index: 'start'
}
}
);
await formatFiles(tree);
}
export default pageGenerator;
export const pageSchematic = convertNxGenerator(pageGenerator);
`
Appends an import statement to the end of import declarations.
- appendAfterImportsInSourceappendAfterImportsInFile
- appendAfterImportsInTree
-
#### Sample usage
`tsimport { x } from 'x'
const code = ;`
appendAfterImportsInTree(
tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/app-routing.module.ts',
code
}
);
await formatFiles(tree);
Inserts an identifier to import into an existing import declaration
- insertImportInSourceinsertImportInFile
- insertImportInTree
-
#### Sample usage
Implicit import id
`ts`
const code = insertImportInFile(filePath, {
importId: 'x',
importFileRef: './my-file',
});
Explicit import code with import alias
`tsx as xman
const code = ;`
const code = insertImportInFile(filePath, {
code,
importId: 'x',
importFileRef: './my-file',
});
Insert code into a named object
`ts
type CollectionInsert = {
index?: CollectionIndex;
findElement?: FindElementFn;
abortIfFound?: CheckUnderNode;
relative?: BeforeOrAfter;
};
interface InsertObjectOptions {
varId: string;
code: string;
insert?: CollectionInsert;
indexAdj?: number;
}
`
- insertIntoNamedObjectInSourceinsertIntoNamedObjectInFile
- insertIntoNamedObjectInTree
-
Inserts the code in the object named varId.
#### Sample usage
`tsx: 2
insertIntoNamedObjectInTree(tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/route-map.module.ts',
varId: 'RouteMap',
code: ,`
// insert code after this property assignment in the object
insert: {
relative: 'after',
findElement: 'rootRoute'
}
}
);
await formatFiles(tree);
#### Insert object options
Insert at start or end of object properties list
`ts`
insert: {
index: 'start'; // or 'end'
}
Insert before numeric position
`ts`
insert: {
relative: 'before',
index: 1;
}
Insert after specific element
`ts`
insert: {
relative: 'after', // 'before' or 'after' node found via findElement
findElement: (node: Node) => {
// find specific property assignment node
}
}
Insert code into a named array
`ts
type CollectionInsert = {
index?: CollectionIndex;
findElement?: FindElementFn;
abortIfFound?: CheckUnderNode;
relative?: BeforeOrAfter;
};
interface InsertArrayOptions {
varId: string;
code: string;
insert?: CollectionInsert;
indexAdj?: number;
}
`
Insert into src loaded from file
- insertIntoNamedArrayInSourceinsertIntoNamedArrayInFile
- insertIntoNamedArrayInTree
-
Inserts the code in the array named varId.
#### Sample usage
`ts{ x: 2 }
insertIntoNamedArrayInTree(tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/app-routing.module.ts',
varId: 'Routes',
code: ,`
insert: {
index: 'end'
}
}
);
await formatFiles(tree);
#### Insert array options
Insert at start or end of array elements list
`ts`
insert: {
index: 'start'; // or 'end'
}
Insert after numeric position
`ts`
insert: {
relative: 'after',
index: 1;
}
Insert before specific element
`ts`
insert: {
relative: 'after', // 'before' or 'after' node found via findElement
findElement: (node: Node) => {
// find specific array element
}
}
Insert before named identifier
`ts`
insert: {
relative: 'before',
findElement: 'rootRoute'
}
Insert code into a function block
- insertInsideFunctionBlockInSourceinsertInsideFunctionBlockInFile
- insertInsideFunctionBlockInTree
-
#### Sample usage
`ts`
insertInsideFunctionBlockInFile(filePath, {
code,
functionId: 'myFun',
insert: {
index: 'end',
},
});
insert allows for the same positional options as for inserting inside an array.
Add a class method to a class
- insertClassMethodInSourceinsertClassMethodInFile
- insertClassMethodInTree
-
#### Sample usage
`tsmyMethod() {}
insertClassMethodInFile(filePath, {
code: ,`
classId: 'myClass',
methodId: 'myMethod',
});
Add class property to a class
- insertClassPropertyInSourceinsertClassPropertyInFile
- insertClassPropertyInTree
-
#### Sample usage
`tsmyProp: User
insertClassPropertyInFile(filePath, {
code: ,`
classId: 'myClass',
propertyId: 'myProp',
});
Add decorator to a class
- insertClassDecoratorInSourceinsertClassDecoratorInFile
- insertClassDecoratorInTree
-
#### Sample usage
`ts@Model()
insertClassDecoratorInFile(filePath, {
code: ,`
classId: 'myClass',
});
Add class method decorator (such as for NestJS)
- insertClassMethodDecoratorInSourceinsertClassMethodDecoratorInFile
- insertClassMethodDecoratorInTree
-
#### Sample usage
`ts@Post()
const code = insertClassMethodDecoratorInFile(filePath, {
code: ,`
classId: 'myClass',
methodId: 'myMethod',
});
Add parameter decorator to a class method
- insertClassMethodParamDecoratorInSourceinsertClassMethodParamDecoratorInFile
- insertClassMethodParamDecoratorInTree
-
#### Sample usage
`ts@Body() body: string
const code = insertClassMethodParamDecoratorInFile(filePath, {
code: ,`
classId: 'myClass',
methodId: 'myMethod',
});
- removeFromNamedArrayremoveClassDecorator
- removeClassMethod
- removeClassMethodDecorator
- removeClassProperty
- removeClassMethodParams
- removeClassMethodParamDecorator
- removeInsideFunctionBlock
- removeImportId
- removeImport
- removeFromNamedObject
-
- replaceInNamedObjectreplaceInNamedArray
- replaceClassDecorator
- replaceClassMethodDecorator
- replaceClassMethodParams
- replaceClassMethod
- replaceClassMethodDecorator
- replaceClassProperty
- replaceImportIds
- replaceInFunction
-
Auto-naming allows automatic generation of identifiers such as variable and function names from an expression or code block. This is essential for use with automated refactorings.
- blockName(block: Block)conditionName(node: Node)
- expressionName(expr: Expression)
-
Automated refactoring leverages auto-naming to allow for specific code constructs to be refactored into cleaner code constructs.
Currently this library includes experimental support for:
- switch statements => functions and function calls
- if/else statements => functions and function calls
See src/refactor for additional API details:
Extract method from a block of code (using auto-naming)
- extractMethods(srcNode: SourceFile, block: Block)
Refactor if/else statements into named functions and function calls with or (||)
- refactorIfStmtsToFunctions(source: string, opts: RefactorIfStmtOpts)extractIfThenStmtToFunctions(srcNode: SourceFile, stmt: IfStatement, opts: AnyOpts)
- extractIfElseStmtToFunctions(srcNode: any, stmt: IfStatement, opts: AnyOpts)
-
Refactor switch statements into named functions and function calls with or (||)
- extractSwitchStatements(srcNode: SourceFile, block: Block)extractSwitch(srcNode: SourceFile, switchStmt: SwitchStatement)`
-