This guide shows how you can build, test, and publish a Codemod 2.0 in a few steps. In the following example, we consider a use case where we want to create a codemod to change imports to direct imports:

// BEFORE 
import {
  AccessAlarm as AccessAlarmIcon,
  ThreeDRotation as NiceIcon,
} from '@mui/icons-material';

//AFTER
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
import NiceIcon from '@mui/icons-material/ThreeDRotation';

Step 1: Initialize a new codemod.

Run npx codemod@latest init in your desired location to store the codemod folder, and select workflow as your engine. This sets up the scaffolding for your codemod, making it easy to provide configuration and metadata. While optional for building and running a codemod, this step enhances discoverability and sharing.

Step 2: Find JS/TS files.

From the previous step, we got a new directory mui-named-import-to-direct-import and file inside src/index.ts. Let’s update this boilerplate file.

We need to get all the files that potentially can have Material UI Icons imports

import type { Api } from '@codemod.com/workflow';

export async function workflow({ files }: Api) {
  await files()
}

Now let’s get Javascript/Typescript context:

import type { Api } from '@codemod.com/workflow';

export async function workflow({ files }: Api) {
  await files()
	  .jsFam()
}

Step 3: Use ast-grep for pattern detection.

Let’s use ast-grep which is built-in into our workflow engine

import type { Api } from '@codemod.com/workflow';

export async function workflow({ files }: Api) {
  await files()
	  .jsFam()
	  .astGrep("import { $$$_ } from '@mui/icons-material'")
}

ast-grep enables us to match patterns in our code using meta variables. In the example, we use the $$$ multi meta variable to match any number of named imports.

Step 4: Use AI for code transformation.

Now that we have all the needed files and imports, we should fix it somehow. We can write some logic to handle different cases, like support of aliases or … we can just ask LLM to make changes

import type { Api } from '@codemod.com/workflow';

export async function workflow({ files }: Api) {
  await files()
    .jsFam()
	  .astGrep("import { $$$_ } from '@mui/icons-material'")
    .ai`
      Migrate named imports from material ui icons to direct imports.
      Make sure preserve import names.

      Example before:
      import { AccessAlarm as AccessAlarmIcon, ThreeDRotation } from '@mui/icons-material'

      Example after:
      import AccessAlarmIcon from '@mui/icons-material/AccessAlarm'
      import ThreeDRotation from '@mui/icons-material/ThreeDRotation'
    `;
	}

Step 5: Run and test the codemod locally.

Let’s check it out:

npx codemod@latest mui-named-import-to-direct-import/src/index.ts \
--OPENAI_API_KEY=$OPEN_API_KEY \
--engine=workflow

And here is our fixed file:

import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
import NiceIcon from '@mui/icons-material/ThreeDRotation';

export function MuiDemo({ title }: { title: string }) {
  return (
    <>
      {title}
      <AccessAlarmIcon />
      <NiceIcon />
    </>
  );
}

export default MuiDemo;

Step 6: Publish the Codemod

Now that our codemod is ready, we can publish it to Codemod Registry. This allows your codemod to be:

  • easily accessible to the open source community and your peers
  • seamlessly distributed upon releasing new versions
  • integrated with the rest of Codemod platform
cd mui-named-import-to-direct-import
npx codemod@latest publish

Now that the codemod is published, you can easily run it from Codemode Registry:

npx codemod@latest mui/named-import-to-direct-import --OPENAI_API_KEY=$OPENAI_API_KEY

P.S.

If you want simplify things more, take a look at workflow engine documentation and improve code a little bit:

import type { Api } from '@codemod.com/workflow';

export async function workflow({ git }: Api) {
  const repositories = await git
    .clone('git@github.com:codemod-com/mui-demo.git')
    .branch('mui-named-import-to-direct-import');

  await repositories
    .files('**/*.{js,jsx,ts,tsx}')
    .jsFam()
	  .astGrep("import { $$$_ } from '@mui/icons-material'")
	  .ai`
      Migrate named imports from material ui icons to direct imports.
      Make sure preserve import names.

      Example before:
      import { AccessAlarm as AccessAlarmIcon, ThreeDRotation } from '@mui/icons-material'

      Example after:
      import AccessAlarmIcon from '@mui/icons-material/AccessAlarm'
      import ThreeDRotation from '@mui/icons-material/ThreeDRotation'
    `;

  await repositories
    .commit('Migrate named imports from material ui icons to direct imports')
    .push();
}