Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/kristianmandrup/nx-code-mods
Nx generator code modification utility functions
https://github.com/kristianmandrup/nx-code-mods
Last synced: about 1 month ago
JSON representation
Nx generator code modification utility functions
- Host: GitHub
- URL: https://github.com/kristianmandrup/nx-code-mods
- Owner: kristianmandrup
- Created: 2022-04-07T03:06:28.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2022-10-29T21:11:54.000Z (almost 2 years ago)
- Last Synced: 2024-07-27T08:33:09.990Z (about 2 months ago)
- Language: TypeScript
- Size: 979 KB
- Stars: 3
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: Readme.md
Awesome Lists containing this project
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.
- [Chainable APIs](#ChainableAPIs)
- [Insert API](#InsertAPI)
- [Full example](#Fullexample)
- [Remove API](#RemoveAPI)
- [Replace API](#ReplaceAPI)In addition the toolkit includes experimental support for:
- [Auto-naming](#Autonaming)
- [Automated refactoring](#Automatedrefactoring)## Test results
```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.
```ts
[
{
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
```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}`
)
: 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`#### Sample usage
```ts
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`#### Sample usage
Implicit import id
```ts
const code = insertImportInFile(filePath, {
importId: 'x',
importFileRef: './my-file',
});
```Explicit import code with import alias
```ts
const code = `x as xman`;
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;
}
```- `insertIntoNamedObjectInSource`
- `insertIntoNamedObjectInFile`
- `insertIntoNamedObjectInTree`Inserts the `code` in the object named `varId`.
#### Sample usage
```ts
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
```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
- `insertIntoNamedArrayInSource`
- `insertIntoNamedArrayInFile`
- `insertIntoNamedArrayInTree`Inserts the `code` in the array named `varId`.
#### Sample usage
```ts
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
```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 into function block
Insert code into a function block
- `insertInsideFunctionBlockInSource`
- `insertInsideFunctionBlockInFile`
- `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
- `insertClassMethodInSource`
- `insertClassMethodInFile`
- `insertClassMethodInTree`#### Sample usage
```ts
insertClassMethodInFile(filePath, {
code: `myMethod() {}`,
classId: 'myClass',
methodId: 'myMethod',
});
```Add class property to a class
- `insertClassPropertyInSource`
- `insertClassPropertyInFile`
- `insertClassPropertyInTree`#### Sample usage
```ts
insertClassPropertyInFile(filePath, {
code: `myProp: User`,
classId: 'myClass',
propertyId: 'myProp',
});
```Add decorator to a class
- `insertClassDecoratorInSource`
- `insertClassDecoratorInFile`
- `insertClassDecoratorInTree`#### Sample usage
```ts
insertClassDecoratorInFile(filePath, {
code: `@Model()`,
classId: 'myClass',
});
```### Insert class method decorator
Add class method decorator (such as for NestJS)
- `insertClassMethodDecoratorInSource`
- `insertClassMethodDecoratorInFile`
- `insertClassMethodDecoratorInTree`#### Sample usage
```ts
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
```ts
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 (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 callsSee `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)`