Search code examples
node.jsmodulesetterdefineproperty

Cross platform module system


Edit: In interest of trying to figure out a solution, I edited the post to explain more clearly of what I'm trying to accomplish.

I am trying to re-invent the wheel, with minimal amount of code to create a cross platform async module loading system

This should ideally work on any ES5 runtime engine, however the primary targets are node.js and browser.


What I'm trying to accomplish is creating a global object with a setter, from which the object being set is the module contents. Node.js accomplishes this with module.exports = {} and I'm trying to replicate this behavior.

The issue i'm having is interesting because the global setter does not create a 1:1 mapping of the module filename and the exported object.


First attempt:

So far I've tried binding a setter to be specific to a particular function call. It's always resorting to the last module being loaded. I thought by wrapping the setter in a closure, it would keep the module parameters in the call stack but I was mistaken - since the setter changes.


An improved solution but not quite there yet:

I've also tried to use the name property defined in the exported object to create this mapping, but has proven to be ineffective and easy to circumvent. I.E. by exporting a name that is not true to what it does, and can intentionally or unintentionally overwrite other modules in the system.


Here's some example code:

let exporter = {}
global.exporter = exporter

const imports = function(module, callback) {
  return new (function(module, callback) {
    Object.defineProperty(exporter, 'exports', {
      enumerable: false,
      configurable: true,
      set: function(exportFile) {
        console.log('Setting export file:', exportFile.name, ':', module)
        callback(exportFile)
      },
    })

    console.log('loading module: ', module)
    require(module)
  })(module, callback)
}

Using setter in module file:

exporter.exports = {
  name: 'File1',
}

Example code that uses the new import.

function load(name) {
  imports(__dirname + '/modules/' + name, function(exportFile) {
    console.log('Module loaded: ', exportFile.name)
  })
}

load('1') // instant
load('2') // 2 second timeout
load('3') // 1 second timeout

Output:

loading module:  .../modules/1
Setting export file: File1 : .../modules/1
Module loaded:  File1
loading module:  .../modules/2
loading module:  .../modules/3
Setting export file: File3 : .../modules/3
Module loaded:  File3
Setting export file: File2 : .../modules/3
Module loaded:  File2


I appreciate any help that could fix this context issue!

I am also open to any other suggestions to accomplish this same task, without using anything node-specific, as I plan to make this cross platform compatible.


Solution

  • What I'm trying to accomplish is creating a global object with a setter, from which the object being set is the module contents. Node.js accomplishes this with module.exports = {} and I'm trying to replicate this behavior.

    Your problem is that you are indeed using a global object. Since the modules are loading asynchronously, the global object may be in an incorrect state when a module executes. There may be a way to reset the global object after your require call so that your specific example works fine, but there are cases that it won't cover, and you'll be playing whack-a-mole with bugs for a long time.

    Although module looks like a global object, it is in fact an object created anew for each module. The documentation is explicit about this:

    [Node.js] helps to provide some global-looking variables that are actually specific to the module, such as:

    • The module and exports objects that the implementor can use to export values from the module.
    • The convenience variables __filename and __dirname, containing the module's absolute filename and directory path.

    Providing your modules with independent objects to modify will make the code much simpler overall.

    Above the part of the documentation I cited above, you find:

    Before a module's code is executed, Node.js will wrap it with a function wrapper that looks like the following:

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

    You could take a page from it and have a wrapper like:

    (function (exporter, ...) {
    // Module code here...
    });
    

    Here's an illustration:

        const source = `
        exporter.exports = {
          SomeVar: "Some Value",
        };
        `;
    
        function wrapInFunction(source) {
          return `(function (exporter) { ${source} })`;
        }
    
        const exporter = {
          exports: {},
        };
    
        eval(wrapInFunction(source))(exporter);
        console.log(exporter);
    

    A note on the use of eval here. You've probably heard "eval is evil". That's true, as far as it goes. The saying is there to remind people that const x = /* value from some user input */; eval('table.' + x ); is both unnecessary (since you can do table[x]) and dangerous because the user input is evaluated raw and you do not trust the user input to run arbitrary code. A user would set x to something that does nefarious things. Using eval is still warranted in some cases, like the case here. In a browser you could avoid eval by shoving the source into a script and listening to the load event but you've gained nothing security-wise. Then again that's platform-specific. If you are in Node.js you can use the vm module, but it comes with this disclaimer "The vm module is not a security mechanism. Do not use it to run untrusted code." And it is also platform-specific.


    By the way, your current code is not cross-platform. Your code depends on the require call, which is available only on some platforms. (It is notably not present on browsers without loading additional modules.) I suspect you put that there as a placeholder for functionality to be developed later but I thought I'd mention it nonetheless since cross-platform support is one of your goals.