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.
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
andexports
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.