'use strict' const getEsmExports = require('./get-esm-exports.js') 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 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 getCjsExports (url, context, parentLoad, source) { if (urlsBeingProcessed.has(url)) { return [] } urlsBeingProcessed.add(url) 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) } } /** * Inspects a module for its type (commonjs or module), attempts to get the * source code for said module from the loader API, and parses the result * for the entities exported from that module. * * @param {string} url A file URL string pointing to the module that * we should get the exports of. * @param {object} context Context object as provided by the `load` * hook from the loaders API. * @param {Function} parentLoad Next hook function in the loaders API * hook chain. * * @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. */ async function getExports (url, context, parentLoad) { // `parentLoad` gives us the possibility of getting the source // from an upstream loader. This doesn't always work though, // so later on we fall back to reading it from disk. const parentCtx = await parentLoad(url, context) let source = parentCtx.source const format = parentCtx.format if (!source) { 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 getCjsExports(url, context, parentLoad, source) } // At this point our `format` is either undefined or not known by us. Fall // back to parsing as ESM/CJS. const esmExports = getEsmExports(source) if (!esmExports.length) { // TODO(bengl) it's might be possible to get here if somehow the format // 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 getCjsExports(url, context, parentLoad, source) } } module.exports = getExports