UNPKG

context-require

Version:

Allows you to require files in a custom vm context.

196 lines (195 loc) 6.8 kB
"use strict"; 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; }