NickHeiner / jscodemod

Codemod runner
MIT License
41 stars 4 forks source link

jscodemod

JSCodemod is a codemod runner. Its codemods are written in JS/TS, or generated via AI. You can operate on any type of file.

Example

$ jscodemod --codemod codemod.js src/**/*.js

Run with --help to see other options.

What's a codemod?

Codemods are automated code transformations – for instance, changing a && a.b && a.b.c to a?.b?.c. Codemods are useful for large codebases, where doing such a change by hand would put you at risk for making mistakes or developing repetitive stress injuries. (Additionally, automating the change allows you to more easily resolve merge conflicts with PR branches.)

Don't we already have jscodeshift for this?

Yes. I've used jscodeshift, which was created in 2015, in the past. However, I was inspired to make a new tool to address some gaps that I saw:

For more detail, see Comparison with JSCodeshift. To see more things that you can do easily with jscodemod that you can't do easily with jscodeshift, see Recipes.

CLI Usage

To specify which files to run on, pass a set of globby patterns. Depending on your shell, you may need to wrap the patterns in single quotes to prevent shell expansion:

# Will be shell expanded, which may not be what you want
$ jscodemod --codemod codemod.js src/**/*.js

# Will not be shell expanded
$ jscodemod --codemod codemod.js 'src/**/*.js'

How to write a codemod

The argument you pass to --codemod is a file that exports a Codemod. Look in Types for the semantics of that object.

Babel Plugin

  1. Use ASTExplorer with the "transform" option enabled for an interactive environment for developing your plugin.
  2. Read the Babel Plugin Handbook to learn how to write a Babel plugin.

Examples

Using the low-level transform API:

import type {Codemod} from '@nick.heiner/jscodemod';

const codemod: Codemod = {
  // Ignore files with paths matching these regexes, or including these strings.
  ignore: [new RegExp('path/to/a.js'), 'path/to/b.js', /directory-to-omit/],

  // Ignore files specified by these config files used by other tools
  ignoreFiles: ['path/to/.eslintignore', 'path/to/.npmignore'],

  transform({source}) { /* ... */ }

  // Take actions after the codemod has run.
  async postProcess(modifiedFiles, {jscodemod}) {
    // Run a second codemod on the set of files we modified in the first phase.
    await jscodemod(
      require.resolve('path/to/second-codemod-phase'),
      modifiedFiles
    )
  }
}

Using the high-level getPlugin API:

import type {Codemod} from '@nick.heiner/jscodemod';
const codemod: Codemod = {
  // Whatever presets are needed to parse your code.
  presets: ['@babel/preset-react', '@babel/preset-typescript', '@babel/preset-env']

  // The transformation you'd like to do in the codemod.
  getPlugin({source, fileName}) {
    return ({types: t}) => ({
      visitor: {
        /* ... your babel plugin here */
      }
    })
  }
}

More Docs

Changelog

3.0.0

// Previous
import type { Codemod } from '@nick.heiner/jscodemod';

// New: Use the specific type that applies to your codemod.
// See ./src/types.ts for definitions.
import type { BabelCodemod, LowLevelCodemod, AICodemod } from '@nick.heiner/jscodemod';

2.0.0

Drop support for NodeJs 12.