diff --git a/node_modules/import-in-the-middle/hook.js b/node_modules/import-in-the-middle/hook.js index 4ec7679..a7c3632 100644 --- a/node_modules/import-in-the-middle/hook.js +++ b/node_modules/import-in-the-middle/hook.js @@ -129,15 +129,32 @@ function isBareSpecifier (specifier) { */ async function processModule ({ srcUrl, context, parentGetSource, parentResolve, excludeDefault }) { const exportNames = await getExports(srcUrl, context, parentGetSource) - const duplicates = new Set() + const starExports = new Set() const setters = new Map() - const addSetter = (name, setter) => { - // When doing an `import *` duplicates become undefined, so do the same + const addSetter = (name, setter, isStarExport = false) => { if (setters.has(name)) { - duplicates.add(name) - setters.delete(name) - } else if (!duplicates.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) } } @@ -161,10 +178,11 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve, srcUrl: modUrl, context, parentGetSource, + parentResolve, excludeDefault: true }) for (const [name, setter] of setters.entries()) { - addSetter(name, setter) + addSetter(name, setter, true) } } else { addSetter(n, ` @@ -243,17 +261,21 @@ function createHook (meta) { } } - async function getSource (url, context, parentGetSource) { + async function load (url, context, parentLoad) { if (hasIitm(url)) { const realUrl = deleteIitm(url) - const setters = await processModule({ - srcUrl: realUrl, - context, - parentGetSource, - parentResolve: cachedResolve - }) - return { - source: ` + + try { + const setters = await processModule({ + srcUrl: realUrl, + context, + parentGetSource: parentLoad, + parentResolve: cachedResolve + }) + return { + shortCircuit: true, + format: 'module', + source: ` import { register } from '${iitmURL}' import * as namespace from ${JSON.stringify(realUrl)} @@ -268,20 +290,25 @@ ${Array.from(setters.values()).join('\n')} register(${JSON.stringify(realUrl)}, _, set, ${JSON.stringify(specifiers.get(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' + } + } 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 + console.warn(err) + + // Revert back to the non-iitm URL + url = realUrl } } @@ -294,7 +321,7 @@ register(${JSON.stringify(realUrl)}, _, set, ${JSON.stringify(specifiers.get(rea return { load, resolve, - getSource, + getSource: load, getFormat (url, context, parentGetFormat) { if (hasIitm(url)) { return { diff --git a/node_modules/import-in-the-middle/index.d.ts b/node_modules/import-in-the-middle/index.d.ts index c7f6177..c468977 100644 --- a/node_modules/import-in-the-middle/index.d.ts +++ b/node_modules/import-in-the-middle/index.d.ts @@ -27,7 +27,7 @@ export type Options = { internals?: boolean } -declare class Hook { +export declare class Hook { /** * Creates a hook to be run on any already loaded modules and any that will * be loaded in the future. It will be run once per loaded module. If diff --git a/node_modules/import-in-the-middle/lib/get-esm-exports.js b/node_modules/import-in-the-middle/lib/get-esm-exports.js index 9c88ac8..32b49db 100644 --- a/node_modules/import-in-the-middle/lib/get-esm-exports.js +++ b/node_modules/import-in-the-middle/lib/get-esm-exports.js @@ -29,7 +29,7 @@ function warn (txt) { * @param {string} params.moduleSource The source code of the module to parse * and interpret. * - * @returns {string[]} The identifiers exported by the module along with any + * @returns {Set} The identifiers exported by the module along with any * custom directives. */ function getEsmExports (moduleSource) { @@ -62,7 +62,7 @@ function getEsmExports (moduleSource) { warn('unrecognized export type: ' + node.type) } } - return Array.from(exportedNames) + return exportedNames } function parseDeclaration (node, exportedNames) { diff --git a/node_modules/import-in-the-middle/lib/get-exports.js b/node_modules/import-in-the-middle/lib/get-exports.js index 7616e6c..9a78e8c 100644 --- a/node_modules/import-in-the-middle/lib/get-exports.js +++ b/node_modules/import-in-the-middle/lib/get-exports.js @@ -1,36 +1,60 @@ 'use strict' const getEsmExports = require('./get-esm-exports.js') -const { parse: getCjsExports } = require('cjs-module-lexer') -const fs = require('fs') +const { parse: parseCjs } = require('cjs-module-lexer') +const { readFileSync } = require('fs') +const { builtinModules } = require('module') const { fileURLToPath, pathToFileURL } = require('url') +const { dirname } = require('path') function addDefault (arr) { - return Array.from(new Set(['default', ...arr])) + return new Set(['default', ...arr]) +} + +// Cached exports for Node built-in modules +const BUILT_INS = new Map() + +function getExportsForNodeBuiltIn (name) { + let exports = BUILT_INS.get() + + if (!exports) { + exports = new Set(addDefault(Object.keys(require(name)))) + BUILT_INS.set(name, exports) + } + + return exports } const urlsBeingProcessed = new Set() // Guard against circular imports. -async function getFullCjsExports (url, context, parentLoad, source) { +async function getCjsExports (url, context, parentLoad, source) { if (urlsBeingProcessed.has(url)) { return [] } urlsBeingProcessed.add(url) - const ex = getCjsExports(source) - const full = Array.from(new Set([ - ...addDefault(ex.exports), - ...(await Promise.all(ex.reexports.map(re => getExports( - (/^(..?($|\/|\\))/).test(re) - ? pathToFileURL(require.resolve(fileURLToPath(new URL(re, url)))).toString() - : pathToFileURL(require.resolve(re)).toString(), - context, - parentLoad - )))).flat() - ])) - - urlsBeingProcessed.delete(url) - return full + try { + const result = parseCjs(source) + const full = addDefault(result.exports) + + await Promise.all(result.reexports.map(async re => { + if (re.startsWith('node:') || builtinModules.includes(re)) { + for (const each of getExportsForNodeBuiltIn(re)) { + full.add(each) + } + } else { + // Resolve the re-exported module relative to the current module. + const newUrl = pathToFileURL(require.resolve(re, { paths: [dirname(fileURLToPath(url))] })).href + for (const each of await getExports(newUrl, context, parentLoad)) { + full.add(each) + } + } + })) + + return full + } finally { + urlsBeingProcessed.delete(url) + } } /** @@ -45,7 +69,7 @@ async function getFullCjsExports (url, context, parentLoad, source) { * @param {Function} parentLoad Next hook function in the loaders API * hook chain. * - * @returns {Promise} An array of identifiers exported by the module. + * @returns {Promise>} An array of identifiers exported by the module. * Please see {@link getEsmExports} for caveats on special identifiers that may * be included in the result set. */ @@ -57,23 +81,23 @@ async function getExports (url, context, parentLoad) { let source = parentCtx.source const format = parentCtx.format - // TODO support non-node/file urls somehow? - if (format === 'builtin') { - // Builtins don't give us the source property, so we're stuck - // just requiring it to get the exports. - return addDefault(Object.keys(require(url))) - } - if (!source) { - // Sometimes source is retrieved by parentLoad, sometimes it isn't. - source = fs.readFileSync(fileURLToPath(url), 'utf8') + if (format === 'builtin') { + // Builtins don't give us the source property, so we're stuck + // just requiring it to get the exports. + return getExportsForNodeBuiltIn(url) + } + + // Sometimes source is retrieved by parentLoad, CommonJs isn't. + source = readFileSync(fileURLToPath(url), 'utf8') } if (format === 'module') { return getEsmExports(source) } + if (format === 'commonjs') { - return getFullCjsExports(url, context, parentLoad, source) + return getCjsExports(url, context, parentLoad, source) } // At this point our `format` is either undefined or not known by us. Fall @@ -84,7 +108,7 @@ async function getExports (url, context, parentLoad) { // isn't set at first and yet we have an ESM module with no exports. // I couldn't construct an example that would do this, so maybe it's // impossible? - return getFullCjsExports(url, context, parentLoad, source) + return getCjsExports(url, context, parentLoad, source) } }