jscodeshift, run programmatically

jscodeshift is a great tool for running migrations on frontend codebase. It saves days of team syncup, cross team collaboration and prioritization,
my team is looking at building a tool to automatically upgrade applications that consume our libraries, instead of giving an upgrade doc and deadlines.

the jscodeshift is a command line interface and works the same way as yeoman’s yo command.
The transform script is just an argument.

While this is making it fairly simple for people working in the same environment, I cannot ask every team to install jscodeshift as a pre-requisite (some developers don’t even know what node.js is).
So I was wondering if it was possible to use jscodeshift as a javascript object, I tried to google that on stackoverflow and in multiple areas but found nothing.

So I decided to dig into the jscodeshift library by myself, to see if I could reuse some modules.

The fact that I am writing that blog post is itself a confirmation.
Here is the piece of – typescript – code that can invoke multiple jscodeshift transforms :

export async function migrateCode(rootDir: string) {
const sources: { [filename: string]: string } = {}
const outputs: { [filename: string]: string } = {}
const transformsByPattern = Object.entries(migrations).flatMap(([name, migration]) =>
migration.patterns.map(pattern => ({ [pattern]: { name, transform: migration.transform } }))
).reduce((acc: { [key: string]: { name: string, transform: Transform }[] }, value) => {
const mergedTransforms = Object.fromEntries(Object.entries(value).map(([key, oneValue]) => [key, […(acc[key] ?? []), oneValue]]));
return { …acc, …mergedTransforms };
}, {});
for (const transformsForOnePattern of Object.entries(transformsByPattern)) {
const [pattern, transforms] = transformsForOnePattern;
const filesetWithIgnoredFiles = await glob(`${rootDir}/${pattern}`, { dot: true });
const fileset = filesetWithIgnoredFiles.filter(filename =>
!filename.includes(‘node_modules’) &&
!filename.endsWith(‘package-lock.json’));
for (const filename of fileset) {
console.log(filename)
const source = outputs[filename] ?? (await fs.readFile(filename)).toString();
sources[filename] = source;
outputs[filename] = outputs[filename] ?? source;
const javascriptSource = filename.endsWith(‘.json’) ? `module.exports = ${source}` : source;
const javascriptFilename = filename.endsWith(‘.json’) ? filename.replace(/\.json$/, ‘.js’) : filename;
const output = transforms.reduce((acc, { name, transform }) => {
try {
return transform({
path: javascriptFilename,
source: acc
},
{
j: jscodeshift,
jscodeshift,
stats: () => { },
report: () => { }
},
{}) as string
} catch (e) {
console.error(`Transform ${name} has failed for file ${filename}`)
console.error(e.stack);
return acc;
}
}, javascriptSource);
outputs[filename] = filename.endsWith(‘.json’) ? output.replace(/module.exports\s*=\s*/, ) : output;
}
}

for (const output of Object.entries(outputs)) {
const [filename, content] = output;
await fs.writeFile(filename, content);
}
}

You will need a migrations object, which is a

{ transform: import('jscodeshift').Transform, patterns: string[]}[]

patterns is a list of glob matchers.
With this function you are able to build a tool that can automatically chain multiple migrations in your codebase.
If you combine the tool with a ContinuousIntegration (CI) script that creates a pull request automatically, all you have to do for a migration to complete is to… press a button.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.