Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to create a "fresh and empty" Node.js JavaScript context within the same process?

Question for Node.js and v8 experts.

I'm developing a new version of the Siesta testing tool.

By default, Siesta runs every test in the newly created Node.js process. However, I'd like to avoid the overhead of spawning a new process and instead provide the ability to run the test in the empty JavaScript context.

Such context can be created with the built-in vm module. However, the context created in this way is an empty JavaScript context, not an empty Node.js context. For example, it does not have global variable process:

> require('vm').runInNewContext('process')
evalmachine.<anonymous>:1
process
^

Uncaught ReferenceError: process is not defined
    at evalmachine.<anonymous>:1:1
    at Script.runInContext (vm.js:143:18)
    at Script.runInNewContext (vm.js:148:17)
    at Object.runInNewContext (vm.js:303:38)
    at REPL30:1:15
    at Script.runInThisContext (vm.js:133:18)
    at REPLServer.defaultEval (repl.js:484:29)
    at bound (domain.js:413:15)
    at REPLServer.runBound [as eval] (domain.js:424:12)
    at REPLServer.onLine (repl.js:817:10)
> 

So question is - what is the best way to create a fresh and empty Node.js context within the same process? I'd expect such context to have all regular globals, like process, require etc. Plus, I'd expect such context to have a separate and initially empty modules cache, so that even if some module is loaded in the main context, it will be loaded again in the new context.

Of course I could map the globals from the main context to the new context, but that would mean those globals are shared between contexts, and I'm aiming for context isolation. Plus the modules cache will be shared as well.

I believe the difference between JavaScript and Node.js context is that the latter is initialized with a certain script. Is it possible to obtain the sources of that script somehow and execute it in the new context?

Thank you!

like image 907
CanonicEpicure Avatar asked Feb 10 '21 15:02

CanonicEpicure


1 Answers

This is what happens when NodeJS loads a new module:

(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});

They use what they call a Module Wrapper

So you would have to do something similar using VM

require('vm').runInNewContext('the code you are running', {
  module:  // your empty module or a wrapper
  exports: // a reference to the module.exports
  require: // your empty require or a wapper
  __filename: // your __filename
  __dirname: // your __dirname
  process
})

I was successful in creating valid new empty require functions after debugging the internals of the Module Class/Function

If you create these 2 following files and run the isolated.js you'll see that it is indeed reloading every time and at the same time the original cache stays intact (although I believe async executions could have unexpected results)

// isolated.js
const Module = require('module')
const vm = require('vm')
const path = require('path')

// this is just to show it wont load it again
const print = require('./print')
print('main file')

const getNewContext = () => {
  const mod = new Module()
  const filename = path.join(__dirname, `test-filename-${Math.random().toString().substr(-6)}.js`)
  const req = Module.createRequire(filename)
  req.cache = Object.create(null)

  return {
    module: mod, // your empty module or a wrapper
    exports: mod.exports, // a reference to the module.exports
    require: req, // your empty require or a wapper
    // require: mod.require, // your empty require or a wapper
    __filename: filename,
    __dirname: path.dirname(filename),
    console,
    process
  }
}

// print('runningInNewContext')
const jsCode = `
// What?
const log = console.log.bind(null, Date.now())
log({from:'print', cache: require.cache})
const print = require('./print.js')
log(print.toString())
print('evaluated code')
log({from:'print', cache: require.cache})
`

function customRunInNewContext (jsCode, context) {
  const { _cache } = Module

  Module._cache = Object.create(null)
  vm.runInNewContext(jsCode, context)
  Module._cache = _cache
}

customRunInNewContext(jsCode, getNewContext())
customRunInNewContext(jsCode, getNewContext())
// print.js
const log = console.log.bind(null, 'PRINT loaded at', new Date().toISOString(), module.parent.filename)
process.stdout.write('\n ====> loading print module\n\n')

module.exports = function print (...args) {
  log(...args)
}

When you run

node ./isolated.js

You should see the message ====> loading print module multiple times

like image 83
Mestre San Avatar answered Oct 18 '22 00:10

Mestre San