npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

nx-code-mods

v1.3.2

Published

Nx generator code modifier toolkit for typescript code using tsquery

Downloads

27

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)