From 16d38f86281beb7dc7a075018cf78a11ad5ca5e8 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sun, 14 Sep 2025 17:57:45 -0400 Subject: [PATCH] src: reduce the nearest parent package JSON cache size --- lib/internal/modules/package_json_reader.js | 56 +++++++++++++++++++-- src/node_modules.cc | 39 -------------- src/node_modules.h | 2 - typings/internalBinding/modules.d.ts | 1 - 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 0dc15c9cd0bfe3..43a131f57cb187 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -6,7 +6,9 @@ const { ObjectDefineProperty, RegExpPrototypeExec, SafeMap, + StringPrototypeEndsWith, StringPrototypeIndexOf, + StringPrototypeLastIndexOf, StringPrototypeSlice, } = primordials; const { @@ -26,6 +28,7 @@ const { const { kEmptyObject } = require('internal/util'); const modulesBinding = internalBinding('modules'); const path = require('path'); +const permission = require('internal/process/permission'); const { validateString } = require('internal/validators'); const internalFsBinding = internalBinding('fs'); @@ -127,6 +130,45 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { }; } +/** + * Given a file path, walk the filesystem upwards until we find its closest parent + * `package.json` file, stopping when: + * 1. we find a `package.json` file; + * 2. we find a path that we do not have permission to read; + * 3. we find a containing `node_modules` directory; + * 4. or, we reach the filesystem root + * @returns {undefined | string} + */ +function findParentPackageJSON(checkPath) { + const enabledPermission = permission.isEnabled(); + + const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, path.sep); + let separatorIndex; + + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, path.sep); + checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); + + if (enabledPermission && !permission.has('fs.read', checkPath + path.sep)) { + return undefined; + } + + if (StringPrototypeEndsWith(checkPath, path.sep + 'node_modules')) { + return undefined; + } + + const maybePackageJSONPath = checkPath + path.sep + 'package.json'; + const stat = internalFsBinding.internalModuleStat(checkPath + path.sep + 'package.json'); + + const packageJSONExists = stat === 0; + if (packageJSONExists) { + return maybePackageJSONPath; + } + } while (separatorIndex > rootSeparatorIndex); + + return undefined; +} + /** * 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. @@ -134,11 +176,17 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { * @returns {undefined | DeserializedPackageConfig} */ function getNearestParentPackageJSON(checkPath) { - if (nearestParentPackageJSONCache.has(checkPath)) { - return nearestParentPackageJSONCache.get(checkPath); + const nearestParentPackageJSON = findParentPackageJSON(checkPath); + + if (nearestParentPackageJSON === undefined) { + return undefined; + } + + if (nearestParentPackageJSONCache.has(nearestParentPackageJSON)) { + return nearestParentPackageJSONCache.get(nearestParentPackageJSON); } - const result = modulesBinding.getNearestParentPackageJSON(checkPath); + const result = modulesBinding.readPackageJSON(nearestParentPackageJSON); if (result === undefined) { nearestParentPackageJSONCache.set(checkPath, undefined); @@ -146,7 +194,7 @@ function getNearestParentPackageJSON(checkPath) { } const packageConfig = deserializePackageJSON(checkPath, result); - nearestParentPackageJSONCache.set(checkPath, packageConfig); + nearestParentPackageJSONCache.set(nearestParentPackageJSON, packageConfig); return packageConfig; } diff --git a/src/node_modules.cc b/src/node_modules.cc index 82b061c0cab735..4626ed590b893f 100644 --- a/src/node_modules.cc +++ b/src/node_modules.cc @@ -320,40 +320,6 @@ const BindingData::PackageConfig* BindingData::TraverseParent( return nullptr; } -void BindingData::GetNearestParentPackageJSON( - const v8::FunctionCallbackInfo& args) { - CHECK_GE(args.Length(), 1); - CHECK(args[0]->IsString()); - - Realm* realm = Realm::GetCurrent(args); - BufferValue path_value(realm->isolate(), args[0]); - // Check if the path has a trailing slash. If so, add it after - // ToNamespacedPath() as it will be deleted by ToNamespacedPath() - bool slashCheck = path_value.ToStringView().ends_with(kPathSeparator); - - ToNamespacedPath(realm->env(), &path_value); - - std::string path_value_str = path_value.ToString(); - if (slashCheck) { - path_value_str.push_back(kPathSeparator); - } - - std::filesystem::path path; - -#ifdef _WIN32 - std::wstring wide_path = ConvertToWideString(path_value_str, GetACP()); - path = std::filesystem::path(wide_path); -#else - path = std::filesystem::path(path_value_str); -#endif - - auto package_json = TraverseParent(realm, path); - - if (package_json != nullptr) { - args.GetReturnValue().Set(package_json->Serialize(realm)); - } -} - void BindingData::GetNearestParentPackageJSONType( const FunctionCallbackInfo& args) { CHECK_GE(args.Length(), 1); @@ -680,10 +646,6 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, target, "getNearestParentPackageJSONType", GetNearestParentPackageJSONType); - SetMethod(isolate, - target, - "getNearestParentPackageJSON", - GetNearestParentPackageJSON); SetMethod( isolate, target, "getPackageScopeConfig", GetPackageScopeConfig); SetMethod(isolate, target, "getPackageType", GetPackageScopeConfig); @@ -740,7 +702,6 @@ void BindingData::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(ReadPackageJSON); registry->Register(GetNearestParentPackageJSONType); - registry->Register(GetNearestParentPackageJSON); registry->Register(GetPackageScopeConfig); registry->Register(GetPackageScopeConfig); registry->Register(EnableCompileCache); diff --git a/src/node_modules.h b/src/node_modules.h index eb2900d8f83852..e4ba6b75bc86d1 100644 --- a/src/node_modules.h +++ b/src/node_modules.h @@ -55,8 +55,6 @@ class BindingData : public SnapshotableObject { SET_MEMORY_INFO_NAME(BindingData) static void ReadPackageJSON(const v8::FunctionCallbackInfo& args); - static void GetNearestParentPackageJSON( - const v8::FunctionCallbackInfo& args); static void GetNearestParentPackageJSONType( const v8::FunctionCallbackInfo& args); template diff --git a/typings/internalBinding/modules.d.ts b/typings/internalBinding/modules.d.ts index 0b1d0e2938319f..c5a79ba9c2957e 100644 --- a/typings/internalBinding/modules.d.ts +++ b/typings/internalBinding/modules.d.ts @@ -23,7 +23,6 @@ export type SerializedPackageConfig = [ export interface ModulesBinding { readPackageJSON(path: string): SerializedPackageConfig | undefined; getNearestParentPackageJSONType(path: string): PackageConfig['type'] - getNearestParentPackageJSON(path: string): SerializedPackageConfig | undefined getPackageScopeConfig(path: string): SerializedPackageConfig | undefined getPackageType(path: string): PackageConfig['type'] | undefined enableCompileCache(path?: string): { status: number, message?: string, directory?: string }