current commit
261028.1 · module: cache compiled WebAssembly modules in the compile cache
#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)