mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Extends the module compile cache to WebAssembly modules loaded through the ES module integration. A new kWasm entry type is keyed on the module URL with the wire bytes as the hashed source, and stores the serialized CompiledWasmModule. On load, cached code is deserialized through v8::WasmModuleCompilation with the same compile options the translator uses (js-string builtins and imported string constants), falling back to compilation when V8 rejects it. As for JavaScript, the cache entry is generated right after compilation. V8 only serializes optimized tier code, so with the default lazy baseline compilation there is nothing to cache and no entry is written; a complete cache requires --no-liftoff --no-wasm-lazy-compilation, which is documented as the way to use the compile cache for WebAssembly. Signed-off-by: Guy Bedford <guybedford@gmail.com> Assisted-by: Claude Code PR-URL: https://github.com/nodejs/node/pull/66191 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1058 lines
37 KiB
C++
1058 lines
37 KiB
C++
#include "node_modules.h"
|
|
#include <cstdio>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <variant>
|
|
#include "base_object-inl.h"
|
|
#include "compile_cache.h"
|
|
#include "node_errors.h"
|
|
#include "node_external_reference.h"
|
|
#include "node_url.h"
|
|
#include "path.h"
|
|
#include "permission/permission.h"
|
|
#include "permission/permission_base.h"
|
|
#include "util-inl.h"
|
|
#include "v8-fast-api-calls.h"
|
|
#include "v8-function-callback.h"
|
|
#include "v8-primitive.h"
|
|
#include "v8-value.h"
|
|
#include "v8.h"
|
|
|
|
#include "simdjson.h"
|
|
|
|
namespace node {
|
|
namespace modules {
|
|
|
|
using v8::Array;
|
|
using v8::Boolean;
|
|
using v8::Context;
|
|
using v8::External;
|
|
using v8::FunctionCallbackInfo;
|
|
using v8::Global;
|
|
using v8::HandleScope;
|
|
using v8::Integer;
|
|
using v8::Isolate;
|
|
using v8::Local;
|
|
using v8::LocalVector;
|
|
using v8::MaybeLocal;
|
|
using v8::MemorySpan;
|
|
using v8::Name;
|
|
using v8::NewStringType;
|
|
using v8::Null;
|
|
using v8::Object;
|
|
using v8::ObjectTemplate;
|
|
using v8::Primitive;
|
|
using v8::String;
|
|
using v8::Undefined;
|
|
using v8::Value;
|
|
using v8::WasmModuleCompilation;
|
|
using v8::WasmModuleObject;
|
|
using v8::WasmStreaming;
|
|
|
|
void BindingData::MemoryInfo(MemoryTracker* tracker) const {
|
|
// Do nothing
|
|
}
|
|
|
|
BindingData::BindingData(Realm* realm,
|
|
v8::Local<v8::Object> object,
|
|
InternalFieldInfo* info)
|
|
: SnapshotableObject(realm, object, type_int) {}
|
|
|
|
bool BindingData::PrepareForSerialization(v8::Local<v8::Context> context,
|
|
v8::SnapshotCreator* creator) {
|
|
// Return true because we need to maintain the reference to the binding from
|
|
// JS land.
|
|
return true;
|
|
}
|
|
|
|
InternalFieldInfoBase* BindingData::Serialize(int index) {
|
|
DCHECK_IS_SNAPSHOT_SLOT(index);
|
|
InternalFieldInfo* info =
|
|
InternalFieldInfoBase::New<InternalFieldInfo>(type());
|
|
return info;
|
|
}
|
|
|
|
void BindingData::Deserialize(v8::Local<v8::Context> context,
|
|
v8::Local<v8::Object> holder,
|
|
int index,
|
|
InternalFieldInfoBase* info) {
|
|
DCHECK_IS_SNAPSHOT_SLOT(index);
|
|
HandleScope scope(Isolate::GetCurrent());
|
|
Realm* realm = Realm::GetCurrent(context);
|
|
BindingData* binding = realm->AddBindingData<BindingData>(holder);
|
|
CHECK_NOT_NULL(binding);
|
|
}
|
|
|
|
Local<Array> BindingData::PackageConfig::Serialize(Realm* realm) const {
|
|
auto isolate = realm->isolate();
|
|
const auto ToString = [isolate](std::string_view input) -> Local<Primitive> {
|
|
return String::NewFromUtf8(
|
|
isolate, input.data(), NewStringType::kNormal, input.size())
|
|
.ToLocalChecked();
|
|
};
|
|
Local<Value> values[7] = {
|
|
name.has_value() ? ToString(*name) : Undefined(isolate),
|
|
main.has_value() ? ToString(*main) : Undefined(isolate),
|
|
ToString(type),
|
|
imports.has_value() ? ToString(*imports) : Undefined(isolate),
|
|
exports.has_value() ? ToString(*exports) : Undefined(isolate),
|
|
ToString(file_path),
|
|
is_private.has_value()
|
|
? Boolean::New(isolate, *is_private).As<Primitive>()
|
|
: Undefined(isolate),
|
|
};
|
|
return Array::New(isolate, values, 7);
|
|
}
|
|
|
|
bool BindingData::ParsePackageJSON(Realm* realm,
|
|
PackageConfig* package_config,
|
|
ErrorContext* error_context) {
|
|
auto binding_data = realm->GetBindingData<BindingData>();
|
|
const std::string_view path = package_config->file_path;
|
|
|
|
simdjson::ondemand::document document;
|
|
simdjson::ondemand::object main_object;
|
|
simdjson::error_code error =
|
|
binding_data->json_parser.iterate(simdjson::pad(package_config->raw_json))
|
|
.get(document);
|
|
|
|
const auto throw_invalid_package_config = [error_context, path, realm]() {
|
|
if (error_context == nullptr) {
|
|
THROW_ERR_INVALID_PACKAGE_CONFIG(
|
|
realm->isolate(), "Invalid package config %s.", path.data());
|
|
} else if (error_context->base.has_value()) {
|
|
auto file_url = ada::parse(error_context->base.value());
|
|
CHECK(file_url);
|
|
auto file_path = url::FileURLToPath(realm->env(), *file_url);
|
|
CHECK(file_path.has_value());
|
|
THROW_ERR_INVALID_PACKAGE_CONFIG(
|
|
realm->isolate(),
|
|
"Invalid package config %s while importing \"%s\" from %s.",
|
|
path.data(),
|
|
error_context->specifier.c_str(),
|
|
file_path->c_str());
|
|
} else {
|
|
THROW_ERR_INVALID_PACKAGE_CONFIG(
|
|
realm->isolate(), "Invalid package config %s.", path.data());
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
if (error || document.get_object().get(main_object)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
|
|
simdjson::ondemand::raw_json_string key;
|
|
simdjson::ondemand::value value;
|
|
std::string_view field_value;
|
|
simdjson::ondemand::json_type field_type;
|
|
|
|
for (auto field : main_object) {
|
|
// Throw error if getting key or value fails.
|
|
if (field.key().get(key) || field.value().get(value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
|
|
// based on coverity using key with == derefs the raw value
|
|
// avoid derefing if its null
|
|
if (key.raw() == nullptr) continue;
|
|
|
|
if (key == "name") {
|
|
// Though there is a key "name" with a corresponding value,
|
|
// the value may not be a string or could be an invalid JSON string
|
|
if (value.get_string(package_config->name)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
} else if (key == "main") {
|
|
// Omit all non-string values
|
|
USE(value.get_string(package_config->main));
|
|
} else if (key == "exports") {
|
|
if (value.type().get(field_type)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
switch (field_type) {
|
|
case simdjson::ondemand::json_type::object:
|
|
case simdjson::ondemand::json_type::array: {
|
|
if (value.raw_json().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
package_config->exports = field_value;
|
|
break;
|
|
}
|
|
case simdjson::ondemand::json_type::string: {
|
|
if (value.get_string(package_config->exports)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
} else if (key == "imports") {
|
|
if (value.type().get(field_type)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
switch (field_type) {
|
|
case simdjson::ondemand::json_type::array:
|
|
case simdjson::ondemand::json_type::object: {
|
|
if (value.raw_json().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
package_config->imports = field_value;
|
|
break;
|
|
}
|
|
case simdjson::ondemand::json_type::string: {
|
|
if (value.get_string(package_config->imports)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
} else if (key == "type") {
|
|
if (value.get_string().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
// Only update type if it is "commonjs" or "module"
|
|
// The default value is "none" for backward compatibility.
|
|
if (field_value == "commonjs" || field_value == "module") {
|
|
package_config->type = field_value;
|
|
}
|
|
} else if (key == "private") {
|
|
bool is_private;
|
|
if (!value.get_bool().get(is_private)) {
|
|
package_config->is_private = is_private;
|
|
}
|
|
} else if (key == "scripts") {
|
|
if (value.type().get(field_type)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
switch (field_type) {
|
|
case simdjson::ondemand::json_type::object: {
|
|
if (value.raw_json().get(field_value)) {
|
|
return throw_invalid_package_config();
|
|
}
|
|
package_config->scripts = field_value;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const BindingData::PackageConfig* BindingData::GetPackageJSON(
|
|
Realm* realm, std::string_view path, ErrorContext* error_context) {
|
|
auto binding_data = realm->GetBindingData<BindingData>();
|
|
|
|
auto cache_entry = binding_data->package_configs_.find(path.data());
|
|
if (cache_entry != binding_data->package_configs_.end()) {
|
|
auto& cache_value = cache_entry->second;
|
|
if (cache_value) {
|
|
return &*cache_value;
|
|
}
|
|
|
|
// If we have a cache entry without a value, we've already
|
|
// attempted to open and read this path and couldn't (it most
|
|
// likely doesn't exist)
|
|
return nullptr;
|
|
}
|
|
|
|
PackageConfig package_config{};
|
|
package_config.file_path = path;
|
|
// No need to exclude BOM since simdjson will skip it.
|
|
int read_error = ReadFileSync(&package_config.raw_json, path.data());
|
|
if (read_error < 0) {
|
|
// No file at this path, a path component that is not a directory, or a
|
|
// "package.json" that is itself a directory all mean there is no package
|
|
// config here. Any other failure means a package.json is present but
|
|
// could not be read. Treating that as absent silently drops fields such
|
|
// as "exports" and "type", which can resolve a specifier to a different
|
|
// file, so surface the read error instead of continuing.
|
|
if (read_error != UV_ENOENT && read_error != UV_ENOTDIR &&
|
|
read_error != UV_EISDIR) {
|
|
THROW_ERR_INVALID_PACKAGE_CONFIG(realm->isolate(),
|
|
"Cannot read package config %s: %s.",
|
|
path.data(),
|
|
uv_strerror(read_error));
|
|
return nullptr;
|
|
}
|
|
|
|
// Add `nullopt` to the package config cache so that we don't
|
|
// need to open and attempt to read this path again
|
|
binding_data->package_configs_.insert({std::string(path), std::nullopt});
|
|
return nullptr;
|
|
}
|
|
|
|
if (!ParsePackageJSON(realm, &package_config, error_context)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// package_config could be quite large, so we should move it instead of
|
|
// copying it.
|
|
auto cached = binding_data->package_configs_.insert(
|
|
{std::string(path), std::move(package_config)});
|
|
|
|
return &*cached.first->second;
|
|
}
|
|
|
|
void BindingData::ReadPackageJSON(const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_GE(args.Length(), 1); // path, [is_esm, base, specifier]
|
|
CHECK(args[0]->IsString()); // path
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
auto isolate = realm->isolate();
|
|
|
|
BufferValue path(isolate, args[0]);
|
|
bool is_esm = args[1]->IsTrue();
|
|
auto error_context = ErrorContext();
|
|
if (is_esm) {
|
|
CHECK(args[2]->IsUndefined() || args[2]->IsString()); // base
|
|
CHECK(args[3]->IsString()); // specifier
|
|
|
|
if (args[2]->IsString()) {
|
|
Utf8Value base_value(isolate, args[2]);
|
|
error_context.base = base_value.ToString();
|
|
}
|
|
Utf8Value specifier(isolate, args[3]);
|
|
error_context.specifier = specifier.ToString();
|
|
}
|
|
|
|
THROW_IF_INSUFFICIENT_PERMISSIONS(
|
|
realm->env(),
|
|
permission::PermissionScope::kFileSystemRead,
|
|
path.ToStringView());
|
|
|
|
ToNamespacedPath(realm->env(), &path);
|
|
auto package_json = GetPackageJSON(
|
|
realm, path.ToStringView(), is_esm ? &error_context : nullptr);
|
|
|
|
if (package_json == nullptr) {
|
|
return;
|
|
}
|
|
|
|
args.GetReturnValue().Set(package_json->Serialize(realm));
|
|
}
|
|
|
|
void BindingData::ParsePackageJSONBinding(
|
|
const FunctionCallbackInfo<Value>& args) {
|
|
// parsePackageJSON(source, jsonPath, isESM, base, specifier)
|
|
CHECK_GE(args.Length(), 2);
|
|
CHECK(args[0]->IsString() || args[0]->IsArrayBufferView()); // source
|
|
CHECK(args[1]->IsString()); // jsonPath
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
auto isolate = realm->isolate();
|
|
|
|
BufferValue json_path(isolate, args[1]);
|
|
bool is_esm = args.Length() >= 3 && args[2]->IsTrue();
|
|
auto error_context = ErrorContext();
|
|
if (is_esm) {
|
|
CHECK(args[3]->IsUndefined() || args[3]->IsString()); // base
|
|
CHECK(args[4]->IsString()); // specifier
|
|
|
|
if (args[3]->IsString()) {
|
|
Utf8Value base_value(isolate, args[3]);
|
|
error_context.base = base_value.ToString();
|
|
}
|
|
Utf8Value specifier(isolate, args[4]);
|
|
error_context.specifier = specifier.ToString();
|
|
}
|
|
|
|
PackageConfig package_config{};
|
|
package_config.file_path = json_path.ToStringView();
|
|
if (args[0]->IsString()) {
|
|
Utf8Value source(isolate, args[0]);
|
|
package_config.raw_json = source.ToString();
|
|
} else {
|
|
ArrayBufferViewContents<char> view(args[0]);
|
|
package_config.raw_json = std::string(view.data(), view.length());
|
|
}
|
|
|
|
if (!ParsePackageJSON(
|
|
realm, &package_config, is_esm ? &error_context : nullptr)) {
|
|
return; // ParsePackageJSON already threw
|
|
}
|
|
|
|
args.GetReturnValue().Set(package_config.Serialize(realm));
|
|
}
|
|
|
|
const BindingData::PackageConfig* BindingData::TraverseParent(
|
|
Realm* realm, const std::filesystem::path& check_path) {
|
|
std::filesystem::path current_path = check_path;
|
|
auto env = realm->env();
|
|
const bool is_permissions_enabled = env->permission()->enabled();
|
|
|
|
do {
|
|
current_path = current_path.parent_path();
|
|
|
|
// We don't need to try "/"
|
|
if (current_path.parent_path() == current_path) {
|
|
break;
|
|
}
|
|
|
|
// Stop the search when the process doesn't have permissions
|
|
// to walk upwards
|
|
if (is_permissions_enabled) {
|
|
if (!env->permission()->is_granted(
|
|
env,
|
|
permission::PermissionScope::kFileSystemRead,
|
|
ConvertGenericPathToUTF8(current_path))) [[unlikely]] {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
// Check if the path ends with `/node_modules`
|
|
if (current_path.filename() == "node_modules") {
|
|
return nullptr;
|
|
}
|
|
|
|
auto package_json_path = current_path / "package.json";
|
|
|
|
auto package_json =
|
|
GetPackageJSON(realm, ConvertPathToUTF8(package_json_path), nullptr);
|
|
if (package_json != nullptr) {
|
|
return package_json;
|
|
}
|
|
} while (true);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const BindingData::PackageConfig* BindingData::FindNodeModulesPackage(
|
|
Realm* realm, const std::filesystem::path& check_path) {
|
|
std::filesystem::path current_path = check_path;
|
|
std::filesystem::path child;
|
|
std::filesystem::path grandchild;
|
|
const PackageConfig* innermost = nullptr;
|
|
auto env = realm->env();
|
|
const bool is_permissions_enabled = env->permission()->enabled();
|
|
|
|
while (true) {
|
|
auto parent_path = current_path.parent_path();
|
|
if (parent_path == current_path) {
|
|
// Reached the filesystem root.
|
|
break;
|
|
}
|
|
|
|
if (current_path.filename() == "node_modules") {
|
|
// `child` is the directory directly under node_modules. For scoped
|
|
// packages the package root is one level further down.
|
|
const std::string child_name = child.filename().generic_string();
|
|
const std::filesystem::path& package_root =
|
|
(!child_name.empty() && child_name[0] == '@') ? grandchild : child;
|
|
if (package_root.empty()) {
|
|
// The path points at a node_modules or bare scope directory rather
|
|
// than into a package.
|
|
return nullptr;
|
|
}
|
|
|
|
auto package_json_path = package_root / "package.json";
|
|
|
|
// GetPackageJSON()'s ReadFileSync() reads the file directly without
|
|
// consulting the permission model, so check read access here as
|
|
// TraverseParent() does. Without permission to confirm the package is
|
|
// private, fail closed and deny stripping.
|
|
if (is_permissions_enabled) [[unlikely]] {
|
|
if (!env->permission()->is_granted(
|
|
env,
|
|
permission::PermissionScope::kFileSystemRead,
|
|
ConvertGenericPathToUTF8(package_json_path))) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
auto package_json =
|
|
GetPackageJSON(realm, ConvertPathToUTF8(package_json_path), nullptr);
|
|
if (package_json != nullptr) {
|
|
if (!package_json->is_private.value_or(false)) {
|
|
// A publishable (non-private) package encloses the file.
|
|
return nullptr;
|
|
}
|
|
if (innermost == nullptr) {
|
|
innermost = package_json;
|
|
}
|
|
}
|
|
// A directory under node_modules with no package.json is not a package
|
|
// (e.g. pnpm's `.pnpm` store); skip it and keep walking upwards.
|
|
}
|
|
|
|
grandchild = child;
|
|
child = current_path;
|
|
current_path = parent_path;
|
|
}
|
|
|
|
return innermost;
|
|
}
|
|
|
|
const std::filesystem::path BindingData::NormalizePath(
|
|
Realm* realm, BufferValue* path_value) {
|
|
// 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);
|
|
|
|
auto path = path_value->ToPath();
|
|
|
|
if (slashCheck) {
|
|
path /= "";
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
void BindingData::GetNearestParentPackageJSON(
|
|
const v8::FunctionCallbackInfo<v8::Value>& args) {
|
|
CHECK_GE(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
BufferValue path_value(realm->isolate(), args[0]);
|
|
|
|
auto path = NormalizePath(realm, &path_value);
|
|
|
|
auto package_json = TraverseParent(realm, path);
|
|
|
|
if (package_json != nullptr) {
|
|
args.GetReturnValue().Set(package_json->Serialize(realm));
|
|
}
|
|
}
|
|
|
|
void BindingData::GetRootPackageJSON(const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_GE(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
BufferValue path_value(realm->isolate(), args[0]);
|
|
|
|
auto path = NormalizePath(realm, &path_value);
|
|
|
|
auto package_json = FindNodeModulesPackage(realm, path);
|
|
|
|
if (package_json != nullptr) {
|
|
args.GetReturnValue().Set(package_json->Serialize(realm));
|
|
}
|
|
}
|
|
|
|
void BindingData::GetNearestParentPackageJSONType(
|
|
const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_GE(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
BufferValue path_value(realm->isolate(), args[0]);
|
|
|
|
auto path = NormalizePath(realm, &path_value);
|
|
|
|
auto package_json = TraverseParent(realm, path);
|
|
|
|
if (package_json == nullptr) {
|
|
return;
|
|
}
|
|
|
|
Local<Value> value;
|
|
if (ToV8Value(realm->context(), package_json->type).ToLocal(&value)) {
|
|
args.GetReturnValue().Set(value);
|
|
}
|
|
}
|
|
|
|
template <bool return_only_type>
|
|
void BindingData::GetPackageScopeConfig(
|
|
const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_GE(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
|
|
Realm* realm = Realm::GetCurrent(args);
|
|
Utf8Value resolved(realm->isolate(), args[0]);
|
|
auto package_json_url_base = ada::parse(resolved.ToStringView());
|
|
if (!package_json_url_base) {
|
|
url::ThrowInvalidURL(realm->env(), resolved.ToStringView(), std::nullopt);
|
|
return;
|
|
}
|
|
auto package_json_url =
|
|
ada::parse("./package.json", &package_json_url_base.value());
|
|
if (!package_json_url) {
|
|
url::ThrowInvalidURL(realm->env(), "./package.json", resolved.ToString());
|
|
return;
|
|
}
|
|
|
|
std::string_view node_modules_package_path = "/node_modules/package.json";
|
|
auto error_context = ErrorContext();
|
|
error_context.is_esm = true;
|
|
|
|
// TODO(@anonrig): Rewrite this function and avoid calling URL parser.
|
|
while (true) {
|
|
auto pathname = package_json_url->get_pathname();
|
|
if (pathname.ends_with(node_modules_package_path)) {
|
|
break;
|
|
}
|
|
|
|
auto file_url = url::FileURLToPath(realm->env(), *package_json_url);
|
|
if (!file_url) {
|
|
url::ThrowInvalidURL(realm->env(), resolved.ToStringView(), std::nullopt);
|
|
return;
|
|
}
|
|
BufferValue file_path_buf(realm->isolate(),
|
|
String::NewFromUtf8(realm->isolate(),
|
|
file_url->c_str(),
|
|
NewStringType::kInternalized,
|
|
file_url->size())
|
|
.ToLocalChecked());
|
|
ToNamespacedPath(realm->env(), &file_path_buf);
|
|
error_context.specifier = resolved.ToString();
|
|
auto package_json =
|
|
GetPackageJSON(realm, file_path_buf.ToStringView(), &error_context);
|
|
if (package_json != nullptr) {
|
|
if constexpr (return_only_type) {
|
|
Local<Value> value;
|
|
if (ToV8Value(realm->context(), package_json->type).ToLocal(&value)) {
|
|
args.GetReturnValue().Set(value);
|
|
}
|
|
return;
|
|
} else {
|
|
return args.GetReturnValue().Set(package_json->Serialize(realm));
|
|
}
|
|
}
|
|
|
|
auto last_href = std::string(package_json_url->get_href());
|
|
auto last_pathname = std::string(package_json_url->get_pathname());
|
|
package_json_url = ada::parse("../package.json", &package_json_url.value());
|
|
if (!package_json_url) {
|
|
url::ThrowInvalidURL(realm->env(), "../package.json", last_href);
|
|
return;
|
|
}
|
|
|
|
// Terminates at root where ../package.json equals ../../package.json
|
|
// (can't just check "/package.json" for Windows support).
|
|
if (package_json_url->get_pathname() == last_pathname) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if constexpr (return_only_type) {
|
|
return;
|
|
}
|
|
|
|
// If the package.json could not be found return a string containing a path
|
|
// to the non-existent package.json file in the initial requested location
|
|
auto package_json_url_as_path =
|
|
url::FileURLToPath(realm->env(), *package_json_url);
|
|
CHECK(package_json_url_as_path);
|
|
Local<Value> ret;
|
|
if (ToV8Value(realm->context(), *package_json_url_as_path, realm->isolate())
|
|
.ToLocal(&ret)) {
|
|
args.GetReturnValue().Set(ret);
|
|
}
|
|
}
|
|
|
|
void FlushCompileCache(const FunctionCallbackInfo<Value>& args) {
|
|
Isolate* isolate = args.GetIsolate();
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
|
|
if (!args[0]->IsBoolean() && !args[0]->IsUndefined()) {
|
|
THROW_ERR_INVALID_ARG_TYPE(env,
|
|
"keepDeserializedCache should be a boolean");
|
|
return;
|
|
}
|
|
Debug(env,
|
|
DebugCategory::COMPILE_CACHE,
|
|
"[compile cache] module.flushCompileCache() requested.\n");
|
|
env->FlushCompileCache();
|
|
Debug(env,
|
|
DebugCategory::COMPILE_CACHE,
|
|
"[compile cache] module.flushCompileCache() finished.\n");
|
|
}
|
|
|
|
void EnableCompileCache(const FunctionCallbackInfo<Value>& args) {
|
|
Isolate* isolate = args.GetIsolate();
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
if (!args[0]->IsString()) {
|
|
THROW_ERR_INVALID_ARG_TYPE(env, "cacheDir should be a string");
|
|
return;
|
|
}
|
|
|
|
EnableOption option = EnableOption::DEFAULT;
|
|
if (args.Length() > 1 && args[1]->IsTrue()) {
|
|
option = option | EnableOption::PORTABLE;
|
|
}
|
|
if (args.Length() > 2 && args[2]->IsTrue()) {
|
|
option = option | EnableOption::READ_ONLY;
|
|
}
|
|
|
|
Utf8Value value(isolate, args[0]);
|
|
CompileCacheEnableResult result = env->EnableCompileCache(*value, option);
|
|
Local<Value> values[3];
|
|
values[0] = v8::Integer::New(isolate, static_cast<uint8_t>(result.status));
|
|
if (ToV8Value(context, result.message).ToLocal(&values[1]) &&
|
|
ToV8Value(context, result.cache_directory).ToLocal(&values[2])) {
|
|
args.GetReturnValue().Set(
|
|
Array::New(isolate, &values[0], arraysize(values)));
|
|
}
|
|
}
|
|
|
|
void GetCompileCacheDir(const FunctionCallbackInfo<Value>& args) {
|
|
Isolate* isolate = args.GetIsolate();
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
if (!env->use_compile_cache()) {
|
|
args.GetReturnValue().Set(v8::String::Empty(isolate));
|
|
return;
|
|
}
|
|
|
|
Local<Value> ret;
|
|
if (ToV8Value(context, env->compile_cache_handler()->cache_dir())
|
|
.ToLocal(&ret)) {
|
|
args.GetReturnValue().Set(ret);
|
|
}
|
|
}
|
|
|
|
void GetCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
|
|
Isolate* isolate = args.GetIsolate();
|
|
CHECK(args[0]->IsString()); // TODO(joyeecheung): accept buffer.
|
|
CHECK(args[1]->IsString());
|
|
CHECK(args[2]->IsUint32());
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
if (!env->use_compile_cache()) {
|
|
return;
|
|
}
|
|
Local<String> source = args[0].As<String>();
|
|
Local<String> filename = args[1].As<String>();
|
|
CachedCodeType type =
|
|
static_cast<CachedCodeType>(args[2].As<v8::Uint32>()->Value());
|
|
auto* cache_entry =
|
|
env->compile_cache_handler()->GetOrInsert(source, filename, type);
|
|
if (cache_entry == nullptr) {
|
|
return;
|
|
}
|
|
|
|
v8::LocalVector<v8::Name> names(isolate,
|
|
{FIXED_ONE_BYTE_STRING(isolate, "external")});
|
|
v8::LocalVector<v8::Value> values(
|
|
isolate,
|
|
{v8::External::New(
|
|
isolate, cache_entry, v8::kExternalPointerTypeTagDefault)});
|
|
if (cache_entry->cache != nullptr) {
|
|
Debug(env,
|
|
DebugCategory::COMPILE_CACHE,
|
|
"[compile cache] retrieving transpile cache for %s %s...",
|
|
cache_entry->type_name(),
|
|
cache_entry->source_filename);
|
|
|
|
std::string_view cache(
|
|
reinterpret_cast<const char*>(cache_entry->cache->data),
|
|
cache_entry->cache->length);
|
|
Local<Value> transpiled;
|
|
// TODO(joyeecheung): convert with simdutf and into external strings
|
|
if (!ToV8Value(context, cache).ToLocal(&transpiled)) {
|
|
Debug(env, DebugCategory::COMPILE_CACHE, "failed\n");
|
|
return;
|
|
} else {
|
|
Debug(env, DebugCategory::COMPILE_CACHE, "success\n");
|
|
}
|
|
names.push_back(FIXED_ONE_BYTE_STRING(isolate, "transpiled"));
|
|
values.push_back(transpiled);
|
|
} else {
|
|
Debug(env,
|
|
DebugCategory::COMPILE_CACHE,
|
|
"[compile cache] no transpile cache for %s %s\n",
|
|
cache_entry->type_name(),
|
|
cache_entry->source_filename);
|
|
}
|
|
args.GetReturnValue().Set(Object::New(
|
|
isolate, v8::Null(isolate), names.data(), values.data(), names.size()));
|
|
}
|
|
|
|
void SaveCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
|
|
Isolate* isolate = args.GetIsolate();
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
DCHECK(env->use_compile_cache());
|
|
CHECK(args[0]->IsExternal());
|
|
CHECK(args[1]->IsString()); // TODO(joyeecheung): accept buffer.
|
|
auto* cache_entry = static_cast<CompileCacheEntry*>(
|
|
args[0].As<External>()->Value(v8::kExternalPointerTypeTagDefault));
|
|
Utf8Value utf8(isolate, args[1].As<String>());
|
|
env->compile_cache_handler()->MaybeSave(cache_entry, utf8.ToStringView());
|
|
}
|
|
|
|
// Deserializes cached compiled code for the given wire bytes. Returns an empty
|
|
// MaybeLocal if V8 rejects the cached code, in which case the module must be
|
|
// compiled from the wire bytes as usual.
|
|
MaybeLocal<WasmModuleObject> DeserializeWasmModule(
|
|
Isolate* isolate,
|
|
MemorySpan<const uint8_t> wire,
|
|
MemorySpan<const uint8_t> code,
|
|
const WasmModuleObject::CompileOptions& options) {
|
|
// The resolution callback runs inside a HandleScope owned by V8, so the
|
|
// result must be persisted in a Global to outlive it.
|
|
struct State {
|
|
bool in_finish = true;
|
|
bool accepted = false;
|
|
Global<WasmModuleObject> module;
|
|
};
|
|
auto state = std::make_shared<State>();
|
|
|
|
WasmModuleCompilation compilation(options);
|
|
compilation.SetHasCompiledModuleBytes();
|
|
compilation.OnBytesReceived(wire.data(), wire.size());
|
|
compilation.Finish(
|
|
isolate,
|
|
[&](WasmStreaming::ModuleCachingInterface& cache) {
|
|
state->accepted = cache.SetCachedCompiledModuleBytes(code);
|
|
},
|
|
[state,
|
|
isolate](std::variant<Local<WasmModuleObject>, Local<Value>> result) {
|
|
// When the cached code is accepted, V8 resolves synchronously within
|
|
// Finish(). Otherwise V8 falls back to compiling the wire bytes
|
|
// asynchronously; that result arrives later and is unused.
|
|
if (!state->in_finish) return;
|
|
if (auto* module = std::get_if<Local<WasmModuleObject>>(&result)) {
|
|
state->module.Reset(isolate, *module);
|
|
}
|
|
});
|
|
state->in_finish = false;
|
|
if (!state->accepted || state->module.IsEmpty()) return {};
|
|
return state->module.Get(isolate);
|
|
}
|
|
|
|
// Reads `{ builtins, importedStringConstants }` as accepted by the
|
|
// WebAssembly.Module constructor. `constants_module` provides storage for the
|
|
// string referenced by the returned options.
|
|
bool ParseWasmCompileOptions(Environment* env,
|
|
Local<Object> options,
|
|
std::string* constants_module,
|
|
WasmModuleObject::CompileOptions* result) {
|
|
Isolate* isolate = env->isolate();
|
|
Local<Context> context = env->context();
|
|
Local<Value> builtins;
|
|
if (!options->Get(context, FIXED_ONE_BYTE_STRING(isolate, "builtins"))
|
|
.ToLocal(&builtins)) {
|
|
return false;
|
|
}
|
|
if (builtins->IsArray()) {
|
|
Local<Array> array = builtins.As<Array>();
|
|
for (uint32_t i = 0; i < array->Length(); i++) {
|
|
Local<Value> builtin;
|
|
if (!array->Get(context, i).ToLocal(&builtin)) return false;
|
|
if (builtin->IsString() &&
|
|
builtin.As<String>()->StringEquals(
|
|
FIXED_ONE_BYTE_STRING(isolate, "js-string"))) {
|
|
result->builtins |=
|
|
WasmModuleObject::CompileOptions::Builtins::kJsString;
|
|
}
|
|
}
|
|
}
|
|
Local<Value> constants;
|
|
if (!options
|
|
->Get(context,
|
|
FIXED_ONE_BYTE_STRING(isolate, "importedStringConstants"))
|
|
.ToLocal(&constants)) {
|
|
return false;
|
|
}
|
|
if (constants->IsString()) {
|
|
*constants_module = Utf8Value(isolate, constants).ToString();
|
|
result->imported_string_constants_module = constants_module->c_str();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Returns undefined when the compile cache is disabled or not applicable,
|
|
// otherwise an object with an `external` handle to the cache entry, and, when
|
|
// the cached compiled code was accepted by V8, the deserialized `module`.
|
|
void GetWasmCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
|
|
Isolate* isolate = args.GetIsolate();
|
|
CHECK(args[0]->IsArrayBufferView());
|
|
CHECK(args[1]->IsString());
|
|
CHECK(args[2]->IsObject());
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
if (!env->use_compile_cache()) {
|
|
return;
|
|
}
|
|
ArrayBufferViewContents<uint8_t> source(args[0]);
|
|
Local<String> filename = args[1].As<String>();
|
|
auto* cache_entry = env->compile_cache_handler()->GetOrInsert(
|
|
source.data(), source.length(), filename, CachedCodeType::kWasm);
|
|
if (cache_entry == nullptr) {
|
|
return;
|
|
}
|
|
|
|
LocalVector<Name> names(isolate,
|
|
{FIXED_ONE_BYTE_STRING(isolate, "external")});
|
|
LocalVector<Value> values(
|
|
isolate,
|
|
{External::New(
|
|
isolate, cache_entry, v8::kExternalPointerTypeTagDefault)});
|
|
if (cache_entry->cache != nullptr) {
|
|
std::string constants_module;
|
|
WasmModuleObject::CompileOptions options;
|
|
if (!ParseWasmCompileOptions(
|
|
env, args[2].As<Object>(), &constants_module, &options)) {
|
|
return;
|
|
}
|
|
Debug(env,
|
|
DebugCategory::COMPILE_CACHE,
|
|
"[compile cache] deserializing %s %s...",
|
|
cache_entry->type_name(),
|
|
cache_entry->source_filename);
|
|
Local<WasmModuleObject> module;
|
|
if (DeserializeWasmModule(isolate,
|
|
{source.data(), source.length()},
|
|
{cache_entry->cache->data,
|
|
static_cast<size_t>(cache_entry->cache->length)},
|
|
options)
|
|
.ToLocal(&module)) {
|
|
Debug(env, DebugCategory::COMPILE_CACHE, "success\n");
|
|
names.push_back(FIXED_ONE_BYTE_STRING(isolate, "module"));
|
|
values.push_back(module);
|
|
} else {
|
|
Debug(env, DebugCategory::COMPILE_CACHE, "rejected\n");
|
|
}
|
|
} else {
|
|
Debug(env,
|
|
DebugCategory::COMPILE_CACHE,
|
|
"[compile cache] no cache for %s %s\n",
|
|
cache_entry->type_name(),
|
|
cache_entry->source_filename);
|
|
}
|
|
args.GetReturnValue().Set(Object::New(
|
|
isolate, v8::Null(isolate), names.data(), values.data(), names.size()));
|
|
}
|
|
|
|
void SaveWasmCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
|
|
Isolate* isolate = args.GetIsolate();
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
DCHECK(env->use_compile_cache());
|
|
CHECK(args[0]->IsExternal());
|
|
CHECK(args[1]->IsWasmModuleObject());
|
|
auto* cache_entry = static_cast<CompileCacheEntry*>(
|
|
args[0].As<External>()->Value(v8::kExternalPointerTypeTagDefault));
|
|
// Only called when the module was compiled from the wire bytes, so any
|
|
// existing cache was rejected.
|
|
env->compile_cache_handler()->MaybeSave(cache_entry,
|
|
args[1].As<WasmModuleObject>(),
|
|
cache_entry->cache != nullptr);
|
|
}
|
|
|
|
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
|
|
Local<ObjectTemplate> target) {
|
|
Isolate* isolate = isolate_data->isolate();
|
|
SetMethod(isolate, target, "readPackageJSON", ReadPackageJSON);
|
|
SetMethod(isolate, target, "parsePackageJSON", ParsePackageJSONBinding);
|
|
SetMethod(isolate,
|
|
target,
|
|
"getNearestParentPackageJSONType",
|
|
GetNearestParentPackageJSONType);
|
|
SetMethod(isolate,
|
|
target,
|
|
"getNearestParentPackageJSON",
|
|
GetNearestParentPackageJSON);
|
|
SetMethod(isolate, target, "getRootPackageJSON", GetRootPackageJSON);
|
|
SetMethod(
|
|
isolate, target, "getPackageScopeConfig", GetPackageScopeConfig<false>);
|
|
SetMethod(isolate, target, "getPackageType", GetPackageScopeConfig<true>);
|
|
SetMethod(isolate, target, "enableCompileCache", EnableCompileCache);
|
|
SetMethod(isolate, target, "getCompileCacheDir", GetCompileCacheDir);
|
|
SetMethod(isolate, target, "flushCompileCache", FlushCompileCache);
|
|
SetMethod(isolate, target, "getCompileCacheEntry", GetCompileCacheEntry);
|
|
SetMethod(isolate, target, "saveCompileCacheEntry", SaveCompileCacheEntry);
|
|
SetMethod(
|
|
isolate, target, "getWasmCompileCacheEntry", GetWasmCompileCacheEntry);
|
|
SetMethod(
|
|
isolate, target, "saveWasmCompileCacheEntry", SaveWasmCompileCacheEntry);
|
|
}
|
|
|
|
void BindingData::CreatePerContextProperties(Local<Object> target,
|
|
Local<Value> unused,
|
|
Local<Context> context,
|
|
void* priv) {
|
|
Realm* realm = Realm::GetCurrent(context);
|
|
realm->AddBindingData<BindingData>(target);
|
|
|
|
Isolate* isolate = Isolate::GetCurrent();
|
|
LocalVector<Value> compile_cache_status_values(isolate);
|
|
|
|
#define V(status) \
|
|
compile_cache_status_values.push_back( \
|
|
FIXED_ONE_BYTE_STRING(isolate, #status));
|
|
COMPILE_CACHE_STATUS(V)
|
|
#undef V
|
|
|
|
USE(target->Set(context,
|
|
FIXED_ONE_BYTE_STRING(isolate, "compileCacheStatus"),
|
|
Array::New(isolate,
|
|
compile_cache_status_values.data(),
|
|
compile_cache_status_values.size())));
|
|
|
|
LocalVector<Name> cached_code_type_keys(isolate);
|
|
LocalVector<Value> cached_code_type_values(isolate);
|
|
|
|
#define V(type, value) \
|
|
cached_code_type_keys.push_back(FIXED_ONE_BYTE_STRING(isolate, #type)); \
|
|
cached_code_type_values.push_back(Integer::New(isolate, value)); \
|
|
DCHECK_EQ(value, cached_code_type_values.size() - 1);
|
|
CACHED_CODE_TYPES(V)
|
|
#undef V
|
|
|
|
USE(target->Set(context,
|
|
FIXED_ONE_BYTE_STRING(isolate, "cachedCodeTypes"),
|
|
Object::New(isolate,
|
|
Null(isolate),
|
|
cached_code_type_keys.data(),
|
|
cached_code_type_values.data(),
|
|
cached_code_type_keys.size())));
|
|
|
|
LocalVector<Name> module_format_keys(isolate);
|
|
LocalVector<Value> module_format_values(isolate);
|
|
module_format_keys.push_back(FIXED_ONE_BYTE_STRING(isolate, "kCommonJS"));
|
|
module_format_values.push_back(
|
|
Integer::New(isolate, static_cast<int>(ModuleFormat::kCommonJS)));
|
|
module_format_keys.push_back(FIXED_ONE_BYTE_STRING(isolate, "kModule"));
|
|
module_format_values.push_back(
|
|
Integer::New(isolate, static_cast<int>(ModuleFormat::kModule)));
|
|
USE(target->Set(context,
|
|
FIXED_ONE_BYTE_STRING(isolate, "moduleFormats"),
|
|
Object::New(isolate,
|
|
Null(isolate),
|
|
module_format_keys.data(),
|
|
module_format_values.data(),
|
|
module_format_keys.size())));
|
|
}
|
|
|
|
void BindingData::RegisterExternalReferences(
|
|
ExternalReferenceRegistry* registry) {
|
|
registry->Register(ReadPackageJSON);
|
|
registry->Register(ParsePackageJSONBinding);
|
|
registry->Register(GetNearestParentPackageJSONType);
|
|
registry->Register(GetNearestParentPackageJSON);
|
|
registry->Register(GetRootPackageJSON);
|
|
registry->Register(GetPackageScopeConfig<false>);
|
|
registry->Register(GetPackageScopeConfig<true>);
|
|
registry->Register(EnableCompileCache);
|
|
registry->Register(GetCompileCacheDir);
|
|
registry->Register(FlushCompileCache);
|
|
registry->Register(GetCompileCacheEntry);
|
|
registry->Register(SaveCompileCacheEntry);
|
|
registry->Register(GetWasmCompileCacheEntry);
|
|
registry->Register(SaveWasmCompileCacheEntry);
|
|
}
|
|
|
|
} // namespace modules
|
|
} // namespace node
|
|
|
|
NODE_BINDING_CONTEXT_AWARE_INTERNAL(
|
|
modules, node::modules::BindingData::CreatePerContextProperties)
|
|
NODE_BINDING_PER_ISOLATE_INIT(
|
|
modules, node::modules::BindingData::CreatePerIsolateProperties)
|
|
NODE_BINDING_EXTERNAL_REFERENCE(
|
|
modules, node::modules::BindingData::RegisterExternalReferences)
|