nx-code-mods
TypeScript icon, indicating that this package has built-in type declarations

1.3.2 • Public • Published

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)

Package Sidebar

Install

npm i nx-code-mods

Weekly Downloads

0

Version

1.3.2

License

ISC

Unpacked Size

528 kB

Total Files

270

Last publish

Collaborators

  • kmandrup