123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- 'use strict'
- const path = require('path')
- const Module = require('module')
- const resolve = require('resolve')
- const debug = require('debug')('require-in-the-middle')
- const parse = require('module-details-from-path')
- module.exports = Hook
- const builtins = Module.builtinModules
- const isCore = builtins
- ? (filename) => builtins.includes(filename)
- // Fallback in case `builtins` isn't available in the current Node.js
- // version. This isn't as acurate, as some core modules contain slashes, but
- // all modern versions of Node.js supports `buildins`, so it shouldn't affect
- // many people.
- : (filename) => filename.includes(path.sep) === false
- // 'foo/bar.js' or 'foo/bar/index.js' => 'foo/bar'
- const normalize = /([/\\]index)?(\.js)?$/
- function Hook (modules, options, onrequire) {
- if ((this instanceof Hook) === false) return new Hook(modules, options, onrequire)
- if (typeof modules === 'function') {
- onrequire = modules
- modules = null
- options = null
- } else if (typeof options === 'function') {
- onrequire = options
- options = null
- }
- if (typeof Module._resolveFilename !== 'function') {
- console.error('Error: Expected Module._resolveFilename to be a function (was: %s) - aborting!', typeof Module._resolveFilename)
- console.error('Please report this error as an issue related to Node.js %s at %s', process.version, require('./package.json').bugs.url)
- return
- }
- this.cache = new Map()
- this._unhooked = false
- this._origRequire = Module.prototype.require
- const self = this
- const patching = new Set()
- const internals = options ? options.internals === true : false
- const hasWhitelist = Array.isArray(modules)
- debug('registering require hook')
- this._require = Module.prototype.require = function (id) {
- if (self._unhooked === true) {
- // if the patched require function could not be removed because
- // someone else patched it after it was patched here, we just
- // abort and pass the request onwards to the original require
- debug('ignoring require call - module is soft-unhooked')
- return self._origRequire.apply(this, arguments)
- }
- const filename = Module._resolveFilename(id, this)
- const core = isCore(filename)
- let moduleName, basedir
- debug('processing %s module require(\'%s\'): %s', core === true ? 'core' : 'non-core', id, filename)
- // return known patched modules immediately
- if (self.cache.has(filename) === true) {
- debug('returning already patched cached module: %s', filename)
- return self.cache.get(filename)
- }
- // Check if this module has a patcher in-progress already.
- // Otherwise, mark this module as patching in-progress.
- const isPatching = patching.has(filename)
- if (isPatching === false) {
- patching.add(filename)
- }
- const exports = self._origRequire.apply(this, arguments)
- // If it's already patched, just return it as-is.
- if (isPatching === true) {
- debug('module is in the process of being patched already - ignoring: %s', filename)
- return exports
- }
- // The module has already been loaded,
- // so the patching mark can be cleaned up.
- patching.delete(filename)
- if (core === true) {
- if (hasWhitelist === true && modules.includes(filename) === false) {
- debug('ignoring core module not on whitelist: %s', filename)
- return exports // abort if module name isn't on whitelist
- }
- moduleName = filename
- } else if (hasWhitelist === true && modules.includes(filename)) {
- // whitelist includes the absolute path to the file including extension
- const parsedPath = path.parse(filename)
- moduleName = parsedPath.name
- basedir = parsedPath.dir
- } else {
- const stat = parse(filename)
- if (stat === undefined) {
- debug('could not parse filename: %s', filename)
- return exports // abort if filename could not be parsed
- }
- moduleName = stat.name
- basedir = stat.basedir
- const fullModuleName = resolveModuleName(stat)
- debug('resolved filename to module: %s (id: %s, resolved: %s, basedir: %s)', moduleName, id, fullModuleName, basedir)
- // Ex: require('foo/lib/../bar.js')
- // moduleName = 'foo'
- // fullModuleName = 'foo/bar'
- if (hasWhitelist === true && modules.includes(moduleName) === false) {
- if (modules.includes(fullModuleName) === false) return exports // abort if module name isn't on whitelist
- // if we get to this point, it means that we're requiring a whitelisted sub-module
- moduleName = fullModuleName
- } else {
- // figure out if this is the main module file, or a file inside the module
- let res
- try {
- res = resolve.sync(moduleName, { basedir })
- } catch (e) {
- debug('could not resolve module: %s', moduleName)
- return exports // abort if module could not be resolved (e.g. no main in package.json and no index.js file)
- }
- if (res !== filename) {
- // this is a module-internal file
- if (internals === true) {
- // use the module-relative path to the file, prefixed by original module name
- moduleName = moduleName + path.sep + path.relative(basedir, filename)
- debug('preparing to process require of internal file: %s', moduleName)
- } else {
- debug('ignoring require of non-main module file: %s', res)
- return exports // abort if not main module file
- }
- }
- }
- }
- // only call onrequire the first time a module is loaded
- if (self.cache.has(filename) === false) {
- // ensure that the cache entry is assigned a value before calling
- // onrequire, in case calling onrequire requires the same module.
- self.cache.set(filename, exports)
- debug('calling require hook: %s', moduleName)
- self.cache.set(filename, onrequire(exports, moduleName, basedir))
- }
- debug('returning module: %s', moduleName)
- return self.cache.get(filename)
- }
- }
- Hook.prototype.unhook = function () {
- this._unhooked = true
- if (this._require === Module.prototype.require) {
- Module.prototype.require = this._origRequire
- debug('unhook successful')
- } else {
- debug('unhook unsuccessful')
- }
- }
- function resolveModuleName (stat) {
- const normalizedPath = path.sep !== '/' ? stat.path.split(path.sep).join('/') : stat.path
- return path.posix.join(stat.name, normalizedPath).replace(normalize, '')
- }
|