Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions lib/internal/modules/package_json_reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();

Expand All @@ -158,30 +167,51 @@ 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.
* @param {string} checkPath The path to start searching from.
* @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);
}

/**
Expand Down
Loading