Skip to content

Commit cd265c8

Browse files
src: cache missing package.json files in the C++ package config cache
1 parent 3c8c1ef commit cd265c8

File tree

4 files changed

+67
-66
lines changed

4 files changed

+67
-66
lines changed

lib/internal/modules/package_json_reader.js

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ const {
55
JSONParse,
66
ObjectDefineProperty,
77
RegExpPrototypeExec,
8-
SafeMap,
9-
StringPrototypeEndsWith,
108
StringPrototypeIndexOf,
11-
StringPrototypeLastIndexOf,
129
StringPrototypeSlice,
1310
} = primordials;
1411
const {
@@ -28,11 +25,9 @@ const {
2825
const { kEmptyObject } = require('internal/util');
2926
const modulesBinding = internalBinding('modules');
3027
const path = require('path');
31-
const permission = require('internal/process/permission');
3228
const { validateString } = require('internal/validators');
3329
const internalFsBinding = internalBinding('fs');
3430

35-
const nearestParentPackageJSONCache = new SafeMap();
3631

3732
/**
3833
* @typedef {import('typings/internalBinding/modules').DeserializedPackageConfig} DeserializedPackageConfig
@@ -130,72 +125,15 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
130125
};
131126
}
132127

133-
/**
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-
172128
/**
173129
* Get the nearest parent package.json file from a given path.
174130
* Return the package.json data and the path to the package.json file, or undefined.
175131
* @param {string} checkPath The path to start searching from.
176132
* @returns {undefined | DeserializedPackageConfig}
177133
*/
178134
function getNearestParentPackageJSON(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);
187-
}
188-
189-
const result = modulesBinding.readPackageJSON(nearestParentPackageJSON);
190-
191-
if (result === undefined) {
192-
nearestParentPackageJSONCache.set(checkPath, undefined);
193-
return undefined;
194-
}
195-
135+
const result = modulesBinding.getNearestParentPackageJSON(checkPath);
196136
const packageConfig = deserializePackageJSON(checkPath, result);
197-
nearestParentPackageJSONCache.set(nearestParentPackageJSON, packageConfig);
198-
199137
return packageConfig;
200138
}
201139

src/node_modules.cc

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,27 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
9898

9999
auto cache_entry = binding_data->package_configs_.find(path.data());
100100
if (cache_entry != binding_data->package_configs_.end()) {
101-
return &cache_entry->second;
101+
auto& cache_value = cache_entry->second;
102+
if (cache_value.has_value()) {
103+
return &cache_value.value();
104+
}
105+
106+
// If we have a cache entry without a value, we've already
107+
// attempted to open and read this path and couldn't (it most
108+
// likely doesn't exist)
109+
return nullptr;
102110
}
103111

104112
PackageConfig package_config{};
105113
package_config.file_path = path;
106114
// No need to exclude BOM since simdjson will skip it.
107115
if (ReadFileSync(&package_config.raw_json, path.data()) < 0) {
116+
// Add `nullopt` to the package config cache so that we don't
117+
// need to open and attempt to read this path again
118+
binding_data->package_configs_.insert({std::string(path), std::nullopt});
108119
return nullptr;
109120
}
121+
110122
simdjson::ondemand::document document;
111123
simdjson::ondemand::object main_object;
112124
simdjson::error_code error =
@@ -239,7 +251,7 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
239251
auto cached = binding_data->package_configs_.insert(
240252
{std::string(path), std::move(package_config)});
241253

242-
return &cached.first->second;
254+
return &cached.first->second.value();
243255
}
244256

245257
void BindingData::ReadPackageJSON(const FunctionCallbackInfo<Value>& args) {
@@ -320,6 +332,40 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
320332
return nullptr;
321333
}
322334

335+
void BindingData::GetNearestParentPackageJSON(
336+
const v8::FunctionCallbackInfo<v8::Value>& args) {
337+
CHECK_GE(args.Length(), 1);
338+
CHECK(args[0]->IsString());
339+
340+
Realm* realm = Realm::GetCurrent(args);
341+
BufferValue path_value(realm->isolate(), args[0]);
342+
// Check if the path has a trailing slash. If so, add it after
343+
// ToNamespacedPath() as it will be deleted by ToNamespacedPath()
344+
bool slashCheck = path_value.ToStringView().ends_with(kPathSeparator);
345+
346+
ToNamespacedPath(realm->env(), &path_value);
347+
348+
std::string path_value_str = path_value.ToString();
349+
if (slashCheck) {
350+
path_value_str.push_back(kPathSeparator);
351+
}
352+
353+
std::filesystem::path path;
354+
355+
#ifdef _WIN32
356+
std::wstring wide_path = ConvertToWideString(path_value_str, GetACP());
357+
path = std::filesystem::path(wide_path);
358+
#else
359+
path = std::filesystem::path(path_value_str);
360+
#endif
361+
362+
auto package_json = TraverseParent(realm, path);
363+
364+
if (package_json != nullptr) {
365+
args.GetReturnValue().Set(package_json->Serialize(realm));
366+
}
367+
}
368+
323369
void BindingData::GetNearestParentPackageJSONType(
324370
const FunctionCallbackInfo<Value>& args) {
325371
CHECK_GE(args.Length(), 1);
@@ -646,6 +692,10 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
646692
target,
647693
"getNearestParentPackageJSONType",
648694
GetNearestParentPackageJSONType);
695+
SetMethod(isolate,
696+
target,
697+
"getNearestParentPackageJSON",
698+
GetNearestParentPackageJSON);
649699
SetMethod(
650700
isolate, target, "getPackageScopeConfig", GetPackageScopeConfig<false>);
651701
SetMethod(isolate, target, "getPackageType", GetPackageScopeConfig<true>);
@@ -702,6 +752,7 @@ void BindingData::RegisterExternalReferences(
702752
ExternalReferenceRegistry* registry) {
703753
registry->Register(ReadPackageJSON);
704754
registry->Register(GetNearestParentPackageJSONType);
755+
registry->Register(GetNearestParentPackageJSON);
705756
registry->Register(GetPackageScopeConfig<false>);
706757
registry->Register(GetPackageScopeConfig<true>);
707758
registry->Register(EnableCompileCache);

src/node_modules.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ class BindingData : public SnapshotableObject {
5555
SET_MEMORY_INFO_NAME(BindingData)
5656

5757
static void ReadPackageJSON(const v8::FunctionCallbackInfo<v8::Value>& args);
58+
static void GetNearestParentPackageJSON(
59+
const v8::FunctionCallbackInfo<v8::Value>& args);
5860
static void GetNearestParentPackageJSONType(
5961
const v8::FunctionCallbackInfo<v8::Value>& args);
6062
template <bool return_only_type>
@@ -72,7 +74,16 @@ class BindingData : public SnapshotableObject {
7274
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
7375

7476
private:
75-
std::unordered_map<std::string, PackageConfig> package_configs_;
77+
/*
78+
* This map caches `PackageConfig` values by `package.json` path.
79+
* An empty optional value indicates that no `package.json` file
80+
* at the given path exists, which we cache to avoid repeated
81+
* attempts to open the same non-existent paths.
82+
*/
83+
std::unordered_map<
84+
std::string,
85+
std::optional<PackageConfig>
86+
> package_configs_;
7687
simdjson::ondemand::parser json_parser;
7788
// returns null on error
7889
static const PackageConfig* GetPackageJSON(

typings/internalBinding/modules.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type SerializedPackageConfig = [
2323
export interface ModulesBinding {
2424
readPackageJSON(path: string): SerializedPackageConfig | undefined;
2525
getNearestParentPackageJSONType(path: string): PackageConfig['type']
26+
getNearestParentPackageJSON(path: string): SerializedPackageConfig | undefined
2627
getPackageScopeConfig(path: string): SerializedPackageConfig | undefined
2728
getPackageType(path: string): PackageConfig['type'] | undefined
2829
enableCompileCache(path?: string): { status: number, message?: string, directory?: string }

0 commit comments

Comments
 (0)