Introduction

Jscodeshift is a toolkit for running codemods over multiple JS files.

Codemods are nothing but code modification. So if you want to modify the JS code in bulk, you can use Jscodeshift

Why would you modify code in bulk?

One of the questions that you might have had is what is the usecase for code modification in bulk. The JScodeshift is primarily used by facebook to modify the code when the version of react changes. So if you have many projects using react 16, and you want to upgrade to react 17.But some api is depreciated in react 17, then you can use JScodeshift to modify the code accross all the projects to make it compatible with react 17.

How does it work?

JScodeshift uses recast, which is AST (Abstract Syntax Tool) transformation tool. The AST code for the following code is as follows

console.log("Hello World");

{
  "type": "Program",
  "start": 0,
  "end": 27,
  "range": [
    0,
    27
  ],
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 27,
      "range": [
        0,
        27
      ],
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 26,
        "range": [
          0,
          26
        ],
        "callee": {
          "type": "MemberExpression",
          "start": 0,
          "end": 11,
          "range": [
            0,
            11
          ],
          "object": {
            "type": "Identifier",
            "start": 0,
            "end": 7,
            "range": [
              0,
              7
            ],
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "range": [
              8,
              11
            ],
            "name": "log"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 12,
            "end": 25,
            "range": [
              12,
              25
            ],
            "value": "Hello World",
            "raw": "\"Hello World\""
          }
        ]
      }
    }
  ],
  "sourceType": "module"
} 

You can play around this AST using the online editor AST Explorer.

AST concept is used by other javascript code-affecting libraries like eslint.

Installing

You can install Jscodeshift from npm using the command

npm install -g jscodeshift

Running the script

In order to run a jscodeshift script on the codebase, you need to run the command

jscodeshift -t <path to script> <target folders/files>

Here -t means transform. So if you have a script name testscript which you need to run on src folder, run the command

jscodeshift -t ./testscript.js ./src

To know the other options, run the help command

jscodeshift --help
$ jscodeshift --help

Usage: jscodeshift [OPTION]... PATH...
  or:  jscodeshift [OPTION]... -t TRANSFORM_PATH PATH...
  or:  jscodeshift [OPTION]... -t URL PATH...
  or:  jscodeshift [OPTION]... --stdin < file_list.txt

Apply transform logic in TRANSFORM_PATH (recursively) to every PATH.
If --stdin is set, each line of the standard input is used as a path.

Options:
"..." behind an option means that it can be supplied multiple times.
All options are also passed to the transformer, which means you can supply custom options that are not listed here.

      --(no-)babel              apply babeljs to the transform file
                                (default: true)
  -c, --cpus=N                  start at most N child processes to process source files
                                (default: max(all - 1, 1))
  -d, --(no-)dry                dry run (no changes are made to files)
                                (default: false)
      --extensions=EXT          transform files with these file extensions (comma separated list)
                                (default: js)
  -h, --help                    print this help and exit
      --ignore-config=FILE ...  ignore files if they match patterns sourced from a configuration file (e.g. a .gitignore)
      --ignore-pattern=GLOB ...  ignore files that match a provided glob expression
      --parser=babel|babylon|flow|ts|tsx  the parser to use for parsing the source files
                                          (default: babel)
      --parser-config=FILE      path to a JSON file containing a custom parser configuration for flow or babylon
  -p, --(no-)print              print transformed files to stdout, useful for development
                                (default: false)
      --(no-)run-in-band        run serially in the current process
                                (default: false)
  -s, --(no-)silent             do not write to stdout or stderr
                                (default: false)
      --(no-)stdin              read file/directory list from stdin
                                (default: false)
  -t, --transform=FILE          path to the transform file. Can be either a local path or url
                                (default: ./transform.js)
  -v, --verbose=0|1|2           show more information about the transform process
                                (default: 0)
      --version                 print version and exit

Examples

Facebook has codemod repositories which contains jscodeshift scripts to update react versions. You can browse the repository https://github.com/reactjs/react-codemod

Lets take one of the script to rename the lifecycle methods in react.

Problem: When react upgraded from 15 to 16, the following life cycle methods were depreciated

componentWillMount, componentWillReceiveProps, componentWillUpdate

As react maintains backward compatibility for 1 major version, it allowed the use of these methods in react 16 provided that these methods were prefixed with UNSAFE_. So the new methods will be

React 15 React 16
componentWillMount UNSAFE_componentWillMount
componentWillReceiveProps UNSAFE_componentWillReceiveProps
componentWillUpdate UNSAFE_componentWillUpdate

So the jscodeshift is as follows

const DEPRECATED_APIS = Object.create(null);

DEPRECATED_APIS.componentWillMount = 'UNSAFE_componentWillMount';
DEPRECATED_APIS.componentWillReceiveProps = 'UNSAFE_componentWillReceiveProps';
DEPRECATED_APIS.componentWillUpdate = 'UNSAFE_componentWillUpdate';

export default (file, api, options) => {
  const j = api.jscodeshift;
  const printOptions = options.printOptions || {
    quote: 'single',
    trailingComma: true,
  };

  const root = j(file.source);

  let hasModifications = false;

  const renameDeprecatedApis = path => {
    const name = path.node.key.name;

    if (DEPRECATED_APIS[name]) {
      path.value.key.name = DEPRECATED_APIS[name];
      hasModifications = true;
    }
  };

  const renameDeprecatedCallExpressions = path => {
    const name = path.node.property.name;

    if (DEPRECATED_APIS[name]) {
      path.node.property.name = DEPRECATED_APIS[name];
      hasModifications = true;
    }
  };

  // Class methods
  root
    .find(j.MethodDefinition)
    .forEach(renameDeprecatedApis);

  // Arrow functions
  root
    .find(j.ClassProperty)
    .forEach(renameDeprecatedApis);

  // createReactClass and mixins
  root
    .find(j.Property)
    .forEach(renameDeprecatedApis);

  ...

  return hasModifications
    ? root.toSource(printOptions)
    : null;
};

Here first they create an object of deprecated apis

DEPRECATED_APIS = {
   componentWillMount: 'UNSAFE_componentWillMount',
   componentWillReceiveProps: 'UNSAFE_componentWillReceiveProps',
   componentWillUpdate: 'UNSAFE_componentWillUpdate'
}

Then, they check for MethodDefination, Property etc using jscodeshift api and replace the key of DEPRECATED_APIS with the value.

path.node.property.name = DEPRECATED_APIS[name];

You can run this script using the following command

jscodeshift -t react-codemod/transforms/rename-unsafe-lifecycles.js <codebase>