28. Mar 2022Frontend

Jscodeshift: write code that rewrites your code

Do you find title of this article interesting? Then it's time to ask how many times you have used the "find and replace" development environment feature, such as deleting a call to a feature that is no longer needed or making other changes to the source code.

Roman HaluškaFrontend developer

There is a faster way to edit a large number of changes, using regular expressions. However, it also has its drawbacks - especially that in the case of more complex changes, you need to understand the context of the code, and also have a lot of patience and time.

"Codemods" come into play

What is the codemod? Codemod is a script that overwrites other code. It works similarly to the "find and replace" function, where the transformation script can find, for example, the variable you are looking for in the code and change its declaration or type. You can also use this script to update the code in the event of a major change to the library you use, as well as extensive changes to the interface. You can basically use Codemodes for any changes to the project.

In this article, we will further explore the capabilities of the codemods tool called jscodeshift and demonstrate its real use in migrating from the moment to dayjs library.

What is Jscodeshift and how it works?

Jscodeshift is a tool that runs a transformation script over one or more JavaScript or TypeScript files. It is also a wrapper for the recast module, which extends this API with additional features. If you need more information about the features that this tool provides, see the documentation. If you need advice, I definitely recommend visiting the jscodeshift community page.

Jscodeshift reads all the files you provide to it at runtime and analyzes and compiles the source into the abstract " AST  " syntax tree as part of the transformation, then looks for matches specified in the transformation script, deletes matches, or replaces them with the required content, and then regenerates the file from the modified AST.

Install of Jscodeshift step by step

Install jscodeshift globally.

npm i -g jscodeshift

At package.json level open file, eg. with name transform.js. In this file you will define transformer function. As a parser you can use babel, next you need to define API and source file. You can see an example of the script down below, which will transform node moment to davis.

const parser = 'babel'

function transformer (file, api) {
	const j = api.jscodeshift;
	const root = j(file.source);

	return root.find(j.Identifier)
		.forEach(path => {
			// find declaration for "moment" identifier
			if (path.node.name === 'moment') {
				j(path).replaceWith(
					j.identifier('dayjs')
				);
			}
		})
		.toSource();
}

module.exports = transformer
module.exports.parser = parser

Running of transformation script

Example of running transformational script transform.js for src directory.

jscodeshift ./src --extensions=js,jsx -t transform.js -p

In case, when you want to run transformation just over specific file, you can replace directory with file path. If it is a typescript, you need to change the value of the "extensions" switch to ts, tsx. When another parser needs to be used, it can be defined via a switch.

--parser=babel|babylon|flow|ts|tsx , or your own parser --parser-config=FILE . If you want the transformation to take place but the changes not be written to the files, you must use the d . You can find more information on Meta GitHub.

What is AST - Abstract syntax tree?

AST is a syntactic tree, ie a tree representation of the abstract syntactic structure of source code written in a formal language. Each node in the tree indicates a construct that appears in the text. I recommend using the online tool AST explorer for the development of the transformation script.

Jscodeshift API uses three types of objects and that is nodes, node-paths and collections.

If you want to work with the API, it is necessary to understand this structure:

  • Node - The node is a basic element of AST, they are simple objects that do not provide any methods.
  • Node-paths/paths - Paths is basically the address of a node in a given tree, which provides a path in case of finding and identifying a node, and thus also stores information about the parents of a given node. You can access the node through the node property.
  • Collections - Is a group of one or many paths, whitch jscodeshift API returns, when you enter request to the source.

Codemods test driven development with help of jest

The transformation script can also be developed by the method of creating test scenarios, where we define inputs and outputs, and then we improve the transformation script by evaluating the tests. Jscodeshift is integrated with a testing utility with the jest framework.

At package.json lever you can create directory __tests__ and __testfixtures__ .

In the test directory, create the file testCases.js , where we define the test. The second parameter of the defineTest function is the name of the file where the transformation script is located, in our case transform.js , and the fourth parameter of the function is the name of the prepared test scenarios, in our case named simple. We create two files in the testfixtures directory, simple.output.js , where we declare the case of the code before running the script, and simple.input.js , where we declare what the code should look like after the transformation.

testCases.js

jest.autoMockOff();
const defineTest = require('jscodeshift/dist/testUtils').defineTest;

defineTest(__dirname, 'transform', null, 'simple');

simple.output.js

import React from 'react'
import dayjs from 'moment'

const page = () => {
   return <div className={'subtitle'}>{dayjs(publishedAt).format(t('dateFormat'))}</div>;
}

We run the tests using the command jest .

Example of a transformation script for declaring a moment module import:

const parser = 'babel'

function transformer (file, api) {
   const j = api.jscodeshift;
   const root = j(file.source);

   // find declaration for "moment" import
   return root.find(j.ImportDeclaration, { source: { value: 'moment'}}).forEach(path => {
	   path.value.source.value = 'dayjs'
   }).toSource();
}

module.exports = transformer
module.exports.parser = parser

Conclusion

I hope this article introduces you to the capabilities of jscodeshift, which provides enough flexibility for any automatic code editing. It allows complex code transformations in a matter of seconds. As you become more familiar with it and become more skilled at writing transformation scripts, your and your team's effectiveness in solving the problem described in this article will increase.

Roman HaluškaFrontend developer