Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Codemods for React and TypeScript

January 19, 2022
reacttypescript

A codemod is an automated way of making modifications to code. I think of it as find and replace on steroids.

Codemods are helpful for library maintainers because they allow them to deprecate and make breaking changes to the public part of the API while minimising upgrade costs for developers consuming the library.

For example, Material UI has the following codemod for updating to version 5.

npx @mui/codemod v5.0.0/preset-safe <path>

This post covers creating codemods for a reusable React and TypeScript component library.

Codemods for React and TypeScript

Photo by Michael Aleo on Unsplash

jscodeshift

jscodeshift is a library built by Facebook to help create transforms. A transform is code that will change consuming code to our library to target a new version.

To install jscodeshift and the TypeScript types, we run the following commands in a terminal:

npm install jscodeshift
npm install --save-dev @types/jscodeshift

AST Explorer

Transforms are based on the code’s abstract syntax tree (AST). AST Explorer is a website that helps us understand the AST for some code.

To configure AST Explorer for React and TypeScript, we select the typescript parser on the toolbar:

AST Explorer typescript parser

Configuring TypeScript

In tsconfig.json, it is important to exclude the test fixture files using the exclude field (more on test fixtures later). We also specify the location for the transpiled transforms using the outDir field:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "target": "esnext",
    "strict": false,
    "jsx": "preserve",
    "lib": ["es2017"],
    "outDir": "dist"  },
  "exclude": ["transforms/__testfixtures__/**"]}

Configuring ESLint

If using ESLint, we again need to ignore test fixtures using the ignorePatterns field:

{
  "root": true,
  "env": { "node": true },
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
  "ignorePatterns": ["**/__testfixtures__"],  "rules": {
    "@typescript-eslint/explicit-function-return-type": "off"
  }
}

Creating a transform to change the name of a component prop

We will create a transform to change a kind prop on a Button element to variant. We will call the transformation button-kind-to-variant.

File structure

All the transforms will be in a transforms folder. The tests will be in a __tests__ folder within the transforms folder. Here’s what the file structure looks like:

transforms
└── button-kind-to-variant.ts      // the transform
└── __tests__
    └── button-kind-to-variant.ts  // test for the transform (references test fixtures below)
└── __testfixtures__
    └── button-kind-to-variant
        └── basic.input.tsx        // Code snippet to test
        └── basic.output.tsx       // expected result test code snippet

Creating a test

We’ll write a test before writing the transform. This will make it clear what the transform needs to do. Let’s start with the test fixtures, which are just input and expected output code snippets:

// basic.input.tsx

import { Button } from "@my/reusable-components";

function SomeComponent() {
  return <Button kind="round">test</Button>;
}
// basic.output.tsx

import { Button } from "@my/reusable-components";

function SomeComponent() {
  return <Button variant="round">test</Button>;
}

So, we expect the kind prop to change to a variant prop on the Button element.

Note: If you are using prettier to format code automatically, it is advised to exclude the test fixture files so that you can tweak their format to match what comes out of jscodeshift:

// .prettierignore

**/__testfixtures__

The test code for a transform is fairly generic and creates a test for each test fixture. The bits that change are the variable for the transform name and the array of test fixture names:

// button-kind-to-variant.ts 

jest.autoMockOff();

import { defineTest } from 'jscodeshift/dist/testUtils';

const name = 'button-kind-to-variant';const fixtures = ['basic'] as const;
describe(name, () => {
  fixtures.forEach((test) =>
    defineTest(__dirname, name, null, `${name}/${test}`, {
      parser: 'tsx',
    }),
  );
});

Creating the transform

Here’s the start of our transform:

import { API, FileInfo, JSXIdentifier } from 'jscodeshift';
export default function transformer(file: FileInfo, api: API) {
  const j = api.jscodeshift;
  const root = j(file.source);

  // TODO find Button JSX elements
  // TODO find `kind` prop
  // TODO change `kind` to `variant`
    
  return root.toSource();
}

jscodeshift expects a function as the default export to do the transformation. The function takes in information about the source file and the jscodeshift API. The jscodeshift API can query the source file and make changes to it.

Before writing the transformation code, we can use AST Explorer to get a feel for the AST structure. Not surprisingly, Button is a JsxElement:

Button JSX element

… and the kind prop is a JsxAttribute:

kind JSX attribute

Here’s the full transform function:

export default function transformer(file: FileInfo, api: API) {
  const j = api.jscodeshift;
  const root = j(file.source);

  // find Button JSX elements
  root    .findJSXElements('Button')    // find `kind` prop
    .find(j.JSXAttribute, {      name: {        type: 'JSXIdentifier',        name: 'kind',      },    })    // change `kind` to `variant`
    .forEach((jsxAttribute) => {      const identifier = jsxAttribute.node.name as JSXIdentifier;      identifier.name = 'variant';    });    
  return root.toSource();
}

The findJSXElements method is used to locate all the Button elements. For the found Button elements, the find method is used to get the kind attribute. The find method returns a collection of matching items, so we use the forEach method to iterate through this collection. We then change the attribute name to 'variant'.

Running the test

We run jest to run the test. So, we can add this in the test script in package.json.

{
  "scripts": {
    "test": "jest",    ...
  },
}

Running npm test will then run the test:

Test result

Our test passes. 😊

Running the codemod

Before running the codemod, we need to transpile the transform into JavaScript. We can do this using a build script in package.json, which calls the TypeScript compiler, tsc:

{
  "scripts": {
    "build": "tsc",    ...
  },
}

Running npm run build will put the JavaScript version of the transform in a dist folder.

To run the codemod, we need to run the jscodeshift CLI. We can put in a codemod script in package.json to do this:

{
  "scripts": {
    "codemod": "jscodeshift",
    ...
  },
}

Usually, the codemod will be executed on code in a separate project. In this example, we will execute the codemod on a file in this project at src\HomePage.tsx. Running the following command will execute a dry run of the transform on the HomePage.tsx file:

npm run codemod -- --parser=tsx -t dist/button-kind-to-variant.js  src/HomePage.tsx --print --dry

Here’s an explanation of the parameters:

  • --parser=tsx means that the TypeScript parser is used to parse the files the codemod is being applied to. We need to specify this because the default parser is babel.
  • The file after -t specifies the transform file. This is dist/button-kind-to-variant.js in our example.
  • The file or directory after the transform path specifies the files the codemod should be applied to. This is src/HomePage.tsx in our example.
  • --print (or -p) specifies that the transformed files are outputted to the terminal.
  • --dry (or d) specifies that just a dry run will happen (rather than changing the source files).

So, running the command prints out what the updated source code would be:

Dry codemod run

The updated source is exactly what we require. 😊

Running the following command can be executed to run the transform and perform the update:

npm run codemod -- --parser=tsx -t dist/button-kind-to-variant.js  src/HomePage.tsx

Nice. 😊

This example codemod is on my GitHub

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about testing React apps, you may find my course useful:

Testing React Apps with Jest and React Testing Library

Testing React Apps with Jest and React Testing Library
Find out more