context-require
Version:
Allows you to require files in a custom vm context.
196 lines (195 loc) • 6.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextModule = void 0;
const Module = require("module");
const vm = require("vm");
const path = require("path");
let moduleId = 0;
const isBuiltin = Module.isBuiltin ||
((id) => Module.builtinModules.indexOf(id) !== -1);
const runScriptOptions = {
displayErrors: true,
};
/**
* Patch nodejs module system to support context,
* compilation and module resolution overrides.
*/
const originalLoad = Module._load;
const originalResolve = Module._resolveFilename;
const originalCompile = Module.prototype._compile;
const originalProtoLoad = Module.prototype.load;
Module._load = loadFile;
Module._resolveFilename = resolveFileHook;
Module.prototype._compile = compileHook;
Module.prototype.load = protoLoad;
// Expose module.
module.exports = exports = createContextRequire;
exports.default = createContextRequire;
class ContextModule extends Module {
/**
* Custom nodejs Module implementation which uses a provided
* resolver, require hooks, and context.
*/
constructor({ dir, context, resolve, extensions }) {
const filename = path.join(dir, `index.${moduleId++}.ctx`);
super(filename);
this.filename = filename;
this._context = context;
this._resolve = resolve;
this._hooks = extensions;
this._cache = {};
this._relativeResolveCache = {};
if (!vm.isContext(context) &&
typeof context.runVMScript !== "function" &&
typeof context.getInternalVMContext !== "function") {
vm.createContext(context);
}
}
}
exports.ContextModule = ContextModule;
/**
* Creates a custom Module object which runs all required scripts in a provided vm context.
*/
function createContextRequire(options) {
const module = new ContextModule(options);
return createRequire(module, module);
}
/**
* Use custom require cache for context modules
*
* @param request The file to resolve.
* @param parentModule The module requiring this file.
* @param isMain
*/
function loadFile(request, parentModule, isMain) {
const canLoadInContext = parentModule && !isBuiltin(request);
const contextModule = canLoadInContext && findNearestContextModule(parentModule);
if (!contextModule) {
return originalLoad(request, parentModule, isMain);
}
const cached = contextModule._cache[resolveFileHook(request, parentModule)];
if (cached) {
if (parentModule.children.indexOf(cached) === -1) {
parentModule.children.push(cached);
}
return cached.exports;
}
const previousCache = Module._cache;
Module._cache = contextModule._cache;
try {
return originalLoad(request, parentModule, isMain);
}
finally {
Module._cache = previousCache;
}
}
/**
* Hijack native file resolution using closest custom resolver.
*
* @param request The file to resolve.
* @param parentModule The module requiring this file.
*/
function resolveFileHook(request, parentModule) {
const canLoadInContext = parentModule && Module.builtinModules.indexOf(request) === -1;
const contextModule = canLoadInContext && findNearestContextModule(parentModule);
if (contextModule) {
const resolver = contextModule._resolve;
if (resolver) {
// Normalize paths for custom resolvers.
const dir = path.dirname(parentModule.filename);
if (path.isAbsolute(request)) {
request = path.relative(dir, request);
if (request[0] !== ".") {
request = "./" + request;
}
}
const relResolveCacheKey = `${dir}\x00${request}`;
return (contextModule._relativeResolveCache[relResolveCacheKey] ||
(contextModule._relativeResolveCache[relResolveCacheKey] = resolver(dir, request, parentModule)));
}
else {
return originalResolve(request, parentModule);
}
}
return originalResolve(request, parentModule);
}
/**
* Patch module.load to use the context's custom extensions if provided.
*
* @param filename
*/
function protoLoad(filename) {
const contextModule = findNearestContextModule(this);
if (contextModule) {
const extensions = contextModule._hooks;
const ext = path.extname(filename);
const compiler = extensions && extensions[ext];
if (compiler) {
const originalCompiler = Module._extensions[ext];
Module._extensions[ext] = compiler;
try {
return originalProtoLoad.apply(this, arguments);
}
finally {
Module._extensions[ext] = originalCompiler;
}
}
}
return originalProtoLoad.apply(this, arguments);
}
/**
* This overrides script compilation to ensure the nearest context module is used.
*
* @param content The file contents of the script.
* @param filename The filename for the script.
*/
function compileHook(content, filename) {
const contextModule = findNearestContextModule(this);
if (contextModule) {
const context = contextModule._context;
const script = new vm.Script(Module.wrap(content), {
filename,
lineOffset: 0,
});
return runScript(context, script).call(this.exports, this.exports, createRequire(this, contextModule), this, filename, path.dirname(filename));
}
return originalCompile.apply(this, arguments);
}
/**
* Walks up a module tree to find the nearest context module.
*
* @param cur The starting module.
*/
function findNearestContextModule(cur) {
do {
if (cur instanceof ContextModule) {
return cur;
}
} while (Boolean((cur = cur.parent)));
}
/**
* Helper which will run a vm script in a context.
* Special case for JSDOM where `runVMScript` is used.
*
* @param context The vm context to run the script in (or a jsdom instance).
* @param script The vm script to run.
*/
function runScript(context, script) {
return context.runVMScript
? context.runVMScript(script, runScriptOptions)
: script.runInContext(context.getInternalVMContext ? context.getInternalVMContext() : context, runScriptOptions);
}
/**
* Creates a require function bound to a module
* and adds a `resolve` function and `cache` object the same as nodejs.
*
* @param module The module to create a require function for.
*/
function createRequire(module, context) {
const req = module.require.bind(module);
req.resolve = (request) => resolveFileHook(request, module);
req.main = require.main;
req.cache = context._cache;
req.extensions = context._hooks || require.extensions;
return req;
}