packages/codemods/documentation/howTo.md
Want to learn how to write and run codemods? You're in the right place!
If you want to learn more about the mods themselves, check out the official README here
There are three principal ways of writing your own codemods:
upgrades.json, and returns a ready-to-run codemod to the user. To learn more about the specifics of the file configurations, check out the custom types filecreateCodeMod() function, which will return a ready-to-run codemod. We'll go through an example of this later.Here are the current features our codemods support:
Let's say you were responsible for renaming the toggled prop in Fluent's Dropdown component to checked. There are three easy ways to do it!
Feel free to try this out on your local branch of fluent! I've set up the files for you :).
upgrades.json file and replace what's there with the following template as defined in src/codemods/types.ts:{
"name": // Classify this collection of codemods
"upgrades": [
{
"name": // What upgrade are you running?
"type": // The type of mod you want to generate. This type affects what mod options are available
"version": // Mod version, as a string
"options": {
"from": {
"importName": // Name of component housing prop
"toRename": // Prop name to change
},
"to": {
"replacementName": // New prop name
}
}
}
]
}
createCodeModFromJson(), which will turn whatever exists in upgrades.json into a list of codemods. Check out ##Testing Your Codemods## for more info on how to verify it works!src/codemods/utilities. To rename a prop, we'll need to use two utility functions:
findJsxTag(file: SourceFile, tag: string)
renameProp( instances: (JsxOpeningElement | JsxSelfClosingElement)[], toRename: string, replacementName: string, replacementValue?: string, transform?: PropTransform) * This function accepts the return value of findJSXTag, as well as names for the deprecated prop and its replacement name. The function also takes in an optional replacement value, if you plan on updating the value of this prop whenever you want to rename it. For developers wanting even more granularity with value changes, there's an optional transform function you can pass in -- readrenamePropTransforms for more.SourceFile. This example suffices:const func = function (file: SourceFile) {
// your codemod utilities here!
};
createCodeMod(options: ModOptions, mod: (file: SourceFile) => Result<ModResult, NoOp>), which accepts a struct containing a name and a version string, as well as the wrapper function that you just created. The return type is a single codemod!Result return type isn't too tricky! Take a look at the return types of the utilities you're given -- they're often results too, which allows for you to pick out error / success messages and wrap them in result you return. To return a Result of type ModResult, NoOp, you'll simply have to return the constructor Ok({ logs: [some success messages]}) when you know a mod has completed running, or Err({ reason: 'why this mod failed' }), if you encounter a place where you know a mod has failed. Feel free to checkout existing codemods for examples.//some imports
const newCodeModName: CodeMod = {
run: (file: SourceFile) => {
try {
// Body of your codemod
} catch (e) {
return Err({ reason: /* display error e */ });
}
return Ok({ logs: [/* list of made changes */] });
},
version: /*some version string*/,
name: 'newCodeModName',
enabled: true,
};
export default newCodeModName;
run() function. Inside run(), the better you log your successes and failures (by either compiling the changelog of a given mod in a list and returning it in Ok() or returning an actionable error message in Err()), the easier time devs in the future will have when working with your mod!configMod.test.ts.project = new Project();
project.addSourceFileAtPaths(`${process.cwd()}addSomePathHere!`);
/* If you want to add many paths, you can do that too. */
project.addSourceFilesAtPaths(`${process.cwd()}someRegexPath`);
runMods(), which accepts an array of codemods, an array of source files, and a callback function that reports on the success of the mod. I've included it in the config test file, but here's what the invocation looks like:runMods(codemodArray, project.getSourceFiles(), result => {
result.result.resolve(
v => {
console.log(`Upgraded file ${result.file.getBaseName()} with mod ${result.mod.name}`, v.logs);
},
e => {
console.warn(`Mod ${result.mod.name} did not run on file ${result.file.getBaseName()} for: `, e.reason);
},
);
});
// inside the 'it' body of a test
expect(somePredicate).toBeTruthy(); // Test fails if SOMEPREDICATE is false
expect(somePredicate).toBeFalsy(); // Test fails if SOMEPREDICATE is true.
ts-morph to be able to navigate the AST and examine the renamed props. Here's some code to get you started that uses existing utilities to return an array of JSX elements that contain the components in the dropdown mock file:const file = const file = project.getSourceFileOrThrow(DropdownPropsFile);
const tags = findJsxTag(file, 'Dropdown');
getText() function. Be wary that ts-morph functions can potentially return undefined!
src file.