├── .node-version ├── .npm-version ├── .gitignore ├── src └── index.js ├── codemods ├── index.ts └── react-to-star-import.ts ├── package.json ├── LICENSE ├── README.md └── run-codemod.ts /.node-version: -------------------------------------------------------------------------------- 1 | v12.22.7 2 | -------------------------------------------------------------------------------- /.npm-version: -------------------------------------------------------------------------------- 1 | 6.14.15 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // This is an example of a file that a codemod would run against 2 | import React from "react"; 3 | -------------------------------------------------------------------------------- /codemods/index.ts: -------------------------------------------------------------------------------- 1 | import reactToStarImport from "./react-to-star-import"; 2 | import { Program } from "@babel/types"; 3 | 4 | // Each codemod in this object is expected to somehow modify the `ast` it receives (or its child nodes). 5 | const codemods: { 6 | [key: string]: (ast: Program, filepath: string, source: string) => void; 7 | } = { 8 | "react-to-star-import": reactToStarImport, 9 | }; 10 | 11 | export default codemods; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-codemod-script", 3 | "version": "1.0.0", 4 | "description": "A small, low-level codemod script", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "xode run-codemod.ts" 8 | }, 9 | "author": "Lily Scott ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@babel/core": "^7.16.7", 13 | "@babel/parser": "^7.16.8", 14 | "@babel/types": "^7.16.8", 15 | "@types/node": "^17.0.8", 16 | "chalk": "^4.1.2", 17 | "globby": "^11.1.0", 18 | "recast": "^0.20.5", 19 | "typescript": "^4.5.4", 20 | "xode": "0.0.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2022 Lily Scott 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /codemods/react-to-star-import.ts: -------------------------------------------------------------------------------- 1 | import * as t from "@babel/types"; 2 | 3 | // This codemod looks for this: 4 | // 5 | // import React from "react"; 6 | // 7 | // And changes it to this: 8 | // 9 | // import * as React from "react"; 10 | // 11 | export default function codemod( 12 | ast: t.Program, 13 | filepath: string, 14 | source: string 15 | ) { 16 | // You could use https://babeljs.io/docs/en/babel-traverse instead of these for loops and if loops 17 | for (const statement of ast.body) { 18 | if (statement.type === "ImportDeclaration") { 19 | if ( 20 | statement.source.type === "StringLiteral" && 21 | statement.source.value === "react" 22 | ) { 23 | for (let i = 0; i < statement.specifiers.length; i++) { 24 | const specifier = statement.specifiers[i]; 25 | 26 | if (specifier.type === "ImportDefaultSpecifier") { 27 | const localIdent = specifier.local; 28 | 29 | // Here, we use the node builder functions from @babel/types to create an ImportNamespaceSpecifier node. 30 | // For creating more complicated AST node structures, you might want to use https://babeljs.io/docs/en/babel-template 31 | const newSpecifier = t.importNamespaceSpecifier(localIdent); 32 | statement.specifiers[i] = newSpecifier; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-codemod-script 2 | 3 | This is an example of how to write your own codemod scripts using babel and recast. 4 | 5 | There are comments throughout the code explaining what's happening, with links to resources. 6 | 7 | To write codemods for your own codebase using the same patterns as this script, fork this repo, modify it to your needs, and add codemods to the `codemods` folder. 8 | 9 | ## Usage 10 | 11 | - Clone the repo 12 | - Run `npm install` 13 | - Add codemods to the `codemods` folder (using `react-to-star-import.ts` as an example) 14 | - Optional: put the code you want to modify in the `src` folder 15 | - Run `npm start` with 1-2 command-line arguments: 16 | - First argument is the name of the codemod to run, as defined by its object key in `codemods/index.ts`. For example, `react-to-star-import`. 17 | - Second argument is optional, and is a glob string indicating which files to run the codemod against. It defaults to `"./src/**/*.{js,jsx,ts,tsx}"`. If the code you want to codemod isn't in the `src` folder, you may wish to specify a different glob; for instance, `"/home/me/my-code/src/**/*.{js,jsx,ts,tsx}"`. 18 | 19 | ## Tips 20 | 21 | - You can comment out the line in `run-codemod.ts` that calls `fs.writeFileSync` to test your codemod without affecting the files themselves; Before/After content will still be printed, but the input files won't be modified. 22 | 23 | ## Explanation of dependencies 24 | 25 | - `@babel/core`: Required peer dependency of babel packages. 26 | - `@babel/parser`: Package that can convert a string of JavaScript/TypeScript/Flow code into an Abstract Syntax Tree (AST). 27 | - `@babel/types`: Package containing AST Node builders and TypeScript types for AST Nodes. 28 | - `@types/node`: Package containing TypeScript types for node.js itself. 29 | - `chalk`: Provides functions for wrapping strings with control characters that makes them appear in different colors when they are printed to the terminal screen. 30 | - `globby`: Provides a function that resolves a glob string (eg "./\*_/_.js") into a list of files that match that glob string. 31 | - `recast`: Wraps an AST with getters/setters/proxies/etc so that it can keep track of mutations to the AST, and then re-print it later in such a way that the source formatting is unaffected except for in places where the AST was modified. 32 | - `typescript`: Type checker and autocomplete provider; installed to improve developer experience when writing codemod scripts in Visual Studio Code. 33 | - `xode`: Replacement for the `node` CLI that can parse (and ignore) TypeScript syntax. Acts as a stand-in for `ts-node`, because `ts-node` performs type checking prior to executing code, which can be annoying when running small scripts or interacting with the CLI. 34 | 35 | ## License 36 | 37 | MIT 38 | -------------------------------------------------------------------------------- /run-codemod.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import * as recast from "recast"; 3 | import * as t from "@babel/types"; 4 | import * as babelParser from "@babel/parser"; 5 | import globby from "globby"; 6 | import chalk from "chalk"; 7 | import codemods from "./codemods"; 8 | 9 | function transformFile(codemodName: string, filepath: string, source: string) { 10 | // Here, we are: 11 | // Parsing the source code string into an AST, using @babel/parser, 12 | // and wrapping that AST with a bunch of getter/setter/proxy stuff, using recast. 13 | // 14 | // Recast is a tool that wraps an AST such that it can track modifications to it, 15 | // and correlate those modifications with the location of the affected code in the input string. 16 | // Then, when you tell Recast to convert the modified AST back to code, it will not touch 17 | // the code formatting of the parts of the AST you didn't modify; it will only affect the formatting 18 | // of modified or added code, that was created by modifying the AST. 19 | // 20 | // Recast is often used in codemod tools because of this "formatting-preserving" behaviour. 21 | // However, if you use prettier on your codebase, you may not need to use Recast at all. 22 | // One other consideration when using prettier in your codebase is that the formatting for 23 | // modified/added code that Recast creates will likely not match prettier's formatting, so 24 | // you may wish to run prettier on the affected files after running a codemod. 25 | // 26 | // Because we are providing Recast with a custom parse function that uses Babel's parser, 27 | // the AST Recast gives us will be in the "@babel/parser" format, which differs slightly 28 | // from the "ESTree" spec used by Acorn, Spidermonkey, etc. The most notable difference is 29 | // that Babel's parser uses nodes like StringLiteral and NumericLiteral to represent strings 30 | // and numbers, instead of representing both with one combined "Literal" node type, as found 31 | // in the ESTree spec. In general, when working with codemods, it's good to be aware of 32 | // this different with literal nodes, because you could end up in a situation where your 33 | // code is expecting to find a StringLiteral node, but the AST contains Literal nodes 34 | // (or vice-versa). 35 | // 36 | // When using ASTExplorer.net, make sure to select "@babel/parser" as your parser. 37 | // You may also want to click the gear icon and change its options to match those specified below. 38 | // 39 | // The formal definition for @babel/parser's AST format is found here: https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md 40 | const ast: t.File = recast.parse(source, { 41 | parser: { 42 | parse(source: string) { 43 | // Specify a bunch of options for Babel so that it can parse almost anything 44 | // you're probably using in the wild. If you run into parse errors, try 45 | // following the instructions in the below comments to identify if any of 46 | // these options need to be modified. 47 | return babelParser.parse(source, { 48 | sourceFilename: filepath, 49 | allowAwaitOutsideFunction: true, 50 | allowImportExportEverywhere: true, 51 | allowReturnOutsideFunction: true, 52 | allowSuperOutsideMethod: true, 53 | allowUndeclaredExports: true, 54 | plugins: [ 55 | // If you're using Flow instead of TypeScript, comment out "typescript" 56 | // and uncomment "flow" and "flowComments". 57 | "typescript", 58 | // "flow", 59 | // "flowComments", 60 | 61 | "jsx", 62 | 63 | "asyncDoExpressions", 64 | "asyncGenerators", 65 | "bigInt", 66 | "classPrivateMethods", 67 | "classPrivateProperties", 68 | "classProperties", 69 | "classStaticBlock", 70 | "decimal", 71 | 72 | // If decorators aren't working, try switching which of these two lines is uncommented 73 | // "decorators", 74 | "decorators-legacy", 75 | 76 | "doExpressions", 77 | "dynamicImport", 78 | "exportDefaultFrom", 79 | "exportNamespaceFrom", 80 | "functionBind", 81 | "functionSent", 82 | "importAssertions", 83 | "importMeta", 84 | "logicalAssignment", 85 | "moduleBlocks", 86 | "moduleStringNames", 87 | "nullishCoalescingOperator", 88 | "numericSeparator", 89 | "objectRestSpread", 90 | "optionalCatchBinding", 91 | "optionalChaining", 92 | "partialApplication", 93 | "privateIn", 94 | "throwExpressions", 95 | "topLevelAwait", 96 | 97 | // If you're using the pipeline operator, you'll have to specify which proposal you're using. 98 | // Comment/uncomment lines below as appropriate. 99 | // If unsure, check what's in your babel config. 100 | // ["pipelineOperator", {proposal: "minimal"}], 101 | ["pipelineOperator", { proposal: "fsharp" }], 102 | // ["pipelineOperator", {proposal: "hack"}], 103 | // ["pipelineOperator", {proposal: "smart"}], 104 | ], 105 | }); 106 | }, 107 | }, 108 | }); 109 | 110 | // `ast` is a File node. We want to pass the Program node to the codemod functions instead. 111 | const program = ast.program; 112 | 113 | const codemod = codemods[codemodName]; 114 | if (!codemod) { 115 | throw new Error( 116 | `No such codemod: ${codemodName}. Valid codemod names: ${Object.keys( 117 | codemods 118 | ).join(", ")}.` 119 | ); 120 | } 121 | 122 | codemod(program, filepath, source); 123 | 124 | const outputSource = recast.print(ast).code; 125 | return outputSource; 126 | } 127 | 128 | // Run this on the CLI with one required parameter and one optional parameter. 129 | // - The required parameter is the name of the codemod. 130 | // - The optional parameter is a glob of files to run the codemod against. 131 | // If omitted, it defaults to all js, jsx, ts, and tsx files in src. 132 | function main() { 133 | // Work around xode bug; argv[2] is wrong 134 | process.argv.splice(1, 1); 135 | 136 | console.log(chalk.green("\n\n--- Starting codemod run. ---")); 137 | 138 | const codemodName = process.argv[2]; 139 | if (!codemodName) { 140 | throw new Error( 141 | `You must specify a codemod to run as the first command-line argument. Valid codemod names: ${Object.keys( 142 | codemods 143 | ).join(", ")}.` 144 | ); 145 | } 146 | 147 | const glob = process.argv[3] || "./src/**/*.{js,jsx,ts,tsx}"; 148 | 149 | console.log(chalk.blue(`Searching for files matching: ${glob}`)); 150 | const files = globby.sync(glob); 151 | 152 | console.log( 153 | chalk.yellow( 154 | `Found ${files.length} file${ 155 | files.length === 1 ? "" : "s" 156 | } to run the codemod on${files.length === 0 ? "." : ":"}` 157 | ) 158 | ); 159 | 160 | if (files.length > 0) { 161 | console.log( 162 | chalk.gray( 163 | "- " + 164 | files.slice(0, 2).join("\n- ") + 165 | (files.length > 2 ? `\n - ...and ${files.length - 2} more` : "") 166 | ) 167 | ); 168 | } 169 | 170 | for (const file of files) { 171 | console.log(chalk.cyan("\nTransforming:"), file); 172 | 173 | const source = fs.readFileSync(file, "utf-8"); 174 | console.log(chalk.magenta("--- Before: ---")); 175 | console.log(source.trim()); 176 | const newSource = transformFile(codemodName, file, source); 177 | console.log(chalk.magenta("--- After: ---")); 178 | console.log(newSource.trim()); 179 | console.log(chalk.magenta("------\n")); 180 | 181 | // Comment out the line below to not update the file. Useful for testing your codemod. 182 | fs.writeFileSync(file, newSource); 183 | } 184 | 185 | console.log(chalk.green("All done!")); 186 | } 187 | 188 | try { 189 | main(); 190 | } catch (err: any) { 191 | console.error(chalk.red("Codemod failed:")); 192 | console.error(err.stack); 193 | } 194 | --------------------------------------------------------------------------------