This library is intended to contain Code Mods (AST Modifiers) for use in generators such as:
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.
In addition the toolkit includes experimental support for:
Test Suites: 36 passed, 36 total
Tests: 188 passed, 188 total
chainApi(source: string)
Example
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);
};
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);
}
Load a JSON structure that defines the Code Mod operations.
[
{
api: 'remove': {
ops: [
{
name: 'imports',
def: {
importFileRef: './legacy-models',
},
},
]
},
{
api: 'insert',
ops: [{
name: 'import',
def: {
code: `import { Model } from './models'`,
},
}, {
name: 'classDecorator',
def: {
code: '@Model()',
classId: 'myClass',
},
],
},
];
Usage Example
const chain = chainApi(source);
chain.setTee(tree);
chain.loadChainFromFile(chainDefFilePath);
chain.applyStores();
await chain.saveFile(sourceFilePath);
insertApi(source: string)
Example
const insert = insertApi(source);
insert.classDecorator({
code: '@Model()',
classId: 'myClass',
});
removeApi(source: string)
Example
const remove = removeApi(source);
remove.fromNamedArray({
varId: 'Routes',
remove: {
index: 'end',
},
});
replaceApi(source: string)
Example
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
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.
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}`
)
: path.join(
options.projectRoot,
`/src/app/${names(options.name).fileName}`
);
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.
appendAfterImportsInSource
appendAfterImportsInFile
appendAfterImportsInTree
const code = `import { x } from 'x'`;
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
insertImportInSource
insertImportInFile
insertImportInTree
Implicit import id
const code = insertImportInFile(filePath, {
importId: 'x',
importFileRef: './my-file',
});
Explicit import code with import alias
const code = `x as xman`;
const code = insertImportInFile(filePath, {
code,
importId: 'x',
importFileRef: './my-file',
});
Insert code into a named object
type CollectionInsert = {
index?: CollectionIndex;
findElement?: FindElementFn;
abortIfFound?: CheckUnderNode;
relative?: BeforeOrAfter;
};
interface InsertObjectOptions {
varId: string;
code: string;
insert?: CollectionInsert;
indexAdj?: number;
}
insertIntoNamedObjectInSource
insertIntoNamedObjectInFile
insertIntoNamedObjectInTree
Inserts the code
in the object named varId
.
insertIntoNamedObjectInTree(tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/route-map.module.ts',
varId: 'RouteMap',
code: `x: 2`,
// insert code after this property assignment in the object
insert: {
relative: 'after',
findElement: 'rootRoute'
}
}
);
await formatFiles(tree);
Insert at start or end of object properties list
insert: {
index: 'start'; // or 'end'
}
Insert before
numeric position
insert: {
relative: 'before',
index: 1;
}
Insert after
specific element
insert: {
relative: 'after', // 'before' or 'after' node found via findElement
findElement: (node: Node) => {
// find specific property assignment node
}
}
Insert code into a named array
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
insertIntoNamedArrayInSource
insertIntoNamedArrayInFile
insertIntoNamedArrayInTree
Inserts the code
in the array named varId
.
insertIntoNamedArrayInTree(tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/app-routing.module.ts',
varId: 'Routes',
code: `{ x: 2 }`,
insert: {
index: 'end'
}
}
);
await formatFiles(tree);
Insert at start
or end
of array elements list
insert: {
index: 'start'; // or 'end'
}
Insert after numeric position
insert: {
relative: 'after',
index: 1;
}
Insert before
specific element
insert: {
relative: 'after', // 'before' or 'after' node found via findElement
findElement: (node: Node) => {
// find specific array element
}
}
Insert before
named identifier
insert: {
relative: 'before',
findElement: 'rootRoute'
}
Insert code into a function block
insertInsideFunctionBlockInSource
insertInsideFunctionBlockInFile
insertInsideFunctionBlockInTree
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
insertClassMethodInSource
insertClassMethodInFile
insertClassMethodInTree
insertClassMethodInFile(filePath, {
code: `myMethod() {}`,
classId: 'myClass',
methodId: 'myMethod',
});
Add class property to a class
insertClassPropertyInSource
insertClassPropertyInFile
insertClassPropertyInTree
insertClassPropertyInFile(filePath, {
code: `myProp: User`,
classId: 'myClass',
propertyId: 'myProp',
});
Add decorator to a class
insertClassDecoratorInSource
insertClassDecoratorInFile
insertClassDecoratorInTree
insertClassDecoratorInFile(filePath, {
code: `@Model()`,
classId: 'myClass',
});
Add class method decorator (such as for NestJS)
insertClassMethodDecoratorInSource
insertClassMethodDecoratorInFile
insertClassMethodDecoratorInTree
const code = insertClassMethodDecoratorInFile(filePath, {
code: `@Post()`,
classId: 'myClass',
methodId: 'myMethod',
});
Add parameter decorator to a class method
insertClassMethodParamDecoratorInSource
insertClassMethodParamDecoratorInFile
insertClassMethodParamDecoratorInTree
const code = insertClassMethodParamDecoratorInFile(filePath, {
code: `@Body() body: string`,
classId: 'myClass',
methodId: 'myMethod',
});
removeFromNamedArray
removeClassDecorator
removeClassMethod
removeClassMethodDecorator
removeClassProperty
removeClassMethodParams
removeClassMethodParamDecorator
removeInsideFunctionBlock
removeImportId
removeImport
removeFromNamedObject
replaceInNamedObject
replaceInNamedArray
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:
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)