current commit
261007.41 · src, test: migrate to cppgc-owned v8::MicrotaskQueue
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

#include "node_contextify.h"

#include "base_object-inl.h"
#include "cppgc/allocation.h"
#include "debug_utils-inl.h"
#include "memory_tracker-inl.h"
#include "module_wrap.h"
#include "node_context_data.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_process.h"
#include "node_sea.h"
#include "node_snapshot_builder.h"
#include "node_url.h"
#include "node_watchdog.h"
#include "util-inl.h"

namespace node {
namespace contextify {

using errors::TryCatchScope;

using v8::Array;
using v8::ArrayBufferView;
using v8::Boolean;
using v8::Context;
using v8::DictionaryTemplate;
using v8::EscapableHandleScope;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::IndexedPropertyHandlerConfiguration;
using v8::IndexFilter;
using v8::Int32;
using v8::Integer;
using v8::Intercepted;
using v8::Isolate;
using v8::JustVoid;
using v8::KeyCollectionMode;
using v8::Local;
using v8::LocalVector;
using v8::Maybe;
using v8::MaybeLocal;
using v8::MeasureMemoryExecution;
using v8::MeasureMemoryMode;
using v8::Message;
using v8::MicrotaskQueue;
using v8::MicrotasksPolicy;
using v8::Name;
using v8::NamedPropertyHandlerConfiguration;
using v8::Nothing;
using v8::Object;
using v8::ObjectTemplate;
using v8::PrimitiveArray;
using v8::Promise;
using v8::PropertyAttribute;
using v8::PropertyCallbackInfo;
using v8::PropertyDescriptor;
using v8::PropertyFilter;
using v8::PropertyHandlerFlags;
using v8::Script;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::String;
using v8::Symbol;
using v8::UnboundScript;
using v8::Value;

// This is a helper function mediates deleted v8::PropertyDescriptor copy/move.
// The `Fn` receives a mutable reference of a stack allocated
// PropertyDescriptor, copied from the passed in const reference of
// PropertyDescriptor.
template <typename Fn>
auto WithPropertyDescriptorCopied(Isolate* isolate,
                                  const PropertyDescriptor& desc,
                                  Fn&& callback) {
  auto apply_attrs = [&](PropertyDescriptor& d) {
    if (desc.has_enumerable()) d.set_enumerable(desc.enumerable());
    if (desc.has_configurable()) d.set_configurable(desc.configurable());
    return callback(d);
  };
  if (desc.has_get() || desc.has_set()) {
    PropertyDescriptor d(
        desc.has_get() ? desc.get() : Undefined(isolate).As<Value>(),
        desc.has_set() ? desc.set() : Undefined(isolate).As<Value>());
    return apply_attrs(d);
  } else if (desc.has_value()) {
    if (desc.has_writable()) {
      PropertyDescriptor d(desc.value(), desc.writable());
      return apply_attrs(d);
    }
    PropertyDescriptor d(desc.value());
    return apply_attrs(d);
  }
  PropertyDescriptor d;
  return apply_attrs(d);
}

// The vm module executes code in a sandboxed environment with a different
// global object than the rest of the code. This is achieved by applying
// every call that changes or queries a property on the global `this` in the
// sandboxed code, to the sandbox object.
//
// The implementation uses V8's interceptors for methods like `set`, `get`,
// `delete`, `defineProperty`, and for any query of the property attributes.
// Property handlers with interceptors are set on the object template for
// the sandboxed code. Handlers for both named properties and for indexed
// properties are used. Their functionality is almost identical, the indexed
// interceptors mostly just call the named interceptors.
//
// For every `get` of a global property in the sandboxed context, the
// interceptor callback checks the sandbox object for the property.
// If the property is defined on the sandbox, that result is returned to
// the original call instead of finishing the query on the global object.
//
// For every `set` of a global property, the interceptor callback defines or
// changes the property both on the sandbox and the global proxy.

ContextifyContext* ContextifyContext::New(Environment* env,
                                          Local<Object> sandbox_obj,
                                          ContextOptions* options) {
  Local<ObjectTemplate> object_template;
  HandleScope scope(env->isolate());
  CHECK_IMPLIES(sandbox_obj.IsEmpty(), options->vanilla);
  if (!sandbox_obj.IsEmpty()) {
    // Do not use the template with interceptors for vanilla contexts.
    object_template = env->contextify_global_template();
    DCHECK(!object_template.IsEmpty());
  }

  const SnapshotData* snapshot_data = env->isolate_data()->snapshot_data();

  MicrotaskQueue* queue =
      options->own_microtask_queue
          ? options->own_microtask_queue
          : env->isolate()->GetCurrentContext()->GetMicrotaskQueue();

  Local<Context> v8_context;
  if (!(CreateV8Context(env->isolate(), object_template, snapshot_data, queue)
            .ToLocal(&v8_context))) {
    // Allocation failure, maximum call stack size reached, termination, etc.
    return {};
  }
  return New(v8_context, env, sandbox_obj, options);
}

void ContextifyContext::Trace(cppgc::Visitor* visitor) const {
  CppgcMixin::Trace(visitor);
  visitor->Trace(context_);
}

ContextifyContext::ContextifyContext(Environment* env,
                                     Local<Object> wrapper,
                                     Local<Context> v8_context,
                                     ContextOptions* options)
    : microtask_queue_(options->own_microtask_queue) {
  CppgcMixin::Wrap(this, env, wrapper);

  context_.Reset(env->isolate(), v8_context);
  // This should only be done after the initial initializations of the context
  // global object is finished.
  DCHECK_NULL(v8_context->GetAlignedPointerFromEmbedderData(
      ContextEmbedderIndex::kContextifyContext,
      EmbedderDataTag::kPerContextData));
  v8_context->SetAlignedPointerInEmbedderData(
      ContextEmbedderIndex::kContextifyContext,
      this,
      EmbedderDataTag::kPerContextData);
}

void ContextifyContext::InitializeGlobalTemplates(IsolateData* isolate_data) {
  DCHECK(isolate_data->contextify_wrapper_template().IsEmpty());
  Local<FunctionTemplate> global_func_template =
      FunctionTemplate::New(isolate_data->isolate());
  Local<ObjectTemplate> global_object_template =
      global_func_template->InstanceTemplate();

  PropertyHandlerFlags flags = static_cast<PropertyHandlerFlags>(
      static_cast<int>(PropertyHandlerFlags::kHasNoSideEffect) |
      static_cast<int>(PropertyHandlerFlags::kHasDontDeleteProperty));
  NamedPropertyHandlerConfiguration config(PropertyGetterCallback,
                                           PropertySetterCallback,
                                           PropertyQueryCallback,
                                           PropertyDeleterCallback,
                                           PropertyEnumeratorCallback,
                                           PropertyDefinerCallback,
                                           PropertyDescriptorCallback,
                                           {},
                                           flags);

  IndexedPropertyHandlerConfiguration indexed_config(
      IndexedPropertyGetterCallback,
      IndexedPropertySetterCallback,
      IndexedPropertyQueryCallback,
      IndexedPropertyDeleterCallback,
      IndexedPropertyEnumeratorCallback,
      IndexedPropertyDefinerCallback,
      IndexedPropertyDescriptorCallback,
      {},
      flags);

  global_object_template->SetHandler(config);
  global_object_template->SetHandler(indexed_config);
  isolate_data->set_contextify_global_template(global_object_template);

  Local<FunctionTemplate> wrapper_func_template =
      BaseObject::MakeLazilyInitializedJSTemplate(isolate_data);
  Local<ObjectTemplate> wrapper_object_template =
      wrapper_func_template->InstanceTemplate();
  isolate_data->set_contextify_wrapper_template(wrapper_object_template);
}

MaybeLocal<Context> ContextifyContext::CreateV8Context(
    Isolate* isolate,
    Local<ObjectTemplate> object_template,
    const SnapshotData* snapshot_data,
    MicrotaskQueue* queue) {
  EscapableHandleScope scope(isolate);

  Local<Context> ctx;
  if (object_template.IsEmpty() || snapshot_data == nullptr) {
    ctx = Context::New(
        isolate,
        nullptr,  // extensions
        object_template,
        {},                                       // global object
        v8::DeserializeInternalFieldsCallback(),  // deserialization callback
        queue);
    if (ctx.IsEmpty() || InitializeBaseContextForSnapshot(ctx).IsNothing()) {
      return MaybeLocal<Context>();
    }
  } else if (!Context::FromSnapshot(
                  isolate,
                  SnapshotData::kNodeVMContextIndex,
                  v8::DeserializeInternalFieldsCallback(),  // deserialization
                                                            // callback
                  nullptr,                                  // extensions
                  {},                                       // global object
                  queue)
                  .ToLocal(&ctx)) {
    return MaybeLocal<Context>();
  }

  return scope.Escape(ctx);
}

ContextifyContext* ContextifyContext::New(Local<Context> v8_context,
                                          Environment* env,
                                          Local<Object> sandbox_obj,
                                          ContextOptions* options) {
  HandleScope scope(env->isolate());
  CHECK_IMPLIES(sandbox_obj.IsEmpty(), options->vanilla);
  // This only initializes part of the context. The primordials are
  // only initialized when needed because even deserializing them slows
  // things down significantly and they are only needed in rare occasions
  // in the vm contexts.
  if (InitializeContextRuntime(v8_context).IsNothing()) {
    return {};
  }

  Local<Context> main_context = env->context();
  Local<Object> new_context_global = v8_context->Global();
  v8_context->SetSecurityToken(main_context->GetSecurityToken());

  // We need to tie the lifetime of the sandbox object with the lifetime of
  // newly created context. We do this by making them hold references to each
  // other. The context can directly hold a reference to the sandbox as an
  // embedder data field. The sandbox uses a private symbol to hold a reference
  // to the ContextifyContext wrapper which in turn internally references
  // the context from its constructor.
  if (sandbox_obj.IsEmpty()) {
    v8_context->SetEmbedderData(ContextEmbedderIndex::kSandboxObject,
                                v8::Undefined(env->isolate()));
  } else {
    v8_context->SetEmbedderData(ContextEmbedderIndex::kSandboxObject,
                                sandbox_obj);
  }

  // Delegate the code generation validation to
  // node::ModifyCodeGenerationFromStrings.
  v8_context->AllowCodeGenerationFromStrings(false);
  v8_context->SetEmbedderData(
      ContextEmbedderIndex::kAllowCodeGenerationFromStrings,
      options->allow_code_gen_strings);
  v8_context->SetEmbedderData(ContextEmbedderIndex::kAllowWasmCodeGeneration,
                              options->allow_code_gen_wasm);

  Utf8Value name_val(env->isolate(), options->name);
  ContextInfo info(name_val.ToString());
  if (!options->origin.IsEmpty()) {
    Utf8Value origin_val(env->isolate(), options->origin);
    info.origin = origin_val.ToString();
  }

  ContextifyContext* result;
  Local<Object> wrapper;
  {
    Context::Scope context_scope(v8_context);
    if (!sandbox_obj.IsEmpty()) {
      Local<String> ctor_name = sandbox_obj->GetConstructorName();
      if (!ctor_name->Equals(v8_context, env->object_string())
               .FromMaybe(false) &&
          new_context_global
              ->DefineOwnProperty(
                  v8_context,
                  v8::Symbol::GetToStringTag(env->isolate()),
                  ctor_name,
                  static_cast<v8::PropertyAttribute>(v8::DontEnum))
              .IsNothing()) {
        return {};
      }
    }

    // Assign host_defined_options_id to the global object so that in the
    // callback of ImportModuleDynamically, we can get the
    // host_defined_options_id from the v8::Context without accessing the
    // wrapper object.
    if (new_context_global
            ->SetPrivate(v8_context,
                         env->host_defined_option_symbol(),
                         options->host_defined_options_id)
            .IsNothing()) {
      return {};
    }

    env->AssignToContext(v8_context, nullptr, info);

    if (!env->contextify_wrapper_template()
             ->NewInstance(v8_context)
             .ToLocal(&wrapper)) {
      return {};
    }
    DCHECK_NOT_NULL(env->isolate()->GetCppHeap());
    result = cppgc::MakeGarbageCollected<ContextifyContext>(
        env->cppgc_allocation_handle(), env, wrapper, v8_context, options);
  }

  Local<Object> wrapper_holder =
      sandbox_obj.IsEmpty() ? new_context_global : sandbox_obj;
  if (!wrapper_holder.IsEmpty() &&
      wrapper_holder
          ->SetPrivate(
              v8_context, env->contextify_context_private_symbol(), wrapper)
          .IsNothing()) {
    return {};
  }

  // Assign host_defined_options_id to the sandbox object or the global object
  // (for vanilla contexts) so that module callbacks like
  // importModuleDynamically can be registered once back to the JS land.
  if (!sandbox_obj.IsEmpty() &&
      sandbox_obj
          ->SetPrivate(v8_context,
                       env->host_defined_option_symbol(),
                       options->host_defined_options_id)
          .IsNothing()) {
    return {};
  }
  return result;
}

void ContextifyContext::CreatePerIsolateProperties(
    IsolateData* isolate_data, Local<ObjectTemplate> target) {
  Isolate* isolate = isolate_data->isolate();
  SetMethod(isolate, target, "makeContext", MakeContext);
}

void ContextifyContext::RegisterExternalReferences(
    ExternalReferenceRegistry* registry) {
  registry->Register(MakeContext);
  registry->Register(PropertyQueryCallback);
  registry->Register(PropertyGetterCallback);
  registry->Register(PropertySetterCallback);
  registry->Register(PropertyDescriptorCallback);
  registry->Register(PropertyDeleterCallback);
  registry->Register(PropertyEnumeratorCallback);
  registry->Register(PropertyDefinerCallback);
  registry->Register(IndexedPropertyQueryCallback);
  registry->Register(IndexedPropertyGetterCallback);
  registry->Register(IndexedPropertySetterCallback);
  registry->Register(IndexedPropertyDescriptorCallback);
  registry->Register(IndexedPropertyDeleterCallback);
  registry->Register(IndexedPropertyDefinerCallback);
  registry->Register(IndexedPropertyEnumeratorCallback);
}

// makeContext(sandbox, name, origin, strings, wasm);
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  ContextOptions options;

  CHECK_EQ(args.Length(), 7);
  Local<Object> sandbox;
  if (args[0]->IsObject()) {
    sandbox = args[0].As<Object>();
    // Don't allow contextifying a sandbox multiple times.
    CHECK(!sandbox
               ->HasPrivate(env->context(),
                            env->contextify_context_private_symbol())
               .FromJust());
  } else {
    CHECK(args[0]->IsSymbol());
    options.vanilla = true;
  }

  CHECK(args[1]->IsString());
  options.name = args[1].As<String>();

  CHECK(args[2]->IsString() || args[2]->IsUndefined());
  if (args[2]->IsString()) {
    options.origin = args[2].As<String>();
  }

  CHECK(args[3]->IsBoolean());
  options.allow_code_gen_strings = args[3].As<Boolean>();

  CHECK(args[4]->IsBoolean());
  options.allow_code_gen_wasm = args[4].As<Boolean>();

  if (args[5]->IsBoolean() && args[5]->BooleanValue(env->isolate())) {
    options.own_microtask_queue =
        MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit);
  }

  CHECK(args[6]->IsSymbol());
  options.host_defined_options_id = args[6].As<Symbol>();

  TryCatchScope try_catch(env);
  ContextifyContext* context_ptr =
      ContextifyContext::New(env, sandbox, &options);

  if (try_catch.HasCaught()) {
    if (!try_catch.HasTerminated())
      try_catch.ReThrow();
    return;
  }

  if (sandbox.IsEmpty()) {
    args.GetReturnValue().Set(context_ptr->context()->Global());
  } else {
    args.GetReturnValue().Set(sandbox);
  }
}

// static
ContextifyContext* ContextifyContext::ContextFromContextifiedSandbox(
    Environment* env, const Local<Object>& wrapper_holder) {
  Local<Value> contextify;
  if (wrapper_holder
          ->GetPrivate(env->context(), env->contextify_context_private_symbol())
          .ToLocal(&contextify) &&
      contextify->IsObject()) {
    return Unwrap<ContextifyContext>(contextify.As<Object>());
  }
  return nullptr;
}

template <typename T>
ContextifyContext* ContextifyContext::Get(const PropertyCallbackInfo<T>& args) {
  // TODO(joyeecheung): it should be fine to simply use
  // args.GetIsolate()->GetCurrentContext() and take the pointer at
  // ContextEmbedderIndex::kContextifyContext, as V8 is supposed to
  // push the creation context before invoking these callbacks.
  return Get(args.Holder());
}

ContextifyContext* ContextifyContext::Get(Local<Object> object) {
  Local<Context> context;
  if (!object->GetCreationContext().ToLocal(&context)) {
    return nullptr;
  }
  if (!ContextEmbedderTag::IsNodeContext(context)) {
    return nullptr;
  }
  return static_cast<ContextifyContext*>(
      context->GetAlignedPointerFromEmbedderData(
          ContextEmbedderIndex::kContextifyContext,
          EmbedderDataTag::kPerContextData));
}

bool ContextifyContext::IsStillInitializing(const ContextifyContext* ctx) {
  return ctx == nullptr || ctx->context_.IsEmpty();
}

// static
Intercepted ContextifyContext::PropertyQueryCallback(
    Local<Name> property, const PropertyCallbackInfo<Integer>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  per_process::Debug(
      DebugCategory::CONTEXTIFY, "PropertyQuery(%s)\n", property);

  Local<Context> context = ctx->context();
  Local<Object> sandbox = ctx->sandbox();

  PropertyAttribute attr;

  Maybe<bool> maybe_has = sandbox->HasOwnProperty(context, property);
  if (maybe_has.IsNothing()) {
    return Intercepted::kNo;
  } else if (maybe_has.FromJust()) {
    Maybe<bool> maybe_attr =
        sandbox->GetPropertyAttributes(context, property, &attr);
    if (!maybe_attr.FromMaybe(false)) {
      return Intercepted::kNo;
    }
    args.GetReturnValue().Set(attr);
    return Intercepted::kYes;
  } else {
    maybe_has = ctx->global_proxy()->HasRealNamedProperty(context, property);
    if (maybe_has.IsNothing()) {
      return Intercepted::kNo;
    } else if (maybe_has.FromJust()) {
      Maybe<PropertyAttribute> maybe_attr =
          ctx->global_proxy()->GetRealNamedPropertyAttributes(context,
                                                              property);
      if (!maybe_attr.To(&attr)) {
        return Intercepted::kNo;
      }
      args.GetReturnValue().Set(attr);
      return Intercepted::kYes;
    }
  }

  return Intercepted::kNo;
}

// static
Intercepted ContextifyContext::PropertyGetterCallback(
    Local<Name> property, const PropertyCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  per_process::Debug(
      DebugCategory::CONTEXTIFY, "PropertyGetter(name: %s)\n", property);

  Local<Context> context = ctx->context();
  Local<Object> sandbox = ctx->sandbox();

  TryCatchScope try_catch(env);
  MaybeLocal<Value> maybe_rv =
      sandbox->GetRealNamedProperty(context, property);
  if (maybe_rv.IsEmpty()) {
    maybe_rv =
        ctx->global_proxy()->GetRealNamedProperty(context, property);
  }

  Local<Value> rv;
  if (maybe_rv.ToLocal(&rv)) {
    if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
      try_catch.ReThrow();
    }
    if (rv == sandbox)
      rv = ctx->global_proxy();

    args.GetReturnValue().Set(rv);
    return Intercepted::kYes;
  }
  return Intercepted::kNo;
}

// static
Intercepted ContextifyContext::PropertySetterCallback(
    Local<Name> property,
    Local<Value> value,
    const PropertyCallbackInfo<Boolean>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  per_process::Debug(DebugCategory::CONTEXTIFY,
                     "PropertySetter(name: %s, value: %s), use-strict(%s)\n",
                     property,
                     value,
                     args.ShouldThrowOnError());

  Local<Context> context = ctx->context();
  PropertyAttribute attributes = PropertyAttribute::None;
  bool is_declared_on_global_proxy = ctx->global_proxy()
      ->GetRealNamedPropertyAttributes(context, property)
      .To(&attributes);
  bool read_only =
      static_cast<int>(attributes) &
      static_cast<int>(PropertyAttribute::ReadOnly);

  bool is_declared_on_sandbox = ctx->sandbox()
      ->GetRealNamedPropertyAttributes(context, property)
      .To(&attributes);
  read_only = read_only ||
      (static_cast<int>(attributes) &
      static_cast<int>(PropertyAttribute::ReadOnly));

  if (read_only) {
    return Intercepted::kNo;
  }

  bool is_declared = is_declared_on_global_proxy || is_declared_on_sandbox;

  if (!is_declared && property->IsSymbol()) {
    return Intercepted::kNo;
  }
  if (ctx->sandbox()->Set(context, property, value).IsNothing()) {
    return Intercepted::kNo;
  }

  Local<Value> desc;
  if (is_declared_on_sandbox &&
      ctx->sandbox()
          ->GetOwnPropertyDescriptor(context, property)
          .ToLocal(&desc) &&
      !desc->IsUndefined()) {
    Environment* env = Environment::GetCurrent(context);
    Local<Object> desc_obj = desc.As<Object>();

    // We have to specify the return value for any contextual or get/set
    // property
    if (desc_obj->HasOwnProperty(context, env->get_string()).FromMaybe(false) ||
        desc_obj->HasOwnProperty(context, env->set_string()).FromMaybe(false)) {
      return Intercepted::kYes;
    }
  }
  return Intercepted::kNo;
}

// static
Intercepted ContextifyContext::PropertyDescriptorCallback(
    Local<Name> property, const PropertyCallbackInfo<Value>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  per_process::Debug(
      DebugCategory::CONTEXTIFY, "PropertyDescriptor(name: %s)\n", property);

  Local<Context> context = ctx->context();

  Local<Object> sandbox = ctx->sandbox();

  if (sandbox->HasOwnProperty(context, property).FromMaybe(false)) {
    Local<Value> desc;
    if (sandbox->GetOwnPropertyDescriptor(context, property).ToLocal(&desc)) {
      args.GetReturnValue().Set(desc);
      return Intercepted::kYes;
    }
  }
  return Intercepted::kNo;
}

// static
Intercepted ContextifyContext::PropertyDefinerCallback(
    Local<Name> property,
    const PropertyDescriptor& desc,
    const PropertyCallbackInfo<Boolean>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  per_process::Debug(
      DebugCategory::CONTEXTIFY, "PropertyDefiner(name: %s)\n", property);

  Local<Context> context = ctx->context();
  Isolate* isolate = Isolate::GetCurrent();

  PropertyAttribute attributes = PropertyAttribute::None;
  bool is_declared =
      ctx->global_proxy()->GetRealNamedPropertyAttributes(context,
                                                          property)
          .To(&attributes);
  bool read_only =
      static_cast<int>(attributes) &
          static_cast<int>(PropertyAttribute::ReadOnly);
  bool dont_delete = static_cast<int>(attributes) &
                     static_cast<int>(PropertyAttribute::DontDelete);

  // If the property is set on the global as neither writable nor
  // configurable, don't change it on the global or sandbox.
  if (is_declared && read_only && dont_delete) {
    return Intercepted::kNo;
  }

  Local<Object> sandbox = ctx->sandbox();

  auto result =
      WithPropertyDescriptorCopied(isolate, desc, [&](PropertyDescriptor& d) {
        return sandbox->DefineProperty(context, property, d);
      });

  if (result.FromMaybe(false)) return Intercepted::kYes;
  return Intercepted::kNo;
}

// static
Intercepted ContextifyContext::PropertyDeleterCallback(
    Local<Name> property, const PropertyCallbackInfo<Boolean>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  per_process::Debug(
      DebugCategory::CONTEXTIFY, "PropertyDeleter(name: %s)\n", property);

  Maybe<bool> success = ctx->sandbox()->Delete(ctx->context(), property);

  if (success.FromMaybe(false)) {
    return Intercepted::kNo;
  }

  // Delete failed on the sandbox, intercept and do not delete on
  // the global object.
  args.GetReturnValue().Set(false);
  return Intercepted::kYes;
}

// static
void ContextifyContext::PropertyEnumeratorCallback(
    const PropertyCallbackInfo<Array>& args) {
  // Named enumerator will be invoked on Object.keys,
  // Object.getOwnPropertyNames, Object.getOwnPropertySymbols,
  // Object.getOwnPropertyDescriptors, for...in, etc. operations.
  // Named enumerator should return all own non-indices property names,
  // including string properties and symbol properties. V8 will filter the
  // result array to match the expected symbol-only, enumerable-only with
  // NamedPropertyQueryCallback.
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) return;

  per_process::Debug(DebugCategory::CONTEXTIFY, "PropertyEnumerator()\n");

  Local<Array> properties;
  // Only get own named properties, exclude indices.
  if (!ctx->sandbox()
           ->GetPropertyNames(
               ctx->context(),
               KeyCollectionMode::kOwnOnly,
               static_cast<PropertyFilter>(PropertyFilter::ALL_PROPERTIES),
               IndexFilter::kSkipIndices)
           .ToLocal(&properties))
    return;

  args.GetReturnValue().Set(properties);
}

// static
void ContextifyContext::IndexedPropertyEnumeratorCallback(
    const PropertyCallbackInfo<Array>& args) {
  // Indexed enumerator will be invoked on Object.keys,
  // Object.getOwnPropertyNames, Object.getOwnPropertyDescriptors, for...in,
  // etc. operations. Indexed enumerator should return all own non-indices index
  // properties. V8 will filter the result array to match the expected
  // enumerable-only with IndexedPropertyQueryCallback.

  Isolate* isolate = args.GetIsolate();
  HandleScope scope(isolate);
  ContextifyContext* ctx = ContextifyContext::Get(args);
  Local<Context> context = ctx->context();

  // Still initializing
  if (IsStillInitializing(ctx)) return;

  per_process::Debug(DebugCategory::CONTEXTIFY,
                     "IndexedPropertyEnumerator()\n");

  Local<Array> properties;

  // Only get own index properties.
  if (!ctx->sandbox()
           ->GetPropertyNames(
               context,
               KeyCollectionMode::kOwnOnly,
               static_cast<PropertyFilter>(PropertyFilter::SKIP_SYMBOLS),
               IndexFilter::kIncludeIndices)
           .ToLocal(&properties))
    return;

  std::vector<v8::Global<Value>> properties_vec;
  if (FromV8Array(context, properties, &properties_vec).IsNothing()) {
    return;
  }

  // Filter out non-number property names.
  LocalVector<Value> indices(isolate);
  for (uint32_t i = 0; i < properties->Length(); i++) {
    Local<Value> prop = properties_vec[i].Get(isolate);
    if (!prop->IsNumber()) {
      continue;
    }
    indices.push_back(prop);
  }

  args.GetReturnValue().Set(
      Array::New(args.GetIsolate(), indices.data(), indices.size()));
}

// static
Intercepted ContextifyContext::IndexedPropertyQueryCallback(
    uint32_t index, const PropertyCallbackInfo<Integer>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  Local<String> name = Uint32ToString(ctx->context(), index);
  return ContextifyContext::PropertyQueryCallback(name, args);
}

// static
Intercepted ContextifyContext::IndexedPropertyGetterCallback(
    uint32_t index, const PropertyCallbackInfo<Value>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  Local<String> name = Uint32ToString(ctx->context(), index);
  return ContextifyContext::PropertyGetterCallback(name, args);
}

Intercepted ContextifyContext::IndexedPropertySetterCallback(
    uint32_t index,
    Local<Value> value,
    const PropertyCallbackInfo<Boolean>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  Local<String> name = Uint32ToString(ctx->context(), index);
  return ContextifyContext::PropertySetterCallback(name, value, args);
}

// static
Intercepted ContextifyContext::IndexedPropertyDescriptorCallback(
    uint32_t index, const PropertyCallbackInfo<Value>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  Local<String> name = Uint32ToString(ctx->context(), index);
  return ContextifyContext::PropertyDescriptorCallback(name, args);
}

Intercepted ContextifyContext::IndexedPropertyDefinerCallback(
    uint32_t index,
    const PropertyDescriptor& desc,
    const PropertyCallbackInfo<Boolean>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  Local<String> name = Uint32ToString(ctx->context(), index);
  return ContextifyContext::PropertyDefinerCallback(name, desc, args);
}

// static
Intercepted ContextifyContext::IndexedPropertyDeleterCallback(
    uint32_t index, const PropertyCallbackInfo<Boolean>& args) {
  ContextifyContext* ctx = ContextifyContext::Get(args);

  // Still initializing
  if (IsStillInitializing(ctx)) {
    return Intercepted::kNo;
  }

  Maybe<bool> success = ctx->sandbox()->Delete(ctx->context(), index);

  if (success.FromMaybe(false)) {
    return Intercepted::kNo;
  }

  // Delete failed on the sandbox, intercept and do not delete on
  // the global object.
  args.GetReturnValue().Set(false);
  return Intercepted::kYes;
}

void ContextifyScript::CreatePerIsolateProperties(
    IsolateData* isolate_data, Local<ObjectTemplate> target) {
  Isolate* isolate = isolate_data->isolate();
  Local<String> class_name = FIXED_ONE_BYTE_STRING(isolate, "ContextifyScript");

  Local<FunctionTemplate> script_tmpl = NewFunctionTemplate(isolate, New);
  script_tmpl->InstanceTemplate()->SetInternalFieldCount(
      ContextifyScript::kInternalFieldCount);
  script_tmpl->SetClassName(class_name);
  SetProtoMethod(isolate, script_tmpl, "createCachedData", CreateCachedData);
  SetProtoMethod(isolate, script_tmpl, "runInContext", RunInContext);

  target->Set(isolate, "ContextifyScript", script_tmpl);
  isolate_data->set_script_context_constructor_template(script_tmpl);
}

void ContextifyScript::RegisterExternalReferences(
    ExternalReferenceRegistry* registry) {
  registry->Register(New);
  registry->Register(CreateCachedData);
  registry->Register(RunInContext);
}

ContextifyScript* ContextifyScript::New(Environment* env,
                                        Local<Object> object) {
  DCHECK_NOT_NULL(env->isolate()->GetCppHeap());
  return cppgc::MakeGarbageCollected<ContextifyScript>(
      env->cppgc_allocation_handle(), env, object);
}

void ContextifyScript::New(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();
  Local<Context> context = env->context();

  CHECK(args.IsConstructCall());

  const int argc = args.Length();
  CHECK_GE(argc, 2);

  CHECK(args[0]->IsString());
  Local<String> code = args[0].As<String>();

  CHECK(args[1]->IsString());
  Local<String> filename = args[1].As<String>();

  int line_offset = 0;
  int column_offset = 0;
  Local<ArrayBufferView> cached_data_buf;
  bool produce_cached_data = false;
  Local<Context> parsing_context = context;

  Local<Symbol> id_symbol;
  if (argc > 2) {
    // new ContextifyScript(code, filename, lineOffset, columnOffset,
    //                      cachedData, produceCachedData, parsingContext,
    //                      hostDefinedOptionId)
    CHECK_EQ(argc, 8);
    CHECK(args[2]->IsNumber());
    line_offset = args[2].As<Int32>()->Value();
    CHECK(args[3]->IsNumber());
    column_offset = args[3].As<Int32>()->Value();
    if (!args[4]->IsUndefined()) {
      CHECK(args[4]->IsArrayBufferView());
      cached_data_buf = args[4].As<ArrayBufferView>();
    }
    CHECK(args[5]->IsBoolean());
    produce_cached_data = args[5]->IsTrue();
    if (!args[6]->IsUndefined()) {
      CHECK(args[6]->IsObject());
      ContextifyContext* sandbox =
          ContextifyContext::ContextFromContextifiedSandbox(
              env, args[6].As<Object>());
      CHECK_NOT_NULL(sandbox);
      parsing_context = sandbox->context();
    }
    CHECK(args[7]->IsSymbol());
    id_symbol = args[7].As<Symbol>();
  }

  ContextifyScript* contextify_script = New(env, args.This());

  if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(
          TRACING_CATEGORY_NODE2(vm, script)) != 0) {
    Utf8Value fn(isolate, filename);
    TRACE_EVENT_BEGIN1(TRACING_CATEGORY_NODE2(vm, script),
                       "ContextifyScript::New",
                       "filename",
                       TRACE_STR_COPY(*fn));
  }

  ScriptCompiler::CachedData* cached_data = nullptr;
  if (!cached_data_buf.IsEmpty()) {
    uint8_t* data = static_cast<uint8_t*>(cached_data_buf->Buffer()->Data());
    cached_data = new ScriptCompiler::CachedData(
        data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength());
  }

  Local<PrimitiveArray> host_defined_options =
      PrimitiveArray::New(isolate, loader::HostDefinedOptions::kLength);
  host_defined_options->Set(
      isolate, loader::HostDefinedOptions::kID, id_symbol);

  ScriptOrigin origin(filename,
                      line_offset,     // line offset
                      column_offset,   // column offset
                      true,            // is cross origin
                      -1,              // script id
                      Local<Value>(),  // source map URL
                      false,           // is opaque (?)
                      false,           // is WASM
                      false,           // is ES Module
                      host_defined_options);
  ScriptCompiler::Source source(code, origin, cached_data);
  ScriptCompiler::CompileOptions compile_options =
      ScriptCompiler::kNoCompileOptions;

  if (source.GetCachedData() != nullptr)
    compile_options = ScriptCompiler::kConsumeCodeCache;

  TryCatchScope try_catch(env);
  ShouldNotAbortOnUncaughtScope no_abort_scope(env);
  Context::Scope scope(parsing_context);

  MaybeLocal<UnboundScript> maybe_v8_script =
      ScriptCompiler::CompileUnboundScript(isolate, &source, compile_options);

  Local<UnboundScript> v8_script;
  if (!maybe_v8_script.ToLocal(&v8_script)) {
    errors::DecorateErrorStack(env, try_catch);
    no_abort_scope.Close();
    if (!try_catch.HasTerminated())
      try_catch.ReThrow();
    TRACE_EVENT_END0(TRACING_CATEGORY_NODE2(vm, script),
                     "ContextifyScript::New");
    return;
  }

  contextify_script->set_unbound_script(v8_script);

  std::unique_ptr<ScriptCompiler::CachedData> new_cached_data;
  if (produce_cached_data) {
    new_cached_data.reset(ScriptCompiler::CreateCodeCache(v8_script));
  }

  auto self = args.This();

  if (contextify_script->object()
          ->SetPrivate(context, env->host_defined_option_symbol(), id_symbol)
          .IsNothing()) {
    return;
  }
  if (StoreCodeCacheResult(env,
                           self,
                           compile_options,
                           source,
                           produce_cached_data,
                           std::move(new_cached_data))
          .IsNothing()) {
    return;
  }
  if (self->Set(env->context(),
                env->source_url_string(),
                v8_script->GetSourceURL())
          .IsNothing()) {
    return;
  }
  if (self->Set(env->context(),
                env->source_map_url_string(),
                v8_script->GetSourceMappingURL())
          .IsNothing()) {
    return;
  }

  TRACE_EVENT_END0(TRACING_CATEGORY_NODE2(vm, script), "ContextifyScript::New");
}

Maybe<void> StoreCodeCacheResult(
    Environment* env,
    Local<Object> target,
    ScriptCompiler::CompileOptions compile_options,
    const v8::ScriptCompiler::Source& source,
    bool produce_cached_data,
    std::unique_ptr<ScriptCompiler::CachedData> new_cached_data) {
  Local<Context> context;
  if (!target->GetCreationContext().ToLocal(&context)) {
    return Nothing<void>();
  }
  if (compile_options == ScriptCompiler::kConsumeCodeCache) {
    if (target
            ->Set(
                context,
                env->cached_data_rejected_string(),
                Boolean::New(env->isolate(), source.GetCachedData()->rejected))
            .IsNothing()) {
      return Nothing<void>();
    }
  }
  if (produce_cached_data) {
    bool cached_data_produced = new_cached_data != nullptr;
    if (cached_data_produced) {
      Local<Object> buf;
      if (!Buffer::Copy(env,
                        reinterpret_cast<const char*>(new_cached_data->data),
                        new_cached_data->length)
               .ToLocal(&buf) ||
          target->Set(context, env->cached_data_string(), buf).IsNothing() ||
          target
              ->Set(context,
                    env->cached_data_produced_string(),
                    Boolean::New(env->isolate(), cached_data_produced))
              .IsNothing()) {
        return Nothing<void>();
      }
    }
  }
  return JustVoid();
}

bool ContextifyScript::InstanceOf(Environment* env,
                                  const Local<Value>& value) {
  return !value.IsEmpty() &&
         env->script_context_constructor_template()->HasInstance(value);
}

void ContextifyScript::CreateCachedData(
    const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  ContextifyScript* wrapped_script;
  ASSIGN_OR_RETURN_UNWRAP_CPPGC(&wrapped_script, args.This());
  std::unique_ptr<ScriptCompiler::CachedData> cached_data(
      ScriptCompiler::CreateCodeCache(wrapped_script->unbound_script()));

  auto maybeRet = ([&] {
    if (!cached_data) {
      return Buffer::New(env, 0);
    }
    return Buffer::Copy(env,
                        reinterpret_cast<const char*>(cached_data->data),
                        cached_data->length);
  })();

  Local<Object> ret;
  if (maybeRet.ToLocal(&ret)) {
    args.GetReturnValue().Set(ret);
  }
}

void ContextifyScript::RunInContext(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  ContextifyScript* wrapped_script;
  ASSIGN_OR_RETURN_UNWRAP_CPPGC(&wrapped_script, args.This());

  CHECK_EQ(args.Length(), 5);
  CHECK(args[0]->IsObject() || args[0]->IsNull());

  Local<Context> context;
  v8::MicrotaskQueue* microtask_queue = nullptr;

  if (args[0]->IsObject()) {
    Local<Object> sandbox = args[0].As<Object>();
    // Get the context from the sandbox
    ContextifyContext* contextify_context =
        ContextifyContext::ContextFromContextifiedSandbox(env, sandbox);
    CHECK_NOT_NULL(contextify_context);
    CHECK_EQ(contextify_context->env(), env);

    context = contextify_context->context();
    if (context.IsEmpty()) return;

    microtask_queue = contextify_context->microtask_queue();
  } else {
    context = env->context();
  }

  TRACE_EVENT0(TRACING_CATEGORY_NODE2(vm, script), "RunInContext");

  CHECK(args[1]->IsNumber());
  int64_t timeout;
  if (!args[1]->IntegerValue(env->context()).To(&timeout)) {
    return;
  }

  CHECK(args[2]->IsBoolean());
  bool display_errors = args[2]->IsTrue();

  CHECK(args[3]->IsBoolean());
  bool break_on_sigint = args[3]->IsTrue();

  CHECK(args[4]->IsBoolean());
  bool break_on_first_line = args[4]->IsTrue();

  // Do the eval within the context
  EvalMachine(context,
              env,
              timeout,
              display_errors,
              break_on_sigint,
              break_on_first_line,
              microtask_queue,
              args);
}

bool ContextifyScript::EvalMachine(Local<Context> context,
                                   Environment* env,
                                   const int64_t timeout,
                                   const bool display_errors,
                                   const bool break_on_sigint,
                                   const bool break_on_first_line,
                                   MicrotaskQueue* mtask_queue,
                                   const FunctionCallbackInfo<Value>& args) {
  Context::Scope context_scope(context);

  if (!env->can_call_into_js())
    return false;
  if (!ContextifyScript::InstanceOf(env, args.This())) {
    THROW_ERR_INVALID_THIS(
        env,
        "Script methods can only be called on script instances.");
    return false;
  }

  TryCatchScope try_catch(env);
  ContextifyScript* wrapped_script;
  ASSIGN_OR_RETURN_UNWRAP_CPPGC(&wrapped_script, args.This(), false);
  Local<Script> script =
      wrapped_script->unbound_script()->BindToCurrentContext();

#if HAVE_INSPECTOR
  if (break_on_first_line) {
    if (!env->permission()->is_granted(env,
                                       permission::PermissionScope::kInspector,
                                       "PauseOnNextJavascriptStatement"))
        [[unlikely]] {
      node::permission::Permission::ThrowAccessDenied(
          env,
          permission::PermissionScope::kInspector,
          "PauseOnNextJavascriptStatement");
      if (display_errors) {
        // We should decorate non-termination exceptions
        errors::DecorateErrorStack(env, try_catch);
      }
      try_catch.ReThrow();
      return false;
    }
    env->inspector_agent()->PauseOnNextJavascriptStatement("Break on start");
  }
#endif

  MaybeLocal<Value> result;
  bool timed_out = false;
  bool received_signal = false;
  {
    auto wd = timeout != -1 ? std::make_optional<Watchdog>(
                                  env->isolate(), timeout, &timed_out)
                            : std::nullopt;
    auto swd = break_on_sigint ? std::make_optional<SigintWatchdog>(
                                     env->isolate(), &received_signal)
                               : std::nullopt;

    result = script->Run(context);
    if (!result.IsEmpty() && mtask_queue != nullptr)
      mtask_queue->PerformCheckpoint(env->isolate());
  }

  // Convert the termination exception into a regular exception.
  if (timed_out || received_signal) {
    if (!env->is_main_thread() && env->is_stopping())
      return false;
    env->isolate()->CancelTerminateExecution();
    // It is possible that execution was terminated by another timeout in
    // which this timeout is nested, so check whether one of the watchdogs
    // from this invocation is responsible for termination.
    if (timed_out) {
      node::THROW_ERR_SCRIPT_EXECUTION_TIMEOUT(env, timeout);
    } else if (received_signal) {
      node::THROW_ERR_SCRIPT_EXECUTION_INTERRUPTED(env);
    }
  }

  if (try_catch.HasCaught()) {
    if (!timed_out && !received_signal && display_errors) {
      // We should decorate non-termination exceptions
      errors::DecorateErrorStack(env, try_catch);
    }

    // If there was an exception thrown during script execution, re-throw it.
    // If one of the above checks threw, re-throw the exception instead of
    // letting try_catch catch it.
    // If execution has been terminated, but not by one of the watchdogs from
    // this invocation, this will re-throw a `null` value.
    if (!try_catch.HasTerminated())
      try_catch.ReThrow();

    return false;
  }

  // We checked for res being empty previously so this is a bit redundant
  // but still safer than using ToLocalChecked.
  Local<Value> res;
  if (!result.ToLocal(&res)) return false;

  args.GetReturnValue().Set(res);
  return true;
}

Local<UnboundScript> ContextifyScript::unbound_script() const {
  return script_.Get(env()->isolate());
}

void ContextifyScript::set_unbound_script(Local<UnboundScript> script) {
  script_.Reset(env()->isolate(), script);
}

void ContextifyScript::Trace(cppgc::Visitor* visitor) const {
  CppgcMixin::Trace(visitor);
  visitor->Trace(script_);
}

ContextifyScript::ContextifyScript(Environment* env, Local<Object> object) {
  CppgcMixin::Wrap(this, env, object);
}

ContextifyScript::~ContextifyScript() {}

void ContextifyFunction::RegisterExternalReferences(
    ExternalReferenceRegistry* registry) {
  registry->Register(CompileFunction);
}

void ContextifyFunction::CreatePerIsolateProperties(
    IsolateData* isolate_data, Local<ObjectTemplate> target) {
  Isolate* isolate = isolate_data->isolate();

  SetMethod(isolate, target, "compileFunction", CompileFunction);
}

void ContextifyFunction::CompileFunction(
    const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();
  Local<Context> context = env->context();

  // Argument 1: source code
  CHECK(args[0]->IsString());
  Local<String> code = args[0].As<String>();

  // Argument 2: filename
  CHECK(args[1]->IsString());
  Local<String> filename = args[1].As<String>();

  // Argument 3: line offset
  CHECK(args[2]->IsNumber());
  int line_offset = args[2].As<Int32>()->Value();

  // Argument 4: column offset
  CHECK(args[3]->IsNumber());
  int column_offset = args[3].As<Int32>()->Value();

  // Argument 5: cached data (optional)
  Local<ArrayBufferView> cached_data_buf;
  if (!args[4]->IsUndefined()) {
    CHECK(args[4]->IsArrayBufferView());
    cached_data_buf = args[4].As<ArrayBufferView>();
  }

  // Argument 6: produce cache data
  CHECK(args[5]->IsBoolean());
  bool produce_cached_data = args[5]->IsTrue();

  // Argument 7: parsing context (optional)
  Local<Context> parsing_context;
  if (!args[6]->IsUndefined()) {
    CHECK(args[6]->IsObject());
    ContextifyContext* sandbox =
        ContextifyContext::ContextFromContextifiedSandbox(
            env, args[6].As<Object>());
    CHECK_NOT_NULL(sandbox);
    parsing_context = sandbox->context();
  } else {
    parsing_context = context;
  }

  // Argument 8: context extensions (optional)
  Local<Array> context_extensions_buf;
  if (!args[7]->IsUndefined()) {
    CHECK(args[7]->IsArray());
    context_extensions_buf = args[7].As<Array>();
  }

  // Argument 9: params for the function (optional)
  Local<Array> params_buf;
  if (!args[8]->IsUndefined()) {
    CHECK(args[8]->IsArray());
    params_buf = args[8].As<Array>();
  }

  // Argument 10: host-defined option symbol
  CHECK(args[9]->IsSymbol());
  Local<Symbol> id_symbol = args[9].As<Symbol>();

  // Read cache from cached data buffer
  ScriptCompiler::CachedData* cached_data = nullptr;
  if (!cached_data_buf.IsEmpty()) {
    uint8_t* data = static_cast<uint8_t*>(cached_data_buf->Buffer()->Data());
    cached_data = new ScriptCompiler::CachedData(
      data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength());
  }

  Local<PrimitiveArray> host_defined_options =
      loader::ModuleWrap::GetHostDefinedOptions(isolate, id_symbol);

  ScriptOrigin origin(filename,
                      line_offset,     // line offset
                      column_offset,   // column offset
                      true,            // is cross origin
                      -1,              // script id
                      Local<Value>(),  // source map URL
                      false,           // is opaque (?)
                      false,           // is WASM
                      false,           // is ES Module
                      host_defined_options);
  ScriptCompiler::Source source(code, origin, cached_data);

  ScriptCompiler::CompileOptions options;
  if (source.GetCachedData() != nullptr) {
    options = ScriptCompiler::kConsumeCodeCache;
  } else {
    options = ScriptCompiler::kNoCompileOptions;
  }

  Context::Scope scope(parsing_context);

  // Read context extensions from buffer
  LocalVector<Object> context_extensions(isolate);
  if (!context_extensions_buf.IsEmpty()) {
    for (uint32_t n = 0; n < context_extensions_buf->Length(); n++) {
      Local<Value> val;
      if (!context_extensions_buf->Get(context, n).ToLocal(&val)) return;
      CHECK(val->IsObject());
      context_extensions.push_back(val.As<Object>());
    }
  }

  // Read params from params buffer
  LocalVector<String> params(isolate);
  if (!params_buf.IsEmpty()) {
    for (uint32_t n = 0; n < params_buf->Length(); n++) {
      Local<Value> val;
      if (!params_buf->Get(context, n).ToLocal(&val)) return;
      CHECK(val->IsString());
      params.push_back(val.As<String>());
    }
  }

  TryCatchScope try_catch(env);
  MaybeLocal<Object> maybe_result =
      CompileFunctionAndCacheResult(env,
                                    parsing_context,
                                    &source,
                                    params,
                                    context_extensions,
                                    options,
                                    produce_cached_data,
                                    id_symbol,
                                    try_catch);
  Local<Object> result;
  if (!maybe_result.ToLocal(&result)) {
    CHECK(try_catch.HasCaught());
    try_catch.ReThrow();
    return;
  }
  args.GetReturnValue().Set(result);
}

static LocalVector<String> GetCJSParameters(IsolateData* data) {
  LocalVector<String> result(data->isolate(),
                             {
                                 data->exports_string(),
                                 data->require_string(),
                                 data->module_string(),
                                 data->__filename_string(),
                                 data->__dirname_string(),
                             });
  return result;
}

MaybeLocal<Object> ContextifyFunction::CompileFunctionAndCacheResult(
    Environment* env,
    Local<Context> parsing_context,
    ScriptCompiler::Source* source,
    LocalVector<String> params,
    LocalVector<Object> context_extensions,
    ScriptCompiler::CompileOptions options,
    bool produce_cached_data,
    Local<Symbol> id_symbol,
    const TryCatchScope& try_catch) {
  MaybeLocal<Function> maybe_fn = ScriptCompiler::CompileFunction(
      parsing_context,
      source,
      params.size(),
      params.data(),
      context_extensions.size(),
      context_extensions.data(),
      options,
      v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason);

  Local<Function> fn;
  if (!maybe_fn.ToLocal(&fn)) {
    CHECK(try_catch.HasCaught());
    if (!try_catch.HasTerminated()) {
      errors::DecorateErrorStack(env, try_catch);
    }
    return {};
  }

  Local<Context> context = env->context();
  if (fn->SetPrivate(context, env->host_defined_option_symbol(), id_symbol)
          .IsNothing()) {
    return {};
  }

  auto tmpl = env->compiled_function_template();
  if (tmpl.IsEmpty()) {
    static constexpr std::string_view names[] = {
        "function",
        "sourceURL",
        "sourceMapURL",
        "cachedDataRejected",
        "cachedDataProduced",
        "cachedData",
    };
    tmpl = DictionaryTemplate::New(env->isolate(), names);
    env->set_compiled_function_template(tmpl);
  }

  auto scriptOrigin = fn->GetScriptOrigin();
  MaybeLocal<Value> values[] = {
      fn,
      // ScriptOrigin::ResourceName() returns SourceURL magic comment content if
      // present.
      scriptOrigin.ResourceName(),
      scriptOrigin.SourceMapUrl(),
      // These are conditionally filled in by StoreCodeCacheResult below.
      Undefined(env->isolate()),  // cachedDataRejected
      Undefined(env->isolate()),  // cachedDataProduced
      Undefined(env->isolate()),  // cachedData
  };

  Local<Object> result;
  if (!NewDictionaryInstance(env->context(), tmpl, values).ToLocal(&result)) {
    return {};
  }

  std::unique_ptr<ScriptCompiler::CachedData> new_cached_data;
  if (produce_cached_data) {
    new_cached_data.reset(ScriptCompiler::CreateCodeCacheForFunction(fn));
  }
  if (StoreCodeCacheResult(env,
                           result,
                           options,
                           *source,
                           produce_cached_data,
                           std::move(new_cached_data))
          .IsNothing()) {
    return {};
  }

  return result;
}

// When compiling as CommonJS source code that contains ESM syntax, the
// following error messages are returned:
// - `import` statements: "Cannot use import statement outside a module"
// - `export` statements: "Unexpected token 'export'"
// - `import.meta` references: "Cannot use 'import.meta' outside a module"
// Dynamic `import()` is permitted in CommonJS, so it does not error.
// While top-level `await` is not permitted in CommonJS, it returns the same
// error message as when `await` is used in a sync function, so we don't use it
// as a disambiguation.
static const auto esm_syntax_error_messages = std::array<std::string_view, 3>{
    "Cannot use import statement outside a module",  // `import` statements
    "Unexpected token 'export'",                     // `export` statements
    "Cannot use 'import.meta' outside a module"};    // `import.meta` references

// Another class of error messages that we need to check for are syntax errors
// where the syntax throws when parsed as CommonJS but succeeds when parsed as
// ESM. So far, the cases we've found are:
// - CommonJS module variables (`module`, `exports`, `require`, `__filename`,
//   `__dirname`): if the user writes code such as `const module =` in the top
//   level of a CommonJS module, it will throw a syntax error; but the same
//   code is valid in ESM.
// - Top-level `await`: if the user writes `await` at the top level of a
//   CommonJS module, it will throw a syntax error; but the same code is valid
//   in ESM.
static const auto throws_only_in_cjs_error_messages =
    std::array<std::string_view, 6>{
        "Identifier 'module' has already been declared",
        "Identifier 'exports' has already been declared",
        "Identifier 'require' has already been declared",
        "Identifier '__filename' has already been declared",
        "Identifier '__dirname' has already been declared",
        "await is only valid in async functions and "
        "the top level bodies of modules"};

static const auto maybe_top_level_await_errors =
    std::array<std::string_view, 2>{
        "missing ) after argument list",  // example: `func(await 1);`
        "SyntaxError: Unexpected"         // example: `if(await 1)`
    };

// If cached_data is provided, it would be used for the compilation and
// the on-disk compilation cache from NODE_COMPILE_CACHE (if configured)
// would be ignored.
static MaybeLocal<Function> CompileFunctionForCJSLoader(
    Environment* env,
    Local<Context> context,
    Local<String> code,
    Local<String> filename,
    bool* cache_rejected,
    bool is_cjs_scope,
    ScriptCompiler::CachedData* cached_data,
    Local<Symbol> host_defined_option_symbol) {
  Isolate* isolate = Isolate::GetCurrent();
  EscapableHandleScope scope(isolate);

  Local<PrimitiveArray> hdo = loader::ModuleWrap::GetHostDefinedOptions(
      isolate, host_defined_option_symbol);
  ScriptOrigin origin(filename,
                      0,               // line offset
                      0,               // column offset
                      true,            // is cross origin
                      -1,              // script id
                      Local<Value>(),  // source map URL
                      false,           // is opaque
                      false,           // is WASM
                      false,           // is ES Module
                      hdo);

  CompileCacheEntry* cache_entry = nullptr;
  if (cached_data == nullptr && env->use_compile_cache()) {
    cache_entry = env->compile_cache_handler()->GetOrInsert(
        code, filename, CachedCodeType::kCommonJS);
  }
  if (cache_entry != nullptr && cache_entry->cache != nullptr) {
    // source will take ownership of cached_data.
    cached_data = cache_entry->CopyCache();
  }

  ScriptCompiler::Source source(code, origin, cached_data);
  ScriptCompiler::CompileOptions options;
  if (cached_data == nullptr) {
    options = ScriptCompiler::kNoCompileOptions;
  } else {
    options = ScriptCompiler::kConsumeCodeCache;
  }

  LocalVector<String> params(isolate);
  if (is_cjs_scope) {
    params = GetCJSParameters(env->isolate_data());
  }
  MaybeLocal<Function> maybe_fn = ScriptCompiler::CompileFunction(
      context,
      &source,
      params.size(),
      params.data(),
      0,       /* context extensions size */
      nullptr, /* context extensions data */
      // TODO(joyeecheung): allow optional eager compilation.
      options);

  Local<Function> fn;
  if (!maybe_fn.ToLocal(&fn)) {
    return scope.EscapeMaybe(MaybeLocal<Function>());
  }

  if (options == ScriptCompiler::kConsumeCodeCache) {
    *cache_rejected = source.GetCachedData()->rejected;
  }
  if (cache_entry != nullptr) {
    env->compile_cache_handler()->MaybeSave(cache_entry, fn, *cache_rejected);
  }
  return scope.Escape(fn);
}

static std::string GetRequireEsmWarning(Local<String> filename) {
  Isolate* isolate = Isolate::GetCurrent();
  Utf8Value filename_utf8(isolate, filename);

  std::string warning_message =
      "Failed to load the ES module: " + filename_utf8.ToString() +
      ". Make sure to set \"type\": \"module\" in the nearest package.json "
      "file "
      "or use the .mjs extension.";
  return warning_message;
}

static bool warned_about_require_esm = false;

static bool ShouldRetryAsESM(Realm* realm,
                             Local<String> message,
                             Local<String> code,
                             Local<String> resource_name);

static void CompileFunctionForCJSLoader(
    const FunctionCallbackInfo<Value>& args) {
  CHECK(args[0]->IsString());
  CHECK(args[1]->IsString());
  CHECK(args[2]->IsBoolean());
  CHECK(args[3]->IsBoolean());
  Local<String> code = args[0].As<String>();
  Local<String> filename = args[1].As<String>();
  bool is_sea_main = args[2].As<Boolean>()->Value();
  bool should_detect_module = args[3].As<Boolean>()->Value();

  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Realm* realm = Realm::GetCurrent(context);
  Environment* env = realm->env();

  Local<Symbol> host_defined_option_symbol =
      env->vm_dynamic_import_default_internal();
  if (args.Length() > 4 && args[4].As<Boolean>()->Value()) {
    host_defined_option_symbol = env->embedder_module_hdo();
  }

  bool cache_rejected = false;
  Local<Function> fn;
  Local<Value> cjs_exception;
  Local<Message> cjs_message;

  ScriptCompiler::CachedData* cached_data = nullptr;
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
  if (is_sea_main) {
    const sea::SeaResource& sea = sea::FindSingleExecutableResource();
    // Use the "main" field in SEA config for the filename.
    Local<Value> filename_from_sea;
    if (!ToV8Value(context, sea.code_path).ToLocal(&filename_from_sea)) {
      return;
    }
    filename = filename_from_sea.As<String>();
    if (sea.use_code_cache()) {
      std::string_view data = sea.code_cache.value();
      cached_data = new ScriptCompiler::CachedData(
          reinterpret_cast<const uint8_t*>(data.data()),
          static_cast<int>(data.size()),
          v8::ScriptCompiler::CachedData::BufferNotOwned);
    }
  }
#endif

  {
    ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
    TryCatchScope try_catch(env);
    if (!CompileFunctionForCJSLoader(env,
                                     context,
                                     code,
                                     filename,
                                     &cache_rejected,
                                     true,
                                     cached_data,
                                     host_defined_option_symbol)
             .ToLocal(&fn)) {
      CHECK(try_catch.HasCaught());
      CHECK(!try_catch.HasTerminated());
      cjs_exception = try_catch.Exception();
      cjs_message = try_catch.Message();
      errors::DecorateErrorStack(env, cjs_exception, cjs_message);
    }
  }

  bool can_parse_as_esm = false;
  if (!cjs_exception.IsEmpty()) {
    // Use the URL to match what would be used in the origin if it's going to
    // be reparsed as ESM.
    Utf8Value filename_utf8(isolate, filename);
    std::string url = url::FromFilePath(filename_utf8.ToStringView());
    Local<Value> url_value;
    if (!ToV8Value(context, url).ToLocal(&url_value)) {
      return;
    }
    can_parse_as_esm = ShouldRetryAsESM(
        realm, cjs_message->Get(), code, url_value.As<String>());
    if (!can_parse_as_esm) {
      // The syntax error is not related to ESM, throw the original error.
      isolate->ThrowException(cjs_exception);
      return;
    }

    if (!should_detect_module) {
      bool should_throw = true;
      if (!warned_about_require_esm) {
        // This needs to call process.emit('warning') in JS which can throw if
        // the user listener throws. In that case, don't try to throw the syntax
        // error.
        std::string warning_message = GetRequireEsmWarning(filename);
        should_throw = ProcessEmitWarningSync(env, warning_message).IsJust();
      }
      if (should_throw) {
        isolate->ThrowException(cjs_exception);
      }
      return;
    }
  }

  auto tmpl = env->compiled_function_cjs_template();
  if (tmpl.IsEmpty()) {
    static constexpr std::string_view names[] = {
        "cachedDataRejected",
        "sourceMapURL",
        "sourceURL",
        "function",
        "canParseAsESM",
    };
    tmpl = DictionaryTemplate::New(isolate, names);
    env->set_compiled_function_cjs_template(tmpl);
  }

  Local<Value> undefined = v8::Undefined(isolate);

  MaybeLocal<Value> values[] = {
      Boolean::New(isolate, cache_rejected),
      fn.IsEmpty() ? undefined : fn->GetScriptOrigin().SourceMapUrl(),
      // ScriptOrigin::ResourceName() returns SourceURL magic comment content if
      // present.
      fn.IsEmpty() ? undefined : fn->GetScriptOrigin().ResourceName(),
      fn.IsEmpty() ? undefined : fn.As<Value>(),
      Boolean::New(isolate, can_parse_as_esm),
  };
  Local<Object> result;
  if (NewDictionaryInstance(env->context(), tmpl, values).ToLocal(&result)) {
    args.GetReturnValue().Set(result);
  }
}

bool ShouldRetryAsESM(Realm* realm,
                      Local<String> message,
                      Local<String> code,
                      Local<String> resource_name) {
  Isolate* isolate = realm->isolate();

  Utf8Value message_value(isolate, message);
  auto message_view = message_value.ToStringView();

  // These indicates that the file contains syntaxes that are only valid in
  // ESM. So it must be true.
  for (const auto& error_message : esm_syntax_error_messages) {
    if (message_view.find(error_message) != std::string_view::npos) {
      return true;
    }
  }

  // Check if the error message is allowed in ESM but not in CommonJS. If it
  // is the case, let's check if file can be compiled as ESM.
  bool maybe_valid_in_esm = false;
  for (const auto& error_message : throws_only_in_cjs_error_messages) {
    if (message_view.find(error_message) != std::string_view::npos) {
      maybe_valid_in_esm = true;
      break;
    }
  }

  for (const auto& error_message : maybe_top_level_await_errors) {
    if (message_view.find(error_message) != std::string_view::npos) {
      // If the error message is related to top-level await, we can try to
      // compile it as ESM.
      maybe_valid_in_esm = true;
      break;
    }
  }

  if (!maybe_valid_in_esm) {
    return false;
  }

  bool cache_rejected = false;
  TryCatchScope try_catch(realm->env());
  ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
  Local<v8::Module> module;
  Local<PrimitiveArray> hdo = loader::ModuleWrap::GetHostDefinedOptions(
      isolate, realm->isolate_data()->source_text_module_default_hdo());
  if (loader::ModuleWrap::CompileSourceTextModule(
          realm, code, resource_name, 0, 0, hdo, std::nullopt, &cache_rejected)
          .ToLocal(&module)) {
    return true;
  }

  return false;
}

static void ContainsModuleSyntax(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Realm* realm = Realm::GetCurrent(context);
  Environment* env = realm->env();

  CHECK_GE(args.Length(), 2);

  // Argument 1: source code
  CHECK(args[0]->IsString());
  Local<String> code = args[0].As<String>();

  // Argument 2: filename
  CHECK(args[1]->IsString());
  Local<String> filename = args[1].As<String>();

  // Argument 3: resource name (URL for ES module).
  Local<String> resource_name = filename;
  if (args[2]->IsString()) {
    resource_name = args[2].As<String>();
  }
  // Argument 4: flag to indicate if CJS variables should not be in scope
  // (they should be for normal CommonJS modules, but not for the
  // CommonJS eval scope).
  bool cjs_var = !args[3]->IsString();

  bool cache_rejected = false;
  Local<String> message;
  {
    Local<Function> fn;
    TryCatchScope try_catch(env);
    ShouldNotAbortOnUncaughtScope no_abort_scope(env);
    if (CompileFunctionForCJSLoader(env,
                                    context,
                                    code,
                                    filename,
                                    &cache_rejected,
                                    cjs_var,
                                    nullptr,
                                    env->vm_dynamic_import_default_internal())
            .ToLocal(&fn)) {
      args.GetReturnValue().Set(false);
      return;
    }
    CHECK(try_catch.HasCaught());
    message = try_catch.Message()->Get();
  }

  bool result = ShouldRetryAsESM(realm, message, code, resource_name);
  args.GetReturnValue().Set(result);
}

// Runs the JavaScript function passed as the first argument with a
// `SigintWatchdog` active, so that a SIGINT received while the function is
// executing terminates execution.
static void RunInterruptible(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = args.GetIsolate();
  CHECK(args[0]->IsFunction());
  Local<Function> fn = args[0].As<Function>();

  bool received_signal = false;
  TryCatchScope try_catch(env);
  {
    SigintWatchdog swd(isolate, &received_signal);
    USE(fn->Call(env->context(), Undefined(isolate), 0, nullptr));
  }

  // Convert the termination exception into a recoverable state.
  if (received_signal) {
    isolate->CancelTerminateExecution();
  } else if (try_catch.HasCaught()) {
    // A genuine exception (not a watchdog-triggered termination); propagate it.
    if (!try_catch.HasTerminated()) try_catch.ReThrow();
    return;
  }

  args.GetReturnValue().Set(received_signal);
}

static void MeasureMemory(const FunctionCallbackInfo<Value>& args) {
  CHECK(args[0]->IsInt32());
  CHECK(args[1]->IsInt32());
  int32_t mode = args[0].As<v8::Int32>()->Value();
  int32_t execution = args[1].As<v8::Int32>()->Value();
  Isolate* isolate = args.GetIsolate();

  Local<Context> current_context = isolate->GetCurrentContext();
  Local<Promise::Resolver> resolver;
  if (!Promise::Resolver::New(current_context).ToLocal(&resolver)) return;
  std::unique_ptr<v8::MeasureMemoryDelegate> delegate =
      v8::MeasureMemoryDelegate::Default(
          isolate,
          current_context,
          resolver,
          static_cast<v8::MeasureMemoryMode>(mode));
  isolate->MeasureMemory(std::move(delegate),
                         static_cast<v8::MeasureMemoryExecution>(execution));
  Local<Promise> promise = resolver->GetPromise();

  args.GetReturnValue().Set(promise);
}

void CreatePerIsolateProperties(IsolateData* isolate_data,
                                Local<ObjectTemplate> target) {
  Isolate* isolate = isolate_data->isolate();

  ContextifyContext::CreatePerIsolateProperties(isolate_data, target);
  ContextifyScript::CreatePerIsolateProperties(isolate_data, target);
  ContextifyFunction::CreatePerIsolateProperties(isolate_data, target);

  SetMethod(isolate, target, "runInterruptible", RunInterruptible);

  SetMethod(isolate, target, "measureMemory", MeasureMemory);
  SetMethod(isolate,
            target,
            "compileFunctionForCJSLoader",
            CompileFunctionForCJSLoader);

  SetMethod(isolate, target, "containsModuleSyntax", ContainsModuleSyntax);
}

static void CreatePerContextProperties(Local<Object> target,
                                       Local<Value> unused,
                                       Local<Context> context,
                                       void* priv) {
  Environment* env = Environment::GetCurrent(context);
  Isolate* isolate = env->isolate();

  Local<Object> constants = Object::New(env->isolate());
  Local<Object> measure_memory = Object::New(env->isolate());
  Local<Object> memory_execution = Object::New(env->isolate());

  {
    Local<Object> memory_mode = Object::New(env->isolate());
    MeasureMemoryMode SUMMARY = MeasureMemoryMode::kSummary;
    MeasureMemoryMode DETAILED = MeasureMemoryMode::kDetailed;
    NODE_DEFINE_CONSTANT(memory_mode, SUMMARY);
    NODE_DEFINE_CONSTANT(memory_mode, DETAILED);
    READONLY_PROPERTY(measure_memory, "mode", memory_mode);
  }

  {
    MeasureMemoryExecution DEFAULT = MeasureMemoryExecution::kDefault;
    MeasureMemoryExecution EAGER = MeasureMemoryExecution::kEager;
    NODE_DEFINE_CONSTANT(memory_execution, DEFAULT);
    NODE_DEFINE_CONSTANT(memory_execution, EAGER);
    READONLY_PROPERTY(measure_memory, "execution", memory_execution);
  }

  READONLY_PROPERTY(constants, "measureMemory", measure_memory);

  target->Set(context, env->constants_string(), constants).Check();
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
  ContextifyContext::RegisterExternalReferences(registry);
  ContextifyScript::RegisterExternalReferences(registry);
  ContextifyFunction::RegisterExternalReferences(registry);

  registry->Register(CompileFunctionForCJSLoader);
  registry->Register(RunInterruptible);
  registry->Register(MeasureMemory);
  registry->Register(ContainsModuleSyntax);
}
}  // namespace contextify
}  // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(
    contextify, node::contextify::CreatePerContextProperties)
NODE_BINDING_PER_ISOLATE_INIT(contextify,
                              node::contextify::CreatePerIsolateProperties)
NODE_BINDING_EXTERNAL_REFERENCE(contextify,
                                node::contextify::RegisterExternalReferences)