Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
202 lines
6.2 KiB
Plaintext
202 lines
6.2 KiB
Plaintext
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2.0 License.
|
|
//
|
|
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
|
|
|
|
const path = require('path')
|
|
const moduleDetailsFromPath = require('module-details-from-path')
|
|
const { fileURLToPath } = require('url')
|
|
const { MessageChannel } = require('worker_threads')
|
|
|
|
let { isBuiltin } = require('module')
|
|
if (!isBuiltin) {
|
|
isBuiltin = () => true
|
|
}
|
|
|
|
const {
|
|
importHooks,
|
|
specifiers,
|
|
toHook
|
|
} = require('./lib/register')
|
|
|
|
function addHook (hook) {
|
|
importHooks.push(hook)
|
|
toHook.forEach(([name, namespace, specifier]) => hook(name, namespace, specifier))
|
|
}
|
|
|
|
function removeHook (hook) {
|
|
const index = importHooks.indexOf(hook)
|
|
if (index > -1) {
|
|
importHooks.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
function callHookFn (hookFn, namespace, name, baseDir) {
|
|
const newDefault = hookFn(namespace, name, baseDir)
|
|
if (newDefault && newDefault !== namespace) {
|
|
// Only ESM modules that actually export `default` can have it reassigned.
|
|
// Some hooks return a value unconditionally; avoid crashing when the module
|
|
// has no default export (see issue #188).
|
|
if ('default' in namespace) {
|
|
namespace.default = newDefault
|
|
}
|
|
}
|
|
}
|
|
|
|
let sendModulesToLoader
|
|
|
|
/**
|
|
* EXPERIMENTAL
|
|
* This feature is experimental and may change in minor versions.
|
|
* **NOTE** This feature is incompatible with the {internals: true} Hook option.
|
|
*
|
|
* Creates a message channel with a port that can be used to add hooks to the
|
|
* list of exclusively included modules.
|
|
*
|
|
* This can be used to only wrap modules that are Hook'ed, however modules need
|
|
* to be hooked before they are imported.
|
|
*
|
|
* ```ts
|
|
* import { register } from 'module'
|
|
* import { Hook, createAddHookMessageChannel } from 'import-in-the-middle'
|
|
*
|
|
* const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel()
|
|
*
|
|
* register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions)
|
|
*
|
|
* Hook(['fs'], (exported, name, baseDir) => {
|
|
* // Instrument the fs module
|
|
* })
|
|
*
|
|
* // Ensure that the loader has acknowledged all the modules
|
|
* // before we allow execution to continue
|
|
* await waitForAllMessagesAcknowledged()
|
|
* ```
|
|
*/
|
|
function createAddHookMessageChannel () {
|
|
const { port1, port2 } = new MessageChannel()
|
|
let pendingAckCount = 0
|
|
let resolveFn
|
|
|
|
sendModulesToLoader = (modules) => {
|
|
pendingAckCount++
|
|
port1.postMessage(modules)
|
|
}
|
|
|
|
port1.on('message', () => {
|
|
pendingAckCount--
|
|
|
|
if (resolveFn && pendingAckCount <= 0) {
|
|
resolveFn()
|
|
}
|
|
}).unref()
|
|
|
|
function waitForAllMessagesAcknowledged () {
|
|
// This timer is to prevent the process from exiting with code 13:
|
|
// 13: Unsettled Top-Level Await.
|
|
const timer = setInterval(() => { }, 1000)
|
|
const promise = new Promise((resolve) => {
|
|
resolveFn = resolve
|
|
}).then(() => { clearInterval(timer) })
|
|
|
|
if (pendingAckCount === 0) {
|
|
resolveFn()
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
const addHookMessagePort = port2
|
|
const registerOptions = { data: { addHookMessagePort, include: [] }, transferList: [addHookMessagePort] }
|
|
|
|
return { registerOptions, addHookMessagePort, waitForAllMessagesAcknowledged }
|
|
}
|
|
|
|
function Hook (modules, options, hookFn) {
|
|
if ((this instanceof Hook) === false) return new Hook(modules, options, hookFn)
|
|
if (typeof modules === 'function') {
|
|
hookFn = modules
|
|
modules = null
|
|
options = null
|
|
} else if (typeof options === 'function') {
|
|
hookFn = options
|
|
options = null
|
|
}
|
|
const internals = options ? options.internals === true : false
|
|
|
|
if (sendModulesToLoader && Array.isArray(modules)) {
|
|
sendModulesToLoader(modules)
|
|
}
|
|
|
|
this._iitmHook = (name, namespace, specifier) => {
|
|
const loadUrl = name
|
|
const isNodeUrl = loadUrl.startsWith('node:')
|
|
let filePath, baseDir
|
|
|
|
if (isNodeUrl) {
|
|
// Normalize builtin module name to *not* have 'node:' prefix, unless
|
|
// required, as it is for 'node:test' and some others. `module.isBuiltin`
|
|
// is available in all Node.js versions that have node:-only modules.
|
|
const unprefixed = name.slice(5)
|
|
if (isBuiltin(unprefixed)) {
|
|
name = unprefixed
|
|
}
|
|
} else if (loadUrl.startsWith('file://')) {
|
|
const stackTraceLimit = Error.stackTraceLimit
|
|
Error.stackTraceLimit = 0
|
|
try {
|
|
filePath = fileURLToPath(name)
|
|
name = filePath
|
|
} catch (e) {}
|
|
Error.stackTraceLimit = stackTraceLimit
|
|
|
|
if (filePath) {
|
|
const details = moduleDetailsFromPath(filePath)
|
|
if (details) {
|
|
name = details.name
|
|
baseDir = details.basedir
|
|
}
|
|
}
|
|
}
|
|
|
|
if (modules) {
|
|
for (const matchArg of modules) {
|
|
if (filePath && matchArg === filePath) {
|
|
// abspath match
|
|
callHookFn(hookFn, namespace, filePath, undefined)
|
|
} else if (matchArg === name) {
|
|
if (!baseDir) {
|
|
// built-in module (or unexpected non file:// name?)
|
|
callHookFn(hookFn, namespace, name, baseDir)
|
|
} else if (baseDir.endsWith(specifiers.get(loadUrl))) {
|
|
// An import of the top-level module (e.g. `import 'ioredis'`).
|
|
// Note: Slight behaviour difference from RITM. RITM uses
|
|
// `require.resolve(name)` to see if filename is the module
|
|
// main file, which will catch `require('ioredis/built/index.js')`.
|
|
// The check here will not catch `import 'ioredis/built/index.js'`.
|
|
callHookFn(hookFn, namespace, name, baseDir)
|
|
} else if (internals) {
|
|
const internalPath = name + path.sep + path.relative(baseDir, filePath)
|
|
callHookFn(hookFn, namespace, internalPath, baseDir)
|
|
}
|
|
} else if (matchArg === specifier) {
|
|
callHookFn(hookFn, namespace, specifier, baseDir)
|
|
}
|
|
}
|
|
} else {
|
|
callHookFn(hookFn, namespace, name, baseDir)
|
|
}
|
|
}
|
|
|
|
addHook(this._iitmHook)
|
|
}
|
|
|
|
Hook.prototype.unhook = function () {
|
|
removeHook(this._iitmHook)
|
|
}
|
|
|
|
module.exports = Hook
|
|
module.exports.Hook = Hook
|
|
module.exports.addHook = addHook
|
|
module.exports.removeHook = removeHook
|
|
module.exports.createAddHookMessageChannel = createAddHookMessageChannel
|