Skip to content

Instantly share code, notes, and snippets.

@donaldpipowitch
Last active January 26, 2023 10:14
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/222b673fc3af4aee5a797920099b7ddd to your computer and use it in GitHub Desktop.
Save donaldpipowitch/222b673fc3af4aee5a797920099b7ddd to your computer and use it in GitHub Desktop.
Create a codemod with Prettier and Babel

Every now and then I'd like to apply a so called codemod to my a codebase. A codemod is a piece of code which modifies other code. Very often this is done by running some transformation on the abstract syntax tree (AST).

Whenever I need to do this I look for "What is the best way to apply a codemod on my TS code base right now?", because when I only do this every couple of month I either have forgotten about how to do it or tools have changed and my old way stopped working... or both. Surprisingly every time I search for that I found the existing tools bloated or quirky or not matching my workflow.

As I already use Babel to compile our source code I would like to create a Babel plugin which transforms my code. Sadly Babel alone is not good in preserving whitespace and formatting. Thankfully I use Prettier for that and because Prettier uses Babel internally I can create a Babel visitor to transform my source code.

The actually easiest and most resilient way for me to do that was not by using a dedicated codemod tool, but just Prettier itself with a custom wrapper (see prettiermod.js). The wrapper allows you to specifiy a file pattern (aka. which files should be codemodded) and shows a preview of the change by default (pass write: true to actually persist the changes).

In the usage.js file you can see an example how my prettiermod.js can be used. It shows how you can convert type Hello = { world: string; } to interface Hello { world: string; } (- I wanted to see, if this has any performance improvements on a large code base as interfaces are handled a bit differently in the TypeScript compiler).


Bonus tip: Use astexplorer.net with @babel/parser and babelv7 as the "transform" setting for quick prototyping.

// @ts-check
const globby = require('globby');
const { readFile, writeFile } = require('fs/promises');
const prettier = require('prettier');
const traverse = require('@babel/traverse');
/**
* @typedef {{
* patterns: string | string[];
* write?: boolean;
* prettierConfig: any;
* visitors: any[];
* }} PrettiermodOptions
*/
module.exports = async function prettiermod(
/** @type {PrettiermodOptions} */ {
patterns,
write = false,
prettierConfig,
visitors,
}
) {
const files = await globby(patterns);
await Promise.all(
files.map(async (file) => {
const code = await readFile(file, 'utf-8');
const result = prettier.format(code, {
...prettierConfig,
parser(code, { 'babel-ts': babel }) {
const ast = babel(code);
visitors.forEach((visitor) => traverse.default(ast, visitor));
return ast;
},
});
if (write) {
await writeFile(file, result);
} else {
console.log(result);
}
})
);
};
const prettiermod = require('./prettiermod');
const prettierConfig = require('./prettier.config');
const typeToInterfaceVisitor = {
TSTypeAliasDeclaration(path) {
// ignore union types, intersection types, function types...
if (path.node.typeAnnotation.type !== 'TSTypeLiteral') return;
path.node.type = 'TSInterfaceDeclaration'; // TSTypeAliasDeclaration
path.node.body = path.node.typeAnnotation;
delete path.node.typeAnnotation;
path.node.body.type = 'TSInterfaceBody';
path.node.body.body = path.node.body.members;
delete path.node.body.members;
},
};
prettiermod({
patterns: ['src/**/*.{ts,tsx}'],
prettierConfig,
visitors: [typeToInterfaceVisitor],
write: true,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment