I am Hack Sparrow
Captain of the Internets.

Scripting a Node.js App

What in the world could "Scripting a Node.js App" mean?

Well, we are talking about the ability to write scripts for your Node.js apps. Wouldn't it be cool to be able to write independent JS scripts and execute them on the fly, in your app? This post is about that ability.

When I first encountered the Node.js vm (Virtual Machine) module, I thought, "Hmmm, this thing looks like eval(). How different is it from eval() and what could it be used for?".

Then one day, I thought of making a scriptable Node.js app. eval() came to my mind, but was bothered by its security concerns. Then I revisited our old friend vm, and discovered that it was a hidden gem:

1. eval() has access to all the objects your script has access to, it executes in the context of your main app.
2. vm will execute the code in its own context, it does not have access to the context of your main app.

While eval() can execute a piece of JavaScript, it also has the potential of completely screwing up your main app. vm can execute a piece of JavaScript too, but does it in an environment of its own, with an optional capability to access global objects. Therefore, vm looks like the right choice for implementing scripting capability in a Node.js app.

Let's create an example of a scriptable Node.js app.

File: app.js

var vm = require('vm');
var fs = require('fs');

// load the script file
var script_code = fs.readFileSync('./script.js');

var name = 'Sparrow';
var age = 195;
var pets = [{"name": "Kiddo", "species": "cat"}, {"name": "Kaka", "species":"crow"}, {"name": "Evul", "species": "monkey"}];

// this object will be passed to the vm context, the keys will become available as global variables in the context
var sandbox = { "module": module, "name": name, "age": age, "pets": pets, "result": false };
// create a script out of the loaded string - the script is compiled
var script = vm.createScript(script_code);
// execute the script in the context of `sandbox`, which becomes its global object
script.runInNewContext(sandbox);

console.log(sandbox.result);

File: script.js

// this script will do interesting stuff only if the name is 'Sparrow'
if (name == 'Sparrow') {
result = 'Captain ' + name + ' has ' + pets.length + ' pet' + (pets.length? 's' : '') + ': ';
pets.forEach(function(pet) {
result += pet.name + ' - the ' + pet.species + ', ';
});

result = result.replace(/, $/, '.');
}
else {
result = 'Invalid User';
}

Execute the app and see the script in action:

$ node app
Captain Sparrow has 3 pets: Kiddo - the cat, Kaka - the crow, Evul - the monkey.

You might be surprised that the variable name and others are available in the script by default. That is because we passed in the sandbox object to script.runInNewContext(), which becomes the global object for the script:

var sandbox = { "module": module, "name": name, "age": age, "pets": pets, "result": false };
...
script.runInNewContext(sandbox);

Although vm executes the code in a sandboxed context, and does not have access to the parent app's context, we can send in objects from the parent app to the sandbox via the sandbox object. The names of the properties of the sandbox object are converted to live objects in the vm context. We also included a property named result, where we want the script to write its return value to.

Now let's modify the script and play around a bit:

File: script.js

console.log('HAI');

Now when you execute app.js, you will be hit by this error:

vm.js:41
return ns[f].apply(ns, arguments);
^
ReferenceError: console is not defined
at undefined:2:1
at Script.Object.keys.forEach.(anonymous function) [as runInNewContext] (vm.js:41:22)
at Object. (/Users/captain/vmtest/app.js:13:8)
at Module._compile (module.js:449:26)
at Object.Module._extensions..js (module.js:467:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.runMain (module.js:492:10)
at process.startup.processNextTick.process._tickCallback (node.js:244:9)

When vm encounters an error, it throws and brings down the whole Node process. We can fix that by modifying app.js, a little bit.

Change:

...
script.runInNewContext(sandbox);
...

To:

...
try {
script.runInNewContext(sandbox);
}
catch(error) {
console.log('ERROR: ' + error);
}
...

Now, when you execute app.js, you should see:

ERROR: ReferenceError: console is not defined
false

Wondering why the script is complaining, "console is not defined"? That's because, vm executes in a pure, unenhanced JavaScript environment of its own, it does not have access to any of the Node.js objects like require, module, global, console, etc. The fact that vm's environment is free from all Node.js objects, and can't access its parent app's objects, is what makes it safer than eval().

According to Node.js docs, vm is currently still unstable because of various reasons. But it should not deter you from being creative with it, and unless you are trying to do very complex things with vm, I would say it is stable enough for simple scripting on a Linux machine.

There is slightly more to vm than what I have covered here, I recommend you hit the vm doc.

i-herd-you-liek-script

One Response to “Scripting a Node.js App”

  1. karan says:

    thanks captain. very useful for my project.

Make a Comment