Skip to content

Commit c8a584a

Browse files
src: cache missing package.json files in the C++ package config cache
1 parent 9f01ff2 commit c8a584a

File tree

4 files changed

+123
-77
lines changed

4 files changed

+123
-77
lines changed

lib/internal/modules/package_json_reader.js

Lines changed: 59 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ const {
66
ObjectDefineProperty,
77
RegExpPrototypeExec,
88
SafeMap,
9-
StringPrototypeEndsWith,
109
StringPrototypeIndexOf,
11-
StringPrototypeLastIndexOf,
1210
StringPrototypeSlice,
1311
} = primordials;
1412
const {
@@ -28,11 +26,9 @@ const {
2826
const { kEmptyObject } = require('internal/util');
2927
const modulesBinding = internalBinding('modules');
3028
const path = require('path');
31-
const permission = require('internal/process/permission');
3229
const { validateString } = require('internal/validators');
3330
const internalFsBinding = internalBinding('fs');
3431

35-
const nearestParentPackageJSONCache = new SafeMap();
3632

3733
/**
3834
* @typedef {import('typings/internalBinding/modules').DeserializedPackageConfig} DeserializedPackageConfig
@@ -68,28 +64,41 @@ function deserializePackageJSON(path, contents) {
6864

6965
const pjsonPath = optionalFilePath ?? path;
7066

71-
return {
72-
data: {
67+
const data = {
68+
__proto__: null,
69+
...(name != null && { name }),
70+
...(main != null && { main }),
71+
...(type != null && { type }),
72+
};
73+
74+
if (plainExports !== null) {
75+
ObjectDefineProperty(data, 'exports', {
76+
__proto__: null,
77+
configurable: true,
78+
enumerable: true,
79+
get() {
80+
const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports;
81+
ObjectDefineProperty(data, 'exports', { __proto__: null, enumerable: true, value });
82+
return value;
83+
},
84+
});
85+
}
86+
87+
if (plainImports !== null) {
88+
ObjectDefineProperty(data, 'imports', {
7389
__proto__: null,
74-
...(name != null && { name }),
75-
...(main != null && { main }),
76-
...(type != null && { type }),
77-
...(plainImports != null && {
78-
// This getters are used to lazily parse the imports and exports fields.
79-
get imports() {
80-
const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports;
81-
ObjectDefineProperty(this, 'imports', { __proto__: null, value });
82-
return this.imports;
83-
},
84-
}),
85-
...(plainExports != null && {
86-
get exports() {
87-
const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports;
88-
ObjectDefineProperty(this, 'exports', { __proto__: null, value });
89-
return this.exports;
90-
},
91-
}),
92-
},
90+
configurable: true,
91+
enumerable: true,
92+
get() {
93+
const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports;
94+
ObjectDefineProperty(data, 'imports', { __proto__: null, enumerable: true, value });
95+
return value;
96+
},
97+
});
98+
}
99+
100+
return {
101+
data,
93102
exists: true,
94103
path: pjsonPath,
95104
};
@@ -131,43 +140,23 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
131140
}
132141

133142
/**
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}
143+
* A cache mapping a module's path to its parent `package.json` file's path.
144+
* This is used in concert with `deserializedPackageJSONCache` to improve
145+
* the performance of `getNearestParentPackageJSON` when called repeatedly
146+
* on the same module paths.
141147
*/
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);
148+
const moduleToParentPackageJSONCache = new SafeMap();
151149

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-
}
150+
/**
151+
* A cache mapping the path of a `package.json` file to its
152+
* {@link DeserializedPackageConfig deserialized representation},
153+
* as produced by {@link deserializedPackageJSONCache}. The purpose of this
154+
* cache is to ensure that we always return the same
155+
* {@link DeserializedPackageConfig} instance for a given `package.json`,
156+
* which is necessary to ensure that we don't re-parse `imports` and
157+
* `exports` redundantly.
158+
*/
159+
const deserializedPackageJSONCache = new SafeMap();
171160

172161
/**
173162
* Get the nearest parent package.json file from a given path.
@@ -176,26 +165,22 @@ function findParentPackageJSON(checkPath) {
176165
* @returns {undefined | DeserializedPackageConfig}
177166
*/
178167
function getNearestParentPackageJSON(checkPath) {
179-
const nearestParentPackageJSON = findParentPackageJSON(checkPath);
180-
181-
if (nearestParentPackageJSON === undefined) {
182-
return undefined;
168+
const parentPackageJSONPath = moduleToParentPackageJSONCache.get(checkPath);
169+
if (parentPackageJSONPath !== undefined) {
170+
return deserializedPackageJSONCache.get(parentPackageJSONPath);
183171
}
184172

185-
if (nearestParentPackageJSONCache.has(nearestParentPackageJSON)) {
186-
return nearestParentPackageJSONCache.get(nearestParentPackageJSON);
187-
}
173+
const result = modulesBinding.getNearestParentPackageJSON(checkPath);
174+
const packageConfig = deserializePackageJSON(checkPath, result);
188175

189-
const result = modulesBinding.readPackageJSON(nearestParentPackageJSON);
176+
moduleToParentPackageJSONCache.set(checkPath, packageConfig.path);
190177

191-
if (result === undefined) {
192-
nearestParentPackageJSONCache.set(checkPath, undefined);
193-
return undefined;
178+
const maybeCachedPackageConfig = deserializedPackageJSONCache.get(packageConfig.path);
179+
if (maybeCachedPackageConfig !== undefined) {
180+
return maybeCachedPackageConfig;
194181
}
195182

196-
const packageConfig = deserializePackageJSON(checkPath, result);
197-
nearestParentPackageJSONCache.set(nearestParentPackageJSON, packageConfig);
198-
183+
deserializedPackageJSONCache.set(packageConfig.path, packageConfig);
199184
return packageConfig;
200185
}
201186

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) {
103+
return &*cache_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;
243255
}
244256

245257
void BindingData::ReadPackageJSON(const FunctionCallbackInfo<Value>& args) {
@@ -322,6 +334,40 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
322334
return nullptr;
323335
}
324336

337+
void BindingData::GetNearestParentPackageJSON(
338+
const v8::FunctionCallbackInfo<v8::Value>& args) {
339+
CHECK_GE(args.Length(), 1);
340+
CHECK(args[0]->IsString());
341+
342+
Realm* realm = Realm::GetCurrent(args);
343+
BufferValue path_value(realm->isolate(), args[0]);
344+
// Check if the path has a trailing slash. If so, add it after
345+
// ToNamespacedPath() as it will be deleted by ToNamespacedPath()
346+
bool slashCheck = path_value.ToStringView().ends_with(kPathSeparator);
347+
348+
ToNamespacedPath(realm->env(), &path_value);
349+
350+
std::string path_value_str = path_value.ToString();
351+
if (slashCheck) {
352+
path_value_str.push_back(kPathSeparator);
353+
}
354+
355+
std::filesystem::path path;
356+
357+
#ifdef _WIN32
358+
std::wstring wide_path = ConvertToWideString(path_value_str, GetACP());
359+
path = std::filesystem::path(wide_path);
360+
#else
361+
path = std::filesystem::path(path_value_str);
362+
#endif
363+
364+
auto package_json = TraverseParent(realm, path);
365+
366+
if (package_json != nullptr) {
367+
args.GetReturnValue().Set(package_json->Serialize(realm));
368+
}
369+
}
370+
325371
void BindingData::GetNearestParentPackageJSONType(
326372
const FunctionCallbackInfo<Value>& args) {
327373
CHECK_GE(args.Length(), 1);
@@ -640,6 +686,10 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
640686
target,
641687
"getNearestParentPackageJSONType",
642688
GetNearestParentPackageJSONType);
689+
SetMethod(isolate,
690+
target,
691+
"getNearestParentPackageJSON",
692+
GetNearestParentPackageJSON);
643693
SetMethod(
644694
isolate, target, "getPackageScopeConfig", GetPackageScopeConfig<false>);
645695
SetMethod(isolate, target, "getPackageType", GetPackageScopeConfig<true>);
@@ -696,6 +746,7 @@ void BindingData::RegisterExternalReferences(
696746
ExternalReferenceRegistry* registry) {
697747
registry->Register(ReadPackageJSON);
698748
registry->Register(GetNearestParentPackageJSONType);
749+
registry->Register(GetNearestParentPackageJSON);
699750
registry->Register(GetPackageScopeConfig<false>);
700751
registry->Register(GetPackageScopeConfig<true>);
701752
registry->Register(EnableCompileCache);

src/node_modules.h

Lines changed: 10 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,14 @@ 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<std::string, std::optional<PackageConfig> >
84+
package_configs_;
7685
simdjson::ondemand::parser json_parser;
7786
// returns null on error
7887
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)