cjs-mock
Version:
NodeJS module mocking for CJS (CommonJS) modules for unit testing purposes.
101 lines • 4.73 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.mock = void 0;
/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-require-imports */
const path_1 = __importDefault(require("path"));
const callsites_1 = __importDefault(require("callsites"));
const colorette_1 = require("colorette");
const Module = require('module');
const depsToMock = new Map();
function debug(msg) {
if (!process.env.CJS_MOCK_DEBUG)
return;
console.log('CJS_MOCK_DEBUG: ', msg);
}
Module.prototype.require = new Proxy(Module.prototype.require, {
apply(target, thisArg, argumentsList) {
const [name] = argumentsList;
// eslint-disable-next-line no-underscore-dangle
const absolutePath = Module._resolveFilename(name, thisArg);
const mock = depsToMock.get(absolutePath);
// Only replace if the module is a direct dependency of the caller
if (mock && mock.parentPath === thisArg.filename) {
debug(`require(): ${(0, colorette_1.green)('REPLACING WITH MOCK')} ${(0, colorette_1.bold)(name)} [${absolutePath}] ${getStackTrace()}`);
depsToMock.delete(absolutePath);
return mock.mockReturnValue;
}
debug(`require(): ${(0, colorette_1.bold)(name)} [${absolutePath}] ${getStackTrace()}`);
return Reflect.apply(target, thisArg, argumentsList);
},
});
/**
* Resolves a module path to an absolute path
* @param modulePath - The module path to resolve
* @param dir - The directory to resolve the module path in
* @param parentModule - The parent module to resolve the module path in
* @returns The absolute path of the module
*/
function resolve(modulePath, dir, parentModule) {
// if path starts with ., then it's relative
if (modulePath.slice(0, 1) === '.') {
const resolvedAbsPath = path_1.default.resolve(dir, modulePath);
return Module._resolveFilename(resolvedAbsPath, parentModule);
}
return Module._resolveFilename(modulePath, parentModule);
}
function registerDepsToReplace(mockModules, dir, parentModule, targetModulePath) {
Object.entries(mockModules).forEach((mockModule) => {
const [modulePath, mockReturnValue] = mockModule;
const absolutePath = resolve(modulePath, dir, parentModule);
debug(`will replace: ${modulePath} [${absolutePath}]`);
depsToMock.set(absolutePath, {
dependencyPath: modulePath,
mockReturnValue,
parentPath: targetModulePath, // This is the module being mocked
});
});
}
function mock(modulePath, deps = {}) {
var _a, _b;
const callerFile = (0, callsites_1.default)()[1].getFileName();
const callerModule = (_a = Object.values(Module._cache).find((mod) => mod.filename === callerFile)) !== null && _a !== void 0 ? _a : (_b = module.parent) === null || _b === void 0 ? void 0 : _b.parent; // this fallback assumes the caller is two levels up (mock.ts -> index.ts -> caller)
const dir = path_1.default.dirname(callerFile);
const absolutePath = resolve(modulePath, dir, callerModule);
const moduleDir = path_1.default.dirname(absolutePath);
debug(`mocking: ${modulePath} [${absolutePath}]`);
// Pass the absolutePath as the targetModulePath
registerDepsToReplace(deps, moduleDir, callerModule, absolutePath);
delete require.cache[absolutePath];
// require the module that we're mocking
const mod = require(absolutePath);
// make sure there are no unused mocks
if (depsToMock.size) {
throw new Error(`The following imports were not found in ${modulePath}:
${[...depsToMock.values()].map((mock) => mock.dependencyPath).join(', ')}`);
}
// make sure this is not cached either, especially as it contains mocks that we don't want to keep around
delete require.cache[absolutePath];
return mod;
}
exports.mock = mock;
function getStackTrace() {
const trace = (0, callsites_1.default)()
.slice(1)
.filter((callsite) => {
const file = callsite.getFileName();
// filter out internal, node_modules, and cjs-mock files
return file
&& !file.includes('internal')
&& !file.includes('node_modules')
&& !file.includes('cjs-mock');
})
.map((callsite) => (0, colorette_1.gray)(` at ${callsite.getFileName()} ${callsite.getLineNumber()}:${callsite.getColumnNumber()}`))
.join('\n');
return trace ? `\n${trace}` : '';
}
//# sourceMappingURL=mock.js.map