UNPKG

import-in-the-middle

Version:
810 lines (710 loc) 31.8 kB
// 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. import { URL, fileURLToPath } from 'url' import { inspect } from 'util' import { builtinModules } from 'module' import { getExports, hasModuleExportsCJSDefault } from './lib/get-exports.mjs' import { RESOLVE, driveSync, driveAsync } from './lib/io.mjs' import { supportsSyncHooks } from './supports-sync-hooks.mjs' // Re-exported for backwards compatibility: `supportsSyncHooks` now lives in its // own import-free module so a CommonJS preloader can check it without loading // this file's acorn / cjs-module-lexer dependency graph. export { supportsSyncHooks } const specifiers = new Map() const isWin = process.platform === 'win32' // Depth at which `processModule` starts tracking visited URLs to break an // `export *` cycle. Real re-export chains are only a few levels deep, so this // is far beyond any legitimate graph yet well below the call-stack limit a // cycle would otherwise hit. Below it the recursion pays only an integer // compare per level and allocates no set. const STAR_CYCLE_DEPTH = 100 // 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)$/ // The `-typescript` formats are listed unconditionally; getExports strips the // types when the runtime supports it and otherwise falls back to onWrapFailure. const HANDLED_FORMATS = new Set([ 'builtin', 'module', 'commonjs', 'module-typescript', 'commonjs-typescript' ]) const TRACE_WARNINGS = process.execArgv.includes('--trace-warnings') function hasIitm (url) { // Fast path: avoid URL parsing on the hot path when there's clearly no iitm. if (typeof url !== 'string' || url.indexOf('iitm') === -1) { return false } 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', 'create-hook.mjs') } function deleteIitm (url) { // Fast path: avoid URL parsing / try-catch on bare specifiers and normal file URLs. if (typeof url !== 'string' || url.indexOf('iitm') === -1) { return url } let resultUrl const stackTraceLimit = Error.stackTraceLimit try { Error.stackTraceLimit = 0 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:///', '') } } else { resultUrl = urlObj.href } } catch { resultUrl = url } Error.stackTraceLimit = stackTraceLimit return resultUrl } /** * 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) } const stackTraceLimit = Error.stackTraceLimit try { Error.stackTraceLimit = 0 // eslint-disable-next-line no-new new URL(specifier) return false } catch (err) { return true } finally { Error.stackTraceLimit = stackTraceLimit } } /** * 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 } const stackTraceLimit = Error.stackTraceLimit try { Error.stackTraceLimit = 0 // 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 } finally { Error.stackTraceLimit = stackTraceLimit } } /** * 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 = TRACE_WARNINGS ? inspect(err) : err process.emitWarning(warnMessage) } /** * Builds the setter/getter/re-export block injected into the wrapper module for * a single named export. This is pure string generation, identical regardless * of how the loader is driven, so both the synchronous and asynchronous paths * share it. * * The value is read from `namespaceVar`, the wrapper's namespace binding for the * module that *defines* the export. For a module's own exports that is the * wrapped module itself; for a name re-exported through `export *` it is the * leaf that declares it. Reading from the defining module rather than the * aggregating one keeps the value resolvable when the same binding reaches the * aggregator through more than one re-export chain — Node sees those chains as * distinct wrapper modules and leaves the name ambiguous (hence `undefined`) on * the aggregate namespace, while the defining module always holds it (#171). * * @param {string} n The exported name. * @param {string} srcUrl The URL of the module the export belongs to. * @param {string} namespaceVar The wrapper binding holding `srcUrl`'s namespace. * @returns {string} */ function buildSetter (n, srcUrl, namespaceVar) { const variableName = `$${n.replace(/[^a-zA-Z0-9_$]/g, '_')}` const objectKey = JSON.stringify(n) const reExportedName = n === 'default' ? n : objectKey // Fall back to namespace['default'] for the module.exports synthetic export, // which builtins don't expose on the native ESM namespace. const useFallback = n === 'module.exports' // Builtins don't expose the module.exports synthetic name, so skip its re-export. const reExportLine = (n === 'module.exports' && (srcUrl.startsWith('node:') || builtinModules.includes(srcUrl))) ? '' : `export { ${variableName} as ${reExportedName} }` return `let ${variableName} __binder.bind(${objectKey}, ${namespaceVar}, v => { ${variableName} = v }, () => ${variableName}, ${useFallback}) ${reExportLine}` } /** * Processes a module's exports and builds a set of setter blocks. * * Written as a "sans-io" generator (see `lib/io.mjs`): instead of calling the * loader's resolve/load hooks directly it `yield`s `[RESOLVE, ...]` to resolve * star re-exports and `[LOAD, ...]` (via {@link getExports}) to read source, * and is driven by either {@link driveSync} (for * `module.registerHooks`) or {@link driveAsync} (for `module.register`). The * body is identical for both, so there is a single implementation to maintain. * * @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 {boolean} [params.excludeDefault = false] Exclude the default export. * @param {number} [params.depth = 0] Star-re-export recursion depth. Used to * detect `export *` cycles (`a` re-exports `b`, `b` re-exports `a`) cheaply: * the acyclic common case pays only an integer compare per level, and the * cycle-tracking set is allocated only once recursion is implausibly deep. * @param {Set<string>} [params.seen] URLs currently on the recursion stack, * created lazily once `depth` crosses {@link STAR_CYCLE_DEPTH}. A URL is added * before descending into its subtree and removed once that subtree finishes, so * it tracks the active path rather than every URL ever visited. * @param {Map<string, string>} [params.originNamespaces] Shared registry mapping * a defining-module URL to the wrapper namespace alias a same-origin `export *` * collision must read it from. Absent until the first such collision; then * threaded through the recursion so one defining module yields one alias and * {@link buildWrapperSource} imports each once. Only `*`-collided names use it; * every other export reads from the wrapped module's own `namespace`. * * @returns {Generator<Array, { setters: Map<string, string>, origins: (Map<string, string> | undefined), originNamespaces: (Map<string, string> | undefined) }>} * A generator that yields I/O operations and ultimately returns the shimmed * setters for all the exports from the module and any transitive export all * modules. `origins` (the defining module per `*`-sourced name) is `undefined` * for a module with no `export *`; `originNamespaces` stays `undefined` unless a * same-origin `*` collision actually needed an alias. */ function * processModule ({ srcUrl, context, excludeDefault = false, depth = 0, seen, originNamespaces }) { const exportNames = yield * getExports(srcUrl, context) const setters = new Map() // Maps each live `*`-sourced name to the module that defined it. Its keys // double as "this name came from a `*` re-export" (so an explicit export can // override it), and its values let two `*` re-exports of the same name be told // apart. Allocated on the first `export *`, never for a module without one; a // single Map carries both facts so a star with no collision pays one structure // and one write per name, not two. let starOrigins // A name pulled in through more than one `export *` chain that all bottom out // at the same module stays exported (ECMAScript ResolveExport; // tc39/ecma262#3715), but the *aggregate* namespace this wrapper imports drops // it: under iitm the chains are distinct wrapped modules, so Node sees the // re-export as ambiguous and the name reads back undefined. Only those names // must instead read from their defining module's own namespace, which always // holds the value. `originNamespaces` maps such a defining module to the alias // the wrapper imports for it; it is allocated on the first surviving // collision, so a module without one emits no extra import (#171). const ensureOriginNamespace = (origin) => { originNamespaces ??= new Map() let alias = originNamespaces.get(origin) if (alias === undefined) { alias = `__ns${originNamespaces.size}` originNamespaces.set(origin, alias) } return alias } const addSetter = (name, setter, isStarExport, origin) => { if (setters.has(name)) { if (isStarExport) { // `starOrigins.has(name)` means the existing entry also came from a `*` // re-export (an explicit export would not be tracked here). if (starOrigins.has(name)) { if (starOrigins.get(name) === origin) { // The same binding reached through two `*` re-export chains. It // stays exported, but the aggregate namespace dropped it, so point // its setter at the defining module's namespace instead. setters.set(name, buildSetter(name, origin, ensureOriginNamespace(origin))) } else { // Genuinely ambiguous: two `*` re-exports name it from different // modules. Per ResolveExport the name is excluded entirely. setters.delete(name) starOrigins.delete(name) } } // An explicit export already shadows the `*` re-export; leave it. } } else { if (isStarExport) { starOrigins.set(name, origin) } setters.set(name, setter) } } for (const n of exportNames) { if (excludeDefault) { const isDefault = n === 'default' || ( n === 'module.exports' && context.format === 'commonjs' && hasModuleExportsCJSDefault ) if (isDefault) 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 resolve bare specifiers to a full URL. We also need to // resolve all sub-modules to get the `format`. We can't rely on the // parent's `format` to know if this sub-module is ESM or CJS! const result = yield [RESOLVE, newSpecifier, { parentURL: srcUrl }] // First `*` re-export: allocate the origin bookkeeping lazily. starOrigins ??= new Map() // `export *` graphs are normally only a handful of levels deep. A cycle // (`a` re-exports `b`, `b` re-exports `a`) instead recurses without bound // and exhausts memory. Rather than track every URL on the common shallow // path, only start recording once the depth is implausibly large for a // real graph; from there a re-export pointing back at a module already on // the recursion stack is the cycle, and is skipped (its exports are // collected by the in-progress ancestor frame). `seen` mirrors the stack, // not every URL visited: a module reached and fully processed through one // sibling branch must stay reachable through a later, more direct branch, // so it is removed again once its subtree finishes. if (depth >= STAR_CYCLE_DEPTH) { seen ??= new Set() if (seen.has(result.url)) continue seen.add(result.url) } try { const sub = yield * processModule({ srcUrl: result.url, context: { ...context, format: result.format }, excludeDefault: true, depth: depth + 1, seen, originNamespaces }) // Adopt any registry a nested `export *` minted before processing this // child's results, so a collision detected here extends the same Map the // child's setters already reference (one alias per defining module across // the whole tree) rather than orphaning the child's into a second Map. originNamespaces ??= sub.originNamespaces // Star targets build their setters against `namespace` like any other // module; only a surviving same-origin collision (in addSetter) rewrites // the affected name to read from its defining module's alias. for (const [name, setter] of sub.setters) { addSetter(name, setter, true, sub.origins?.get(name) ?? result.url) } } finally { seen?.delete(result.url) } } else { addSetter(n, buildSetter(n, srcUrl, 'namespace'), false) } } return { setters, origins: starOrigins, originNamespaces } } function addIitm (url) { const urlObj = new URL(url) urlObj.searchParams.set('iitm', 'true') return urlObj.href } export function createHook (meta) { let cachedResolve const iitmURL = new URL('lib/register.js', meta.url).toString() let includeModules, excludeModules let shouldInclude = defaultShouldInclude // Track CJS module URLs that IITM has wrapped. On Node 24+, CJS modules loaded // via loadCJSModule (in an ESM import chain) have their require() calls for // builtins routed through the ESM resolver. Without this guard, IITM would // intercept those require() calls and return an ESM namespace object instead // of the native CJS module value (e.g. EventEmitter constructor), breaking // patterns like `class App extends require('events') {}`. const cjsInIitmChain = new Set() // Default matcher, used unless the consumer supplies its own `shouldInclude` // (see applyOptions). It applies the include/exclude lists, so finishResolve // always has a predicate to call and never has to special-case its absence. // // We check the specifier to match libraries loaded with bare specifiers from // node_modules, and the full file URL for non-bare specifier imports (relative // paths would be error prone). An absolute path entry added via Hook over the // message port matches the resolved file path, so it is resolved here. function defaultShouldInclude (url, specifier) { let resultPath if (url.startsWith('file:')) { const stackTraceLimit = Error.stackTraceLimit Error.stackTraceLimit = 0 try { resultPath = fileURLToPath(url) } catch {} Error.stackTraceLimit = stackTraceLimit } function match (each) { if (each instanceof RegExp) { return each.test(url) } return each === specifier || each === url || (resultPath && each === resultPath) } if (includeModules && !includeModules.some(match)) { return false } if (excludeModules && excludeModules.some(match)) { return false } return true } // Applies the include/exclude/message-port configuration. Shared by the // asynchronous `initialize` (off-thread `module.register`, which receives // `data` over the registration boundary) and by synchronous registration // (`module.registerHooks`), which has no `initialize` step and passes the // same options directly. function applyOptions (data) { includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include') excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude') // A consumer can supply its own matcher as `shouldInclude(url, specifier)`, // taking ownership of the include/exclude decision instead of expressing it // as bare-specifier / file-URL / regex lists. It replaces the default list // matcher and is called with the resolved URL and specifier; otherwise the // default applies the include/exclude options. shouldInclude = typeof data.shouldInclude === 'function' ? data.shouldInclude : defaultShouldInclude 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 initialize (data) { if (global.__import_in_the_middle_initialized__) { process.emitWarning("The 'import-in-the-middle' hook has already been initialized") } global.__import_in_the_middle_initialized__ = true if (data) { applyOptions(data) } } // Shared post-processing for the `resolve` hook: everything that happens // once the parent loader has turned the specifier into a resolved URL. The // only difference between the asynchronous and synchronous hooks is whether // that resolution was awaited, so all the wrapping decisions live here. function finishResolve (result, specifier, context, parentURL) { // Do not wrap the entrypoint module. Many CLIs check whether they are the // "main" module (e.g. require.main === module). Wrapping changes how they // are evaluated, and can make them exit without doing anything. if (parentURL === '') { if (!EXTENSION_RE.test(result.url) && !hasIitm(result.url)) { return { url: result.url, format: 'commonjs' } } return result } // Never wrap a module whose format we don't handle (e.g. json, wasm); this // holds regardless of how inclusion is decided below. if (result.format && !HANDLED_FORMATS.has(result.format)) { return result } // The synchronous hooks (`module.registerHooks`) fire for `require()` as well // as `import`, but iitm only owns the ESM graph: CommonJS modules are // instrumented separately through require-in-the-middle, and `require()` must // return the native, mutable module value (e.g. graceful-fs does // `Object.defineProperty(require('fs'), ...)`, which throws on a frozen ESM // namespace). Node reports the active module system in `context.conditions` // ('require' vs 'import'), so leave any require() resolution untouched. The // asynchronous hook never sees the 'require' condition, so this is a no-op // there and only affects the synchronous path. if (context.conditions?.includes('require')) { return result } // `shouldInclude` is always set (the include/exclude list matcher by default, // a consumer-provided predicate otherwise), so no nullish check is needed. if (!shouldInclude(result.url, specifier)) { return result } if (isIitm(parentURL, meta) || (parentURL && hasIitm(parentURL))) { return result } // When a CJS module is loaded by an IITM shim, its require() calls for // builtins may be routed through the ESM resolver on Node 24+. Skip IITM // wrapping in that case so require() returns the native module value. // We also propagate the membership to the resolved child so that its own // transitive require() calls are likewise skipped (the entire synchronous // CJS require chain must remain unwrapped to avoid ERR_VM_MODULE_LINK_FAILURE). if (cjsInIitmChain.has(parentURL)) { cjsInIitmChain.add(result.url) 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 const importAttributes = context.importAttributes || context.importAssertions if (importAttributes && 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, // Node's synchronous resolver drops `format: 'builtin'` for bare builtin // specifiers (`require('crypto')` -> `node:crypto`), so restore it; // otherwise the load hook reads `node:crypto` from disk and throws ENOENT. format: result.format ?? (result.url.startsWith('node:') ? 'builtin' : undefined) } } 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) return finishResolve(result, specifier, context, parentURL) } // Synchronous counterpart to `resolve`, for `module.registerHooks`. The // synchronous `nextResolve` returns its result directly. We stash it so the // synchronous `load` hook can resolve star re-exports later, mirroring how // `resolve` caches `parentResolve`. function resolveSync (specifier, context, nextResolve) { cachedResolve = nextResolve 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 = nextResolve(newSpecifier, context) return finishResolve(result, specifier, context, parentURL) } // Builds the wrapper module source that re-exports the real module through // iitm's proxy. Pure string generation shared by the asynchronous and // synchronous `load` paths. function buildWrapperSource (realUrl, setters, originalSpecifier, originNamespaces) { // The wrapped module imports its namespace as `namespace`, which serves // every export but the ones a same-origin `export *` collision forced onto // their defining module (#171): the aggregate namespace drops those as // ambiguous under iitm, so each such defining module gets its own alias the // wrapper imports. Absent the registry (no such collision) nothing is added. let originImports = '' if (originNamespaces !== undefined) { for (const [originUrl, alias] of originNamespaces) { originImports += `import * as ${alias} from ${JSON.stringify(originUrl)}\n` } } return ` import { register, ModuleBinder } from '${iitmURL}' import * as namespace from ${JSON.stringify(realUrl)} ${originImports} const __binder = new ModuleBinder() ${Array.from(setters.values()).join('\n')} __binder.flush() register(${JSON.stringify(realUrl)}, __binder.namespace, __binder.set, __binder.get, ${JSON.stringify(originalSpecifier)}) ` } // Bookkeeping shared by the async and sync wrap paths once `processModule` // succeeds: free the specifier entry early, and remember CJS modules so their // transitive require() chain bypasses iitm (see `load`). Returns the wrapper // module source. function onWrapSuccess (realUrl, context, originalSpecifier, setters, originNamespaces) { specifiers.delete(realUrl) // context.format is set to 'commonjs' by getCjsExports during processModule. if (context.format === 'commonjs') { cjsInIitmChain.add(realUrl) } return buildWrapperSource(realUrl, setters, originalSpecifier, originNamespaces) } // Bookkeeping shared by the async and sync wrap paths when `processModule` // throws. iitm falls back to the parent loader so the module loads unwrapped // (it just can't be Hook'ed) rather than taking down the whole app. We free // the specifier entry to avoid a leak, and log because a failure here is // usually an iitm bug and would otherwise be very tricky to debug. function onWrapFailure (realUrl, cause) { specifiers.delete(realUrl) const err = new Error(`'import-in-the-middle' failed to wrap '${realUrl}'`) err.cause = cause emitWarning(err) } async function getSource (url, context, parentGetSource) { if (hasIitm(url)) { const realUrl = deleteIitm(url) const originalSpecifier = specifiers.get(realUrl) try { const { setters, originNamespaces } = await driveAsync( processModule({ srcUrl: realUrl, context }), { resolve: cachedResolve, load: parentGetSource } ) return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters, originNamespaces) } } catch (cause) { onWrapFailure(realUrl, cause) // Revert back to the non-iitm URL url = realUrl } } return parentGetSource(url, context) } // Synchronous counterpart to `getSource`, for `module.registerHooks`. Drives // `processModule` straight through; all bookkeeping and source generation is // shared with `getSource`. function getSourceSync (url, context, nextLoad) { if (hasIitm(url)) { const realUrl = deleteIitm(url) const originalSpecifier = specifiers.get(realUrl) try { const { setters, originNamespaces } = driveSync( processModule({ srcUrl: realUrl, context }), { resolve: cachedResolve, load: nextLoad } ) return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters, originNamespaces) } } catch (cause) { onWrapFailure(realUrl, cause) url = realUrl } } return nextLoad(url, context) } async function load (url, context, parentLoad) { if (hasIitm(url)) { const result = await getSource(url, context, parentLoad) // If wrapping failed, `getSource()` may have fallen back to `parentLoad`, // which can legally return `source: null` (e.g. for non-JS formats). if (result && typeof result === 'object' && result.source != null) { return { source: result.source, shortCircuit: true, format: 'module' } } // Fall back to the parent loader with the original (non-iitm) URL. return parentLoad(deleteIitm(url), context) } // On Node 22+, when a CJS module is loaded through the ESM translator and // another loader hook provides its source (instead of leaving source null // for Node to read natively), require() calls inside that CJS module for // packages using the "module-sync" exports condition fail with // ERR_VM_MODULE_LINK_FAILURE. Work around this Node bug by stripping // hook-provided source for CJS modules in the synchronous require chain, // forcing Node to use its native CJS loader which handles this correctly. if (cjsInIitmChain.has(url)) { const result = await parentLoad(url, context) if (result.format === 'commonjs' && result.source != null) { return { format: result.format, source: undefined } } return result } return parentLoad(url, context) } // Synchronous counterpart to `load`, for `module.registerHooks`. Mirrors the // async `load` exactly — wrapping via `getSourceSync` and applying the same // CJS-in-iitm-chain source stripping — only without awaiting. function loadSync (url, context, nextLoad) { if (hasIitm(url)) { const result = getSourceSync(url, context, nextLoad) // If wrapping failed, `getSourceSync()` may have fallen back to `nextLoad`, // which can legally return `source: null` (e.g. for non-JS formats). if (result && typeof result === 'object' && result.source != null) { return { source: result.source, shortCircuit: true, format: 'module' } } // Fall back to the parent loader with the original (non-iitm) URL. return nextLoad(deleteIitm(url), context) } if (cjsInIitmChain.has(url)) { const result = nextLoad(url, context) if (result.format === 'commonjs' && result.source != null) { return { format: result.format, source: undefined } } return result } return nextLoad(url, context) } return { initialize, load, resolve, resolveSync, loadSync, applyOptions } }