Yash Ladha Blog

Runtime Polymorphism in NodeJS


Node.js is a very popular runtime for developing javascript applications for any layer, be it frontend or backend. In this blog we will focus on how can we exploit the interpretted nature of runtime and plug custom logic in already executing code.

Have you wondered that the code we had wrote, does it gurantee that it cannot be changed outside the scope of main running application.

To better understand this scenario let’s take an example of framework X which is a very popular backend framework, and has a great plugin system. So I have gone ahead and created a plugin Y for my use-case.

Now the plugin Y which I wrote, can it change the code that is being executed in main framework X (which is using that plugin)?

Can we somehow change the code that is being executed at runtime (specially in nodejs)? 🤔

As a developer we always have the mindset that the code execution (source code which is being executed) cannot be modified until and unless explicitly written in the source itself. This might not always be true in the case of Node.js.

Let’s first identify the internals, to be precise where actually the code lies when we are executing it using node binary.

We import the modules and dependencies using require notation in cjs (Common js) format, also hold’s true for EJS format. When we run the application using node <some_file_name> it resolves all the dependencies and loads them in the memory.

How can we access that data structure in memory from our code, where the source code actually lies?

Node loads all the modules in a data structure called modules cache which can be accessed using module API.

Let’s understand this with the help of an example:

  1. Say we have created this following JS file and execute using node.
const os = require('os');
console.log(os.homedir());
  1. We will be gretted with the following output.
❯ node
Welcome to Node.js v12.22.7.
Type ".help" for more information.
> os.homedir()
'/Users/yashLadha'
  1. If we change the code in just a slight manner.
const os = require('os');
console.log(os.homedir());
console.log(module);  // Logging the moduleParentCache also on the console.
  1. Only change in the output from previous code will addition of this new log line.
...
Module {
  id: '.',
  path: '/private/tmp',
  exports: {},
  parent: null,
  filename: '/private/tmp/demo.js',
  loaded: false,
  children: [],
  paths: [
    '/private/tmp/node_modules',
    '/private/node_modules',
    '/node_modules'
  ]
}

The above module structure is generated at this place in nodejs source code.

function Module(id = '', parent) {
  this.id = id;
  this.path = path.dirname(id);
  this.exports = {};
  moduleParentCache.set(this, parent);
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

We can observe certain terminologies, like children, parent which suggests that it might be some tree-structure.

This was a very simple example, now let’s consider an example where we are including dependencies in the file.

Let’s create a file superAwesomeLib.js which will export a function.

const awesomeFn = () => console.log('Awesome fn');

module.exports = {
  awesomeFn
};

Now let’s include this library in our previous demo.js file and see the output when executed.

const os = require('os');
const awesome = require('./superAwesomeLib');

console.log(os.homedir());
console.log(module);

Output of the above program when executed is as follows:

/tmp 29s
❯ node demo.js
/Users/yashLadha
Module {
  id: '.',
  path: '/private/tmp',
  exports: {},
  parent: null,
  filename: '/private/tmp/demo.js',
  loaded: false,
  children: [
    Module {
      id: '/private/tmp/superAwesomeLib.js',
      path: '/private/tmp',
      exports: [Object],
      parent: [Circular],
      filename: '/private/tmp/superAwesomeLib.js',
      loaded: true,
      children: [],
      paths: [Array]
    }
  ],
  paths: [
    '/private/tmp/node_modules',
    '/private/node_modules',
    '/node_modules'
  ]
}

You can see now children key is not empty and contains one element of type Module. That module is the representation of the required library.

One more thing to note is that, it only contains user imported code and not contains the default module. This is because we do not update the children in case of native modules as per the implementation in node source code.

Again coming back to Module object we see that there is a key of exports which contains array of object. This is the same export which we use to export functions from the current scope in nodejs.

On printing that exports object on console we can clearly see that.

{ awesomeFn: [Function: awesomeFn] }

What does this means?

This means that we have an object, which stores a key that points to the actual function in memory. To verify this lets check the type of the key awesomeFn and execute it (in case of function type).

const fn = module.children[0].exports.awesomeFn;
if (typeof fn === 'function') {
    fn();
}

Output of the above program is as follows:

Awesome fn

🎉 Awesome!! so how we change the code that is being executed in awesomeFn() function?

Easy, as everything in JS is an Object, and we can set the properties of an object or replace existing one’s very easily. So if we update the value of the awesomeFn key in exports, theoretically the reference to that key should also gets changed.

Let’s try this in code.

module.children[0].exports.awesomeFn = () => console.log('Code is updated');

const fn = module.children[0].exports.awesomeFn;
if (typeof fn === 'function') {
    fn();
}

What should be the output???

Code is updated

🎉 We changed the code of an import at the runtime.

Practical application of this thing is very subjective, be its use in mocking libraries or chaning the behavior of some internal library as a lazy hack. Regardless, I got to learn lot of new things while experimenting with this and hope you will also take some nifty hacks from this blog.

Next: Personal Dev Environment using Docker