Building a require directive

If you have ever used node.js you’re probably familiar with the require function which essentially loads a module into the current namespace. Until recently I was under the impression that was some sort of built-in for the language, a bit like new or Object. It turns out however that require is a ‘real’ function and something we can build ourselves!

Setting the scene

One of the aims of modules is to keep code neatly (?) organised and manageable. That is, we don’t want to pollute the global namespace. There are a couple of ways to achieve that - one of the most common ones being to use objects:

> var foo = {}
undefined
> foo.sayHello = function() {console.log("Hello");}
[Function]
> foo.sayHello()
Hello

Functionality is associated with the foo object. However that may not provide as much encapsulation as we’d like and everything is exposed:

> foo.secretRatio = 1.5
1.5
> foo.range = function(mult) { return this.secretRatio * mult; }
[Function]
> foo.range(2)
3
> foo.secretRatio = 2
2
> foo.range(2)
4

We can do better though by leveraging JavaScript’s Function constructor:

> var foo2 = new Function("", `const secretRatio=1.5;return {range: function(mult) {return secretRatio * mult;}}`)();
undefined
> Object.keys(foo2)
[ 'range' ]
> Object.keys(foo)
[ 'sayHello', 'secretRatio', 'range' ]
> foo2.range(2)
3

Note that the 2nd argument to Function was nothing more than a string containing our code - which, really, is what a module file actually is.

require, first cut

With this in mind it should be possible to write a thin wrapper that reads a file and shoves its content in the body of a Function object:

var fs = require('fs');
function myRequire(path) {
  const c = fs.readFileSync(path, 'utf8'); // we should handle errors!
  var modFun = new Function("", c);
  return modFun();
}

var foo3 = myRequire('foo3.js');
console.log(foo3.range(2));

Note we’re cheating a little by leveraging the fs module - in the browser you’d probably use the FileReader API. If we were writing an interpreter from scratch that’d probably be part of the built-in functionality (otherwise it’s a bit of a chicken and egg problem).

Improving

The above wasn’t particlularly clever - it loads modules alright, but subsequent calls will load them again. We can address this by exposing a single global variable which will store all our modules and act as a cache:

function myRequire(path) {
  if (path in myRequire._cache) return myRequire._cache[path];

  const moduleCode = fs.readFileSync(path, 'utf8'); // we should handle errors!
  var exports = {};
  var moduleFunction = new Function("exports", moduleCode); // `exports` will be made available inside `moduleCode`
  moduleFunction(exports);
  myRequire._cache[path] = exports;
  return exports;
}
myRequire._cache = {};

Wouldn’t it be cool if we could reload modules automatically? It turns out we can do just that with a few modifications. We’ll introduce a reload argument to bypass the cache and leverage fs.watch to trigger reloads:

function myRequire(path, reload=false) {

  var exports = {};
  if (path in myRequire._cache) {
    if (reload) exports = myRequire._cache[path];
    else return myRequire._cache[path];
  }

  const moduleCode = fs.readFileSync(path, 'utf8'); // we should handle errors!
  var moduleFunction = new Function("exports", moduleCode); // `exports` will be made available inside `moduleCode`
  moduleFunction(exports);
  myRequire._cache[path] = exports;

  fs.watch(path, (et, fname) => { console.log('change detected'); myRequire(path, true);})

  return exports;
}

Taking it for a spin:

> var f = myRequire('/tmp/foo.js')
undefined
> f.x
4
// modify /tmp/foo.js and set x equal to 3
> change detected
> f.x
3

This works because myRequire returns a reference to the exports object. As long as we don’t redefine it the caller will point to the updated version. Cool heh?

Taking it further

Dependency management and lazy loading are beyond the scope of this short post but well worth looking into. I strongly recommend reading chapter 10 of Marijn Haverbeke’s excellent Eloquent JavaScript, who does an amazing job at breaking this down.