nx-code-mods
v1.3.2
Published
Nx generator code modifier toolkit for typescript code using tsquery
Downloads
9
Maintainers
Readme
Generator code mods
This 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.
In addition the toolkit includes experimental support for:
Test results
Test Suites: 36 passed, 36 total
Tests: 188 passed, 188 total
Chainable APIs
- Chain API
- Insert API
- Remove API
- Replace API
- Transform API
Chain API
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);
};
Sample Nx usage
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.
[
{
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);
Insert Chain API
insertApi(source: string)
Example
const insert = insertApi(source);
insert.classDecorator({
code: '@Model()',
classId: 'myClass',
});
Remove Chain API
removeApi(source: string)
Example
const remove = removeApi(source);
remove.fromNamedArray({
varId: 'Routes',
remove: {
index: 'end',
},
});
Replace Chain API
replaceApi(source: string)
Example
const replace = replaceApi(source);
replace.inNamedObject({
varId: 'Routes',
code: `{ x: 2 }`,
replace: {
index: 'end',
},
});
Transform API
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);
Insert API
Full example
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);
Append after last import
Appends an import statement to the end of import declarations.
appendAfterImportsInSource
appendAfterImportsInFile
appendAfterImportsInTree
Sample usage
const code = `import { x } from 'x'`;
appendAfterImportsInTree(
tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/app-routing.module.ts',
code
}
);
await formatFiles(tree);
Insert into import
Inserts an identifier to import into an existing import declaration
insertImportInSource
insertImportInFile
insertImportInTree
Sample usage
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 into named Object
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
.
Sample usage
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 object options
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 into named Array
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
.
Sample usage
insertIntoNamedArrayInTree(tree,
{
normalizedOptions.projectRoot,
relTargetFilePath: '/src/app/app-routing.module.ts',
varId: 'Routes',
code: `{ x: 2 }`,
insert: {
index: 'end'
}
}
);
await formatFiles(tree);
Insert array options
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 into function block
Insert code into a function block
insertInsideFunctionBlockInSource
insertInsideFunctionBlockInFile
insertInsideFunctionBlockInTree
Sample usage
insertInsideFunctionBlockInFile(filePath, {
code,
functionId: 'myFun',
insert: {
index: 'end',
},
});
insert
allows for the same positional options as for inserting inside an array.
Insert class method
Add a class method to a class
insertClassMethodInSource
insertClassMethodInFile
insertClassMethodInTree
Sample usage
insertClassMethodInFile(filePath, {
code: `myMethod() {}`,
classId: 'myClass',
methodId: 'myMethod',
});
Insert class property
Add class property to a class
insertClassPropertyInSource
insertClassPropertyInFile
insertClassPropertyInTree
Sample usage
insertClassPropertyInFile(filePath, {
code: `myProp: User`,
classId: 'myClass',
propertyId: 'myProp',
});
Insert class decorator
Add decorator to a class
insertClassDecoratorInSource
insertClassDecoratorInFile
insertClassDecoratorInTree
Sample usage
insertClassDecoratorInFile(filePath, {
code: `@Model()`,
classId: 'myClass',
});
Insert class method decorator
Add class method decorator (such as for NestJS)
insertClassMethodDecoratorInSource
insertClassMethodDecoratorInFile
insertClassMethodDecoratorInTree
Sample usage
const code = insertClassMethodDecoratorInFile(filePath, {
code: `@Post()`,
classId: 'myClass',
methodId: 'myMethod',
});
Insert class method parameter decorator
Add parameter decorator to a class method
insertClassMethodParamDecoratorInSource
insertClassMethodParamDecoratorInFile
insertClassMethodParamDecoratorInTree
Sample usage
const code = insertClassMethodParamDecoratorInFile(filePath, {
code: `@Body() body: string`,
classId: 'myClass',
methodId: 'myMethod',
});
Remove API
removeFromNamedArray
removeClassDecorator
removeClassMethod
removeClassMethodDecorator
removeClassProperty
removeClassMethodParams
removeClassMethodParamDecorator
removeInsideFunctionBlock
removeImportId
removeImport
removeFromNamedObject
Replace API
replaceInNamedObject
replaceInNamedArray
replaceClassDecorator
replaceClassMethodDecorator
replaceClassMethodParams
replaceClassMethod
replaceClassMethodDecorator
replaceClassProperty
replaceImportIds
replaceInFunction
Auto-naming (Experimental)
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 (Experimental)
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 methods
Extract method from a block of code (using auto-naming)
extractMethods(srcNode: SourceFile, block: Block)
Refactor If/Else statements
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
Refactor switch statements into named functions and function calls with or (||
)
extractSwitchStatements(srcNode: SourceFile, block: Block)
extractSwitch(srcNode: SourceFile, switchStmt: SwitchStatement)