rewire
Version:
Easy dependency injection for node.js unit testing
104 lines (89 loc) • 3.98 kB
JavaScript
;
var Module = require("module"),
pirates = require("pirates"),
eslint = require("eslint");
var moduleWrapper0 = Module.wrapper[0],
moduleWrapper1 = Module.wrapper[1],
linter = new eslint.Linter(),
eslintOptions = {
languageOptions: {
ecmaVersion: 6,
parserOptions: {
ecmaFeatures: {
globalReturn: true,
jsx: true,
experimentalObjectRestSpread: true
},
},
},
rules: {
"no-const-assign": 2
}
},
// The following regular expression is used to replace const declarations with let.
// This regex replacement is not 100% safe because transforming JavaScript requires an actual parser.
// However, parsing (e.g. via babel) comes with its own problems because now the parser needs to
// be aware of syntax extensions which might not be supported by the parser, but the underlying
// JavaScript engine. In fact, rewire used to have babel in place here but required an extra
// transform for the object spread operator (check out commit d9a81c0cdacf6995b24d205b4a2068adbd8b34ff
// or see https://github.com/jhnns/rewire/pull/128). It was also notable slower
// (see https://github.com/jhnns/rewire/issues/132).
// There is another issue: replacing const with let is not safe because of their different behavior.
// That's why we also have ESLint in place which tries to identify this error case.
// There is one edge case though: when a new syntax is used *and* a const re-assignment happens,
// rewire would compile happily in this situation but the actual code wouldn't work.
// However, since most projects have a seperate linting step which catches these const re-assignment
// errors anyway, it's probably still a reasonable trade-off.
// Test the regular expresssion at https://regex101.com/r/dvnZPv/2 and also check out testLib/constModule.js.
matchConst = /(^|\s|\}|;)const(\/\*|\s|{)/gm,
// Required for importing modules with shebang declarations, since NodeJS 12.16.0
shebang = /^#!.+/,
nodeRequire,
currentModule;
function load(targetModule) {
nodeRequire = targetModule.require;
targetModule.require = requireProxy;
currentModule = targetModule;
var restoreExtensions = pirates.addHook(patchSources, { extensions: ['.js', '.cjs', '.mjs', '.ts']});
targetModule.load(targetModule.id);
restoreExtensions();
// This is only necessary if nothing has been required within the module
reset();
}
function reset() {
Module.wrapper[0] = moduleWrapper0;
Module.wrapper[1] = moduleWrapper1;
}
function inject(prelude, appendix) {
Module.wrapper[0] = moduleWrapper0 + prelude;
Module.wrapper[1] = appendix + moduleWrapper1;
}
/**
* Proxies the first require call in order to draw back all changes to the Module.wrapper.
* Thus our changes don't influence other modules
*
* @param {!String} path
*/
function requireProxy(path) {
reset();
currentModule.require = nodeRequire;
return nodeRequire.call(currentModule, path); // node's require only works when "this" points to the module
}
function isNoConstAssignMessage(message) {
return message.ruleId === "no-const-assign";
}
function patchSources(content, filename) {
var noConstAssignMessage = linter.verify(content, eslintOptions).find(isNoConstAssignMessage);
var line;
var column;
if (noConstAssignMessage !== undefined) {
line = noConstAssignMessage.line;
column = noConstAssignMessage.column;
throw new TypeError(`Assignment to constant variable at ${filename}:${line}:${column}`);
}
return content
.replace(shebang, '') // Remove shebang declarations
.replace(matchConst, "$1let $2"); // replace const with let, while maintaining the column width
}
exports.load = load;
exports.inject = inject;