diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 417679ff..23fd5d87 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -155,6 +155,7 @@ if(ENABLE_JS_RUNTIME) runtime/modules/node/FS.cpp runtime/modules/node/Path.cpp runtime/modules/node/Process.cpp + runtime/modules/node/VM.cpp runtime/modules/performance/Performance.cpp runtime/ThreadSafeFunction.mm runtime/Bundle.mm diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index 4b1146ce..c55946be 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -4328,3 +4328,20 @@ napi_status qjs_update_stack_top(napi_env env) { JS_UpdateStackTop(env->runtime->runtime); return napi_clear_last_error(env); } + +napi_status qjs_create_scoped_value(napi_env env, JSValue value, + napi_value *result) { + return CreateScopedResult(env, value, result); +} + +JSContext *qjs_get_context(napi_env env) { + return env != NULL ? env->context : NULL; +} + +JSRuntime *qjs_get_runtime(napi_env env) { + if (env == NULL || env->runtime == NULL) { + return NULL; + } + + return env->runtime->runtime; +} diff --git a/NativeScript/napi/quickjs/quicks-runtime.h b/NativeScript/napi/quickjs/quicks-runtime.h index e1d969ca..43c3c47e 100644 --- a/NativeScript/napi/quickjs/quicks-runtime.h +++ b/NativeScript/napi/quickjs/quicks-runtime.h @@ -5,12 +5,14 @@ #ifndef TEST_APP_QUICKS_RUNTIME_H #define TEST_APP_QUICKS_RUNTIME_H #include "js_native_api.h" +#include "quickjs.h" EXTERN_C_START -NAPI_EXTERN napi_status NAPI_CDECL qjs_create_runtime(napi_runtime *runtime); +NAPI_EXTERN napi_status NAPI_CDECL qjs_create_runtime(napi_runtime* runtime); -NAPI_EXTERN napi_status NAPI_CDECL qjs_create_napi_env(napi_env *env, napi_runtime runtime); +NAPI_EXTERN napi_status NAPI_CDECL qjs_create_napi_env(napi_env* env, + napi_runtime runtime); NAPI_EXTERN napi_status NAPI_CDECL qjs_free_napi_env(napi_env env); @@ -18,18 +20,27 @@ NAPI_EXTERN napi_status NAPI_CDECL qjs_free_runtime(napi_runtime runtime); NAPI_EXTERN napi_status NAPI_CDECL qjs_execute_script(napi_env env, napi_value script, - const char *file, - napi_value *result); + const char* file, + napi_value* result); -NAPI_EXTERN napi_status NAPI_CDECL qjs_runtime_before_gc_callback(napi_env env, napi_finalize cb, void *data); - -NAPI_EXTERN napi_status NAPI_CDECL qjs_runtime_after_gc_callback(napi_env env, napi_finalize cb, void *data); +NAPI_EXTERN napi_status NAPI_CDECL +qjs_runtime_before_gc_callback(napi_env env, napi_finalize cb, void* data); +NAPI_EXTERN napi_status NAPI_CDECL +qjs_runtime_after_gc_callback(napi_env env, napi_finalize cb, void* data); NAPI_EXTERN napi_status NAPI_CDECL qjs_execute_pending_jobs(napi_env env); NAPI_EXTERN napi_status NAPI_CDECL qjs_update_stack_top(napi_env env); +NAPI_EXTERN JSContext* NAPI_CDECL qjs_get_context(napi_env env); + +NAPI_EXTERN JSRuntime* NAPI_CDECL qjs_get_runtime(napi_env env); + +NAPI_EXTERN napi_status NAPI_CDECL qjs_create_scoped_value(napi_env env, + JSValue value, + napi_value* result); + EXTERN_C_END -#endif //TEST_APP_QUICKS_RUNTIME_H +#endif // TEST_APP_QUICKS_RUNTIME_H diff --git a/NativeScript/napi/quickjs/source/quickjs.c b/NativeScript/napi/quickjs/source/quickjs.c index fde16ca2..31177f46 100644 --- a/NativeScript/napi/quickjs/source/quickjs.c +++ b/NativeScript/napi/quickjs/source/quickjs.c @@ -33614,7 +33614,8 @@ static JSValue __JS_EvalInternal(JSContext *ctx, JSValue this_obj, /* Could add a flag to avoid resolution if necessary */ if (m) { m->func_obj = fun_obj; - if (js_resolve_module(ctx, m) < 0) + if (!(flags & JS_EVAL_FLAG_COMPILE_ONLY_NO_RESOLVE) && + js_resolve_module(ctx, m) < 0) goto fail1; fun_obj = JS_NewModuleValue(ctx, m); } diff --git a/NativeScript/napi/quickjs/source/quickjs.h b/NativeScript/napi/quickjs/source/quickjs.h index 8b74d74e..17f142e7 100644 --- a/NativeScript/napi/quickjs/source/quickjs.h +++ b/NativeScript/napi/quickjs/source/quickjs.h @@ -312,6 +312,8 @@ static inline bool JS_VALUE_IS_NAN(JSValue v) JS_TAG_FUNCTION_BYTECODE or JS_TAG_MODULE tag. It can be executed with JS_EvalFunction(). */ #define JS_EVAL_FLAG_COMPILE_ONLY (1 << 5) +/* compile a module without resolving its imports yet */ +#define JS_EVAL_FLAG_COMPILE_ONLY_NO_RESOLVE JS_EVAL_FLAG_UNUSED /* don't include the stack frames before this eval in the Error() backtraces */ #define JS_EVAL_FLAG_BACKTRACE_BARRIER (1 << 6) /* allow top-level await in normal script. JS_Eval() returns a diff --git a/NativeScript/runtime/modules/node/Node.cpp b/NativeScript/runtime/modules/node/Node.cpp index a2fafbf8..fde90c65 100644 --- a/NativeScript/runtime/modules/node/Node.cpp +++ b/NativeScript/runtime/modules/node/Node.cpp @@ -3,13 +3,12 @@ #include "FS.h" #include "Path.h" #include "Process.h" +#include "VM.h" #include "native_api_util.h" namespace nativescript { -void Node::Init(napi_env env, napi_value global) { - Process::Init(env, global); -} +void Node::Init(napi_env env, napi_value global) { Process::Init(env, global); } napi_value Node::LoadInternalModule(napi_env env, const std::string& moduleName) { @@ -25,6 +24,10 @@ napi_value Node::LoadInternalModule(napi_env env, return Process::CreateModule(env); } + if (moduleName == "vm" || moduleName == "node:vm") { + return VM::CreateModule(env); + } + if (moduleName == "fs/promises" || moduleName == "node:fs/promises") { napi_value fsModule = FS::CreateModule(env); if (napi_util::is_null_or_undefined(env, fsModule)) { diff --git a/NativeScript/runtime/modules/node/VM.cpp b/NativeScript/runtime/modules/node/VM.cpp new file mode 100644 index 00000000..8e654e53 --- /dev/null +++ b/NativeScript/runtime/modules/node/VM.cpp @@ -0,0 +1,3578 @@ +#include "VM.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "js_native_api.h" +#include "native_api_util.h" + +#if defined(TARGET_ENGINE_V8) +#include "v8-api.h" +#elif defined(TARGET_ENGINE_QUICKJS) +#include "quickjs.h" +#include "quicks-runtime.h" +#endif + +namespace nativescript { + +namespace { + +constexpr char kContextSymbolName[] = "nativescript.vm.context"; +constexpr char kDefaultFilename[] = "vm.js"; +constexpr char kDefaultModuleIdentifier[] = "vm:module"; + +bool IsNullOrUndefined(napi_env env, napi_value value) { + if (value == nullptr) { + return true; + } + + napi_valuetype type; + if (napi_typeof(env, value, &type) != napi_ok) { + return false; + } + + return type == napi_null || type == napi_undefined; +} + +bool IsObjectLike(napi_env env, napi_value value) { + if (value == nullptr) { + return false; + } + + napi_valuetype type; + if (napi_typeof(env, value, &type) != napi_ok) { + return false; + } + + return type == napi_object || type == napi_function; +} + +bool CoerceToString(napi_env env, napi_value value, std::string& out) { + napi_value coerced; + if (napi_coerce_to_string(env, value, &coerced) != napi_ok) { + return false; + } + + size_t length = 0; + if (napi_get_value_string_utf8(env, coerced, nullptr, 0, &length) != + napi_ok) { + return false; + } + + std::vector buffer(length + 1); + if (napi_get_value_string_utf8(env, coerced, buffer.data(), buffer.size(), + nullptr) != napi_ok) { + return false; + } + + out.assign(buffer.data(), length); + return true; +} + +bool ReadOptionalStringProperty(napi_env env, napi_value object, + const char* property, std::string& out) { + if (IsNullOrUndefined(env, object)) { + return false; + } + + bool hasProperty = false; + if (napi_has_named_property(env, object, property, &hasProperty) != napi_ok || + !hasProperty) { + return false; + } + + napi_value value; + if (napi_get_named_property(env, object, property, &value) != napi_ok) { + return false; + } + + return CoerceToString(env, value, out); +} + +bool ReadOptionalProperty(napi_env env, napi_value object, const char* property, + napi_value* out) { + if (IsNullOrUndefined(env, object)) { + return false; + } + + bool hasProperty = false; + if (napi_has_named_property(env, object, property, &hasProperty) != napi_ok || + !hasProperty) { + return false; + } + + return napi_get_named_property(env, object, property, out) == napi_ok; +} + +std::string ReadIdentifierOption( + napi_env env, napi_value options, + const std::string& fallback = kDefaultModuleIdentifier) { + std::string identifier; + if (ReadOptionalStringProperty(env, options, "identifier", identifier) && + !identifier.empty()) { + return identifier; + } + + if (ReadOptionalStringProperty(env, options, "filename", identifier) && + !identifier.empty()) { + return identifier; + } + + if (!identifier.empty()) { + return identifier; + } + + return fallback; +} + +napi_value CreateUint8ArrayCopy(napi_env env, const uint8_t* data, + size_t length) { + void* bufferData = nullptr; + napi_value arrayBuffer; + if (napi_create_arraybuffer(env, length, &bufferData, &arrayBuffer) != + napi_ok) { + return nullptr; + } + + if (length > 0 && data != nullptr) { + memcpy(bufferData, data, length); + } + + napi_value typedArray; + if (napi_create_typedarray(env, napi_uint8_array, length, arrayBuffer, 0, + &typedArray) != napi_ok) { + return nullptr; + } + + return typedArray; +} + +napi_value CreateStringArray(napi_env env, + const std::vector& values) { + napi_value array; + if (napi_create_array_with_length(env, values.size(), &array) != napi_ok) { + return nullptr; + } + + for (size_t index = 0; index < values.size(); ++index) { + napi_value entry; + if (napi_create_string_utf8(env, values[index].c_str(), + values[index].size(), &entry) != napi_ok || + napi_set_element(env, array, index, entry) != napi_ok) { + return nullptr; + } + } + + return array; +} + +bool GetStringArray(napi_env env, napi_value value, + std::vector& out) { + bool isArray = false; + if (napi_is_array(env, value, &isArray) != napi_ok || !isArray) { + return false; + } + + uint32_t length = 0; + if (napi_get_array_length(env, value, &length) != napi_ok) { + return false; + } + + out.clear(); + out.reserve(length); + for (uint32_t index = 0; index < length; ++index) { + napi_value element; + std::string text; + if (napi_get_element(env, value, index, &element) != napi_ok || + !CoerceToString(env, element, text)) { + return false; + } + out.push_back(text); + } + + return true; +} + +bool CreatePromiseSettledWithUndefined(napi_env env, bool rejected, + napi_value reasonOrValue, + napi_value* result) { + napi_deferred deferred; + if (napi_create_promise(env, &deferred, result) != napi_ok) { + return false; + } + + napi_value undefined; + napi_get_undefined(env, &undefined); + + if (rejected) { + return napi_reject_deferred( + env, deferred, + reasonOrValue != nullptr ? reasonOrValue : undefined) == napi_ok; + } + + return napi_resolve_deferred( + env, deferred, + reasonOrValue != nullptr ? reasonOrValue : undefined) == napi_ok; +} + +std::vector ExtractModuleDependencySpecifiers( + const std::string& source) { + static const std::regex kImportFromPattern( + R"((?:^|[\r\n])\s*import\s+[^;]*?\s+from\s+['"]([^'"]+)['"])", + std::regex::ECMAScript); + static const std::regex kImportOnlyPattern( + R"((?:^|[\r\n])\s*import\s+['"]([^'"]+)['"])", std::regex::ECMAScript); + + std::vector specifiers; + std::unordered_set seen; + + for (std::sregex_iterator + it(source.begin(), source.end(), kImportFromPattern), + end; + it != end; ++it) { + const std::string specifier = (*it)[1].str(); + if (seen.insert(specifier).second) { + specifiers.push_back(specifier); + } + } + + for (std::sregex_iterator + it(source.begin(), source.end(), kImportOnlyPattern), + end; + it != end; ++it) { + const std::string specifier = (*it)[1].str(); + if (seen.insert(specifier).second) { + specifiers.push_back(specifier); + } + } + + return specifiers; +} + +std::string ReadFilenameOption(napi_env env, napi_value options, + const std::string& fallback = kDefaultFilename) { + if (IsNullOrUndefined(env, options)) { + return fallback; + } + + napi_valuetype type; + if (napi_typeof(env, options, &type) != napi_ok) { + return fallback; + } + + if (type == napi_string) { + std::string filename; + if (CoerceToString(env, options, filename) && !filename.empty()) { + return filename; + } + return fallback; + } + + if (type != napi_object) { + return fallback; + } + + bool hasFilename = false; + if (napi_has_named_property(env, options, "filename", &hasFilename) != + napi_ok || + !hasFilename) { + return fallback; + } + + napi_value filenameValue; + if (napi_get_named_property(env, options, "filename", &filenameValue) != + napi_ok) { + return fallback; + } + + std::string filename; + if (CoerceToString(env, filenameValue, filename) && !filename.empty()) { + return filename; + } + + return fallback; +} + +napi_value GetContextSymbol(napi_env env) { + napi_value global; + napi_value symbolCtor; + napi_value symbolFor; + napi_value description; + napi_value symbol; + + napi_get_global(env, &global); + napi_get_named_property(env, global, "Symbol", &symbolCtor); + napi_get_named_property(env, symbolCtor, "for", &symbolFor); + napi_create_string_utf8(env, kContextSymbolName, NAPI_AUTO_LENGTH, + &description); + napi_call_function(env, symbolCtor, symbolFor, 1, &description, &symbol); + return symbol; +} + +struct ContextState { +#if defined(TARGET_ENGINE_V8) + v8::Global context; +#elif defined(TARGET_ENGINE_QUICKJS) + JSRuntime* runtime = nullptr; + JSContext* context = nullptr; +#endif + std::unordered_set baselineKeys; + + ~ContextState() { +#if defined(TARGET_ENGINE_V8) + context.Reset(); +#elif defined(TARGET_ENGINE_QUICKJS) + if (context != nullptr) { + JS_FreeContext(context); + context = nullptr; + } +#endif + } +}; + +struct ScriptState { + std::string source; + std::string filename; +}; + +enum class ModuleKind { + kSourceText, + kSynthetic, +}; + +struct ModuleState { + napi_env env = nullptr; + ModuleKind kind = ModuleKind::kSourceText; + std::string identifier; + std::vector dependencySpecifiers; + std::vector exportNames; + std::vector linkedModules; + napi_ref contextRef = nullptr; + ContextState* contextState = nullptr; + napi_ref errorRef = nullptr; + +#if defined(TARGET_ENGINE_V8) + v8::Global module; +#elif defined(TARGET_ENGINE_QUICKJS) + JSRuntime* runtime = nullptr; + JSContext* context = nullptr; + JSValue moduleValue = JS_UNDEFINED; + std::unordered_map syntheticExports; + bool linked = false; + bool evaluating = false; + bool evaluated = false; + bool errored = false; + std::string errorMessage; +#endif +}; + +#if defined(TARGET_ENGINE_V8) +std::unordered_map& GetV8ModuleRegistry(); +#elif defined(TARGET_ENGINE_QUICKJS) +struct QuickJSModuleRegistry { + bool installed = false; + std::unordered_map modulesById; + std::unordered_map modulesByDef; + std::unordered_map resolutions; +}; + +std::unordered_map& +GetQuickJSModuleRegistries(); +std::string MakeQuickJSResolutionKey(const std::string& base, + const std::string& specifier); +QuickJSModuleRegistry& EnsureQuickJSModuleRegistry(JSRuntime* runtime); +std::string GetQuickJSModuleStatusString(ModuleState* state); +bool EnsureQuickJSImportMeta(ModuleState* state); +bool EnsureQuickJSLinked(napi_env env, ModuleState* state); +bool CacheQuickJSError(napi_env env, ModuleState* state, JSValue exception); +bool CreateQuickJSSourceTextModule(napi_env env, const std::string& sourceText, + napi_value options, napi_value* result); +bool CreateQuickJSSyntheticModule(napi_env env, + const std::vector& exportNames, + napi_value options, napi_value* result); +void CleanupQuickJSModuleState(ModuleState* state); +bool ApplyQuickJSSyntheticExports(ModuleState* state); +#endif + +void FinalizeContextState(napi_env env, void* data, void* /*hint*/) { + delete static_cast(data); +} + +void FinalizeScriptState(napi_env env, void* data, void* /*hint*/) { + delete static_cast(data); +} + +void FinalizeModuleState(napi_env env, void* data, void* /*hint*/) { + ModuleState* state = static_cast(data); + if (state == nullptr) { + return; + } + + if (state->contextRef != nullptr) { + napi_delete_reference(env, state->contextRef); + state->contextRef = nullptr; + } + + if (state->errorRef != nullptr) { + napi_delete_reference(env, state->errorRef); + state->errorRef = nullptr; + } + +#if defined(TARGET_ENGINE_V8) + auto& registry = GetV8ModuleRegistry(); + for (auto it = registry.begin(); it != registry.end();) { + if (it->second == state) { + it = registry.erase(it); + } else { + ++it; + } + } + state->module.Reset(); +#elif defined(TARGET_ENGINE_QUICKJS) + CleanupQuickJSModuleState(state); +#endif + + delete state; +} + +bool GetContextState(napi_env env, napi_value sandbox, ContextState** out) { + *out = nullptr; + + if (!IsObjectLike(env, sandbox)) { + return false; + } + + napi_value symbol = GetContextSymbol(env); + bool hasState = false; + if (napi_has_property(env, sandbox, symbol, &hasState) != napi_ok || + !hasState) { + return false; + } + + napi_value stateValue; + if (napi_get_property(env, sandbox, symbol, &stateValue) != napi_ok) { + return false; + } + + void* rawState = nullptr; + if (napi_get_value_external(env, stateValue, &rawState) != napi_ok || + rawState == nullptr) { + return false; + } + + *out = static_cast(rawState); + return true; +} + +bool RequireContextState(napi_env env, napi_value sandbox, ContextState** out) { + if (GetContextState(env, sandbox, out)) { + return true; + } + + napi_throw_type_error(env, nullptr, + "The \"contextifiedObject\" argument must be a vm " + "context created by vm.createContext()"); + return false; +} + +bool ReadModuleContextOption(napi_env env, napi_value options, + ContextState** outState, + napi_value* outContextObject) { + *outState = nullptr; + if (outContextObject != nullptr) { + *outContextObject = nullptr; + } + + napi_value contextValue; + if (!ReadOptionalProperty(env, options, "context", &contextValue) || + IsNullOrUndefined(env, contextValue)) { + return true; + } + + if (!RequireContextState(env, contextValue, outState)) { + return false; + } + + if (outContextObject != nullptr) { + *outContextObject = contextValue; + } + + return true; +} + +bool CreateModuleHandle(napi_env env, ModuleState* state, napi_value* result) { + return napi_create_external(env, state, FinalizeModuleState, nullptr, + result) == napi_ok; +} + +bool GetModuleState(napi_env env, napi_value value, ModuleState** out) { + *out = nullptr; + void* raw = nullptr; + if (napi_get_value_external(env, value, &raw) != napi_ok || raw == nullptr) { + napi_throw_type_error(env, nullptr, "Invalid vm.Module handle"); + return false; + } + + *out = static_cast(raw); + return true; +} + +bool SetModuleError(napi_env env, ModuleState* state, napi_value error) { + if (state->errorRef != nullptr) { + napi_delete_reference(env, state->errorRef); + state->errorRef = nullptr; + } + + if (IsNullOrUndefined(env, error)) { + return true; + } + + return napi_create_reference(env, error, 1, &state->errorRef) == napi_ok; +} + +napi_value GetStoredModuleError(napi_env env, ModuleState* state) { + if (state->errorRef == nullptr) { + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; + } + + napi_value error; + if (napi_get_reference_value(env, state->errorRef, &error) != napi_ok) { + return nullptr; + } + + return error; +} + +napi_value GetStoredModuleContext(napi_env env, ModuleState* state) { + if (state->contextRef == nullptr) { + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; + } + + napi_value context; + if (napi_get_reference_value(env, state->contextRef, &context) != napi_ok) { + return nullptr; + } + + return context; +} + +#if defined(TARGET_ENGINE_V8) + +std::vector CollectV8OwnStringKeys(v8::Isolate* isolate, + v8::Local context, + v8::Local object) { + std::vector result; + + v8::Local names; + if (!object->GetOwnPropertyNames(context).ToLocal(&names)) { + return result; + } + + const uint32_t length = names->Length(); + result.reserve(length); + + for (uint32_t index = 0; index < length; ++index) { + v8::Local key; + if (!names->Get(context, index).ToLocal(&key) || !key->IsString()) { + continue; + } + + v8::String::Utf8Value utf8(isolate, key); + if (*utf8 == nullptr) { + continue; + } + + result.emplace_back(*utf8, utf8.length()); + } + + return result; +} + +std::unordered_set ToKeySet(const std::vector& keys) { + return std::unordered_set(keys.begin(), keys.end()); +} + +#if defined(TARGET_ENGINE_QUICKJS) +struct QuickJSModuleRegistry { + bool installed = false; + std::unordered_map modulesById; + std::unordered_map resolutions; +}; + +std::unordered_map& +GetQuickJSModuleRegistries() { + static std::unordered_map registries; + return registries; +} + +std::string MakeQuickJSResolutionKey(const std::string& base, + const std::string& specifier) { + return base + "\n" + specifier; +} + +char* NormalizeQuickJSVmModule(JSContext* ctx, const char* base_name, + const char* name, void* opaque) { + QuickJSModuleRegistry* registry = static_cast(opaque); + if (registry == nullptr) { + return js_strdup(ctx, name); + } + + const std::string base = base_name != nullptr ? base_name : ""; + const auto it = + registry->resolutions.find(MakeQuickJSResolutionKey(base, name)); + if (it != registry->resolutions.end()) { + return js_strdup(ctx, it->second.c_str()); + } + + return js_strdup(ctx, name); +} + +JSModuleDef* LoadQuickJSVmModule(JSContext* /*ctx*/, const char* module_name, + void* opaque) { + QuickJSModuleRegistry* registry = static_cast(opaque); + if (registry == nullptr) { + return nullptr; + } + + const auto it = registry->modulesById.find(module_name); + if (it == registry->modulesById.end() || it->second == nullptr) { + return nullptr; + } + + return static_cast(JS_VALUE_GET_PTR(it->second->moduleValue)); +} + +QuickJSModuleRegistry& EnsureQuickJSModuleRegistry(JSRuntime* runtime) { + auto& registries = GetQuickJSModuleRegistries(); + QuickJSModuleRegistry& registry = registries[runtime]; + if (!registry.installed) { + JS_SetModuleLoaderFunc(runtime, &NormalizeQuickJSVmModule, + &LoadQuickJSVmModule, ®istry); + registry.installed = true; + } + return registry; +} + +std::string GetQuickJSModuleStatusString(ModuleState* state) { + if (state->errored) { + return "errored"; + } + if (state->evaluating) { + return "evaluating"; + } + if (state->evaluated) { + return "evaluated"; + } + if (state->linked) { + return "linked"; + } + return "unlinked"; +} + +bool EnsureQuickJSImportMeta(ModuleState* state) { + JSModuleDef* module = + static_cast(JS_VALUE_GET_PTR(state->moduleValue)); + JSValue meta = JS_GetImportMeta(state->context, module); + if (JS_IsException(meta)) { + return false; + } + + if (JS_DefinePropertyValueStr( + state->context, meta, "url", + JS_NewString(state->context, state->identifier.c_str()), + JS_PROP_C_W_E) < 0 || + JS_DefinePropertyValueStr(state->context, meta, "main", JS_FALSE, + JS_PROP_C_W_E) < 0) { + JS_FreeValue(state->context, meta); + return false; + } + + JS_FreeValue(state->context, meta); + return true; +} + +bool EnsureQuickJSLinked(napi_env env, ModuleState* state) { + if (state->linked) { + return true; + } + + if (JS_ResolveModule(state->context, state->moduleValue) < 0) { + state->errored = true; + state->errorMessage = GetQuickJSExceptionMessage( + state->context, JS_GetException(state->context)); + napi_throw_error(env, nullptr, state->errorMessage.c_str()); + return false; + } + + state->linked = true; + return true; +} + +bool CacheQuickJSError(napi_env env, ModuleState* state, JSValue exception) { + JSContext* mainContext = qjs_get_context(env); + JSValue cloned = CloneQuickJSValue(state->context, mainContext, exception); + JS_FreeValue(state->context, exception); + if (JS_IsException(cloned)) { + state->errorMessage = + GetQuickJSExceptionMessage(mainContext, JS_GetException(mainContext)); + napi_throw_error(env, nullptr, state->errorMessage.c_str()); + return false; + } + + napi_value error; + if (qjs_create_scoped_value(env, cloned, &error) != napi_ok || + !SetModuleError(env, state, error)) { + return false; + } + + return true; +} + +bool CreateQuickJSSourceTextModule(napi_env env, const std::string& sourceText, + napi_value options, napi_value* result) { + ContextState* contextState = nullptr; + napi_value contextObject = nullptr; + if (!ReadModuleContextOption(env, options, &contextState, &contextObject)) { + return false; + } + + JSContext* context = + contextState != nullptr ? contextState->context : qjs_get_context(env); + JSRuntime* runtime = JS_GetRuntime(context); + QuickJSModuleRegistry& registry = EnsureQuickJSModuleRegistry(runtime); + const std::string identifier = ReadIdentifierOption(env, options); + + JSValue moduleValue = JS_Eval( + context, sourceText.c_str(), sourceText.size(), identifier.c_str(), + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY | + JS_EVAL_FLAG_COMPILE_ONLY_NO_RESOLVE); + if (JS_IsException(moduleValue)) { + return ThrowLatestQuickJSException(env, context); + } + + std::unique_ptr state(new ModuleState()); + state->env = env; + state->kind = ModuleKind::kSourceText; + state->identifier = identifier; + state->contextState = contextState; + state->runtime = runtime; + state->context = context; + state->moduleValue = moduleValue; + state->dependencySpecifiers = ExtractModuleDependencySpecifiers(sourceText); + if (contextObject != nullptr && + napi_create_reference(env, contextObject, 1, &state->contextRef) != + napi_ok) { + return false; + } + + registry.modulesById[identifier] = state.get(); + registry + .modulesByDef[static_cast(JS_VALUE_GET_PTR(moduleValue))] = + state.get(); + if (!CreateModuleHandle(env, state.get(), result)) { + return false; + } + + state.release(); + return true; +} + +bool ApplyQuickJSSyntheticExports(ModuleState* state) { + JSModuleDef* moduleDef = + static_cast(JS_VALUE_GET_PTR(state->moduleValue)); + for (const auto& exportName : state->exportNames) { + JSValue exportValue = JS_UNDEFINED; + auto valueIt = state->syntheticExports.find(exportName); + if (valueIt != state->syntheticExports.end()) { + exportValue = JS_DupValue(state->context, valueIt->second); + } + + if (JS_SetModuleExport(state->context, moduleDef, exportName.c_str(), + exportValue) < 0) { + return false; + } + } + + return true; +} + +int InitializeQuickJSSyntheticModule(JSContext* ctx, JSModuleDef* moduleDef) { + auto& registries = GetQuickJSModuleRegistries(); + auto registryIt = registries.find(JS_GetRuntime(ctx)); + if (registryIt == registries.end()) { + return -1; + } + + auto stateIt = registryIt->second.modulesByDef.find(moduleDef); + if (stateIt == registryIt->second.modulesByDef.end() || + stateIt->second == nullptr) { + return -1; + } + + return ApplyQuickJSSyntheticExports(stateIt->second) ? 0 : -1; +} + +bool CreateQuickJSSyntheticModule(napi_env env, + const std::vector& exportNames, + napi_value options, napi_value* result) { + ContextState* contextState = nullptr; + napi_value contextObject = nullptr; + if (!ReadModuleContextOption(env, options, &contextState, &contextObject)) { + return false; + } + + JSContext* context = + contextState != nullptr ? contextState->context : qjs_get_context(env); + JSRuntime* runtime = JS_GetRuntime(context); + QuickJSModuleRegistry& registry = EnsureQuickJSModuleRegistry(runtime); + const std::string identifier = ReadIdentifierOption(env, options); + + JSModuleDef* moduleDef = JS_NewCModule(context, identifier.c_str(), + &InitializeQuickJSSyntheticModule); + if (moduleDef == nullptr) { + return ThrowLatestQuickJSException(env, context); + } + + for (const auto& exportName : exportNames) { + if (JS_AddModuleExport(context, moduleDef, exportName.c_str()) < 0) { + return ThrowLatestQuickJSException(env, context); + } + } + + std::unique_ptr state(new ModuleState()); + state->env = env; + state->kind = ModuleKind::kSynthetic; + state->identifier = identifier; + state->contextState = contextState; + state->runtime = runtime; + state->context = context; + state->exportNames = exportNames; + state->moduleValue = JS_DupValue(context, JS_MKPTR(JS_TAG_MODULE, moduleDef)); + if (contextObject != nullptr && + napi_create_reference(env, contextObject, 1, &state->contextRef) != + napi_ok) { + return false; + } + + registry.modulesById[identifier] = state.get(); + registry.modulesByDef[moduleDef] = state.get(); + if (!EnsureQuickJSLinked(env, state.get())) { + return false; + } + + if (!CreateModuleHandle(env, state.get(), result)) { + return false; + } + + state.release(); + return true; +} +#endif + +std::unordered_map& GetV8ModuleRegistry() { + static std::unordered_map registry; + return registry; +} + +v8::Local GetV8ModuleContext(napi_env env, ModuleState* state) { + return state->contextState != nullptr + ? state->contextState->context.Get(env->isolate) + : env->context(); +} + +std::string GetV8ModuleStatusString(v8::Module::Status status) { + switch (status) { + case v8::Module::kUninstantiated: + return "unlinked"; + case v8::Module::kInstantiating: + return "linking"; + case v8::Module::kInstantiated: + return "linked"; + case v8::Module::kEvaluating: + return "evaluating"; + case v8::Module::kEvaluated: + return "evaluated"; + case v8::Module::kErrored: + return "errored"; + } + + return "unlinked"; +} + +bool SetV8ModuleRegistryEntry(v8::Local module, + ModuleState* state) { + GetV8ModuleRegistry()[module->GetIdentityHash()] = state; + return true; +} + +v8::MaybeLocal ResolveV8VmModuleByIndex( + v8::Local context, size_t index, + v8::Local referrer) { + auto& registry = GetV8ModuleRegistry(); + auto it = registry.find(referrer->GetIdentityHash()); + if (it == registry.end() || it->second == nullptr) { + return v8::MaybeLocal(); + } + + ModuleState* referrerState = it->second; + if (index >= referrerState->linkedModules.size() || + referrerState->linkedModules[index] == nullptr) { + return v8::MaybeLocal(); + } + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return referrerState->linkedModules[index]->module.Get(isolate); +} + +v8::MaybeLocal EvaluateV8SyntheticModule( + v8::Local context, v8::Local /*module*/) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Local resolver = + v8::Promise::Resolver::New(context).ToLocalChecked(); + resolver->Resolve(context, v8::Undefined(isolate)).ToChecked(); + return resolver->GetPromise(); +} + +bool ExtractV8DependencySpecifiers(v8::Isolate* isolate, + v8::Local module, + std::vector& out) { + out.clear(); + + v8::Local requests = module->GetModuleRequests(); + const int length = requests->Length(); + out.reserve(length); + + for (int index = 0; index < length; ++index) { + v8::Local entry = + requests->Get(isolate->GetCurrentContext(), index); + v8::Local specifier = + v8::ModuleRequest::Cast(*entry)->GetSpecifier(); + v8::String::Utf8Value text(isolate, specifier); + if (*text == nullptr) { + return false; + } + out.emplace_back(*text, text.length()); + } + + return true; +} + +std::vector CollectV8ModuleExportNames(napi_env env, + ModuleState* state) { + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope scope(isolate); + v8::Local context = GetV8ModuleContext(env, state); + v8::Context::Scope contextScope(context); + + v8::Local module = state->module.Get(isolate); + if (module.IsEmpty() || module->GetStatus() < v8::Module::kInstantiated) { + return {}; + } + + v8::Local ns = module->GetModuleNamespace().As(); + return CollectV8OwnStringKeys(isolate, context, ns); +} + +bool ThrowV8ModuleException(napi_env env, ModuleState* state) { + v8::Isolate* isolate = env->isolate; + v8::HandleScope scope(isolate); + v8::Local module = state->module.Get(isolate); + napi_throw(env, v8impl::JsValueFromV8LocalValue(module->GetException())); + return false; +} + +bool CacheV8ModuleError(napi_env env, ModuleState* state) { + v8::Isolate* isolate = env->isolate; + v8::HandleScope scope(isolate); + v8::Local module = state->module.Get(isolate); + return SetModuleError( + env, state, v8impl::JsValueFromV8LocalValue(module->GetException())); +} + +bool CreateV8SourceTextModule(napi_env env, const std::string& sourceText, + napi_value options, napi_value* result) { + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope scope(isolate); + + ContextState* contextState = nullptr; + napi_value contextObject = nullptr; + if (!ReadModuleContextOption(env, options, &contextState, &contextObject)) { + return false; + } + + v8::Local context = contextState != nullptr + ? contextState->context.Get(isolate) + : env->context(); + v8::Context::Scope contextScope(context); + v8::TryCatch tryCatch(isolate); + + const std::string identifier = ReadIdentifierOption(env, options); + v8::Local sourceValue = + v8::String::NewFromUtf8(isolate, sourceText.c_str(), + v8::NewStringType::kNormal, + static_cast(sourceText.size())) + .ToLocalChecked(); + v8::Local nameValue = + v8::String::NewFromUtf8(isolate, identifier.c_str(), + v8::NewStringType::kNormal, + static_cast(identifier.size())) + .ToLocalChecked(); + +#if V8_MAJOR_VERSION >= 14 + v8::ScriptOrigin origin(nameValue, 0, 0, false, -1, v8::Local(), + false, false, true); +#else + v8::ScriptOrigin origin(isolate, nameValue, 0, 0, false, -1, + v8::Local(), false, false, true); +#endif + + v8::ScriptCompiler::Source source(sourceValue, origin); + v8::Local module; + if (!v8::ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } else { + napi_throw_error(env, nullptr, "Failed to compile vm.SourceTextModule"); + } + return false; + } + + std::unique_ptr state(new ModuleState()); + state->env = env; + state->kind = ModuleKind::kSourceText; + state->identifier = identifier; + state->contextState = contextState; + state->dependencySpecifiers.clear(); + if (!ExtractV8DependencySpecifiers(isolate, module, + state->dependencySpecifiers)) { + napi_throw_error(env, nullptr, "Failed to inspect module requests"); + return false; + } + state->module.Reset(isolate, module); + if (contextObject != nullptr && + napi_create_reference(env, contextObject, 1, &state->contextRef) != + napi_ok) { + return false; + } + + SetV8ModuleRegistryEntry(module, state.get()); + if (!CreateModuleHandle(env, state.get(), result)) { + return false; + } + + state.release(); + return true; +} + +bool CreateV8SyntheticModule(napi_env env, + const std::vector& exportNames, + napi_value options, napi_value* result) { + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope scope(isolate); + + ContextState* contextState = nullptr; + napi_value contextObject = nullptr; + if (!ReadModuleContextOption(env, options, &contextState, &contextObject)) { + return false; + } + + const std::string identifier = ReadIdentifierOption(env, options); + std::vector> exports; + exports.reserve(exportNames.size()); + for (const auto& exportName : exportNames) { + exports.push_back( + v8::String::NewFromUtf8(isolate, exportName.c_str(), + v8::NewStringType::kNormal, + static_cast(exportName.size())) + .ToLocalChecked()); + } + + v8::Local nameValue = + v8::String::NewFromUtf8(isolate, identifier.c_str(), + v8::NewStringType::kNormal, + static_cast(identifier.size())) + .ToLocalChecked(); + v8::Local module = v8::Module::CreateSyntheticModule( + isolate, nameValue, + v8::MemorySpan>(exports.data(), + exports.size()), + &EvaluateV8SyntheticModule); + + std::unique_ptr state(new ModuleState()); + state->env = env; + state->kind = ModuleKind::kSynthetic; + state->identifier = identifier; + state->contextState = contextState; + state->exportNames = exportNames; + state->module.Reset(isolate, module); + if (contextObject != nullptr && + napi_create_reference(env, contextObject, 1, &state->contextRef) != + napi_ok) { + return false; + } + + SetV8ModuleRegistryEntry(module, state.get()); + if (!CreateModuleHandle(env, state.get(), result)) { + return false; + } + + state.release(); + return true; +} + +bool CompileOnlyV8(napi_env env, const std::string& source, + const std::string& filename) { + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope handleScope(isolate); + v8::TryCatch tryCatch(isolate); + + v8::Local context = env->context(); + v8::Context::Scope contextScope(context); + + v8::Local sourceValue = + v8::String::NewFromUtf8(isolate, source.c_str(), + v8::NewStringType::kNormal, + static_cast(source.size())) + .ToLocalChecked(); + v8::Local filenameValue = + v8::String::NewFromUtf8(isolate, filename.c_str(), + v8::NewStringType::kNormal, + static_cast(filename.size())) + .ToLocalChecked(); + +#if V8_MAJOR_VERSION >= 14 + v8::ScriptOrigin origin(filenameValue); +#else + v8::ScriptOrigin origin(isolate, filenameValue); +#endif + + if (v8::Script::Compile(context, sourceValue, &origin).IsEmpty()) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } else { + napi_throw_error(env, nullptr, "Failed to compile vm.Script"); + } + return false; + } + + return true; +} + +bool CreateContextState(napi_env env, ContextState* state) { + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope handleScope(isolate); + + v8::Local context = v8::Context::New(isolate); + v8::Context::Scope contextScope(context); + + state->baselineKeys = + ToKeySet(CollectV8OwnStringKeys(isolate, context, context->Global())); + state->context.Reset(isolate, context); + return true; +} + +std::unordered_set SyncSandboxToContext(napi_env env, + napi_value sandboxValue, + ContextState* state) { + v8::Isolate* isolate = env->isolate; + v8::Local mainContext = env->context(); + v8::Local sandbox = + v8impl::V8LocalValueFromJsValue(sandboxValue).As(); + v8::Local childContext = state->context.Get(isolate); + + v8::Context::Scope childScope(childContext); + v8::Local childGlobal = childContext->Global(); + + auto sandboxKeys = CollectV8OwnStringKeys(isolate, mainContext, sandbox); + auto sandboxKeySet = ToKeySet(sandboxKeys); + + for (const auto& key : sandboxKeys) { + v8::Local keyValue = + v8::String::NewFromUtf8(isolate, key.c_str(), + v8::NewStringType::kNormal, + static_cast(key.size())) + .ToLocalChecked(); + + v8::Local value; + if (!sandbox->Get(mainContext, keyValue).ToLocal(&value)) { + continue; + } + + childGlobal->Set(childContext, keyValue, value).FromMaybe(false); + } + + return sandboxKeySet; +} + +bool SyncContextToSandbox(napi_env env, napi_value sandboxValue, + ContextState* state, + const std::unordered_set& sandboxKeys) { + v8::Isolate* isolate = env->isolate; + v8::Local mainContext = env->context(); + v8::Local sandbox = + v8impl::V8LocalValueFromJsValue(sandboxValue).As(); + v8::Local childContext = state->context.Get(isolate); + + v8::Context::Scope childScope(childContext); + v8::Local childGlobal = childContext->Global(); + auto childKeys = CollectV8OwnStringKeys(isolate, childContext, childGlobal); + auto childKeySet = ToKeySet(childKeys); + + for (const auto& key : sandboxKeys) { + if (childKeySet.count(key) != 0) { + continue; + } + + v8::Local keyValue = + v8::String::NewFromUtf8(isolate, key.c_str(), + v8::NewStringType::kNormal, + static_cast(key.size())) + .ToLocalChecked(); + sandbox->Delete(mainContext, keyValue).FromMaybe(false); + } + + for (const auto& key : childKeys) { + if (sandboxKeys.count(key) == 0 && state->baselineKeys.count(key) != 0) { + continue; + } + + v8::Local keyValue = + v8::String::NewFromUtf8(isolate, key.c_str(), + v8::NewStringType::kNormal, + static_cast(key.size())) + .ToLocalChecked(); + + v8::Local value; + if (!childGlobal->Get(childContext, keyValue).ToLocal(&value)) { + continue; + } + + sandbox->Set(mainContext, keyValue, value).FromMaybe(false); + } + + return true; +} + +napi_value RunInContextImpl(napi_env env, ContextState* state, + napi_value sandboxValue, const std::string& source, + const std::string& filename) { + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope handleScope(isolate); + v8::TryCatch tryCatch(isolate); + + const auto sandboxKeys = SyncSandboxToContext(env, sandboxValue, state); + v8::Local childContext = state->context.Get(isolate); + v8::Context::Scope childScope(childContext); + + v8::Local sourceValue = + v8::String::NewFromUtf8(isolate, source.c_str(), + v8::NewStringType::kNormal, + static_cast(source.size())) + .ToLocalChecked(); + v8::Local filenameValue = + v8::String::NewFromUtf8(isolate, filename.c_str(), + v8::NewStringType::kNormal, + static_cast(filename.size())) + .ToLocalChecked(); + +#if V8_MAJOR_VERSION >= 14 + v8::ScriptOrigin origin(filenameValue); +#else + v8::ScriptOrigin origin(isolate, filenameValue); +#endif + + v8::Local script; + if (!v8::Script::Compile(childContext, sourceValue, &origin) + .ToLocal(&script)) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } else { + napi_throw_error(env, nullptr, "Failed to compile vm context script"); + } + return nullptr; + } + + v8::Local result; + if (!script->Run(childContext).ToLocal(&result)) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } else { + napi_throw_error(env, nullptr, "Failed to run vm context script"); + } + return nullptr; + } + + if (!SyncContextToSandbox(env, sandboxValue, state, sandboxKeys)) { + return nullptr; + } + + return v8impl::JsValueFromV8LocalValue(handleScope.Escape(result)); +} + +#elif defined(TARGET_ENGINE_QUICKJS) + +static inline JSValue QuickJSToValue(napi_value value) { + return *reinterpret_cast(value); +} + +std::string GetQuickJSExceptionMessage(JSContext* context, + JSValueConst exception); +JSValue CloneQuickJSValue(JSContext* sourceContext, + JSContext* destinationContext, JSValueConst value); +bool ThrowLatestQuickJSException(napi_env env, JSContext* context); + +std::unordered_map& +GetQuickJSModuleRegistries() { + static std::unordered_map registries; + return registries; +} + +std::string MakeQuickJSResolutionKey(const std::string& base, + const std::string& specifier) { + return base + "\n" + specifier; +} + +char* NormalizeQuickJSVmModule(JSContext* ctx, const char* base_name, + const char* name, void* opaque) { + QuickJSModuleRegistry* registry = static_cast(opaque); + if (registry == nullptr) { + return js_strdup(ctx, name); + } + + const std::string base = base_name != nullptr ? base_name : ""; + const auto it = + registry->resolutions.find(MakeQuickJSResolutionKey(base, name)); + if (it != registry->resolutions.end()) { + return js_strdup(ctx, it->second.c_str()); + } + + return js_strdup(ctx, name); +} + +JSModuleDef* LoadQuickJSVmModule(JSContext* /*ctx*/, const char* module_name, + void* opaque) { + QuickJSModuleRegistry* registry = static_cast(opaque); + if (registry == nullptr) { + return nullptr; + } + + const auto it = registry->modulesById.find(module_name); + if (it == registry->modulesById.end() || it->second == nullptr) { + return nullptr; + } + + return static_cast(JS_VALUE_GET_PTR(it->second->moduleValue)); +} + +QuickJSModuleRegistry& EnsureQuickJSModuleRegistry(JSRuntime* runtime) { + auto& registries = GetQuickJSModuleRegistries(); + QuickJSModuleRegistry& registry = registries[runtime]; + if (!registry.installed) { + JS_SetModuleLoaderFunc(runtime, &NormalizeQuickJSVmModule, + &LoadQuickJSVmModule, ®istry); + registry.installed = true; + } + return registry; +} + +void CleanupQuickJSModuleState(ModuleState* state) { + auto& registries = GetQuickJSModuleRegistries(); + auto registryIt = registries.find(state->runtime); + if (registryIt != registries.end()) { + auto& registry = registryIt->second; + for (auto it = registry.modulesById.begin(); + it != registry.modulesById.end();) { + if (it->second == state) { + it = registry.modulesById.erase(it); + } else { + ++it; + } + } + + for (auto it = registry.modulesByDef.begin(); + it != registry.modulesByDef.end();) { + if (it->second == state) { + it = registry.modulesByDef.erase(it); + } else { + ++it; + } + } + + for (auto it = registry.resolutions.begin(); + it != registry.resolutions.end();) { + if (it->second == state->identifier) { + it = registry.resolutions.erase(it); + } else { + ++it; + } + } + } + + for (auto& entry : state->syntheticExports) { + JS_FreeValue(state->context, entry.second); + } + state->syntheticExports.clear(); + + if (state->context != nullptr && !JS_IsUndefined(state->moduleValue)) { + JS_FreeValue(state->context, state->moduleValue); + state->moduleValue = JS_UNDEFINED; + } +} + +std::string GetQuickJSModuleStatusString(ModuleState* state) { + if (state->errored) { + return "errored"; + } + if (state->evaluating) { + return "evaluating"; + } + if (state->evaluated) { + return "evaluated"; + } + if (state->linked) { + return "linked"; + } + return "unlinked"; +} + +bool EnsureQuickJSImportMeta(ModuleState* state) { + JSModuleDef* module = + static_cast(JS_VALUE_GET_PTR(state->moduleValue)); + JSValue meta = JS_GetImportMeta(state->context, module); + if (JS_IsException(meta)) { + return false; + } + + if (JS_DefinePropertyValueStr( + state->context, meta, "url", + JS_NewString(state->context, state->identifier.c_str()), + JS_PROP_C_W_E) < 0 || + JS_DefinePropertyValueStr(state->context, meta, "main", JS_FALSE, + JS_PROP_C_W_E) < 0) { + JS_FreeValue(state->context, meta); + return false; + } + + JS_FreeValue(state->context, meta); + return true; +} + +bool EnsureQuickJSLinked(napi_env env, ModuleState* state) { + if (state->linked) { + return true; + } + + if (JS_ResolveModule(state->context, state->moduleValue) < 0) { + state->errored = true; + state->errorMessage = GetQuickJSExceptionMessage( + state->context, JS_GetException(state->context)); + napi_throw_error(env, nullptr, state->errorMessage.c_str()); + return false; + } + + state->linked = true; + return true; +} + +bool CacheQuickJSError(napi_env env, ModuleState* state, JSValue exception) { + JSContext* mainContext = qjs_get_context(env); + JSValue cloned = CloneQuickJSValue(state->context, mainContext, exception); + JS_FreeValue(state->context, exception); + if (JS_IsException(cloned)) { + state->errorMessage = + GetQuickJSExceptionMessage(mainContext, JS_GetException(mainContext)); + napi_throw_error(env, nullptr, state->errorMessage.c_str()); + return false; + } + + napi_value error; + if (qjs_create_scoped_value(env, cloned, &error) != napi_ok || + !SetModuleError(env, state, error)) { + return false; + } + + return true; +} + +bool CreateQuickJSSourceTextModule(napi_env env, const std::string& sourceText, + napi_value options, napi_value* result) { + ContextState* contextState = nullptr; + napi_value contextObject = nullptr; + if (!ReadModuleContextOption(env, options, &contextState, &contextObject)) { + return false; + } + + JSContext* context = + contextState != nullptr ? contextState->context : qjs_get_context(env); + JSRuntime* runtime = JS_GetRuntime(context); + QuickJSModuleRegistry& registry = EnsureQuickJSModuleRegistry(runtime); + const std::string identifier = ReadIdentifierOption(env, options); + + JSValue moduleValue = JS_Eval( + context, sourceText.c_str(), sourceText.size(), identifier.c_str(), + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY | + JS_EVAL_FLAG_COMPILE_ONLY_NO_RESOLVE); + if (JS_IsException(moduleValue)) { + return ThrowLatestQuickJSException(env, context); + } + + std::unique_ptr state(new ModuleState()); + state->env = env; + state->kind = ModuleKind::kSourceText; + state->identifier = identifier; + state->contextState = contextState; + state->runtime = runtime; + state->context = context; + state->moduleValue = moduleValue; + state->dependencySpecifiers = ExtractModuleDependencySpecifiers(sourceText); + if (contextObject != nullptr && + napi_create_reference(env, contextObject, 1, &state->contextRef) != + napi_ok) { + return false; + } + + registry.modulesById[identifier] = state.get(); + registry + .modulesByDef[static_cast(JS_VALUE_GET_PTR(moduleValue))] = + state.get(); + if (!CreateModuleHandle(env, state.get(), result)) { + return false; + } + + state.release(); + return true; +} + +bool ApplyQuickJSSyntheticExports(ModuleState* state) { + JSModuleDef* moduleDef = + static_cast(JS_VALUE_GET_PTR(state->moduleValue)); + for (const auto& exportName : state->exportNames) { + JSValue exportValue = JS_UNDEFINED; + auto valueIt = state->syntheticExports.find(exportName); + if (valueIt != state->syntheticExports.end()) { + exportValue = JS_DupValue(state->context, valueIt->second); + } + + if (JS_SetModuleExport(state->context, moduleDef, exportName.c_str(), + exportValue) < 0) { + return false; + } + } + + return true; +} + +int InitializeQuickJSSyntheticModule(JSContext* ctx, JSModuleDef* moduleDef) { + auto& registries = GetQuickJSModuleRegistries(); + auto registryIt = registries.find(JS_GetRuntime(ctx)); + if (registryIt == registries.end()) { + return -1; + } + + auto stateIt = registryIt->second.modulesByDef.find(moduleDef); + if (stateIt == registryIt->second.modulesByDef.end() || + stateIt->second == nullptr) { + return -1; + } + + return ApplyQuickJSSyntheticExports(stateIt->second) ? 0 : -1; +} + +bool CreateQuickJSSyntheticModule(napi_env env, + const std::vector& exportNames, + napi_value options, napi_value* result) { + ContextState* contextState = nullptr; + napi_value contextObject = nullptr; + if (!ReadModuleContextOption(env, options, &contextState, &contextObject)) { + return false; + } + + JSContext* context = + contextState != nullptr ? contextState->context : qjs_get_context(env); + JSRuntime* runtime = JS_GetRuntime(context); + QuickJSModuleRegistry& registry = EnsureQuickJSModuleRegistry(runtime); + const std::string identifier = ReadIdentifierOption(env, options); + + JSModuleDef* moduleDef = JS_NewCModule(context, identifier.c_str(), + &InitializeQuickJSSyntheticModule); + if (moduleDef == nullptr) { + return ThrowLatestQuickJSException(env, context); + } + + for (const auto& exportName : exportNames) { + if (JS_AddModuleExport(context, moduleDef, exportName.c_str()) < 0) { + return ThrowLatestQuickJSException(env, context); + } + } + + std::unique_ptr state(new ModuleState()); + state->env = env; + state->kind = ModuleKind::kSynthetic; + state->identifier = identifier; + state->contextState = contextState; + state->runtime = runtime; + state->context = context; + state->exportNames = exportNames; + state->moduleValue = JS_DupValue(context, JS_MKPTR(JS_TAG_MODULE, moduleDef)); + if (contextObject != nullptr && + napi_create_reference(env, contextObject, 1, &state->contextRef) != + napi_ok) { + return false; + } + + registry.modulesById[identifier] = state.get(); + registry.modulesByDef[moduleDef] = state.get(); + if (!EnsureQuickJSLinked(env, state.get())) { + return false; + } + + if (!CreateModuleHandle(env, state.get(), result)) { + return false; + } + + state.release(); + return true; +} + +std::vector CollectQuickJSOwnStringKeys(JSContext* context, + JSValueConst object) { + std::vector result; + JSPropertyEnum* properties = nullptr; + uint32_t count = 0; + + if (JS_GetOwnPropertyNames(context, &properties, &count, object, + JS_GPN_STRING_MASK) < 0) { + return result; + } + + result.reserve(count); + for (uint32_t index = 0; index < count; ++index) { + const char* key = JS_AtomToCString(context, properties[index].atom); + if (key != nullptr) { + result.emplace_back(key); + JS_FreeCString(context, key); + } + + JS_FreeAtom(context, properties[index].atom); + } + + js_free(context, properties); + return result; +} + +std::unordered_set ToKeySet(const std::vector& keys) { + return std::unordered_set(keys.begin(), keys.end()); +} + +std::string GetQuickJSExceptionMessage(JSContext* context, + JSValueConst exception) { + std::string message; + + JSValue stack = JS_GetPropertyStr(context, exception, "stack"); + if (!JS_IsException(stack) && !JS_IsUndefined(stack) && !JS_IsNull(stack)) { + const char* stackChars = JS_ToCString(context, stack); + if (stackChars != nullptr) { + message.assign(stackChars); + JS_FreeCString(context, stackChars); + } + } + JS_FreeValue(context, stack); + + if (!message.empty()) { + return message; + } + + JSValue detail = JS_GetPropertyStr(context, exception, "message"); + if (!JS_IsException(detail) && !JS_IsUndefined(detail) && + !JS_IsNull(detail)) { + const char* detailChars = JS_ToCString(context, detail); + if (detailChars != nullptr) { + message.assign(detailChars); + JS_FreeCString(context, detailChars); + } + } + JS_FreeValue(context, detail); + + if (!message.empty()) { + return message; + } + + const char* chars = JS_ToCString(context, exception); + if (chars != nullptr) { + message.assign(chars); + JS_FreeCString(context, chars); + } + + if (message.empty()) { + message = "QuickJS vm execution failed"; + } + + return message; +} + +bool ThrowQuickJSException(napi_env env, JSContext* context, + JSValue exception) { + std::string message = GetQuickJSExceptionMessage(context, exception); + JS_FreeValue(context, exception); + napi_throw_error(env, nullptr, message.c_str()); + return false; +} + +bool ThrowLatestQuickJSException(napi_env env, JSContext* context) { + return ThrowQuickJSException(env, context, JS_GetException(context)); +} + +JSValue CloneQuickJSValue(JSContext* sourceContext, + JSContext* destinationContext, JSValueConst value) { + if (JS_IsUndefined(value)) { + return JS_UNDEFINED; + } + + if (JS_IsNull(value)) { + return JS_NULL; + } + + if (JS_IsBool(value)) { + int boolValue = JS_ToBool(sourceContext, value); + if (boolValue < 0) { + return JS_EXCEPTION; + } + return JS_NewBool(destinationContext, boolValue); + } + + if (JS_IsString(value)) { + size_t length = 0; + const char* chars = JS_ToCStringLen(sourceContext, &length, value); + if (chars == nullptr) { + return JS_EXCEPTION; + } + + JSValue result = JS_NewStringLen(destinationContext, chars, length); + JS_FreeCString(sourceContext, chars); + return result; + } + + if (JS_IsBigInt(sourceContext, value)) { + int64_t bigintValue = 0; + if (JS_ToBigInt64(sourceContext, &bigintValue, value) < 0) { + JS_ThrowTypeError(destinationContext, + "Only signed 64-bit BigInt values are supported in " + "vm contexts"); + return JS_EXCEPTION; + } + return JS_NewBigInt64(destinationContext, bigintValue); + } + + if (JS_IsNumber(value)) { + double numberValue = 0; + if (JS_ToFloat64(sourceContext, &numberValue, value) < 0) { + return JS_EXCEPTION; + } + + if (std::isfinite(numberValue) && std::floor(numberValue) == numberValue && + numberValue >= static_cast(INT32_MIN) && + numberValue <= static_cast(INT32_MAX)) { + return JS_NewInt32(destinationContext, static_cast(numberValue)); + } + + return JS_NewFloat64(destinationContext, numberValue); + } + + if (JS_IsObject(value)) { + if (JS_IsFunction(sourceContext, value)) { + JS_ThrowTypeError(destinationContext, + "Functions are not supported in vm sandbox cloning"); + return JS_EXCEPTION; + } + + JSValue json = JS_JSONStringify(sourceContext, const_cast(value), + JS_UNDEFINED, JS_UNDEFINED); + if (JS_IsException(json)) { + JS_ThrowTypeError(destinationContext, + "Unsupported object value in vm sandbox"); + return JS_EXCEPTION; + } + + size_t jsonLength = 0; + const char* jsonChars = JS_ToCStringLen(sourceContext, &jsonLength, json); + JS_FreeValue(sourceContext, json); + if (jsonChars == nullptr) { + return JS_EXCEPTION; + } + + JSValue result = + JS_ParseJSON(destinationContext, jsonChars, jsonLength, ""); + JS_FreeCString(sourceContext, jsonChars); + return result; + } + + JS_ThrowTypeError(destinationContext, + "Unsupported value type in vm sandbox cloning"); + return JS_EXCEPTION; +} + +bool CompileOnlyQuickJS(napi_env env, const std::string& source, + const std::string& filename) { + JSContext* context = qjs_get_context(env); + JSValue compiled = + JS_Eval(context, source.c_str(), source.size(), filename.c_str(), + JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_COMPILE_ONLY); + if (JS_IsException(compiled)) { + return ThrowLatestQuickJSException(env, context); + } + + JS_FreeValue(context, compiled); + return true; +} + +bool CreateContextState(napi_env env, ContextState* state) { + state->runtime = qjs_get_runtime(env); + state->context = JS_NewContext(state->runtime); + if (state->context == nullptr) { + napi_throw_error(env, nullptr, "Failed to create QuickJS vm context"); + return false; + } + + JSValue global = JS_GetGlobalObject(state->context); + state->baselineKeys = + ToKeySet(CollectQuickJSOwnStringKeys(state->context, global)); + JS_FreeValue(state->context, global); + return true; +} + +bool SyncSandboxToContext(napi_env env, napi_value sandboxValue, + ContextState* state, + std::unordered_set& sandboxKeys) { + JSContext* mainContext = qjs_get_context(env); + JSContext* childContext = state->context; + JSValue childGlobal = JS_GetGlobalObject(childContext); + + napi_value keyArray; + if (napi_get_all_property_names( + env, sandboxValue, napi_key_own_only, + static_cast(napi_key_all_properties | + napi_key_skip_symbols), + napi_key_numbers_to_strings, &keyArray) != napi_ok) { + JS_FreeValue(childContext, childGlobal); + return false; + } + + uint32_t keyCount = 0; + napi_get_array_length(env, keyArray, &keyCount); + + for (uint32_t index = 0; index < keyCount; ++index) { + napi_value keyValue; + if (napi_get_element(env, keyArray, index, &keyValue) != napi_ok) { + continue; + } + + std::string key; + if (!CoerceToString(env, keyValue, key)) { + continue; + } + + sandboxKeys.insert(key); + + napi_value propertyValue; + if (napi_get_named_property(env, sandboxValue, key.c_str(), + &propertyValue) != napi_ok) { + continue; + } + + JSValue cloned = CloneQuickJSValue(mainContext, childContext, + QuickJSToValue(propertyValue)); + if (JS_IsException(cloned)) { + JS_FreeValue(childContext, childGlobal); + return ThrowLatestQuickJSException(env, childContext); + } + + if (JS_SetPropertyStr(childContext, childGlobal, key.c_str(), cloned) < 0) { + JS_FreeValue(childContext, childGlobal); + return ThrowLatestQuickJSException(env, childContext); + } + } + + JS_FreeValue(childContext, childGlobal); + return true; +} + +bool SyncContextToSandbox(napi_env env, napi_value sandboxValue, + ContextState* state, + const std::unordered_set& sandboxKeys) { + JSContext* mainContext = qjs_get_context(env); + JSContext* childContext = state->context; + JSValue childGlobal = JS_GetGlobalObject(childContext); + auto childKeys = CollectQuickJSOwnStringKeys(childContext, childGlobal); + auto childKeySet = ToKeySet(childKeys); + + for (const auto& key : sandboxKeys) { + if (childKeySet.count(key) != 0) { + continue; + } + + napi_value keyValue; + if (napi_create_string_utf8(env, key.c_str(), key.size(), &keyValue) != + napi_ok) { + JS_FreeValue(childContext, childGlobal); + return false; + } + + bool deleted = false; + if (napi_delete_property(env, sandboxValue, keyValue, &deleted) != + napi_ok) { + JS_FreeValue(childContext, childGlobal); + return false; + } + } + + for (const auto& key : childKeys) { + if (sandboxKeys.count(key) == 0 && state->baselineKeys.count(key) != 0) { + continue; + } + + JSValue childValue = + JS_GetPropertyStr(childContext, childGlobal, key.c_str()); + if (JS_IsException(childValue)) { + JS_FreeValue(childContext, childGlobal); + return ThrowLatestQuickJSException(env, childContext); + } + + JSValue cloned = CloneQuickJSValue(childContext, mainContext, childValue); + JS_FreeValue(childContext, childValue); + if (JS_IsException(cloned)) { + JS_FreeValue(childContext, childGlobal); + return ThrowLatestQuickJSException(env, mainContext); + } + + napi_value resultValue; + if (qjs_create_scoped_value(env, cloned, &resultValue) != napi_ok) { + JS_FreeValue(childContext, childGlobal); + return false; + } + + if (napi_set_named_property(env, sandboxValue, key.c_str(), resultValue) != + napi_ok) { + JS_FreeValue(childContext, childGlobal); + return false; + } + } + + JS_FreeValue(childContext, childGlobal); + return true; +} + +napi_value RunInContextImpl(napi_env env, ContextState* state, + napi_value sandboxValue, const std::string& source, + const std::string& filename) { + std::unordered_set sandboxKeys; + if (!SyncSandboxToContext(env, sandboxValue, state, sandboxKeys)) { + return nullptr; + } + + JSContext* childContext = state->context; + JSValue result = JS_Eval(childContext, source.c_str(), source.size(), + filename.c_str(), JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(result)) { + ThrowLatestQuickJSException(env, childContext); + return nullptr; + } + + if (!SyncContextToSandbox(env, sandboxValue, state, sandboxKeys)) { + JS_FreeValue(childContext, result); + return nullptr; + } + + JSContext* mainContext = qjs_get_context(env); + JSValue clonedResult = CloneQuickJSValue(childContext, mainContext, result); + JS_FreeValue(childContext, result); + if (JS_IsException(clonedResult)) { + ThrowLatestQuickJSException(env, mainContext); + return nullptr; + } + + napi_value resultValue; + if (qjs_create_scoped_value(env, clonedResult, &resultValue) != napi_ok) { + return nullptr; + } + + return resultValue; +} + +#endif + +bool CreateAndAttachContextState(napi_env env, napi_value sandbox, + ContextState** out) { + ContextState* existing = nullptr; + if (GetContextState(env, sandbox, &existing)) { + *out = existing; + return true; + } + + std::unique_ptr state(new ContextState()); + if (!CreateContextState(env, state.get())) { + return false; + } + + napi_value stateValue; + if (napi_create_external(env, state.get(), FinalizeContextState, nullptr, + &stateValue) != napi_ok) { + return false; + } + + napi_value symbol = GetContextSymbol(env); + if (napi_set_property(env, sandbox, symbol, stateValue) != napi_ok) { + return false; + } + + *out = state.release(); + return true; +} + +bool ValidateSourceArgument(napi_env env, napi_value value, + std::string& source) { + if (IsNullOrUndefined(env, value)) { + napi_throw_type_error(env, nullptr, + "The \"code\" argument must be coercible to string"); + return false; + } + + if (!CoerceToString(env, value, source)) { + napi_throw_type_error(env, nullptr, + "The \"code\" argument must be coercible to string"); + return false; + } + + return true; +} + +bool ResolveSandboxArgument(napi_env env, napi_value value, + napi_value* sandbox) { + if (IsNullOrUndefined(env, value)) { + return napi_create_object(env, sandbox) == napi_ok; + } + + if (!IsObjectLike(env, value)) { + napi_throw_type_error(env, nullptr, + "The \"sandbox\" argument must be an object"); + return false; + } + + *sandbox = value; + return true; +} + +napi_value RunSourceInThisContext(napi_env env, const std::string& source, + const std::string& filename) { + napi_value script; + napi_value result; + if (napi_create_string_utf8(env, source.c_str(), source.size(), &script) != + napi_ok || + napi_run_script_source(env, script, filename.c_str(), &result) != + napi_ok) { + return nullptr; + } + + return result; +} + +napi_value CreateContextCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + napi_value sandbox; + if (argc < 1 || IsNullOrUndefined(env, argv[0])) { + napi_create_object(env, &sandbox); + } else if (!IsObjectLike(env, argv[0])) { + napi_throw_type_error(env, nullptr, + "The \"contextObject\" argument must be an object"); + return nullptr; + } else { + sandbox = argv[0]; + } + + ContextState* state = nullptr; + if (!CreateAndAttachContextState(env, sandbox, &state)) { + return nullptr; + } + + return sandbox; +} + +napi_value IsContextCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + + bool isContext = false; + if (argc >= 1) { + ContextState* state = nullptr; + isContext = GetContextState(env, argv[0], &state) && state != nullptr; + } + + napi_value result; + napi_get_boolean(env, isContext, &result); + return result; +} + +napi_value RunInContextCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(3) + + if (argc < 2) { + napi_throw_type_error(env, nullptr, + "vm.runInContext(code, contextifiedObject) requires " + "a contextified object"); + return nullptr; + } + + std::string source; + if (!ValidateSourceArgument(env, argv[0], source)) { + return nullptr; + } + + ContextState* state = nullptr; + if (!RequireContextState(env, argv[1], &state)) { + return nullptr; + } + + const std::string filename = + ReadFilenameOption(env, argc >= 3 ? argv[2] : nullptr); + return RunInContextImpl(env, state, argv[1], source, filename); +} + +napi_value RunInNewContextCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(3) + + if (argc < 1) { + napi_throw_type_error(env, nullptr, + "vm.runInNewContext(code[, sandbox]) requires code"); + return nullptr; + } + + std::string source; + if (!ValidateSourceArgument(env, argv[0], source)) { + return nullptr; + } + + napi_value sandbox = nullptr; + napi_value options = nullptr; + + if (argc >= 2 && IsObjectLike(env, argv[1])) { + sandbox = argv[1]; + options = argc >= 3 ? argv[2] : nullptr; + } else { + options = argc >= 2 ? argv[1] : nullptr; + } + + if (!ResolveSandboxArgument(env, sandbox, &sandbox)) { + return nullptr; + } + + ContextState* state = nullptr; + if (!CreateAndAttachContextState(env, sandbox, &state)) { + return nullptr; + } + + const std::string filename = ReadFilenameOption(env, options); + return RunInContextImpl(env, state, sandbox, source, filename); +} + +napi_value RunInThisContextCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + if (argc < 1) { + napi_throw_type_error(env, nullptr, + "vm.runInThisContext(code[, options]) requires code"); + return nullptr; + } + + std::string source; + if (!ValidateSourceArgument(env, argv[0], source)) { + return nullptr; + } + + const std::string filename = + ReadFilenameOption(env, argc >= 2 ? argv[1] : nullptr); + return RunSourceInThisContext(env, source, filename); +} + +ScriptState* GetScriptState(napi_env env, napi_callback_info info, + napi_value* thisValue = nullptr) { + napi_value jsThis; + if (napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr) != + napi_ok) { + return nullptr; + } + + if (thisValue != nullptr) { + *thisValue = jsThis; + } + + ScriptState* script = nullptr; + if (napi_unwrap(env, jsThis, reinterpret_cast(&script)) != napi_ok || + script == nullptr) { + napi_throw_type_error(env, nullptr, "Invalid vm.Script receiver"); + return nullptr; + } + + return script; +} + +napi_value ScriptConstructor(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + if (argc < 1) { + napi_throw_type_error(env, nullptr, + "new vm.Script(code[, options]) requires code"); + return nullptr; + } + + std::string source; + if (!ValidateSourceArgument(env, argv[0], source)) { + return nullptr; + } + + const std::string filename = + ReadFilenameOption(env, argc >= 2 ? argv[1] : nullptr); + +#if defined(TARGET_ENGINE_V8) + if (!CompileOnlyV8(env, source, filename)) { + return nullptr; + } +#elif defined(TARGET_ENGINE_QUICKJS) + if (!CompileOnlyQuickJS(env, source, filename)) { + return nullptr; + } +#endif + + ScriptState* script = new ScriptState{source, filename}; + if (napi_wrap(env, jsThis, script, FinalizeScriptState, nullptr, nullptr) != + napi_ok) { + delete script; + return nullptr; + } + + return jsThis; +} + +napi_value ScriptRunInContext(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + ScriptState* script = GetScriptState(env, info); + if (script == nullptr) { + return nullptr; + } + + if (argc < 1) { + napi_throw_type_error(env, nullptr, + "script.runInContext(contextifiedObject[, options]) " + "requires a context"); + return nullptr; + } + + ContextState* state = nullptr; + if (!RequireContextState(env, argv[0], &state)) { + return nullptr; + } + + const std::string filename = + ReadFilenameOption(env, argc >= 2 ? argv[1] : nullptr, script->filename); + return RunInContextImpl(env, state, argv[0], script->source, filename); +} + +napi_value ScriptRunInNewContext(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + ScriptState* script = GetScriptState(env, info); + if (script == nullptr) { + return nullptr; + } + + napi_value sandbox = nullptr; + napi_value options = nullptr; + + if (argc >= 1 && IsObjectLike(env, argv[0])) { + sandbox = argv[0]; + options = argc >= 2 ? argv[1] : nullptr; + } else { + options = argc >= 1 ? argv[0] : nullptr; + } + + if (!ResolveSandboxArgument(env, sandbox, &sandbox)) { + return nullptr; + } + + ContextState* state = nullptr; + if (!CreateAndAttachContextState(env, sandbox, &state)) { + return nullptr; + } + + const std::string filename = + ReadFilenameOption(env, options, script->filename); + return RunInContextImpl(env, state, sandbox, script->source, filename); +} + +napi_value ScriptRunInThisContext(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + + ScriptState* script = GetScriptState(env, info); + if (script == nullptr) { + return nullptr; + } + + const std::string filename = + ReadFilenameOption(env, argc >= 1 ? argv[0] : nullptr, script->filename); + return RunSourceInThisContext(env, script->source, filename); +} + +napi_value CreateSourceTextModuleCallback(napi_env env, + napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + if (argc < 1) { + napi_throw_type_error(env, nullptr, + "vm.SourceTextModule requires source text"); + return nullptr; + } + + std::string sourceText; + if (!ValidateSourceArgument(env, argv[0], sourceText)) { + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + napi_value result; + if (!CreateV8SourceTextModule(env, sourceText, argc >= 2 ? argv[1] : nullptr, + &result)) { + return nullptr; + } + return result; +#elif defined(TARGET_ENGINE_QUICKJS) + napi_value result; + if (!CreateQuickJSSourceTextModule(env, sourceText, + argc >= 2 ? argv[1] : nullptr, &result)) { + return nullptr; + } + return result; +#endif +} + +napi_value CreateSyntheticModuleCallback(napi_env env, + napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + if (argc < 1) { + napi_throw_type_error(env, nullptr, + "vm.SyntheticModule requires export names"); + return nullptr; + } + + std::vector exportNames; + if (!GetStringArray(env, argv[0], exportNames)) { + napi_throw_type_error(env, nullptr, + "The \"exportNames\" argument must be an Array"); + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + napi_value result; + if (!CreateV8SyntheticModule(env, exportNames, argc >= 2 ? argv[1] : nullptr, + &result)) { + return nullptr; + } + return result; +#elif defined(TARGET_ENGINE_QUICKJS) + napi_value result; + if (!CreateQuickJSSyntheticModule(env, exportNames, + argc >= 2 ? argv[1] : nullptr, &result)) { + return nullptr; + } + return result; +#endif +} + +napi_value ModuleGetIdentifierCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + + napi_value result; + napi_create_string_utf8(env, state->identifier.c_str(), + state->identifier.size(), &result); + return result; +} + +napi_value ModuleGetContextCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + return GetStoredModuleContext(env, state); +} + +napi_value ModuleGetDependencySpecifiersCallback(napi_env env, + napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + return CreateStringArray(env, state->dependencySpecifiers); +} + +napi_value ModuleGetStatusCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + + std::string statusText; +#if defined(TARGET_ENGINE_V8) + statusText = + GetV8ModuleStatusString(state->module.Get(env->isolate)->GetStatus()); +#elif defined(TARGET_ENGINE_QUICKJS) + statusText = GetQuickJSModuleStatusString(state); +#endif + + napi_value result; + napi_create_string_utf8(env, statusText.c_str(), statusText.size(), &result); + return result; +} + +napi_value ModuleGetErrorCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + if (state->module.Get(env->isolate)->GetStatus() == v8::Module::kErrored) { + if (!CacheV8ModuleError(env, state)) { + return nullptr; + } + } +#endif + return GetStoredModuleError(env, state); +} + +napi_value ModuleLinkRequestsCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + + bool isArray = false; + if (argc < 2 || napi_is_array(env, argv[1], &isArray) != napi_ok || + !isArray) { + napi_throw_type_error(env, nullptr, + "The \"linkedModules\" argument must be an Array"); + return nullptr; + } + + uint32_t length = 0; + napi_get_array_length(env, argv[1], &length); + state->linkedModules.assign(length, nullptr); + for (uint32_t index = 0; index < length; ++index) { + napi_value entry; + if (napi_get_element(env, argv[1], index, &entry) != napi_ok || + !GetModuleState(env, entry, &state->linkedModules[index])) { + return nullptr; + } + } + +#if defined(TARGET_ENGINE_V8) + v8::Isolate* isolate = env->isolate; + v8::HandleScope scope(isolate); + v8::TryCatch tryCatch(isolate); + v8::Local context = GetV8ModuleContext(env, state); + v8::Context::Scope contextScope(context); + v8::Local module = state->module.Get(isolate); + if (!module->InstantiateModule(context, &ResolveV8VmModuleByIndex) + .FromMaybe(false)) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } else if (module->GetStatus() == v8::Module::kErrored) { + CacheV8ModuleError(env, state); + ThrowV8ModuleException(env, state); + } else { + napi_throw_error(env, nullptr, "Failed to link vm.Module"); + } + return nullptr; + } +#elif defined(TARGET_ENGINE_QUICKJS) + QuickJSModuleRegistry& registry = EnsureQuickJSModuleRegistry(state->runtime); + for (const auto& specifier : state->dependencySpecifiers) { + registry.resolutions.erase( + MakeQuickJSResolutionKey(state->identifier, specifier)); + } + for (size_t index = 0; index < state->linkedModules.size() && + index < state->dependencySpecifiers.size(); + ++index) { + ModuleState* linked = state->linkedModules[index]; + if (linked == nullptr) { + continue; + } + registry.resolutions[MakeQuickJSResolutionKey( + state->identifier, state->dependencySpecifiers[index])] = + linked->identifier; + } + + if (!EnsureQuickJSLinked(env, state)) { + return nullptr; + } +#endif + + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; +} + +napi_value ModuleInstantiateCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + v8::Isolate* isolate = env->isolate; + v8::HandleScope scope(isolate); + v8::TryCatch tryCatch(isolate); + v8::Local context = GetV8ModuleContext(env, state); + v8::Context::Scope contextScope(context); + v8::Local module = state->module.Get(isolate); + if (module->GetStatus() == v8::Module::kUninstantiated && + !module->InstantiateModule(context, &ResolveV8VmModuleByIndex) + .FromMaybe(false)) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } else { + napi_throw_error(env, nullptr, "Failed to instantiate vm.Module"); + } + return nullptr; + } +#elif defined(TARGET_ENGINE_QUICKJS) + if (!EnsureQuickJSLinked(env, state)) { + return nullptr; + } +#endif + + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; +} + +napi_value ModuleEvaluateCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope scope(isolate); + v8::TryCatch tryCatch(isolate); + v8::Local context = GetV8ModuleContext(env, state); + v8::Context::Scope contextScope(context); + v8::Local module = state->module.Get(isolate); + + v8::MaybeLocal maybeResult = module->Evaluate(context); + if (maybeResult.IsEmpty()) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } else if (module->GetStatus() == v8::Module::kErrored) { + CacheV8ModuleError(env, state); + ThrowV8ModuleException(env, state); + } else { + napi_throw_error(env, nullptr, "Failed to evaluate vm.Module"); + } + return nullptr; + } + + return v8impl::JsValueFromV8LocalValue( + scope.Escape(maybeResult.ToLocalChecked())); +#elif defined(TARGET_ENGINE_QUICKJS) + if (!EnsureQuickJSLinked(env, state) || !EnsureQuickJSImportMeta(state)) { + return nullptr; + } + + state->evaluating = true; + JSValue result = JS_EvalFunction( + state->context, JS_DupValue(state->context, state->moduleValue)); + if (JS_IsException(result)) { + state->evaluating = false; + state->errored = true; + JSValue exception = JS_GetException(state->context); + state->errorMessage = GetQuickJSExceptionMessage(state->context, exception); + if (!CacheQuickJSError(env, state, exception)) { + return nullptr; + } + napi_value error = GetStoredModuleError(env, state); + napi_value promise; + CreatePromiseSettledWithUndefined(env, true, error, &promise); + return promise; + } + + if (JS_IsObject(result) && + JS_PromiseState(state->context, result) == JS_PROMISE_PENDING) { + while (JS_PromiseState(state->context, result) == JS_PROMISE_PENDING) { + if (qjs_execute_pending_jobs(env) != napi_ok) { + JS_FreeValue(state->context, result); + return nullptr; + } + } + } + + bool rejected = + JS_IsObject(result) && + JS_PromiseState(state->context, result) == JS_PROMISE_REJECTED; + if (rejected) { + state->evaluating = false; + state->errored = true; + JSValue rejection = JS_PromiseResult(state->context, result); + JS_FreeValue(state->context, result); + if (!CacheQuickJSError(env, state, rejection)) { + return nullptr; + } + napi_value error = GetStoredModuleError(env, state); + napi_value promise; + CreatePromiseSettledWithUndefined(env, true, error, &promise); + return promise; + } + + if (state->kind == ModuleKind::kSynthetic && + !ApplyQuickJSSyntheticExports(state)) { + JS_FreeValue(state->context, result); + return ThrowLatestQuickJSException(env, state->context), nullptr; + } + + state->evaluating = false; + state->evaluated = true; + SetModuleError(env, state, nullptr); + JS_FreeValue(state->context, result); + napi_value promise; + CreatePromiseSettledWithUndefined(env, false, nullptr, &promise); + return promise; +#endif +} + +napi_value ModuleGetExportNamesCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + + std::vector exportNames = state->exportNames; +#if defined(TARGET_ENGINE_V8) + if (exportNames.empty()) { + exportNames = CollectV8ModuleExportNames(env, state); + } +#elif defined(TARGET_ENGINE_QUICKJS) + if (exportNames.empty() && state->evaluated) { + JSModuleDef* module = + static_cast(JS_VALUE_GET_PTR(state->moduleValue)); + JSValue ns = JS_GetModuleNamespace(state->context, module); + if (!JS_IsException(ns)) { + exportNames = CollectQuickJSOwnStringKeys(state->context, ns); + JS_FreeValue(state->context, ns); + } + } +#endif + return CreateStringArray(env, exportNames); +} + +napi_value ModuleGetNamespaceValueCallback(napi_env env, + napi_callback_info info) { + NAPI_CALLBACK_BEGIN(2) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + + std::string exportName; + if (argc < 2 || !CoerceToString(env, argv[1], exportName)) { + napi_throw_type_error(env, nullptr, "Missing export name"); + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + v8::Isolate* isolate = env->isolate; + v8::EscapableHandleScope scope(isolate); + v8::TryCatch tryCatch(isolate); + v8::Local context = GetV8ModuleContext(env, state); + v8::Context::Scope contextScope(context); + v8::Local ns = + state->module.Get(isolate)->GetModuleNamespace().As(); + v8::Local key = + v8::String::NewFromUtf8(isolate, exportName.c_str(), + v8::NewStringType::kNormal, + static_cast(exportName.size())) + .ToLocalChecked(); + v8::Local value; + if (!ns->Get(context, key).ToLocal(&value)) { + if (tryCatch.HasCaught()) { + napi_throw(env, v8impl::JsValueFromV8LocalValue(tryCatch.Exception())); + } + return nullptr; + } + return v8impl::JsValueFromV8LocalValue(scope.Escape(value)); +#elif defined(TARGET_ENGINE_QUICKJS) + JSModuleDef* module = + static_cast(JS_VALUE_GET_PTR(state->moduleValue)); + JSValue ns = JS_GetModuleNamespace(state->context, module); + if (JS_IsException(ns)) { + return nullptr; + } + + JSValue value = JS_GetPropertyStr(state->context, ns, exportName.c_str()); + JS_FreeValue(state->context, ns); + if (JS_IsException(value)) { + return ThrowLatestQuickJSException(env, state->context), nullptr; + } + + JSValue cloned = + CloneQuickJSValue(state->context, qjs_get_context(env), value); + JS_FreeValue(state->context, value); + if (JS_IsException(cloned)) { + return ThrowLatestQuickJSException(env, qjs_get_context(env)), nullptr; + } + + napi_value result; + if (qjs_create_scoped_value(env, cloned, &result) != napi_ok) { + return nullptr; + } + return result; +#endif +} + +napi_value ModuleCreateCachedDataCallback(napi_env env, + napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + v8::Isolate* isolate = env->isolate; + v8::HandleScope scope(isolate); + v8::Local module = state->module.Get(isolate); + v8::ScriptCompiler::CachedData* cachedData = + v8::ScriptCompiler::CreateCodeCache(module->GetUnboundModuleScript()); + if (cachedData == nullptr) { + return CreateUint8ArrayCopy(env, nullptr, 0); + } + napi_value result = CreateUint8ArrayCopy( + env, reinterpret_cast(cachedData->data), + cachedData->length); + delete cachedData; + return result; +#elif defined(TARGET_ENGINE_QUICKJS) + size_t length = 0; + uint8_t* bytecodeData = JS_WriteObject( + state->context, &length, state->moduleValue, JS_WRITE_OBJ_BYTECODE); + if (bytecodeData == nullptr) { + return ThrowLatestQuickJSException(env, state->context), nullptr; + } + napi_value result = CreateUint8ArrayCopy(env, bytecodeData, length); + js_free(state->context, bytecodeData); + return result; +#endif +} + +napi_value ModuleHasTopLevelAwaitCallback(napi_env env, + napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + bool resultBool = false; +#if defined(TARGET_ENGINE_V8) + resultBool = state->module.Get(env->isolate)->HasTopLevelAwait(); +#elif defined(TARGET_ENGINE_QUICKJS) + resultBool = false; +#endif + napi_value result; + napi_get_boolean(env, resultBool, &result); + return result; +} + +napi_value ModuleHasAsyncGraphCallback(napi_env env, napi_callback_info info) { + NAPI_CALLBACK_BEGIN(1) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + bool resultBool = false; +#if defined(TARGET_ENGINE_V8) + if (state->module.Get(env->isolate)->GetStatus() >= + v8::Module::kInstantiated) { + resultBool = state->module.Get(env->isolate)->IsGraphAsync(); + } +#elif defined(TARGET_ENGINE_QUICKJS) + resultBool = false; +#endif + napi_value result; + napi_get_boolean(env, resultBool, &result); + return result; +} + +napi_value ModuleSetSyntheticExportCallback(napi_env env, + napi_callback_info info) { + NAPI_CALLBACK_BEGIN(3) + ModuleState* state = nullptr; + if (!GetModuleState(env, argv[0], &state)) { + return nullptr; + } + + std::string exportName; + if (argc < 3 || !CoerceToString(env, argv[1], exportName)) { + napi_throw_type_error(env, nullptr, "Synthetic export name is required"); + return nullptr; + } + +#if defined(TARGET_ENGINE_V8) + v8::Isolate* isolate = env->isolate; + v8::HandleScope scope(isolate); + v8::Local value = v8impl::V8LocalValueFromJsValue(argv[2]); + v8::Local key = + v8::String::NewFromUtf8(isolate, exportName.c_str(), + v8::NewStringType::kNormal, + static_cast(exportName.size())) + .ToLocalChecked(); + if (!state->module.Get(isolate) + ->SetSyntheticModuleExport(isolate, key, value) + .FromMaybe(false)) { + napi_throw_error(env, nullptr, "Failed to set synthetic export"); + return nullptr; + } +#elif defined(TARGET_ENGINE_QUICKJS) + JSValue cloned = CloneQuickJSValue(qjs_get_context(env), state->context, + QuickJSToValue(argv[2])); + if (JS_IsException(cloned)) { + return ThrowLatestQuickJSException(env, state->context), nullptr; + } + + auto existing = state->syntheticExports.find(exportName); + if (existing != state->syntheticExports.end()) { + JS_FreeValue(state->context, existing->second); + existing->second = cloned; + } else { + state->syntheticExports.emplace(exportName, cloned); + } + + if (state->evaluated && + JS_SetModuleExport( + state->context, + static_cast(JS_VALUE_GET_PTR(state->moduleValue)), + exportName.c_str(), + JS_DupValue(state->context, state->syntheticExports[exportName])) < + 0) { + return ThrowLatestQuickJSException(env, state->context), nullptr; + } +#endif + + napi_value undefined; + napi_get_undefined(env, &undefined); + return undefined; +} + +napi_value CreatePublicVmExports(napi_env env, napi_value binding) { + static const char* kWrapperSource = R"JS( +(function(binding) { + const NativeScriptCtor = binding.Script; + const scriptState = new WeakMap(); + const nativeScript = new WeakMap(); + const moduleState = new WeakMap(); + const kDontContextify = Object.freeze({ __vmDontContextify: true }); + const kUseMainContextDefaultLoader = Symbol('vm.useMainContextDefaultLoader'); + const constants = Object.freeze({ + DONT_CONTEXTIFY: kDontContextify, + USE_MAIN_CONTEXT_DEFAULT_LOADER: kUseMainContextDefaultLoader, + }); + + let nextId = 1; + + function nextToken(prefix) { + return `__ns_vm_${prefix}_${nextId++}`; + } + + function isObjectLike(value) { + return value !== null && (typeof value === 'object' || typeof value === 'function'); + } + + function normalizeFilename(options, fallback = 'vm.js') { + if (typeof options === 'string' && options) { + return options; + } + + if (!options || typeof options !== 'object') { + return fallback; + } + + if (typeof options.filename === 'string' && options.filename) { + return options.filename; + } + + return fallback; + } + + function extractSourceMapURL(source) { + const match = String(source).match(/[#@]\s*sourceMappingURL\s*=\s*(\S+)\s*$/m); + return match ? match[1] : undefined; + } + + function encodeSource(source) { + const text = String(source); + const bytes = new Uint8Array(text.length); + for (let i = 0; i < text.length; i += 1) { + bytes[i] = text.charCodeAt(i) & 0xff; + } + return bytes; + } + + function ensureContextObject(value) { + if (value === kDontContextify || value == null) { + return binding.createContext({}); + } + + if (!isObjectLike(value)) { + throw new TypeError('The "contextObject" argument must be an object'); + } + + return binding.createContext(value); + } + + function ensureExistingContext(value) { + if (!binding.isContext(value)) { + throw new TypeError( + 'The "contextifiedObject" argument must be a vm context created by vm.createContext()', + ); + } + return value; + } + + function copyExtensionProps(target, extensions) { + const backups = []; + for (const extension of extensions) { + if (!isObjectLike(extension)) { + continue; + } + + for (const key of Object.keys(extension)) { + backups.push({ + key, + existed: Object.prototype.hasOwnProperty.call(target, key), + value: target[key], + }); + target[key] = extension[key]; + } + } + return backups; + } + + function restoreExtensionProps(target, backups) { + for (let index = backups.length - 1; index >= 0; index -= 1) { + const entry = backups[index]; + if (entry.existed) { + target[entry.key] = entry.value; + } else { + delete target[entry.key]; + } + } + } + + function resolveRunInNewContextArgs(contextObject, options) { + if (arguments.length === 0) { + return { sandbox: binding.createContext({}), options: undefined }; + } + + if (contextObject == null || contextObject === kDontContextify) { + return { sandbox: binding.createContext({}), options }; + } + + if (isObjectLike(contextObject)) { + return { sandbox: binding.createContext(contextObject), options }; + } + + return { sandbox: binding.createContext({}), options: contextObject }; + } + + class Script { + constructor(code, options) { + const source = String(code); + const filename = normalizeFilename(options); + const native = new NativeScriptCtor(source, options); + nativeScript.set(this, native); + scriptState.set(this, { + source, + filename, + cachedDataRejected: false, + sourceMapURL: extractSourceMapURL(source), + }); + } + + runInContext(contextifiedObject, options) { + return nativeScript.get(this).runInContext(ensureExistingContext(contextifiedObject), options); + } + + runInNewContext(contextObject, options) { + const resolved = resolveRunInNewContextArgs(contextObject, options); + return nativeScript.get(this).runInNewContext(resolved.sandbox, resolved.options); + } + + runInThisContext(options) { + return nativeScript.get(this).runInThisContext(options); + } + + createCachedData() { + return encodeSource(scriptState.get(this).source); + } + + get cachedDataRejected() { + return scriptState.get(this).cachedDataRejected; + } + + get sourceMapURL() { + return scriptState.get(this).sourceMapURL; + } + } + + function createContext(contextObject) { + return ensureContextObject(contextObject); + } + + function isContext(value) { + return binding.isContext(value); + } + + function runInContext(code, contextifiedObject, options) { + return binding.runInContext(code, ensureExistingContext(contextifiedObject), options); + } + + function runInNewContext(code, contextObject, options) { + const resolved = resolveRunInNewContextArgs(contextObject, options); + return binding.runInContext(code, resolved.sandbox, resolved.options); + } + + function runInThisContext(code, options) { + return binding.runInThisContext(code, options); + } + + function normalizeParams(params) { + if (params == null) { + return []; + } + + if (!Array.isArray(params)) { + throw new TypeError('The "params" argument must be an Array'); + } + + return params.map((item) => String(item)); + } + + function compileFunction(code, params, options = {}) { + const body = String(code); + const paramNames = normalizeParams(params); + const filename = normalizeFilename(options); + const parsingContext = options.parsingContext + ? ensureExistingContext(options.parsingContext) + : null; + const contextExtensions = Array.isArray(options.contextExtensions) + ? options.contextExtensions + : []; + const sourceMapURL = extractSourceMapURL(body); + + const invoke = function invoke(thisArg, argsLike) { + const args = Array.from(argsLike); + const target = parsingContext || globalThis; + const argsKey = nextToken('args'); + const thisKey = nextToken('this'); + const backups = parsingContext ? copyExtensionProps(target, contextExtensions) : []; + + target[argsKey] = args; + target[thisKey] = thisArg; + + const invocationSource = ` + (function() { + const __vmArgs = globalThis[${JSON.stringify(argsKey)}]; + const __vmThis = globalThis[${JSON.stringify(thisKey)}]; + return (function(${paramNames.join(',')}) { +${body} + }).apply(__vmThis, __vmArgs); + })() + `; + + try { + if (parsingContext) { + return binding.runInContext(invocationSource, target, { filename }); + } + return binding.runInThisContext(invocationSource, { filename }); + } finally { + delete target[argsKey]; + delete target[thisKey]; + if (parsingContext) { + restoreExtensionProps(target, backups); + } + } + }; + + const wrapperFactory = Function( + 'invoke', + `return function(${paramNames.join(',')}) { return invoke(new.target ? this : undefined, arguments, new.target); };`, + ); + const compiled = wrapperFactory(invoke); + + Object.defineProperties(compiled, { + cachedDataRejected: { + configurable: true, + enumerable: false, + get() { + return false; + }, + }, + sourceMapURL: { + configurable: true, + enumerable: false, + get() { + return sourceMapURL; + }, + }, + createCachedData: { + configurable: true, + enumerable: false, + writable: true, + value() { + return encodeSource(body); + }, + }, + }); + + return compiled; + } + + function getModuleData(module) { + const state = moduleState.get(module); + if (!state) { + throw new TypeError('Invalid vm.Module receiver'); + } + return state; + } + + class ModuleBase { + constructor(handle) { + if (new.target === ModuleBase) { + throw new TypeError('vm.Module is an abstract class'); + } + + moduleState.set(this, { + handle, + namespace: null, + linkedModules: [], + linkerPromise: null, + evaluatePromise: null, + localError: undefined, + statusOverride: null, + }); + } + + get identifier() { + return binding.moduleGetIdentifier(getModuleData(this).handle); + } + + get context() { + return binding.moduleGetContext(getModuleData(this).handle); + } + + get status() { + const state = getModuleData(this); + return state.statusOverride || binding.moduleGetStatus(state.handle); + } + + get namespace() { + const state = getModuleData(this); + if (this.status === 'unlinked') { + throw new Error('Module has not been linked'); + } + + if (!state.namespace) { + const handle = state.handle; + const exportNames = binding.moduleGetExportNames(handle); + const namespace = {}; + for (const name of exportNames) { + Object.defineProperty(namespace, name, { + configurable: false, + enumerable: true, + get() { + return binding.moduleGetNamespaceValue(handle, name); + }, + }); + } + state.namespace = Object.freeze(namespace); + } + + return state.namespace; + } + + get error() { + const state = getModuleData(this); + return state.localError !== undefined ? state.localError : binding.moduleGetError(state.handle); + } + + async link(linker) { + if (typeof linker !== 'function') { + throw new TypeError('The "linker" argument must be a function'); + } + + const state = getModuleData(this); + if (state.linkerPromise) { + return state.linkerPromise; + } + + state.statusOverride = 'linking'; + state.localError = undefined; + state.linkerPromise = (async () => { + const dependencySpecifiers = binding.moduleGetDependencySpecifiers(state.handle); + const linked = []; + for (const specifier of dependencySpecifiers) { + const resolved = await linker(specifier, this); + if (!(resolved instanceof ModuleBase)) { + throw new TypeError('Linker must return vm.Module instances'); + } + linked.push(resolved); + } + state.linkedModules = linked; + binding.moduleLinkRequests( + state.handle, + linked.map((module) => getModuleData(module).handle), + ); + state.statusOverride = null; + })().catch((error) => { + state.statusOverride = 'errored'; + state.localError = error; + throw error; + }); + + return state.linkerPromise; + } + + async evaluate() { + throw new TypeError('evaluate() is not implemented for this vm.Module'); + } + } + + class SourceTextModule extends ModuleBase { + constructor(sourceText, options = {}) { + const source = String(sourceText); + super(binding.createSourceTextModule(source, options)); + const state = getModuleData(this); + state.sourceText = source; + state.sourceMapURL = extractSourceMapURL(state.sourceText); + } + + get dependencySpecifiers() { + return binding.moduleGetDependencySpecifiers(getModuleData(this).handle); + } + + get moduleRequests() { + return this.dependencySpecifiers.map((specifier) => ({ + specifier, + attributes: Object.freeze({}), + phase: 'evaluation', + })); + } + + get sourceMapURL() { + return getModuleData(this).sourceMapURL; + } + + createCachedData() { + return binding.moduleCreateCachedData(getModuleData(this).handle); + } + + hasTopLevelAwait() { + return binding.moduleHasTopLevelAwait(getModuleData(this).handle); + } + + hasAsyncGraph() { + return binding.moduleHasAsyncGraph(getModuleData(this).handle); + } + + linkRequests(modules) { + const state = getModuleData(this); + if (!Array.isArray(modules)) { + throw new TypeError('The "modules" argument must be an Array'); + } + if (modules.length !== this.dependencySpecifiers.length) { + throw new Error('linkRequests() module count must match dependencySpecifiers'); + } + + for (let index = 0; index < modules.length; index += 1) { + const mod = modules[index]; + if (!(mod instanceof ModuleBase)) { + throw new TypeError('linkRequests() expects vm.Module instances'); + } + } + state.linkedModules = modules.slice(); + } + + instantiate() { + const state = getModuleData(this); + if (this.status === 'linked') { + return; + } + if (state.linkedModules.length !== this.dependencySpecifiers.length) { + throw new Error('linkRequests() module count must match dependencySpecifiers'); + } + binding.moduleLinkRequests( + state.handle, + state.linkedModules.map((module) => { + if (!(module instanceof ModuleBase)) { + throw new TypeError('linkRequests() expects vm.Module instances'); + } + return getModuleData(module).handle; + }), + ); + binding.moduleInstantiate(state.handle); + } + + async evaluate() { + const state = getModuleData(this); + if (this.status === 'evaluated') { + return undefined; + } + if (this.status === 'errored') { + throw this.error; + } + if (state.evaluatePromise) { + return state.evaluatePromise; + } + if (this.status === 'unlinked') { + this.instantiate(); + } + + state.statusOverride = 'evaluating'; + state.localError = undefined; + state.evaluatePromise = (async () => { + for (const mod of state.linkedModules) { + if (!(mod instanceof ModuleBase)) { + throw new Error('Missing linked module'); + } + await mod.evaluate(); + } + await Promise.resolve(binding.moduleEvaluate(state.handle)); + state.statusOverride = null; + return undefined; + })().catch((error) => { + state.statusOverride = + binding.moduleGetStatus(state.handle) === 'errored' ? null : 'errored'; + state.localError = error; + throw error; + }); + return state.evaluatePromise; + } + } + + class SyntheticModule extends ModuleBase { + constructor(exportNames, evaluateCallback, options = {}) { + if (!Array.isArray(exportNames)) { + throw new TypeError('The "exportNames" argument must be an Array'); + } + if (typeof evaluateCallback !== 'function') { + throw new TypeError('The "evaluateCallback" argument must be a function'); + } + + super(binding.createSyntheticModule(exportNames.map((name) => String(name)), options)); + const state = getModuleData(this); + state.exportNames = exportNames.map((name) => String(name)); + state.evaluateCallback = evaluateCallback; + } + + setExport(name, value) { + const state = getModuleData(this); + const exportName = String(name); + if (!state.exportNames.includes(exportName)) { + throw new Error(`Unknown synthetic module export "${exportName}"`); + } + binding.moduleSetSyntheticExport(state.handle, exportName, value); + state.namespace = null; + } + + linkRequests(modules) { + if (Array.isArray(modules) && modules.length !== 0) { + throw new Error('SyntheticModule does not accept linked requests'); + } + } + + instantiate() { + binding.moduleInstantiate(getModuleData(this).handle); + } + + async evaluate() { + const state = getModuleData(this); + if (this.status === 'evaluated') { + return undefined; + } + if (this.status === 'errored') { + throw this.error; + } + if (state.evaluatePromise) { + return state.evaluatePromise; + } + + state.statusOverride = 'evaluating'; + state.localError = undefined; + state.evaluatePromise = (async () => { + this.instantiate(); + await state.evaluateCallback.call(this); + await Promise.resolve(binding.moduleEvaluate(state.handle)); + state.statusOverride = null; + return undefined; + })().catch((error) => { + state.statusOverride = + binding.moduleGetStatus(state.handle) === 'errored' ? null : 'errored'; + state.localError = error; + throw error; + }); + return state.evaluatePromise; + } + } + + function measureMemory() { + const usage = + typeof process !== 'undefined' && process && typeof process.memoryUsage === 'function' + ? process.memoryUsage() + : { heapUsed: 0 }; + const estimate = Number(usage.heapUsed) || 0; + return Promise.resolve({ + total: { + jsMemoryEstimate: estimate, + jsMemoryRange: [estimate, estimate], + }, + current: { + jsMemoryEstimate: estimate, + jsMemoryRange: [estimate, estimate], + }, + other: [], + }); + } + + return { + Script, + Module: ModuleBase, + SourceTextModule, + SyntheticModule, + compileFunction, + constants, + createContext, + isContext, + measureMemory, + runInContext, + runInNewContext, + runInThisContext, + }; +}) +)JS"; + + napi_value source; + napi_value factory; + napi_value global; + napi_value exports; + if (napi_create_string_utf8(env, kWrapperSource, NAPI_AUTO_LENGTH, &source) != + napi_ok || + napi_run_script_source(env, source, "node:vm-wrapper.js", &factory) != + napi_ok || + napi_get_global(env, &global) != napi_ok || + napi_call_function(env, global, factory, 1, &binding, &exports) != + napi_ok) { + return nullptr; + } + + return exports; +} + +} // namespace + +napi_value VM::CreateModule(napi_env env) { + napi_value module; + napi_value binding; + napi_value scriptCtor; + + napi_create_object(env, &module); + napi_create_object(env, &binding); + + const napi_property_descriptor scriptProperties[] = { + napi_util::desc("runInContext", ScriptRunInContext, nullptr), + napi_util::desc("runInNewContext", ScriptRunInNewContext, nullptr), + napi_util::desc("runInThisContext", ScriptRunInThisContext, nullptr), + }; + + napi_define_class(env, "Script", NAPI_AUTO_LENGTH, ScriptConstructor, nullptr, + 3, scriptProperties, &scriptCtor); + + const napi_property_descriptor bindingProperties[] = { + napi_util::desc("Script", scriptCtor), + napi_util::desc("createContext", CreateContextCallback, nullptr), + napi_util::desc("isContext", IsContextCallback, nullptr), + napi_util::desc("createSourceTextModule", CreateSourceTextModuleCallback, + nullptr), + napi_util::desc("createSyntheticModule", CreateSyntheticModuleCallback, + nullptr), + napi_util::desc("moduleGetIdentifier", ModuleGetIdentifierCallback, + nullptr), + napi_util::desc("moduleGetContext", ModuleGetContextCallback, nullptr), + napi_util::desc("moduleGetDependencySpecifiers", + ModuleGetDependencySpecifiersCallback, nullptr), + napi_util::desc("moduleGetStatus", ModuleGetStatusCallback, nullptr), + napi_util::desc("moduleGetError", ModuleGetErrorCallback, nullptr), + napi_util::desc("moduleLinkRequests", ModuleLinkRequestsCallback, + nullptr), + napi_util::desc("moduleInstantiate", ModuleInstantiateCallback, nullptr), + napi_util::desc("moduleEvaluate", ModuleEvaluateCallback, nullptr), + napi_util::desc("moduleGetExportNames", ModuleGetExportNamesCallback, + nullptr), + napi_util::desc("moduleGetNamespaceValue", + ModuleGetNamespaceValueCallback, nullptr), + napi_util::desc("moduleCreateCachedData", ModuleCreateCachedDataCallback, + nullptr), + napi_util::desc("moduleHasTopLevelAwait", ModuleHasTopLevelAwaitCallback, + nullptr), + napi_util::desc("moduleHasAsyncGraph", ModuleHasAsyncGraphCallback, + nullptr), + napi_util::desc("moduleSetSyntheticExport", + ModuleSetSyntheticExportCallback, nullptr), + napi_util::desc("runInContext", RunInContextCallback, nullptr), + napi_util::desc("runInNewContext", RunInNewContextCallback, nullptr), + napi_util::desc("runInThisContext", RunInThisContextCallback, nullptr), + }; + + napi_define_properties( + env, binding, sizeof(bindingProperties) / sizeof(bindingProperties[0]), + bindingProperties); + napi_value exports = CreatePublicVmExports(env, binding); + if (exports == nullptr) { + return nullptr; + } + napi_set_named_property(env, module, "exports", exports); + return module; +} + +} // namespace nativescript diff --git a/NativeScript/runtime/modules/node/VM.h b/NativeScript/runtime/modules/node/VM.h new file mode 100644 index 00000000..c26123f4 --- /dev/null +++ b/NativeScript/runtime/modules/node/VM.h @@ -0,0 +1,15 @@ +#ifndef NATIVE_NODE_VM_H +#define NATIVE_NODE_VM_H + +#include "js_native_api_types.h" + +namespace nativescript { + +class VM { + public: + static napi_value CreateModule(napi_env env); +}; + +} // namespace nativescript + +#endif // NATIVE_NODE_VM_H diff --git a/cli_tests/vm.js b/cli_tests/vm.js new file mode 100644 index 00000000..92bdb37f --- /dev/null +++ b/cli_tests/vm.js @@ -0,0 +1,336 @@ +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function assertEqual(actual, expected, message) { + if (!Object.is(actual, expected)) { + throw new Error( + `${message}: expected ${String(expected)}, got ${String(actual)}`, + ); + } +} + +const vm = require("node:vm"); +const vmAlias = require("vm"); + +assert(typeof vm.createContext === "function", "vm.createContext should exist"); +assert( + typeof vmAlias.runInContext === "function", + "require('vm') should resolve the builtin module", +); + +const sandbox = { count: 1 }; +const context = vm.createContext(sandbox); + +assert(context === sandbox, "createContext should return the sandbox"); +assert(vm.isContext(sandbox) === true, "contextified sandbox should be tagged"); +assert(vm.isContext({}) === false, "plain objects are not vm contexts"); + +const contextResult = vm.runInContext( + "count += 2; sameGlobal = this === globalThis; result = count * 3; result", + sandbox, + { filename: "vm-context.js" }, +); + +assertEqual(contextResult, 9, "runInContext should return the last expression"); +assertEqual(sandbox.count, 3, "runInContext should persist sandbox mutations"); +assertEqual( + sandbox.sameGlobal, + true, + "sandbox execution should bind this to globalThis", +); +assertEqual(sandbox.result, 9, "runInContext should sync new globals back"); + +const newContextSandbox = { total: 4 }; +const newContextResult = vm.runInNewContext( + "total += 5; status = 'ok'; total", + newContextSandbox, + { filename: "vm-new-context.js" }, +); + +assertEqual( + newContextResult, + 9, + "runInNewContext should evaluate against a fresh sandbox", +); +assertEqual( + newContextSandbox.total, + 9, + "runInNewContext should write mutations back to the provided sandbox", +); +assertEqual( + newContextSandbox.status, + "ok", + "runInNewContext should preserve newly assigned globals", +); + +globalThis.__vmGlobalValue = 10; +const thisContextResult = vm.runInThisContext( + "globalThis.__vmGlobalValue += 7; globalThis.__vmGlobalValue", + { filename: "vm-this-context.js" }, +); +assertEqual( + thisContextResult, + 17, + "runInThisContext should execute against the current global", +); +assertEqual( + globalThis.__vmGlobalValue, + 17, + "runInThisContext should mutate the active global object", +); +delete globalThis.__vmGlobalValue; + +const script = new vm.Script("value += 3; marker = 'script'; value", { + filename: "vm-script.js", +}); +const scriptSandbox = vm.createContext({ value: 5 }); +const scriptResult = script.runInContext(scriptSandbox); + +assertEqual(scriptResult, 8, "Script.runInContext should execute stored code"); +assertEqual(scriptSandbox.value, 8, "Script.runInContext should update sandbox"); +assertEqual( + scriptSandbox.marker, + "script", + "Script.runInContext should sync new globals", +); + +const scriptNewContextSandbox = { value: 2 }; +const scriptNewContextResult = script.runInNewContext(scriptNewContextSandbox); + +assertEqual( + scriptNewContextResult, + 5, + "Script.runInNewContext should run inside a fresh context", +); +assertEqual( + scriptNewContextSandbox.value, + 5, + "Script.runInNewContext should sync sandbox changes", +); + +globalThis.__vmScriptValue = 1; +const scriptThisResult = new vm.Script( + "globalThis.__vmScriptValue += 4; globalThis.__vmScriptValue", +).runInThisContext(); +assertEqual( + scriptThisResult, + 5, + "Script.runInThisContext should execute against the active global", +); +assertEqual( + globalThis.__vmScriptValue, + 5, + "Script.runInThisContext should update the active global", +); +delete globalThis.__vmScriptValue; + +assert( + typeof vm.compileFunction === "function", + "vm.compileFunction should exist", +); +assert( + typeof vm.SourceTextModule === "function", + "vm.SourceTextModule should exist", +); +assert( + typeof vm.SyntheticModule === "function", + "vm.SyntheticModule should exist", +); +assert( + typeof vm.measureMemory === "function", + "vm.measureMemory should exist", +); +assert( + vm.constants && typeof vm.constants === "object", + "vm.constants should exist", +); +assert( + vm.constants.DONT_CONTEXTIFY, + "vm.constants.DONT_CONTEXTIFY should exist", +); +assert( + vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, + "vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER should exist", +); + +const dontContextify = vm.createContext(vm.constants.DONT_CONTEXTIFY); +assert( + vm.isContext(dontContextify) === true, + "createContext(DONT_CONTEXTIFY) should still create a context", +); + +const cachedData = script.createCachedData(); +assert( + cachedData && typeof cachedData.byteLength === "number", + "Script.createCachedData should return a byte container", +); +assert( + cachedData.byteLength > 0, + "Script.createCachedData should not be empty", +); + +const sourceMapScript = new vm.Script( + "value = 1;\n//# sourceMappingURL=vm-script.map", +); +assertEqual( + sourceMapScript.sourceMapURL, + "vm-script.map", + "Script.sourceMapURL should expose the magic comment value", +); +assertEqual( + sourceMapScript.cachedDataRejected, + false, + "Script.cachedDataRejected should default to false", +); + +const compileSandbox = vm.createContext({ base: 6 }); +const compiled = vm.compileFunction("return base + increment + offset;", ["increment"], { + filename: "vm-compile.js", + parsingContext: compileSandbox, + contextExtensions: [{ offset: 5 }], +}); + +assertEqual(compiled.length, 1, "compileFunction should preserve arity"); +assertEqual( + compiled(4), + 15, + "compileFunction should run against the parsing context", +); +const compiledCache = compiled.createCachedData(); +assert( + compiledCache && typeof compiledCache.byteLength === "number", + "compileFunction.createCachedData should return a byte container", +); + +let moduleCtorThrew = false; +try { + // eslint-disable-next-line no-new + new vm.Module(); +} catch (error) { + moduleCtorThrew = true; +} +assert(moduleCtorThrew, "vm.Module should be abstract"); + +(async () => { + const synthetic = new vm.SyntheticModule( + ["value", "label"], + function () { + this.setExport("value", 40); + this.setExport("label", "synthetic"); + }, + { identifier: "synthetic:dep" }, + ); + + await synthetic.evaluate(); + assertEqual( + synthetic.namespace.value, + 40, + "SyntheticModule.evaluate should populate namespace exports", + ); + + const linkedModule = new vm.SourceTextModule( + [ + "import { value } from 'dep';", + "const result = value + 2;", + "export { result };", + "export default value + 1;", + ].join("\n"), + { identifier: "source:text" }, + ); + + assertEqual( + linkedModule.status, + "unlinked", + "SourceTextModule should start unlinked", + ); + + await linkedModule.link(async (specifier) => { + assertEqual(specifier, "dep", "SourceTextModule.link should receive import specifiers"); + return synthetic; + }); + + assertEqual( + linkedModule.status, + "linked", + "SourceTextModule.link should transition to linked", + ); + assertEqual( + linkedModule.identifier, + "source:text", + "SourceTextModule should preserve identifier", + ); + assertEqual( + linkedModule.dependencySpecifiers[0], + "dep", + "dependencySpecifiers should expose static imports", + ); + assertEqual( + linkedModule.moduleRequests[0].specifier, + "dep", + "moduleRequests should expose static imports", + ); + assertEqual( + linkedModule.hasTopLevelAwait(), + false, + "hasTopLevelAwait should reflect simple synchronous modules", + ); + assertEqual( + linkedModule.hasAsyncGraph(), + false, + "hasAsyncGraph should be false for synchronous graphs", + ); + + const moduleCache = linkedModule.createCachedData(); + assert( + moduleCache && typeof moduleCache.byteLength === "number", + "SourceTextModule.createCachedData should return a byte container", + ); + + await linkedModule.evaluate(); + assertEqual( + linkedModule.status, + "evaluated", + "SourceTextModule.evaluate should transition to evaluated", + ); + assertEqual( + linkedModule.namespace.result, + 42, + "SourceTextModule.evaluate should populate named exports", + ); + assertEqual( + linkedModule.namespace.default, + 41, + "SourceTextModule.evaluate should populate the default export", + ); + + const linkedByRequests = new vm.SourceTextModule( + [ + "import { value } from 'dep';", + "export const total = value + 3;", + ].join("\n"), + { identifier: "source:requests" }, + ); + linkedByRequests.linkRequests([synthetic]); + linkedByRequests.instantiate(); + await linkedByRequests.evaluate(); + assertEqual( + linkedByRequests.namespace.total, + 43, + "linkRequests() + instantiate() should support SourceTextModule evaluation", + ); + + const memory = await vm.measureMemory({ mode: "summary" }); + assert(memory && typeof memory === "object", "measureMemory should resolve an object"); + assert( + memory.total && typeof memory.total.jsMemoryEstimate === "number", + "measureMemory should include a numeric total.jsMemoryEstimate", + ); + + console.log(`vm PASS (${process.versions.engine})`); +})().catch((error) => { + console.log(error && error.stack ? error.stack : String(error)); + throw error; +});