index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. 'use strict'
  2. const path = require('path')
  3. const Module = require('module')
  4. const resolve = require('resolve')
  5. const debug = require('debug')('require-in-the-middle')
  6. const parse = require('module-details-from-path')
  7. module.exports = Hook
  8. const builtins = Module.builtinModules
  9. const isCore = builtins
  10. ? (filename) => builtins.includes(filename)
  11. // Fallback in case `builtins` isn't available in the current Node.js
  12. // version. This isn't as acurate, as some core modules contain slashes, but
  13. // all modern versions of Node.js supports `buildins`, so it shouldn't affect
  14. // many people.
  15. : (filename) => filename.includes(path.sep) === false
  16. // 'foo/bar.js' or 'foo/bar/index.js' => 'foo/bar'
  17. const normalize = /([/\\]index)?(\.js)?$/
  18. function Hook (modules, options, onrequire) {
  19. if ((this instanceof Hook) === false) return new Hook(modules, options, onrequire)
  20. if (typeof modules === 'function') {
  21. onrequire = modules
  22. modules = null
  23. options = null
  24. } else if (typeof options === 'function') {
  25. onrequire = options
  26. options = null
  27. }
  28. if (typeof Module._resolveFilename !== 'function') {
  29. console.error('Error: Expected Module._resolveFilename to be a function (was: %s) - aborting!', typeof Module._resolveFilename)
  30. console.error('Please report this error as an issue related to Node.js %s at %s', process.version, require('./package.json').bugs.url)
  31. return
  32. }
  33. this.cache = new Map()
  34. this._unhooked = false
  35. this._origRequire = Module.prototype.require
  36. const self = this
  37. const patching = new Set()
  38. const internals = options ? options.internals === true : false
  39. const hasWhitelist = Array.isArray(modules)
  40. debug('registering require hook')
  41. this._require = Module.prototype.require = function (id) {
  42. if (self._unhooked === true) {
  43. // if the patched require function could not be removed because
  44. // someone else patched it after it was patched here, we just
  45. // abort and pass the request onwards to the original require
  46. debug('ignoring require call - module is soft-unhooked')
  47. return self._origRequire.apply(this, arguments)
  48. }
  49. const filename = Module._resolveFilename(id, this)
  50. const core = isCore(filename)
  51. let moduleName, basedir
  52. debug('processing %s module require(\'%s\'): %s', core === true ? 'core' : 'non-core', id, filename)
  53. // return known patched modules immediately
  54. if (self.cache.has(filename) === true) {
  55. debug('returning already patched cached module: %s', filename)
  56. return self.cache.get(filename)
  57. }
  58. // Check if this module has a patcher in-progress already.
  59. // Otherwise, mark this module as patching in-progress.
  60. const isPatching = patching.has(filename)
  61. if (isPatching === false) {
  62. patching.add(filename)
  63. }
  64. const exports = self._origRequire.apply(this, arguments)
  65. // If it's already patched, just return it as-is.
  66. if (isPatching === true) {
  67. debug('module is in the process of being patched already - ignoring: %s', filename)
  68. return exports
  69. }
  70. // The module has already been loaded,
  71. // so the patching mark can be cleaned up.
  72. patching.delete(filename)
  73. if (core === true) {
  74. if (hasWhitelist === true && modules.includes(filename) === false) {
  75. debug('ignoring core module not on whitelist: %s', filename)
  76. return exports // abort if module name isn't on whitelist
  77. }
  78. moduleName = filename
  79. } else if (hasWhitelist === true && modules.includes(filename)) {
  80. // whitelist includes the absolute path to the file including extension
  81. const parsedPath = path.parse(filename)
  82. moduleName = parsedPath.name
  83. basedir = parsedPath.dir
  84. } else {
  85. const stat = parse(filename)
  86. if (stat === undefined) {
  87. debug('could not parse filename: %s', filename)
  88. return exports // abort if filename could not be parsed
  89. }
  90. moduleName = stat.name
  91. basedir = stat.basedir
  92. const fullModuleName = resolveModuleName(stat)
  93. debug('resolved filename to module: %s (id: %s, resolved: %s, basedir: %s)', moduleName, id, fullModuleName, basedir)
  94. // Ex: require('foo/lib/../bar.js')
  95. // moduleName = 'foo'
  96. // fullModuleName = 'foo/bar'
  97. if (hasWhitelist === true && modules.includes(moduleName) === false) {
  98. if (modules.includes(fullModuleName) === false) return exports // abort if module name isn't on whitelist
  99. // if we get to this point, it means that we're requiring a whitelisted sub-module
  100. moduleName = fullModuleName
  101. } else {
  102. // figure out if this is the main module file, or a file inside the module
  103. let res
  104. try {
  105. res = resolve.sync(moduleName, { basedir })
  106. } catch (e) {
  107. debug('could not resolve module: %s', moduleName)
  108. return exports // abort if module could not be resolved (e.g. no main in package.json and no index.js file)
  109. }
  110. if (res !== filename) {
  111. // this is a module-internal file
  112. if (internals === true) {
  113. // use the module-relative path to the file, prefixed by original module name
  114. moduleName = moduleName + path.sep + path.relative(basedir, filename)
  115. debug('preparing to process require of internal file: %s', moduleName)
  116. } else {
  117. debug('ignoring require of non-main module file: %s', res)
  118. return exports // abort if not main module file
  119. }
  120. }
  121. }
  122. }
  123. // only call onrequire the first time a module is loaded
  124. if (self.cache.has(filename) === false) {
  125. // ensure that the cache entry is assigned a value before calling
  126. // onrequire, in case calling onrequire requires the same module.
  127. self.cache.set(filename, exports)
  128. debug('calling require hook: %s', moduleName)
  129. self.cache.set(filename, onrequire(exports, moduleName, basedir))
  130. }
  131. debug('returning module: %s', moduleName)
  132. return self.cache.get(filename)
  133. }
  134. }
  135. Hook.prototype.unhook = function () {
  136. this._unhooked = true
  137. if (this._require === Module.prototype.require) {
  138. Module.prototype.require = this._origRequire
  139. debug('unhook successful')
  140. } else {
  141. debug('unhook unsuccessful')
  142. }
  143. }
  144. function resolveModuleName (stat) {
  145. const normalizedPath = path.sep !== '/' ? stat.path.split(path.sep).join('/') : stat.path
  146. return path.posix.join(stat.name, normalizedPath).replace(normalize, '')
  147. }