From aea0d64b31b780da2ab09503e7f78b9e7e752c70 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 7 Sep 2023 23:04:50 -0700 Subject: [PATCH] cjs/loader.js --- lib/internal/modules/cjs/loader.js | 288 ++++++++++++++++++++++++----- 1 file changed, 242 insertions(+), 46 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index e8f84aed3b6468..dec7dbdf72b821 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,24 +204,45 @@ 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); @@ -235,16 +265,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]; }; @@ -295,10 +333,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); } @@ -325,7 +370,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(); @@ -373,6 +421,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; } @@ -528,10 +614,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 = '' } = @@ -553,9 +647,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) { @@ -673,11 +768,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); @@ -692,6 +790,7 @@ if (isWindows) { CHAR_BACKWARD_SLASH && 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); @@ -723,7 +822,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); @@ -734,6 +836,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); @@ -762,6 +865,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); @@ -775,6 +883,7 @@ Module._resolveLookupPaths = function(request, parent) { StringPrototypeCharAt(request, 1) !== '/' && (!isWindows || StringPrototypeCharAt(request, 1) !== '\\'))) { + /** @type {string[]} */ let paths; if (parent?.paths?.length) { paths = ArrayPrototypeSlice(modulePaths); @@ -804,6 +913,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 ` + @@ -834,6 +947,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) && @@ -851,13 +970,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) { @@ -953,6 +1076,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; @@ -1041,6 +1173,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) { @@ -1055,6 +1195,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}'`); @@ -1064,7 +1209,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); @@ -1090,9 +1238,12 @@ Module.prototype.load = function(filename) { !cascadedLoader.cjsCache.has(this)) { 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 === '') { @@ -1107,11 +1258,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); @@ -1177,10 +1340,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; @@ -1235,7 +1400,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); @@ -1284,8 +1453,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'); @@ -1303,8 +1475,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) { @@ -1316,6 +1491,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 = @@ -1336,6 +1515,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; @@ -1357,6 +1542,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'); @@ -1387,6 +1575,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; } @@ -1408,6 +1600,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)) {