dd-trace
Version:
Datadog APM tracing client for JavaScript
215 lines (181 loc) • 6.28 kB
JavaScript
// The content of this file is copied from the `import-in-the-middle` package with minor modifications (https://www.npmjs.com/package/import-in-the-middle)
const { pathToFileURL, fileURLToPath } = require('node:url')
const fs = require('node:fs')
const path = require('node:path')
const { NODE_MAJOR, NODE_MINOR } = require('../../../version.js')
const getExportsImporting = (url) => import(url).then(Object.keys)
let getExportsModulePromise
const loadGetExportsModule = () => {
if (!getExportsModulePromise) {
getExportsModulePromise = import('import-in-the-middle/lib/get-exports.mjs')
}
return getExportsModulePromise
}
const getExports = NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)
? async (srcUrl, context, getSource) => {
const mod = await loadGetExportsModule()
return mod.getExports(srcUrl, context, getSource)
}
: getExportsImporting
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.)
if (URL.hasOwnProperty('canParse')) {
// eslint-disable-next-line n/no-unsupported-features/node-builtins
return !URL.canParse(specifier)
}
try {
// eslint-disable-next-line no-new
new URL(specifier)
return false
} catch {
return true
}
}
function resolve (specifier, context) {
// This comes from an import, that is why import makes preference
const conditions = ['import']
if (specifier.startsWith('file://')) {
specifier = fileURLToPath(specifier)
}
const resolved = require.resolve(specifier, { conditions, paths: [fileURLToPath(context.parentURL)] })
return {
url: pathToFileURL(resolved),
format: isESMFile(resolved) ? 'module' : 'commonjs'
}
}
function getSource (url, { format }) {
return {
source: fs.readFileSync(fileURLToPath(url), 'utf8'),
format
}
}
/**
* Generates the pieces of code for the proxy module before the path
*
* @param {object} moduleData
* @param {string} moduleData.path
* @param {boolean} moduleData.internal
* @param {object} moduleData.context
* @param {boolean} moduleData.excludeDefault
* @returns {Promise<Map>}
*/
async function processModule ({ path, internal, context, excludeDefault }) {
let exportNames, srcUrl
if (internal) {
// we can not read and parse of internal modules
exportNames = await getExportsImporting(path)
} else {
srcUrl = pathToFileURL(path)
exportNames = await getExports(srcUrl, context, getSource)
}
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) {
// export * from 'wherever'
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 = resolve(newSpecifier, { parentURL: srcUrl })
// eslint-disable-next-line no-await-in-loop
const subSetters = await processModule({
path: fileURLToPath(result.url),
context: { ...context, format: result.format },
excludeDefault: true
})
for (const [name, setter] of subSetters.entries()) {
addSetter(name, setter, true)
}
} else {
const variableName = `$${n.replaceAll(/[^a-zA-Z0-9_$]/g, '_')}`
const objectKey = JSON.stringify(n)
const reExportedName = n === 'default' ? n : objectKey
addSetter(n, `
let ${variableName}
try {
${variableName} = _[${objectKey}] = namespace[${objectKey}]
} catch (err) {
if (!(err instanceof ReferenceError)) throw err
}
export { ${variableName} as ${reExportedName} }
set[${objectKey}] = (v) => {
${variableName} = v
return true
}
get[${objectKey}] = () => ${variableName}
`)
}
}
return setters
}
/**
* Determines if a file is a ESM module or CommonJS
*
* @param {string} fullPathToModule File to analize
* @param {string} [modulePackageJsonPath] Path of the package.json
* @param {object} [packageJson] The content of the module package.json
* @returns {boolean}
*/
function isESMFile (fullPathToModule, modulePackageJsonPath, packageJson = {}) {
if (fullPathToModule.endsWith('.mjs')) return true
if (fullPathToModule.endsWith('.cjs')) return false
const pathParts = fullPathToModule.split(path.sep)
do {
pathParts.pop()
const packageJsonPath = [...pathParts, 'package.json'].join(path.sep)
if (packageJsonPath === modulePackageJsonPath) {
return packageJson.type === 'module'
}
try {
const packageJsonContent = fs.readFileSync(packageJsonPath).toString()
const packageJson = JSON.parse(packageJsonContent)
return packageJson.type === 'module'
} catch {
// file does not exit, continue
}
} while (pathParts.length > 0)
return packageJson.type === 'module'
}
module.exports = {
processModule,
isESMFile
}