Repository URL to install this package:
|
Version:
0.1.10 ▾
|
odigos
/
etc
/
odigos-vmagent
/
instrumentations
/
nodejs-ebpf
/
node_modules
/
import-in-the-middle
/
hook.js
|
|---|
// 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 { URL } = require('url')
const { inspect } = require('util')
const { builtinModules } = require('module')
const specifiers = new Map()
const isWin = process.platform === 'win32'
// FIXME: Typescript extensions are added temporarily until we find a better
// way of supporting arbitrary extensions
const EXTENSION_RE = /\.(js|mjs|cjs|ts|mts|cts)$/
const NODE_VERSION = process.versions.node.split('.')
const NODE_MAJOR = Number(NODE_VERSION[0])
const NODE_MINOR = Number(NODE_VERSION[1])
let entrypoint
let getExports
if (NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)) {
getExports = require('./lib/get-exports.js')
} else {
getExports = (url) => import(url).then(Object.keys)
}
function hasIitm (url) {
try {
return new URL(url).searchParams.has('iitm')
} catch {
return false
}
}
function isIitm (url, meta) {
return url === meta.url || url === meta.url.replace('hook.mjs', 'hook.js')
}
function deleteIitm (url) {
let resultUrl
try {
const urlObj = new URL(url)
if (urlObj.searchParams.has('iitm')) {
urlObj.searchParams.delete('iitm')
resultUrl = urlObj.href
if (resultUrl.startsWith('file:node:')) {
resultUrl = resultUrl.replace('file:', '')
}
if (resultUrl.startsWith('file:///node:')) {
resultUrl = resultUrl.replace('file:///', '')
}
} else {
resultUrl = urlObj.href
}
} catch {
resultUrl = url
}
return resultUrl
}
function isNodeMajor16AndMinor17OrGreater () {
return NODE_MAJOR === 16 && NODE_MINOR >= 17
}
function isFileProtocol (urlObj) {
return urlObj.protocol === 'file:'
}
function isNodeProtocol (urlObj) {
return urlObj.protocol === 'node:'
}
function needsToAddFileProtocol (urlObj) {
if (NODE_MAJOR === 17) {
return !isFileProtocol(urlObj)
}
if (isNodeMajor16AndMinor17OrGreater()) {
return !isFileProtocol(urlObj) && !isNodeProtocol(urlObj)
}
return !isFileProtocol(urlObj) && NODE_MAJOR < 18
}
/**
* Determines if a specifier represents an export all ESM line.
* Note that the expected `line` isn't 100% valid ESM. It is derived
* from the `getExports` function wherein we have recognized the true
* line and re-mapped it to one we expect.
*
* @param {string} line
* @returns {boolean}
*/
function isStarExportLine (line) {
return /^\* from /.test(line)
}
function isBareSpecifier (specifier) {
// Relative and absolute paths are not bare specifiers.
if (
specifier.startsWith('.') ||
specifier.startsWith('/')) {
return false
}
// Valid URLs are not bare specifiers. (file:, http:, node:, etc.)
// eslint-disable-next-line no-prototype-builtins
if (URL.hasOwnProperty('canParse')) {
return !URL.canParse(specifier)
}
try {
// eslint-disable-next-line no-new
new URL(specifier)
return false
} catch (err) {
return true
}
}
/**
* Determines whether the input is a bare specifier, file URL or a regular expression.
*
* - node: prefixed URL strings are considered bare specifiers in this context.
*/
function isBareSpecifierFileUrlOrRegex (input) {
if (input instanceof RegExp) {
return true
}
// Relative and absolute paths
if (
input.startsWith('.') ||
input.startsWith('/')) {
return false
}
try {
// eslint-disable-next-line no-new
const url = new URL(input)
// We consider node: URLs bare specifiers in this context
return url.protocol === 'file:' || url.protocol === 'node:'
} catch (err) {
// Anything that fails parsing is a bare specifier
return true
}
}
/**
* Ensure an array only contains bare specifiers, file URLs or regular expressions.
*
* - We consider node: prefixed URL string as bare specifiers in this context.
* - For node built-in modules, we add additional node: prefixed modules to the
* output array.
*/
function ensureArrayWithBareSpecifiersFileUrlsAndRegex (array, type) {
if (!Array.isArray(array)) {
return undefined
}
const invalid = array.filter(s => !isBareSpecifierFileUrlOrRegex(s))
if (invalid.length) {
throw new Error(`'${type}' option only supports bare specifiers, file URLs or regular expressions. Invalid entries: ${inspect(invalid)}`)
}
// Rather than evaluate whether we have a node: scoped built-in-module for
// every call to resolve, we just add them to include/exclude now.
for (const each of array) {
if (typeof each === 'string' && !each.startsWith('node:') && builtinModules.includes(each)) {
array.push(`node:${each}`)
}
}
return array
}
function emitWarning (err) {
// Unfortunately, process.emitWarning does not output the full error
// with error.cause like console.warn does so we need to inspect it when
// tracing warnings
const warnMessage = process.execArgv.includes('--trace-warnings') ? inspect(err) : err
process.emitWarning(warnMessage)
}
/**
* Processes a module's exports and builds a set of setter blocks.
*
* @param {object} params
* @param {string} params.srcUrl The full URL to the module to process.
* @param {object} params.context Provided by the loaders API.
* @param {Function} params.parentGetSource Provides the source code for the parent module.
* @param {bool} params.excludeDefault Exclude the default export.
*
* @returns {Promise<Map<string, string>>} The shimmed setters for all the exports
* from the module and any transitive export all modules.
*/
async function processModule ({ srcUrl, context, parentGetSource, parentResolve, excludeDefault }) {
const exportNames = await getExports(srcUrl, context, parentGetSource)
const starExports = new Set()
const setters = new Map()
const addSetter = (name, setter, isStarExport = false) => {
if (setters.has(name)) {
if (isStarExport) {
// If there's already a matching star export, delete it
if (starExports.has(name)) {
setters.delete(name)
}
// and return so this is excluded
return
}
// if we already have this export but it is from a * export, overwrite it
if (starExports.has(name)) {
starExports.delete(name)
setters.set(name, setter)
}
} else {
// Store export * exports so we know they can be overridden by explicit
// named exports
if (isStarExport) {
starExports.add(name)
}
setters.set(name, setter)
}
}
for (const n of exportNames) {
if (n === 'default' && excludeDefault) continue
if (isStarExportLine(n) === true) {
const [, modFile] = n.split('* from ')
// Relative paths need to be resolved relative to the parent module
const newSpecifier = isBareSpecifier(modFile) ? modFile : new URL(modFile, srcUrl).href
// We need to call `parentResolve` to resolve bare specifiers to a full
// URL. We also need to call `parentResolve` for all sub-modules to get
// the `format`. We can't rely on the parents `format` to know if this
// sub-module is ESM or CJS!
const result = await parentResolve(newSpecifier, { parentURL: srcUrl })
const subSetters = await processModule({
srcUrl: result.url,
context: { ...context, format: result.format },
parentGetSource,
parentResolve,
excludeDefault: true
})
for (const [name, setter] of subSetters.entries()) {
addSetter(name, setter, true)
}
} else {
addSetter(n, `
let $${n} = _.${n}
export { $${n} as ${n} }
set.${n} = (v) => {
$${n} = v
return true
}
`)
}
}
return setters
}
function addIitm (url) {
const urlObj = new URL(url)
urlObj.searchParams.set('iitm', 'true')
return needsToAddFileProtocol(urlObj) ? 'file:' + urlObj.href : urlObj.href
}
function createHook (meta) {
let cachedResolve
const iitmURL = new URL('lib/register.js', meta.url).toString()
let includeModules, excludeModules
async function initialize (data) {
if (data) {
includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
if (data.addHookMessagePort) {
data.addHookMessagePort.on('message', (modules) => {
if (includeModules === undefined) {
includeModules = []
}
for (const each of modules) {
if (!each.startsWith('node:') && builtinModules.includes(each)) {
includeModules.push(`node:${each}`)
}
includeModules.push(each)
}
data.addHookMessagePort.postMessage('ack')
}).unref()
}
}
}
async function resolve (specifier, context, parentResolve) {
cachedResolve = parentResolve
// See https://github.com/nodejs/import-in-the-middle/pull/76.
if (specifier === iitmURL) {
return {
url: specifier,
shortCircuit: true
}
}
const { parentURL = '' } = context
const newSpecifier = deleteIitm(specifier)
if (isWin && parentURL.indexOf('file:node') === 0) {
context.parentURL = ''
}
const result = await parentResolve(newSpecifier, context, parentResolve)
if (parentURL === '' && !EXTENSION_RE.test(result.url)) {
entrypoint = result.url
return { url: result.url, format: 'commonjs' }
}
// For included/excluded modules, we check the specifier to match libraries
// that are loaded with bare specifiers from node_modules.
//
// For non-bare specifier imports, we match to the full file URL because
// using relative paths would be very error prone!
function match (each) {
if (each instanceof RegExp) {
return each.test(result.url)
}
return each === specifier || each === result.url
}
if (includeModules && !includeModules.some(match)) {
return result
}
if (excludeModules && excludeModules.some(match)) {
return result
}
if (isIitm(parentURL, meta) || hasIitm(parentURL)) {
return result
}
// We don't want to attempt to wrap native modules
if (result.url.endsWith('.node')) {
return result
}
// Node.js v21 renames importAssertions to importAttributes
if (
(context.importAssertions && context.importAssertions.type === 'json') ||
(context.importAttributes && context.importAttributes.type === 'json')
) {
return result
}
// If the file is referencing itself, we need to skip adding the iitm search params
if (result.url === parentURL) {
return {
url: result.url,
shortCircuit: true,
format: result.format
}
}
specifiers.set(result.url, specifier)
return {
url: addIitm(result.url),
shortCircuit: true,
format: result.format
}
}
async function getSource (url, context, parentGetSource) {
if (hasIitm(url)) {
const realUrl = deleteIitm(url)
try {
const setters = await processModule({
srcUrl: realUrl,
context,
parentGetSource,
parentResolve: cachedResolve
})
return {
source: `
import { register } from '${iitmURL}'
import * as namespace from ${JSON.stringify(realUrl)}
// Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
const _ = Object.assign(
Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }),
namespace
)
const set = {}
${Array.from(setters.values()).join('\n')}
register(${JSON.stringify(realUrl)}, _, set, ${JSON.stringify(specifiers.get(realUrl))})
`
}
} catch (cause) {
// If there are other ESM loader hooks registered as well as iitm,
// depending on the order they are registered, source might not be
// JavaScript.
//
// If we fail to parse a module for exports, we should fall back to the
// parent loader. These modules will not be wrapped with proxies and
// cannot be Hook'ed but at least this does not take down the entire app
// and block iitm from being used.
//
// We log the error because there might be bugs in iitm and without this
// it would be very tricky to debug
const err = new Error(`'import-in-the-middle' failed to wrap '${realUrl}'`)
err.cause = cause
emitWarning(err)
// Revert back to the non-iitm URL
url = realUrl
}
}
return parentGetSource(url, context, parentGetSource)
}
// For Node.js 16.12.0 and higher.
async function load (url, context, parentLoad) {
if (hasIitm(url)) {
const { source } = await getSource(url, context, parentLoad)
return {
source,
shortCircuit: true,
format: 'module'
}
}
return parentLoad(url, context, parentLoad)
}
if (NODE_MAJOR >= 17 || (NODE_MAJOR === 16 && NODE_MINOR >= 12)) {
return { initialize, load, resolve }
} else {
return {
initialize,
load,
resolve,
getSource,
getFormat (url, context, parentGetFormat) {
if (hasIitm(url)) {
return {
format: 'module'
}
}
if (url === entrypoint) {
return {
format: 'commonjs'
}
}
return parentGetFormat(url, context, parentGetFormat)
}
}
}
}
module.exports = { createHook }