Skip to content

Instantly share code, notes, and snippets.

@donaldpipowitch
Last active April 2, 2022 01:35
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save donaldpipowitch/46757da0459667bf8a1b22d7c19eb903 to your computer and use it in GitHub Desktop.
Save donaldpipowitch/46757da0459667bf8a1b22d7c19eb903 to your computer and use it in GitHub Desktop.
Example for a codemod which can run on a TypeScript code base

Install @codemod/cli globally:

$ npm install -g @codemod/cli
# or
$ yarn global add @codemod/cli

This package works out of the box with most code bases, because it comes bundled with @babel/preset-env and @babel/preset-typescript. If you need other presets or plugins for parsing your source code you can use a custom Babel config as well. Note that the codemod will not apply the transformations from these presets and plugins - they are only used for parsing. Therefor you keep your TypeScript types in your source code for example. Formatting will be kept the same as much as possible.

If you want to apply a transformation you set the plugin which should be used for this transformation explicitely.

Let's write a custom Babel plugin which will prepend readonly to all params in a constructor. (E.g. sonarlint wants you to do that, if that is your flavour. 🙂) Note that this will be just a quick example and very likely not feature complete.

This will transform this code:

class SomeComponent {
  constructor(private growl: Growl) {}
}

To this code:

class SomeComponent {
  constructor(private readonly growl: Growl) {}
}

This can be done with this custom Babel plugin:

module.exports = function transform() {
  return {
    visitor: {
      ClassMethod({ node }) {
        const isConstructor = node.kind === 'constructor';
        if (!isConstructor) return;

        node.params.forEach((param) => {
          if (param.type !== 'TSParameterProperty') return;
          param.readonly = true;
        });
      }
    }
  };
};

It looks for ClassMethod's with a kind of 'constructor' and then loops over all params with a type of 'TSParameterProperty' to set readonly to true.

That's it. Now we can run this transformation for all files in a directory like src/:

$ codemod --plugin ./your-custom-plugin.js src/

If you want to debug your AST please check out AST explorer. Paste an example on the left and choose "JavaScript" as the language, "babylon7" as the parser and make sure to uncheck "flow" and check "typescript" in the "babylon7 Settings".


You also use jscodeshift, if you want to:

$ npm install -g jscodeshift
# or
$ yarn global add jscodeshift

The transform file is very similar:

module.exports = function transform(file, { jscodeshift }) {
    return jscodeshift(file.source)
        .find(jscodeshift.ClassMethod)
        .replaceWith(({ node }) => {
            const isConstructor = node.kind === 'constructor';
            if (!isConstructor) return node;

            node.params.forEach((param) => {
              if (param.type !== 'TSParameterProperty') return;
              param.readonly = true;
            });

            return node;
        })
        .toSource();
};

module.exports.parser = 'ts';
module.exports.extensions = 'ts';

And you run it like this:

$ jscodeshift --transform=your-custom-transform.js src/**/*.ts

Sadly globbing will not work on old Macs with Bash 3. Either update your Bash or try this (slow) workaround:

$ find ./src -name '*.ts'  | xargs -I F jscodeshift "F" --transform=your-custom-transform.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment