UNPKG

d2-ui

Version:
272 lines (226 loc) 8.2 kB
'use strict'; /*jshint laxbreak:true, loopfunc:true*/ var path = require('path') , Module = require('module') , resolve = require('resolve') , dirname = require('path').dirname , ProxyquireError = require('./proxyquire-error') , is = require('./is') , assert = require('assert') , fillMissingKeys = require('fill-keys') , moduleNotFoundError = require('module-not-found-error') , hasOwnProperty = Object.prototype.hasOwnProperty ; function validateArguments(request, stubs) { var msg = (function getMessage() { if (!request) return 'Missing argument: "request". Need it to resolve desired module.'; if (!stubs) return 'Missing argument: "stubs". If no stubbing is needed, use regular require instead.'; if (!is.String(request)) return 'Invalid argument: "request". Needs to be a requirable string that is the module to load.'; if (!is.Object(stubs)) return 'Invalid argument: "stubs". Needs to be an object containing overrides e.g., {"path": { extname: function () { ... } } }.'; })(); if (msg) throw new ProxyquireError(msg); } function Proxyquire(parent) { var self = this , fn = self.load.bind(self) , proto = Proxyquire.prototype ; this._parent = parent; this._preserveCache = true; Object.keys(proto) .forEach(function (key) { if (is.Function(proto[key])) fn[key] = proto[key].bind(self); }); self.fn = fn; return fn; } /** * Disables call thru, which determines if keys of original modules will be used * when they weren't stubbed out. * @name noCallThru * @function * @private * @return {object} The proxyquire function to allow chaining */ Proxyquire.prototype.noCallThru = function () { this._noCallThru = true; return this.fn; }; /** * Enables call thru, which determines if keys of original modules will be used * when they weren't stubbed out. * @name callThru * @function * @private * @return {object} The proxyquire function to allow chaining */ Proxyquire.prototype.callThru = function () { this._noCallThru = false; return this.fn; }; /** * Will make proxyquire remove the requested modules from the `require.cache` in order to force * them to be reloaded the next time they are proxyquired. * This behavior differs from the way nodejs `require` works, but for some tests this maybe useful. * * @name noPreserveCache * @function * @private * @return {object} The proxyquire function to allow chaining */ Proxyquire.prototype.noPreserveCache = function() { this._preserveCache = false; return this.fn; }; /** * Restores proxyquire caching behavior to match the one of nodejs `require` * * @name preserveCache * @function * @private * @return {object} The proxyquire function to allow chaining */ Proxyquire.prototype.preserveCache = function() { this._preserveCache = true; return this.fn; }; /** * Loads a module using the given stubs instead of their normally resolved required modules. * @param request The requirable module path to load. * @param stubs The stubs to use. e.g., { "path": { extname: function () { ... } } } * @return {*} A newly resolved module with the given stubs. */ Proxyquire.prototype.load = function (request, stubs) { validateArguments(request, stubs); // Find out if any of the passed stubs are global overrides for (var key in stubs) { var stub = stubs[key]; if (stub === null) continue; if (typeof stub === 'undefined') { throw new ProxyquireError('Invalid stub: "' + key + '" cannot be undefined'); } if (hasOwnProperty.call(stub, '@global')) { this._containsGlobal = true; } if (hasOwnProperty.call(stub, '@runtimeGlobal')) { this._containsGlobal = true; this._containsRuntimeGlobal = true; } } // Ignore the module cache when return the requested module return this._withoutCache(this._parent, stubs, request, this._parent.require.bind(this._parent, request)); }; // This replaces a module's require function Proxyquire.prototype._require = function(module, stubs, path) { assert(typeof path === 'string', 'path must be a string'); assert(path, 'missing path'); if (hasOwnProperty.call(stubs, path)) { var stub = stubs[path]; if (stub === null) { // Mimic the module-not-found exception thrown by node.js. throw moduleNotFoundError(path); } if (hasOwnProperty.call(stub, '@noCallThru') ? !stub['@noCallThru'] : !this._noCallThru) { fillMissingKeys(stub, Module._load(path, module)); } // We are top level or this stub is marked as global if (module.parent == this._parent || hasOwnProperty.call(stub, '@global') || hasOwnProperty.call(stub, '@runtimeGlobal')) { return stub; } } // Only ignore the cache if we have global stubs if (this._containsRuntimeGlobal) { return this._withoutCache(module, stubs, path, Module._load.bind(Module, path, module)); } else { return Module._load(path, module); } }; Proxyquire.prototype._withoutCache = function(module, stubs, path, func) { // Temporarily disable the cache - either per-module or globally if we have global stubs var restoreCache = this._disableCache(module, path); // Override all require extension handlers var restoreExtensionHandlers = this._overrideExtensionHandlers(module, stubs); try { // Execute the function that needs the module cache disabled return func(); } finally { // Restore the cache if we are preserving it if (this._preserveCache) { restoreCache(); } else { var id = Module._resolveFilename(path, module); var stubIds = Object.keys(stubs).map(function (stubPath) { try { return resolve.sync(stubPath, { basedir: dirname(id), extensions: Object.keys(require.extensions), paths: Module.globalPaths }) } catch (_) {} }); var ids = [id].concat(stubIds.filter(Boolean)); ids.forEach(function (id) { delete require.cache[id]; }); } // Finally restore the original extension handlers restoreExtensionHandlers(); } }; Proxyquire.prototype._disableCache = function(module, path) { if (this._containsGlobal) { // empty the require cache because if we are stubbing C but requiring A, // and if A requires B and B requires C, then B and C might be cached already // and we'll never get the chance to return our stub return this._disableGlobalCache(); } // Temporarily delete the SUT from the require cache return this._disableModuleCache(path, module); }; Proxyquire.prototype._disableGlobalCache = function() { var cache = require.cache; require.cache = Module._cache = {}; // Return a function that will undo what we just did return function() { require.cache = Module._cache = cache; }; }; Proxyquire.prototype._disableModuleCache = function(path, module) { // Find the ID (location) of the SUT, relative to the parent var id = Module._resolveFilename(path, module); var cached = Module._cache[id]; delete Module._cache[id]; // Return a function that will undo what we just did return function() { if (cached) { Module._cache[id] = cached; } }; }; Proxyquire.prototype._overrideExtensionHandlers = function(module, stubs) { var originalExtensions = {}; var self = this; Object.keys(require.extensions).forEach(function(extension) { // Store the original so we can restore it later if (!originalExtensions[extension]) { originalExtensions[extension] = require.extensions[extension]; } // Override the default handler for the requested file extension require.extensions[extension] = function(module, filename) { // Override the require method for this module module.require = self._require.bind(self, module, stubs); return originalExtensions[extension](module, filename); }; }); // Return a function that will undo what we just did return function() { Object.keys(originalExtensions).forEach(function(extension) { require.extensions[extension] = originalExtensions[extension]; }); }; }; module.exports = Proxyquire;