A Require for ClearScript

ClearScript is the C# scripting engine that can execute JavaScript inside a C# context. It’s way smarter than that description suggests. But it does not provide a require (think ‘code module loader’) capability. So how about we make one…

Quick shout out to Michele Nasti who wrote an article explaining the require process – I based the JS side of my require on his code in that blog. He in turn wrote his article to better understand the explanation of this function at chapter 10 (Modules) of the very excellent book Eloquent Javascript.

What is the problem we are trying to solve?

The issue is that ClearScript (CS from now on) can execute code that you have to pass it in a variable. All file loading, script composition, etc, has to be done in your C#.

But what about if your project runs into tens or hundreds of component JS files that you need to keep separate for (a) sanity and (b) because you only want the CS engine to have to parse & handle the code required at any given point.

That implies some kind of process where an initial script can ‘call in’ or ‘require’ other script files to be loaded. I might have a request handler – doThing.js – that needs the services of utility and customer classes stored in utility.js & customer.js. But I might have hundreds of such classes and I only need to load those two right now. My initial doThing code needs to somehow trigger the loading of those two others from their respective files. I could load and concatenate all three of them into one large script string and give that to CS. However, that would mean some kind of special machanism to specify that utils & customer should be loaded for doThing to run. What we need is a JS solution.

What will the solution look like in JS?

What we need to do is to create a ‘require’ handler then use it somewhere in doThing.js,

function doThing(){

require('/utils/utils.js');
require('/classes/customer.js');
...

}

Now let’s look at what the require function looks like and understand it.

function myRequire(name) {   
    log(`myRequire: You require file ${name}`)
    if (!(name in myRequire.cache)) {
        log(`myRequire: ${name} is not in cache; reading from disk`)
        let code = muApi.require(name); // load the code      
        let module = {exports: {}};
        myRequire.cache[name] = module;     
        let wrapper = Function("require, exports, module", code);     
        wrapper(myRequire, module.exports, module);
    }
    log(`myRequire: ${name} is in cache. Returning it...`)
    return myRequire.cache[name].exports;
}

Ignore the log() lines and also the cache at the moment – this is just a way of ensuring we don’t repeatedly load the same source code. The trick here is at line 5 where the muApi.require() function is called. Remember this a C# project and muApi is a C# object passed into the CS context at initialisation time. – this is a capability of CS.

What this means is that the JS code executing in CS can call allowed functions in C#. So I built a require function in the C# object that does the file handling to load and return the JS source code for the given file. You could adapt this approach to execution methods other than CS – that just happens to be my target today. If you do then this line will be where you do the file handling to get the file you want to load. Node.js function readFileSync() could be an option, for example.

We are back to pure JS at line 6 where we create an empty module object with a single ‘exports’ property and stuff it into the cache at line 7. It’s a dumb object right now which we will update in a moment.

Here’s the magic…At line 8 we use the JS ‘Function’ constructor, which takes two strings – one being the function args and the other a string with the function code, to create a function object called ‘wrapper’. What this means is that we are creating a new function variable which will expect the arguments ‘require’, ‘exports’, and ‘module’. We then execute that function on the next line, passing the require function (so that we can use require in the loaded module), the exports property of the module, and the module code.

What happens in that instant is that the source code we loaded gets executed creating the ‘module’ object. And any properties in its ‘exports’ object become available, including variables and functions.

Lets see it working

I’m going to create instances of two simple objects – utils and customer – via the require approach. Each has a single method called ‘say’ which will log a passed-in message. The code is in this CodePen if you want to experiment.

The utils.js file looks like this

  let utils = function (){
    this.say = function(x){
      log('utils says = ' + x)
      };
      return this;
  }
  exports.utils= utils; // Important to assign to exports so it can be seen!

and a customer.js file looks like this

  let customer = function (){
    this.say = function(x){
      log('Customer: says = ' + x)
      };
      return this;
  }
  exports.customer = customer; // Important to assign to exports so it can be seen!

We can then get a utils and customer object with the following code

// Require c1 will create an object with exports.
let util = new myRequire('c1').utils();
util.say('I am alive!')
 
log("");

// Require c2 will create an object with exports.
let cust = new myRequire('c2').customer();
cust.say('I am alive too!')

Giving the log trace

myRequire: You require file c1
myRequire: c1 is not in cache; reading from disk
myRequire: c1 is in cache. Returning it...
utils: says = I am alive!

myRequire: You require file c2
myRequire: c2 is not in cache; reading from disk
myRequire: c2 is in cache. Returning it...
Customer: says = I am alive too!

Summary

This simple require process will let us load code from any JS file we can reach, giving the ability for us to work with a modular approach which will (a) keep us sane, and (b) only have to load the code we need and so keep code handling overheads to a minimum.

There is a clear overhead in that we need to use the ‘exports’ variable in the required source code in order to publish the internals of the loaded source to the code that required it. However, in my view, all modular processes have rules of this type to follow both to enable the process and to define scope, and this is a reasonable time-cost for the benefits of modularisation.

Thanks for reading.

VW June 2021.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: