diff --git a/lib/internal/dns/promises.js b/lib/internal/dns/promises.js index 79be8591bbcad2..1169b2735d4efe 100644 --- a/lib/internal/dns/promises.js +++ b/lib/internal/dns/promises.js @@ -113,6 +113,19 @@ function onlookupall(err, addresses) { } } +/** + * Creates a promise that resolves with the IP address of the given hostname. + * @param {0 | 4 | 6} family - The IP address family (4 or 6, or 0 for both). + * @param {string} hostname - The hostname to resolve. + * @param {boolean} all - Whether to resolve with all IP addresses for the hostname. + * @param {number} hints - One or more supported getaddrinfo flags (supply multiple via + * bitwise OR). + * @param {boolean} verbatim - Whether to use the hostname verbatim. + * @returns {Promise} The IP address(es) of the hostname. + * @typedef {object} DNSLookupResult + * @property {string} address - The IP address. + * @property {0 | 4 | 6} family - The IP address type. 4 for IPv4 or 6 for IPv6, or 0 (for both). + */ function createLookupPromise(family, hostname, all, hints, verbatim) { return new Promise((resolve, reject) => { if (!hostname) { @@ -154,6 +167,17 @@ function createLookupPromise(family, hostname, all, hints, verbatim) { } const validFamilies = [0, 4, 6]; +/** + * Get the IP address for a given hostname. + * @param {string} hostname - The hostname to resolve (ex. 'nodejs.org'). + * @param {object} [options] - Optional settings. + * @param {boolean} [options.all=false] - Whether to return all or just the first resolved address. + * @param {0 | 4 | 6} [options.family=0] - The record family. Must be 4, 6, or 0 (for both). + * @param {number} [options.hints] - One or more supported getaddrinfo flags (supply multiple via + * bitwise OR). + * @param {boolean} [options.verbatim=false] - Return results in same order DNS resolved them; + * otherwise IPv4 then IPv6. New code should supply `true`. + */ function lookup(hostname, options) { let hints = 0; let family = 0; diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 19a7d7e671f5ab..5fe9e874c7ad9f 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -155,6 +155,11 @@ let requireDepth = 0; let isPreloading = false; let statCache = null; +/** + * Our internal implementation of `require`. + * @param {Module} module Parent module of what is being required + * @param {string} id Specifier of the child module being imported + */ function internalRequire(module, id) { validateString(id, 'id'); if (id === '') { @@ -169,6 +174,10 @@ function internalRequire(module, id) { } } +/** + * Get a path's properties, using an in-memory cache to minimize lookups. + * @param {string} filename Absolute path to the file + */ function stat(filename) { filename = path.toNamespacedPath(filename); if (statCache !== null) { @@ -195,25 +204,46 @@ ObjectDefineProperty(Module, '_stat', { configurable: true, }); +/** + * Update the parent's children array with the child module. + * @param {Module} parent Module requiring the children + * @param {Module} child Module being required + * @param {boolean} scan Add the child to the parent's children if not already present + */ function updateChildren(parent, child, scan) { const children = parent?.children; if (children && !(scan && ArrayPrototypeIncludes(children, child))) ArrayPrototypePush(children, child); } +/** + * Tell the watch mode that a module was required. + * @param {string} filename Absolute path of the module + */ function reportModuleToWatchMode(filename) { if (shouldReportRequiredModules() && process.send) { process.send({ 'watch:require': [filename] }); } } +/** + * Tell the watch mode that a module was not found. + * @param {string} basePath The absolute path that errored + * @param {string[]} extensions The extensions that were tried + */ function reportModuleNotFoundToWatchMode(basePath, extensions) { if (shouldReportRequiredModules() && process.send) { process.send({ 'watch:require': ArrayPrototypeMap(extensions, (ext) => path.resolve(`${basePath}${ext}`)) }); } } +/** @type {Map} */ const moduleParentCache = new SafeWeakMap(); +/** + * Create a new module instance. + * @param {string} id + * @param {Module} parent + */ function Module(id = '', parent) { this.id = id; this.path = path.dirname(id); @@ -236,16 +266,24 @@ function Module(id = '', parent) { this[require_private_symbol] = internalRequire; } +/** @type {Record} */ Module._cache = { __proto__: null }; +/** @type {Record} */ Module._pathCache = { __proto__: null }; +/** @type {Record void>} */ Module._extensions = { __proto__: null }; +/** @type {string[]} */ let modulePaths = []; +/** @type {string[]} */ Module.globalPaths = []; let patched = false; -// eslint-disable-next-line func-style -let wrap = function(script) { +/** + * Add the CommonJS wrapper around a module's source code. + * @param {string} script Module source code + */ +let wrap = function(script) { // eslint-disable-line func-style return Module.wrapper[0] + script + Module.wrapper[1]; }; @@ -296,10 +334,17 @@ const isPreloadingDesc = { get() { return isPreloading; } }; ObjectDefineProperty(Module.prototype, 'isPreloading', isPreloadingDesc); ObjectDefineProperty(BuiltinModule.prototype, 'isPreloading', isPreloadingDesc); +/** + * Get the parent of the current module from our cache. + */ function getModuleParent() { return moduleParentCache.get(this); } +/** + * Set the parent of the current module in our cache. + * @param {Module} value + */ function setModuleParent(value) { moduleParentCache.set(this, value); } @@ -326,7 +371,10 @@ ObjectDefineProperty(Module.prototype, 'parent', { Module._debug = pendingDeprecate(debug, 'Module._debug is deprecated.', 'DEP0077'); Module.isBuiltin = BuiltinModule.isBuiltin; -// This function is called during pre-execution, before any user code is run. +/** + * Prepare to run CommonJS code. + * This function is called during pre-execution, before any user code is run. + */ function initializeCJS() { // This need to be done at runtime in case --expose-internals is set. const builtinModules = BuiltinModule.getCanBeRequiredByUsersWithoutSchemeList(); @@ -374,6 +422,11 @@ ObjectDefineProperty(Module, '_readPackage', { configurable: true, }); +/** + * Get the nearest parent package.json file from a given path. + * Return the package.json data and the path to the package.json file, or false. + * @param {string} checkPath The path to start searching from. + */ function readPackageScope(checkPath) { const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); let separatorIndex; @@ -397,6 +450,13 @@ function readPackageScope(checkPath) { return false; } +/** + * Try to load a specifier as a package. + * @param {string} requestPath The path to what we are trying to load + * @param {string[]} exts File extensions to try appending in order to resolve the file + * @param {boolean} isMain Whether the file is the main entry point of the app + * @param {string} originalPath The specifier passed to `require` + */ function tryPackage(requestPath, exts, isMain, originalPath) { const pkg = _readPackage(requestPath).main; @@ -434,15 +494,20 @@ function tryPackage(requestPath, exts, isMain, originalPath) { return actual; } -// In order to minimize unnecessary lstat() calls, -// this cache is a list of known-real paths. -// Set to an empty Map to reset. +/** + * Cache for storing resolved real paths of modules. + * In order to minimize unnecessary lstat() calls, this cache is a list of known-real paths. + * Set to an empty Map to reset. + * @type {Map} + */ const realpathCache = new SafeMap(); -// Check if the file exists and is not a directory -// if using --preserve-symlinks and isMain is false, -// keep symlinks intact, otherwise resolve to the -// absolute realpath. +/** + * Check if the file exists and is not a directory if using `--preserve-symlinks` and `isMain` is false, keep symlinks + * intact, otherwise resolve to the absolute realpath. + * @param {string} requestPath The path to the file to load. + * @param {boolean} isMain Whether the file is the main module. + */ function tryFile(requestPath, isMain) { const rc = _stat(requestPath); if (rc !== 0) return; @@ -452,16 +517,26 @@ function tryFile(requestPath, isMain) { return toRealPath(requestPath); } + +/** + * Resolves the path of a given `require` specifier, following symlinks. + * @param {string} requestPath The `require` specifier + */ function toRealPath(requestPath) { return fs.realpathSync(requestPath, { [internalFS.realpathCacheKey]: realpathCache, }); } -// Given a path, check if the file exists with any of the set extensions -function tryExtensions(p, exts, isMain) { +/** + * Given a path, check if the file exists with any of the set extensions. + * @param {string} basePath The path and filename without extension + * @param {string[]} exts The extensions to try + * @param {boolean} isMain Whether the module is the main module + */ +function tryExtensions(basePath, exts, isMain) { for (let i = 0; i < exts.length; i++) { - const filename = tryFile(p + exts[i], isMain); + const filename = tryFile(basePath + exts[i], isMain); if (filename) { return filename; @@ -470,8 +545,10 @@ function tryExtensions(p, exts, isMain) { return false; } -// Find the longest (possibly multi-dot) extension registered in -// Module._extensions +/** + * Find the longest (possibly multi-dot) extension registered in `Module._extensions`. + * @param {string} filename The filename to find the longest registered extension for. + */ function findLongestRegisteredExtension(filename) { const name = path.basename(filename); let currentExtension; @@ -486,6 +563,10 @@ function findLongestRegisteredExtension(filename) { return '.js'; } +/** + * Tries to get the absolute file path of the parent module. + * @param {Module} parent The parent module object. + */ function trySelfParentPath(parent) { if (!parent) return false; @@ -500,6 +581,11 @@ function trySelfParentPath(parent) { } } +/** + * Attempt to resolve a module request using the parent module package metadata. + * @param {string} parentPath The path of the parent module + * @param {string} request The module request to resolve + */ function trySelf(parentPath, request) { if (!parentPath) return false; @@ -529,10 +615,18 @@ function trySelf(parentPath, request) { } } -// This only applies to requests of a specific form: -// 1. name/.* -// 2. @scope/name/.* +/** + * This only applies to requests of a specific form: + * 1. `name/.*` + * 2. `@scope/name/.*` + */ const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; + +/** + * Resolves the exports for a given module path and request. + * @param {string} nmPath The path to the module. + * @param {string} request The request for the module. + */ function resolveExports(nmPath, request) { // The implementation's behavior is meant to mirror resolution in ESM. const { 1: name, 2: expansion = '' } = @@ -556,9 +650,10 @@ function resolveExports(nmPath, request) { } /** - * @param {string} request a relative or absolute file path - * @param {Array} paths file system directories to search as file paths - * @param {boolean} isMain if the request is the main app entry point + * Get the absolute path to a module. + * @param {string} request Relative or absolute file path + * @param {Array} paths Folders to search as file paths + * @param {boolean} isMain Whether the request is the main app entry point * @returns {string | false} */ Module._findPath = function(request, paths, isMain) { @@ -680,11 +775,14 @@ Module._findPath = function(request, paths, isMain) { return false; }; -// 'node_modules' character codes reversed +/** `node_modules` character codes reversed */ const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]; const nmLen = nmChars.length; if (isWindows) { - // 'from' is the __dirname of the module. + /** + * Get the paths to the `node_modules` folder for a given path. + * @param {string} from `__dirname` of the module + */ Module._nodeModulePaths = function(from) { // Guarantee that 'from' is absolute. from = path.resolve(from); @@ -700,6 +798,7 @@ if (isWindows) { StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON) return [from + 'node_modules']; + /** @type {string[]} */ const paths = []; for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { const code = StringPrototypeCharCodeAt(from, i); @@ -730,7 +829,10 @@ if (isWindows) { return paths; }; } else { // posix - // 'from' is the __dirname of the module. + /** + * Get the paths to the `node_modules` folder for a given path. + * @param {string} from `__dirname` of the module + */ Module._nodeModulePaths = function(from) { // Guarantee that 'from' is absolute. from = path.resolve(from); @@ -742,6 +844,7 @@ if (isWindows) { // note: this approach *only* works when the path is guaranteed // to be absolute. Doing a fully-edge-case-correct path.split // that works on both Windows and Posix is non-trivial. + /** @type {string[]} */ const paths = []; for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { const code = StringPrototypeCharCodeAt(from, i); @@ -769,6 +872,11 @@ if (isWindows) { }; } +/** + * Get the paths for module resolution. + * @param {string} request + * @param {Module} parent + */ Module._resolveLookupPaths = function(request, parent) { if (BuiltinModule.normalizeRequirableId(request)) { debug('looking for %j in []', request); @@ -782,6 +890,7 @@ Module._resolveLookupPaths = function(request, parent) { StringPrototypeCharAt(request, 1) !== '/' && (!isWindows || StringPrototypeCharAt(request, 1) !== '\\'))) { + /** @type {string[]} */ let paths; if (parent?.paths?.length) { paths = ArrayPrototypeSlice(modulePaths); @@ -811,6 +920,10 @@ Module._resolveLookupPaths = function(request, parent) { return parentDir; }; +/** + * Emits a warning when a non-existent property of module exports is accessed inside a circular dependency. + * @param {string} prop The name of the non-existent property. + */ function emitCircularRequireWarning(prop) { process.emitWarning( `Accessing non-existent property '${String(prop)}' of module exports ` + @@ -840,6 +953,12 @@ const CircularRequirePrototypeWarningProxy = new Proxy({}, { }, }); +/** + * Returns the exports object for a module that has a circular `require`. + * If the exports object is a plain object, it is wrapped in a proxy that warns + * about circular dependencies. + * @param {Module} module The module instance + */ function getExportsForCircularRequire(module) { if (module.exports && !isProxy(module.exports) && @@ -857,13 +976,17 @@ function getExportsForCircularRequire(module) { return module.exports; } -// Check the cache for the requested file. -// 1. If a module already exists in the cache: return its exports object. -// 2. If the module is native: call -// `BuiltinModule.prototype.compileForPublicLoader()` and return the exports. -// 3. Otherwise, create a new module for the file and save it to the cache. -// Then have it load the file contents before returning its exports -// object. +/** + * Load a module from cache if it exists, otherwise create a new module instance. + * 1. If a module already exists in the cache: return its exports object. + * 2. If the module is native: call + * `BuiltinModule.prototype.compileForPublicLoader()` and return the exports. + * 3. Otherwise, create a new module for the file and save it to the cache. + * Then have it load the file contents before returning its exports object. + * @param {string} request Specifier of module to load via `require` + * @param {string} parent Absolute path of the module importing the child + * @param {boolean} isMain Whether the module is the main entry point + */ Module._load = function(request, parent, isMain) { let relResolveCacheIdentifier; if (parent) { @@ -961,6 +1084,15 @@ Module._load = function(request, parent, isMain) { return module.exports; }; +/** + * Given a `require` string and its context, get its absolute file path. + * @param {string} request The specifier to resolve + * @param {Module} parent The module containing the `require` call + * @param {boolean} isMain Whether the module is the main entry point + * @param {ResolveFilenameOptions} options Options object + * @typedef {object} ResolveFilenameOptions + * @property {string[]} paths Paths to search for modules in + */ Module._resolveFilename = function(request, parent, isMain, options) { if (BuiltinModule.normalizeRequirableId(request)) { return request; @@ -1051,6 +1183,14 @@ Module._resolveFilename = function(request, parent, isMain, options) { throw err; }; +/** + * Finishes resolving an ES module specifier into an absolute file path. + * @param {string} resolved The resolved module specifier + * @param {string} parentPath The path of the parent module + * @param {string} pkgPath The path of the package.json file + * @throws {ERR_INVALID_MODULE_SPECIFIER} If the resolved module specifier contains encoded `/` or `\\` characters + * @throws {Error} If the module cannot be found + */ function finalizeEsmResolution(resolved, parentPath, pkgPath) { const { encodedSepRegEx } = require('internal/modules/esm/resolve'); if (RegExpPrototypeExec(encodedSepRegEx, resolved) !== null) @@ -1065,6 +1205,11 @@ function finalizeEsmResolution(resolved, parentPath, pkgPath) { throw err; } +/** + * Creates an error object for when a requested ES module cannot be found. + * @param {string} request The name of the requested module + * @param {string} [path] The path to the requested module + */ function createEsmNotFoundErr(request, path) { // eslint-disable-next-line no-restricted-syntax const err = new Error(`Cannot find module '${request}'`); @@ -1075,7 +1220,10 @@ function createEsmNotFoundErr(request, path) { return err; } -// Given a file name, pass it to the proper extension handler. +/** + * Given a file name, pass it to the proper extension handler. + * @param {string} filename The `require` specifier + */ Module.prototype.load = function(filename) { debug('load %j for module %j', filename, this.id); @@ -1101,9 +1249,12 @@ Module.prototype.load = function(filename) { cascadedLoader.cjsCache.set(this, exports); }; -// Loads a module at the given file path. Returns that module's -// `exports` property. -// Note: when using the experimental policy mechanism this function is overridden +/** + * Loads a module at the given file path. Returns that module's `exports` property. + * Note: when using the experimental policy mechanism this function is overridden. + * @param {string} id + * @throws {ERR_INVALID_ARG_TYPE} When `id` is not a string + */ Module.prototype.require = function(id) { validateString(id, 'id'); if (id === '') { @@ -1118,11 +1269,23 @@ Module.prototype.require = function(id) { } }; -// Resolved path to process.argv[1] will be lazily placed here -// (needed for setting breakpoint when called with --inspect-brk) +/** + * Resolved path to `process.argv[1]` will be lazily placed here + * (needed for setting breakpoint when called with `--inspect-brk`). + * @type {string | undefined} + */ let resolvedArgv; let hasPausedEntry = false; +/** @type {import('vm').Script} */ let Script; + +/** + * Wraps the given content in a script and runs it in a new context. + * @param {string} filename The name of the file being loaded + * @param {string} content The content of the file being loaded + * @param {Module} cjsModuleInstance The CommonJS loader instance + * @param {object} codeCache The SEA code cache + */ function wrapSafe(filename, content, cjsModuleInstance, codeCache) { if (patched) { const wrapper = Module.wrap(content); @@ -1188,10 +1351,12 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) { } } -// Run the file contents in the correct scope or sandbox. Expose -// the correct helper variables (require, module, exports) to -// the file. -// Returns exception, if any. +/** + * Run the file contents in the correct scope or sandbox. Expose the correct helper variables (`require`, `module`, + * `exports`) to the file. Returns exception, if any. + * @param {string} content The source code of the module + * @param {string} filename The file path of the module + */ Module.prototype._compile = function(content, filename) { let moduleURL; let redirects; @@ -1246,7 +1411,11 @@ Module.prototype._compile = function(content, filename) { return result; }; -// Native extension for .js +/** + * Native handler for `.js` files. + * @param {Module} module The module to compile + * @param {string} filename The file path of the module + */ Module._extensions['.js'] = function(module, filename) { // If already analyzed the source, then it will be cached. const cached = cjsParseCache.get(module); @@ -1295,8 +1464,11 @@ Module._extensions['.js'] = function(module, filename) { module._compile(content, filename); }; - -// Native extension for .json +/** + * Native handler for `.json` files. + * @param {Module} module The module to compile + * @param {string} filename The file path of the module + */ Module._extensions['.json'] = function(module, filename) { const content = fs.readFileSync(filename, 'utf8'); @@ -1314,8 +1486,11 @@ Module._extensions['.json'] = function(module, filename) { } }; - -// Native extension for .node +/** + * Native handler for `.node` files. + * @param {Module} module The module to compile + * @param {string} filename The file path of the module + */ Module._extensions['.node'] = function(module, filename) { const manifest = policy()?.manifest; if (manifest) { @@ -1327,6 +1502,10 @@ Module._extensions['.node'] = function(module, filename) { return process.dlopen(module, path.toNamespacedPath(filename)); }; +/** + * Creates a `require` function that can be used to load modules from the specified path. + * @param {string} filename The path to the module + */ function createRequireFromPath(filename) { // Allow a directory to be passed as the filename const trailingSlash = @@ -1347,6 +1526,12 @@ function createRequireFromPath(filename) { const createRequireError = 'must be a file URL object, file URL string, or ' + 'absolute path string'; +/** + * Creates a new `require` function that can be used to load modules. + * @param {string | URL} filename The path or URL to the module context for this `require` + * @throws {ERR_INVALID_ARG_VALUE} If `filename` is not a string or URL, or if it is a relative path that cannot be + * resolved to an absolute path. + */ function createRequire(filename) { let filepath; @@ -1368,6 +1553,9 @@ function createRequire(filename) { Module.createRequire = createRequire; +/** + * Define the paths to use for resolving a module. + */ Module._initPaths = function() { const homeDir = isWindows ? process.env.USERPROFILE : safeGetenv('HOME'); const nodePath = isWindows ? process.env.NODE_PATH : safeGetenv('NODE_PATH'); @@ -1398,6 +1586,10 @@ Module._initPaths = function() { Module.globalPaths = ArrayPrototypeSlice(modulePaths); }; +/** + * Handle modules loaded via `--require`. + * @param {string[]} requests The values of `--require` + */ Module._preloadModules = function(requests) { if (!ArrayIsArray(requests)) return; @@ -1421,6 +1613,10 @@ Module._preloadModules = function(requests) { isPreloading = false; }; +/** + * If the user has overridden an export from a builtin module, this function can ensure that the override is used in + * both CommonJS and ES module contexts. + */ Module.syncBuiltinESMExports = function syncBuiltinESMExports() { for (const mod of BuiltinModule.map.values()) { if (BuiltinModule.canBeRequiredWithoutScheme(mod.id)) { diff --git a/lib/internal/modules/esm/create_dynamic_module.js b/lib/internal/modules/esm/create_dynamic_module.js index 5d7f3fed60ed8e..09f46272e8cb9e 100644 --- a/lib/internal/modules/esm/create_dynamic_module.js +++ b/lib/internal/modules/esm/create_dynamic_module.js @@ -11,6 +11,11 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { debug = fn; }); +/** + * Creates an import statement for a given module path and index. + * @param {string} impt - The module path to import. + * @param {number} index - The index of the import statement. + */ function createImport(impt, index) { const imptPath = JSONStringify(impt); return `import * as $import_${index} from ${imptPath}; @@ -27,6 +32,17 @@ import.meta.exports[${nameStringLit}] = { };`; } +/** + * Creates a dynamic module with the given imports, exports, URL, and evaluate function. + * @param {string[]} imports - An array of imports. + * @param {string[]} exports - An array of exports. + * @param {string} [url=''] - The URL of the module. + * @param {(reflect: DynamicModuleReflect) => void} evaluate - The function to evaluate the module. + * @typedef {object} DynamicModuleReflect + * @property {string[]} imports - The imports of the module. + * @property {string[]} exports - The exports of the module. + * @property {(cb: (reflect: DynamicModuleReflect) => void) => void} onReady - Callback to evaluate the module. + */ const createDynamicModule = (imports, exports, url = '', evaluate) => { debug('creating ESM facade for %s with exports: %j', url, exports); const source = ` @@ -38,6 +54,7 @@ import.meta.done(); const m = new ModuleWrap(`${url}`, undefined, source, 0, 0); const readyfns = new SafeSet(); + /** @type {DynamicModuleReflect} */ const reflect = { exports: { __proto__: null }, onReady: (cb) => { readyfns.add(cb); }, diff --git a/lib/internal/modules/esm/fetch_module.js b/lib/internal/modules/esm/fetch_module.js index ca5c9c83c316de..21b7456899604f 100644 --- a/lib/internal/modules/esm/fetch_module.js +++ b/lib/internal/modules/esm/fetch_module.js @@ -44,37 +44,56 @@ const cacheForGET = new SafeMap(); // [2] Creating a new agent instead of using the gloabl agent improves // performance and precludes the agent becoming tainted. +/** @type {import('https').Agent} The Cached HTTP Agent for **secure** HTTP requests. */ let HTTPSAgent; -function HTTPSGet(url, opts) { +/** + * Make a HTTPs GET request (handling agent setup if needed, caching the agent to avoid + * redudant instantiations). + * @param {Parameters[0]} input - The URI to fetch. + * @param {Parameters[1]} options - See https.get() options. + */ +function HTTPSGet(input, options) { const https = require('https'); // [1] HTTPSAgent ??= new https.Agent({ // [2] keepAlive: true, }); - return https.get(url, { + return https.get(input, { agent: HTTPSAgent, - ...opts, + ...options, }); } +/** @type {import('https').Agent} The Cached HTTP Agent for **insecure** HTTP requests. */ let HTTPAgent; -function HTTPGet(url, opts) { +/** + * Make a HTTP GET request (handling agent setup if needed, caching the agent to avoid + * redudant instantiations). + * @param {Parameters[0]} input - The URI to fetch. + * @param {Parameters[1]} options - See http.get() options. + */ +function HTTPGet(input, options) { const http = require('http'); // [1] HTTPAgent ??= new http.Agent({ // [2] keepAlive: true, }); - return http.get(url, { + return http.get(input, { agent: HTTPAgent, - ...opts, + ...options, }); } -function dnsLookup(name, opts) { +/** @type {import('../../dns/promises.js').lookup} */ +function dnsLookup(hostname, options) { // eslint-disable-next-line no-func-assign dnsLookup = require('dns/promises').lookup; - return dnsLookup(name, opts); + return dnsLookup(hostname, options); } let zlib; +/** + * Create a decompressor for the Brotli format. + * @returns {import('zlib').BrotliDecompress} + */ function createBrotliDecompress() { zlib ??= require('zlib'); // [1] // eslint-disable-next-line no-func-assign @@ -82,6 +101,10 @@ function createBrotliDecompress() { return createBrotliDecompress(); } +/** + * Create an unzip handler. + * @returns {import('zlib').Unzip} + */ function createUnzip() { zlib ??= require('zlib'); // [1] // eslint-disable-next-line no-func-assign diff --git a/lib/internal/modules/esm/handle_process_exit.js b/lib/internal/modules/esm/handle_process_exit.js index 9d6b609ef1cfc3..4689ef6bb204c0 100644 --- a/lib/internal/modules/esm/handle_process_exit.js +++ b/lib/internal/modules/esm/handle_process_exit.js @@ -2,9 +2,11 @@ const { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors'); -// Handle a Promise from running code that potentially does Top-Level Await. -// In that case, it makes sense to set the exit code to a specific non-zero -// value if the main code never finishes running. +/** + * Handle a Promise from running code that potentially does Top-Level Await. + * In that case, it makes sense to set the exit code to a specific non-zero value + * if the main code never finishes running. + */ function handleProcessExit() { process.exitCode ??= kUnfinishedTopLevelAwait; } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 109ec9be8aee69..0b08db7ffef7ec 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -28,16 +28,28 @@ const { } = require('internal/modules/esm/utils'); let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer; +/** + * Lazy loads the module_map module and returns a new instance of ResolveCache. + * @returns {import('./module_map.js').ResolveCache')} + */ function newResolveCache() { const { ResolveCache } = require('internal/modules/esm/module_map'); return new ResolveCache(); } +/** + * Generate a load cache (to store the final result of a load-chain for a particular module). + * @returns {import('./module_map.js').LoadCache')} + */ function newLoadCache() { const { LoadCache } = require('internal/modules/esm/module_map'); return new LoadCache(); } +/** + * Lazy-load translators to avoid potentially unnecessary work at startup (ex if ESM is not used). + * @returns {import('./translators.js').Translators} + */ function getTranslators() { const { translators } = require('internal/modules/esm/translators'); return translators; diff --git a/lib/internal/modules/esm/package_config.js b/lib/internal/modules/esm/package_config.js index 4ca701d4810f74..5da47764c9de2c 100644 --- a/lib/internal/modules/esm/package_config.js +++ b/lib/internal/modules/esm/package_config.js @@ -7,8 +7,23 @@ const { URL, fileURLToPath } = require('internal/url'); const packageJsonReader = require('internal/modules/package_json_reader'); /** - * @param {URL | string} resolved - * @returns {PackageConfig} + * @typedef {object} PackageConfig + * @property {string} pjsonPath - The path to the package.json file. + * @property {boolean} exists - Whether the package.json file exists. + * @property {'none' | 'commonjs' | 'module'} type - The type of the package. + * @property {string} [name] - The name of the package. + * @property {string} [main] - The main entry point of the package. + * @property {PackageTarget} [exports] - The exports configuration of the package. + * @property {Record>} [imports] - The imports configuration of the package. + */ +/** + * @typedef {string | string[] | Record>} PackageTarget + */ + +/** + * Returns the package configuration for the given resolved URL. + * @param {URL | string} resolved - The resolved URL. + * @returns {PackageConfig} - The package configuration. */ function getPackageScopeConfig(resolved) { let packageJSONUrl = new URL('./package.json', resolved); diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index acb5ddca8af3d2..38ccf0a8496aa3 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -67,6 +67,13 @@ const { internalModuleStat } = internalBinding('fs'); const emittedPackageWarnings = new SafeSet(); +/** + * Emits a deprecation warning for the use of a deprecated trailing slash pattern mapping in the "exports" field + * module resolution of a package. + * @param {string} match - The deprecated trailing slash pattern mapping. + * @param {string} pjsonUrl - The URL of the package.json file. + * @param {string} base - The URL of the module that imported the package. + */ function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) { const pjsonPath = fileURLToPath(pjsonUrl); if (emittedPackageWarnings.has(pjsonPath + '|' + match)) @@ -84,6 +91,16 @@ function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) { const doubleSlashRegEx = /[/\\][/\\]/; +/** + * Emits a deprecation warning for invalid segment in module resolution. + * @param {string} target - The target module. + * @param {string} request - The requested module. + * @param {string} match - The matched module. + * @param {string} pjsonUrl - The package.json URL. + * @param {boolean} internal - Whether the module is in the "imports" or "exports" field. + * @param {string} base - The base URL. + * @param {boolean} isTarget - Whether the target is a module. + */ function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, internal, base, isTarget) { const pjsonPath = fileURLToPath(pjsonUrl); const double = RegExpPrototypeExec(doubleSlashRegEx, isTarget ? target : request) !== null; @@ -99,11 +116,12 @@ function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, interna } /** - * @param {URL} url - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base - * @param {string} [main] - * @returns {void} + * Emits a deprecation warning if the given URL is a module and + * the package.json file does not define a "main" or "exports" field. + * @param {URL} url - The URL of the module being resolved. + * @param {URL} packageJSONUrl - The URL of the package.json file for the module. + * @param {string | URL} [base] - The base URL for the module being resolved. + * @param {string} [main] - The "main" field from the package.json file. */ function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) { const format = defaultGetFormatWithoutErrors(url); @@ -198,10 +216,15 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) { const encodedSepRegEx = /%2F|%5C/i; /** - * @param {URL} resolved - * @param {string | URL | undefined} base - * @param {boolean} preserveSymlinks - * @returns {URL | undefined} + * Finalizes the resolution of a module specifier by checking if the resolved pathname contains encoded "/" or "\\" + * characters, checking if the resolved pathname is a directory or file, and resolving any symlinks if necessary. + * @param {URL} resolved - The resolved URL object. + * @param {string | URL | undefined} base - The base URL object. + * @param {boolean} preserveSymlinks - Whether to preserve symlinks or not. + * @returns {URL} - The finalized URL object. + * @throws {ERR_INVALID_MODULE_SPECIFIER} - If the resolved pathname contains encoded "/" or "\\" characters. + * @throws {ERR_UNSUPPORTED_DIR_IMPORT} - If the resolved pathname is a directory. + * @throws {ERR_MODULE_NOT_FOUND} - If the resolved pathname is not a file. */ function finalizeResolution(resolved, base, preserveSymlinks) { if (RegExpPrototypeExec(encodedSepRegEx, resolved.pathname) !== null) @@ -249,9 +272,11 @@ function finalizeResolution(resolved, base, preserveSymlinks) { } /** - * @param {string} specifier - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base + * Returns an error object indicating that the specified import is not defined. + * @param {string} specifier - The import specifier that is not defined. + * @param {URL} packageJSONUrl - The URL of the package.json file, or null if not available. + * @param {string | URL | undefined} base - The base URL to use for resolving relative URLs. + * @returns {ERR_PACKAGE_IMPORT_NOT_DEFINED} - The error object. */ function importNotDefined(specifier, packageJSONUrl, base) { return new ERR_PACKAGE_IMPORT_NOT_DEFINED( @@ -260,9 +285,11 @@ function importNotDefined(specifier, packageJSONUrl, base) { } /** - * @param {string} subpath - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base + * Returns an error object indicating that the specified subpath was not exported by the package. + * @param {string} subpath - The subpath that was not exported. + * @param {URL} packageJSONUrl - The URL of the package.json file. + * @param {string | URL | undefined} [base] - The base URL to use for resolving the subpath. + * @returns {ERR_PACKAGE_PATH_NOT_EXPORTED} - The error object. */ function exportsNotFound(subpath, packageJSONUrl, base) { return new ERR_PACKAGE_PATH_NOT_EXPORTED( @@ -271,12 +298,13 @@ function exportsNotFound(subpath, packageJSONUrl, base) { } /** - * - * @param {string} request - * @param {string} match - * @param {URL} packageJSONUrl - * @param {boolean} internal - * @param {string | URL | undefined} base + * Throws an error indicating that the given request is not a valid subpath match for the specified pattern. + * @param {string} request - The request that failed to match the pattern. + * @param {string} match - The pattern that the request was compared against. + * @param {URL} packageJSONUrl - The URL of the package.json file being resolved. + * @param {boolean} internal - Whether the resolution is for an "imports" or "exports" field in package.json. + * @param {string | URL | undefined} base - The base URL for the resolution. + * @throws {ERR_INVALID_MODULE_SPECIFIER} When the request is not a valid match for the pattern. */ function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) { const reason = `request is not a valid match in pattern "${match}" for the "${ @@ -286,6 +314,15 @@ function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) { base && fileURLToPath(base)); } +/** + * Creates an error object for an invalid package target. + * @param {string} subpath - The subpath. + * @param {import('internal/modules/esm/package_config.js').PackageTarget} target - The target. + * @param {URL} packageJSONUrl - The URL of the package.json file. + * @param {boolean} internal - Whether the package is internal. + * @param {string | URL | undefined} base - The base URL. + * @returns {ERR_INVALID_PACKAGE_TARGET} - The error object. + */ function invalidPackageTarget( subpath, target, packageJSONUrl, internal, base) { if (typeof target === 'object' && target !== null) { @@ -304,17 +341,19 @@ const invalidPackageNameRegEx = /^\.|%|\\/; const patternRegEx = /\*/g; /** - * - * @param {string} target - * @param {*} subpath - * @param {*} match - * @param {*} packageJSONUrl - * @param {*} base - * @param {*} pattern - * @param {*} internal - * @param {*} isPathMap - * @param {*} conditions - * @returns {URL} + * Resolves the package target string to a URL object. + * @param {string} target - The target string to resolve. + * @param {string} subpath - The subpath to append to the resolved URL. + * @param {RegExpMatchArray} match - The matched string array from the import statement. + * @param {string} packageJSONUrl - The URL of the package.json file. + * @param {string} base - The base URL to resolve the target against. + * @param {RegExp} pattern - The pattern to replace in the target string. + * @param {boolean} internal - Whether the target is internal to the package. + * @param {boolean} isPathMap - Whether the target is a path map. + * @param {string[]} conditions - The import conditions. + * @returns {URL} - The resolved URL object. + * @throws {ERR_INVALID_PACKAGE_TARGET} - If the target is invalid. + * @throws {ERR_INVALID_SUBPATH} - If the subpath is invalid. */ function resolvePackageTargetString( target, @@ -395,8 +434,9 @@ function resolvePackageTargetString( } /** - * @param {string} key - * @returns {boolean} + * Checks if the given key is a valid array index. + * @param {string} key - The key to check. + * @returns {boolean} - Returns `true` if the key is a valid array index, else `false`. */ function isArrayIndex(key) { const keyNum = +key; @@ -405,17 +445,17 @@ function isArrayIndex(key) { } /** - * - * @param {*} packageJSONUrl - * @param {string|[string]} target - * @param {*} subpath - * @param {*} packageSubpath - * @param {*} base - * @param {*} pattern - * @param {*} internal - * @param {*} isPathMap - * @param {*} conditions - * @returns {URL|null} + * Resolves the target of a package based on the provided parameters. + * @param {string} packageJSONUrl - The URL of the package.json file. + * @param {import('internal/modules/esm/package_config.js').PackageTarget} target - The target to resolve. + * @param {string} subpath - The subpath to resolve. + * @param {string} packageSubpath - The subpath of the package to resolve. + * @param {string} base - The base path to resolve. + * @param {RegExp} pattern - The pattern to match. + * @param {boolean} internal - Whether the package is internal. + * @param {boolean} isPathMap - Whether the package is a path map. + * @param {Set} conditions - The conditions to match. + * @returns {URL | null | undefined} - The resolved target, or null if not found, or undefined if not resolvable. */ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, base, pattern, internal, isPathMap, conditions) { @@ -486,11 +526,10 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, } /** - * - * @param {import('internal/modules/esm/package_config.js').Exports} exports - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base - * @returns {boolean} + * Is the given exports object using the shorthand syntax? + * @param {import('internal/modules/esm/package_config.js').PackageConfig['exports']} exports + * @param {URL} packageJSONUrl The URL of the package.json file. + * @param {string | URL | undefined} base The base URL. */ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { if (typeof exports === 'string' || ArrayIsArray(exports)) return true; @@ -516,12 +555,13 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { } /** - * @param {URL} packageJSONUrl - * @param {string} packageSubpath - * @param {PackageConfig} packageConfig - * @param {string | URL | undefined} base - * @param {Set} conditions - * @returns {URL} + * Resolves the exports of a package. + * @param {URL} packageJSONUrl - The URL of the package.json file. + * @param {string} packageSubpath - The subpath of the package to resolve. + * @param {import('internal/modules/esm/package_config.js').PackageConfig} packageConfig - The package metadata. + * @param {string | URL | undefined} base - The base path to resolve from. + * @param {Set} conditions - An array of conditions to match. + * @returns {URL} - The resolved package target. */ function packageExportsResolve( packageJSONUrl, packageSubpath, packageConfig, base, conditions) { @@ -598,6 +638,13 @@ function packageExportsResolve( throw exportsNotFound(packageSubpath, packageJSONUrl, base); } +/** + * Compares two strings that may contain a wildcard character ('*') and returns a value indicating their order. + * @param {string} a - The first string to compare. + * @param {string} b - The second string to compare. + * @returns {number} - A negative number if `a` should come before `b`, a positive number if `a` should come after `b`, + * or 0 if they are equal. + */ function patternKeyCompare(a, b) { const aPatternIndex = StringPrototypeIndexOf(a, '*'); const bPatternIndex = StringPrototypeIndexOf(b, '*'); @@ -613,10 +660,13 @@ function patternKeyCompare(a, b) { } /** - * @param {string} name - * @param {string | URL | undefined} base - * @param {Set} conditions - * @returns {URL} + * Resolves the given import name for a package. + * @param {string} name - The name of the import to resolve. + * @param {string | URL | undefined} base - The base URL to resolve the import from. + * @param {Set} conditions - An object containing the import conditions. + * @throws {ERR_INVALID_MODULE_SPECIFIER} If the import name is not valid. + * @throws {ERR_PACKAGE_IMPORT_NOT_DEFINED} If the import name cannot be resolved. + * @returns {URL} The resolved import URL. */ function packageImportsResolve(name, base, conditions) { if (name === '#' || StringPrototypeStartsWith(name, '#/') || @@ -679,8 +729,8 @@ function packageImportsResolve(name, base, conditions) { } /** - * @param {URL} url - * @returns {import('internal/modules/esm/package_config.js').PackageType} + * Returns the package type for a given URL. + * @param {URL} url - The URL to get the package type for. */ function getPackageType(url) { const packageConfig = getPackageScopeConfig(url); @@ -688,9 +738,9 @@ function getPackageType(url) { } /** - * @param {string} specifier - * @param {string | URL | undefined} base - * @returns {{ packageName: string, packageSubpath: string, isScoped: boolean }} + * Parse a package name from a specifier. + * @param {string} specifier - The import specifier. + * @param {string | URL | undefined} base - The parent URL. */ function parsePackageName(specifier, base) { let separatorIndex = StringPrototypeIndexOf(specifier, '/'); @@ -726,10 +776,11 @@ function parsePackageName(specifier, base) { } /** - * @param {string} specifier - * @param {string | URL | undefined} base - * @param {Set} conditions - * @returns {resolved: URL, format? : string} + * Resolves a package specifier to a URL. + * @param {string} specifier - The package specifier to resolve. + * @param {string | URL | undefined} base - The base URL to use for resolution. + * @param {Set} conditions - An object containing the conditions for resolution. + * @returns {URL} - The resolved URL. */ function packageResolve(specifier, base, conditions) { if (BuiltinModule.canBeRequiredWithoutScheme(specifier)) { @@ -790,13 +841,17 @@ function packageResolve(specifier, base, conditions) { } /** - * @param {string} specifier - * @returns {boolean} + * Checks if a specifier is a bare specifier. + * @param {string} specifier - The specifier to check. */ function isBareSpecifier(specifier) { return specifier[0] && specifier[0] !== '/' && specifier[0] !== '.'; } +/** + * Determines whether a specifier is a relative path. + * @param {string} specifier - The specifier to check. + */ function isRelativeSpecifier(specifier) { if (specifier[0] === '.') { if (specifier.length === 1 || specifier[1] === '/') return true; @@ -807,6 +862,10 @@ function isRelativeSpecifier(specifier) { return false; } +/** + * Determines whether a specifier should be treated as a relative or absolute path. + * @param {string} specifier - The specifier to check. + */ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) { if (specifier === '') return false; if (specifier[0] === '/') return true; @@ -814,11 +873,11 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) { } /** - * @param {string} specifier - * @param {string | URL | undefined} base - * @param {Set} conditions - * @param {boolean} preserveSymlinks - * @returns {url: URL, format?: string} + * Resolves a module specifier to a URL. + * @param {string} specifier - The module specifier to resolve. + * @param {string | URL | undefined} base - The base URL to resolve against. + * @param {Set} conditions - An object containing environment conditions. + * @param {boolean} preserveSymlinks - Whether to preserve symlinks in the resolved URL. */ function moduleResolve(specifier, base, conditions, preserveSymlinks) { const isRemote = base.protocol === 'http:' || @@ -846,10 +905,9 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) { } /** - * Try to resolve an import as a CommonJS module - * @param {string} specifier - * @param {string} parentURL - * @returns {boolean|string} + * Try to resolve an import as a CommonJS module. + * @param {string} specifier - The specifier to resolve. + * @param {string} parentURL - The base URL. */ function resolveAsCommonJS(specifier, parentURL) { try { @@ -891,7 +949,14 @@ function resolveAsCommonJS(specifier, parentURL) { } } -// TODO(@JakobJingleheimer): de-dupe `specifier` & `parsed` +/** + * Throw an error if an import is not allowed. + * TODO(@JakobJingleheimer): de-dupe `specifier` & `parsed` + * @param {string} specifier - The import specifier. + * @param {URL} parsed - The parsed URL of the import specifier. + * @param {URL} parsedParentURL - The parsed URL of the parent module. + * @throws {ERR_NETWORK_IMPORT_DISALLOWED} - If the import is disallowed. + */ function checkIfDisallowedImport(specifier, parsed, parsedParentURL) { if (parsedParentURL) { // Avoid accessing the `protocol` property due to the lazy getters. @@ -937,6 +1002,7 @@ function checkIfDisallowedImport(specifier, parsed, parsedParentURL) { /** * Validate user-input in `context` supplied by a custom loader. + * @param {string | URL | undefined} parentURL - The parent URL. */ function throwIfInvalidParentURL(parentURL) { if (parentURL === undefined) { @@ -947,7 +1013,15 @@ function throwIfInvalidParentURL(parentURL) { } } - +/** + * Resolves the given specifier using the provided context, which includes the parent URL and conditions. + * Throws an error if the parent URL is invalid or if the resolution is disallowed by the policy manifest. + * Otherwise, attempts to resolve the specifier and returns the resulting URL and format. + * @param {string} specifier - The specifier to resolve. + * @param {object} [context={}] - The context object containing the parent URL and conditions. + * @param {string} [context.parentURL] - The URL of the parent module. + * @param {string[]} [context.conditions] - The conditions for resolving the specifier. + */ function defaultResolve(specifier, context = {}) { let { parentURL, conditions } = context; throwIfInvalidParentURL(parentURL); diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index b143cd0ad34d0e..cf13aadcb4645f 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -18,7 +18,11 @@ const { globalThis: { WebAssembly }, } = primordials; +/** @type {import('internal/util/types')} */ let _TYPES = null; +/** + * Lazily loads and returns the internal/util/types module. + */ function lazyTypes() { if (_TYPES !== null) return _TYPES; return _TYPES = require('internal/util/types'); @@ -52,7 +56,13 @@ const asyncESM = require('internal/process/esm_loader'); const { emitWarningSync } = require('internal/process/warning'); const { internalCompileFunction } = require('internal/vm'); +/** @type {import('deps/cjs-module-lexer/lexer.js').parse} */ let cjsParse; +/** + * Initializes the CommonJS module lexer parser. + * If WebAssembly is available, it uses the optimized version from the dist folder. + * Otherwise, it falls back to the JavaScript version from the lexer folder. + */ async function initCJSParse() { if (typeof WebAssembly === 'undefined') { cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; @@ -73,6 +83,14 @@ exports.translators = translators; exports.enrichCJSError = enrichCJSError; let DECODER = null; +/** + * Asserts that the given body is a buffer source (either a string, array buffer, or typed array). + * Throws an error if the body is not a buffer source. + * @param {string | ArrayBufferView | ArrayBuffer} body - The body to check. + * @param {boolean} allowString - Whether or not to allow a string as a valid buffer source. + * @param {string} hookName - The name of the hook being called. + * @throws {ERR_INVALID_RETURN_PROPERTY_VALUE} If the body is not a buffer source. + */ function assertBufferSource(body, allowString, hookName) { if (allowString && typeof body === 'string') { return; @@ -89,6 +107,11 @@ function assertBufferSource(body, allowString, hookName) { ); } +/** + * Converts a buffer or buffer-like object to a string. + * @param {string | ArrayBuffer | ArrayBufferView} body - The buffer or buffer-like object to convert to a string. + * @returns {string} The resulting string. + */ function stringify(body) { if (typeof body === 'string') return body; assertBufferSource(body, false, 'transformSource'); @@ -97,6 +120,10 @@ function stringify(body) { return DECODER.decode(body); } +/** + * Converts a URL to a file path if the URL protocol is 'file:'. + * @param {string} url - The URL to convert. + */ function errPath(url) { const parsed = new URL(url); if (parsed.protocol === 'file:') { @@ -105,6 +132,14 @@ function errPath(url) { return url; } +/** + * Dynamically imports a module using the ESM loader. + * @param {string} specifier - The module specifier to import. + * @param {object} options - An object containing options for the import. + * @param {string} options.url - The URL of the module requesting the import. + * @param {Record} [assertions] - An object containing assertions for the import. + * @returns {Promise} The imported module. + */ async function importModuleDynamically(specifier, { url }, assertions) { return asyncESM.esmLoader.import(specifier, url, assertions); } @@ -126,6 +161,7 @@ translators.set('module', async function moduleStrategy(url, source, isMain) { }); /** + * Provide a more informative error for CommonJS imports. * @param {Error | any} err * @param {string} [content] Content of the file, if known. * @param {string} [filename] Useful only if `content` is unknown. @@ -148,6 +184,10 @@ function enrichCJSError(err, content, filename) { * This translator creates its own version of the `require` function passed into CommonJS modules. * Any monkey patches applied to the CommonJS Loader will not affect this module. * Any `require` calls in this module will load all children in the same way. + * @param {import('internal/modules/cjs/loader').Module} module - The module to load. + * @param {string} source - The source code of the module. + * @param {string} url - The URL of the module. + * @param {string} filename - The filename of the module. */ function loadCJSModule(module, source, url, filename) { let compiledWrapper; @@ -212,6 +252,14 @@ function loadCJSModule(module, source, url, filename) { // TODO: can we use a weak map instead? const cjsCache = new SafeMap(); +/** + * Creates a ModuleWrap object for a CommonJS module. + * @param {string} url - The URL of the module. + * @param {string} source - The source code of the module. + * @param {boolean} isMain - Whether the module is the main module. + * @param {typeof loadCJSModule} [loadCJS=loadCJSModule] - The function to load the CommonJS module. + * @returns {ModuleWrap} The ModuleWrap object for the CommonJS module. + */ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) { debug(`Translating CJSModule ${url}`); @@ -298,6 +346,11 @@ translators.set('commonjs', async function commonjsStrategy(url, source, }); +/** + * Pre-parses a CommonJS module's exports and re-exports. + * @param {string} filename - The filename of the module. + * @param {string} [source] - The source code of the module. + */ function cjsPreparseModuleExports(filename, source) { // TODO: Do we want to keep hitting the user mutable CJS loader here? let module = CJSModule._cache[filename]; diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 98578438302445..2c4b2a4a3c2aec 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -1,11 +1,6 @@ 'use strict'; -const { - ArrayIsArray, - SafeSet, - SafeWeakMap, - ObjectFreeze, -} = primordials; +const { ArrayIsArray, SafeSet, SafeWeakMap, ObjectFreeze } = primordials; const { privateSymbols: { @@ -29,18 +24,28 @@ const { const assert = require('internal/assert'); let defaultConditions; +/** + * Returns the default conditions for ES module loading. + */ function getDefaultConditions() { assert(defaultConditions !== undefined); return defaultConditions; } +/** @type {Set} */ let defaultConditionsSet; +/** + * Returns the default conditions for ES module loading, as a Set. + */ function getDefaultConditionsSet() { assert(defaultConditionsSet !== undefined); return defaultConditionsSet; } -// This function is called during pre-execution, before any user code is run. +/** + * Initializes the default conditions for ESM module loading. + * This function is called during pre-execution, before any user code is run. + */ function initializeDefaultConditions() { const userConditions = getOptionValue('--conditions'); const noAddons = getOptionValue('--no-addons'); @@ -62,8 +67,11 @@ function initializeDefaultConditions() { function getConditionsSet(conditions) { if (conditions !== undefined && conditions !== getDefaultConditions()) { if (!ArrayIsArray(conditions)) { - throw new ERR_INVALID_ARG_VALUE('conditions', conditions, - 'expected an array'); + throw new ERR_INVALID_ARG_VALUE( + 'conditions', + conditions, + 'expected an array', + ); } return new SafeSet(conditions); } @@ -144,9 +152,13 @@ async function importModuleDynamicallyCallback(symbol, specifier, assertions) { throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING(); } -// This is configured during pre-execution. Specifically it's set to true for -// the loader worker in internal/main/worker_thread.js. let _isLoaderWorker = false; +/** + * Initializes handling of ES modules. + * This is configured during pre-execution. Specifically it's set to true for + * the loader worker in internal/main/worker_thread.js. + * @param {boolean} [isLoaderWorker=false] - A boolean indicating whether the loader is a worker or not. + */ function initializeESM(isLoaderWorker = false) { _isLoaderWorker = isLoaderWorker; initializeDefaultConditions(); @@ -156,10 +168,17 @@ function initializeESM(isLoaderWorker = false) { setImportModuleDynamicallyCallback(importModuleDynamicallyCallback); } +/** + * Determine whether the current process is a loader worker. + * @returns {boolean} Whether the current process is a loader worker. + */ function isLoaderWorker() { return _isLoaderWorker; } +/** + * Register module customization hooks. + */ async function initializeHooks() { const customLoaderURLs = getOptionValue('--experimental-loader'); @@ -181,10 +200,7 @@ async function initializeHooks() { const parentURL = getCWDURL().href; for (let i = 0; i < customLoaderURLs.length; i++) { - await hooks.register( - customLoaderURLs[i], - parentURL, - ); + await hooks.register(customLoaderURLs[i], parentURL); } const preloadScripts = hooks.initializeGlobalPreload(); diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js index 380906accf6fe9..2d45b3b9f46234 100644 --- a/lib/internal/modules/esm/worker.js +++ b/lib/internal/modules/esm/worker.js @@ -32,6 +32,11 @@ const { const { initializeHooks } = require('internal/modules/esm/utils'); +/** + * Transfers an ArrayBuffer, TypedArray, or DataView to a worker thread. + * @param {boolean} hasError - Whether an error occurred during transfer. + * @param {ArrayBuffer | TypedArray | DataView} source - The data to transfer. + */ function transferArrayBuffer(hasError, source) { if (hasError || source == null) return; if (isArrayBuffer(source)) return [source]; @@ -39,6 +44,11 @@ function transferArrayBuffer(hasError, source) { if (isDataView(source)) return [DataViewPrototypeGetBuffer(source)]; } +/** + * Wraps a message with a status and body, and serializes the body if necessary. + * @param {string} status - The status of the message. + * @param {unknown} body - The body of the message. + */ function wrapMessage(status, body) { if (status === 'success' || body === null || (typeof body !== 'object' && @@ -65,6 +75,14 @@ function wrapMessage(status, body) { }; } +/** + * Initializes a worker thread for a customized module loader. + * @param {SharedArrayBuffer} lock - The lock used to synchronize communication between the worker and the main thread. + * @param {MessagePort} syncCommPort - The message port used for synchronous communication between the worker and the + * main thread. + * @param {(err: Error, origin?: string) => void} errorHandler - The function to use for uncaught exceptions. + * @returns {Promise} A promise that resolves when the worker thread has been initialized. + */ async function customizedModuleWorker(lock, syncCommPort, errorHandler) { let hooks, preloadScripts, initializationError; let hasInitializationError = false; @@ -107,6 +125,9 @@ async function customizedModuleWorker(lock, syncCommPort, errorHandler) { AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION); let immediate; + /** + * Checks for messages on the syncCommPort and handles them asynchronously. + */ function checkForMessages() { immediate = setImmediate(checkForMessages).unref(); // We need to let the event loop tick a few times to give the main thread a chance to send @@ -140,6 +161,13 @@ async function customizedModuleWorker(lock, syncCommPort, errorHandler) { setImmediate(() => {}); }); + /** + * Handles incoming messages from the main thread or other workers. + * @param {object} options - The options object. + * @param {string} options.method - The name of the hook. + * @param {Array} options.args - The arguments to pass to the method. + * @param {MessagePort} options.port - The message port to use for communication. + */ async function handleMessage({ method, args, port }) { // Each potential exception needs to be caught individually so that the correct error is sent to // the main thread. @@ -198,11 +226,19 @@ async function customizedModuleWorker(lock, syncCommPort, errorHandler) { } /** + * Initializes a worker thread for a module with customized hooks. * ! Run everything possible within this function so errors get reported. + * @param {{lock: SharedArrayBuffer}} workerData - The lock used to synchronize with the main thread. + * @param {MessagePort} syncCommPort - The communication port used to communicate with the main thread. */ module.exports = function setupModuleWorker(workerData, syncCommPort) { const lock = new Int32Array(workerData.lock); + /** + * Handles errors that occur in the worker thread. + * @param {Error} err - The error that occurred. + * @param {string} [origin='unhandledRejection'] - The origin of the error. + */ function errorHandler(err, origin = 'unhandledRejection') { AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1); AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION); diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index 307a34cb09b512..cc32e95c4eb413 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -37,7 +37,13 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => { debug = fn; }); +/** @typedef {import('internal/modules/cjs/loader.js').Module} Module */ + +/** @type {Set} */ let cjsConditions; +/** + * Define the conditions that apply to the CommonJS loader. + */ function initializeCjsConditions() { const userConditions = getOptionValue('--conditions'); const noAddons = getOptionValue('--no-addons'); @@ -51,6 +57,9 @@ function initializeCjsConditions() { ]); } +/** + * Get the conditions that apply to the CommonJS loader. + */ function getCjsConditions() { if (cjsConditions === undefined) { initializeCjsConditions(); @@ -58,27 +67,45 @@ function getCjsConditions() { return cjsConditions; } -function loadBuiltinModule(filename, request) { - if (!BuiltinModule.canBeRequiredByUsers(filename)) { +/** + * Provide one of Node.js' public modules to user code. + * @param {string} id - The identifier/specifier of the builtin module to load + * @param {string} request - The module requiring or importing the builtin module + */ +function loadBuiltinModule(id, request) { + if (!BuiltinModule.canBeRequiredByUsers(id)) { return; } - const mod = BuiltinModule.map.get(filename); + /** @type {import('internal/bootstrap/realm.js').BuiltinModule} */ + const mod = BuiltinModule.map.get(id); debug('load built-in module %s', request); // compileForPublicLoader() throws if canBeRequiredByUsers is false: mod.compileForPublicLoader(); return mod; } +/** @type {Module} */ let $Module = null; +/** + * Import the Module class on first use. + */ function lazyModule() { $Module = $Module || require('internal/modules/cjs/loader').Module; return $Module; } -// Invoke with makeRequireFunction(module) where |module| is the Module object -// to use as the context for the require() function. -// Use redirects to set up a mapping from a policy and restrict dependencies +/** + * Invoke with `makeRequireFunction(module)` where `module` is the `Module` object to use as the context for the + * `require()` function. + * Use redirects to set up a mapping from a policy and restrict dependencies. + */ const urlToFileCache = new SafeMap(); +/** + * Create the module-scoped `require` function to pass into CommonJS modules. + * @param {Module} mod - The module to create the `require` function for. + * @param {ReturnType} redirects + * @typedef {(specifier: string) => unknown} RequireFunction + */ function makeRequireFunction(mod, redirects) { // lazy due to cycle const Module = lazyModule(); @@ -86,6 +113,7 @@ function makeRequireFunction(mod, redirects) { throw new ERR_INVALID_ARG_TYPE('mod', 'Module', mod); } + /** @type {RequireFunction} */ let require; if (redirects) { const id = mod.filename || mod.id; @@ -131,6 +159,11 @@ function makeRequireFunction(mod, redirects) { }; } + /** + * The `resolve` method that gets attached to module-scope `require`. + * @param {string} request + * @param {Parameters[3]} options + */ function resolve(request, options) { validateString(request, 'request'); return Module._resolveFilename(request, mod, false, options); @@ -138,6 +171,10 @@ function makeRequireFunction(mod, redirects) { require.resolve = resolve; + /** + * The `paths` method that gets attached to module-scope `require`. + * @param {string} request + */ function paths(request) { validateString(request, 'request'); return Module._resolveLookupPaths(request, mod); @@ -159,6 +196,7 @@ function makeRequireFunction(mod, redirects) { * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) * because the buffer-to-string conversion in `fs.readFileSync()` * translates it to FEFF, the UTF-16 BOM. + * @param {string} content */ function stripBOM(content) { if (StringPrototypeCharCodeAt(content) === 0xFEFF) { @@ -167,6 +205,11 @@ function stripBOM(content) { return content; } +/** + * Add built-in modules to a global or REPL scope object. + * @param {Record} object - The object such as `globalThis` to add the built-in modules to. + * @param {string} dummyModuleName - The label representing the set of built-in modules to add. + */ function addBuiltinLibsToObject(object, dummyModuleName) { // Make built-in modules available directly (loaded lazily). const Module = require('internal/modules/cjs/loader').Module; @@ -227,9 +270,8 @@ function addBuiltinLibsToObject(object, dummyModuleName) { } /** - * + * If a referrer is an URL instance or absolute path, convert it into an URL string. * @param {string | URL} referrer - * @returns {string} */ function normalizeReferrerURL(referrer) { if (typeof referrer === 'string' && path.isAbsolute(referrer)) { @@ -238,7 +280,10 @@ function normalizeReferrerURL(referrer) { return new URL(referrer).href; } -// For error messages only - used to check if ESM syntax is in use. +/** + * For error messages only, check if ESM syntax is in use. + * @param {string} code + */ function hasEsmSyntax(code) { debug('Checking for ESM syntax'); const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 0bfe7b11241416..7ffb87e11506f7 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -7,6 +7,10 @@ const { const { getOptionValue } = require('internal/options'); const path = require('path'); +/** + * Get the absolute path to the main entry point. + * @param {string} main Entry point path + */ function resolveMainPath(main) { // Note extension resolution for the main entry point can be deprecated in a // future major. @@ -23,6 +27,10 @@ function resolveMainPath(main) { return mainPath; } +/** + * Determine whether the main entry point should be loaded through the ESM Loader. + * @param {string} mainPath Absolute path to the main entry point + */ function shouldUseESMLoader(mainPath) { /** * @type {string[]} userLoaders A list of custom loaders registered by the user @@ -46,6 +54,10 @@ function shouldUseESMLoader(mainPath) { return pkg && pkg.data.type === 'module'; } +/** + * Run the main entry point through the ESM Loader. + * @param {string} mainPath Absolute path to the main entry point + */ function runMainESM(mainPath) { const { loadESM } = require('internal/process/esm_loader'); const { pathToFileURL } = require('internal/url'); @@ -57,6 +69,10 @@ function runMainESM(mainPath) { })); } +/** + * Handle process exit events around the main entry point promise. + * @param {Promise} promise Main entry point promise + */ async function handleMainPromise(promise) { const { handleProcessExit, @@ -69,9 +85,12 @@ async function handleMainPromise(promise) { } } -// For backwards compatibility, we have to run a bunch of -// monkey-patchable code that belongs to the CJS loader (exposed by -// `require('module')`) even when the entry point is ESM. +/** + * Parse the CLI main entry point string and run it. + * For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed + * by `require('module')`) even when the entry point is ESM. + * @param {string} main CLI main entry point string + */ function executeUserEntryPoint(main = process.argv[1]) { const resolvedMain = resolveMainPath(main); const useESMLoader = shouldUseESMLoader(resolvedMain);