text: '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 && new Set(Module.builtinModules)
  
  const isCore = builtins
    ? (filename) => builtins.has(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 {
        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, '')
  }
  