feat(@embark/core): Improve VM

Improve support for external requires. Allows external requires to be called like so:
```
Embark.events.on('runcode:ready', () => {
    Embark.events.emit('runcode:register', 'generateClass', require('eth-contract-class'), false);

    Embark.registerCustomContractGenerator(contract => {
      return `
        ${contract.className} = generateClass(${JSON.stringify(contract.abiDefinition)}, '${contract.code}');
        ${contract.className}Instance = new ${contract.className}(web3, '${contract.deployedAddress}');
        `;
    });
  });
  Embark.events.once('contracts:deploy:afterAll', () => {
    Embark.events.request('runcode:eval', 'SimpleStorageInstance', (err, SimpleStorageInstance) => {
      if(err) return console.error(err);
      SimpleStorageInstance.get().then((result) => {
        console.log(`=====> SimpleStorageInstance.get(): ${result}`);
      });
    });
  });
```

* Add `runcode:ready` that is fired when the VM is ready to accept registration of variables.
* Add support for registration of ES6 modules
* Add callback to `registerVar` and `setupNodeVm` in the `VM` class.
* Add support for retaining modified sandbox values across new VM instances.
* Add VM unit tests for external reaquries and modified sandbox state.
This commit is contained in:
emizzle 2019-01-16 16:34:28 +11:00 committed by Eric Mastro
parent df872fdd5b
commit c1a5bfee3c
5 changed files with 88 additions and 10 deletions

View File

@ -32,10 +32,12 @@ class CodeRunner {
}
}
}, this.logger);
this.registerIpcEvents();
this.IpcClientListen();
this.registerEvents();
this.registerCommands();
this.events.emit('runcode:ready');
}
registerIpcEvents() {
@ -74,12 +76,12 @@ class CodeRunner {
this.events.setCommandHandler('runcode:eval', this.evalCode.bind(this));
}
registerVar(varName, code, toRecord = true) {
registerVar(varName, code, toRecord = true, cb = () => {}) {
if (this.ipc.isServer() && toRecord) {
this.commands.push({varName, code});
this.ipc.broadcast("runcode:newCommand", {varName, code});
}
this.vm.registerVar(varName, code);
this.vm.registerVar(varName, code, cb);
}
async evalCode(code, cb, isNotUserInput = false, tolerateError = false) {

View File

@ -1,9 +1,10 @@
import { each } from "async";
import { NodeVM, NodeVMOptions } from "vm2";
import { Callback } from "../../../../typings/callbacks";
import { Logger } from "../../../../typings/logger";
const fs = require("../../fs");
const { recursiveMerge } = require("../../../utils/utils");
const { recursiveMerge, isEs6Module } = require("../../../utils/utils");
const Utils = require("../../../utils/utils");
const WEB3_INVALID_RESPONSE_ERROR: string = "Invalid JSON RPC response";
@ -49,7 +50,7 @@ class VM {
constructor(options: NodeVMOptions, private logger: Logger) {
this.options = recursiveMerge(this.options, options);
this.setupNodeVm();
this.setupNodeVm(() => { });
}
/**
@ -108,12 +109,33 @@ class VM {
* @param {String} varName Name of the variable to register.
* @param {any} code Value of the variable to register.
*/
public registerVar(varName: string, code: any) {
public registerVar(varName: string, code: any, cb: Callback<null>) {
// Disallow `eval` and `require`, just in case.
if (code === eval || code === require) { return; }
// handle ES6 modules
if (isEs6Module(code)) {
code = code.default;
}
this.updateState((_err) => {
this.options.sandbox[varName] = code;
this.setupNodeVm();
this.setupNodeVm(cb);
});
}
private updateState(cb: Callback<null>) {
if (!this.vm) { return cb(); }
// update sandbox state from VM
each(Object.keys(this.options.sandbox), (sandboxVar: string, next: Callback<null>) => {
this.doEval(sandboxVar, false, (err, result) => {
if (!err) {
this.options.sandbox[sandboxVar] = result;
}
next(err);
});
}, cb);
}
/**
@ -121,8 +143,9 @@ class VM {
* @type {VM} class is instantiated. The "sandbox" member of the options can be modified
* by calling @function {registerVar}.
*/
private setupNodeVm() {
private setupNodeVm(cb: Callback<null>) {
this.vm = new NodeVM(this.options);
cb();
}
/**

View File

@ -90,7 +90,7 @@ Plugin.prototype.loadPlugin = function() {
if (this.shouldInterceptLogs) {
this.setUpLogger();
}
if (typeof this.pluginModule === 'object' && typeof this.pluginModule.default === 'function' && this.pluginModule.__esModule){
if (utils.isEs6Module(this.pluginModule)) {
this.pluginModule = this.pluginModule.default;
return new this.pluginModule(this);
}

View File

@ -620,6 +620,10 @@ function toposort(graph) {
return toposortGraph(graph);
}
function isEs6Module(module) {
return typeof module === 'object' && typeof module.default === 'function' && module.__esModule;
}
module.exports = {
joinPath,
dirname,
@ -669,5 +673,6 @@ module.exports = {
fuzzySearch,
jsonFunctionReplacer,
getWindowSize,
toposort
toposort,
isEs6Module
};

View File

@ -33,4 +33,52 @@ describe('embark.vm', function () {
});
});
});
describe('#registerVar', function () {
it('should be able to evaluate code on a registered variable', function (done) {
vm.registerVar('success', true, () => {
vm.doEval('success', false, (err, result) => {
expect(err).to.be.null;
expect(result).to.be.equal(true);
done();
});
});
});
it('should be able to access a required module that was registered as a variable', function (done) {
vm.registerVar('externalRequire', (module.exports = () => { return "success"; }), () => {
vm.doEval('externalRequire()', false, (err, result) => {
expect(err).to.be.null;
expect(result).to.be.equal('success');
done();
});
});
});
it('should be able to access a required ES6 module that was registered as a variable', function (done) {
const es6Module = {
default: () => { return "es6"; },
__esModule: true
};
vm.registerVar('externalRequireES6', es6Module, () => {
vm.doEval('externalRequireES6()', false, (err, result) => {
expect(err).to.be.null;
expect(result).to.be.equal("es6");
done();
});
});
});
it('should be able to access changed state', function (done) {
vm.registerVar('one', 1, () => {
vm.doEval('one += 1; one;', false, (err1, result1) => {
expect(err1).to.be.null;
expect(result1).to.be.equal(2);
vm.registerVar('x', 'x', () => { // instantiates new VM, but should save state
vm.doEval('one', false, (err2, result2) => {
expect(err2).to.be.null;
expect(result2).to.be.equal(2);
done();
});
});
});
});
});
});
});