DEV Community

Eduardo Maciel for Entria

Posted on • Originally published at github.com

Migrating our scripts to Node.js v16 using codemod

At Entria we have a lot of Node.js scripts to automate common tasks
we also make a script for any migration and to make it easy to run a single migration for test purposes and each script starts with anonymous async functions that is auto executed.
Like this template

const run = async () => {
   //migration code
}

(async () => {
  try {
    await run();

  } catch (err) {
    // eslint-disable-next-line
    console.log('err: ', err);
    process.exit(1);
  }

  process.exit(0);
})();
Enter fullscreen mode Exit fullscreen mode

This works well, but not enough for our use case. Because we create tests for our scripts and migrations if they are used in production.

If you import the run function in your test files it will run the async functions in your tests, which is not the behavior you wanna. So we have a check async function that is only auto executed when we are running directly.

To make this check, we used module.parent propriety, but it will be deprecated on Node v16.

Context of Node v14 and v16

In October 26th, 2021 Node.js v16 replaced v14 as the LTS release.
And with these changes we on Entria bring to us breaking changes into our codebase on Entria, like a module.parent.

module.parent has used on Node v14 to locate if script is a module or executable, like:

if (!module.parent) {
   // running like `node myfile.js`
} else {
   // running like require('./myfile.js')
}
Enter fullscreen mode Exit fullscreen mode

We had 206 files that use module.parent

And we want changes all occurrences of module.parent to require.main, that allows we check the same thing of module.parent.

if (require.main === module) {
   /// running like `node myfile.js`
} else {
   // running like require('./myfile.js')
}
Enter fullscreen mode Exit fullscreen mode

To change all occurrences of module.parent we used a codemod, with jscodeshift. Codemod is a tool/library to assist our with large-scale codebase refactors that can be partially automated.

But Eduardo, why you not use find and replace of your IDE?

R: Because this require a lot of attention and time of our developers, and if we not used codemod can't sure that can exists more module.parent on the future.

Time of code

We want change

if(!module.parent) {

}
Enter fullscreen mode Exit fullscreen mode

to

if(require.main === module){

}
Enter fullscreen mode Exit fullscreen mode

How ?

We used jscodeshift to codemod.

First you should add jscodeshift in your node_modules with

npm install -g jscodeshift
Enter fullscreen mode Exit fullscreen mode

After this, you should create an archive that contains our codemod, in this case, replace-module-parent.js.

First, we should create a function that is used in all files of the folder that we select, and pass two arguments, fileInfo and api.

The fileInfo argument represents information of the currently processed file, and api is object that exposes the jscodeshift library and helper functions from the runner.

// replace-module-parent.js
function transform (fileInfo, api) {

};

module.exports = transform;
Enter fullscreen mode Exit fullscreen mode

Now we want to get jscodeshift helpers, from api.jscodeshift and transform our code to AST (Abstract System Types).
And you can explore more of our AST here AST Explorer.

const j = api.jscodeshift;

const root = j(fileInfo.source)
Enter fullscreen mode Exit fullscreen mode

Now, we want find all occurrences of if(!module.parent), and replace to if(require.main === module)

// finding all ocurrences of if(!module.parent)
root
  .find(j.IfStatement, {
      type : 'IfStatement',
      test : {
        type : 'UnaryExpression',
        operator : '!',
        argument : {
          type: 'MemberExpression',
          object: {
            type: 'Identifier',
            name: 'module'
          },
          property: {
            type: 'Identifier',
            name: 'parent'
        }
      }
    }
  })
  .filter((path) => {
    if (path.node.test.type !== 'UnaryExpression') {
      return false;
    }
    return true;
  })
Enter fullscreen mode Exit fullscreen mode

Replacing all to require.main

.forEach((path) => {
    const requireMain = j.ifStatement(
      j.binaryExpression(
        '===',
        j.memberExpression(
          j.identifier('require'),
          j.identifier('main')
        ),
        j.identifier('module')
      ),
      path.node.consequent,
      path.node.alternate
    )
    j(path).replaceWith(requireMain)
  });
  return root.toSource();
Enter fullscreen mode Exit fullscreen mode

And on final our codemod is

function transform (fileInfo, api) {
  const j = api.jscodeshift;

  const root = j(fileInfo.source)

  root
  .find(j.IfStatement, {
      type : 'IfStatement',
      test : {
        type : 'UnaryExpression',
        operator : '!',
        argument : {
          type: 'MemberExpression',
          object: {
            type: 'Identifier',
            name: 'module'
          },
          property: {
            type: 'Identifier',
            name: 'parent'
        }
      }
    }
  })
  .filter((path) => {
    if (path.node.test.type !== 'UnaryExpression') {
      return false;
    }
    return true;
  })
  .forEach((path) => {
    const requireMain = j.ifStatement(
      j.binaryExpression(
        '===',
        j.memberExpression(
          j.identifier('require'),
          j.identifier('main')
        ),
        j.identifier('module')
      ),
      path.node.consequent,
      path.node.alternate
    )
    j(path).replaceWith(requireMain)
  });
  return root.toSource();
};

module.exports = transform;
module.exports.parser = 'ts';
Enter fullscreen mode Exit fullscreen mode

To run this code you can use this on your terminal:

jscodeshift -t replace-module-parent.js [your-input-files] -d -p
Enter fullscreen mode Exit fullscreen mode

Top comments (0)