diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 6c6bf0383bc3..c33cf166f53b 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -23,7 +23,8 @@ const { ERR_MODULE_NOT_FOUND, }, } = require('internal/errors'); -const { kEmptyObject } = require('internal/util'); +const { kEmptyObject, isWindows } = require('internal/util'); +const permission = require('internal/process/permission'); const modulesBinding = internalBinding('modules'); const path = require('path'); const { validateString } = require('internal/validators'); @@ -140,10 +141,18 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { } /** - * A cache mapping a module's path to its parent `package.json` file's path. - * This is used in concert with `deserializedPackageJSONCache` to improve - * the performance of `getNearestParentPackageJSON` when called repeatedly - * on the same module paths. + * A cache mapping a directory to the path of the nearest `package.json` at or + * above it (`null` when there is none). The native traversal for a module + * starts at the module's directory, so every module in a directory shares one + * entry and one native call. Used in concert with + * `deserializedPackageJSONCache`. + */ +const directoryToParentPackageJSONPathCache = new SafeMap(); + +/** + * When the permission model is enabled the native traversal also depends on + * the read permissions in effect at the time of the call, so results are only + * remembered per exact module path, as before. */ const moduleToParentPackageJSONCache = new SafeMap(); @@ -158,6 +167,21 @@ const moduleToParentPackageJSONCache = new SafeMap(); */ const deserializedPackageJSONCache = new SafeMap(); +/** + * The directory the native nearest-parent traversal starts from for `checkPath` + * (see BindingData::NormalizePath/TraverseParent): the path itself when it has + * a trailing separator, its dirname otherwise. + * @param {string} checkPath + * @returns {string} + */ +function getTraversalStartDirectory(checkPath) { + const last = checkPath[checkPath.length - 1]; + if (last === '/' || (isWindows && last === '\\')) { + return StringPrototypeSlice(checkPath, 0, -1); + } + return path.dirname(checkPath); +} + /** * 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 undefined. @@ -165,23 +189,29 @@ const deserializedPackageJSONCache = new SafeMap(); * @returns {undefined | DeserializedPackageConfig} */ function getNearestParentPackageJSON(checkPath) { - const parentPackageJSONPath = moduleToParentPackageJSONCache.get(checkPath); - if (parentPackageJSONPath !== undefined) { - return deserializedPackageJSONCache.get(parentPackageJSONPath); + const permissionEnabled = permission.isEnabled(); + const cache = permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache; + const key = permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath); + let parentPackageJSONPath = cache.get(key); + if (parentPackageJSONPath === undefined) { + const result = modulesBinding.getNearestParentPackageJSON(checkPath); + if (result === undefined) { + parentPackageJSONPath = null; + } else { + const packageConfig = deserializePackageJSON(checkPath, result); + parentPackageJSONPath = packageConfig.path; + if (!deserializedPackageJSONCache.has(parentPackageJSONPath)) { + deserializedPackageJSONCache.set(parentPackageJSONPath, packageConfig); + } + } + cache.set(key, parentPackageJSONPath); } - const result = modulesBinding.getNearestParentPackageJSON(checkPath); - const packageConfig = deserializePackageJSON(checkPath, result); - - moduleToParentPackageJSONCache.set(checkPath, packageConfig.path); - - const maybeCachedPackageConfig = deserializedPackageJSONCache.get(packageConfig.path); - if (maybeCachedPackageConfig !== undefined) { - return maybeCachedPackageConfig; + if (parentPackageJSONPath === null) { + // No package.json above this path: same shape as before, carrying the queried path. + return deserializePackageJSON(checkPath, undefined); } - - deserializedPackageJSONCache.set(packageConfig.path, packageConfig); - return packageConfig; + return deserializedPackageJSONCache.get(parentPackageJSONPath); } /**