I first heard about Javascript codemods from this repo: JS codeshift. Codemods, also known as metaprogramming, are basically just bits of code that rewrites other code.
My motivation to research and implement something like this was brought on by the need to refactor several outdated Javascript application repos for a client in a limited timeframe. I knew that doing it all by hand would be arduous and mind-numbing and that there most definitely had to be a way for a computer to do most if not all of these repetitive tasks.
I'm not going to go into all of the particulars about what exactly a codemod is, or the history of them, or where they originated from and who gets the credit since it really doesn't matter. The point is, they saved me a ton of time and I am ever grateful. If you'd like to learn more about them, here are some great articles I read before I started writing some of my own:
Ok now that that's out of the way, my main motivation here was I had a bunch of older applications using Object.assign(...). I also a bunch of newer applications using spread syntax. For a while, build systems for lots of applications were being shimmed with various Babel proposals to support spread syntax or to support Object.assign in itself since the build system was ancient and the application was meant to run on IE8.
With the advent of modern build tools and updated JS standards, none of this is necessary. I spent some time making upgrades to various build systems, and now it's time for upgrades to the applications themselves.
Many JS programmers might feel there's nothing wrong with it, and it's fine to do one or the other, or both. However, my main goals are to:
Object.assign(unsafeAssignmentObj, { someVar: 'Banana'})
First off, I needed a fast way to test out codemods as I authored them, as I observed from other programmers. It looks like most people have come to rely on AST Explorer as the preferred sandbox of choice. This tool reallyed help me to figure out 1) Figure out how JS codemods work, and 2) How I test and verify them.
What you get with this are three code windows, and an explorer window. The editor windows display your current codemod, a script you wish to transform, and then a version of your modified code after being ran through the codemod.
The inspector is very useful for diving into each portion of the code, such as assignment operators, functions, variable declarations and so many more. There's an almost boundless amount of information out there you could find on how to classify every character or string of code so I'm not going to even attempt to describe it here for you. Again, I'm leaving this up to you to research.
I did a ton of research here across all of the applications since I wanted to be sure I covered every use case. I found 5 commonly occuring patterns I would need to account for with the codemod in order to replace it the spread syntax.
This is the most notorious one and one of the most annoying in my opinion. It's a perfectly valid usage of Object.assign, but it's bad practice in my opinion.
let spec = {} const var1 = 'test' Object.assign(spec, { [`prefix-${var1}`]: { check: 'isBool' } })
There were sections of code I encountered where there didn't seem to be any assignment made and the assignment wasn't used. I wanted to make sure I didn't cause a runtime error by just replacing it with a spread syntax.
Object.assign({}, { imNotUsed: 'asdasd' })
function functionToTestReturnStmt() { const obj2 = { parm1: '123' } return Object.assign({}, obj2, { parm2: 'www' } ) }
By far the most common and a pattern that is reused in other patterns.
const obj3 = { parm3: '3333' } const assignment1 = Object.assign({}, obj3, { parm4: 'asd' } )
Very similar to others but something I ended up having to account for specifically.
const obj4 = { parm5: '2222' } function function1(...args) { // Do nothing } function1('testString', Object.assign({}, obj4, { parm6: '333' } ), 'testString', {}, 123)
After doing lots of experimentation using the AST Explorer, I found the magic combination I was looking for was simply a MemberExpression with an Identifier object having a name
of "Object", and an Identifier property having a name of "assign". This would form the backbone of any codemod I would write moving forward.
I started to track some expected outputs for the inputs I listed above, and compared these to what AST Explorer would produce. I'd simply just revise my guesses if they needed it, but here's the versions I ultimately ended up with:
let spec = {} const var1 = 'test' spec = { [`prefix-${var1}`]: { check: 'isBool' } };
Object.assign({}, { imNotUsed: 'asdasd' })
function functionToTestReturnStmt() { const obj2 = { parm1: '123' } return { ...obj2, parm2: 'www' }; }
const obj3 = { parm3: '3333' } const assignment1 = { ...obj3, parm4: 'asd' }
const obj4 = { parm5: '2222' } function function1(...args) { // Do nothing } function1('testString', { ...obj4, parm6: '333' }, 'testString', {}, 123)
I initially made a codemod for each scenario, however I quickly discovered this was overkill and I just needed to identify the different patterns as AST types to appropriately target the codemod.
Now that I can target code I would like to modify, I need to craft the replacement code to be inserted. I'll also need to extract some information from the old Object.assign code to insert into the equivalent spread syntax.
Instead of a MemberExpression, I will be looking to use a basic ObjectExpression since now I'm looking to represent this as a simple Javascript object instead of some overly verbose piece of code. In order to show the spread syntax, there is a codeshift function called spreadProperty
which handles all of this for you.
It wasn't immediately obvious how all of this worked since there really isn't any great documentation around it yet. I did find some generic API docs however it left a lot to be desired in terms of instruction.
After a lot of revisions, this is what I ended up with. (I might come back to this later and progressively show how this evolved, but for now I don't feel it's necessary.)
In the codemod, I find all of the ObjectExpressions matching my requirement, and just loop through them while keeping in mind to check for certain conditions regarding assignment, or function relationships etc.
module.exports = (file, api, options) => { const j = api.jscodeshift; const printOptions = options.printOptions || { quote: "single" }; const root = j(file.source); const rmObjectAssignCall = (path) => j(path).replaceWith( j.objectExpression( path.value.arguments.reduce( (allProperties, next) => next.type === "ObjectExpression" ? [...allProperties, ...next.properties] : [...allProperties, { ...j.spreadProperty(next) }], [] ) ) ); root .find(j.CallExpression, { callee: { type: "MemberExpression", object: { name: "Object" }, property: { name: "assign" } } }) .forEach((path) => { if (path.parentPath.value.type === "ExpressionStatement") { if (path.value.arguments[0].type === "Identifier") { const identifierName = path.value.arguments[0].name; const [identifierNode, ...remNodes] = path.value.arguments; j(path).replaceWith( j.expressionStatement( j.assignmentExpression( "=", j.identifier(identifierName), j.objectExpression( remNodes.reduce( (allProperties, next) => next.type === "ObjectExpression" ? [...allProperties, ...next.properties] : [...allProperties, { ...j.spreadProperty(next) }], [] ) ) ) ) ); } } else { rmObjectAssignCall(path); } }); return root.toSource(printOptions); };
Reading through the command line docs, I was able to run my codemod in a directory and against all files recursively.
jscodeshift ./folder -t ./tools/codemod/templates.js -d -p
It would be awesome in the future to be able to unit these against the input files I created and match them to expected outputs I now maintain. I imagine the best way to do this is either matching their ASTs using some sort of deep compare, or maybe some flavor of a textual diff tool. I'll keep using this tool and will probably have a good followup regarding this in another codemod post.