src: reduce the nearest parent package JSON cache size · nodejs/node@ac78491
@@ -6,7 +6,9 @@ const {
66 ObjectDefineProperty,
77 RegExpPrototypeExec,
88 SafeMap,
9+ StringPrototypeEndsWith,
910 StringPrototypeIndexOf,
11+ StringPrototypeLastIndexOf,
1012 StringPrototypeSlice,
1113} = primordials;
1214const {
@@ -26,6 +28,7 @@ const {
2628const { kEmptyObject } = require('internal/util');
2729const modulesBinding = internalBinding('modules');
2830const path = require('path');
31+const permission = require('internal/process/permission');
2932const { validateString } = require('internal/validators');
3033const internalFsBinding = internalBinding('fs');
3134@@ -127,26 +130,71 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
127130};
128131}
129132133+/**
134+ * Given a file path, walk the filesystem upwards until we find its closest parent
135+ * `package.json` file, stopping when:
136+ * 1. we find a `package.json` file;
137+ * 2. we find a path that we do not have permission to read;
138+ * 3. we find a containing `node_modules` directory;
139+ * 4. or, we reach the filesystem root
140+ * @returns {undefined | string}
141+ */
142+function findParentPackageJSON(checkPath) {
143+const enabledPermission = permission.isEnabled();
144+145+const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, path.sep);
146+let separatorIndex;
147+148+do {
149+separatorIndex = StringPrototypeLastIndexOf(checkPath, path.sep);
150+checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex);
151+152+if (enabledPermission && !permission.has('fs.read', checkPath + path.sep)) {
153+return undefined;
154+}
155+156+if (StringPrototypeEndsWith(checkPath, path.sep + 'node_modules')) {
157+return undefined;
158+}
159+160+const maybePackageJSONPath = checkPath + path.sep + 'package.json';
161+const stat = internalFsBinding.internalModuleStat(checkPath + path.sep + 'package.json');
162+163+const packageJSONExists = stat === 0;
164+if (packageJSONExists) {
165+return maybePackageJSONPath;
166+}
167+} while (separatorIndex > rootSeparatorIndex);
168+169+return undefined;
170+}
171+130172/**
131173 * Get the nearest parent package.json file from a given path.
132174 * Return the package.json data and the path to the package.json file, or undefined.
133175 * @param {string} checkPath The path to start searching from.
134176 * @returns {undefined | DeserializedPackageConfig}
135177 */
136178function getNearestParentPackageJSON(checkPath) {
137-if (nearestParentPackageJSONCache.has(checkPath)) {
138-return nearestParentPackageJSONCache.get(checkPath);
179+const nearestParentPackageJSON = findParentPackageJSON(checkPath);
180+181+if (nearestParentPackageJSON === undefined) {
182+return undefined;
183+}
184+185+if (nearestParentPackageJSONCache.has(nearestParentPackageJSON)) {
186+return nearestParentPackageJSONCache.get(nearestParentPackageJSON);
139187}
140188141-const result = modulesBinding.getNearestParentPackageJSON(checkPath);
189+const result = modulesBinding.readPackageJSON(nearestParentPackageJSON);
142190143191if (result === undefined) {
144192nearestParentPackageJSONCache.set(checkPath, undefined);
145193return undefined;
146194}
147195148196const packageConfig = deserializePackageJSON(checkPath, result);
149-nearestParentPackageJSONCache.set(checkPath, packageConfig);
197+nearestParentPackageJSONCache.set(nearestParentPackageJSON, packageConfig);
150198151199return packageConfig;
152200}