Skip to content

Commit 9c5c99f

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

File tree

4 files changed

+112
-81
lines changed

4 files changed

+112
-81
lines changed

lib/internal/modules/package_json_reader.js

Lines changed: 46 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ const {
55
JSONParse,
66
ObjectDefineProperty,
77
RegExpPrototypeExec,
8-
SafeMap,
9-
StringPrototypeEndsWith,
108
StringPrototypeIndexOf,
11-
StringPrototypeLastIndexOf,
129
StringPrototypeSlice,
10+
SafeMap,
1311
} = primordials;
1412
const {
1513
fileURLToPath,
@@ -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', {
7376
__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-
},
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', {
89+
__proto__: null,
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
};
@@ -130,44 +139,8 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
130139
};
131140
}
132141

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-
}
142+
const moduleToParentPackageJSONCache = new SafeMap();
143+
const deserializedPackageJSONCache = new SafeMap();
171144

172145
/**
173146
* Get the nearest parent package.json file from a given path.
@@ -176,26 +149,21 @@ function findParentPackageJSON(checkPath) {
176149
* @returns {undefined | DeserializedPackageConfig}
177150
*/
178151
function getNearestParentPackageJSON(checkPath) {
179-
const nearestParentPackageJSON = findParentPackageJSON(checkPath);
180-
181-
if (nearestParentPackageJSON === undefined) {
182-
return undefined;
152+
if (moduleToParentPackageJSONCache.has(checkPath)) {
153+
const parentPackageJSONPath = moduleToParentPackageJSONCache.get(checkPath);
154+
return deserializedPackageJSONCache.get(parentPackageJSONPath);
183155
}
184156

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

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

191-
if (result === undefined) {
192-
nearestParentPackageJSONCache.set(checkPath, undefined);
193-
return undefined;
162+
if (deserializedPackageJSONCache.has(packageConfig.path)) {
163+
return deserializedPackageJSONCache.get(packageConfig.path);
194164
}
195165

196-
const packageConfig = deserializePackageJSON(checkPath, result);
197-
nearestParentPackageJSONCache.set(nearestParentPackageJSON, packageConfig);
198-
166+
deserializedPackageJSONCache.set(packageConfig.path, packageConfig);
199167
return packageConfig;
200168
}
201169

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)