diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3357bf59..af35dbba 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -54,14 +54,24 @@ jobs: - name: Run Tests working-directory: Build/ubuntu/Tests/UnitTests - run: ./UnitTests - env: - TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt', github.workspace) || '' }} + run: | # JSC's concurrent GC on Linux uses SIGUSR1 + sem_wait to suspend mutator # threads at safepoints. TSan's signal interception delays SIGUSR1 delivery # indefinitely, deadlocking the Collector Thread's sem_wait. Disabling the # concurrent collector removes the dedicated Collector Thread, so GC runs # on the mutator without cross-thread signals. macOS JSC uses Mach # thread_suspend() and is unaffected. - JSC_useConcurrentGC: ${{ inputs.enable-thread-sanitizer && '0' || '' }} + if [[ "${{ inputs.enable-thread-sanitizer }}" == "true" ]]; then + export TSAN_OPTIONS="suppressions=${{ github.workspace }}/.github/tsan_suppressions.txt" + export JSC_useConcurrentGC=0 + fi + ./UnitTests + - name: Run Node-API v7 conformance + working-directory: Build/ubuntu/Tests/NodeApi + run: | + if [[ "${{ inputs.enable-thread-sanitizer }}" == "true" ]]; then + export TSAN_OPTIONS="suppressions=${{ github.workspace }}/.github/tsan_suppressions.txt" + export JSC_useConcurrentGC=0 + fi + ./NodeApiTests diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index e58aac78..224004cc 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -41,7 +41,7 @@ jobs: -D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }} - name: Build - run: cmake --build Build/macOS --target UnitTests --config RelWithDebInfo + run: cmake --build Build/macOS --target UnitTests NodeApiTests --config RelWithDebInfo - name: Run Tests working-directory: Build/macOS/Tests/UnitTests/RelWithDebInfo @@ -49,3 +49,8 @@ jobs: env: TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt', github.workspace) || '' }} + - name: Run Node-API v7 conformance + working-directory: Build/macOS/Tests/NodeApi/RelWithDebInfo + run: ./NodeApiTests + env: + TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt', github.workspace) || '' }} diff --git a/.github/workflows/worker-wpt.yml b/.github/workflows/worker-wpt.yml new file mode 100644 index 00000000..9b515256 --- /dev/null +++ b/.github/workflows/worker-wpt.yml @@ -0,0 +1,29 @@ +name: Worker WPT + +on: + pull_request: + branches: [napi-v7] + +jobs: + Ubuntu_JSC: + uses: ./.github/workflows/build-linux.yml + + Ubuntu_JSC_Sanitizers: + uses: ./.github/workflows/build-linux.yml + with: + cc: clang + cxx: clang++ + enable-sanitizers: true + + Ubuntu_QuickJS_ThreadSanitizer: + uses: ./.github/workflows/build-linux.yml + with: + cc: clang + cxx: clang++ + # Linux JSC suspends mutator threads with SIGUSR1. TSan intercepts the + # signal and deadlocks inside the uninstrumented system library, even + # with concurrent GC disabled. QuickJS still exercises the instrumented + # Worker ownership, message queues, and teardown paths without hiding + # host races behind an incompatible third-party runtime. + js-engine: QuickJS + enable-thread-sanitizer: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 481e848f..9d4308a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,11 +51,14 @@ FetchContent_Declare(UrlLib GIT_REPOSITORY https://github.com/BabylonJS/UrlLib.git GIT_TAG e86ffb34e77092266145497681efc74e0a920ffe EXCLUDE_FROM_ALL) +FetchContent_Declare(zlib + URL "https://github.com/madler/zlib/archive/refs/tags/v1.3.1.tar.gz" + EXCLUDE_FROM_ALL) FetchContent_Declare(quickjs-ng GIT_REPOSITORY https://github.com/quickjs-ng/quickjs.git GIT_TAG 93d3f7df465027f487ed37e175a0bc3012fee79e EXCLUDE_FROM_ALL) - + # -------------------------------------------------- FetchContent_MakeAvailable(CMakeExtensions) @@ -75,6 +78,14 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# A newer MSVC (windows-latest) emits C4875 ("a non-string literal argument to [[gsl::suppress]] is +# deprecated") from the vendored GSL headers, which several targets compile with warnings-as-error. +# Suppress this dependency-side deprecation so the Windows build isn't broken by toolchain drift. Must +# be set before arcana.cpp / Core / Polyfills are added below so they inherit it. +if(MSVC) + add_compile_options(/wd4875) +endif() + # -------------------------------------------------- # Options # -------------------------------------------------- @@ -102,6 +113,10 @@ option(JSRUNTIMEHOST_POLYFILL_FILE "Include JsRuntimeHost Polyfill File and File option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON) option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON) option(JSRUNTIMEHOST_POLYFILL_TEXTENCODER "Include JsRuntimeHost Polyfill TextEncoder." ON) +option(JSRUNTIMEHOST_POLYFILL_WORKER "Include JsRuntimeHost Polyfill Worker." ON) +option(JSRUNTIMEHOST_POLYFILL_STREAMS "Include JsRuntimeHost Polyfill Web Streams." ON) +option(JSRUNTIMEHOST_POLYFILL_COMPRESSION "Include JsRuntimeHost Polyfills CompressionStream and DecompressionStream." ON) +option(JSRUNTIMEHOST_POLYFILL_INDEXEDDB "Include JsRuntimeHost Polyfill IndexedDB." ON) # Sanitizers option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) @@ -154,6 +169,17 @@ endif() FetchContent_MakeAvailable_With_Message(arcana.cpp) set_property(TARGET arcana PROPERTY FOLDER Dependencies) +if(ANDROID) + FetchContent_GetProperties(AndroidExtensions) + if(NOT AndroidExtensions_POPULATED) + FetchContent_Populate(AndroidExtensions) + FetchContent_GetProperties(AndroidExtensions) + add_subdirectory(${androidextensions_SOURCE_DIR} ${androidextensions_BINARY_DIR}) + else() + add_subdirectory(${androidextensions_SOURCE_DIR} ${androidextensions_BINARY_DIR}) + endif() +endif() + if(JSRUNTIMEHOST_POLYFILL_XMLHTTPREQUEST OR JSRUNTIMEHOST_POLYFILL_FETCH) FetchContent_MakeAvailable_With_Message(UrlLib) set_property(TARGET UrlLib PROPERTY FOLDER Dependencies) diff --git a/Core/AppRuntime/CMakeLists.txt b/Core/AppRuntime/CMakeLists.txt index f7bc649d..29b95d28 100644 --- a/Core/AppRuntime/CMakeLists.txt +++ b/Core/AppRuntime/CMakeLists.txt @@ -54,6 +54,8 @@ if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8" AND JSRUNTIMEHOST_CORE_APPRUNTIME_V8_INS set_property(TARGET v8inspector PROPERTY FOLDER Dependencies) elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "QuickJS") target_link_libraries(AppRuntime PRIVATE qjs) +elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore" AND UNIX AND NOT APPLE) + target_link_libraries(AppRuntime PRIVATE ${CMAKE_DL_LIBS}) endif() set_property(TARGET AppRuntime PROPERTY FOLDER Core) diff --git a/Core/AppRuntime/Include/Babylon/AppRuntime.h b/Core/AppRuntime/Include/Babylon/AppRuntime.h index e001fa32..571ae7d8 100644 --- a/Core/AppRuntime/Include/Babylon/AppRuntime.h +++ b/Core/AppRuntime/Include/Babylon/AppRuntime.h @@ -21,6 +21,11 @@ namespace Babylon // Optional handler for unhandled exceptions. std::function UnhandledExceptionHandler{DefaultUnhandledExceptionHandler}; + // Optional final runtime-thread notification after engine and + // environment teardown. The callback is the thread's last action + // and must not access that runtime's Napi objects. + std::function ThreadExitHandler{}; + // Defines whether to enable the debugger. Only implemented for V8 and Chakra. bool EnableDebugger{false}; @@ -43,6 +48,17 @@ namespace Babylon void Suspend(); void Resume(); + // Permanently stop accepting work and exit after the currently + // executing dispatch returns. Unlike Terminate(), this does not + // interrupt JavaScript in the middle of its current task. + void Close(); + + // Permanently stop accepting work and request interruption of any + // JavaScript currently executing. The interruption is immediate on + // engines with an interrupt hook (including system JavaScriptCore) and + // cooperative between dispatches on the remaining engines. + void Terminate(); + void Dispatch(Dispatchable callback); // Default unhandled exception handler that outputs the error message to the program output. @@ -76,6 +92,14 @@ namespace Babylon // queue explicitly (Napi::DrainJobs / JS_ExecutePendingJob). void DrainMicrotasks(Napi::Env env); + // Engine tiers may query the shared termination flag without exposing + // engine types in the public API. + bool IsTerminationRequested() const noexcept; + + // Execution watchdogs use a separate flag so Close() can finish the + // current task while Terminate() can still interrupt a tight loop. + bool IsExecutionTerminationRequested() const noexcept; + Options m_options; class Impl; diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index 176bc849..6f5b2f12 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -36,6 +37,8 @@ namespace Babylon arcana::cancellation_source m_cancelSource{}; arcana::manual_dispatcher<128> m_dispatcher{}; std::thread m_thread; + std::atomic_bool m_terminationRequested{false}; + std::atomic_bool m_executionTerminationRequested{false}; }; AppRuntime::AppRuntime() : @@ -47,7 +50,13 @@ namespace Babylon : m_options{std::move(options)} , m_impl{std::make_unique()} { - m_impl->m_thread = std::thread{[this] { RunPlatformTier(); }}; + m_impl->m_thread = std::thread{[this] { + RunPlatformTier(); + if (m_options.ThreadExitHandler) + { + m_options.ThreadExitHandler(); + } + }}; Dispatch([this](Napi::Env env) { JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); }); @@ -61,17 +70,7 @@ namespace Babylon m_impl->m_suspensionLock.reset(); } - // Cancel immediately so pending work is dropped promptly, then append - // a no-op work item to wake the worker thread from blocking_tick. The - // no-op goes through push() which acquires the queue mutex, avoiding - // the race where a bare notify_all() can be missed by wait(). - // - // NOTE: This preserves the existing shutdown behavior where pending - // callbacks are dropped on cancellation. A more complete solution - // would add cooperative shutdown (e.g. NotifyDisposing/Rundown) so - // consumers can finish cleanup work before the runtime is destroyed. - m_impl->m_cancelSource.cancel(); - m_impl->Append([](Napi::Env) {}); + Terminate(); m_impl->m_thread.join(); } @@ -105,8 +104,44 @@ namespace Babylon m_impl->m_suspensionLock.reset(); } + void AppRuntime::Terminate() + { + m_impl->m_executionTerminationRequested.store(true); + Close(); + } + + void AppRuntime::Close() + { + if (m_impl->m_terminationRequested.exchange(true)) + { + return; + } + + m_impl->m_cancelSource.cancel(); + + // Queueing under the dispatcher's mutex makes the wake-up immune to + // the missed-notification race covered by DestroyDoesNotDeadlock. + // The cancelled run loop drops this no-op rather than executing it. + m_impl->m_dispatcher.queue([]() {}); + } + + bool AppRuntime::IsTerminationRequested() const noexcept + { + return m_impl->m_terminationRequested.load(); + } + + bool AppRuntime::IsExecutionTerminationRequested() const noexcept + { + return m_impl->m_executionTerminationRequested.load(); + } + void AppRuntime::Dispatch(Dispatchable func) { + if (IsTerminationRequested()) + { + return; + } + m_impl->Append([this, func{std::move(func)}](Napi::Env env) mutable { Execute([this, env, func{std::move(func)}]() mutable { // Some engines (notably Hermes) require an open NAPI handle diff --git a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp index b1334c22..ff527fdb 100644 --- a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp +++ b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp @@ -1,12 +1,62 @@ #include "AppRuntime.h" #include +#if __has_include() +#include +#define JSRUNTIMEHOST_HAS_JSC_EXECUTION_TIME_LIMIT 1 +#elif defined(__unix__) +#include +#define JSRUNTIMEHOST_LOOKUP_JSC_EXECUTION_TIME_LIMIT 1 +#endif + +namespace +{ +#if defined(JSRUNTIMEHOST_LOOKUP_JSC_EXECUTION_TIME_LIMIT) + using SetExecutionTimeLimit = void (*)(JSContextGroupRef, double, bool (*)(JSContextRef, void*), void*); + using ClearExecutionTimeLimit = void (*)(JSContextGroupRef); +#endif +} + namespace Babylon { void AppRuntime::RunEnvironmentTier(const char*) { auto globalContext = JSGlobalContextCreateInGroup(nullptr, nullptr); +#if defined(JSRUNTIMEHOST_HAS_JSC_EXECUTION_TIME_LIMIT) || \ + defined(JSRUNTIMEHOST_LOOKUP_JSC_EXECUTION_TIME_LIMIT) + auto contextGroup = JSContextGetGroup(globalContext); + const auto shouldTerminateJSC = [](JSContextRef, void* context) { + return static_cast(context)->IsExecutionTerminationRequested(); + }; +#endif + +#if defined(JSRUNTIMEHOST_HAS_JSC_EXECUTION_TIME_LIMIT) + // Poll at a modest interval while JS is running. Returning true from + // this callback raises a catchable termination exception and lets the + // AppRuntime thread unwind, so Worker::terminate() also stops a tight + // loop that never reaches the dispatch queue. + JSContextGroupSetExecutionTimeLimit( + contextGroup, + 0.05, + shouldTerminateJSC, + this); +#elif defined(JSRUNTIMEHOST_LOOKUP_JSC_EXECUTION_TIME_LIMIT) + // WebKitGTK deliberately omits JSContextRefPrivate.h from its dev + // package, but current system builds export the same C ABI. Resolve it + // dynamically so JsRuntimeHost stays buildable against the public + // package and gracefully falls back to between-dispatch termination on + // older builds that do not export the watchdog. + auto setExecutionTimeLimit = reinterpret_cast( + dlsym(RTLD_DEFAULT, "JSContextGroupSetExecutionTimeLimit")); + auto clearExecutionTimeLimit = reinterpret_cast( + dlsym(RTLD_DEFAULT, "JSContextGroupClearExecutionTimeLimit")); + if (setExecutionTimeLimit != nullptr && clearExecutionTimeLimit != nullptr) + { + setExecutionTimeLimit(contextGroup, 0.05, shouldTerminateJSC, this); + } +#endif + #if __APPLE__ if (__builtin_available(iOS 16.4, macOS 13.3, *)) { @@ -18,6 +68,15 @@ namespace Babylon Run(env); +#if defined(JSRUNTIMEHOST_HAS_JSC_EXECUTION_TIME_LIMIT) + JSContextGroupClearExecutionTimeLimit(contextGroup); +#elif defined(JSRUNTIMEHOST_LOOKUP_JSC_EXECUTION_TIME_LIMIT) + if (setExecutionTimeLimit != nullptr && clearExecutionTimeLimit != nullptr) + { + clearExecutionTimeLimit(contextGroup); + } +#endif + JSGlobalContextRelease(globalContext); // Detach must come after JSGlobalContextRelease since it triggers finalizers which require env. diff --git a/Core/Node-API-JSI/CMakeLists.txt b/Core/Node-API-JSI/CMakeLists.txt index e8e79a96..2b9ca89c 100644 --- a/Core/Node-API-JSI/CMakeLists.txt +++ b/Core/Node-API-JSI/CMakeLists.txt @@ -57,7 +57,12 @@ if(NOT TARGET jsi) endif() target_include_directories(napi - PUBLIC "include") + PUBLIC "include" + # napi.h pulls in the shared , which lives in + # Core/Node-API/Include/Shared. That sibling isn't built when the engine is JSI + # (Core/CMakeLists.txt selects Node-API-JSI instead of Node-API), so reference the shared + # headers directly here -- otherwise the JSI napi fails to compile with C1083. + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../Node-API/Include/Shared") target_link_libraries(napi PUBLIC jsi) diff --git a/Core/Node-API-JSI/Include/napi/napi.h b/Core/Node-API-JSI/Include/napi/napi.h index 76342bc6..cec68d71 100644 --- a/Core/Node-API-JSI/Include/napi/napi.h +++ b/Core/Node-API-JSI/Include/napi/napi.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,42 +9,6 @@ #include #include -// Copied from js_native_api_types.h (https://git.io/J8aI5) -typedef enum { - napi_default = 0, - napi_writable = 1 << 0, - napi_enumerable = 1 << 1, - napi_configurable = 1 << 2, -} napi_property_attributes; - -typedef enum { - // ES6 types (corresponds to typeof) - napi_undefined, - napi_null, - napi_boolean, - napi_number, - napi_string, - napi_symbol, - napi_object, - napi_function, - napi_external, -} napi_valuetype; - -typedef enum { - napi_int8_array, - napi_uint8_array, - napi_uint8_clamped_array, - napi_int16_array, - napi_uint16_array, - napi_int32_array, - napi_uint32_array, - napi_float32_array, - napi_float64_array, - // JSI doesn't support bigint. - // napi_bigint64_array, - // napi_biguint64_array, -} napi_typedarray_type; - struct napi_env__ { napi_env__(facebook::jsi::Runtime& rt) : rt{rt} diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 5f495695..377c4fb0 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -1,15 +1,23 @@ -# Set per-platform defaults if unspecified. -if(WIN32) - set(NAPI_JAVASCRIPT_ENGINE "Chakra" CACHE STRING "JavaScript engine for Node-API") -elseif(APPLE) - set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") -elseif(ANDROID) - set(NAPI_JAVASCRIPT_ENGINE "V8" CACHE STRING "JavaScript engine for Node-API") -elseif(UNIX) - set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") - set(JAVASCRIPTCORE_LIBRARY "/usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so" CACHE STRING "Path to the JavaScriptCore shared library") -else() - message(FATAL_ERROR "Unable to select Node-API JavaScript engine for platform") +# Set per-platform defaults only when the caller did not select an engine. +if(NOT NAPI_JAVASCRIPT_ENGINE) + if(WIN32) + set(NAPI_JAVASCRIPT_ENGINE "Chakra" CACHE STRING "JavaScript engine for Node-API") + elseif(APPLE) + set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") + elseif(ANDROID) + set(NAPI_JAVASCRIPT_ENGINE "V8" CACHE STRING "JavaScript engine for Node-API") + elseif(UNIX) + set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") + else() + message(FATAL_ERROR "Unable to select Node-API JavaScript engine for platform") + endif() +endif() + +if(UNIX AND NOT APPLE AND NOT ANDROID AND NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore") + find_library(JAVASCRIPTCORE_LIBRARY javascriptcoregtk-4.1) + if(NOT JAVASCRIPTCORE_LIBRARY) + message(FATAL_ERROR "JavaScriptCore library not found. Please install libwebkit2gtk-4.1-dev") + endif() endif() set(SOURCES @@ -94,6 +102,13 @@ if(NAPI_BUILD_ABI) else() message(FATAL_ERROR "Unsupported JavaScript engine: ${NAPI_JAVASCRIPT_ENGINE}") endif() + + # The maintained Apple and WebKitGTK headers/runtimes expose the + # BigInt typed-array enum values. The frozen jsc-android package does + # not, so keep its capability-gated build on the older enum surface. + if(NOT ANDROID) + set(JSR_JSC_HAS_BIGINT_TYPED_ARRAYS ON) + endif() elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8") set(SOURCES ${SOURCES} "Source/env_v8.cc" @@ -187,9 +202,9 @@ Make sure Hermes was fetched at the top-level CMakeLists.txt and NAPI_JAVASCRIPT # (bigint::exponentiate, regex::parseRegex, hbc::compileEvalModule, # llvh::raw_ostream, platform_unicode::normalize, ...) which # `hermesvm_a` bundles together for us. - set(LINK_LIBRARIES ${LINK_LIBRARIES} - PRIVATE hermesNapi - PRIVATE hermesvm_a) + set(NAPI_HERMES_LINK_LIBRARIES + hermesNapi + hermesvm_a) # `hermes_napi.h` lives in Hermes's `API/napi/` source directory and # is NOT installed alongside the other `include/hermes/napi/` @@ -261,11 +276,61 @@ Make sure Hermes was fetched at the top-level CMakeLists.txt and NAPI_JAVASCRIPT message(STATUS "Selected ${NAPI_JAVASCRIPT_ENGINE}") endif() -add_library(napi ${SOURCES}) +# On Android, native addons are dlopen'd as standalone .node modules and resolve their napi_* imports +# from a shared napi at load time -- bionic will not surface a statically-linked host's napi to a +# dlopen'd module, so the host and every addon must share a single libnapi.so. Default napi to a +# shared library on Android so that model works out of the box; an integrator who wants a static napi +# (e.g. for size/packaging) can override with -DJSR_NAPI_SHARED=OFF. The option defaults OFF on other +# platforms, where napi keeps following the project's default library type (i.e. honors +# BUILD_SHARED_LIBS). +set(JSR_NAPI_SHARED_DEFAULT OFF) +if(ANDROID) + set(JSR_NAPI_SHARED_DEFAULT ON) +endif() +option(JSR_NAPI_SHARED "Build napi as a shared library (libnapi.so)" ${JSR_NAPI_SHARED_DEFAULT}) + +if(JSR_NAPI_SHARED) + add_library(napi SHARED ${SOURCES}) +else() + add_library(napi ${SOURCES}) +endif() target_include_directories(napi ${INCLUDE_DIRECTORIES}) target_link_libraries(napi ${LINK_LIBRARIES}) +if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes") + if(JSR_NAPI_SHARED) + # hermesNapi is a static archive. When it is folded into libnapi.so, + # the linker would normally extract only the objects referenced by + # env_hermes.cc, leaving most exported napi_* entry points out of the + # shared library. Android addons then fail to resolve even core APIs + # such as napi_create_arraybuffer and napi_wrap. Include the complete + # archive so the shared library provides the full Node-API surface. + if(MSVC) + target_link_libraries(napi PRIVATE ${NAPI_HERMES_LINK_LIBRARIES}) + target_link_options(napi PRIVATE "/WHOLEARCHIVE:$") + else() + target_link_libraries(napi PRIVATE + "-Wl,--whole-archive" + hermesNapi + "-Wl,--no-whole-archive" + hermesvm_a) + endif() + else() + target_link_libraries(napi PRIVATE ${NAPI_HERMES_LINK_LIBRARIES}) + endif() +endif() + +if(JSR_JSC_HAS_BIGINT_TYPED_ARRAYS) + target_compile_definitions(napi PRIVATE JSR_JSC_HAS_BIGINT_TYPED_ARRAYS=1) +endif() + +# Expose the selected engine as a compile definition so engine-agnostic consumers (the node_lite test +# harness) can branch on engine capability instead of guessing from the platform +# (__APPLE__ == JSC / __ANDROID__ == V8 breaks Android-JSC, Linux-V8, Windows-Chakra, ...). +string(TOUPPER "${NAPI_JAVASCRIPT_ENGINE}" NAPI_ENGINE_UPPER) +target_compile_definitions(napi PUBLIC JSR_NAPI_ENGINE_${NAPI_ENGINE_UPPER}) + if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes") # Apply Hermes-specific warning suppressions ONLY to env_hermes.cc so # they don't relax the rules for the rest of the napi sources. diff --git a/Core/Node-API/Include/Engine/Hermes/napi/env.h b/Core/Node-API/Include/Engine/Hermes/napi/env.h index ac806a4b..3feca679 100644 --- a/Core/Node-API/Include/Engine/Hermes/napi/env.h +++ b/Core/Node-API/Include/Engine/Hermes/napi/env.h @@ -30,4 +30,7 @@ namespace Napi // top-level dispatch. Equivalent engines (V8, Chakra) auto-drain // microtasks at scope exit; Hermes requires an explicit drainJobs(). void DrainJobs(Napi::Env env); + + // Force a collection for conformance tests that expose global.gc(). + void CollectGarbage(Napi::Env env); } diff --git a/Core/Node-API/Include/Shared/napi/js_native_api.h b/Core/Node-API/Include/Shared/napi/js_native_api.h index 961b30f2..a4f79bdb 100644 --- a/Core/Node-API/Include/Shared/napi/js_native_api.h +++ b/Core/Node-API/Include/Shared/napi/js_native_api.h @@ -3,7 +3,7 @@ // [BABYLON-NATIVE-ADDITION] #ifndef NAPI_VERSION -#define NAPI_VERSION 5 +#define NAPI_VERSION 7 #endif // This file needs to be compatible with C compilers. diff --git a/Core/Node-API/Include/Shared/napi/js_native_api_types.h b/Core/Node-API/Include/Shared/napi/js_native_api_types.h index bea78ecd..4a6cbc33 100644 --- a/Core/Node-API/Include/Shared/napi/js_native_api_types.h +++ b/Core/Node-API/Include/Shared/napi/js_native_api_types.h @@ -3,7 +3,7 @@ // [BABYLON-NATIVE-ADDITION] #ifndef NAPI_VERSION -#define NAPI_VERSION 5 +#define NAPI_VERSION 7 #endif // This file needs to be compatible with C compilers. diff --git a/Core/Node-API/Include/Shared/napi/napi-inl.h b/Core/Node-API/Include/Shared/napi/napi-inl.h index 338a7f0a..3ac68171 100644 --- a/Core/Node-API/Include/Shared/napi/napi-inl.h +++ b/Core/Node-API/Include/Shared/napi/napi-inl.h @@ -1986,13 +1986,13 @@ inline size_t ArrayBuffer::ByteLength() const { // [BABYLON-NATIVE-ADDITION] inline void ArrayBuffer::EnsureInfo() const { - // The ArrayBuffer instance may have been constructed from a napi_value whose - // length/data are not yet known. Fetch and cache these values just once, - // since they can never change during the lifetime of the ArrayBuffer. - if (_data == nullptr) { - napi_status status = napi_get_arraybuffer_info(_env, _value, &_data, &_length); - NAPI_THROW_IF_FAILED_VOID(_env, status); - } + // Detachment can change both the backing pointer and byte length, including + // when JavaScript transfers the buffer without going through this wrapper. + // Re-query Node-API on every observation instead of returning stale cached + // metadata. New()/the private constructor still seed these fields so callers + // that never observe the buffer do not pay an extra query. + napi_status status = napi_get_arraybuffer_info(_env, _value, &_data, &_length); + NAPI_THROW_IF_FAILED_VOID(_env, status); } #if NAPI_VERSION >= 7 @@ -2006,6 +2006,8 @@ inline bool ArrayBuffer::IsDetached() const { inline void ArrayBuffer::Detach() { napi_status status = napi_detach_arraybuffer(_env, _value); NAPI_THROW_IF_FAILED_VOID(_env, status); + _data = nullptr; + _length = 0; } #endif // NAPI_VERSION >= 7 diff --git a/Core/Node-API/Include/Shared/napi/napi.h b/Core/Node-API/Include/Shared/napi/napi.h index 24b044eb..c702ca41 100644 --- a/Core/Node-API/Include/Shared/napi/napi.h +++ b/Core/Node-API/Include/Shared/napi/napi.h @@ -9,7 +9,7 @@ #define NODE_ADDON_API_DISABLE_NODE_SPECIFIC #endif #ifndef NAPI_VERSION -#define NAPI_VERSION 5 +#define NAPI_VERSION 7 #endif #ifndef NAPI_HAS_THREADS #define NAPI_HAS_THREADS 0 diff --git a/Core/Node-API/Source/env_hermes.cc b/Core/Node-API/Source/env_hermes.cc index c3a97db0..98d865ce 100644 --- a/Core/Node-API/Source/env_hermes.cc +++ b/Core/Node-API/Source/env_hermes.cc @@ -26,7 +26,7 @@ // `hermes_run_script` directly — so the linker is never asked to find // the 4-arg symbol). // -// We keep `NAPI_VERSION` at the shared default (5) so the inline wrappers +// We keep `NAPI_VERSION` at the shared default (7) so the inline wrappers // in napi-inl.h that target newer NAPI revisions (e.g. // `Env::GetModuleFileName` which calls `node_api_get_module_file_name`) // aren't pulled in — they would reference symbols absent from our shared @@ -166,6 +166,15 @@ namespace Napi (void)runtime->drainJobs(); } + void CollectGarbage(Napi::Env env) + { + hermes::vm::Runtime* runtime = LookupRuntime(env); + if (runtime != nullptr) + { + runtime->collect("node_lite explicit collection"); + } + } + Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl) { napi_env env_ptr{env}; diff --git a/Core/Node-API/Source/env_quickjs.cc b/Core/Node-API/Source/env_quickjs.cc index 382fe86c..4c6a7f34 100644 --- a/Core/Node-API/Source/env_quickjs.cc +++ b/Core/Node-API/Source/env_quickjs.cc @@ -65,6 +65,20 @@ namespace Napi napi_env env_ptr{env}; if (env_ptr) { + // Node-API instance data belongs to the environment rather than a + // JS object. Finalize it while both the context and env are still + // valid; user finalizers are permitted to call Node-API. + if (env_ptr->instance_data_finalize != nullptr) + { + auto finalize = env_ptr->instance_data_finalize; + void* data = env_ptr->instance_data; + void* hint = env_ptr->instance_data_finalize_hint; + env_ptr->instance_data = nullptr; + env_ptr->instance_data_finalize = nullptr; + env_ptr->instance_data_finalize_hint = nullptr; + finalize(env_ptr, data, hint); + } + // Release every strong napi_ref still outstanding. This mirrors // the V8 impl (napi_env__::DeleteMe) and is essential on QuickJS: // any surviving strong ref pins a JS value from outside the GC diff --git a/Core/Node-API/Source/js_native_api_chakra.cc b/Core/Node-API/Source/js_native_api_chakra.cc index 67066d92..1989e4b9 100644 --- a/Core/Node-API/Source/js_native_api_chakra.cc +++ b/Core/Node-API/Source/js_native_api_chakra.cc @@ -2470,6 +2470,79 @@ napi_status napi_run_script(napi_env env, return napi_ok; } +// === N-API v6 / v7 === +// +// napi_set_instance_data / napi_get_instance_data (v6): per-env data slot, finalized at env teardown +// by ~napi_env__ (see js_native_api_chakra.h). +napi_status napi_set_instance_data(napi_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint) { + CHECK_ENV(env); + env->instance_data = data; + env->instance_data_finalize_cb = finalize_cb; + env->instance_data_finalize_hint = finalize_hint; + return napi_ok; +} + +napi_status napi_get_instance_data(napi_env env, void** data) { + CHECK_ENV(env); + CHECK_ARG(env, data); + *data = env->instance_data; + return napi_ok; +} + +// BigInt (v6): the Win10 OS edge-mode Chakra (jsrt) predates BigInt and exposes no JsBigInt* API, so +// there is no value-preserving fallback. Per the Node-API feature-detection-by-exception pattern, throw +// a JS-catchable error tagged "ENOTSUP" (so JS land can detect + polyfill) and return a pending +// exception rather than silently failing. (ChakraCore added BigInt behind a flag, but the OS Chakra +// this backend targets did not ship it.) +static napi_status napi_bigint_not_supported(napi_env env) { + CHECK_ENV(env); + CHECK_NAPI(napi_throw_error( + env, "ENOTSUP", + "BigInt is not supported by the underlying JavaScript engine (Chakra).")); + return napi_set_last_error(env, napi_pending_exception); +} + +napi_status napi_create_bigint_int64(napi_env env, int64_t value, napi_value* result) { + return napi_bigint_not_supported(env); +} + +napi_status napi_create_bigint_uint64(napi_env env, uint64_t value, napi_value* result) { + return napi_bigint_not_supported(env); +} + +napi_status napi_create_bigint_words(napi_env env, + int sign_bit, + size_t word_count, + const uint64_t* words, + napi_value* result) { + return napi_bigint_not_supported(env); +} + +napi_status napi_get_value_bigint_int64(napi_env env, + napi_value value, + int64_t* result, + bool* lossless) { + return napi_bigint_not_supported(env); +} + +napi_status napi_get_value_bigint_uint64(napi_env env, + napi_value value, + uint64_t* result, + bool* lossless) { + return napi_bigint_not_supported(env); +} + +napi_status napi_get_value_bigint_words(napi_env env, + napi_value value, + int* sign_bit, + size_t* word_count, + uint64_t* words) { + return napi_bigint_not_supported(env); +} + napi_status napi_add_finalizer(napi_env env, napi_value js_object, void* native_object, diff --git a/Core/Node-API/Source/js_native_api_chakra.h b/Core/Node-API/Source/js_native_api_chakra.h index 420cdaaa..b68fb9bc 100644 --- a/Core/Node-API/Source/js_native_api_chakra.h +++ b/Core/Node-API/Source/js_native_api_chakra.h @@ -15,7 +15,19 @@ struct napi_env__ { JsPropertyIdRef wrap_property_id = JS_INVALID_REFERENCE; + // napi_set_instance_data / napi_get_instance_data (N-API v6). + void* instance_data = nullptr; + napi_finalize instance_data_finalize_cb = nullptr; + void* instance_data_finalize_hint = nullptr; + const std::thread::id thread_id{std::this_thread::get_id()}; + + ~napi_env__() { + // Run the instance-data finalizer at env teardown (env_chakra.cc deletes the env), matching V8/JSC. + if (instance_data_finalize_cb != nullptr) { + instance_data_finalize_cb(this, instance_data, instance_data_finalize_hint); + } + } }; #define RETURN_STATUS_IF_FALSE(env, condition, status) \ diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index 2c9e8e80..47c6a213 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -1,7 +1,10 @@ #include "js_native_api_javascriptcore.h" #include #include +#include #include +#include +#include #include #include #include @@ -446,7 +449,13 @@ namespace { return napi_set_last_error(env, napi_generic_failure); } - JSObjectRef function{JSObjectMakeFunctionWithCallback(env->context, JSString(utf8name), CallAsFunction)}; + // `length` is a byte count and the name need not be null-terminated. + // The Node-API CTS deliberately passes "Name_extra" with length 5 and + // expects Function#name to be "Name_". + JSString functionName{ + utf8name != nullptr ? utf8name : "", + utf8name != nullptr ? length : 0}; + JSObjectRef function{JSObjectMakeFunctionWithCallback(env->context, functionName, CallAsFunction)}; JSObjectRef sentinel{JSObjectMake(env->context, info->_class, info)}; CHECK_NAPI(NativeInfo::Link(env, function, sentinel)); @@ -561,16 +570,29 @@ namespace { static void Finalize(JSObjectRef object) { T* info = Get(object); assert(info->Type() == TType); - for (const FinalizerT& finalizer : info->_finalizers) { - finalizer(info); + if (info->Env()->defer_finalizer_if_requested([info]() { + RunFinalizers(info); + })) { + // JSC invokes JSClass finalizers while the heap is collecting, where + // re-entering JavaScript is unsafe. Node-API finalizers are allowed to + // call back through Node-API, so the explicit test-GC path drains them + // immediately after the synchronous collection returns. + return; } - delete info; + RunFinalizers(info); } napi_env _env; void* _data{}; std::vector _finalizers{}; JSClassRef _class{}; + + static void RunFinalizers(T* info) { + for (const FinalizerT& finalizer : info->_finalizers) { + finalizer(info); + } + delete info; + } }; class ExternalInfo: public BaseInfoT { @@ -666,10 +688,20 @@ namespace { return napi_ok; } + bool IsWrapped() const { + return _wrapped; + } + + void IsWrapped(bool wrapped) { + _wrapped = wrapped; + } + private: WrapperInfo(napi_env env) : BaseInfoT{env, "Native (Wrapper)"} { } + + bool _wrapped{}; }; class ExternalArrayBufferInfo { @@ -740,6 +772,9 @@ struct napi_ref__ { CHECK_NAPI(ReferenceInfo::GetObjectId(env, _value, &_objectId)); if (_objectId == 0) { CHECK_NAPI(ReferenceInfo::Initialize(env, _value, [value = _value](ReferenceInfo* info) { + if (info->Env()->shutting_down) { + return; + } auto entry{info->Env()->active_ref_values.find(value)}; // NOTE: The finalizer callback is actually on a "sentinel" JS object that is linked to the // actual JS object we are trying to track. This means it is possible for the tracked object @@ -832,6 +867,20 @@ void napi_env__::deinit_refs() { } } +void napi_env__::delete_remaining_refs() { + // Addons may delete wrap-owned references from their finalizers while + // JSGlobalContextRelease tears down the heap. Keep every reference tracked + // until that teardown has run, then reclaim only the references the addon + // left behind. Deleting strong references before context teardown would + // make a finalizer's napi_delete_reference double-free them; never deleting + // the leftovers leaks references whose finalizer deliberately does not. + assert(strong_refs.empty()); + for (napi_ref ref : refs) { + delete ref; + } + refs.clear(); +} + void napi_env__::init_symbol(JSValueRef &symbol, const char *description) { symbol = JSValueMakeSymbol(context, JSString(description)); JSValueProtect(context, symbol); @@ -841,6 +890,44 @@ void napi_env__::deinit_symbol(JSValueRef symbol) { JSValueUnprotect(context, symbol); } +void napi_env__::init_function_prototype_call() { + // Capture the canonical Function.prototype.call once, at env init, so napi_call_function does not + // depend on a target function's own (user-overridable) "call" property. + JSObjectRef global = JSContextGetGlobalObject(context); + JSValueRef function_ctor = JSObjectGetProperty(context, global, JSString("Function"), nullptr); + JSObjectRef function_ctor_obj = JSValueToObject(context, function_ctor, nullptr); + JSValueRef prototype = JSObjectGetProperty(context, function_ctor_obj, JSString("prototype"), nullptr); + JSObjectRef prototype_obj = JSValueToObject(context, prototype, nullptr); + function_prototype_call = JSObjectGetProperty(context, prototype_obj, JSString("call"), nullptr); + JSValueProtect(context, function_prototype_call); +} + +void napi_env__::init_is_bigint_function() { + // Cache a `typeof v === 'bigint'` predicate so napi_typeof can detect BigInt on JSC builds whose C + // API does not expose kJSTypeBigInt (e.g. jsc-android). On macOS 15+/iOS 18+ napi_typeof uses the + // kJSTypeBigInt fast path and this predicate is unused. + JSStringRef script = JSStringCreateWithUTF8CString("(function (v) { return typeof v === 'bigint'; })"); + JSValueRef exception = nullptr; + is_bigint_function = JSEvaluateScript(context, script, nullptr, nullptr, 0, &exception); + JSStringRelease(script); + if (is_bigint_function != nullptr) { + JSValueProtect(context, is_bigint_function); + } +} + +void napi_env__::init_bigint_supported() { + // Detect BigInt once at env init. jsc-android (~2020) ships without BigInt -- its parser even rejects + // `0n` literals -- so the eval/C-API BigInt paths must feature-detect-fail there. `typeof BigInt` + // parses on every JSC (no literal syntax), and BigInt(1) confirms the global is actually functional. + JSStringRef script = JSStringCreateWithUTF8CString( + "(function () { try { return typeof BigInt === 'function' && typeof BigInt(1) === 'bigint'; }" + " catch (e) { return false; } })()"); + JSValueRef exception = nullptr; + JSValueRef result = JSEvaluateScript(context, script, nullptr, nullptr, 0, &exception); + JSStringRelease(script); + bigint_supported = (exception == nullptr) && (result != nullptr) && JSValueToBoolean(context, result); +} + // Warning: Keep in-sync with napi_status enum static const char* error_messages[] = { nullptr, @@ -893,6 +980,7 @@ napi_status napi_create_function(napi_env env, void* callback_data, napi_value* result) { CHECK_ENV(env); + CHECK_ARG(env, cb); CHECK_ARG(env, result); CHECK_NAPI(FunctionInfo::Create(env, utf8name, length, cb, callback_data, result)); @@ -1536,7 +1624,6 @@ napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result) CHECK_ARG(env, value); CHECK_ARG(env, result); - // JSC does not support BigInt JSType valueType = JSValueGetType(env->context, ToJSValue(value)); switch (valueType) { case kJSTypeUndefined: *result = napi_undefined; break; @@ -1545,7 +1632,28 @@ napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result) case kJSTypeNumber: *result = napi_number; break; case kJSTypeString: *result = napi_string; break; case kJSTypeSymbol: *result = napi_symbol; break; - default: +#if defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000 + // kJSTypeBigInt is macOS 15+/iOS 18+. On older JSC (e.g. jsc-android) a BigInt falls to the + // default branch; a typeof-based fallback is added with the Android bring-up. + case kJSTypeBigInt: *result = napi_bigint; break; +#endif + default: { +#if !(defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000) + // Older JSC (e.g. jsc-android) does not report kJSTypeBigInt through JSValueGetType, so detect + // BigInt with the cached `typeof v === 'bigint'` predicate before treating value as an object. + if (env->is_bigint_function != nullptr) { + JSValueRef arg = ToJSValue(value); + JSValueRef exception = nullptr; + JSObjectRef predicate = JSValueToObject(env->context, env->is_bigint_function, &exception); + if (exception == nullptr && predicate != nullptr) { + JSValueRef matched = JSObjectCallAsFunction(env->context, predicate, nullptr, 1, &arg, &exception); + if (exception == nullptr && JSValueToBoolean(env->context, matched)) { + *result = napi_bigint; + break; + } + } + } +#endif JSObjectRef object{ToJSObject(env, value)}; if (JSObjectIsFunction(env->context, object)) { *result = napi_function; @@ -1558,6 +1666,7 @@ napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result) } } break; + } } return napi_ok; @@ -1642,14 +1751,27 @@ napi_status napi_call_function(napi_env env, CHECK_ARG(env, argv); } + JSObjectRef function_object = ToJSObject(env, func); + + std::vector call_args(argc + 1); + call_args[0] = ToJSValue(recv); + for (size_t i = 0; i < argc; ++i) { + call_args[i + 1] = ToJSValue(argv[i]); + } + JSValueRef exception{}; - JSValueRef return_value{JSObjectCallAsFunction( - env->context, - ToJSObject(env, func), - JSValueIsUndefined(env->context, ToJSValue(recv)) ? nullptr : ToJSObject(env, recv), - argc, - ToJSValues(argv), - &exception)}; + // Invoke through the canonical Function.prototype.call (captured at env init), not the target's own + // "call" property -- user code could override func.call and change native call behavior. + JSObjectRef call_object = + JSValueToObject(env->context, env->function_prototype_call, &exception); + CHECK_JSC(env, exception); + + JSValueRef return_value{JSObjectCallAsFunction(env->context, + call_object, + function_object, + call_args.size(), + call_args.data(), + &exception)}; CHECK_JSC(env, exception); if (result != nullptr) { @@ -1666,6 +1788,302 @@ napi_status napi_get_global(napi_env env, napi_value* result) { return napi_ok; } +// N-API v6: per-environment instance data. The finalizer (if any) runs when the env is torn down +// (see ~napi_env__). +napi_status napi_set_instance_data(napi_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint) { + CHECK_ENV(env); + env->instance_data = data; + env->instance_data_finalize_cb = finalize_cb; + env->instance_data_finalize_hint = finalize_hint; + return napi_ok; +} + +napi_status napi_get_instance_data(napi_env env, void** data) { + CHECK_ENV(env); + CHECK_ARG(env, data); + *data = env->instance_data; + return napi_ok; +} + +//============================================================================= +// N-API v6 BigInt + v7 ArrayBuffer detach (JavaScriptCore) +// +// The JSC public C API only ships BigInt create functions on macOS 15+/iOS 18+, and ships no +// ArrayBuffer-detach call at all. The JS-level BigInt global, however, has been in JSC since ~2018, +// and ArrayBuffer.prototype.transfer() (ES2024) detaches a buffer -- both reachable through the +// public C API. So: BigInt uses the native C API where available and the JS BigInt global otherwise; +// detach uses transfer(). Extraction (get_value_bigint_*) is always string/round-trip based, since +// the C API has no BigInt readers. +//============================================================================= + +namespace { + +// Call BigInt.asIntN(64, value) / BigInt.asUintN(64, value); yields the low 64 bits as a BigInt. +// Reported when the underlying JSC build has no BigInt (e.g. jsc-android ~2020). Matches the Chakra +// backend: a JS-catchable ENOTSUP error per the Node-API feature-detection pattern. +napi_status napi_bigint_unsupported(napi_env env) { + CHECK_NAPI(napi_throw_error(env, "ENOTSUP", + "BigInt is not supported by the underlying JavaScript engine.")); + return napi_set_last_error(env, napi_pending_exception); +} + +napi_status BigIntLow64(napi_env env, napi_value value, const char* method, JSValueRef* low) { + if (!env->bigint_supported) { + return napi_bigint_unsupported(env); + } + JSObjectRef global = JSContextGetGlobalObject(env->context); + JSValueRef exception{}; + JSValueRef bigIntCtor = JSObjectGetProperty(env->context, global, JSString("BigInt"), &exception); + CHECK_JSC(env, exception); + JSObjectRef bigIntObj = JSValueToObject(env->context, bigIntCtor, &exception); + CHECK_JSC(env, exception); + JSValueRef fn = JSObjectGetProperty(env->context, bigIntObj, JSString(method), &exception); + CHECK_JSC(env, exception); + JSObjectRef fnObj = JSValueToObject(env->context, fn, &exception); + CHECK_JSC(env, exception); + JSValueRef args[2] = {JSValueMakeNumber(env->context, 64), ToJSValue(value)}; + *low = JSObjectCallAsFunction(env->context, fnObj, bigIntObj, 2, args, &exception); + CHECK_JSC(env, exception); + return napi_ok; +} + +// value.toString(radix) for a BigInt primitive (boxes the primitive, then calls toString). +napi_status BigIntToString(napi_env env, napi_value value, int radix, std::string* out) { + if (!env->bigint_supported) { + return napi_bigint_unsupported(env); + } + JSValueRef exception{}; + JSObjectRef boxed = JSValueToObject(env->context, ToJSValue(value), &exception); + CHECK_JSC(env, exception); + JSValueRef toString = JSObjectGetProperty(env->context, boxed, JSString("toString"), &exception); + CHECK_JSC(env, exception); + JSObjectRef toStringFn = JSValueToObject(env->context, toString, &exception); + CHECK_JSC(env, exception); + JSValueRef radixArg = JSValueMakeNumber(env->context, radix); + JSValueRef str = JSObjectCallAsFunction(env->context, toStringFn, boxed, 1, &radixArg, &exception); + CHECK_JSC(env, exception); + JSStringRef jsStr = JSValueToStringCopy(env->context, str, &exception); + CHECK_JSC(env, exception); + size_t cap = JSStringGetMaximumUTF8CStringSize(jsStr); + out->resize(cap); + size_t written = JSStringGetUTF8CString(jsStr, out->data(), cap); + JSStringRelease(jsStr); + if (written > 0) out->resize(written - 1); // drop the trailing NUL + return napi_ok; +} + +// Construct a BigInt from a (controlled) JS source expression; used as the create path where the C +// API is unavailable. Only embeds numeric/hex literals -> no injection surface. +napi_status BigIntFromExpr(napi_env env, const std::string& expr, napi_value* result) { + if (!env->bigint_supported) { + return napi_bigint_unsupported(env); + } + JSStringRef src = JSStringCreateWithUTF8CString(expr.c_str()); + JSValueRef exception{}; + JSValueRef big = JSEvaluateScript(env->context, src, nullptr, nullptr, 0, &exception); + JSStringRelease(src); + CHECK_JSC(env, exception); + *result = ToNapi(big); + return napi_ok; +} + +} // namespace + +napi_status napi_create_bigint_int64(napi_env env, int64_t value, napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); +#if defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000 + if (__builtin_available(macOS 15.0, *)) { + JSValueRef exception{}; + JSValueRef big = JSBigIntCreateWithInt64(env->context, value, &exception); + CHECK_JSC(env, exception); + *result = ToNapi(big); + return napi_ok; + } +#endif + return BigIntFromExpr(env, "BigInt(\"" + std::to_string(value) + "\")", result); +} + +napi_status napi_create_bigint_uint64(napi_env env, uint64_t value, napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); +#if defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000 + if (__builtin_available(macOS 15.0, *)) { + JSValueRef exception{}; + JSValueRef big = JSBigIntCreateWithUInt64(env->context, value, &exception); + CHECK_JSC(env, exception); + *result = ToNapi(big); + return napi_ok; + } +#endif + return BigIntFromExpr(env, "BigInt(\"" + std::to_string(value) + "\")", result); +} + +napi_status napi_create_bigint_words(napi_env env, + int sign_bit, + size_t word_count, + const uint64_t* words, + napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + // Match Node/V8 limits *before* touching the (possibly under-sized) words buffer: beyond INT_MAX + // it's napi_invalid_arg; beyond the engine's BigInt size limit it's a RangeError. + if (word_count > static_cast(INT_MAX)) { + return napi_set_last_error(env, napi_invalid_arg); + } + if (word_count > (static_cast(1) << 24)) { // ~ kMaxBigIntLengthBits / 64 + napi_throw_range_error(env, nullptr, "Maximum BigInt size exceeded"); + return napi_set_last_error(env, napi_pending_exception); + } + if (word_count > 0) { + CHECK_ARG(env, words); + } + // Big-endian hex from the little-endian words (words[0] is the least-significant 64 bits). + std::string hex; + for (size_t i = word_count; i-- > 0;) { + char buf[17]; + snprintf(buf, sizeof(buf), "%016llx", static_cast(words[i])); + hex += buf; + } + if (hex.empty()) { + hex = "0"; + } + std::string expr = (sign_bit ? "-BigInt(\"0x" : "BigInt(\"0x") + hex + "\")"; + return BigIntFromExpr(env, expr, result); +} + +napi_status napi_get_value_bigint_int64(napi_env env, + napi_value value, + int64_t* result, + bool* lossless) { + CHECK_ENV(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + CHECK_ARG(env, lossless); + JSValueRef low{}; + CHECK_NAPI(BigIntLow64(env, value, "asIntN", &low)); + std::string decimal; + CHECK_NAPI(BigIntToString(env, ToNapi(low), 10, &decimal)); + *result = static_cast(strtoll(decimal.c_str(), nullptr, 10)); + *lossless = JSValueIsStrictEqual(env->context, ToJSValue(value), low); + return napi_ok; +} + +napi_status napi_get_value_bigint_uint64(napi_env env, + napi_value value, + uint64_t* result, + bool* lossless) { + CHECK_ENV(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + CHECK_ARG(env, lossless); + JSValueRef low{}; + CHECK_NAPI(BigIntLow64(env, value, "asUintN", &low)); + std::string decimal; + CHECK_NAPI(BigIntToString(env, ToNapi(low), 10, &decimal)); + *result = static_cast(strtoull(decimal.c_str(), nullptr, 10)); + *lossless = JSValueIsStrictEqual(env->context, ToJSValue(value), low); + return napi_ok; +} + +napi_status napi_get_value_bigint_words(napi_env env, + napi_value value, + int* sign_bit, + size_t* word_count, + uint64_t* words) { + CHECK_ENV(env); + CHECK_ARG(env, value); + CHECK_ARG(env, word_count); + std::string hex; + CHECK_NAPI(BigIntToString(env, value, 16, &hex)); + bool negative = !hex.empty() && hex[0] == '-'; + std::string digits = negative ? hex.substr(1) : hex; + if (digits == "0") { + digits.clear(); + } + size_t needed = (digits.length() + 15) / 16; + if (words == nullptr) { + *word_count = needed; + return napi_ok; + } + if (sign_bit != nullptr) { + *sign_bit = negative ? 1 : 0; + } + size_t capacity = *word_count; + *word_count = needed; + for (size_t w = 0; w < needed && w < capacity; ++w) { + size_t end = digits.length() - w * 16; + size_t start = end >= 16 ? end - 16 : 0; + std::string chunk = digits.substr(start, end - start); + words[w] = static_cast(strtoull(chunk.c_str(), nullptr, 16)); + } + return napi_ok; +} + +napi_status napi_detach_arraybuffer(napi_env env, napi_value arraybuffer) { + CHECK_ENV(env); + CHECK_ARG(env, arraybuffer); + JSObjectRef ab = ToJSObject(env, arraybuffer); + JSValueRef exception{}; + JSValueRef transfer = JSObjectGetProperty(env->context, ab, JSString("transfer"), &exception); + CHECK_JSC(env, exception); + if (!JSValueIsObject(env->context, transfer)) { + // ArrayBuffer.prototype.transfer() (ES2024) is the only public-API detach path; if the engine + // predates it (older jsc-android), throw a catchable ENOTSUP so JS land can polyfill. + napi_throw_error(env, "ENOTSUP", + "ArrayBuffer detach is not supported by the underlying JavaScript engine."); + return napi_set_last_error(env, napi_pending_exception); + } + JSObjectRef transferFn = JSValueToObject(env->context, transfer, &exception); + CHECK_JSC(env, exception); + JSObjectCallAsFunction(env->context, transferFn, ab, 0, nullptr, &exception); // detaches `ab` + CHECK_JSC(env, exception); + + // Some JSC C-API builds keep an ArrayBuffer attached after its backing + // pointer has been exposed to native code. Node-API permits engines to impose + // detachability conditions, but a successful status must mean the requested + // detach actually happened. + bool detached = false; + CHECK_NAPI(napi_is_detached_arraybuffer(env, arraybuffer, &detached)); + if (!detached) { + return napi_set_last_error(env, napi_detachable_arraybuffer_expected); + } + return napi_ok; +} + +napi_status napi_is_detached_arraybuffer(napi_env env, napi_value arraybuffer, bool* result) { + CHECK_ENV(env); + CHECK_ARG(env, arraybuffer); + CHECK_ARG(env, result); + JSObjectRef ab = ToJSObject(env, arraybuffer); + JSValueRef exception{}; + JSValueRef detached = JSObjectGetProperty(env->context, ab, JSString("detached"), &exception); + CHECK_JSC(env, exception); + if (JSValueIsBoolean(env->context, detached)) { + *result = JSValueToBoolean(env->context, detached); + } else { + // Some JSC C-API releases expose transfer() before the standards-track + // `detached` getter, and may keep returning the former backing-store + // address after transfer. ArrayBuffer.prototype.slice performs the + // ECMAScript detached-buffer check before copying; a zero-argument call is + // non-mutating and still succeeds for a valid attached zero-length buffer. + JSValueRef sliceException{}; + JSValueRef slice = JSObjectGetProperty(env->context, ab, JSString("slice"), &sliceException); + CHECK_JSC(env, sliceException); + if (!JSValueIsObject(env->context, slice)) { + return napi_set_last_error(env, napi_generic_failure); + } + JSObjectRef sliceFn = JSValueToObject(env->context, slice, &sliceException); + CHECK_JSC(env, sliceException); + JSObjectCallAsFunction(env->context, sliceFn, ab, 0, nullptr, &sliceException); + *result = sliceException != nullptr; + } + return napi_ok; +} + napi_status napi_throw(napi_env env, napi_value error) { CHECK_ENV(env); napi_status status{napi_set_exception(env, ToJSValue(error))}; @@ -1966,8 +2384,9 @@ napi_status napi_wrap(napi_env env, WrapperInfo* info{}; CHECK_NAPI(WrapperInfo::Wrap(env, js_object, &info)); - RETURN_STATUS_IF_FALSE(env, info->Data() == nullptr, napi_invalid_arg); + RETURN_STATUS_IF_FALSE(env, !info->IsWrapped(), napi_invalid_arg); + info->IsWrapped(true); info->Data(native_object); if (finalize_cb != nullptr) { @@ -1989,7 +2408,7 @@ napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) { WrapperInfo* info{}; CHECK_NAPI(WrapperInfo::Unwrap(env, js_object, &info)); - RETURN_STATUS_IF_FALSE(env, info != nullptr && info->Data() != nullptr, napi_invalid_arg); + RETURN_STATUS_IF_FALSE(env, info != nullptr && info->IsWrapped(), napi_invalid_arg); *result = info->Data(); return napi_ok; @@ -1999,17 +2418,16 @@ napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) CHECK_ENV(env); CHECK_ARG(env, js_object); - // REVIEW: Should we remove the wrapper if we are removing finalizers anyway? - WrapperInfo* info{}; CHECK_NAPI(WrapperInfo::Unwrap(env, js_object, &info)); - RETURN_STATUS_IF_FALSE(env, info != nullptr && info->Data() != nullptr, napi_invalid_arg); + RETURN_STATUS_IF_FALSE(env, info != nullptr && info->IsWrapped(), napi_invalid_arg); if (result) { *result = info->Data(); } + info->IsWrapped(false); info->Data(nullptr); info->RemoveFinalizers(); @@ -2047,12 +2465,35 @@ napi_status napi_create_reference(napi_env env, CHECK_ARG(env, value); CHECK_ARG(env, result); + // References are backed by metadata attached to a JavaScript object. + // Reject unsupported primitives before ReferenceInfo calls ToJSObject(): + // that conversion asserts, while node-addon-api deliberately probes this + // API and wraps a non-object pending exception when it is rejected. + // + // This is especially important for JavaScriptCore's watchdog termination + // exception. WebKit represents it as the string "JavaScript execution + // terminated.", so attempting to persist it as an object would abort the + // host while stopping a tight-loop Worker. + // https://github.com/WebKit/WebKit/blob/46327f724be09f4b27c1c72b9dd083ca34d6abcc/Source/JavaScriptCore/API/tests/ExecutionTimeLimitTest.cpp + napi_valuetype value_type{}; + CHECK_NAPI(napi_typeof(env, value, &value_type)); + if (value_type != napi_object && + value_type != napi_function && + value_type != napi_external) { + return napi_set_last_error(env, napi_object_expected); + } + napi_ref__* ref{new napi_ref__{}}; if (ref == nullptr) { return napi_set_last_error(env, napi_generic_failure); } - ref->init(env, value, initial_refcount); + const napi_status status{ref->init(env, value, initial_refcount)}; + if (status != napi_ok) { + delete ref; + return status; + } + env->track_ref(ref); *result = ref; return napi_ok; @@ -2064,6 +2505,7 @@ napi_status napi_delete_reference(napi_env env, napi_ref ref) { CHECK_ENV(env); CHECK_ARG(env, ref); + env->untrack_ref(ref); ref->deinit(env); delete ref; @@ -2286,6 +2728,22 @@ napi_status napi_get_arraybuffer_info(napi_env env, CHECK_ENV(env); CHECK_ARG(env, arraybuffer); + // JSObjectGetArrayBufferBytesPtr may keep returning the former backing-store + // address after ArrayBuffer.prototype.transfer() detaches a buffer. Normalize + // the observable Node-API state before consulting those JavaScriptCore C APIs + // so wrappers cannot retain a dangling pointer. + bool detached = false; + CHECK_NAPI(napi_is_detached_arraybuffer(env, arraybuffer, &detached)); + if (detached) { + if (data != nullptr) { + *data = nullptr; + } + if (byte_length != nullptr) { + *byte_length = 0; + } + return napi_ok; + } + JSValueRef exception{}; if (data != nullptr) { @@ -2353,6 +2811,14 @@ napi_status napi_create_typedarray(napi_env env, case napi_float64_array: jsType = kJSTypedArrayTypeFloat64Array; break; +#if defined(JSR_JSC_HAS_BIGINT_TYPED_ARRAYS) + case napi_bigint64_array: + jsType = kJSTypedArrayTypeBigInt64Array; + break; + case napi_biguint64_array: + jsType = kJSTypedArrayTypeBigUint64Array; + break; +#endif default: return napi_set_last_error(env, napi_invalid_arg); } @@ -2416,6 +2882,14 @@ napi_status napi_get_typedarray_info(napi_env env, case kJSTypedArrayTypeFloat64Array: *type = napi_float64_array; break; +#if defined(JSR_JSC_HAS_BIGINT_TYPED_ARRAYS) + case kJSTypedArrayTypeBigInt64Array: + *type = napi_bigint64_array; + break; + case kJSTypedArrayTypeBigUint64Array: + *type = napi_biguint64_array; + break; +#endif default: return napi_set_last_error(env, napi_generic_failure); } diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index da74596e..3d97d3d7 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -3,9 +3,14 @@ #include #include #include +#include #include +#include #include +#include #include +#include +#include #include struct napi_env__ { @@ -14,11 +19,20 @@ struct napi_env__ { napi_extended_error_info last_error{nullptr, nullptr, 0, napi_ok}; std::unordered_map active_ref_values{}; std::list strong_refs{}; + bool shutting_down{false}; + + // napi_set_instance_data / napi_get_instance_data (N-API v6). + void* instance_data{}; + napi_finalize instance_data_finalize_cb{}; + void* instance_data_finalize_hint{}; JSValueRef constructor_info_symbol{}; JSValueRef function_info_symbol{}; JSValueRef reference_info_symbol{}; JSValueRef wrapper_info_symbol{}; + JSValueRef function_prototype_call{}; + JSValueRef is_bigint_function{}; + bool bigint_supported{false}; const std::thread::id thread_id{std::this_thread::get_id()}; @@ -29,15 +43,25 @@ struct napi_env__ { init_symbol(function_info_symbol, "BabylonNative_FunctionInfo"); init_symbol(reference_info_symbol, "BabylonNative_ReferenceInfo"); init_symbol(wrapper_info_symbol, "BabylonNative_WrapperInfo"); + init_function_prototype_call(); + init_is_bigint_function(); + init_bigint_supported(); } ~napi_env__() { + shutting_down = true; + if (instance_data_finalize_cb != nullptr) { + instance_data_finalize_cb(this, instance_data, instance_data_finalize_hint); + } deinit_refs(); + deinit_symbol(is_bigint_function); + deinit_symbol(function_prototype_call); deinit_symbol(wrapper_info_symbol); deinit_symbol(reference_info_symbol); deinit_symbol(function_info_symbol); deinit_symbol(constructor_info_symbol); JSGlobalContextRelease(context); + delete_remaining_refs(); napi_envs.erase(context); } @@ -50,11 +74,58 @@ struct napi_env__ { } } + void set_defer_finalizers(bool defer) { + std::lock_guard lock{deferred_finalizers_mutex}; + defer_finalizers = defer; + } + + template + bool defer_finalizer_if_requested(TCallback&& callback) { + std::lock_guard lock{deferred_finalizers_mutex}; + if (!defer_finalizers) { + return false; + } + deferred_finalizers.emplace_back(std::forward(callback)); + return true; + } + + void drain_deferred_finalizers() { + for (;;) { + std::vector> finalizers; + { + std::lock_guard lock{deferred_finalizers_mutex}; + if (deferred_finalizers.empty()) { + return; + } + finalizers.swap(deferred_finalizers); + } + for (auto& finalizer : finalizers) { + finalizer(); + } + } + } + + void track_ref(napi_ref ref) { + refs.insert(ref); + } + + void untrack_ref(napi_ref ref) { + refs.erase(ref); + } + private: static inline std::unordered_map napi_envs{}; + std::unordered_set refs{}; + std::mutex deferred_finalizers_mutex{}; + bool defer_finalizers{false}; + std::vector> deferred_finalizers{}; void deinit_refs(); + void delete_remaining_refs(); void init_symbol(JSValueRef& symbol, const char* description); + void init_function_prototype_call(); + void init_is_bigint_function(); + void init_bigint_supported(); void deinit_symbol(JSValueRef symbol); }; diff --git a/Core/Node-API/Source/js_native_api_quickjs.cc b/Core/Node-API/Source/js_native_api_quickjs.cc index c9e2823d..a1edfa1d 100644 --- a/Core/Node-API/Source/js_native_api_quickjs.cc +++ b/Core/Node-API/Source/js_native_api_quickjs.cc @@ -10,7 +10,10 @@ #endif #include #include +#include #include +#include +#include #include #include #include @@ -256,6 +259,11 @@ static JSValue Callback(JSContext *ctx, JSValueConst this_val, int argc, JSValue // Reference info for preventing GC - defined in js_native_api_quickjs.h. +struct HandleScopeInfo { + size_t start; + bool escaped; +}; + // Initialize class IDs (allocate once globally, register per runtime) void InitClassIds(JSRuntime* rt) { if (!class_ids_allocated) { @@ -1585,40 +1593,63 @@ napi_status napi_throw(napi_env env, napi_value error) { // Throw into the CURRENT execution context, not the stored context JSContext* targetCtx = env->current_context ? env->current_context : env->context; JS_Throw(targetCtx, JS_DupValue(targetCtx, jsError)); - - return napi_set_last_error(env, napi_pending_exception); + + // Per Node-API, successfully scheduling the exception returns napi_ok. + // Reporting napi_pending_exception here makes node-addon-api's + // Error::ThrowAsJavaScriptException throw a second C++ exception while it is + // already translating the first one. + napi_clear_last_error(env); + return napi_ok; } // Throw error napi_status napi_throw_error(napi_env env, const char* code, const char* msg) { CHECK_ENV(env); - JSValue error = JS_NewError(env->context); + JSContext* targetCtx = env->current_context ? env->current_context : env->context; + JSValue error = JS_NewError(targetCtx); if (msg) { - JS_SetPropertyStr(env->context, error, "message", JS_NewString(env->context, msg)); + JS_SetPropertyStr(targetCtx, error, "message", JS_NewString(targetCtx, msg)); } if (code) { - JS_SetPropertyStr(env->context, error, "code", JS_NewString(env->context, code)); + JS_SetPropertyStr(targetCtx, error, "code", JS_NewString(targetCtx, code)); } - JS_Throw(env->context, error); - return napi_set_last_error(env, napi_pending_exception); + JS_Throw(targetCtx, error); + napi_clear_last_error(env); + return napi_ok; } // Throw type error napi_status napi_throw_type_error(napi_env env, const char* code, const char* msg) { CHECK_ENV(env); - JS_ThrowTypeError(env->context, "%s", msg ? msg : ""); - return napi_set_last_error(env, napi_pending_exception); + JSContext* targetCtx = env->current_context ? env->current_context : env->context; + JS_ThrowTypeError(targetCtx, "%s", msg ? msg : ""); + if (code) { + JSValue error = JS_GetException(targetCtx); + JS_SetPropertyStr(targetCtx, error, "code", JS_NewString(targetCtx, code)); + JS_Throw(targetCtx, error); + } + + napi_clear_last_error(env); + return napi_ok; } // Throw range error napi_status napi_throw_range_error(napi_env env, const char* code, const char* msg) { CHECK_ENV(env); - JS_ThrowRangeError(env->context, "%s", msg ? msg : ""); - return napi_set_last_error(env, napi_pending_exception); + JSContext* targetCtx = env->current_context ? env->current_context : env->context; + JS_ThrowRangeError(targetCtx, "%s", msg ? msg : ""); + if (code) { + JSValue error = JS_GetException(targetCtx); + JS_SetPropertyStr(targetCtx, error, "code", JS_NewString(targetCtx, code)); + JS_Throw(targetCtx, error); + } + + napi_clear_last_error(env); + return napi_ok; } // Create error @@ -1886,7 +1917,8 @@ napi_status napi_open_handle_scope(napi_env env, napi_handle_scope* result) { CHECK_ARG(env, result); env->current_scope_start = env->handle_scope_stack.size(); - *result = reinterpret_cast(env->current_scope_start + 1); + *result = reinterpret_cast( + new HandleScopeInfo{env->current_scope_start, false}); napi_clear_last_error(env); return napi_ok; @@ -1897,7 +1929,8 @@ napi_status napi_close_handle_scope(napi_env env, napi_handle_scope scope) { CHECK_ARG(env, scope); // Free all JSValues created in this scope - size_t scope_start = reinterpret_cast(scope) - 1; + auto* scopeInfo = reinterpret_cast(scope); + size_t scope_start = scopeInfo->start; // Call JS_FreeValue on all values in scope for (size_t i = scope_start; i < env->handle_scope_stack.size(); i++) { @@ -1907,6 +1940,7 @@ napi_status napi_close_handle_scope(napi_env env, napi_handle_scope scope) { // Remove from stack env->handle_scope_stack.resize(scope_start); env->current_scope_start = scope_start; + delete scopeInfo; napi_clear_last_error(env); return napi_ok; @@ -1917,9 +1951,9 @@ napi_status napi_open_escapable_handle_scope(napi_env env, napi_escapable_handle CHECK_ENV(env); CHECK_ARG(env, result); - // Same as regular handle scope for QuickJS env->current_scope_start = env->handle_scope_stack.size(); - *result = reinterpret_cast(env->current_scope_start + 1); + *result = reinterpret_cast( + new HandleScopeInfo{env->current_scope_start, false}); napi_clear_last_error(env); return napi_ok; @@ -1929,8 +1963,8 @@ napi_status napi_close_escapable_handle_scope(napi_env env, napi_escapable_handl CHECK_ENV(env); CHECK_ARG(env, scope); - // Same cleanup as regular handle scope - size_t scope_start = reinterpret_cast(scope) - 1; + auto* scopeInfo = reinterpret_cast(scope); + size_t scope_start = scopeInfo->start; for (size_t i = scope_start; i < env->handle_scope_stack.size(); i++) { JS_FreeValue(env->context, *env->handle_scope_stack[i]); @@ -1938,6 +1972,7 @@ napi_status napi_close_escapable_handle_scope(napi_env env, napi_escapable_handl env->handle_scope_stack.resize(scope_start); env->current_scope_start = scope_start; + delete scopeInfo; napi_clear_last_error(env); return napi_ok; @@ -1949,8 +1984,11 @@ napi_status napi_escape_handle(napi_env env, napi_escapable_handle_scope scope, CHECK_ARG(env, escapee); CHECK_ARG(env, result); - // Get the scope start index - size_t scope_start = reinterpret_cast(scope) - 1; + auto* scopeInfo = reinterpret_cast(scope); + if (scopeInfo->escaped) { + return napi_set_last_error(env, napi_escape_called_twice); + } + size_t scope_start = scopeInfo->start; // Duplicate the JSValue to create a new handle that will outlive the current scope JSValue jsValue = ToJSValue(escapee); @@ -1960,30 +1998,16 @@ napi_status napi_escape_handle(napi_env env, napi_escapable_handle_scope scope, auto parentPtr = std::make_unique(escapedValue); napi_value parentHandle = reinterpret_cast(parentPtr.get()); - // Insert at parent scope position (before current scope) - if (scope_start > 0) { - env->handle_scope_stack.insert( - env->handle_scope_stack.begin() + scope_start, - std::move(parentPtr) - ); - - // Note: Inserting shifts indices, but since we're inserting at scope_start, - // the current scope's start index is now scope_start + 1 - // We need to update current_scope_start if it was pointing to this scope - if (env->current_scope_start == scope_start) { - env->current_scope_start = scope_start + 1; - } - } else { - // No parent scope - just add to the beginning - env->handle_scope_stack.insert( - env->handle_scope_stack.begin(), - std::move(parentPtr) - ); - - if (env->current_scope_start == 0) { - env->current_scope_start = 1; - } - } + // Insert immediately before the current scope. Advancing this scope's start + // keeps close_escapable_handle_scope from releasing the escaped handle; an + // enclosing scope will still release it at the correct time. At the root it + // remains alive until environment teardown. + env->handle_scope_stack.insert( + env->handle_scope_stack.begin() + scope_start, + std::move(parentPtr)); + scopeInfo->start++; + scopeInfo->escaped = true; + env->current_scope_start = scopeInfo->start; *result = parentHandle; napi_clear_last_error(env); @@ -2024,33 +2048,90 @@ napi_status napi_create_arraybuffer(napi_env env, size_t byte_length, void** dat } namespace { - void ArrayBufferFreeCallback(JSRuntime* rt, void* opaque, void* ptr) { - ExternalData* externalData = reinterpret_cast(opaque); - if (externalData != nullptr) { - // Invoke via the class finalizer which calls _cb properly - ExternalData::RunCallback(externalData); - delete externalData; + napi_status AttachFinalizerHolder(napi_env env, + JSValueConst jsObject, + void* finalizeData, + napi_finalize finalizeCb, + void* finalizeHint) { + JSRuntime* rt = JS_GetRuntime(env->context); + InitClassIds(rt); + + JSValue holder = JS_NewObjectClass(env->context, js_external_class_id); + if (JS_IsException(holder)) { + return napi_set_last_error(env, napi_generic_failure); + } + + JSValue symbol = JS_NewSymbol(env->context, "napi.finalizer", false); + if (JS_IsException(symbol)) { + JS_FreeValue(env->context, holder); + return napi_set_last_error(env, napi_generic_failure); + } + + JSAtom atom = JS_ValueToAtom(env->context, symbol); + JS_FreeValue(env->context, symbol); + if (atom == JS_ATOM_NULL) { + JS_FreeValue(env->context, holder); + return napi_set_last_error(env, napi_generic_failure); } + + // JS_DefinePropertyValue consumes its value regardless of success. Pass a + // duplicate and retain the local reference until the definition succeeds, + // so an attachment failure does not invoke a finalizer for memory that + // remains owned by the caller. + int defineResult = JS_DefinePropertyValue( + env->context, + jsObject, + atom, + JS_DupValue(env->context, holder), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | JS_PROP_CONFIGURABLE); + JS_FreeAtom(env->context, atom); + if (defineResult < 0) { + JS_FreeValue(env->context, holder); + return napi_set_last_error(env, napi_pending_exception); + } + + JS_SetOpaque(holder, new ExternalData(env, finalizeData, finalizeCb, finalizeHint)); + JS_FreeValue(env->context, holder); + return napi_ok; } } napi_status napi_create_external_arraybuffer(napi_env env, void* external_data, size_t byte_length, napi_finalize finalize_cb, void* finalize_hint, napi_value* result) { CHECK_ENV(env); CHECK_ARG(env, result); - - ExternalData* externalDataInfo = new ExternalData(env, external_data, finalize_cb, finalize_hint); + + // Node treats a zero-length external ArrayBuffer with a null backing store + // as detached. quickjs-ng otherwise creates a valid zero-length buffer. + const bool createDetached = external_data == nullptr && byte_length == 0; JSValue arrayBuffer = JS_NewArrayBuffer(env->context, reinterpret_cast(external_data), byte_length, - ArrayBufferFreeCallback, - externalDataInfo, + nullptr, + nullptr, 0); if (JS_IsException(arrayBuffer)) { - delete externalDataInfo; return napi_set_last_error(env, napi_generic_failure); } + + // Let a holder owned by the ArrayBuffer run the Node-API finalizer. Using + // QuickJS's backing-store free callback is unsafe for detachable buffers: + // quickjs-ng invokes it once during detach and again during object teardown. + // The holder instead survives detach and finalizes exactly once with the + // ArrayBuffer object, while QuickJS never tries to free embedder-owned data. + if (finalize_cb != nullptr) { + napi_status status = AttachFinalizerHolder( + env, arrayBuffer, external_data, finalize_cb, finalize_hint); + if (status != napi_ok) { + JS_FreeValue(env->context, arrayBuffer); + return status; + } + } + + if (createDetached) { + JS_DetachArrayBuffer(env->context, arrayBuffer); + } *result = FromJSValue(env, arrayBuffer); napi_clear_last_error(env); @@ -2441,6 +2522,37 @@ napi_status napi_run_script(napi_env env, napi_value script, const char* source_ return napi_ok; } +napi_status napi_add_finalizer(napi_env env, + napi_value js_object, + void* finalize_data, + napi_finalize finalize_cb, + void* finalize_hint, + napi_ref* result) { + CHECK_ENV(env); + CHECK_ARG(env, js_object); + CHECK_ARG(env, finalize_cb); + + JSValue jsObject = ToJSValue(js_object); + if (!JS_IsObject(jsObject)) { + return napi_set_last_error(env, napi_invalid_arg); + } + + // QuickJS does not expose an embedder field for arbitrary objects. Keep a + // native-finalized holder alive through a unique Symbol property instead; + // collecting the target releases the holder and runs the Node-API callback. + // A fresh Symbol makes the attachment collision-free and invisible to + // string-keyed reflection. + CHECK_NAPI(AttachFinalizerHolder( + env, jsObject, finalize_data, finalize_cb, finalize_hint)); + + if (result != nullptr) { + CHECK_NAPI(napi_create_reference(env, js_object, 0, result)); + } + + napi_clear_last_error(env); + return napi_ok; +} + // Wrap/Unwrap for native objects // // Instances created by a napi class constructor carry the js_wrap_class_id @@ -2694,20 +2806,74 @@ napi_status napi_create_bigint_uint64(napi_env env, uint64_t value, napi_value* return napi_ok; } +napi_status napi_create_bigint_words(napi_env env, + int sign_bit, + size_t word_count, + const uint64_t* words, + napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + + if (word_count > static_cast(INT_MAX)) { + return napi_set_last_error(env, napi_invalid_arg); + } + if (word_count > (static_cast(1) << 24)) { + napi_throw_range_error(env, nullptr, "Maximum BigInt size exceeded"); + return napi_set_last_error(env, napi_pending_exception); + } + if (word_count > 0) { + CHECK_ARG(env, words); + } + + // Node-API words are little-endian 64-bit limbs. Build a hexadecimal + // literal from most- to least-significant limb and let QuickJS create the + // arbitrary-precision value. + std::string hex; + hex.reserve(word_count * 16); + for (size_t i = word_count; i-- > 0;) { + char limb[17]; + std::snprintf(limb, sizeof(limb), "%016llx", + static_cast(words[i])); + hex += limb; + } + if (hex.empty()) { + hex = "0"; + } + + std::string expression = + (sign_bit ? "-BigInt(\"0x" : "BigInt(\"0x") + hex + "\")"; + JSValue bigint = JS_Eval(env->context, + expression.c_str(), + expression.size(), + "napi_create_bigint_words", + JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(bigint)) { + return napi_set_last_error(env, napi_pending_exception); + } + + *result = FromJSValue(env, bigint); + napi_clear_last_error(env); + return napi_ok; +} + napi_status napi_get_value_bigint_int64(napi_env env, napi_value value, int64_t* result, bool* lossless) { CHECK_ENV(env); CHECK_ARG(env, value); CHECK_ARG(env, result); + CHECK_ARG(env, lossless); JSValue jsValue = ToJSValue(value); - - if (JS_ToBigInt64(env->context, result, jsValue) < 0) { + + if (!JS_IsBigInt(jsValue)) { return napi_set_last_error(env, napi_bigint_expected); } - - if (lossless != nullptr) { - *lossless = true; // QuickJS BigInt is arbitrary precision + if (JS_ToBigInt64(env->context, result, jsValue) < 0) { + return napi_set_last_error(env, napi_bigint_expected); } + + JSValue truncated = JS_NewBigInt64(env->context, *result); + *lossless = JS_IsStrictEqual(env->context, jsValue, truncated); + JS_FreeValue(env->context, truncated); napi_clear_last_error(env); return napi_ok; @@ -2717,20 +2883,105 @@ napi_status napi_get_value_bigint_uint64(napi_env env, napi_value value, uint64_ CHECK_ENV(env); CHECK_ARG(env, value); CHECK_ARG(env, result); + CHECK_ARG(env, lossless); JSValue jsValue = ToJSValue(value); - - int64_t val; - if (JS_ToBigInt64(env->context, &val, jsValue) < 0) { + + if (!JS_IsBigInt(jsValue)) { return napi_set_last_error(env, napi_bigint_expected); } - - *result = static_cast(val); - - if (lossless != nullptr) { - *lossless = (val >= 0); + if (JS_ToBigUint64(env->context, result, jsValue) < 0) { + return napi_set_last_error(env, napi_bigint_expected); } - + + JSValue truncated = JS_NewBigUint64(env->context, *result); + *lossless = JS_IsStrictEqual(env->context, jsValue, truncated); + JS_FreeValue(env->context, truncated); + + napi_clear_last_error(env); + return napi_ok; +} + +napi_status napi_get_value_bigint_words(napi_env env, + napi_value value, + int* sign_bit, + size_t* word_count, + uint64_t* words) { + CHECK_ENV(env); + CHECK_ARG(env, value); + CHECK_ARG(env, word_count); + + JSValue jsValue = ToJSValue(value); + if (!JS_IsBigInt(jsValue)) { + return napi_set_last_error(env, napi_bigint_expected); + } + + JSValue toString = JS_GetPropertyStr(env->context, jsValue, "toString"); + if (JS_IsException(toString)) { + return napi_set_last_error(env, napi_pending_exception); + } + JSValue radix = JS_NewInt32(env->context, 16); + JSValue hexValue = JS_Call(env->context, toString, jsValue, 1, &radix); + JS_FreeValue(env->context, radix); + JS_FreeValue(env->context, toString); + if (JS_IsException(hexValue)) { + return napi_set_last_error(env, napi_pending_exception); + } + + const char* rawHex = JS_ToCString(env->context, hexValue); + if (rawHex == nullptr) { + JS_FreeValue(env->context, hexValue); + return napi_set_last_error(env, napi_pending_exception); + } + std::string hex{rawHex}; + JS_FreeCString(env->context, rawHex); + JS_FreeValue(env->context, hexValue); + + const bool negative = !hex.empty() && hex.front() == '-'; + std::string digits = negative ? hex.substr(1) : std::move(hex); + if (digits == "0") { + digits.clear(); + } + const size_t needed = (digits.size() + 15) / 16; + + if (words == nullptr) { + *word_count = needed; + napi_clear_last_error(env); + return napi_ok; + } + + CHECK_ARG(env, sign_bit); + const size_t capacity = *word_count; + *word_count = needed; + *sign_bit = negative ? 1 : 0; + for (size_t i = 0; i < needed && i < capacity; ++i) { + const size_t end = digits.size() - i * 16; + const size_t start = end > 16 ? end - 16 : 0; + const std::string limb = digits.substr(start, end - start); + words[i] = static_cast( + std::strtoull(limb.c_str(), nullptr, 16)); + } + + napi_clear_last_error(env); + return napi_ok; +} + +napi_status napi_set_instance_data(napi_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint) { + CHECK_ENV(env); + env->instance_data = data; + env->instance_data_finalize = finalize_cb; + env->instance_data_finalize_hint = finalize_hint; + napi_clear_last_error(env); + return napi_ok; +} + +napi_status napi_get_instance_data(napi_env env, void** data) { + CHECK_ENV(env); + CHECK_ARG(env, data); + *data = env->instance_data; napi_clear_last_error(env); return napi_ok; } @@ -2834,6 +3085,12 @@ napi_status napi_detach_arraybuffer(napi_env env, napi_value arraybuffer) { CHECK_ARG(env, arraybuffer); JSValue jsArrayBuffer = ToJSValue(arraybuffer); + if (!JS_IsArrayBuffer(jsArrayBuffer)) { + return napi_set_last_error(env, napi_arraybuffer_expected); + } + if (JS_IsImmutableArrayBuffer(jsArrayBuffer) != 0) { + return napi_set_last_error(env, napi_detachable_arraybuffer_expected); + } JS_DetachArrayBuffer(env->context, jsArrayBuffer); napi_clear_last_error(env); @@ -2846,12 +3103,26 @@ napi_status napi_is_detached_arraybuffer(napi_env env, napi_value value, bool* r CHECK_ARG(env, result); JSValue jsValue = ToJSValue(value); - size_t size; - uint8_t* data = JS_GetArrayBuffer(env->context, &size, jsValue); - - // A detached ArrayBuffer returns NULL for data - *result = (data == nullptr); - + if (!JS_IsArrayBuffer(jsValue)) { + *result = false; + napi_clear_last_error(env); + return napi_ok; + } + + // quickjs-ng exposes the standards-track ArrayBuffer.prototype.detached + // getter. Unlike testing the backing pointer, this correctly distinguishes + // detached buffers from valid zero-length buffers. + JSValue detached = JS_GetPropertyStr(env->context, jsValue, "detached"); + if (JS_IsException(detached)) { + return napi_set_last_error(env, napi_pending_exception); + } + int detachedBool = JS_ToBool(env->context, detached); + JS_FreeValue(env->context, detached); + if (detachedBool < 0) { + return napi_set_last_error(env, napi_pending_exception); + } + *result = detachedBool != 0; + napi_clear_last_error(env); return napi_ok; } diff --git a/Core/Node-API/Source/js_native_api_quickjs.h b/Core/Node-API/Source/js_native_api_quickjs.h index 7b84fe12..c1a6fa61 100644 --- a/Core/Node-API/Source/js_native_api_quickjs.h +++ b/Core/Node-API/Source/js_native_api_quickjs.h @@ -41,6 +41,13 @@ struct napi_env__ { // JS_FreeRuntime. std::vector refs_list; + // Per-environment instance data (Node-API v6). Replacing instance data does + // not finalize the previous value; the currently installed finalizer runs + // once when the environment is detached. + void* instance_data = nullptr; + napi_finalize instance_data_finalize = nullptr; + void* instance_data_finalize_hint = nullptr; + // Set to true once Detach has run. Subsequent napi_delete_reference // calls (from native destructors running during the JS teardown // cascade) must not touch the context or the (already emptied) diff --git a/Core/Node-API/Source/js_native_api_v8.cc b/Core/Node-API/Source/js_native_api_v8.cc index 8105a0bf..f7d4749a 100644 --- a/Core/Node-API/Source/js_native_api_v8.cc +++ b/Core/Node-API/Source/js_native_api_v8.cc @@ -357,18 +357,29 @@ inline napi_status Unwrap(napi_env env, v8::Local value = v8impl::V8LocalValueFromJsValue(js_object); RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg); v8::Local obj = value.As(); - - // [BABYLON-NATIVE-ADDITION]: Increase perf by using internal field instead of private property - Reference* reference = - static_cast(obj->GetAlignedPointerFromInternalField(0)); + + // Node-API permits wrapping any object, including plain object literals + // without V8 internal fields. Match Node's V8 adapter by keeping the native + // reference in an engine-private property. + auto maybe_value = + obj->GetPrivate(context, NAPI_WRAPPER_PRIVATE_KEY(context)); + CHECK_MAYBE_EMPTY(env, maybe_value, napi_generic_failure); + v8::Local wrapped_value = maybe_value.ToLocalChecked(); + RETURN_STATUS_IF_FALSE(env, wrapped_value->IsExternal(), napi_invalid_arg); + Reference* reference = static_cast( + wrapped_value.As()->Value()); + RETURN_STATUS_IF_FALSE(env, reference != nullptr, napi_invalid_arg); if (result) { *result = reference->Data(); } if (action == RemoveWrap) { - // [BABYLON-NATIVE-ADDITION]: Increase perf by using internal field instead of private property - obj->SetAlignedPointerInInternalField(0, nullptr); + auto maybe_deleted = + obj->DeletePrivate(context, NAPI_WRAPPER_PRIVATE_KEY(context)); + CHECK_MAYBE_NOTHING(env, maybe_deleted, napi_generic_failure); + RETURN_STATUS_IF_FALSE( + env, maybe_deleted.FromJust(), napi_generic_failure); if (reference->ownership() == Ownership::kUserland) { // When the wrap is been removed, the finalizer should be reset. reference->ResetFinalizer(); @@ -567,6 +578,11 @@ inline napi_status Wrap(napi_env env, RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg); v8::Local obj = value.As(); + auto maybe_has = + obj->HasPrivate(context, NAPI_WRAPPER_PRIVATE_KEY(context)); + CHECK_MAYBE_NOTHING(env, maybe_has, napi_generic_failure); + RETURN_STATUS_IF_FALSE(env, !maybe_has.FromJust(), napi_invalid_arg); + v8impl::Reference* reference = nullptr; if (result != nullptr) { // The returned reference should be deleted via napi_delete_reference() @@ -594,8 +610,17 @@ inline napi_status Wrap(napi_env env, finalize_cb == nullptr ? nullptr : finalize_hint); } - // [BABYLON-NATIVE-ADDITION]: Increase perf by using internal field instead of private property - obj->SetAlignedPointerInInternalField(0, reference); + auto maybe_set = obj->SetPrivate( + context, + NAPI_WRAPPER_PRIVATE_KEY(context), + v8::External::New(env->isolate, reference)); + if (maybe_set.IsNothing() || !maybe_set.FromJust()) { + if (result != nullptr) { + *result = nullptr; + } + delete reference; + return napi_set_last_error(env, napi_generic_failure); + } return GET_RETURN_STATUS(env); } @@ -3503,8 +3528,7 @@ napi_status NAPI_CDECL napi_is_detached_arraybuffer(napi_env env, v8::Local value = v8impl::V8LocalValueFromJsValue(arraybuffer); - //*result = - // value->IsArrayBuffer() && value.As()->WasDetached(); - *result = true; + *result = + value->IsArrayBuffer() && value.As()->WasDetached(); return napi_clear_last_error(env); } diff --git a/Core/Node-API/Source/js_native_api_v8_internals.h b/Core/Node-API/Source/js_native_api_v8_internals.h index 039e249b..70aeca81 100644 --- a/Core/Node-API/Source/js_native_api_v8_internals.h +++ b/Core/Node-API/Source/js_native_api_v8_internals.h @@ -75,8 +75,23 @@ class PersistentToLocal { #define CHECK_LE(a, b) CHECK((a) <= (b)) #endif -// [BABYLON-NATIVE-ADDITION]: Increase perf by using internal field instead of private property -//#define NAPI_PRIVATE_KEY(context) \ -// (v8::Private::New(context->GetIsolate())) +inline v8::Local NapiPrivateKey( + v8::Local context, + const char* name, + size_t length) { + v8::Isolate* isolate = context->GetIsolate(); + return v8::Private::ForApi( + isolate, OneByteString(isolate, name, static_cast(length))); +} + +#define NAPI_PRIVATE_KEY(context) \ + NapiPrivateKey((context), \ + "BabylonNative_NodeApiTypeTag", \ + sizeof("BabylonNative_NodeApiTypeTag") - 1) + +#define NAPI_WRAPPER_PRIVATE_KEY(context) \ + NapiPrivateKey((context), \ + "BabylonNative_NodeApiWrapper", \ + sizeof("BabylonNative_NodeApiWrapper") - 1) #endif // SRC_JS_NATIVE_API_V8_INTERNALS_H_ diff --git a/Polyfills/AbortController/Source/AbortSignal.cpp b/Polyfills/AbortController/Source/AbortSignal.cpp index c2d44544..b9266f78 100644 --- a/Polyfills/AbortController/Source/AbortSignal.cpp +++ b/Polyfills/AbortController/Source/AbortSignal.cpp @@ -51,7 +51,13 @@ namespace Babylon::Polyfills::Internal const Napi::Value resolvedReason = (reason.IsUndefined() || reason.IsEmpty()) ? CreateAbortError(env, "The operation was aborted.") : reason; - m_reason = Napi::Persistent(resolvedReason); + // Node-API references may only target objects/functions/externals, but + // AbortController.abort(reason) accepts any JavaScript value. Keep the + // arbitrary reason in an object property and persist that holder. + // This also covers the primitive cleanup reason used by WPT. + Napi::Object reasonHolder = Napi::Object::New(env); + reasonHolder.Set("value", resolvedReason); + m_reason = Napi::Persistent(reasonHolder); auto onabort = m_onabort.Value(); if (!onabort.IsNull() && !onabort.IsUndefined()) @@ -83,7 +89,7 @@ namespace Babylon::Polyfills::Internal return Env().Undefined(); } - return m_reason.Value(); + return m_reason.Value().Get("value"); } void AbortSignal::ThrowIfAborted(const Napi::CallbackInfo& info) diff --git a/Polyfills/AbortController/Source/AbortSignal.h b/Polyfills/AbortController/Source/AbortSignal.h index a2888c80..ea963af3 100644 --- a/Polyfills/AbortController/Source/AbortSignal.h +++ b/Polyfills/AbortController/Source/AbortSignal.h @@ -45,7 +45,7 @@ namespace Babylon::Polyfills::Internal std::unordered_map> m_eventHandlerRefs; Napi::FunctionReference m_onabort; - Napi::Reference m_reason; + Napi::ObjectReference m_reason; bool m_aborted = false; }; -} \ No newline at end of file +} diff --git a/Polyfills/Blob/CMakeLists.txt b/Polyfills/Blob/CMakeLists.txt index 1b77ef6d..7577e668 100644 --- a/Polyfills/Blob/CMakeLists.txt +++ b/Polyfills/Blob/CMakeLists.txt @@ -1,5 +1,6 @@ set(SOURCES "Include/Babylon/Polyfills/Blob.h" + "README.md" "Source/Blob.cpp" "Source/Blob.h") diff --git a/Polyfills/Blob/README.md b/Polyfills/Blob/README.md new file mode 100644 index 00000000..2206f9c4 --- /dev/null +++ b/Polyfills/Blob/README.md @@ -0,0 +1,12 @@ +# Blob + +Provides the browser `Blob` constructor, byte/text readers, zero-copy Blob +composition and slicing, and a lazily pulled byte `ReadableStream`. + +Call `Babylon::Polyfills::Streams::Initialize` before using `Blob.stream()` on +engines that do not provide Web Streams. Stream reads copy only the requested +chunk into JavaScript-owned memory; composing Blobs and slicing share immutable +native byte segments. + +The focused tests are adapted from WPT `FileAPI/blob`, WebKit's Blob stream +chunk/crash regressions, and Firefox's large Blob `pipeTo` regression. diff --git a/Polyfills/Blob/Source/Blob.cpp b/Polyfills/Blob/Source/Blob.cpp index 789c346c..5b29f730 100644 --- a/Polyfills/Blob/Source/Blob.cpp +++ b/Polyfills/Blob/Source/Blob.cpp @@ -2,8 +2,170 @@ #include #include +#include +#include +#include + namespace Babylon::Polyfills::Internal { + struct Blob::Segment + { + std::shared_ptr> Bytes; + size_t Offset{}; + size_t Length{}; + }; + + struct Blob::Data + { + std::vector Segments; + size_t Size{}; + + void Append(const Segment& segment) + { + if (segment.Length == 0) + { + return; + } + + Segments.emplace_back(segment); + Size += segment.Length; + } + + void CopyTo(size_t position, std::byte* destination, size_t length) const + { + size_t segmentStart{}; + for (const auto& segment : Segments) + { + const auto segmentEnd = segmentStart + segment.Length; + if (position < segmentEnd && length > 0) + { + const auto localOffset = position > segmentStart ? position - segmentStart : 0; + const auto copyLength = std::min(segment.Length - localOffset, length); + std::memcpy(destination, segment.Bytes->data() + segment.Offset + localOffset, copyLength); + destination += copyLength; + position += copyLength; + length -= copyLength; + } + segmentStart = segmentEnd; + } + } + }; + + class Blob::StreamSource final : public Napi::ObjectWrap + { + public: + static Napi::Function Define(Napi::Env env) + { + return DefineClass( + env, + "BlobStreamSource", + { + InstanceMethod("pull", &StreamSource::Pull), + InstanceMethod("cancel", &StreamSource::Cancel), + }); + } + + explicit StreamSource(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + if (info.Length() != 1 || !info[0].IsObject()) + { + throw Napi::TypeError::New(info.Env(), "Blob stream source requires a Blob."); + } + + const auto blob = Napi::ObjectWrap::Unwrap(info[0].As()); + if (blob == nullptr) + { + throw Napi::TypeError::New(info.Env(), "Blob stream source requires a Blob."); + } + m_blobData = blob->m_data; + } + + private: + Napi::Value Pull(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + auto controller = info[0].As(); + if (!m_blobData || m_position >= m_blobData->Size) + { + controller.Get("close").As().Call(controller, {}); + m_blobData.reset(); + return env.Undefined(); + } + + constexpr size_t CHUNK_SIZE = 64 * 1024; + const auto copyInto = [this](std::byte* destination, size_t outputLength) { + auto segmentIndex = m_segmentIndex; + auto segmentOffset = m_segmentOffset; + size_t remaining = outputLength; + + while (remaining > 0) + { + const auto& segment = m_blobData->Segments[segmentIndex]; + const auto copyLength = std::min(segment.Length - segmentOffset, remaining); + std::memcpy(destination, segment.Bytes->data() + segment.Offset + segmentOffset, copyLength); + destination += copyLength; + remaining -= copyLength; + segmentOffset += copyLength; + if (segmentOffset == segment.Length) + { + ++segmentIndex; + segmentOffset = 0; + } + } + + m_position += outputLength; + m_segmentIndex = segmentIndex; + m_segmentOffset = segmentOffset; + }; + + const auto byobRequestValue = controller.Get("byobRequest"); + if (!byobRequestValue.IsUndefined() && !byobRequestValue.IsNull()) + { + const auto byobRequest = byobRequestValue.As(); + const auto viewValue = byobRequest.Get("view"); + if (!viewValue.IsTypedArray()) + { + throw Napi::TypeError::New(env, "Blob.stream() received an invalid BYOB request view."); + } + + const auto view = viewValue.As(); + const auto outputLength = std::min({CHUNK_SIZE, view.ByteLength(), m_blobData->Size - m_position}); + const auto outputBuffer = view.ArrayBuffer(); + auto destination = static_cast(outputBuffer.Data()) + view.ByteOffset(); + copyInto(destination, outputLength); + byobRequest.Get("respond").As().Call(byobRequest, {Napi::Number::New(env, outputLength)}); + } + else + { + const auto outputLength = std::min(CHUNK_SIZE, m_blobData->Size - m_position); + auto outputBuffer = Napi::ArrayBuffer::New(env, outputLength); + copyInto(static_cast(outputBuffer.Data()), outputLength); + auto output = Napi::Uint8Array::New(env, outputLength, outputBuffer, 0); + controller.Get("enqueue").As().Call(controller, {output}); + } + + if (m_position == m_blobData->Size) + { + controller.Get("close").As().Call(controller, {}); + m_blobData.reset(); + } + + return env.Undefined(); + } + + Napi::Value Cancel(const Napi::CallbackInfo& info) + { + m_blobData.reset(); + return info.Env().Undefined(); + } + + std::shared_ptr m_blobData; + size_t m_position{}; + size_t m_segmentIndex{}; + size_t m_segmentOffset{}; + }; + void Blob::Initialize(Napi::Env env) { static constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob"; @@ -18,8 +180,25 @@ namespace Babylon::Polyfills::Internal InstanceMethod("text", &Blob::Text), InstanceMethod("arrayBuffer", &Blob::ArrayBuffer), InstanceMethod("bytes", &Blob::Bytes), + InstanceMethod("slice", &Blob::Slice), + InstanceMethod("stream", &Blob::Stream), }); + auto descriptor = Napi::Object::New(env); + descriptor.Set("configurable", true); + descriptor.Set("value", JS_BLOB_CONSTRUCTOR_NAME); + env.Global().Get("Object").As().Get("defineProperty").As().Call(env.Global().Get("Object"), {func.Get("prototype"), Napi::Symbol::WellKnown(env, "toStringTag"), descriptor}); + + // Underlying stream sources are native ObjectWrap instances so + // pull/cancel live on one shared prototype. Creating two native + // functions for every Blob.stream() caused avoidable JSC Function + // structure transitions and exposed a WebKitGTK LeakSanitizer + // allocation during Worker teardown. + auto sourceDescriptor = Napi::Object::New(env); + sourceDescriptor.Set("value", StreamSource::Define(env)); + env.Global().Get("Object").As().Get("defineProperty").As().Call( + env.Global().Get("Object"), + {func, Napi::String::New(env, "__jsRuntimeHostBlobStreamSource"), sourceDescriptor}); env.Global().Set(JS_BLOB_CONSTRUCTOR_NAME, func); } } @@ -27,36 +206,122 @@ namespace Babylon::Polyfills::Internal Blob::Blob(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { - if (info.Length() > 0) + auto env = Env(); + auto data = std::make_shared(); + std::vector stringSegmentIndices; + bool convertLineEndings{}; + + if (info.Length() > 0 && !info[0].IsUndefined()) { - const auto blobParts = info[0].As(); + if (!info[0].IsObject() && !info[0].IsFunction()) + { + throw Napi::TypeError::New(env, "Blob parts must be an iterable object."); + } - if (blobParts.Length() > 0) + const auto blobParts = info[0].As(); + if (blobParts.IsArray()) { - const auto firstPart = blobParts.Get(uint32_t{0}); - ProcessBlobPart(firstPart); + data->Segments.reserve(blobParts.As().Length()); + } + + const auto reflect = env.Global().Get("Reflect").As(); + const auto iteratorMethod = reflect.Get("get").As().Call( + reflect, + {blobParts, Napi::Symbol::WellKnown(env, "iterator")}); + if (!iteratorMethod.IsFunction()) + { + throw Napi::TypeError::New(env, "Blob parts must be iterable."); + } + + const auto iterator = iteratorMethod.As().Call(blobParts, {}).As(); + const auto next = iterator.Get("next").As(); + try + { + while (true) + { + const auto result = next.Call(iterator, {}).As(); + if (result.Get("done").ToBoolean().Value()) + { + break; + } - if (blobParts.Length() > 1) + if (AppendBlobPart(*data, result.Get("value"))) + { + stringSegmentIndices.emplace_back(data->Segments.size() - 1); + } + } + } + catch (...) + { + try { - throw Napi::Error::New(Env(), "Using multiple BlobParts in Blob constructor is not implemented."); + const auto returnMethod = iterator.Get("return"); + if (returnMethod.IsFunction()) + { + returnMethod.As().Call(iterator, {}); + } } + catch (...) + { + // Preserve the exception that interrupted part conversion. + } + throw; } } - if (info.Length() > 1) + if (info.Length() > 1 && !info[1].IsUndefined() && !info[1].IsNull()) { + if (!info[1].IsObject() && !info[1].IsFunction()) + { + throw Napi::TypeError::New(env, "Blob options must be an object."); + } + const auto options = info[1].As(); + const auto endings = options.Get("endings"); + if (!endings.IsUndefined()) + { + const auto value = endings.ToString().Utf8Value(); + if (value != "transparent" && value != "native") + { + throw Napi::TypeError::New(env, "Blob endings must be 'transparent' or 'native'."); + } + convertLineEndings = value == "native"; + } - if (options.Has("type")) + const auto type = options.Get("type"); + if (!type.IsUndefined()) { - m_type = options.Get("type").As().Utf8Value(); + m_type = NormalizeType(type.ToString().Utf8Value()); } } + + if (convertLineEndings) + { + for (const auto segmentIndex : stringSegmentIndices) + { + auto& segment = data->Segments[segmentIndex]; + auto value = std::string{ + reinterpret_cast(segment.Bytes->data() + segment.Offset), + segment.Length}; + value = NormalizeLineEndings(std::move(value)); + + auto bytes = std::make_shared>(value.size()); + if (!value.empty()) + { + std::memcpy(bytes->data(), value.data(), value.size()); + } + data->Size -= segment.Length; + segment = {std::move(bytes), 0, value.size()}; + data->Size += segment.Length; + } + } + + m_data = std::move(data); } Napi::Value Blob::GetSize(const Napi::CallbackInfo&) { - return Napi::Value::From(Env(), m_data.size()); + return Napi::Value::From(Env(), m_data->Size); } Napi::Value Blob::GetType(const Napi::CallbackInfo&) @@ -67,8 +332,11 @@ namespace Babylon::Polyfills::Internal Napi::Value Blob::Text(const Napi::CallbackInfo&) { // NOTE: This will not check for UTF-8 validity - const auto begin = reinterpret_cast(m_data.data()); - std::string text(begin, m_data.size()); + std::string text(m_data->Size, '\0'); + if (!text.empty()) + { + m_data->CopyTo(0, reinterpret_cast(text.data()), text.size()); + } const auto deferred = Napi::Promise::Deferred::New(Env()); deferred.Resolve(Napi::String::New(Env(), text)); @@ -77,59 +345,259 @@ namespace Babylon::Polyfills::Internal Napi::Value Blob::ArrayBuffer(const Napi::CallbackInfo&) { - const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size()); - if (m_data.data()) - { - std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size()); - } - const auto deferred = Napi::Promise::Deferred::New(Env()); - deferred.Resolve(arrayBuffer); + deferred.Resolve(CreateArrayBuffer()); return deferred.Promise(); } Napi::Value Blob::Bytes(const Napi::CallbackInfo&) { - const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size()); - if (m_data.data()) - { - std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size()); - } - const auto uint8Array = Napi::Uint8Array::New(Env(), m_data.size(), arrayBuffer, 0); + const auto arrayBuffer = CreateArrayBuffer(); + const auto uint8Array = Napi::Uint8Array::New(Env(), m_data->Size, arrayBuffer, 0); const auto deferred = Napi::Promise::Deferred::New(Env()); deferred.Resolve(uint8Array); return deferred.Promise(); } - void Blob::ProcessBlobPart(const Napi::Value& blobPart) + Napi::Value Blob::Slice(const Napi::CallbackInfo& info) + { + const auto start = NormalizeSliceIndex( + info.Length() > 0 && !info[0].IsUndefined() ? info[0].ToNumber().DoubleValue() : 0, + m_data->Size); + const auto end = NormalizeSliceIndex( + info.Length() > 1 && !info[1].IsUndefined() ? info[1].ToNumber().DoubleValue() : static_cast(m_data->Size), + m_data->Size); + const auto sliceEnd = std::max(start, end); + + auto sliceData = std::make_shared(); + size_t segmentStart{}; + for (const auto& segment : m_data->Segments) + { + const auto segmentEnd = segmentStart + segment.Length; + const auto overlapStart = std::max(start, segmentStart); + const auto overlapEnd = std::min(sliceEnd, segmentEnd); + if (overlapStart < overlapEnd) + { + sliceData->Append({segment.Bytes, segment.Offset + overlapStart - segmentStart, overlapEnd - overlapStart}); + } + segmentStart = segmentEnd; + if (segmentStart >= sliceEnd) + { + break; + } + } + + std::string contentType; + if (info.Length() > 2 && !info[2].IsUndefined()) + { + contentType = NormalizeType(info[2].ToString().Utf8Value()); + } + + const auto blobConstructor = Env().Global().Get("Blob").As(); + const auto result = blobConstructor.New({Napi::Array::New(Env())}); + auto resultBlob = Napi::ObjectWrap::Unwrap(result); + resultBlob->m_data = std::move(sliceData); + resultBlob->m_type = std::move(contentType); + return result; + } + + Napi::Value Blob::Stream(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + const auto readableStreamValue = env.Global().Get("ReadableStream"); + if (!readableStreamValue.IsFunction()) + { + throw Napi::TypeError::New(env, "Blob.stream() requires ReadableStream to be installed."); + } + + const auto blobConstructor = env.Global().Get("Blob").As(); + const auto sourceConstructor = blobConstructor.Get("__jsRuntimeHostBlobStreamSource").As(); + auto source = sourceConstructor.New({info.This()}); + source.Set("type", "bytes"); + + return readableStreamValue.As().New({source}); + } + + bool Blob::AppendBlobPart(Data& data, const Napi::Value& blobPart) { + const std::byte* source{}; + size_t length{}; + bool isBufferSource{}; + if (blobPart.IsArrayBuffer()) { const auto buffer = blobPart.As(); - const auto begin = static_cast(buffer.Data()); - m_data.assign(begin, begin + buffer.ByteLength()); + source = static_cast(buffer.Data()); + length = buffer.ByteLength(); + isBufferSource = true; } - else if (blobPart.IsTypedArray() || blobPart.IsDataView()) + else if (blobPart.IsTypedArray()) { const auto array = blobPart.As(); const auto buffer = array.ArrayBuffer(); - const auto begin = static_cast(buffer.Data()) + array.ByteOffset(); - m_data.assign(begin, begin + array.ByteLength()); + const auto bufferData = static_cast(buffer.Data()); + source = bufferData == nullptr ? nullptr : bufferData + array.ByteOffset(); + length = array.ByteLength(); + isBufferSource = true; + } + else if (blobPart.IsDataView()) + { + const auto view = blobPart.As(); + const auto buffer = view.ArrayBuffer(); + const auto bufferData = static_cast(buffer.Data()); + source = bufferData == nullptr ? nullptr : bufferData + view.ByteOffset(); + length = view.ByteLength(); + isBufferSource = true; + } + else if (blobPart.IsObject()) + { + const auto object = blobPart.As(); + const auto blobConstructor = Env().Global().Get("Blob").As(); + if (object.InstanceOf(blobConstructor)) + { + auto nativeBlob = object; + if (!object.Get("constructor").StrictEquals(blobConstructor)) + { + nativeBlob = object.Get("slice").As().Call(object, {Napi::Number::New(Env(), 0), object.Get("size")}).As(); + } + + const auto blob = Napi::ObjectWrap::Unwrap(nativeBlob); + data.Segments.reserve(data.Segments.size() + blob->m_data->Segments.size()); + for (const auto& segment : blob->m_data->Segments) + { + data.Append(segment); + } + return false; + } + } + + if (isBufferSource) + { + auto bytes = std::make_shared>(length); + if (length > 0) + { + std::memcpy(bytes->data(), source, length); + } + data.Append({std::move(bytes), 0, length}); + return false; + } + + auto value = blobPart.ToString().Utf8Value(); + auto bytes = std::make_shared>(value.size()); + if (!value.empty()) + { + std::memcpy(bytes->data(), value.data(), value.size()); } - else if (blobPart.IsString()) + data.Append({std::move(bytes), 0, value.size()}); + return !value.empty(); + } + + Napi::ArrayBuffer Blob::CreateArrayBuffer() const + { + auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data->Size); + if (m_data->Size > 0) + { + m_data->CopyTo(0, static_cast(arrayBuffer.Data()), m_data->Size); + } + return arrayBuffer; + } + + std::string Blob::NormalizeType(std::string type) + { + for (auto& character : type) + { + const auto value = static_cast(character); + if (value < 0x20 || value > 0x7E) + { + return {}; + } + if (character >= 'A' && character <= 'Z') + { + character = static_cast(character - 'A' + 'a'); + } + } + return type; + } + + std::string Blob::NormalizeLineEndings(std::string value) + { +#ifdef _WIN32 + static constexpr auto nativeNewline = "\r\n"; +#else + static constexpr auto nativeNewline = "\n"; +#endif + size_t outputLength{}; + for (size_t index{}; index < value.size(); ++index) + { + if (value[index] == '\r') + { + if (index + 1 < value.size() && value[index + 1] == '\n') + { + ++index; + } + outputLength += std::char_traits::length(nativeNewline); + } + else if (value[index] == '\n') + { + outputLength += std::char_traits::length(nativeNewline); + } + else + { + ++outputLength; + } + } + + std::string result; + result.reserve(outputLength); + for (size_t index{}; index < value.size(); ++index) { - const auto str = blobPart.As().Utf8Value(); - const auto begin = reinterpret_cast(str.data()); - m_data.assign(begin, begin + str.length()); + if (value[index] == '\r') + { + if (index + 1 < value.size() && value[index + 1] == '\n') + { + ++index; + } + result.append(nativeNewline); + } + else if (value[index] == '\n') + { + result.append(nativeNewline); + } + else + { + result.push_back(value[index]); + } } - else + return result; + } + + size_t Blob::NormalizeSliceIndex(double value, size_t size) + { + if (std::isnan(value)) { - // Assume it's another Blob object - const auto obj = blobPart.As(); - const auto blobObj = Napi::ObjectWrap::Unwrap(obj); - m_data.assign(blobObj->m_data.begin(), blobObj->m_data.end()); + return 0; } + + const auto magnitude = std::abs(value); + const auto lower = std::floor(magnitude); + const auto fraction = magnitude - lower; + auto rounded = lower; + if (fraction > 0.5 || (fraction == 0.5 && std::fmod(lower, 2.0) != 0.0)) + { + rounded += 1.0; + } + + if (value < 0) + { + return !std::isfinite(rounded) || rounded >= static_cast(size) + ? 0 + : size - static_cast(rounded); + } + + return !std::isfinite(rounded) || rounded >= static_cast(size) + ? size + : static_cast(rounded); } } diff --git a/Polyfills/Blob/Source/Blob.h b/Polyfills/Blob/Source/Blob.h index df04e834..6174d0af 100644 --- a/Polyfills/Blob/Source/Blob.h +++ b/Polyfills/Blob/Source/Blob.h @@ -2,8 +2,9 @@ #include -#include +#include #include +#include namespace Babylon::Polyfills::Internal { @@ -20,10 +21,21 @@ namespace Babylon::Polyfills::Internal Napi::Value Text(const Napi::CallbackInfo& info); Napi::Value ArrayBuffer(const Napi::CallbackInfo& info); Napi::Value Bytes(const Napi::CallbackInfo& info); + Napi::Value Slice(const Napi::CallbackInfo& info); + Napi::Value Stream(const Napi::CallbackInfo& info); + + struct Segment; + struct Data; + class StreamSource; + + bool AppendBlobPart(Data& data, const Napi::Value& blobPart); + Napi::ArrayBuffer CreateArrayBuffer() const; - void ProcessBlobPart(const Napi::Value& blobPart); + static std::string NormalizeType(std::string type); + static std::string NormalizeLineEndings(std::string value); + static size_t NormalizeSliceIndex(double value, size_t size); - std::vector m_data; + std::shared_ptr m_data; std::string m_type; }; -} \ No newline at end of file +} diff --git a/Polyfills/CMakeLists.txt b/Polyfills/CMakeLists.txt index a44765fb..12a6e6b6 100644 --- a/Polyfills/CMakeLists.txt +++ b/Polyfills/CMakeLists.txt @@ -44,4 +44,52 @@ endif() if(JSRUNTIMEHOST_POLYFILL_TEXTENCODER) add_subdirectory(TextEncoder) -endif() \ No newline at end of file +endif() + +if(JSRUNTIMEHOST_POLYFILL_STREAMS) + add_subdirectory(Streams) +endif() + +if(JSRUNTIMEHOST_POLYFILL_COMPRESSION) + if(NOT JSRUNTIMEHOST_POLYFILL_STREAMS) + message(FATAL_ERROR "The Compression polyfill requires the Streams polyfill.") + endif() + add_subdirectory(Compression) +endif() + +if(JSRUNTIMEHOST_POLYFILL_INDEXEDDB) + add_subdirectory(IndexedDB) +endif() + +if(JSRUNTIMEHOST_POLYFILL_WORKER) + if(NAPI_JAVASCRIPT_ENGINE STREQUAL "JSI") + # The legacy JSI adapter only implements the v5-shaped C++ surface and + # has no detachable ArrayBuffer ABI. Keep existing JSI configurations + # buildable while the v7-capable engine adapters expose Worker. + message(STATUS "Worker polyfill is unavailable with the legacy JSI adapter") + else() + set(_worker_required_polyfills + JSRUNTIMEHOST_POLYFILL_CONSOLE + JSRUNTIMEHOST_POLYFILL_SCHEDULING + JSRUNTIMEHOST_POLYFILL_XMLHTTPREQUEST + JSRUNTIMEHOST_POLYFILL_FETCH + JSRUNTIMEHOST_POLYFILL_URL + JSRUNTIMEHOST_POLYFILL_ABORT_CONTROLLER + JSRUNTIMEHOST_POLYFILL_WEBSOCKET + JSRUNTIMEHOST_POLYFILL_BLOB + JSRUNTIMEHOST_POLYFILL_FILE + JSRUNTIMEHOST_POLYFILL_PERFORMANCE + JSRUNTIMEHOST_POLYFILL_TEXTDECODER + JSRUNTIMEHOST_POLYFILL_TEXTENCODER + JSRUNTIMEHOST_POLYFILL_STREAMS + JSRUNTIMEHOST_POLYFILL_COMPRESSION + JSRUNTIMEHOST_POLYFILL_INDEXEDDB) + foreach(_worker_dependency IN LISTS _worker_required_polyfills) + if(NOT ${_worker_dependency}) + message(FATAL_ERROR + "JSRUNTIMEHOST_POLYFILL_WORKER requires ${_worker_dependency}=ON so Worker realms expose the browser baseline.") + endif() + endforeach() + add_subdirectory(Worker) + endif() +endif() diff --git a/Polyfills/Compression/CMakeLists.txt b/Polyfills/Compression/CMakeLists.txt new file mode 100644 index 00000000..d049f155 --- /dev/null +++ b/Polyfills/Compression/CMakeLists.txt @@ -0,0 +1,59 @@ +if(APPLE) + # FindZLIB exposes the SDK's usr/include as an explicit system include on + # recent Xcode versions. That breaks libc++'s include_next ordering. The + # header and library are both SDK-provided, so only add the link library. + find_library(JSRUNTIMEHOST_COMPRESSION_ZLIB_LIBRARY NAMES z REQUIRED) + set(JSRUNTIMEHOST_COMPRESSION_ZLIB_TARGET "${JSRUNTIMEHOST_COMPRESSION_ZLIB_LIBRARY}") +elseif(TARGET ZLIB::ZLIB) + set(JSRUNTIMEHOST_COMPRESSION_ZLIB_TARGET ZLIB::ZLIB) +elseif(TARGET zlibstatic) + set(JSRUNTIMEHOST_COMPRESSION_ZLIB_TARGET zlibstatic) +elseif(TARGET zlib) + set(JSRUNTIMEHOST_COMPRESSION_ZLIB_TARGET zlib) +else() + find_package(ZLIB QUIET) + if(ZLIB_FOUND) + set(JSRUNTIMEHOST_COMPRESSION_ZLIB_TARGET ZLIB::ZLIB) + else() + set(ZLIB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(SKIP_INSTALL_ALL ON CACHE BOOL "" FORCE) + FetchContent_Declare(jsruntimehost_zlib + GIT_REPOSITORY https://github.com/madler/zlib.git + GIT_TAG 51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf + EXCLUDE_FROM_ALL) + FetchContent_MakeAvailable_With_Message(jsruntimehost_zlib) + set(JSRUNTIMEHOST_COMPRESSION_ZLIB_TARGET zlibstatic) + set_property(TARGET zlibstatic PROPERTY FOLDER Dependencies) + endif() +endif() + +set(COMPRESSION_POLYFILL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/Source/CompressionPolyfill.js") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${COMPRESSION_POLYFILL_FILE}") +file(READ "${COMPRESSION_POLYFILL_FILE}" COMPRESSION_POLYFILL_SOURCE) +configure_file( + "Source/CompressionScripts.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/Generated/CompressionScripts.h" + @ONLY) + +set(SOURCES + "Include/Babylon/Polyfills/Compression.h" + "README.md" + "Source/Compression.cpp" + "Source/CompressionPolyfill.js" + "Source/CompressionScripts.h.in" + "ThirdParty/zlib/LICENSE") + +add_library(Compression ${SOURCES}) +warnings_as_errors(Compression) + +target_include_directories(Compression + PUBLIC "Include" + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/Generated") + +target_link_libraries(Compression + PUBLIC JsRuntime + PUBLIC Streams + PRIVATE ${JSRUNTIMEHOST_COMPRESSION_ZLIB_TARGET}) + +set_property(TARGET Compression PROPERTY FOLDER Polyfills) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Polyfills/Compression/Include/Babylon/Polyfills/Compression.h b/Polyfills/Compression/Include/Babylon/Polyfills/Compression.h new file mode 100644 index 00000000..44cb9693 --- /dev/null +++ b/Polyfills/Compression/Include/Babylon/Polyfills/Compression.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::Compression +{ + // Installs CompressionStream and DecompressionStream when the host does + // not already provide them. The Streams polyfill is initialized first. + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Polyfills/Compression/README.md b/Polyfills/Compression/README.md new file mode 100644 index 00000000..78a0d424 --- /dev/null +++ b/Polyfills/Compression/README.md @@ -0,0 +1,16 @@ +# Compression streams + +This optional polyfill installs the browser `CompressionStream` and +`DecompressionStream` interfaces when the JavaScript host does not already +provide them. It supports the interoperable `deflate`, `deflate-raw`, and +`gzip` formats using zlib and the WHATWG Streams API. + +Input `BufferSource` memory is borrowed only for the duration of a synchronous +codec call. Output is accumulated before invoking JavaScript so an enqueue +callback cannot invalidate an input buffer still in use. A native 64 KiB +scratch buffer is reused for the lifetime of each active stream; each emitted +`Uint8Array` receives one exact-size copy into JavaScript-owned memory. + +The module uses an existing CMake zlib target or the platform zlib package when +available. It fetches pinned zlib 1.3.1 only when no zlib package is present. +The zlib license is included under `ThirdParty/zlib`. diff --git a/Polyfills/Compression/Source/Compression.cpp b/Polyfills/Compression/Source/Compression.cpp new file mode 100644 index 00000000..aab2ab10 --- /dev/null +++ b/Polyfills/Compression/Source/Compression.cpp @@ -0,0 +1,407 @@ +#include "CompressionScripts.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Babylon::Polyfills::Internal +{ + namespace + { + constexpr size_t OUTPUT_BUFFER_SIZE = 64 * 1024; + + int WindowBits(const std::string& format) + { + if (format == "deflate") + { + return 15; + } + if (format == "deflate-raw") + { + return -15; + } + if (format == "gzip") + { + return 15 + 16; + } + return 0; + } + + class CompressionCodec final : public Napi::ObjectWrap + { + public: + static Napi::Function CreateConstructor(Napi::Env env) + { + return DefineClass( + env, + "NativeCompressionCodec", + { + InstanceMethod("transform", &CompressionCodec::Transform), + InstanceMethod("finish", &CompressionCodec::Finish), + InstanceMethod("close", &CompressionCodec::Close), + }); + } + + explicit CompressionCodec(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + auto env = info.Env(); + if (info.Length() < 2 || !info[0].IsString()) + { + throw Napi::TypeError::New(env, "Invalid compression codec arguments"); + } + + const auto format = info[0].As().Utf8Value(); + const auto windowBits = WindowBits(format); + if (windowBits == 0) + { + throw Napi::TypeError::New(env, "Unsupported compression format"); + } + + m_compressing = info[1].ToBoolean().Value(); + const auto result = m_compressing + ? deflateInit2(&m_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, windowBits, 8, Z_DEFAULT_STRATEGY) + : inflateInit2(&m_stream, windowBits); + if (result != Z_OK) + { + throw Napi::Error::New(env, "Unable to initialize the compression codec"); + } + m_initialized = true; + } + + ~CompressionCodec() override + { + EndStream(); + } + + private: + Napi::Value Transform(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + if (m_closed || m_finished) + { + throw Napi::TypeError::New(env, "The compression stream is no longer writable"); + } + if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsFunction()) + { + throw Napi::TypeError::New(env, "Invalid compression transform arguments"); + } + + const auto input = info[0].As(); + if (input.TypedArrayType() != napi_uint8_array) + { + throw Napi::TypeError::New(env, "Compression input must be a Uint8Array"); + } + + const auto bytes = info[0].As(); + if (bytes.ByteLength() > 0) + { + if (!Process(bytes.Data(), bytes.ByteLength(), false, info[1].As())) + { + return {}; + } + } + return env.Undefined(); + } + + Napi::Value Finish(const Napi::CallbackInfo& info) + { + auto env = info.Env(); + if (!m_closed && !m_finished) + { + if (info.Length() < 1 || !info[0].IsFunction()) + { + throw Napi::TypeError::New(env, "Invalid compression finish arguments"); + } + if (!Process(nullptr, 0, true, info[0].As())) + { + return {}; + } + } + return env.Undefined(); + } + + Napi::Value Close(const Napi::CallbackInfo& info) + { + m_closed = true; + EndStream(); + ReleasePendingStorage(); + return info.Env().Undefined(); + } + + void EnsureOutputBuffer() + { + if (!m_outputBuffer) + { + m_outputBuffer = std::make_unique(OUTPUT_BUFFER_SIZE); + } + } + + void CaptureOutput(Napi::Env env, size_t byteLength) + { + if (byteLength == 0) + { + return; + } + + auto output = Napi::Uint8Array::New(env, byteLength); + std::memcpy(output.Data(), m_outputBuffer.get(), byteLength); + m_pendingOutput.emplace_back(std::move(output)); + } + + void EnqueuePending(const Napi::Function& enqueue) + { + for (const auto& output : m_pendingOutput) + { + enqueue.Call({output}); + } + m_pendingOutput.clear(); + } + + bool Process(const uint8_t* input, size_t inputLength, bool finishing, const Napi::Function& enqueue) + { + auto env = enqueue.Env(); + if (!m_initialized) + { + throw Napi::TypeError::New(env, "The compression stream is no longer writable"); + } + + EnsureOutputBuffer(); + m_pendingOutput.clear(); + + const uint8_t* inputCursor = input; + size_t inputRemaining = inputLength; + bool reachedEnd{}; + bool closeAfterEnqueue{}; + std::string failure; + + try + { + while (true) + { + if (m_stream.avail_in == 0 && inputRemaining > 0) + { + const auto nextLength = std::min( + inputRemaining, + static_cast(std::numeric_limits::max())); + m_stream.next_in = const_cast(reinterpret_cast(inputCursor)); + m_stream.avail_in = static_cast(nextLength); + inputCursor += nextLength; + inputRemaining -= nextLength; + } + + m_stream.next_out = reinterpret_cast(m_outputBuffer.get()); + m_stream.avail_out = static_cast(OUTPUT_BUFFER_SIZE); + + const auto inputBefore = m_stream.avail_in; + const auto result = m_compressing + ? deflate(&m_stream, finishing ? Z_FINISH : Z_NO_FLUSH) + : inflate(&m_stream, finishing ? Z_FINISH : Z_NO_FLUSH); + const auto produced = OUTPUT_BUFFER_SIZE - m_stream.avail_out; + CaptureOutput(env, produced); + + if (result == Z_STREAM_END) + { + reachedEnd = true; + closeAfterEnqueue = true; + if (!m_compressing && (m_stream.avail_in > 0 || inputRemaining > 0)) + { + failure = "Unexpected input after the end of the compressed stream"; + } + break; + } + + if (result == Z_DATA_ERROR || result == Z_NEED_DICT) + { + failure = m_stream.msg != nullptr + ? std::string{"The compressed data is invalid: "} + m_stream.msg + : "The compressed data is invalid"; + closeAfterEnqueue = true; + break; + } + if (result == Z_MEM_ERROR) + { + failure = "The compression codec ran out of memory"; + closeAfterEnqueue = true; + break; + } + if (result != Z_OK && result != Z_BUF_ERROR) + { + failure = "The compression codec entered an invalid state"; + closeAfterEnqueue = true; + break; + } + + const bool madeProgress = produced > 0 || m_stream.avail_in < inputBefore; + const bool hasInput = m_stream.avail_in > 0 || inputRemaining > 0; + if (!finishing && !hasInput && m_stream.avail_out > 0) + { + break; + } + if (!madeProgress && result == Z_BUF_ERROR) + { + if (finishing && !m_compressing) + { + failure = "The compressed input ended before the end of the stream"; + } + else + { + failure = "The compression codec could not make progress"; + } + closeAfterEnqueue = true; + break; + } + if (!madeProgress && finishing) + { + failure = m_compressing + ? "The compression codec could not finish" + : "The compressed input ended before the end of the stream"; + closeAfterEnqueue = true; + break; + } + } + } + catch (...) + { + ResetZlibPointers(); + m_pendingOutput.clear(); + m_closed = true; + EndStream(); + ReleasePendingStorage(); + throw; + } + + ResetZlibPointers(); + if (closeAfterEnqueue) + { + EndStream(); + } + + try + { + // Complete zlib's access to the borrowed input before this + // callback can run JavaScript and mutate or detach it. + EnqueuePending(enqueue); + } + catch (...) + { + m_closed = true; + EndStream(); + ReleasePendingStorage(); + throw; + } + + if (!failure.empty()) + { + m_closed = true; + ReleasePendingStorage(); + // This error is reported after EnqueuePending has called + // back into JavaScript. Setting the pending exception + // directly avoids a second C++ exception conversion in + // Node-API backends with reentrant callback contexts. + static_cast(napi_throw_type_error(env, nullptr, failure.c_str())); + return false; + } + + if (reachedEnd || finishing) + { + m_finished = true; + EndStream(); + ReleasePendingStorage(); + } + return true; + } + + void ResetZlibPointers() noexcept + { + m_stream.next_in = Z_NULL; + m_stream.avail_in = 0; + m_stream.next_out = Z_NULL; + m_stream.avail_out = 0; + } + + void EndStream() noexcept + { + if (m_initialized) + { + if (m_compressing) + { + deflateEnd(&m_stream); + } + else + { + inflateEnd(&m_stream); + } + m_initialized = false; + } + m_outputBuffer.reset(); + } + + void ReleasePendingStorage() + { + m_pendingOutput.clear(); + std::vector{}.swap(m_pendingOutput); + } + + z_stream m_stream{}; + std::unique_ptr m_outputBuffer; + std::vector m_pendingOutput; + bool m_compressing{}; + bool m_initialized{}; + bool m_finished{}; + bool m_closed{}; + }; + } +} + +namespace Babylon::Polyfills::Compression +{ + void BABYLON_API Initialize(Napi::Env env) + { + Streams::Initialize(env); + + Napi::HandleScope scope{env}; + auto global = env.Global(); + const auto compressionStream = global.Get("CompressionStream"); + const auto decompressionStream = global.Get("DecompressionStream"); + if (!compressionStream.IsUndefined() && !decompressionStream.IsUndefined()) + { + return; + } + + if (!global.Get("TransformStream").IsFunction()) + { + throw Napi::Error::New(env, "Compression streams require TransformStream"); + } + + const auto nativeConstructor = Internal::CompressionCodec::CreateConstructor(env); + const auto factory = Napi::Eval( + env, + Internal::CompressionScripts::Polyfill, + "jsruntimehost://compression-polyfill.js") + .As(); + const auto exports = factory.Call({nativeConstructor}).As(); + + if (compressionStream.IsUndefined()) + { + global.Set("CompressionStream", exports.Get("CompressionStream")); + } + if (decompressionStream.IsUndefined()) + { + global.Set("DecompressionStream", exports.Get("DecompressionStream")); + } + } +} diff --git a/Polyfills/Compression/Source/CompressionPolyfill.js b/Polyfills/Compression/Source/CompressionPolyfill.js new file mode 100644 index 00000000..92c7c468 --- /dev/null +++ b/Polyfills/Compression/Source/CompressionPolyfill.js @@ -0,0 +1,137 @@ +(function() { + "use strict"; + + return function createCompressionPolyfill(NativeCompressionCodec) { + var compressionStates = new WeakMap(); + var decompressionStates = new WeakMap(); + + function normalizeFormat(value) { + // String concatenation follows Web IDL's DOMString conversion, + // including throwing for Symbol values and propagating user + // conversion exceptions. + var format = value + ""; + if (format !== "deflate" && format !== "deflate-raw" && format !== "gzip") { + throw new TypeError("Unsupported compression format: '" + format + "'"); + } + return format; + } + + function toUint8Array(chunk) { + if (chunk instanceof ArrayBuffer) { + return new Uint8Array(chunk); + } + if (ArrayBuffer.isView(chunk) && chunk.buffer instanceof ArrayBuffer) { + return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); + } + throw new TypeError("Compression stream chunks must be BufferSource values"); + } + + function initialize(instance, states, format, compressing) { + var codec = new NativeCompressionCodec(format, compressing); + var enqueue; + var transform = new TransformStream({ + start: function(controller) { + enqueue = controller.enqueue.bind(controller); + }, + transform: function(chunk) { + var bytes = toUint8Array(chunk); + if (bytes.byteLength === 0) { + return; + } + + try { + codec.transform(bytes, enqueue); + } catch (error) { + codec.close(); + codec = null; + enqueue = null; + throw error; + } + }, + flush: function() { + try { + codec.finish(enqueue); + } finally { + codec.close(); + codec = null; + enqueue = null; + } + } + }); + + states.set(instance, { + readable: transform.readable, + writable: transform.writable + }); + } + + function getState(states, value) { + var state = states.get(value); + if (!state) { + throw new TypeError("Illegal invocation"); + } + return state; + } + + function CompressionStream(format) { + if (!(this instanceof CompressionStream)) { + throw new TypeError("CompressionStream must be constructed with new"); + } + if (arguments.length === 0) { + throw new TypeError("CompressionStream requires a format"); + } + initialize(this, compressionStates, normalizeFormat(format), true); + } + + function DecompressionStream(format) { + if (!(this instanceof DecompressionStream)) { + throw new TypeError("DecompressionStream must be constructed with new"); + } + if (arguments.length === 0) { + throw new TypeError("DecompressionStream requires a format"); + } + initialize(this, decompressionStates, normalizeFormat(format), false); + } + + Object.defineProperties(CompressionStream.prototype, { + readable: { + configurable: true, + enumerable: true, + get: function() { return getState(compressionStates, this).readable; } + }, + writable: { + configurable: true, + enumerable: true, + get: function() { return getState(compressionStates, this).writable; } + } + }); + Object.defineProperties(DecompressionStream.prototype, { + readable: { + configurable: true, + enumerable: true, + get: function() { return getState(decompressionStates, this).readable; } + }, + writable: { + configurable: true, + enumerable: true, + get: function() { return getState(decompressionStates, this).writable; } + } + }); + + if (typeof Symbol === "function" && Symbol.toStringTag) { + Object.defineProperty(CompressionStream.prototype, Symbol.toStringTag, { + configurable: true, + value: "CompressionStream" + }); + Object.defineProperty(DecompressionStream.prototype, Symbol.toStringTag, { + configurable: true, + value: "DecompressionStream" + }); + } + + return { + CompressionStream: CompressionStream, + DecompressionStream: DecompressionStream + }; + }; +})() diff --git a/Polyfills/Compression/Source/CompressionScripts.h.in b/Polyfills/Compression/Source/CompressionScripts.h.in new file mode 100644 index 00000000..abee928f --- /dev/null +++ b/Polyfills/Compression/Source/CompressionScripts.h.in @@ -0,0 +1,8 @@ +#pragma once + +namespace Babylon::Polyfills::Internal::CompressionScripts +{ + inline constexpr char Polyfill[] = R"JSRHCOMPRESSION( +@COMPRESSION_POLYFILL_SOURCE@ +)JSRHCOMPRESSION"; +} diff --git a/Polyfills/Compression/ThirdParty/zlib/LICENSE b/Polyfills/Compression/ThirdParty/zlib/LICENSE new file mode 100644 index 00000000..ab8ee6f7 --- /dev/null +++ b/Polyfills/Compression/ThirdParty/zlib/LICENSE @@ -0,0 +1,22 @@ +Copyright notice: + + (C) 1995-2022 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu diff --git a/Polyfills/Fetch/CMakeLists.txt b/Polyfills/Fetch/CMakeLists.txt index 62b46175..24346db0 100644 --- a/Polyfills/Fetch/CMakeLists.txt +++ b/Polyfills/Fetch/CMakeLists.txt @@ -1,12 +1,25 @@ +set(FETCH_POLYFILL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/Source/FetchPolyfill.js") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${FETCH_POLYFILL_FILE}") +file(READ "${FETCH_POLYFILL_FILE}" FETCH_POLYFILL_SOURCE) +configure_file( + "Source/FetchScripts.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/Generated/FetchScripts.h" + @ONLY) + set(SOURCES "Include/Babylon/Polyfills/Fetch.h" + "Readme.md" "Source/Fetch.h" - "Source/Fetch.cpp") + "Source/Fetch.cpp" + "Source/FetchPolyfill.js" + "Source/FetchScripts.h.in") add_library(Fetch ${SOURCES}) warnings_as_errors(Fetch) -target_include_directories(Fetch PUBLIC "Include") +target_include_directories(Fetch + PUBLIC "Include" + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/Generated") target_link_libraries(Fetch PUBLIC JsRuntime diff --git a/Polyfills/Fetch/Readme.md b/Polyfills/Fetch/Readme.md index 00c41cfc..b3d21a4e 100644 --- a/Polyfills/Fetch/Readme.md +++ b/Polyfills/Fetch/Readme.md @@ -1,5 +1,7 @@ # Fetch -Minimal implementation of the [WHATWG `fetch()`](https://fetch.spec.whatwg.org/) API. Like `XMLHttpRequest`, it is implemented on top of the platform-specific transports in the `UrlLib` dependency, so network behavior (libcurl / WinHTTP / etc.) is identical between the two polyfills. +Implementation of the [WHATWG `fetch()`](https://fetch.spec.whatwg.org/) API. +Like `XMLHttpRequest`, it uses the platform-specific transport from `UrlLib`, so +network behavior is shared between the two polyfills. ```js const response = await fetch("https://example.com/data.json"); @@ -8,16 +10,24 @@ if (response.ok) { } ``` -## Response -`fetch()` returns a `Promise` that resolves to a `Response`-like object exposing: -* `ok`, `status`, `statusText`, `url`, `redirected`, `type`, `bodyUsed` -* `headers` with `get(name)`, `has(name)`, and `forEach(callback)` (header names are matched case-insensitively) -* `text()`, `arrayBuffer()`, `json()`, `blob()` (each returns a `Promise`) -* `clone()` +## Headers and Response +The module installs `Headers` and `Response` when the host does not already +provide them. Response bodies use a `ReadableStream` and follow the browser's +single-consumption behavior through `bodyUsed`. The supported body readers are +`arrayBuffer()`, `blob()`, `bytes()`, `json()`, and `text()`. -The response body is fully buffered before the promise resolves. The body accessors may therefore be called more than once (`bodyUsed` is always reported as `false`), which is a deliberate, lenient deviation from the spec's single-use semantics. +Initialize the Streams, Blob, TextEncoder, TextDecoder, and URL polyfills before +using body-bearing responses on engines that do not provide those globals. -`blob()` requires the `Blob` polyfill to be initialized; otherwise the returned promise rejects. +A completed native response is copied once into JavaScript-owned memory and +exposed as a byte stream. Body readers retain chunk references while consuming +a stream and allocate a contiguous result at most once when that result requires +one. Repeated header iteration reuses a mutation-versioned normalized view, and +header deletion and replacement compact the field list in place. + +The focused conformance tests are adapted from WPT `fetch/api/headers` and +`fetch/api/response`, plus WebKit, Firefox, and Chromium response-body +regressions. ## Local files Like `XMLHttpRequest`, `fetch()` supports loading local resources: @@ -60,4 +70,3 @@ The rejection's `stack` is captured synchronously at the `fetch()` call site (be is handed to a worker thread), so crash reports can attribute the failing call rather than an empty scheduler tick. (Engines that only materialize `.stack` when an error is thrown may omit the frames.) - diff --git a/Polyfills/Fetch/Source/Fetch.cpp b/Polyfills/Fetch/Source/Fetch.cpp index 7a500df7..847e2e32 100644 --- a/Polyfills/Fetch/Source/Fetch.cpp +++ b/Polyfills/Fetch/Source/Fetch.cpp @@ -1,4 +1,5 @@ #include "Fetch.h" +#include "FetchScripts.h" #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -20,23 +22,13 @@ namespace Babylon::Polyfills::Internal { namespace { - // Buffered response payload shared between the Response object and any clones it produces. - struct ResponseData - { - int statusCode{}; - std::string statusText; - std::string url; - std::vector> headers; - std::vector body; - }; - // Shared state for honoring an AbortSignal passed via init.signal. Co-owned by the "abort" // listener (which sets the flag, captures the reason, and cancels the transport) and the // completion continuation (which reports the AbortError and tears the listener down). struct AbortState { bool aborted{false}; - Napi::Reference reason; + Napi::ObjectReference reasonHolder; Napi::ObjectReference signal; Napi::FunctionReference listener; }; @@ -63,10 +55,202 @@ namespace Babylon::Polyfills::Internal }); } + struct DataUrlResponse + { + std::string contentType; + std::string url; + std::vector body; + }; + + bool IsAsciiWhitespace(uint8_t value) + { + return value == 0x09 || value == 0x0A || value == 0x0C || value == 0x0D || value == 0x20; + } + + void TrimAsciiWhitespace(std::string& value) + { + const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char character) { + return IsAsciiWhitespace(character); + }); + const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char character) { + return IsAsciiWhitespace(character); + }).base(); + value = first < last ? std::string{first, last} : std::string{}; + } + + int HexDigitValue(char value) + { + if (value >= '0' && value <= '9') + { + return value - '0'; + } + if (value >= 'a' && value <= 'f') + { + return value - 'a' + 10; + } + if (value >= 'A' && value <= 'F') + { + return value - 'A' + 10; + } + return -1; + } + + template + void ForEachPercentDecodedByte(std::string_view value, TCallback&& callback) + { + for (size_t index = 0; index < value.size(); ++index) + { + if (value[index] == '%' && index + 2 < value.size()) + { + const int high = HexDigitValue(value[index + 1]); + const int low = HexDigitValue(value[index + 2]); + if (high >= 0 && low >= 0) + { + callback(static_cast((high << 4) | low)); + index += 2; + continue; + } + } + callback(static_cast(value[index])); + } + } + + std::vector PercentDecode(std::string_view value) + { + std::vector decoded; + decoded.reserve(value.size()); + ForEachPercentDecodedByte(value, [&decoded](uint8_t byte) { + decoded.push_back(byte); + }); + return decoded; + } + + int Base64DigitValue(uint8_t value) + { + if (value >= 'A' && value <= 'Z') + { + return value - 'A'; + } + if (value >= 'a' && value <= 'z') + { + return value - 'a' + 26; + } + if (value >= '0' && value <= '9') + { + return value - '0' + 52; + } + if (value == '+') + { + return 62; + } + if (value == '/') + { + return 63; + } + return -1; + } + + std::vector ForgivingBase64Decode(std::string_view input) + { + size_t digitCount{}; + size_t paddingCount{}; + bool sawPadding{}; + ForEachPercentDecodedByte(input, [&](uint8_t value) { + if (IsAsciiWhitespace(value)) + { + return; + } + if (value == '=') + { + sawPadding = true; + ++paddingCount; + return; + } + if (sawPadding || Base64DigitValue(value) < 0) + { + throw std::runtime_error{"fetch: invalid base64 data URL"}; + } + ++digitCount; + }); + + const auto encodedCount = digitCount + paddingCount; + if ((paddingCount > 0 && (encodedCount % 4 != 0 || paddingCount > 2)) || digitCount % 4 == 1) + { + throw std::runtime_error{"fetch: invalid base64 data URL"}; + } + + std::vector decoded(digitCount / 4 * 3 + digitCount % 4 * 3 / 4); + size_t outputIndex{}; + uint32_t accumulator{}; + size_t availableBits{}; + ForEachPercentDecodedByte(input, [&](uint8_t value) { + if (IsAsciiWhitespace(value) || value == '=') + { + return; + } + accumulator = (accumulator << 6) | static_cast(Base64DigitValue(value)); + availableBits += 6; + if (availableBits >= 8) + { + availableBits -= 8; + decoded[outputIndex++] = static_cast(accumulator >> availableBits); + accumulator &= (uint32_t{1} << availableBits) - 1; + } + }); + return decoded; + } + + std::optional ParseDataUrl(std::string_view url) + { + if (url.size() < 5 || !EqualsIgnoreCase(url.substr(0, 4), "data") || url[4] != ':') + { + return std::nullopt; + } + + const auto comma = url.find(',', 5); + if (comma == std::string_view::npos) + { + throw std::runtime_error{"fetch: malformed data URL"}; + } + + std::string mediaType{url.substr(5, comma - 5)}; + TrimAsciiWhitespace(mediaType); + bool base64 = false; + if (const auto semicolon = mediaType.rfind(';'); semicolon != std::string::npos) + { + std::string finalParameter{mediaType.substr(semicolon + 1)}; + TrimAsciiWhitespace(finalParameter); + if (EqualsIgnoreCase(finalParameter, "base64")) + { + mediaType.resize(semicolon); + TrimAsciiWhitespace(mediaType); + base64 = true; + } + } + if (mediaType.empty()) + { + mediaType = "text/plain;charset=US-ASCII"; + } + else if (mediaType.front() == ';') + { + mediaType.insert(0, "text/plain"); + } + + const auto fragment = url.find('#', comma + 1); + const auto payload = url.substr(comma + 1, fragment == std::string_view::npos ? std::string_view::npos : fragment - comma - 1); + auto decodedPayload = base64 ? ForgivingBase64Decode(payload) : PercentDecode(payload); + + return DataUrlResponse{ + std::move(mediaType), + std::string{url.substr(0, fragment)}, + std::move(decodedPayload)}; + } + // Stable message used for every transport-failure rejection. Browsers and Node both keep // this constant (the variable detail rides on `cause`) so crash-report grouping stays // intact; we follow Node/undici's "fetch failed" spelling. constexpr const char* FETCH_FAILED_MESSAGE = "fetch failed"; + constexpr const char* JS_FETCH_POLYFILL_EXPORTS_NAME = "fetchPolyfillExports"; // Snapshot the JS call-site stack synchronously, inside fetch(), before SendAsync() hands // the request to a worker thread. The transport-failure rejection is otherwise built in a @@ -157,18 +341,6 @@ namespace Babylon::Polyfills::Internal throw std::runtime_error{"Unsupported fetch method: " + method + " (only GET and POST are supported)"}; } - std::optional FindHeader(const ResponseData& data, std::string_view name) - { - for (const auto& header : data.headers) - { - if (EqualsIgnoreCase(header.first, name)) - { - return header.second; - } - } - return std::nullopt; - } - void ApplyRequestHeaders(UrlLib::UrlRequest& request, const Napi::Value& headers) { if (headers.IsUndefined() || headers.IsNull()) @@ -176,169 +348,85 @@ namespace Babylon::Polyfills::Internal return; } - Napi::Env env = headers.Env(); - - // Array of [name, value] pairs. - if (headers.IsArray()) + const auto env = headers.Env(); + const auto headersConstructor = env.Global().Get("Headers"); + if (headersConstructor.IsUndefined() || headersConstructor.IsNull()) { - const auto array = headers.As(); - for (uint32_t i = 0; i < array.Length(); ++i) - { - const auto pair = array.Get(i); - if (pair.IsArray()) - { - const auto entry = pair.As(); - request.SetRequestHeader(entry.Get(0u).ToString().Utf8Value(), entry.Get(1u).ToString().Utf8Value()); - } - } - return; + throw Napi::TypeError::New(env, "fetch requires Headers to be installed."); } - if (headers.IsObject()) - { - const auto object = headers.As(); - - // Headers / Map instances expose forEach((value, key) => ...). - const auto forEach = object.Get("forEach"); - if (forEach.IsFunction()) - { - const auto callback = Napi::Function::New(env, [&request](const Napi::CallbackInfo& info) { - if (info.Length() >= 2) - { - request.SetRequestHeader(info[1].ToString().Utf8Value(), info[0].ToString().Utf8Value()); - } - }); - forEach.As().Call(object, {callback}); - return; - } - - // Plain object of name/value properties. - const auto names = object.GetPropertyNames(); - for (uint32_t i = 0; i < names.Length(); ++i) + auto normalizedHeaders = headersConstructor.As().New({headers}); + const auto callback = Napi::Function::New(env, [&request](const Napi::CallbackInfo& info) { + if (info.Length() >= 2) { - const auto key = names.Get(i); - request.SetRequestHeader(key.ToString().Utf8Value(), object.Get(key).ToString().Utf8Value()); + request.SetRequestHeader(info[1].ToString().Utf8Value(), info[0].ToString().Utf8Value()); } - } + }); + normalizedHeaders.Get("forEach").As().Call(normalizedHeaders, {callback}); } - Napi::Object BuildHeaders(Napi::Env env, const std::shared_ptr& data) + void InitializeFetchClasses(Napi::Env env) { - Napi::Object headers = Napi::Object::New(env); - - headers.Set("get", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto value = FindHeader(*data, info[0].ToString().Utf8Value()); - return value ? Napi::Value{Napi::String::New(env, *value)} : Napi::Value{env.Null()}; - }, "get")); - - headers.Set("has", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - return Napi::Boolean::New(info.Env(), FindHeader(*data, info[0].ToString().Utf8Value()).has_value()); - }, "has")); - - headers.Set("forEach", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto callback = info[0].As(); - const auto thisArg = info.Length() > 1 ? info[1] : env.Undefined(); - for (const auto& header : data->headers) - { - callback.Call(thisArg, {Napi::String::New(env, header.second), Napi::String::New(env, header.first)}); - } - return env.Undefined(); - }, "forEach")); - - return headers; + auto global = env.Global(); + auto nativeObject = JsRuntime::NativeObject::GetFromJavaScript(env); + auto exportsValue = nativeObject.Get(JS_FETCH_POLYFILL_EXPORTS_NAME); + if (exportsValue.IsUndefined() || exportsValue.IsNull()) + { + exportsValue = Napi::Eval(env, FetchScripts::Polyfill, "jsruntimehost://fetch-polyfill.js"); + nativeObject.Set(JS_FETCH_POLYFILL_EXPORTS_NAME, exportsValue); + } + const auto exports = exportsValue.As(); + const auto headers = global.Get("Headers"); + const auto response = global.Get("Response"); + if (headers.IsUndefined() || headers.IsNull() || response.IsUndefined() || response.IsNull()) + { + global.Set("Headers", exports.Get("Headers")); + global.Set("Response", exports.Get("Response")); + } + nativeObject.Set("createFetchResponse", exports.Get("createFetchResponse")); } - Napi::Object BuildResponse(Napi::Env env, const std::shared_ptr& data) + template + Napi::Value CreateFetchResponse( + Napi::Env env, + const void* body, + size_t bodySize, + const THeaders& headers, + int status, + std::string_view statusText, + std::string_view url, + bool redirected) { - Napi::Object response = Napi::Object::New(env); - - const bool ok = data->statusCode >= 200 && data->statusCode < 300; - response.Set("ok", Napi::Boolean::New(env, ok)); - response.Set("status", Napi::Number::New(env, data->statusCode)); - response.Set("statusText", Napi::String::New(env, data->statusText)); - response.Set("url", Napi::String::New(env, data->url)); - response.Set("redirected", Napi::Boolean::New(env, false)); - response.Set("type", Napi::String::New(env, "basic")); - response.Set("bodyUsed", Napi::Boolean::New(env, false)); - response.Set("headers", BuildHeaders(env, data)); - - response.Set("text", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - std::string text{reinterpret_cast(data->body.data()), data->body.size()}; - deferred.Resolve(Napi::String::New(env, text)); - return deferred.Promise(); - }, "text")); - - response.Set("arrayBuffer", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - const auto arrayBuffer = Napi::ArrayBuffer::New(env, data->body.size()); - if (!data->body.empty()) - { - std::memcpy(arrayBuffer.Data(), data->body.data(), data->body.size()); - } - deferred.Resolve(arrayBuffer); - return deferred.Promise(); - }, "arrayBuffer")); - - response.Set("json", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - std::string text{reinterpret_cast(data->body.data()), data->body.size()}; - const auto json = env.Global().Get("JSON").As(); - const auto parse = json.Get("parse").As(); - try - { - deferred.Resolve(parse.Call(json, {Napi::String::New(env, text)})); - } - catch (const Napi::Error& error) - { - deferred.Reject(error.Value()); - } - return deferred.Promise(); - }, "json")); - - response.Set("blob", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - Napi::Env env = info.Env(); - const auto deferred = Napi::Promise::Deferred::New(env); - - // Use IsUndefined()/IsNull() rather than IsFunction() to detect the Blob - // polyfill: some JavaScriptCore/JSI builds classify constructor functions as - // typeof 'object', so napi_typeof reports napi_object and IsFunction() would - // incorrectly reject even when the Blob polyfill is installed. - const auto blobConstructor = env.Global().Get("Blob"); - if (blobConstructor.IsUndefined() || blobConstructor.IsNull()) - { - deferred.Reject(Napi::Error::New(env, "fetch: Blob is not available in this environment").Value()); - return deferred.Promise(); - } - - const auto arrayBuffer = Napi::ArrayBuffer::New(env, data->body.size()); - if (!data->body.empty()) - { - std::memcpy(arrayBuffer.Data(), data->body.data(), data->body.size()); - } - const auto bytes = Napi::Uint8Array::New(env, data->body.size(), arrayBuffer, 0); - - Napi::Array parts = Napi::Array::New(env, 1); - parts.Set(0u, bytes); + auto arrayBuffer = Napi::ArrayBuffer::New(env, bodySize); + if (bodySize > 0) + { + std::memcpy(arrayBuffer.Data(), body, bodySize); + } + const auto bytes = Napi::Uint8Array::New(env, bodySize, arrayBuffer, 0); - Napi::Object options = Napi::Object::New(env); - const auto contentType = FindHeader(*data, "content-type"); - options.Set("type", Napi::String::New(env, contentType.value_or(""))); + auto responseHeaders = Napi::Array::New(env, headers.size()); + uint32_t headerIndex{}; + for (const auto& header : headers) + { + auto pair = Napi::Array::New(env, 2); + pair.Set(uint32_t{0}, Napi::String::New(env, header.first)); + pair.Set(uint32_t{1}, Napi::String::New(env, header.second)); + responseHeaders.Set(headerIndex++, pair); + } - deferred.Resolve(blobConstructor.As().New({parts, options})); - return deferred.Promise(); - }, "blob")); + auto init = Napi::Object::New(env); + init.Set("headers", responseHeaders); + init.Set("status", Napi::Number::New(env, status)); + init.Set("statusText", Napi::String::New(env, statusText.data(), statusText.size())); - response.Set("clone", Napi::Function::New(env, [data](const Napi::CallbackInfo& info) -> Napi::Value { - return BuildResponse(info.Env(), data); - }, "clone")); + auto metadata = Napi::Object::New(env); + metadata.Set("redirected", Napi::Boolean::New(env, redirected)); + metadata.Set("type", Napi::String::New(env, "basic")); + metadata.Set("url", Napi::String::New(env, url.data(), url.size())); - return response; + const auto nativeObject = JsRuntime::NativeObject::GetFromJavaScript(env); + const auto createResponse = nativeObject.Get("createFetchResponse").As(); + return createResponse.Call(nativeObject, {env.Global().Get("Response"), bytes, init, metadata}); } } @@ -347,6 +435,7 @@ namespace Babylon::Polyfills::Internal void Initialize(Napi::Env env) { static constexpr auto JS_FETCH_NAME = "fetch"; + InitializeFetchClasses(env); auto fetchFunction = Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { Napi::Env env = info.Env(); @@ -404,6 +493,47 @@ namespace Babylon::Polyfills::Internal signal = init.Get("signal"); } + if (signal.IsObject()) + { + const auto signalObject = signal.As(); + if (signalObject.Get("aborted").ToBoolean().Value()) + { + deferred.Reject(GetAbortReason(env, signalObject)); + return deferred.Promise(); + } + } + + std::optional dataUrl; + try + { + dataUrl = ParseDataUrl(url); + } + catch (const std::runtime_error& error) + { + deferred.Reject(Napi::TypeError::New(env, error.what()).Value()); + return deferred.Promise(); + } + + if (dataUrl) + { + if (method != UrlLib::UrlMethod::Get || body.has_value()) + { + throw std::runtime_error{"fetch: data URLs only support GET requests"}; + } + const std::vector> responseHeaders{ + {"content-type", dataUrl->contentType}}; + deferred.Resolve(CreateFetchResponse( + env, + dataUrl->body.data(), + dataUrl->body.size(), + responseHeaders, + 200, + "OK", + dataUrl->url, + false)); + return deferred.Promise(); + } + auto request = std::make_shared(); request->Open(method, url); request->ResponseType(UrlLib::UrlResponseType::Buffer); @@ -441,7 +571,12 @@ namespace Babylon::Polyfills::Internal if (!abortState->aborted) { abortState->aborted = true; - abortState->reason = Napi::Persistent(GetAbortReason(env, abortState->signal.Value())); + // Node-API references cannot target primitive + // values, while AbortSignal reasons can be any + // JavaScript value. + Napi::Object reasonHolder = Napi::Object::New(env); + reasonHolder.Set("value", GetAbortReason(env, abortState->signal.Value())); + abortState->reasonHolder = Napi::Persistent(reasonHolder); // Cancel the in-flight transport; the completion continuation then // rejects with the AbortError instead of a transport TypeError. request->Abort(); @@ -475,7 +610,7 @@ namespace Babylon::Polyfills::Internal { // Per the fetch spec, an aborted request rejects with the // signal's reason (an AbortError), not a network error. - deferred.Reject(abortState->reason.Value()); + deferred.Reject(abortState->reasonHolder.Value().Get("value")); return; } } @@ -494,18 +629,19 @@ namespace Babylon::Polyfills::Internal return; } - auto data = std::make_shared(); - data->statusCode = status; - data->statusText = std::string{request->StatusText()}; - data->url = std::string{request->ResponseUrl()}; - for (const auto& header : request->GetAllResponseHeaders()) - { - data->headers.emplace_back(header.first, header.second); - } const auto responseBuffer = request->ResponseBuffer(); - data->body.assign(responseBuffer.begin(), responseBuffer.end()); - - deferred.Resolve(BuildResponse(env, data)); + const auto statusText = request->StatusText(); + const std::string responseUrl{request->ResponseUrl()}; + const std::string_view finalUrl = responseUrl.empty() ? std::string_view{url} : std::string_view{responseUrl}; + deferred.Resolve(CreateFetchResponse( + env, + responseBuffer.data(), + responseBuffer.size(), + request->GetAllResponseHeaders(), + status, + statusText, + finalUrl, + !responseUrl.empty() && responseUrl != url)); }) .then(*scheduler, arcana::cancellation::none(), [deferred, env, scheduler](const arcana::expected& result) { @@ -525,8 +661,7 @@ namespace Babylon::Polyfills::Internal deferred.Reject(Napi::Error::New(env, std::current_exception()).Value()); } - return deferred.Promise(); - }, JS_FETCH_NAME); + return deferred.Promise(); }, JS_FETCH_NAME); if (env.Global().Get(JS_FETCH_NAME).IsUndefined()) { diff --git a/Polyfills/Fetch/Source/FetchPolyfill.js b/Polyfills/Fetch/Source/FetchPolyfill.js new file mode 100644 index 00000000..4435d1ef --- /dev/null +++ b/Polyfills/Fetch/Source/FetchPolyfill.js @@ -0,0 +1,704 @@ +(function(global) { + "use strict"; + + var headerStates = new WeakMap(); + var responseStates = new WeakMap(); + var streamTrackers = new WeakMap(); + var readerTrackers = new WeakMap(); + var iteratorTrackers = new WeakMap(); + var headerNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + var nullBodyStatuses = [204, 205, 304]; + + function wrapMethod(target, name, createWrapper) { + var descriptor = Object.getOwnPropertyDescriptor(target, name); + var original = descriptor ? descriptor.value : target[name]; + if (typeof original !== "function") { + return; + } + if (!descriptor) { + descriptor = { + configurable: true, + enumerable: false, + writable: true + }; + } + descriptor.value = createWrapper(original); + Object.defineProperty(target, name, descriptor); + } + + function trackStream(stream) { + var tracker = streamTrackers.get(stream); + if (!tracker) { + tracker = { disturbed: false }; + streamTrackers.set(stream, tracker); + } + return tracker; + } + + function trackReader(reader, tracker) { + if (readerTrackers.has(reader)) { + return reader; + } + readerTrackers.set(reader, tracker); + for (var name of ["read", "cancel"]) { + wrapMethod(reader, name, function(original) { + return function() { + tracker.disturbed = true; + return original.apply(this, arguments); + }; + }); + } + return reader; + } + + function trackIterator(iterator, tracker) { + if (iteratorTrackers.has(iterator)) { + return iterator; + } + iteratorTrackers.set(iterator, tracker); + for (var name of ["next", "return", "throw"]) { + wrapMethod(iterator, name, function(original) { + return function() { + tracker.disturbed = true; + return original.apply(this, arguments); + }; + }); + } + return iterator; + } + + function instrumentReadableStreams() { + if (global.ReadableStream === undefined || global.ReadableStream === null) { + return; + } + var prototype = global.ReadableStream.prototype; + wrapMethod(prototype, "getReader", function(original) { + return function() { + return trackReader(original.apply(this, arguments), trackStream(this)); + }; + }); + for (var name of ["cancel", "pipeTo", "pipeThrough", "tee"]) { + wrapMethod(prototype, name, function(original) { + return function() { + var result = original.apply(this, arguments); + trackStream(this).disturbed = true; + return result; + }; + }); + } + wrapMethod(prototype, "values", function(original) { + return function() { + return trackIterator(original.apply(this, arguments), trackStream(this)); + }; + }); + if (typeof Symbol.asyncIterator === "symbol") { + wrapMethod(prototype, Symbol.asyncIterator, function(original) { + return function() { + return trackIterator(original.apply(this, arguments), trackStream(this)); + }; + }); + } + } + + instrumentReadableStreams(); + + function requireHeaderState(value) { + var state = headerStates.get(value); + if (!state) { + throw new TypeError("Illegal invocation"); + } + return state; + } + + function requireResponseState(value) { + var state = responseStates.get(value); + if (!state) { + throw new TypeError("Illegal invocation"); + } + return state; + } + + function toByteString(value, description) { + var string = String(value); + for (var index = 0; index < string.length; ++index) { + if (string.charCodeAt(index) > 255) { + throw new TypeError(description + " is not a valid ByteString"); + } + } + return string; + } + + function normalizeHeaderName(value) { + var name = toByteString(value, "Header name"); + if (!headerNamePattern.test(name)) { + throw new TypeError("Invalid header name"); + } + return name.toLowerCase(); + } + + function normalizeHeaderValue(value) { + var normalized = toByteString(value, "Header value") + .replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, ""); + if (/[\0\r\n]/.test(normalized)) { + throw new TypeError("Invalid header value"); + } + return normalized; + } + + function isForbiddenResponseHeader(name) { + return name === "set-cookie" || name === "set-cookie2"; + } + + function appendHeader(state, name, value) { + if (state.guard === "response" && isForbiddenResponseHeader(name)) { + return; + } + state.list.push([name, value]); + state.version++; + } + + function removeHeader(state, name) { + var writeIndex = 0; + var removed = false; + for (var readIndex = 0; readIndex < state.list.length; ++readIndex) { + var entry = state.list[readIndex]; + if (entry[0] === name) { + removed = true; + } else { + state.list[writeIndex++] = entry; + } + } + state.list.length = writeIndex; + return removed; + } + + function fillHeaders(target, init) { + var state = requireHeaderState(target); + if (init === undefined) { + return; + } + if ((typeof init !== "object" && typeof init !== "function") || init === null) { + throw new TypeError("Headers initializer must be an object"); + } + + var iteratorMethod = init[Symbol.iterator]; + if (iteratorMethod !== undefined) { + if (typeof iteratorMethod !== "function") { + throw new TypeError("Headers initializer is not iterable"); + } + for (var entry of init) { + if ((typeof entry !== "object" && typeof entry !== "function") || entry === null) { + throw new TypeError("Each header pair must be iterable"); + } + var pair = Array.from(entry); + if (pair.length !== 2) { + throw new TypeError("Each header pair must contain exactly two items"); + } + appendHeader(state, normalizeHeaderName(pair[0]), normalizeHeaderValue(pair[1])); + } + return; + } + + for (var name of Object.keys(init)) { + appendHeader(state, normalizeHeaderName(name), normalizeHeaderValue(init[name])); + } + } + + function combinedHeaderEntries(state) { + if (state.cacheVersion === state.version) { + return state.cache; + } + var valuesByName = new Map(); + for (var entry of state.list) { + var values = valuesByName.get(entry[0]); + if (!values) { + values = []; + valuesByName.set(entry[0], values); + } + values.push(entry[1]); + } + + var result = []; + var names = Array.from(valuesByName.keys()).sort(); + for (var name of names) { + var values = valuesByName.get(name); + if (name === "set-cookie") { + for (var value of values) { + result.push([name, value]); + } + } else { + result.push([name, values.join(", ")]); + } + } + state.cache = result; + state.cacheVersion = state.version; + return state.cache; + } + + function* iterateHeaders(headers, kind) { + var index = 0; + while (true) { + var entries = combinedHeaderEntries(requireHeaderState(headers)); + if (index >= entries.length) { + return; + } + var entry = entries[index++]; + if (kind === "key") { + yield entry[0]; + } else if (kind === "value") { + yield entry[1]; + } else { + yield [entry[0], entry[1]]; + } + } + } + + class Headers { + constructor(init) { + headerStates.set(this, { + cache: [], + cacheVersion: -1, + guard: "none", + list: [], + version: 0 + }); + fillHeaders(this, init); + } + + append(name, value) { + var state = requireHeaderState(this); + appendHeader(state, normalizeHeaderName(name), normalizeHeaderValue(value)); + } + + delete(name) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + if (state.guard === "response" && isForbiddenResponseHeader(name)) { + return; + } + if (removeHeader(state, name)) { + state.version++; + } + } + + get(name) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + var value = null; + for (var entry of state.list) { + if (entry[0] === name) { + value = value === null ? entry[1] : value + ", " + entry[1]; + } + } + return value; + } + + getSetCookie() { + var state = requireHeaderState(this); + var values = []; + for (var entry of state.list) { + if (entry[0] === "set-cookie") { + values.push(entry[1]); + } + } + return values; + } + + has(name) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + return state.list.some(function(entry) { return entry[0] === name; }); + } + + set(name, value) { + var state = requireHeaderState(this); + name = normalizeHeaderName(name); + value = normalizeHeaderValue(value); + if (state.guard === "response" && isForbiddenResponseHeader(name)) { + return; + } + removeHeader(state, name); + state.list.push([name, value]); + state.version++; + } + + entries() { + requireHeaderState(this); + return iterateHeaders(this, "entry"); + } + + keys() { + requireHeaderState(this); + return iterateHeaders(this, "key"); + } + + values() { + requireHeaderState(this); + return iterateHeaders(this, "value"); + } + + forEach(callback, thisArg) { + requireHeaderState(this); + if (typeof callback !== "function") { + throw new TypeError("Headers.forEach callback must be a function"); + } + for (var entry of this) { + callback.call(thisArg, entry[1], entry[0], this); + } + } + + [Symbol.iterator]() { + return this.entries(); + } + } + + Object.defineProperty(Headers.prototype, Symbol.toStringTag, { + configurable: true, + value: "Headers" + }); + + function createResponseHeaders(init) { + var headers = new Headers(init); + var state = requireHeaderState(headers); + state.guard = "response"; + var removedForbidden = removeHeader(state, "set-cookie"); + removedForbidden = removeHeader(state, "set-cookie2") || removedForbidden; + if (removedForbidden) { + state.version++; + } + return headers; + } + + function isReadableStream(value) { + return global.ReadableStream !== undefined && global.ReadableStream !== null && + value instanceof global.ReadableStream; + } + + function streamFromBytes(bytes) { + var chunk = bytes; + return new global.ReadableStream({ + type: "bytes", + pull: function(controller) { + if (chunk !== null) { + var value = chunk; + chunk = null; + if (value.byteLength !== 0) { + controller.enqueue(value); + } + } + controller.close(); + }, + cancel: function() { + chunk = null; + } + }); + } + + function copyBufferSource(value) { + var source; + if (value instanceof ArrayBuffer) { + source = new Uint8Array(value); + } else { + source = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + var copy = new Uint8Array(source.byteLength); + copy.set(source); + return copy; + } + + function extractBody(body) { + if (body === null || body === undefined) { + return { stream: null, contentType: null }; + } + if (isReadableStream(body)) { + if (body.locked || trackStream(body).disturbed) { + throw new TypeError("Response body stream is already disturbed or locked"); + } + return { stream: body, contentType: null }; + } + if (global.Blob !== undefined && global.Blob !== null && body instanceof global.Blob) { + return { stream: body.stream(), contentType: body.type || null }; + } + if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { + return { stream: streamFromBytes(copyBufferSource(body)), contentType: null }; + } + if (global.URLSearchParams !== undefined && global.URLSearchParams !== null && body instanceof global.URLSearchParams) { + return { + stream: streamFromBytes(new global.TextEncoder().encode(body.toString())), + contentType: "application/x-www-form-urlencoded;charset=UTF-8" + }; + } + if (global.FormData !== undefined && global.FormData !== null && body instanceof global.FormData) { + throw new TypeError("FormData response bodies are not supported by this runtime"); + } + + return { + stream: streamFromBytes(new global.TextEncoder().encode(String(body))), + contentType: "text/plain;charset=UTF-8" + }; + } + + function bodyIsUnusable(state) { + return state.used || (state.body !== null && (state.body.locked || state.bodyTracker.disturbed)); + } + + async function readBodyChunks(state) { + if (state.body === null) { + return [[], 0]; + } + if (bodyIsUnusable(state)) { + throw new TypeError("Response body is already used"); + } + + state.used = true; + var chunks = []; + var total = 0; + var reader = state.body.getReader(); + while (true) { + var result = await reader.read(); + if (result.done) { + return [chunks, total]; + } + if (!(result.value instanceof Uint8Array)) { + throw new TypeError("Response body stream yielded a non-Uint8Array chunk"); + } + if (result.value.byteLength === 0) { + continue; + } + if (total > Number.MAX_SAFE_INTEGER - result.value.byteLength) { + throw new RangeError("Response body is too large"); + } + chunks.push(result.value); + total += result.value.byteLength; + } + } + + function joinChunks(chunksAndTotal) { + var chunks = chunksAndTotal[0]; + var total = chunksAndTotal[1]; + if (chunks.length === 0) { + return new Uint8Array(0); + } + if (chunks.length === 1) { + return chunks[0]; + } + + var bytes = new Uint8Array(total); + var offset = 0; + for (var chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return bytes; + } + + async function consumeText(state) { + if (state.body === null) { + return ""; + } + if (bodyIsUnusable(state)) { + throw new TypeError("Response body is already used"); + } + + state.used = true; + var decoder = new global.TextDecoder(); + var pieces = []; + var reader = state.body.getReader(); + while (true) { + var result = await reader.read(); + if (result.done) { + pieces.push(decoder.decode()); + return pieces.join(""); + } + if (!(result.value instanceof Uint8Array)) { + throw new TypeError("Response body stream yielded a non-Uint8Array chunk"); + } + if (result.value.byteLength !== 0) { + pieces.push(decoder.decode(result.value, { stream: true })); + } + } + } + + function validateResponseInit(init) { + if (init === undefined || init === null) { + init = {}; + } else if (typeof init !== "object" && typeof init !== "function") { + throw new TypeError("Response init must be an object"); + } + + var status = init.status === undefined ? 200 : Math.trunc(Number(init.status)); + if (!Number.isFinite(status) || status < 200 || status > 599) { + throw new RangeError("Response status must be between 200 and 599"); + } + var statusText = init.statusText === undefined ? "" : + toByteString(init.statusText, "Response statusText"); + if (/[\r\n]/.test(statusText)) { + throw new TypeError("Invalid Response statusText"); + } + return { + headers: createResponseHeaders(init.headers), + status: status, + statusText: statusText + }; + } + + function initializeResponse(response, body, init, metadata) { + var normalizedInit = validateResponseInit(init); + var extracted = extractBody(body); + if (extracted.stream !== null && nullBodyStatuses.indexOf(normalizedInit.status) !== -1) { + throw new TypeError("Response with this status cannot have a body"); + } + if (extracted.contentType !== null && !normalizedInit.headers.has("content-type")) { + normalizedInit.headers.append("content-type", extracted.contentType); + } + + responseStates.set(response, { + body: extracted.stream, + bodyTracker: extracted.stream === null ? null : trackStream(extracted.stream), + headers: normalizedInit.headers, + redirected: metadata && metadata.redirected === true, + status: normalizedInit.status, + statusText: normalizedInit.statusText, + type: metadata && metadata.type ? String(metadata.type) : "default", + url: metadata && metadata.url ? String(metadata.url) : "", + used: false + }); + } + + class Response { + constructor(body, init) { + initializeResponse(this, body === undefined ? null : body, init, null); + } + + get body() { return requireResponseState(this).body; } + get bodyUsed() { + var state = requireResponseState(this); + return state.used || (state.body !== null && state.bodyTracker.disturbed); + } + get headers() { return requireResponseState(this).headers; } + get ok() { + var status = requireResponseState(this).status; + return status >= 200 && status <= 299; + } + get redirected() { return requireResponseState(this).redirected; } + get status() { return requireResponseState(this).status; } + get statusText() { return requireResponseState(this).statusText; } + get type() { return requireResponseState(this).type; } + get url() { return requireResponseState(this).url; } + + async arrayBuffer() { + var bytes = joinChunks(await readBodyChunks(requireResponseState(this))); + if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) { + return bytes.buffer; + } + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + } + + async blob() { + var state = requireResponseState(this); + var bytes = joinChunks(await readBodyChunks(state)); + return new global.Blob([bytes], { type: state.headers.get("content-type") || "" }); + } + + async bytes() { + return joinChunks(await readBodyChunks(requireResponseState(this))); + } + + clone() { + var state = requireResponseState(this); + if (bodyIsUnusable(state)) { + throw new TypeError("Cannot clone a used Response"); + } + + var cloneBody = null; + if (state.body !== null) { + var branches = state.body.tee(); + state.body = branches[0]; + state.bodyTracker = trackStream(state.body); + cloneBody = branches[1]; + } + var clone = new Response(cloneBody, { + headers: state.headers, + status: state.status, + statusText: state.statusText + }); + var cloneState = requireResponseState(clone); + cloneState.redirected = state.redirected; + cloneState.type = state.type; + cloneState.url = state.url; + return clone; + } + + async json() { + return JSON.parse(await consumeText(requireResponseState(this))); + } + + async text() { + return consumeText(requireResponseState(this)); + } + + static error() { + var response = Object.create(Response.prototype); + responseStates.set(response, { + body: null, + headers: createResponseHeaders(), + redirected: false, + status: 0, + statusText: "", + type: "error", + url: "", + used: false + }); + return response; + } + + static json(data, init) { + var text = JSON.stringify(data); + if (text === undefined) { + throw new TypeError("Response.json data is not JSON serializable"); + } + var hasExplicitContentType = init !== undefined && init !== null && + new Headers(init.headers).has("content-type"); + var response = new Response(text, init); + if (!hasExplicitContentType) { + response.headers.set("content-type", "application/json"); + } + return response; + } + + static redirect(url, status) { + status = status === undefined ? 302 : Math.trunc(Number(status)); + if ([301, 302, 303, 307, 308].indexOf(status) === -1) { + throw new RangeError("Invalid redirect status"); + } + var location = global.URL !== undefined && global.URL !== null + ? new global.URL(String(url), global.location && global.location.href || undefined).toString() + : String(url); + return new Response(null, { status: status, headers: { location: location } }); + } + } + + Object.defineProperty(Response.prototype, Symbol.toStringTag, { + configurable: true, + value: "Response" + }); + + function createFetchResponse(ResponseConstructor, bytes, init, metadata) { + var status = init && init.status; + var body = nullBodyStatuses.indexOf(status) === -1 ? streamFromBytes(bytes) : null; + var response = new ResponseConstructor(body, init); + var state = responseStates.get(response); + if (state) { + state.redirected = metadata.redirected === true; + state.type = metadata.type || "basic"; + state.url = metadata.url || ""; + } + return response; + } + + return { + Headers: Headers, + Response: Response, + createFetchResponse: createFetchResponse + }; +})(globalThis) diff --git a/Polyfills/Fetch/Source/FetchScripts.h.in b/Polyfills/Fetch/Source/FetchScripts.h.in new file mode 100644 index 00000000..8cc0e511 --- /dev/null +++ b/Polyfills/Fetch/Source/FetchScripts.h.in @@ -0,0 +1,8 @@ +#pragma once + +namespace Babylon::Polyfills::Internal::FetchScripts +{ + inline constexpr char Polyfill[] = R"JSRHFETCH( +@FETCH_POLYFILL_SOURCE@ +)JSRHFETCH"; +} diff --git a/Polyfills/File/Source/File.cpp b/Polyfills/File/Source/File.cpp index 5cc3de02..2bc19e62 100644 --- a/Polyfills/File/Source/File.cpp +++ b/Polyfills/File/Source/File.cpp @@ -50,6 +50,8 @@ namespace Babylon::Polyfills::Internal InstanceMethod("arrayBuffer", &File::ArrayBuffer), InstanceMethod("text", &File::Text), InstanceMethod("bytes", &File::Bytes), + InstanceMethod("slice", &File::Slice), + InstanceMethod("stream", &File::Stream), }); global.Set(JS_FILE_CONSTRUCTOR_NAME, func); @@ -59,12 +61,16 @@ namespace Babylon::Polyfills::Internal // a Blob subtype; BJS core (fileTools, Offline/database, // abstractEngine, thinNativeEngine) branches on `instanceof Blob` // and needs File inputs to satisfy that check. - auto setPrototypeOf = env.Global().Get("Object").As() - .Get("setPrototypeOf").As(); + auto setPrototypeOf = env.Global().Get("Object").As().Get("setPrototypeOf").As(); setPrototypeOf.Call({ func.Get("prototype"), blob.As().Get("prototype"), }); + + auto descriptor = Napi::Object::New(env); + descriptor.Set("configurable", true); + descriptor.Set("value", JS_FILE_CONSTRUCTOR_NAME); + global.Get("Object").As().Get("defineProperty").As().Call(global.Get("Object"), {func.Get("prototype"), Napi::Symbol::WellKnown(env, "toStringTag"), descriptor}); } File::File(const Napi::CallbackInfo& info) @@ -79,7 +85,7 @@ namespace Babylon::Polyfills::Internal { throw Napi::TypeError::New(env, "Failed to construct 'File': 2 arguments required, but only " + - std::to_string(info.Length()) + " present."); + std::to_string(info.Length()) + " present."); } Napi::Value parts = info[0]; @@ -118,21 +124,11 @@ namespace Babylon::Polyfills::Internal } } - Napi::Value partsArray; - if (parts.IsArray()) - { - partsArray = parts; - } - else - { - partsArray = Napi::Array::New(env, 0); - } - // Delegate byte-buffer construction to the native Blob polyfill so // we benefit from its existing BlobPart handling (ArrayBuffer, // typed array, string, Blob). auto blobCtor = env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).As(); - auto blobInstance = blobCtor.New({partsArray, blobOptions}); + auto blobInstance = blobCtor.New({parts, blobOptions}); m_blob = Napi::Persistent(blobInstance); } @@ -173,6 +169,29 @@ namespace Babylon::Polyfills::Internal auto blob = m_blob.Value(); return blob.Get("bytes").As().Call(blob, {}); } + + Napi::Value File::Slice(const Napi::CallbackInfo& info) + { + auto blob = m_blob.Value(); + const auto slice = blob.Get("slice").As(); + switch (info.Length()) + { + case 0: + return slice.Call(blob, {}); + case 1: + return slice.Call(blob, {info[0]}); + case 2: + return slice.Call(blob, {info[0], info[1]}); + default: + return slice.Call(blob, {info[0], info[1], info[2]}); + } + } + + Napi::Value File::Stream(const Napi::CallbackInfo&) + { + auto blob = m_blob.Value(); + return blob.Get("stream").As().Call(blob, {}); + } } namespace Babylon::Polyfills::File diff --git a/Polyfills/File/Source/File.h b/Polyfills/File/Source/File.h index 9d73edb2..49473666 100644 --- a/Polyfills/File/Source/File.h +++ b/Polyfills/File/Source/File.h @@ -22,6 +22,8 @@ namespace Babylon::Polyfills::Internal Napi::Value ArrayBuffer(const Napi::CallbackInfo& info); Napi::Value Text(const Napi::CallbackInfo& info); Napi::Value Bytes(const Napi::CallbackInfo& info); + Napi::Value Slice(const Napi::CallbackInfo& info); + Napi::Value Stream(const Napi::CallbackInfo& info); Napi::ObjectReference m_blob; std::string m_name; diff --git a/Polyfills/IndexedDB/CMakeLists.txt b/Polyfills/IndexedDB/CMakeLists.txt new file mode 100644 index 00000000..2ddad6ef --- /dev/null +++ b/Polyfills/IndexedDB/CMakeLists.txt @@ -0,0 +1,59 @@ +set(INDEXEDDB_POLYFILL_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/ThirdParty/fake-indexeddb/fake-indexeddb.js") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${INDEXEDDB_POLYFILL_FILE}") +file(READ "${INDEXEDDB_POLYFILL_FILE}" INDEXEDDB_POLYFILL_SOURCE) + +# MSVC limits an individual narrow string literal to 16,380 characters +# (C2026). Keep the generated literals comfortably below that limit and join +# them at initialization time without changing bytes at chunk boundaries. +set(INDEXEDDB_POLYFILL_CHUNK_SIZE 8000) +string(LENGTH "${INDEXEDDB_POLYFILL_SOURCE}" INDEXEDDB_POLYFILL_LENGTH) +set(INDEXEDDB_POLYFILL_OFFSET 0) +set(INDEXEDDB_POLYFILL_PARTS "") +while(INDEXEDDB_POLYFILL_OFFSET LESS INDEXEDDB_POLYFILL_LENGTH) + math(EXPR INDEXEDDB_POLYFILL_REMAINING + "${INDEXEDDB_POLYFILL_LENGTH} - ${INDEXEDDB_POLYFILL_OFFSET}") + if(INDEXEDDB_POLYFILL_REMAINING GREATER INDEXEDDB_POLYFILL_CHUNK_SIZE) + set(INDEXEDDB_POLYFILL_PART_LENGTH ${INDEXEDDB_POLYFILL_CHUNK_SIZE}) + else() + set(INDEXEDDB_POLYFILL_PART_LENGTH ${INDEXEDDB_POLYFILL_REMAINING}) + endif() + string(SUBSTRING + "${INDEXEDDB_POLYFILL_SOURCE}" + ${INDEXEDDB_POLYFILL_OFFSET} + ${INDEXEDDB_POLYFILL_PART_LENGTH} + INDEXEDDB_POLYFILL_PART) + string(APPEND INDEXEDDB_POLYFILL_PARTS + " std::string_view{R\"JSRHINDEXEDDB(${INDEXEDDB_POLYFILL_PART})JSRHINDEXEDDB\"},\n") + math(EXPR INDEXEDDB_POLYFILL_OFFSET + "${INDEXEDDB_POLYFILL_OFFSET} + ${INDEXEDDB_POLYFILL_PART_LENGTH}") +endwhile() + +configure_file( + "Source/IndexedDBScripts.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/Generated/IndexedDBScripts.h" + @ONLY) + +set(SOURCES + "Include/Babylon/Polyfills/IndexedDB.h" + "Source/IndexedDB.cpp" + "Source/IndexedDBScripts.h.in" + "ThirdParty/fake-indexeddb/BuildEntry.js" + "ThirdParty/fake-indexeddb/StorageClone.js" + "ThirdParty/fake-indexeddb/LICENSE" + "ThirdParty/fake-indexeddb/fake-indexeddb.js") + +add_library(IndexedDB ${SOURCES}) +warnings_as_errors(IndexedDB) + +target_include_directories(IndexedDB + PUBLIC "Include" + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/Generated") + +target_link_libraries(IndexedDB + PUBLIC JsRuntime + PRIVATE Scheduling) + +set_property(TARGET IndexedDB PROPERTY FOLDER Polyfills) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Polyfills/IndexedDB/Include/Babylon/Polyfills/IndexedDB.h b/Polyfills/IndexedDB/Include/Babylon/Polyfills/IndexedDB.h new file mode 100644 index 00000000..df66db52 --- /dev/null +++ b/Polyfills/IndexedDB/Include/Babylon/Polyfills/IndexedDB.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::IndexedDB +{ + // Installs an in-memory IndexedDB implementation when the selected + // JavaScript engine does not already provide one. + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Polyfills/IndexedDB/README.md b/Polyfills/IndexedDB/README.md new file mode 100644 index 00000000..1cc33593 --- /dev/null +++ b/Polyfills/IndexedDB/README.md @@ -0,0 +1,41 @@ +# IndexedDB + +`Babylon::Polyfills::IndexedDB::Initialize` installs the standard IndexedDB +globals when the selected JavaScript engine does not already provide them. +The implementation is backed by memory for the lifetime of the JavaScript +runtime; it does not persist data across runtime or process restarts. + +The embedded implementation is +[`fake-indexeddb` 6.2.5](https://github.com/dumbmatter/fakeIndexedDB/tree/v6.2.5), +licensed under Apache-2.0. That release passes 82.8% of the applicable +IndexedDB Web Platform Tests and supplies object stores, indexes, cursors, +key ranges, key paths, generated keys, transaction rollback, blocked upgrades, +and version-change events. + +JsRuntimeHost supplies a private storage-clone fallback to the embedded +implementation because bare JavaScript engines do not normally expose the +browser `structuredClone` global. It preserves cyclic graphs, shared +references, typed-array backing buffers, dates, regular expressions, maps, +sets, and errors. It is used only by IndexedDB and does not install a partial +public `structuredClone`. A standards-shaped `DOMException` constructor is +installed only when the engine does not already provide one, so IndexedDB +errors remain testable with normal browser code. + +Applications that require durable storage should install a host-backed +IndexedDB implementation before calling `Initialize`; an existing +`globalThis.indexedDB` is preserved. + +## Updating the embedded implementation + +From this directory: + +```sh +npm install --no-save esbuild@0.28.1 fake-indexeddb@6.2.5 +npx esbuild ThirdParty/fake-indexeddb/BuildEntry.js \ + --bundle --format=iife --target=es2017 --minify --legal-comments=none \ + --inject:ThirdParty/fake-indexeddb/StorageClone.js \ + --banner:js='/* fake-indexeddb 6.2.5 | Apache-2.0 | JsRuntimeHost storage-clone adapter */' \ + --outfile=ThirdParty/fake-indexeddb/fake-indexeddb.js +``` + +Keep the upstream Apache-2.0 `LICENSE` beside the generated bundle. diff --git a/Polyfills/IndexedDB/Source/IndexedDB.cpp b/Polyfills/IndexedDB/Source/IndexedDB.cpp new file mode 100644 index 00000000..52aaa614 --- /dev/null +++ b/Polyfills/IndexedDB/Source/IndexedDB.cpp @@ -0,0 +1,41 @@ +#include +#include + +#include "IndexedDBScripts.h" + +#include + +namespace Babylon::Polyfills::IndexedDB +{ + void BABYLON_API Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + auto global = env.Global(); + const auto indexedDB = global.Get("indexedDB"); + if (!indexedDB.IsUndefined() && !indexedDB.IsNull()) + { + return; + } + + // ChakraCore predates globalThis. fake-indexeddb deliberately targets + // that browser global, so provide the standard alias on older hosts + // before evaluating the bundle while preserving any host definition. + const auto globalThis = global.Get("globalThis"); + if (globalThis.IsUndefined() || globalThis.IsNull()) + { + global.Set("globalThis", global); + } + + // IndexedDB queues database work as tasks rather than microtasks. + Scheduling::Initialize(env); + std::string source; + for (const auto part : Internal::IndexedDBScripts::PolyfillParts) + { + source.append(part.data(), part.size()); + } + Napi::Eval( + env, + source.c_str(), + "jsruntimehost://fake-indexeddb.js"); + } +} diff --git a/Polyfills/IndexedDB/Source/IndexedDBScripts.h.in b/Polyfills/IndexedDB/Source/IndexedDBScripts.h.in new file mode 100644 index 00000000..e77144e8 --- /dev/null +++ b/Polyfills/IndexedDB/Source/IndexedDBScripts.h.in @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace Babylon::Polyfills::Internal::IndexedDBScripts +{ + inline constexpr std::string_view PolyfillParts[]{ +@INDEXEDDB_POLYFILL_PARTS@ }; +} diff --git a/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/BuildEntry.js b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/BuildEntry.js new file mode 100644 index 00000000..1604beb6 --- /dev/null +++ b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/BuildEntry.js @@ -0,0 +1,65 @@ +import fakeIndexedDB from "fake-indexeddb/lib/fakeIndexedDB"; +import FDBCursor from "fake-indexeddb/lib/FDBCursor"; +import FDBCursorWithValue from "fake-indexeddb/lib/FDBCursorWithValue"; +import FDBDatabase from "fake-indexeddb/lib/FDBDatabase"; +import FDBFactory from "fake-indexeddb/lib/FDBFactory"; +import FDBIndex from "fake-indexeddb/lib/FDBIndex"; +import FDBKeyRange from "fake-indexeddb/lib/FDBKeyRange"; +import FDBObjectStore from "fake-indexeddb/lib/FDBObjectStore"; +import FDBOpenDBRequest from "fake-indexeddb/lib/FDBOpenDBRequest"; +import FDBRequest from "fake-indexeddb/lib/FDBRequest"; +import FDBTransaction from "fake-indexeddb/lib/FDBTransaction"; +import FDBVersionChangeEvent from "fake-indexeddb/lib/FDBVersionChangeEvent"; +import { DOMException as StorageDOMException } from "./StorageClone.js"; + +if (Array.prototype.findLast === undefined) { + Object.defineProperty(Array.prototype, "findLast", { + configurable: true, + writable: true, + value(predicate, thisArg) { + for (let index = this.length - 1; index >= 0; --index) { + if (predicate.call(thisArg, this[index], index, this)) { + return this[index]; + } + } + return undefined; + }, + }); +} + +if (Object.hasOwn === undefined) { + Object.defineProperty(Object, "hasOwn", { + configurable: true, + writable: true, + value(object, property) { + return Object.prototype.hasOwnProperty.call(object, property); + }, + }); +} + +const descriptor = value => ({ + value, + enumerable: false, + configurable: true, + writable: true, +}); + +Object.defineProperties(globalThis, { + ...( + typeof globalThis.DOMException === "function" + ? {} + : { DOMException: descriptor(StorageDOMException) } + ), + indexedDB: descriptor(fakeIndexedDB), + IDBCursor: descriptor(FDBCursor), + IDBCursorWithValue: descriptor(FDBCursorWithValue), + IDBDatabase: descriptor(FDBDatabase), + IDBFactory: descriptor(FDBFactory), + IDBIndex: descriptor(FDBIndex), + IDBKeyRange: descriptor(FDBKeyRange), + IDBObjectStore: descriptor(FDBObjectStore), + IDBOpenDBRequest: descriptor(FDBOpenDBRequest), + IDBRequest: descriptor(FDBRequest), + IDBTransaction: descriptor(FDBTransaction), + IDBVersionChangeEvent: descriptor(FDBVersionChangeEvent), +}); diff --git a/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/LICENSE b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/LICENSE new file mode 100644 index 00000000..ad44ec8c --- /dev/null +++ b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/LICENSE @@ -0,0 +1,209 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, and + distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by the + copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all other + entities that control, are controlled by, or are under common control with + that entity. For the purposes of this definition, "control" means (i) the + power, direct or indirect, to cause the direction or management of such + entity, whether by contract or otherwise, or (ii) ownership of + fifty percent (50%) or more of the outstanding shares, or (iii) beneficial + ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity exercising + permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation source, + and configuration files. + + "Object" form shall mean any form resulting from mechanical transformation + or translation of a Source form, including but not limited to compiled + object code, generated documentation, and conversions to + other media types. + + "Work" shall mean the work of authorship, whether in Source or Object + form, made available under the License, as indicated by a copyright notice + that is included in or attached to the work (an example is provided in the + Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object form, + that is based on (or derived from) the Work and for which the editorial + revisions, annotations, elaborations, or other modifications represent, + as a whole, an original work of authorship. For the purposes of this + License, Derivative Works shall not include works that remain separable + from, or merely link (or bind by name) to the interfaces of, the Work and + Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the original + version of the Work and any modifications or additions to that Work or + Derivative Works thereof, that is intentionally submitted to Licensor for + inclusion in the Work by the copyright owner or by an individual or + Legal Entity authorized to submit on behalf of the copyright owner. + For the purposes of this definition, "submitted" means any form of + electronic, verbal, or written communication sent to the Licensor or its + representatives, including but not limited to communication on electronic + mailing lists, source code control systems, and issue tracking systems + that are managed by, or on behalf of, the Licensor for the purpose of + discussing and improving the Work, but excluding communication that is + conspicuously marked or otherwise designated in writing by the copyright + owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity on + behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. + + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable copyright license to reproduce, prepare + Derivative Works of, publicly display, publicly perform, sublicense, + and distribute the Work and such Derivative Works in + Source or Object form. + +3. Grant of Patent License. + + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable (except as stated in this section) patent + license to make, have made, use, offer to sell, sell, import, and + otherwise transfer the Work, where such license applies only to those + patent claims licensable by such Contributor that are necessarily + infringed by their Contribution(s) alone or by combination of their + Contribution(s) with the Work to which such Contribution(s) was submitted. + If You institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work or a + Contribution incorporated within the Work constitutes direct or + contributory patent infringement, then any patent licenses granted to + You under this License for that Work shall terminate as of the date such + litigation is filed. + +4. Redistribution. + + You may reproduce and distribute copies of the Work or Derivative Works + thereof in any medium, with or without modifications, and in Source or + Object form, provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative Works a + copy of this License; and + + 2. You must cause any modified files to carry prominent notices stating + that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that You + distribute, all copyright, patent, trademark, and attribution notices from + the Source form of the Work, excluding those notices that do not pertain + to any part of the Derivative Works; and + + 4. If the Work includes a "NOTICE" text file as part of its distribution, + then any Derivative Works that You distribute must include a readable copy + of the attribution notices contained within such NOTICE file, excluding + those notices that do not pertain to any part of the Derivative Works, + in at least one of the following places: within a NOTICE text file + distributed as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, within a + display generated by the Derivative Works, if and wherever such + third-party notices normally appear. The contents of the NOTICE file are + for informational purposes only and do not modify the License. + You may add Your own attribution notices within Derivative Works that You + distribute, alongside or as an addendum to the NOTICE text from the Work, + provided that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and may + provide additional or different license terms and conditions for use, + reproduction, or distribution of Your modifications, or for any such + Derivative Works as a whole, provided Your use, reproduction, and + distribution of the Work otherwise complies with the conditions + stated in this License. + +5. Submission of Contributions. + + Unless You explicitly state otherwise, any Contribution intentionally + submitted for inclusion in the Work by You to the Licensor shall be under + the terms and conditions of this License, without any additional + terms or conditions. Notwithstanding the above, nothing herein shall + supersede or modify the terms of any separate license agreement you may + have executed with Licensor regarding such Contributions. + +6. Trademarks. + + This License does not grant permission to use the trade names, trademarks, + service marks, or product names of the Licensor, except as required for + reasonable and customary use in describing the origin of the Work and + reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + + Unless required by applicable law or agreed to in writing, Licensor + provides the Work (and each Contributor provides its Contributions) + on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied, including, without limitation, any warranties + or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS + FOR A PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any risks + associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + + In no event and under no legal theory, whether in tort + (including negligence), contract, or otherwise, unless required by + applicable law (such as deliberate and grossly negligent acts) or agreed + to in writing, shall any Contributor be liable to You for damages, + including any direct, indirect, special, incidental, or consequential + damages of any character arising as a result of this License or out of + the use or inability to use the Work (including but not limited to damages + for loss of goodwill, work stoppage, computer failure or malfunction, + or any and all other commercial damages or losses), even if such + Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + + While redistributing the Work or Derivative Works thereof, You may choose + to offer, and charge a fee for, acceptance of support, warranty, + indemnity, or other liability obligations and/or rights consistent with + this License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf of any + other Contributor, and only if You agree to indemnify, defend, and hold + each Contributor harmless for any liability incurred by, or claims + asserted against, such Contributor by reason of your accepting any such + warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + + To apply the Apache License to your work, attach the following boilerplate + notice, with the fields enclosed by brackets "[]" replaced with your own + identifying information. (Don't include the brackets!) The text should be + enclosed in the appropriate comment syntax for the file format. We also + recommend that a file or class name and description of purpose be included + on the same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Jeremy Scheff + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied. See the License for the specific language governing + permissions and limitations under the License. + diff --git a/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/StorageClone.js b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/StorageClone.js new file mode 100644 index 00000000..5930aaab --- /dev/null +++ b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/StorageClone.js @@ -0,0 +1,161 @@ +class StorageDOMException extends Error { + constructor(message = "", name = "Error") { + super(String(message)); + this.name = String(name); + const codes = { + IndexSizeError: 1, + HierarchyRequestError: 3, + WrongDocumentError: 4, + InvalidCharacterError: 5, + NoModificationAllowedError: 7, + NotFoundError: 8, + NotSupportedError: 9, + InUseAttributeError: 10, + InvalidStateError: 11, + SyntaxError: 12, + InvalidModificationError: 13, + NamespaceError: 14, + InvalidAccessError: 15, + TypeMismatchError: 17, + SecurityError: 18, + NetworkError: 19, + AbortError: 20, + URLMismatchError: 21, + QuotaExceededError: 22, + TimeoutError: 23, + InvalidNodeTypeError: 24, + DataCloneError: 25, + }; + Object.defineProperty(this, "code", { + value: codes[this.name] || 0, + enumerable: true, + }); + } +} + +const DOMExceptionImplementation = + typeof globalThis.DOMException === "function" + ? globalThis.DOMException + : StorageDOMException; + +const AggregateErrorImplementation = + typeof globalThis.AggregateError === "function" + ? globalThis.AggregateError + : class AggregateError extends Error { + constructor(errors, message = "") { + super(String(message)); + this.name = "AggregateError"; + this.errors = Array.from(errors); + } + }; + +function dataCloneError(message = "The object could not be cloned.") { + return new DOMExceptionImplementation(message, "DataCloneError"); +} + +function cloneStorageValue(value, seen = new Map()) { + if ( + value === null || + typeof value === "string" || + typeof value === "boolean" || + typeof value === "number" || + typeof value === "bigint" || + value === undefined + ) { + return value; + } + if (typeof value === "function" || typeof value === "symbol") { + throw dataCloneError(); + } + if (seen.has(value)) { + return seen.get(value); + } + if ( + (typeof WeakMap !== "undefined" && value instanceof WeakMap) || + (typeof WeakSet !== "undefined" && value instanceof WeakSet) || + (typeof Promise !== "undefined" && value instanceof Promise) || + (typeof SharedArrayBuffer !== "undefined" && + value instanceof SharedArrayBuffer) + ) { + throw dataCloneError(); + } + + let clone; + if (typeof Blob !== "undefined" && value instanceof Blob) { + if (typeof File !== "undefined" && value instanceof File) { + clone = new File([value], value.name, { + type: value.type, + lastModified: value.lastModified, + }); + } else { + clone = new Blob([value], { type: value.type }); + } + seen.set(value, clone); + return clone; + } + if (value instanceof ArrayBuffer) { + clone = value.slice(0); + seen.set(value, clone); + return clone; + } + if (ArrayBuffer.isView(value)) { + const buffer = cloneStorageValue(value.buffer, seen); + clone = + value instanceof DataView + ? new DataView(buffer, value.byteOffset, value.byteLength) + : new value.constructor(buffer, value.byteOffset, value.length); + seen.set(value, clone); + return clone; + } + if (Array.isArray(value)) { + clone = new Array(value.length); + seen.set(value, clone); + for (const key of Object.keys(value)) { + clone[key] = cloneStorageValue(value[key], seen); + } + return clone; + } + if (value instanceof Date) { + clone = new Date(value.getTime()); + } else if (value instanceof RegExp) { + clone = new RegExp(value.source, value.flags); + clone.lastIndex = value.lastIndex; + } else if (value instanceof Map) { + clone = new Map(); + seen.set(value, clone); + for (const [key, entry] of value) { + clone.set( + cloneStorageValue(key, seen), + cloneStorageValue(entry, seen) + ); + } + return clone; + } else if (value instanceof Set) { + clone = new Set(); + seen.set(value, clone); + for (const entry of value) { + clone.add(cloneStorageValue(entry, seen)); + } + return clone; + } else if (value instanceof Error) { + clone = new Error(value.message); + clone.name = value.name; + if ("stack" in value) { + clone.stack = value.stack; + } + } else { + clone = {}; + } + + seen.set(value, clone); + for (const key of Object.keys(value)) { + clone[key] = cloneStorageValue(value[key], seen); + } + return clone; +} + +export { + AggregateErrorImplementation as AggregateError, + DOMExceptionImplementation as DOMException, + cloneStorageValue as structuredClone, +}; diff --git a/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/fake-indexeddb.js b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/fake-indexeddb.js new file mode 100644 index 00000000..3e1b1d04 --- /dev/null +++ b/Polyfills/IndexedDB/ThirdParty/fake-indexeddb/fake-indexeddb.js @@ -0,0 +1,2 @@ +/* fake-indexeddb 6.2.5 | Apache-2.0 | JsRuntimeHost storage-clone adapter */ +(()=>{var pt=Object.defineProperty,wt=Object.defineProperties;var mt=Object.getOwnPropertyDescriptors;var Ge=Object.getOwnPropertySymbols;var _t=Object.prototype.hasOwnProperty,yt=Object.prototype.propertyIsEnumerable;var pe=(n,e)=>(e=Symbol[n])?e:Symbol.for("Symbol."+n),bt=n=>{throw TypeError(n)};var we=(n,e,t)=>e in n?pt(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t,ze=(n,e)=>{for(var t in e||(e={}))_t.call(e,t)&&we(n,t,e[t]);if(Ge)for(var t of Ge(e))yt.call(e,t)&&we(n,t,e[t]);return n},Ue=(n,e)=>wt(n,mt(e));var a=(n,e,t)=>we(n,typeof e!="symbol"?e+"":e,t);var Ft=function(n,e){this[0]=n,this[1]=e};var W=n=>{var e=n[pe("asyncIterator")],t=!1,r,u={};return e==null?(e=n[pe("iterator")](),r=s=>u[s]=o=>e[s](o)):(e=e.call(n),r=s=>u[s]=o=>{if(t){if(t=!1,s==="throw")throw o;return o}return t=!0,{done:!1,value:new Ft(new Promise(c=>{var i=e[s](o);i instanceof Object||bt("Object expected"),c(i)}),1)}}),u[pe("iterator")]=()=>u,r("next"),"throw"in e?r("throw"):u.throw=s=>{throw s},"return"in e&&r("return"),u};var me=class extends Error{constructor(e="",t="Error"){super(String(e)),this.name=String(t),Object.defineProperty(this,"code",{value:{IndexSizeError:1,HierarchyRequestError:3,WrongDocumentError:4,InvalidCharacterError:5,NoModificationAllowedError:7,NotFoundError:8,NotSupportedError:9,InUseAttributeError:10,InvalidStateError:11,SyntaxError:12,InvalidModificationError:13,NamespaceError:14,InvalidAccessError:15,TypeMismatchError:17,SecurityError:18,NetworkError:19,AbortError:20,URLMismatchError:21,QuotaExceededError:22,TimeoutError:23,InvalidNodeTypeError:24,DataCloneError:25}[this.name]||0,enumerable:!0})}},l=typeof globalThis.DOMException=="function"?globalThis.DOMException:me,h=typeof globalThis.AggregateError=="function"?globalThis.AggregateError:class extends Error{constructor(e,t=""){super(String(t)),this.name="AggregateError",this.errors=Array.from(e)}};function We(n="The object could not be cloned."){return new l(n,"DataCloneError")}function f(n,e=new Map){if(n===null||typeof n=="string"||typeof n=="boolean"||typeof n=="number"||typeof n=="bigint"||n===void 0)return n;if(typeof n=="function"||typeof n=="symbol")throw We();if(e.has(n))return e.get(n);if(typeof WeakMap!="undefined"&&n instanceof WeakMap||typeof WeakSet!="undefined"&&n instanceof WeakSet||typeof Promise!="undefined"&&n instanceof Promise||typeof SharedArrayBuffer!="undefined"&&n instanceof SharedArrayBuffer)throw We();let t;if(typeof Blob!="undefined"&&n instanceof Blob)return typeof File!="undefined"&&n instanceof File?t=new File([n],n.name,{type:n.type,lastModified:n.lastModified}):t=new Blob([n],{type:n.type}),e.set(n,t),t;if(n instanceof ArrayBuffer)return t=n.slice(0),e.set(n,t),t;if(ArrayBuffer.isView(n)){let r=f(n.buffer,e);return t=n instanceof DataView?new DataView(r,n.byteOffset,n.byteLength):new n.constructor(r,n.byteOffset,n.length),e.set(n,t),t}if(Array.isArray(n)){t=new Array(n.length),e.set(n,t);for(let r of Object.keys(n))t[r]=f(n[r],e);return t}if(n instanceof Date)t=new Date(n.getTime());else if(n instanceof RegExp)t=new RegExp(n.source,n.flags),t.lastIndex=n.lastIndex;else if(n instanceof Map){t=new Map,e.set(n,t);for(let[r,u]of n)t.set(f(r,e),f(u,e));return t}else if(n instanceof Set){t=new Set,e.set(n,t);for(let r of n)t.add(f(r,e));return t}else n instanceof Error?(t=new Error(n.message),t.name=n.name,"stack"in n&&(t.stack=n.stack)):t={};e.set(n,t);for(let r of Object.keys(n))t[r]=f(n[r],e);return t}var T={AbortError:"A request was aborted, for example through a call to IDBTransaction.abort.",ConstraintError:"A mutation operation in the transaction failed because a constraint was not satisfied. For example, an object such as an object store or index already exists and a request attempted to create a new one.",DataCloneError:"The data being stored could not be cloned by the internal structured cloning algorithm.",DataError:"Data provided to an operation does not meet requirements.",InvalidAccessError:"An invalid operation was performed on an object. For example transaction creation attempt was made, but an empty scope was provided.",InvalidStateError:"An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.",NotFoundError:"The operation failed because the requested database object could not be found. For example, an object store did not exist but was being opened.",ReadOnlyError:'The mutating operation was attempted in a "readonly" transaction.',TransactionInactiveError:"A request was placed against a transaction which is currently not active, or which is finished.",SyntaxError:"The keypath argument contains an invalid key path",VersionError:"An attempt was made to open a database using a lower version than the existing version."},re=(n,e)=>{Object.defineProperty(n,"code",{value:e,writable:!1,enumerable:!0,configurable:!1})},z=class extends l{constructor(e=T.AbortError){super(e,"AbortError")}},C=class extends l{constructor(e=T.ConstraintError){super(e,"ConstraintError")}};var b=class extends l{constructor(e=T.DataError){super(e,"DataError"),re(this,0)}},R=class extends l{constructor(e=T.InvalidAccessError){super(e,"InvalidAccessError")}},A=class extends l{constructor(e=T.InvalidStateError){super(e,"InvalidStateError"),re(this,11)}},O=class extends l{constructor(e=T.NotFoundError){super(e,"NotFoundError")}},q=class extends l{constructor(e=T.ReadOnlyError){super(e,"ReadOnlyError")}},U=class extends l{constructor(e=T.VersionError){super(e,"SyntaxError"),re(this,12)}},B=class extends l{constructor(e=T.TransactionInactiveError){super(e,"TransactionInactiveError"),re(this,0)}},te=class extends l{constructor(e=T.VersionError){super(e,"VersionError")}};function ne(n){return typeof SharedArrayBuffer!="undefined"&&n instanceof SharedArrayBuffer}var $=Symbol("INVALID_TYPE"),V=Symbol("INVALID_VALUE"),$e=(n,e)=>{if(typeof n=="number")return isNaN(n)?V:n;if(Object.prototype.toString.call(n)==="[object Date]"){let t=n.valueOf();return isNaN(t)?V:new Date(t)}else{if(typeof n=="string")return n;if(n instanceof ArrayBuffer||ne(n)||typeof ArrayBuffer!="undefined"&&ArrayBuffer.isView&&ArrayBuffer.isView(n)){if("detached"in n?n.detached:n.byteLength===0)return V;let t,r=0,u=0;return n instanceof ArrayBuffer||ne(n)?(t=n,u=n.byteLength):(t=n.buffer,r=n.byteOffset,u=n.byteLength),t.slice(r,r+u)}else if(Array.isArray(n)){if(e===void 0)e=new Set;else if(e.has(n))return V;e.add(n);let t=!1,r=Array.from({length:n.length},(u,s)=>{if(t)return;if(!Object.hasOwn(n,s)){t=!0;return}let c=n[s],i=$e(c,e);if(i===V||i===$){t=!0;return}return i});return t?V:r}else return $}},ue=$e;var Dt=(n,e)=>{let t=ue(n,e);if(t===V||t===$)throw new b;return t},w=Dt;var He=n=>{if(typeof n=="number")return"Number";if(Object.prototype.toString.call(n)==="[object Date]")return"Date";if(Array.isArray(n))return"Array";if(typeof n=="string")return"String";if(n instanceof ArrayBuffer)return"Binary";throw new b},Ye=(n,e)=>{if(e===void 0)throw new TypeError;n=w(n),e=w(e);let t=He(n),r=He(e);if(t!==r)return t==="Array"||t==="Binary"&&(r==="String"||r==="Date"||r==="Number")||t==="String"&&(r==="Date"||r==="Number")||t==="Date"&&r==="Number"?1:-1;if(t==="Binary"&&(n=new Uint8Array(n),e=new Uint8Array(e)),t==="Array"||t==="Binary"){let u=Math.min(n.length,e.length);for(let s=0;se.length?1:n.lengthe?1:-1},m=Ye;var _e=class n{static only(e){if(arguments.length===0)throw new TypeError;return e=w(e),new n(e,e,!1,!1)}static lowerBound(e,t=!1){if(arguments.length===0)throw new TypeError;return e=w(e),new n(e,void 0,t,!0)}static upperBound(e,t=!1){if(arguments.length===0)throw new TypeError;return e=w(e),new n(void 0,e,!0,t)}static bound(e,t,r=!1,u=!1){if(arguments.length<2)throw new TypeError;let s=m(e,t);if(s===1||s===0&&(r||u))throw new b;return e=w(e),t=w(t),new n(e,t,r,u)}constructor(e,t,r,u){this.lower=e,this.upper=t,this.lowerOpen=r,this.upperOpen=u}includes(e){if(arguments.length===0)throw new TypeError;if(e=w(e),this.lower!==void 0){let t=m(this.lower,e);if(t===1||t===0&&this.lowerOpen)return!1}if(this.upper!==void 0){let t=m(this.upper,e);if(t===-1||t===0&&this.upperOpen)return!1}return!0}get[Symbol.toStringTag](){return"IDBKeyRange"}},p=_e;var Qe=(n,e)=>{if(Array.isArray(n)){let u=[];for(let s of n){s!=null&&typeof s!="string"&&s.toString&&(s=s.toString());let o=Qe(s,e).key;u.push(w(o))}return{type:"found",key:u}}if(n==="")return{type:"found",key:e};let t=n,r=e;for(;t!==null;){let u,s=t.indexOf(".");if(s>=0?(u=t.slice(0,s),t=t.slice(s+1)):(u=t,t=null),!(u==="length"&&(typeof r=="string"||Array.isArray(r))||(u==="size"||u==="type")&&typeof Blob!="undefined"&&r instanceof Blob||(u==="name"||u==="lastModified")&&typeof File!="undefined"&&r instanceof File)&&(typeof r!="object"||r===null||!Object.hasOwn(r,u)))return{type:"notFound"};r=r[u]}return{type:"found",key:r}},K=Qe;function se(n,e){if(e._state!=="active")throw new Error("Assert: transaction state is active");e._state="inactive";try{return f(n)}finally{e._state="active"}}var Z=n=>n.source instanceof S?n.source:n.source.objectStore,oe=(n,e,t)=>{let r=n!==void 0?n.lower:void 0,u=n!==void 0?n.upper:void 0;for(let s of e)s!==void 0&&(r===void 0||m(r,s)===1)&&(r=s);for(let s of t)s!==void 0&&(u===void 0||m(u,s)===-1)&&(u=s);if(r!==void 0&&u!==void 0)return p.bound(r,u);if(r!==void 0)return p.lowerBound(r);if(u!==void 0)return p.upperBound(u)},ye=class{constructor(e,t,r="next",u,s=!1){a(this,"_gotValue",!1);a(this,"_position");a(this,"_objectStorePosition");a(this,"_keyOnly",!1);a(this,"_key");a(this,"_primaryKey");this._range=t,this._source=e,this._direction=r,this._request=u,this._keyOnly=s}get source(){return this._source}set source(e){}get request(){return this._request}set request(e){}get direction(){return this._direction}set direction(e){}get key(){return this._key}set key(e){}get primaryKey(){return this._primaryKey}set primaryKey(e){}_iterate(e,t){let r=this.source instanceof S,u=this.source instanceof S?this.source._rawObjectStore.records:this.source._rawIndex.records,s;if(this.direction==="next"){let c=oe(this._range,[e,this._position],[]);for(let i of u.values(c)){let d=e!==void 0?m(i.key,e):void 0,_=this._position!==void 0?m(i.key,this._position):void 0;if(!(e!==void 0&&d===-1)){if(t!==void 0){if(d===-1)continue;let y=m(i.value,t);if(d===0&&y===-1)continue}if(!(this._position!==void 0&&r&&_!==1)&&!(this._position!==void 0&&!r&&(_===-1||_===0&&m(i.value,this._objectStorePosition)!==1))&&!(this._range!==void 0&&!this._range.includes(i.key))){s=i;break}}}}else if(this.direction==="nextunique"){let c=oe(this._range,[e,this._position],[]);for(let i of u.values(c))if(!(e!==void 0&&m(i.key,e)===-1)&&!(this._position!==void 0&&m(i.key,this._position)!==1)&&!(this._range!==void 0&&!this._range.includes(i.key))){s=i;break}}else if(this.direction==="prev"){let c=oe(this._range,[],[e,this._position]);for(let i of u.values(c,"prev")){let d=e!==void 0?m(i.key,e):void 0,_=this._position!==void 0?m(i.key,this._position):void 0;if(!(e!==void 0&&d===1)){if(t!==void 0){if(d===1)continue;let y=m(i.value,t);if(d===0&&y===1)continue}if(!(this._position!==void 0&&r&&_!==-1)&&!(this._position!==void 0&&!r&&(_===1||_===0&&m(i.value,this._objectStorePosition)!==-1))&&!(this._range!==void 0&&!this._range.includes(i.key))){s=i;break}}}}else if(this.direction==="prevunique"){let c,i=oe(this._range,[],[e,this._position]);for(let d of u.values(i,"prev"))if(!(e!==void 0&&m(d.key,e)===1)&&!(this._position!==void 0&&m(d.key,this._position)!==-1)&&!(this._range!==void 0&&!this._range.includes(d.key))){c=d;break}c&&(s=u.get(c.key))}let o;if(!s)this._key=void 0,r||(this._objectStorePosition=void 0),!this._keyOnly&&this.toString()==="[object IDBCursorWithValue]"&&(this.value=void 0),o=null;else{if(this._position=s.key,r||(this._objectStorePosition=s.value),this._key=s.key,r)this._primaryKey=f(s.key),!this._keyOnly&&this.toString()==="[object IDBCursorWithValue]"&&(this.value=f(s.value));else if(this._primaryKey=f(s.value),!this._keyOnly&&this.toString()==="[object IDBCursorWithValue]"){if(this.source instanceof S)throw new Error("This should never happen");let c=this.source.objectStore._rawObjectStore.getValue(s.value);this.value=f(c)}this._gotValue=!0,o=this}return o}update(e){if(e===void 0)throw new TypeError;let t=Z(this),r=Object.hasOwn(this.source,"_rawIndex")?this.primaryKey:this._position,u=t.transaction;if(u._state!=="active")throw new B;if(u.mode==="readonly")throw new q;if(t._rawObjectStore.deleted)throw new A;if(!(this.source instanceof S)&&this.source._rawIndex.deleted)throw new A;if(!this._gotValue||!Object.hasOwn(this,"value"))throw new A;let s=se(e,u);if(t.keyPath!==null){let c;try{c=K(t.keyPath,s).key}catch(i){}if(m(c,r)!==0)throw new b}let o={key:r,value:s};return u._execRequestAsync({operation:t._rawObjectStore.storeRecord.bind(t._rawObjectStore,o,!1,u._rollbackLog),source:this})}advance(e){if(!Number.isInteger(e)||e<=0)throw new TypeError;let t=Z(this),r=t.transaction;if(r._state!=="active")throw new B;if(t._rawObjectStore.deleted)throw new A;if(!(this.source instanceof S)&&this.source._rawIndex.deleted)throw new A;if(!this._gotValue)throw new A;this._request&&(this._request.readyState="pending"),r._execRequestAsync({operation:()=>{let u;for(let s=0;s=0&&(this.direction==="prev"||this.direction==="prevunique"))throw new b}this._request&&(this._request.readyState="pending"),r._execRequestAsync({operation:this._iterate.bind(this,e),request:this._request,source:this.source}),this._gotValue=!1}continuePrimaryKey(e,t){let r=Z(this),u=r.transaction;if(u._state!=="active")throw new B;if(r._rawObjectStore.deleted)throw new A;if(!(this.source instanceof S)&&this.source._rawIndex.deleted)throw new A;if(this.source instanceof S||this.direction!=="next"&&this.direction!=="prev")throw new R;if(!this._gotValue)throw new A;if(e===void 0||t===void 0)throw new b;e=w(e);let s=m(e,this._position);if(s===-1&&this.direction==="next"||s===1&&this.direction==="prev")throw new b;let o=m(t,this._objectStorePosition);if(s===0&&(o<=0&&this.direction==="next"||o>=0&&this.direction==="prev"))throw new b;this._request&&(this._request.readyState="pending"),u._execRequestAsync({operation:this._iterate.bind(this,e,t),request:this._request,source:this.source}),this._gotValue=!1}delete(){let e=Z(this),t=Object.hasOwn(this.source,"_rawIndex")?this.primaryKey:this._position,r=e.transaction;if(r._state!=="active")throw new B;if(r.mode==="readonly")throw new q;if(e._rawObjectStore.deleted)throw new A;if(!(this.source instanceof S)&&this.source._rawIndex.deleted)throw new A;if(!this._gotValue||!Object.hasOwn(this,"value"))throw new A;return r._execRequestAsync({operation:e._rawObjectStore.deleteRecord.bind(e._rawObjectStore,t,r._rollbackLog),source:this})}get[Symbol.toStringTag](){return"IDBCursor"}},P=ye;var be=class extends P{constructor(t,r,u,s){super(t,r,u,s);a(this,"value")}get[Symbol.toStringTag](){return"IDBCursorWithValue"}},H=be;var Xe=(n,e)=>n.immediatePropagationStopped||n.eventPhase===n.CAPTURING_PHASE&&e.capture===!1||n.eventPhase===n.BUBBLING_PHASE&&e.capture===!0,Fe=(n,e)=>{n.currentTarget=e;let t=[],r=c=>{try{(typeof c=="function"?c:c.handleEvent).call(n.currentTarget,n)}catch(i){t.push(i)}};for(let c of e.listeners.slice())n.type!==c.type||Xe(n,c)||r(c.callback);let s={abort:"onabort",blocked:"onblocked",close:"onclose",complete:"oncomplete",error:"onerror",success:"onsuccess",upgradeneeded:"onupgradeneeded",versionchange:"onversionchange"}[n.type];if(s===void 0)throw new Error(`Unknown event type: "${n.type}"`);let o=n.currentTarget[s];if(o){let c={callback:o,capture:!1,type:n.type};Xe(n,c)||r(c.callback)}if(t.length)throw new h(t)},De=class{constructor(){a(this,"listeners",[])}addEventListener(e,t,r){let u=!!(typeof r=="object"&&r?r.capture:r);this.listeners.push({callback:t,capture:u,type:e})}removeEventListener(e,t,r){let u=!!(typeof r=="object"&&r?r.capture:r),s=this.listeners.findIndex(o=>o.type===e&&o.callback===t&&o.capture===u);this.listeners.splice(s,1)}dispatchEvent(e){if(e.dispatched||!e.initialized)throw new A("The object is in an invalid state.");e.isTrusted=!1,e.dispatched=!0,e.target=this,e.eventPhase=e.CAPTURING_PHASE;for(let t of e.eventPath)e.propagationStopped||Fe(e,t);if(e.eventPhase=e.AT_TARGET,e.propagationStopped||Fe(e,e.target),e.bubbles){e.eventPath.reverse(),e.eventPhase=e.BUBBLING_PHASE;for(let t of e.eventPath)e.propagationStopped||Fe(e,t)}return e.dispatched=!1,e.eventPhase=e.NONE,e.currentTarget=null,!e.canceled}},Y=De;var ge=class extends Y{constructor(){super(...arguments);a(this,"_result",null);a(this,"_error",null);a(this,"source",null);a(this,"transaction",null);a(this,"readyState","pending");a(this,"onsuccess",null);a(this,"onerror",null)}get error(){if(this.readyState==="pending")throw new A;return this._error}set error(t){this._error=t}get result(){if(this.readyState==="pending")throw new A;return this._result}set result(t){this._result=t}get[Symbol.toStringTag](){return"IDBRequest"}},I=ge;var Be=class{constructor(...e){this._values=e;for(let t=0;t=this._values.length?null:this._values[e]}get length(){return this._values.length}[Symbol.iterator](){return this._values[Symbol.iterator]()}_push(...e){for(let t=0;t{if(n instanceof p)return n;if(n==null){if(e)throw new b;return new p(void 0,void 0,!1,!1)}let t=w(n);return p.only(t)},L=gt;var Ze=n=>typeof n=="object"&&n?n+"":n;function ie(n){return Array.isArray(n)?n.map(Ze):Ze(n)}var Bt=n=>n instanceof p?!0:ue(n)!==$,Je=Bt;var Et=(n,e)=>{let r=e==="unsigned long"?4294967295:9007199254740991;if(isNaN(n)||n<0||n>r)throw new TypeError;if(n>=0)return Math.floor(n)},k=Et;var Ct=(n,e,t)=>{let r,u;if(n==null||Je(n))r=n,t>1&&e!==void 0&&(e=k(e,"unsigned long"));else{let s=n;s.query!==void 0&&(r=s.query),s.count!==void 0&&(e=k(s.count,"unsigned long")),s.direction!==void 0&&(u=s.direction)}return{query:r,count:e,direction:u}},Q=Ct;var M=n=>{if(n._rawIndex.deleted||n.objectStore._rawObjectStore.deleted)throw new A;if(n.objectStore.transaction._state!=="active")throw new B},Ee=class{constructor(e,t){this._rawIndex=t,this._name=t.name,this.objectStore=e,this.keyPath=ie(t.keyPath),this.multiEntry=t.multiEntry,this.unique=t.unique}get name(){return this._name}set name(e){let t=this.objectStore.transaction;if(!t.db._runningVersionchangeTransaction)throw t._state==="active"?new A:new B;if(t._state!=="active")throw new B;if(this._rawIndex.deleted||this.objectStore._rawObjectStore.deleted)throw new A;if(e=String(e),e===this._name)return;if(this.objectStore.indexNames.contains(e))throw new C;let r=this._name,u=[...this.objectStore.indexNames];this._name=e,this._rawIndex.name=e,this.objectStore._indexesCache.delete(r),this.objectStore._indexesCache.set(e,this),this.objectStore._rawObjectStore.rawIndexes.delete(r),this.objectStore._rawObjectStore.rawIndexes.set(e,this._rawIndex),this.objectStore.indexNames=new F(...Array.from(this.objectStore._rawObjectStore.rawIndexes.keys()).filter(s=>{let o=this.objectStore._rawObjectStore.rawIndexes.get(s);return o&&!o.deleted}).sort()),this.objectStore.transaction._createdIndexes.has(this._rawIndex)||t._rollbackLog.push(()=>{this._name=r,this._rawIndex.name=r,this.objectStore._indexesCache.delete(e),this.objectStore._indexesCache.set(r,this),this.objectStore._rawObjectStore.rawIndexes.delete(e),this.objectStore._rawObjectStore.rawIndexes.set(r,this._rawIndex),this.objectStore.indexNames=new F(...u)})}openCursor(e,t){M(this),e===null&&(e=void 0),e!==void 0&&!(e instanceof p)&&(e=p.only(w(e)));let r=new I;r.source=this,r.transaction=this.objectStore.transaction;let u=new H(this,e,t,r);return this.objectStore.transaction._execRequestAsync({operation:u._iterate.bind(u),request:r,source:this})}openKeyCursor(e,t){M(this),e===null&&(e=void 0),e!==void 0&&!(e instanceof p)&&(e=p.only(w(e)));let r=new I;r.source=this,r.transaction=this.objectStore.transaction;let u=new P(this,e,t,r,!0);return this.objectStore.transaction._execRequestAsync({operation:u._iterate.bind(u),request:r,source:this})}get(e){return M(this),e instanceof p||(e=w(e)),this.objectStore.transaction._execRequestAsync({operation:this._rawIndex.getValue.bind(this._rawIndex,e),source:this})}getAll(e,t){let r=Q(e,t,arguments.length);M(this);let u=L(r.query);return this.objectStore.transaction._execRequestAsync({operation:this._rawIndex.getAllValues.bind(this._rawIndex,u,r.count,r.direction),source:this})}getKey(e){return M(this),e instanceof p||(e=w(e)),this.objectStore.transaction._execRequestAsync({operation:this._rawIndex.getKey.bind(this._rawIndex,e),source:this})}getAllKeys(e,t){let r=Q(e,t,arguments.length);M(this);let u=L(r.query);return this.objectStore.transaction._execRequestAsync({operation:this._rawIndex.getAllKeys.bind(this._rawIndex,u,r.count,r.direction),source:this})}getAllRecords(e){let t,r,u;e!==void 0&&(e.query!==void 0&&(t=e.query),e.count!==void 0&&(r=k(e.count,"unsigned long")),e.direction!==void 0&&(u=e.direction)),M(this);let s=L(t);return this.objectStore.transaction._execRequestAsync({operation:this._rawIndex.getAllRecords.bind(this._rawIndex,s,r,u),source:this})}count(e){return M(this),e===null&&(e=void 0),e!==void 0&&!(e instanceof p)&&(e=p.only(w(e))),this.objectStore.transaction._execRequestAsync({operation:()=>this._rawIndex.count(e),source:this})}get[Symbol.toStringTag](){return"IDBIndex"}},J=Ee;var St=(n,e)=>{if(Array.isArray(n))throw new Error("The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.");let t=n.split(".");if(t.length===0)throw new Error("Assert: identifiers is not empty");t.pop();for(let r of t){if(typeof e!="object"&&!Array.isArray(e))return!1;if(!Object.hasOwn(e,r))return!0;e=e[r]}return typeof e=="object"||Array.isArray(e)},et=St;var Ce=class{constructor(e,t,r){this._key=e,this._primaryKey=t,this._value=r}get key(){return this._key}set key(e){}get primaryKey(){return this._primaryKey}set primaryKey(e){}get value(){return this._value}set value(e){}get[Symbol.toStringTag](){return"IDBRecord"}},ae=Ce;var xt=2/3,jt=new p(void 0,void 0,!1,!1),X=class{constructor(e){a(this,"_numTombstones",0);a(this,"_numNodes",0);this._keysAreUnique=!!e}size(){return this._numNodes-this._numTombstones}get(e){return this._getByComparator(this._root,t=>this._compare(e,t))}contains(e){return!!this.get(e)}_compare(e,t){let r=m(e.key,t.key);return r!==0?r:this._keysAreUnique?0:m(e.value,t.value)}_getByComparator(e,t){let r=e;for(;r;){let u=t(r.record);if(u<0)r=r.left;else if(u>0)r=r.right;else return r.record}}put(e,t=!1){if(!this._root){this._root={record:e,left:void 0,right:void 0,parent:void 0,deleted:!1,red:!1},this._numNodes++;return}return this._put(this._root,e,t)}_put(e,t,r){let u=this._compare(t,e.record);if(u<0){if(e.left)return this._put(e.left,t,r);e.left={record:t,left:void 0,right:void 0,parent:e,deleted:!1,red:!0},this._onNewNodeInserted(e.left)}else if(u>0){if(e.right)return this._put(e.right,t,r);e.right={record:t,left:void 0,right:void 0,parent:e,deleted:!1,red:!0},this._onNewNodeInserted(e.right)}else if(e.deleted)e.deleted=!1,e.record=t,this._numTombstones--;else{if(r)throw new C;{let s=e.record;return e.record=t,s}}}delete(e){if(this._root&&(this._delete(this._root,e),this._numTombstones>this._numNodes*xt)){let t=[...this.getAllRecords()];this._root=this._rebuild(t,void 0,!1),this._numNodes=t.length,this._numTombstones=0}}_delete(e,t){if(!e)return;let r=this._compare(t,e.record);r<0?this._delete(e.left,t):r>0?this._delete(e.right,t):e.deleted||(this._numTombstones++,e.deleted=!0)}*getAllRecords(e=!1){yield*W(this.getRecords(jt,e))}*getRecords(e,t=!1){yield*W(this._getRecordsForNode(this._root,e,t))}*_getRecordsForNode(e,t,r=!1){e&&(yield*W(this._findRecords(e,t,r)))}*_findRecords(e,t,r=!1){let{lower:u,upper:s,lowerOpen:o,upperOpen:c}=t,{record:{key:i}}=e,d=u===void 0?-1:m(u,i),_=s===void 0?1:m(s,i),y=this._keysAreUnique?d<0:d<=0,E=this._keysAreUnique?_>0:_>=0,g=r?E:y,G=r?y:E,Le=r?"right":"left",Me=r?"left":"right",ht=o?d<0:d<=0,At=c?_>0:_>=0;g&&e[Le]&&(yield*W(this._findRecords(e[Le],t,r))),ht&&At&&!e.deleted&&(yield e.record),G&&e[Me]&&(yield*W(this._findRecords(e[Me],t,r)))}_onNewNodeInserted(e){this._numNodes++,this._rebalanceTree(e)}_rebalanceTree(e){let t=e.parent;do{if(!t.red)return;let r=t.parent;if(!r){t.red=!1;return}let u=t===r.right,s=u?r.left:r.right;if(!s||!s.red){e===(u?t.left:t.right)&&(this._rotateSubtree(t,u),e=t,t=u?r.right:r.left),this._rotateSubtree(r,!u),t.red=!1,r.red=!0;return}t.red=!1,s.red=!1,r.red=!0,e=r}while(e.parent&&(t=e.parent))}_rotateSubtree(e,t){let r=e.parent,u=t?e.left:e.right,s=t?u.right:u.left;return e[t?"left":"right"]=s,s&&(s.parent=e),u[t?"right":"left"]=e,u.parent=r,e.parent=u,r?r[e===r.right?"right":"left"]=u:this._root=u,u}_rebuild(e,t,r){let{length:u}=e;if(!u)return;let s=u>>>1,o={record:e[s],left:void 0,right:void 0,parent:t,deleted:!1,red:r},c=this._rebuild(e.slice(0,s),o,!r),i=this._rebuild(e.slice(s+1),o,!r);return o.left=c,o.right=i,o}};var Se=class{constructor(e){this.keysAreUnique=e,this.records=new X(this.keysAreUnique)}get(e){let t=e instanceof p?e:p.only(e);return this.records.getRecords(t).next().value}put(e,t=!1){return this.records.put(e,t)}delete(e){let t=e instanceof p?e:p.only(e),r=[...this.records.getRecords(t)];for(let u of r)this.records.delete(u);return r}deleteByValue(e){let t=e instanceof p?e:p.only(e),r=[];for(let u of this.records.getAllRecords())t.includes(u.value)&&(this.records.delete(u),r.push(u));return r}clear(){let e=[...this.records.getAllRecords()];return this.records=new X(this.keysAreUnique),e}values(e,t="next"){let r=t==="prev"||t==="prevunique",u=e?this.records.getRecords(e,r):this.records.getAllRecords(r);return{[Symbol.iterator]:()=>{let s=()=>u.next();if(t==="next"||t==="prev")return{next:s};if(t==="nextunique"){let i;return{next:()=>{let d=s();for(;!d.done&&i!==void 0&&m(i.key,d.value.key)===0;)d=s();return i=d.value,d}}}let o=s(),c=s();return{next:()=>{for(;!c.done&&m(o.value.key,c.value.key)===0;)o=c,c=s();let i=o;return o=c,c=s(),i}}}}}size(){return this.records.size()}},ce=Se;var xe=class{constructor(e,t,r,u,s){a(this,"deleted",!1);a(this,"initialized",!1);this.rawObjectStore=e,this.name=t,this.keyPath=r,this.multiEntry=u,this.unique=s,this.records=new ce(s)}getKey(e){let t=this.records.get(e);return t!==void 0?t.value:void 0}getAllKeys(e,t,r){(t===void 0||t===0)&&(t=1/0);let u=[];for(let s of this.records.values(e,r))if(u.push(f(s.value)),u.length>=t)break;return u}getValue(e){let t=this.records.get(e);return t!==void 0?this.rawObjectStore.getValue(t.value):void 0}getAllValues(e,t,r){(t===void 0||t===0)&&(t=1/0);let u=[];for(let s of this.records.values(e,r))if(u.push(this.rawObjectStore.getValue(s.value)),u.length>=t)break;return u}getAllRecords(e,t,r){(t===void 0||t===0)&&(t=1/0);let u=[];for(let s of this.records.values(e,r))if(u.push(new ae(f(s.key),f(this.rawObjectStore.getKey(s.value)),this.rawObjectStore.getValue(s.value))),u.length>=t)break;return u}storeRecord(e){let t;try{t=K(this.keyPath,e.value).key}catch(r){if(r.name==="DataError")return;throw r}if(!this.multiEntry||!Array.isArray(t))try{w(t)}catch(r){return}else{let r=[];for(let u of t)if(r.indexOf(u)<0)try{r.push(w(u))}catch(s){}t=r}if(!this.multiEntry||!Array.isArray(t)){if(this.unique&&this.records.get(t))throw new C}else if(this.unique){for(let r of t)if(this.records.get(r))throw new C}if(!this.multiEntry||!Array.isArray(t))this.records.put({key:t,value:e.key});else for(let r of t)this.records.put({key:r,value:e.key})}initialize(e){if(this.initialized)throw new Error("Index already initialized");e._execRequestAsync({operation:()=>{try{for(let t of this.rawObjectStore.records.values())this.storeRecord(t);this.initialized=!0}catch(t){e._abort(t.name)}},source:null})}count(e){let t=0;for(let r of this.records.values(e))t+=1;return t}},tt=xe;var je=(n,e)=>{if(n!=null&&typeof n!="string"&&n.toString&&(e==="array"||!Array.isArray(n))&&(n=n.toString()),typeof n=="string"){if(n===""&&e!=="string")return;try{let t=/^(?:[$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B2\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])(?:[$0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B2\u08E4-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA69D\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2D\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])*$/;if(n.length>=1&&t.test(n))return}catch(t){throw new U(t.message)}if(n.indexOf(" ")>=0)throw new U("The keypath argument contains an invalid key path (no spaces allowed).")}if(Array.isArray(n)&&n.length>0){if(e)throw new U("The keypath argument contains an invalid key path (nested arrays).");for(let t of n)je(t,"array");return}else if(typeof n=="string"&&n.indexOf(".")>=0){n=n.split(".");for(let t of n)je(t,"string");return}throw new U},de=je;var x=n=>{if(n._rawObjectStore.deleted)throw new A;if(n.transaction._state!=="active")throw new B},rt=(n,e,t)=>{if(x(n),n.transaction.mode==="readonly")throw new q;if(n.keyPath!==null&&t!==void 0)throw new b;let r=se(e,n.transaction);if(n.keyPath!==null){let u=K(n.keyPath,r);if(u.type==="found")w(u.key);else if(n._rawObjectStore.keyGenerator){if(!et(n.keyPath,r))throw new b}else throw new b}if(n.keyPath===null&&n._rawObjectStore.keyGenerator===null&&t===void 0)throw new b;return t!==void 0&&(t=w(t)),{key:t,value:r}},Ie=class{constructor(e,t){a(this,"_indexesCache",new Map);this._rawObjectStore=t,this._name=t.name,this.keyPath=ie(t.keyPath),this.autoIncrement=t.autoIncrement,this.transaction=e,this.indexNames=new F(...Array.from(t.rawIndexes.keys()).sort())}get name(){return this._name}set name(e){let t=this.transaction;if(!t.db._runningVersionchangeTransaction)throw t._state==="active"?new A:new B;if(x(this),e=String(e),e===this._name)return;if(this._rawObjectStore.rawDatabase.rawObjectStores.has(e))throw new C;let r=this._name,u=[...t.db.objectStoreNames];this._name=e,this._rawObjectStore.name=e,this.transaction._objectStoresCache.delete(r),this.transaction._objectStoresCache.set(e,this),this._rawObjectStore.rawDatabase.rawObjectStores.delete(r),this._rawObjectStore.rawDatabase.rawObjectStores.set(e,this._rawObjectStore),t.db.objectStoreNames=new F(...Array.from(this._rawObjectStore.rawDatabase.rawObjectStores.keys()).filter(c=>{let i=this._rawObjectStore.rawDatabase.rawObjectStores.get(c);return i&&!i.deleted}).sort());let s=new Set(t._scope),o=[...t.objectStoreNames];this.transaction._scope.delete(r),t._scope.add(e),t.objectStoreNames=new F(...Array.from(t._scope).sort()),this.transaction._createdObjectStores.has(this._rawObjectStore)||t._rollbackLog.push(()=>{this._name=r,this._rawObjectStore.name=r,this.transaction._objectStoresCache.delete(e),this.transaction._objectStoresCache.set(r,this),this._rawObjectStore.rawDatabase.rawObjectStores.delete(e),this._rawObjectStore.rawDatabase.rawObjectStores.set(r,this._rawObjectStore),t.db.objectStoreNames=new F(...u),t._scope=s,t.objectStoreNames=new F(...o)})}put(e,t){if(arguments.length===0)throw new TypeError;let r=rt(this,e,t);return this.transaction._execRequestAsync({operation:this._rawObjectStore.storeRecord.bind(this._rawObjectStore,r,!1,this.transaction._rollbackLog),source:this})}add(e,t){if(arguments.length===0)throw new TypeError;let r=rt(this,e,t);return this.transaction._execRequestAsync({operation:this._rawObjectStore.storeRecord.bind(this._rawObjectStore,r,!0,this.transaction._rollbackLog),source:this})}delete(e){if(arguments.length===0)throw new TypeError;if(x(this),this.transaction.mode==="readonly")throw new q;return e instanceof p||(e=w(e)),this.transaction._execRequestAsync({operation:this._rawObjectStore.deleteRecord.bind(this._rawObjectStore,e,this.transaction._rollbackLog),source:this})}get(e){if(arguments.length===0)throw new TypeError;return x(this),e instanceof p||(e=w(e)),this.transaction._execRequestAsync({operation:this._rawObjectStore.getValue.bind(this._rawObjectStore,e),source:this})}getAll(e,t){let r=Q(e,t,arguments.length);x(this);let u=L(r.query);return this.transaction._execRequestAsync({operation:this._rawObjectStore.getAllValues.bind(this._rawObjectStore,u,r.count,r.direction),source:this})}getKey(e){if(arguments.length===0)throw new TypeError;return x(this),e instanceof p||(e=w(e)),this.transaction._execRequestAsync({operation:this._rawObjectStore.getKey.bind(this._rawObjectStore,e),source:this})}getAllKeys(e,t){let r=Q(e,t,arguments.length);x(this);let u=L(r.query);return this.transaction._execRequestAsync({operation:this._rawObjectStore.getAllKeys.bind(this._rawObjectStore,u,r.count,r.direction),source:this})}getAllRecords(e){let t,r,u;e!==void 0&&(e.query!==void 0&&(t=e.query),e.count!==void 0&&(r=k(e.count,"unsigned long")),e.direction!==void 0&&(u=e.direction)),x(this);let s=L(t);return this.transaction._execRequestAsync({operation:this._rawObjectStore.getAllRecords.bind(this._rawObjectStore,s,r,u),source:this})}clear(){if(x(this),this.transaction.mode==="readonly")throw new q;return this.transaction._execRequestAsync({operation:this._rawObjectStore.clear.bind(this._rawObjectStore,this.transaction._rollbackLog),source:this})}openCursor(e,t){x(this),e===null&&(e=void 0),e!==void 0&&!(e instanceof p)&&(e=p.only(w(e)));let r=new I;r.source=this,r.transaction=this.transaction;let u=new H(this,e,t,r);return this.transaction._execRequestAsync({operation:u._iterate.bind(u),request:r,source:this})}openKeyCursor(e,t){x(this),e===null&&(e=void 0),e!==void 0&&!(e instanceof p)&&(e=p.only(w(e)));let r=new I;r.source=this,r.transaction=this.transaction;let u=new P(this,e,t,r,!0);return this.transaction._execRequestAsync({operation:u._iterate.bind(u),request:r,source:this})}createIndex(e,t,r={}){if(arguments.length<2)throw new TypeError;let u=r.multiEntry!==void 0?r.multiEntry:!1,s=r.unique!==void 0?r.unique:!1;if(this.transaction.mode!=="versionchange")throw new A;if(x(this),this.indexNames.contains(e))throw new C;if(de(t),Array.isArray(t)&&u)throw new R;let o=[...this.indexNames],c=new tt(this._rawObjectStore,e,t,u,s);return this.indexNames._push(e),this.indexNames._sort(),this.transaction._createdIndexes.add(c),this._rawObjectStore.rawIndexes.set(e,c),c.initialize(this.transaction),this.transaction._rollbackLog.push(()=>{c.deleted=!0,this.indexNames=new F(...o),this._rawObjectStore.rawIndexes.delete(c.name)}),new J(this,c)}index(e){if(arguments.length===0)throw new TypeError;if(this._rawObjectStore.deleted||this.transaction._state==="finished")throw new A;let t=this._indexesCache.get(e);if(t!==void 0)return t;let r=this._rawObjectStore.rawIndexes.get(e);if(!this.indexNames.contains(e)||r===void 0)throw new O;let u=new J(this,r);return this._indexesCache.set(e,u),u}deleteIndex(e){if(arguments.length===0)throw new TypeError;if(this.transaction.mode!=="versionchange")throw new A;x(this);let t=this._rawObjectStore.rawIndexes.get(e);if(t===void 0)throw new O;this.transaction._rollbackLog.push(()=>{t.deleted=!1,this._rawObjectStore.rawIndexes.set(t.name,t),this.indexNames._push(t.name),this.indexNames._sort()}),this.indexNames=new F(...Array.from(this.indexNames).filter(r=>r!==e)),t.deleted=!0,this.transaction._execRequestAsync({operation:()=>{let r=this._rawObjectStore.rawIndexes.get(e);t===r&&this._rawObjectStore.rawIndexes.delete(e)},source:this})}count(e){return x(this),e===null&&(e=void 0),e!==void 0&&!(e instanceof p)&&(e=p.only(w(e))),this.transaction._execRequestAsync({operation:()=>this._rawObjectStore.count(e),source:this})}get[Symbol.toStringTag](){return"IDBObjectStore"}},S=Ie;var ve=class{constructor(e,t={}){a(this,"eventPath",[]);a(this,"NONE",0);a(this,"CAPTURING_PHASE",1);a(this,"AT_TARGET",2);a(this,"BUBBLING_PHASE",3);a(this,"propagationStopped",!1);a(this,"immediatePropagationStopped",!1);a(this,"canceled",!1);a(this,"initialized",!0);a(this,"dispatched",!1);a(this,"target",null);a(this,"currentTarget",null);a(this,"eventPhase",0);a(this,"defaultPrevented",!1);a(this,"isTrusted",!1);a(this,"timeStamp",Date.now());this.type=e,this.bubbles=t.bubbles!==void 0?t.bubbles:!1,this.cancelable=t.cancelable!==void 0?t.cancelable:!1}preventDefault(){this.cancelable&&(this.canceled=!0)}stopPropagation(){this.propagationStopped=!0}stopImmediatePropagation(){this.propagationStopped=!0,this.immediatePropagationStopped=!0}},j=ve;function It(){if(typeof navigator!="undefined"&&/jsdom/.test(navigator.userAgent)){let n=Node.constructor;return new n("return setImmediate")()}else return}var vt=typeof scheduler!="undefined"&&(n=>scheduler.postTask(n)),Ot=n=>setTimeout(n,0),D=n=>{(globalThis.setImmediate||It()||vt||Ot)(n)};var Tt=["error","abort","complete"],Oe=class extends Y{constructor(t,r,u,s){super();a(this,"_state","active");a(this,"_started",!1);a(this,"_rollbackLog",[]);a(this,"_objectStoresCache",new Map);a(this,"_openRequest",null);a(this,"error",null);a(this,"onabort",null);a(this,"oncomplete",null);a(this,"onerror",null);a(this,"_prioritizedListeners",new Map);a(this,"_requests",[]);a(this,"_createdIndexes",new Set);a(this,"_createdObjectStores",new Set);this._scope=new Set(t),this.mode=r,this.durability=u,this.db=s,this.objectStoreNames=new F(...Array.from(this._scope).sort());for(let o of Tt)this.addEventListener(o,()=>{var c;(c=this._prioritizedListeners.get(o))==null||c()})}_abort(t){for(let r of this._rollbackLog.reverse())r();if(t!==null){let r=new l(void 0,t);this.error=r}for(let{request:r}of this._requests)r.readyState!=="done"&&(r.readyState="done",r.source&&D(()=>{r.result=void 0,r.error=new z;let u=new j("error",{bubbles:!0,cancelable:!0});u.eventPath=[this.db,this];try{r.dispatchEvent(u)}catch(s){this._state==="active"&&this._abort("AbortError")}}));D(()=>{let r=this.mode==="versionchange";r&&(this.db._rawDatabase.connections=this.db._rawDatabase.connections.filter(s=>!s._rawDatabase.transactions.includes(this)));let u=new j("abort",{bubbles:!0,cancelable:!1});if(u.eventPath=[this.db],this.dispatchEvent(u),r){let s=this._openRequest;s.transaction=null,s.result=void 0}}),this._state="finished"}abort(){if(this._state==="committing"||this._state==="finished")throw new A;this._state="active",this._abort(null)}objectStore(t){if(this._state!=="active")throw new A;let r=this._objectStoresCache.get(t);if(r!==void 0)return r;let u=this.db._rawDatabase.rawObjectStores.get(t);if(!this._scope.has(t)||u===void 0)throw new O;let s=new S(this,u);return this._objectStoresCache.set(t,s),s}_execRequestAsync(t){let r=t.source,u=t.operation,s=Object.hasOwn(t,"request")?t.request:null;if(this._state!=="active")throw new B;return s||(r?(s=new I,s.source=r,s.transaction=r.transaction):s=new I),this._requests.push({operation:u,request:s}),s}_start(){this._started=!0;let t,r;for(;this._requests.length>0;){let u=this._requests.shift();if(u&&u.request.readyState!=="done"){r=u.request,t=u.operation;break}}if(r&&t){if(!r.source)t();else{let u,s;try{let o=t();r.readyState="done",r.result=o,r.error=void 0,this._state==="inactive"&&(this._state="active"),s=new j("success",{bubbles:!1,cancelable:!1})}catch(o){r.readyState="done",r.result=void 0,r.error=o,this._state==="inactive"&&(this._state="active"),s=new j("error",{bubbles:!0,cancelable:!0}),u=this._abort.bind(this,o.name)}try{s.eventPath=[this.db,this],r.dispatchEvent(s)}catch(o){this._state==="active"&&(this._abort("AbortError"),u=void 0)}s.canceled||u&&u()}D(this._start.bind(this));return}if(this._state!=="finished"&&(this._state="finished",!this.error)){let u=new j("complete");this.dispatchEvent(u)}}commit(){if(this._state!=="active")throw new A;this._state="committing"}get[Symbol.toStringTag](){return"IDBTransaction"}},fe=Oe;var nt=9007199254740992,Te=class{constructor(){a(this,"num",0)}next(){if(this.num>=nt)throw new C;return this.num+=1,this.num}setIfLarger(e){let t=Math.floor(Math.min(e,nt))-1;t>=this.num&&(this.num=t+1)}},ut=Te;var Re=class{constructor(e,t,r,u){a(this,"deleted",!1);a(this,"records",new ce(!0));a(this,"rawIndexes",new Map);this.rawDatabase=e,this.keyGenerator=u===!0?new ut:null,this.deleted=!1,this.name=t,this.keyPath=r,this.autoIncrement=u}getKey(e){let t=this.records.get(e);return t!==void 0?f(t.key):void 0}getAllKeys(e,t,r){(t===void 0||t===0)&&(t=1/0);let u=[];for(let s of this.records.values(e,r))if(u.push(f(s.key)),u.length>=t)break;return u}getValue(e){let t=this.records.get(e);return t!==void 0?f(t.value):void 0}getAllValues(e,t,r){(t===void 0||t===0)&&(t=1/0);let u=[];for(let s of this.records.values(e,r))if(u.push(f(s.value)),u.length>=t)break;return u}getAllRecords(e,t,r){(t===void 0||t===0)&&(t=1/0);let u=[];for(let s of this.records.values(e,r))if(u.push(new ae(f(s.key),f(s.key),f(s.value))),u.length>=t)break;return u}storeRecord(e,t,r){if(this.keyPath!==null){let i=K(this.keyPath,e.value).key;i!==void 0&&(e.key=i)}let u=[];if(this.keyGenerator!==null&&e.key===void 0){let i=!1,d=this.keyGenerator.num,_=()=>{i||(i=!0,this.keyGenerator&&(this.keyGenerator.num=d))};if(u.push(_),r&&r.push(_),e.key=this.keyGenerator.next(),this.keyPath!==null){if(Array.isArray(this.keyPath))throw new Error("Cannot have an array key path in an object store with a key generator");let y=this.keyPath,E=e.value,g,G=0;for(;G>=0;){if(typeof E!="object")throw new b;G=y.indexOf("."),G>=0&&(g=y.slice(0,G),y=y.slice(G+1),Object.hasOwn(E,g)||Object.defineProperty(E,g,{configurable:!0,enumerable:!0,writable:!0,value:{}}),E=E[g])}g=y,Object.defineProperty(E,g,{configurable:!0,enumerable:!0,writable:!0,value:e.key})}}else this.keyGenerator!==null&&typeof e.key=="number"&&this.keyGenerator.setIfLarger(e.key);let s=this.records.put(e,t),o=!1,c=()=>{o||(o=!0,s?this.storeRecord(s,!1):this.deleteRecord(e.key))};if(u.push(c),r&&r.push(c),s)for(let i of this.rawIndexes.values())i.records.deleteByValue(e.key);try{for(let i of this.rawIndexes.values())i.initialized&&i.storeRecord(e)}catch(i){if(i.name==="ConstraintError")for(let d of u)d();throw i}return e.key}deleteRecord(e,t){let r=this.records.delete(e);if(t)for(let u of r)t.push(()=>{this.storeRecord(u,!0)});for(let u of this.rawIndexes.values())u.records.deleteByValue(e)}clear(e){let t=this.records.clear();if(e)for(let r of t)e.push(()=>{this.storeRecord(r,!0)});for(let r of this.rawIndexes.values())r.records.clear()}count(e){if(e===void 0||e.lower===void 0&&e.upper===void 0)return this.records.size();let t=0;for(let r of this.records.values(e))t+=1;return t}},st=Re;var ot=(n,e=!1)=>{if(n._closePending=!0,n._rawDatabase.transactions.every(r=>r._state==="finished")){if(n._closed=!0,n._rawDatabase.connections=n._rawDatabase.connections.filter(r=>n!==r),e){let r=new j("close",{bubbles:!1,cancelable:!1});r.eventPath=[],n.dispatchEvent(r)}}else D(()=>{ot(n,e)})},it=ot;var at=n=>{let e;if(n._runningVersionchangeTransaction&&(e=n._rawDatabase.transactions.findLast(t=>t.mode==="versionchange")),!e)throw new A;if(e._state!=="active")throw new B;return e},qe=class extends Y{constructor(t){super();a(this,"_closePending",!1);a(this,"_closed",!1);a(this,"_runningVersionchangeTransaction",!1);this._rawDatabase=t,this._rawDatabase.connections.push(this),this.name=t.name,this.version=t.version,this.objectStoreNames=new F(...Array.from(t.rawObjectStores.keys()).sort())}createObjectStore(t,r={}){if(t===void 0)throw new TypeError;let u=at(this),s=r!==null&&r.keyPath!==void 0?r.keyPath:null,o=r!==null&&r.autoIncrement!==void 0?r.autoIncrement:!1;if(s!==null&&de(s),this._rawDatabase.rawObjectStores.has(t))throw new C;if(o&&(s===""||Array.isArray(s)))throw new R;let c=[...this.objectStoreNames],i=[...u.objectStoreNames],d=new st(this._rawDatabase,t,s,o);return this.objectStoreNames._push(t),this.objectStoreNames._sort(),u._scope.add(t),u._createdObjectStores.add(d),this._rawDatabase.rawObjectStores.set(t,d),u.objectStoreNames=new F(...this.objectStoreNames),u._rollbackLog.push(()=>{d.deleted=!0,this.objectStoreNames=new F(...c),u.objectStoreNames=new F(...i),u._scope.delete(d.name),this._rawDatabase.rawObjectStores.delete(d.name)}),u.objectStore(t)}deleteObjectStore(t){if(t===void 0)throw new TypeError;let r=at(this),u=this._rawDatabase.rawObjectStores.get(t);if(u===void 0)throw new O;this.objectStoreNames=new F(...Array.from(this.objectStoreNames).filter(c=>c!==t)),r.objectStoreNames=new F(...this.objectStoreNames);let s=r._objectStoresCache.get(t),o;s&&(o=[...s.indexNames],s.indexNames=new F),r._rollbackLog.push(()=>{u.deleted=!1,this._rawDatabase.rawObjectStores.set(u.name,u),this.objectStoreNames._push(u.name),r.objectStoreNames._push(u.name),this.objectStoreNames._sort(),s&&o&&(s.indexNames=new F(...o))}),u.deleted=!0,this._rawDatabase.rawObjectStores.delete(t),r._objectStoresCache.delete(t)}transaction(t,r,u){var i;if(r=r!==void 0?r:"readonly",r!=="readonly"&&r!=="readwrite"&&r!=="versionchange")throw new TypeError("Invalid mode: "+r);if(this._rawDatabase.transactions.some(d=>d._state==="active"&&d.mode==="versionchange"&&d.db===this))throw new A;if(this._closePending)throw new A;if(Array.isArray(t)||(t=[t]),t.length===0&&r!=="versionchange")throw new R;for(let d of t)if(!this.objectStoreNames.contains(d))throw new O("No objectStore named "+d+" in this database");let o=(i=u==null?void 0:u.durability)!=null?i:"default";if(o!=="default"&&o!=="strict"&&o!=="relaxed")throw new TypeError(`'${o}' (value of 'durability' member of IDBTransactionOptions) is not a valid value for enumeration IDBTransactionDurability`);let c=new fe(t,r,o,this);return this._rawDatabase.transactions.push(c),this._rawDatabase.processTransactions(),c}close(){it(this)}get[Symbol.toStringTag](){return"IDBDatabase"}},le=qe;var ke=class extends I{constructor(){super(...arguments);a(this,"onupgradeneeded",null);a(this,"onblocked",null)}get[Symbol.toStringTag](){return"IDBOpenDBRequest"}},ee=ke;var Ne=class extends j{constructor(e,t={}){super(e),this.newVersion=t.newVersion!==void 0?t.newVersion:null,this.oldVersion=t.oldVersion!==void 0?t.oldVersion:0}get[Symbol.toStringTag](){return"IDBVersionChangeEvent"}},N=Ne;function Ve(n,e){return"intersection"in n?n.intersection(e):new Set([...n].filter(t=>e.has(t)))}var Ke=class{constructor(e,t){a(this,"transactions",[]);a(this,"rawObjectStores",new Map);a(this,"connections",[]);this.name=e,this.version=t,this.processTransactions=this.processTransactions.bind(this)}processTransactions(){D(()=>{let e=this.transactions.filter(u=>u._started&&u._state!=="finished"),t=this.transactions.filter(u=>!u._started&&u._state!=="finished"),r=t.find((u,s)=>e.some(i=>!(u.mode==="readonly"&&i.mode==="readonly")&&Ve(i._scope,u._scope).size>0)?!1:!t.slice(0,s).some(i=>Ve(i._scope,u._scope).size>0));r&&(r.addEventListener("complete",this.processTransactions),r.addEventListener("abort",this.processTransactions),r._start())})}},ct=Ke;function he(n,e,t){if(n{var u;let r=(u=n.get(e))!=null?u:Promise.resolve();n.set(e,r.then(t))},ft=(n,e,t,r)=>{if(t.some(s=>!s._closed&&!s._closePending)){D(()=>ft(n,e,t,r));return}n.delete(e),r(null)},Rt=(n,e,t,r,u)=>{dt(e,t,()=>new Promise(o=>{let c=n.get(t),i=c!==void 0?c.version:0,d=_=>{try{_?u(_):u(null,i)}finally{o()}};try{let _=n.get(t);if(_===void 0){d(null);return}let y=_.connections.filter(E=>!E._closed);for(let E of y)E._closePending||D(()=>{let g=new N("versionchange",{newVersion:null,oldVersion:_.version});E.dispatchEvent(g)});D(()=>{y.some(g=>!g._closed&&!g._closePending)&&D(()=>{let g=new N("blocked",{newVersion:null,oldVersion:_.version});r.dispatchEvent(g)}),ft(n,t,y,d)})}catch(_){d(_)}}))},qt=(n,e,t,r)=>{n._runningVersionchangeTransaction=!0;let u=n._oldVersion=n.version,s=n._rawDatabase.connections.filter(o=>n!==o);for(let o of s)!o._closed&&!o._closePending&&D(()=>{let c=new N("versionchange",{newVersion:e,oldVersion:u});o.dispatchEvent(c)});D(()=>{s.some(i=>!i._closed&&!i._closePending)&&D(()=>{let i=new N("blocked",{newVersion:e,oldVersion:u});t.dispatchEvent(i)});let c=()=>{if(s.some(g=>!g._closed&&!g._closePending)){D(c);return}n._rawDatabase.version=e,n.version=e;let d=n.transaction(Array.from(n.objectStoreNames),"versionchange");d._openRequest=t,t.result=n,t.readyState="done",t.transaction=d,d._rollbackLog.push(()=>{n._rawDatabase.version=u,n.version=u}),d._state="active";let _=new N("upgradeneeded",{newVersion:e,oldVersion:u}),y=!1;try{t.dispatchEvent(_)}catch(g){y=!0}let E=()=>{d._state==="active"&&(d._state="inactive",y&&d._abort("AbortError"))};y?E():D(E),d._prioritizedListeners.set("error",()=>{n._runningVersionchangeTransaction=!1,n._oldVersion=void 0}),d._prioritizedListeners.set("abort",()=>{n._runningVersionchangeTransaction=!1,n._oldVersion=void 0,D(()=>{t.transaction=null,r(new z)})}),d._prioritizedListeners.set("complete",()=>{n._runningVersionchangeTransaction=!1,n._oldVersion=void 0,D(()=>{t.transaction=null,n._closePending?r(new z):r(null)})})};c()})},kt=(n,e,t,r,u,s)=>{dt(e,t,()=>new Promise(c=>{let i=y=>{try{y?s(y):s(null,_)}finally{c()}},d=n.get(t);if(d===void 0&&(d=new ct(t,0),n.set(t,d)),r===void 0&&(r=d.version!==0?d.version:1),d.version>r)return i(new te);let _=new le(d);d.version{i(y)}):i(null)}))},Pe=class{constructor(){a(this,"_databases",new Map);a(this,"_connectionQueues",new Map)}cmp(e,t){return he(arguments.length,2,"IDBFactory.cmp"),m(e,t)}deleteDatabase(e){he(arguments.length,1,"IDBFactory.deleteDatabase");let t=new ee;return t.source=null,D(()=>{Rt(this._databases,this._connectionQueues,e,t,(r,u)=>{if(r){t.error=new l(r.message,r.name),t.readyState="done";let o=new j("error",{bubbles:!0,cancelable:!0});o.eventPath=[],t.dispatchEvent(o);return}t.result=void 0,t.readyState="done";let s=new N("success",{newVersion:null,oldVersion:u});t.dispatchEvent(s)})}),t}open(e,t){if(he(arguments.length,1,"IDBFactory.open"),arguments.length>1&&t!==void 0&&(t=k(t,"MAX_SAFE_INTEGER")),t===0)throw new TypeError("Database version cannot be 0");let r=new ee;return r.source=null,D(()=>{kt(this._databases,this._connectionQueues,e,t,r,(u,s)=>{if(u){r.result=void 0,r.readyState="done",r.error=new l(u.message,u.name);let c=new j("error",{bubbles:!0,cancelable:!0});c.eventPath=[],r.dispatchEvent(c);return}r.result=s,r.readyState="done";let o=new j("success");o.eventPath=[],r.dispatchEvent(o)})}),r}databases(){return Promise.resolve(Array.from(this._databases.entries(),([e,t])=>{let r=t.connections.find(s=>s._runningVersionchangeTransaction),u=r?r._oldVersion:t.version;return{name:e,version:u}}).filter(({version:e})=>e>0))}get[Symbol.toStringTag](){return"IDBFactory"}},Ae=Pe;var Nt=new Ae,lt=Nt;Array.prototype.findLast===void 0&&Object.defineProperty(Array.prototype,"findLast",{configurable:!0,writable:!0,value(n,e){for(let t=this.length-1;t>=0;--t)if(n.call(e,this[t],t,this))return this[t]}});Object.hasOwn===void 0&&Object.defineProperty(Object,"hasOwn",{configurable:!0,writable:!0,value(n,e){return Object.prototype.hasOwnProperty.call(n,e)}});var v=n=>({value:n,enumerable:!1,configurable:!0,writable:!0});Object.defineProperties(globalThis,Ue(ze({},typeof globalThis.DOMException=="function"?{}:{DOMException:v(l)}),{indexedDB:v(lt),IDBCursor:v(P),IDBCursorWithValue:v(H),IDBDatabase:v(le),IDBFactory:v(Ae),IDBIndex:v(J),IDBKeyRange:v(p),IDBObjectStore:v(S),IDBOpenDBRequest:v(ee),IDBRequest:v(I),IDBTransaction:v(fe),IDBVersionChangeEvent:v(N)}));})(); diff --git a/Polyfills/Streams/CMakeLists.txt b/Polyfills/Streams/CMakeLists.txt new file mode 100644 index 00000000..6c00ed81 --- /dev/null +++ b/Polyfills/Streams/CMakeLists.txt @@ -0,0 +1,30 @@ +set(WEB_STREAMS_POLYFILL_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/ThirdParty/web-streams-polyfill/ponyfill.es5.js") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${WEB_STREAMS_POLYFILL_FILE}") +file(READ "${WEB_STREAMS_POLYFILL_FILE}" WEB_STREAMS_POLYFILL_SOURCE) +string(SUBSTRING "${WEB_STREAMS_POLYFILL_SOURCE}" 0 60000 WEB_STREAMS_POLYFILL_SOURCE_1) +string(SUBSTRING "${WEB_STREAMS_POLYFILL_SOURCE}" 60000 -1 WEB_STREAMS_POLYFILL_SOURCE_2) +configure_file( + "Source/StreamsScripts.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/Generated/StreamsScripts.h" + @ONLY) + +set(SOURCES + "Include/Babylon/Polyfills/Streams.h" + "Source/Streams.cpp" + "Source/StreamsScripts.h.in" + "ThirdParty/web-streams-polyfill/LICENSE" + "ThirdParty/web-streams-polyfill/ponyfill.es5.js") + +add_library(Streams ${SOURCES}) +warnings_as_errors(Streams) + +target_include_directories(Streams + PUBLIC "Include" + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/Generated") + +target_link_libraries(Streams PUBLIC JsRuntime) + +set_property(TARGET Streams PROPERTY FOLDER Polyfills) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Polyfills/Streams/Include/Babylon/Polyfills/Streams.h b/Polyfills/Streams/Include/Babylon/Polyfills/Streams.h new file mode 100644 index 00000000..01e5a15c --- /dev/null +++ b/Polyfills/Streams/Include/Babylon/Polyfills/Streams.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::Streams +{ + // Installs the WHATWG Streams constructors that are not already supplied + // by the JavaScript engine. + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Polyfills/Streams/README.md b/Polyfills/Streams/README.md new file mode 100644 index 00000000..c84a38e3 --- /dev/null +++ b/Polyfills/Streams/README.md @@ -0,0 +1,14 @@ +# Web Streams + +Installs the standard `ReadableStream`, `WritableStream`, `TransformStream`, +reader, writer, controller, and queuing-strategy globals when the selected +JavaScript engine does not provide them. + +The implementation is the ES5 ponyfill bundle from +[`web-streams-polyfill` 4.3.0](https://github.com/MattiasBuelens/web-streams-polyfill/tree/add69059ed08eae6a18559aba49575a280d1529e), +which is based on the WHATWG reference implementation. The vendored project +tests this release against the Streams portion of WPT at revision +[`c05b4473`](https://github.com/web-platform-tests/wpt/tree/c05b447326585237713013c66341eab2cdf967b6/streams). + +`Initialize` preserves any Streams constructors already supplied by the host +engine and fills only missing globals. diff --git a/Polyfills/Streams/Source/Streams.cpp b/Polyfills/Streams/Source/Streams.cpp new file mode 100644 index 00000000..e742344f --- /dev/null +++ b/Polyfills/Streams/Source/Streams.cpp @@ -0,0 +1,53 @@ +#include + +#include "StreamsScripts.h" + +#include +#include + +namespace Babylon::Polyfills::Streams +{ + void BABYLON_API Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + auto global = env.Global(); + + static constexpr std::array constructorNames{ + "ReadableStream", + "ReadableStreamDefaultController", + "ReadableByteStreamController", + "ReadableStreamBYOBRequest", + "ReadableStreamDefaultReader", + "ReadableStreamBYOBReader", + "WritableStream", + "WritableStreamDefaultController", + "WritableStreamDefaultWriter", + "ByteLengthQueuingStrategy", + "CountQueuingStrategy", + "TransformStream", + "TransformStreamDefaultController", + }; + + bool needsPonyfill{}; + for (const auto name : constructorNames) + { + const auto constructor = global.Get(name.data()); + if (constructor.IsUndefined() || constructor.IsNull()) + { + needsPonyfill = true; + break; + } + } + + if (!needsPonyfill) + { + return; + } + + const auto exports = Napi::Eval(env, Internal::StreamsScripts::Ponyfill.data(), "jsruntimehost://web-streams-polyfill.js").As(); + for (const auto name : constructorNames) + { + global.Set(name.data(), exports.Get(name.data())); + } + } +} diff --git a/Polyfills/Streams/Source/StreamsScripts.h.in b/Polyfills/Streams/Source/StreamsScripts.h.in new file mode 100644 index 00000000..126a23f7 --- /dev/null +++ b/Polyfills/Streams/Source/StreamsScripts.h.in @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::Internal::StreamsScripts +{ + inline constexpr char PonyfillPart1[] = R"JSRHSTREAM( +(function() { + var exports = {}; + var module = { exports: exports }; +@WEB_STREAMS_POLYFILL_SOURCE_1@)JSRHSTREAM"; + + // Keep the split transparent: inserting whitespace here can divide an + // identifier or string literal when the vendored bundle is regenerated. + inline constexpr char PonyfillPart2[] = R"JSRHSTREAM(@WEB_STREAMS_POLYFILL_SOURCE_2@ + return module.exports; +})() +)JSRHSTREAM"; + + template + consteval auto Join(const char (&part1)[Part1Size], const char (&part2)[Part2Size]) + { + std::array result{}; + for (std::size_t index{}; index < Part1Size - 1; ++index) + { + result[index] = part1[index]; + } + for (std::size_t index{}; index < Part2Size; ++index) + { + result[Part1Size - 1 + index] = part2[index]; + } + return result; + } + + inline constexpr auto Ponyfill = Join(PonyfillPart1, PonyfillPart2); +} diff --git a/Polyfills/Streams/ThirdParty/web-streams-polyfill/LICENSE b/Polyfills/Streams/ThirdParty/web-streams-polyfill/LICENSE new file mode 100644 index 00000000..7adbcf57 --- /dev/null +++ b/Polyfills/Streams/ThirdParty/web-streams-polyfill/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2026 Mattias Buelens +Copyright (c) 2016 Diwank Singh Tomer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Polyfills/Streams/ThirdParty/web-streams-polyfill/ponyfill.es5.js b/Polyfills/Streams/ThirdParty/web-streams-polyfill/ponyfill.es5.js new file mode 100644 index 00000000..18d63683 --- /dev/null +++ b/Polyfills/Streams/ThirdParty/web-streams-polyfill/ponyfill.es5.js @@ -0,0 +1,8 @@ +/** + * @license + * web-streams-polyfill v4.3.0 + * Copyright 2026 Mattias Buelens, Diwank Singh Tomer and other contributors. + * This code is released under the MIT license. + * SPDX-License-Identifier: MIT + */ +!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).WebStreamsPolyfill={})}(this,function(e){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol:function(e){return"Symbol(".concat(e,")")};function t(){}function o(e){return"object"==typeof e&&null!==e||"function"==typeof e}"function"==typeof SuppressedError&&SuppressedError;var n=t;function i(e,r){try{Object.defineProperty(e,"name",{value:r,configurable:!0})}catch(e){}}var a=Promise,u=Promise.resolve.bind(a),l=Promise.prototype.then,s=Promise.reject.bind(a),c=u;function f(e){return new a(e)}function d(e){return f(function(r){return r(e)})}function p(e){return s(e)}function b(e,r,t){return l.call(e,r,t)}function h(e,r,t){b(b(e,r,t),void 0,n)}function _(e,r){h(e,r)}function m(e,r){h(e,void 0,r)}function v(e,r,t){return b(e,r,t)}function y(e){b(e,void 0,n)}var g=function(e){if("function"==typeof queueMicrotask)g=queueMicrotask;else{var r=d(void 0);g=function(e){return b(r,e)}}return g(e)};function S(e,r,t){if("function"!=typeof e)throw new TypeError("Argument is not a function");return Function.prototype.apply.call(e,r,t)}function w(e,r,t){try{return d(S(e,r,t))}catch(e){return p(e)}}var R=function(){function e(){this._cursor=0,this._size=0,this._front={_elements:[],_next:void 0},this._back=this._front,this._cursor=0,this._size=0}return Object.defineProperty(e.prototype,"length",{get:function(){return this._size},enumerable:!1,configurable:!0}),e.prototype.push=function(e){var r=this._back,t=r;16383===r._elements.length&&(t={_elements:[],_next:void 0}),r._elements.push(e),t!==r&&(this._back=t,r._next=t),++this._size},e.prototype.shift=function(){var e=this._front,r=e,t=this._cursor,o=t+1,n=e._elements,i=n[t];return 16384===o&&(r=e._next,o=0),--this._size,this._cursor=o,e!==r&&(this._front=r),n[t]=void 0,i},e.prototype.forEach=function(e){for(var r=this._cursor,t=this._front,o=t._elements;!(r===o.length&&void 0===t._next||r===o.length&&(r=0,0===(o=(t=t._next)._elements).length));)e(o[r]),++r},e.prototype.peek=function(){var e=this._front,r=this._cursor;return e._elements[r]},e}(),T=r("[[AbortSteps]]"),P=r("[[ErrorSteps]]"),C=r("[[CancelSteps]]"),q=r("[[PullSteps]]"),E=r("[[CanPullSyncSteps]]"),W=r("[[ReleaseSteps]]");function O(e,r){e._ownerReadableStream=r,r._reader=e,"readable"===r._state?A(e):"closed"===r._state?function(e){A(e),F(e)}(e):z(e,r._storedError)}function j(e,r){return eo(e._ownerReadableStream,r)}function B(e){var r=e._ownerReadableStream;"readable"===r._state?D(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")):function(e,r){z(e,r)}(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")),r._readableStreamController[W](),r._reader=void 0,e._ownerReadableStream=void 0}function k(e){return new TypeError("Cannot "+e+" a stream using a released reader")}function A(e){e._closedPromise=f(function(r,t){e._closedPromise_resolve=r,e._closedPromise_reject=t})}function z(e,r){A(e),D(e,r)}function D(e,r){void 0!==e._closedPromise_reject&&(y(e._closedPromise),e._closedPromise_reject(r),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}function F(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}var L=Number.isFinite||function(e){return"number"==typeof e&&isFinite(e)},I=Math.trunc||function(e){return e<0?Math.ceil(e):Math.floor(e)};function M(e,r){if(void 0!==e&&("object"!=typeof(t=e)&&"function"!=typeof t))throw new TypeError("".concat(r," is not an object."));var t}function x(e,r){if("function"!=typeof e)throw new TypeError("".concat(r," is not a function."))}function Y(e,r){if(!function(e){return"object"==typeof e&&null!==e||"function"==typeof e}(e))throw new TypeError("".concat(r," is not an object."))}function Q(e,r,t){if(void 0===e)throw new TypeError("Parameter ".concat(r," is required in '").concat(t,"'."))}function N(e,r,t){if(void 0===e)throw new TypeError("".concat(r," is required in '").concat(t,"'."))}function H(e){return Number(e)}function V(e){return 0===e?0:e}function U(e,r){var t=Number.MAX_SAFE_INTEGER,o=Number(e);if(o=V(o),!L(o))throw new TypeError("".concat(r," is not a finite number"));if((o=function(e){return V(I(e))}(o))<0||o>t)throw new TypeError("".concat(r," is outside the accepted range of ").concat(0," to ").concat(t,", inclusive"));return L(o)&&0!==o?o:0}function G(e,r){if(!Zt(e))throw new TypeError("".concat(r," is not a ReadableStream."))}function X(e){return new ee(e)}function J(e,r){e._reader._readRequests.push(r)}function K(e,r,t){var o=e._reader._readRequests.shift();t?o._closeSteps():o._chunkSteps(r)}function Z(e){return e._reader._readRequests.length}function $(e){var r=e._reader;return void 0!==r&&!!ae(r)}var ee=function(){function ReadableStreamDefaultReader(e){if(Q(e,1,"ReadableStreamDefaultReader"),G(e,"First parameter"),$t(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");O(this,e),this._readRequests=new R}return Object.defineProperty(ReadableStreamDefaultReader.prototype,"closed",{get:function(){return ae(this)?this._closedPromise:p(ce("closed"))},enumerable:!1,configurable:!0}),ReadableStreamDefaultReader.prototype.cancel=function(e){return void 0===e&&(e=void 0),ae(this)?void 0===this._ownerReadableStream?p(k("cancel")):j(this,e):p(ce("cancel"))},ReadableStreamDefaultReader.prototype.read=function(){if(!ae(this))return p(ce("read"));if(void 0===this._ownerReadableStream)return p(k("read from"));var e=le(this)?new ie:new ne;return ue(this,e),e._promise},ReadableStreamDefaultReader.prototype.releaseLock=function(){if(!ae(this))throw ce("releaseLock");void 0!==this._ownerReadableStream&&function(e){B(e);var r=new TypeError("Reader was released");se(e,r)}(this)},ReadableStreamDefaultReader}();Object.defineProperties(ee.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),i(ee.prototype.cancel,"cancel"),i(ee.prototype.read,"read"),i(ee.prototype.releaseLock,"releaseLock"),"symbol"==typeof r.toStringTag&&Object.defineProperty(ee.prototype,r.toStringTag,{value:"ReadableStreamDefaultReader",configurable:!0});var re,te,oe,ne=function(){function e(){var e=this;this._promise=f(function(r,t){e._resolvePromise=r,e._rejectPromise=t})}return e.prototype._chunkSteps=function(e){this._resolvePromise({value:e,done:!1})},e.prototype._closeSteps=function(){this._resolvePromise({value:void 0,done:!0})},e.prototype._errorSteps=function(e){this._rejectPromise(e)},e}(),ie=function(){function e(){this._promise=void 0}return e.prototype._chunkSteps=function(e){this._promise=c({value:e,done:!1})},e.prototype._closeSteps=function(){this._promise=c({value:void 0,done:!0})},e.prototype._errorSteps=function(e){this._promise=p(e)},e}();function ae(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readRequests")&&e instanceof ee)}function ue(e,r){var t=e._ownerReadableStream;t._disturbed=!0,"closed"===t._state?r._closeSteps():"errored"===t._state?r._errorSteps(t._storedError):t._readableStreamController[q](r)}function le(e){var r=e._ownerReadableStream;return"closed"===r._state||("errored"===r._state||r._readableStreamController[E]())}function se(e,r){var t=e._readRequests;e._readRequests=new R,t.forEach(function(e){e._errorSteps(r)})}function ce(e){return new TypeError("ReadableStreamDefaultReader.prototype.".concat(e," can only be used on a ReadableStreamDefaultReader"))}function fe(e){return e.slice()}function de(e,r,t,o,n){new Uint8Array(e).set(new Uint8Array(t,o,n),r)}var pe=function(e){return(pe="function"==typeof e.transfer?function(e){return e.transfer()}:"function"==typeof structuredClone?function(e){return structuredClone(e,{transfer:[e]})}:function(e){return e})(e)},be=function(e){return(be="boolean"==typeof e.detached?function(e){return e.detached}:function(e){return 0===e.byteLength})(e)};function he(e,r,t){if(e.slice)return e.slice(r,t);var o=t-r,n=new ArrayBuffer(o);return de(n,0,e,r,o),n}function _e(e,r){var t=e[r];if(null!=t){if("function"!=typeof t)throw new TypeError("".concat(String(r)," is not a function"));return t}}function me(e){try{var r=e.done,t=e.value;return b(c(t),function(e){return{done:r,value:e}})}catch(e){return p(e)}}var ve,ye=null!==(oe=null!==(re=r.asyncIterator)&&void 0!==re?re:null===(te=r.for)||void 0===te?void 0:te.call(r,"Symbol.asyncIterator"))&&void 0!==oe?oe:"@@asyncIterator";function ge(e,t,n){if(void 0===t&&(t="sync"),void 0===n)if("async"===t){if(void 0===(n=_e(e,ye)))return function(e){var r={next:function(){var r;try{r=Se(e)}catch(e){return p(e)}return me(r)},return:function(r){var t;try{var n=_e(e.iterator,"return");if(void 0===n)return d({done:!0,value:r});t=S(n,e.iterator,[r])}catch(e){return p(e)}return o(t)?me(t):p(new TypeError("The iterator.return() method must return an object"))}};return{iterator:r,nextMethod:r.next,done:!1}}(ge(e,"sync",_e(e,r.iterator)))}else n=_e(e,r.iterator);if(void 0===n)throw new TypeError("The object is not iterable");var i=S(n,e,[]);if(!o(i))throw new TypeError("The iterator method must return an object");return{iterator:i,nextMethod:i.next,done:!1}}function Se(e){var r=S(e.nextMethod,e.iterator,[]);if(!o(r))throw new TypeError("The iterator.next() method must return an object");return r}var we=function(){function e(e,r){this._ongoingPromise=void 0,this._isFinished=!1,this._reader=e,this._preventCancel=r}return e.prototype.next=function(){var e=this,r=function(){return e._nextSteps()};return this._ongoingPromise=this._ongoingPromise?v(this._ongoingPromise,r,r):r(),this._ongoingPromise},e.prototype.return=function(e){var r=this,t=function(){return r._returnSteps(e)};return this._ongoingPromise=this._ongoingPromise?v(this._ongoingPromise,t,t):t(),this._ongoingPromise},e.prototype._nextSteps=function(){if(this._isFinished)return Promise.resolve({value:void 0,done:!0});var e=this._reader,r=new Re(this);return ue(e,r),r._promise},e.prototype._returnSteps=function(e){if(this._isFinished)return Promise.resolve({value:e,done:!0});this._isFinished=!0;var r=this._reader;if(!this._preventCancel){var t=j(r,e);return B(r),v(t,function(){return{value:e,done:!0}})}return B(r),d({value:e,done:!0})},e}(),Re=function(){function e(e){var r=this;this._iterator=e,this._promise=f(function(e,t){r._resolvePromise=e,r._rejectPromise=t})}return e.prototype._chunkSteps=function(e){var r=this;this._iterator._ongoingPromise=void 0,g(function(){return r._resolvePromise({value:e,done:!1})})},e.prototype._closeSteps=function(){var e=this._iterator;e._ongoingPromise=void 0,e._isFinished=!0,B(e._reader),this._resolvePromise({value:void 0,done:!0})},e.prototype._errorSteps=function(e){var r=this._iterator;r._ongoingPromise=void 0,r._isFinished=!0,B(r._reader),this._rejectPromise(e)},e}(),Te=((ve={next:function(){return Pe(this)?this._asyncIteratorImpl.next():p(Ce("next"))},return:function(e){return Pe(this)?this._asyncIteratorImpl.return(e):p(Ce("return"))}})[ye]=function(){return this},ve);function Pe(e){if(!o(e))return!1;if(!Object.prototype.hasOwnProperty.call(e,"_asyncIteratorImpl"))return!1;try{return e._asyncIteratorImpl instanceof we}catch(e){return!1}}function Ce(e){return new TypeError("ReadableStreamAsyncIterator.".concat(e," can only be used on a ReadableSteamAsyncIterator"))}Object.defineProperty(Te,ye,{enumerable:!1});var qe=Number.isNaN||function(e){return e!=e};function Ee(e){var r=he(e.buffer,e.byteOffset,e.byteOffset+e.byteLength);return new Uint8Array(r)}function We(e){var r=e._queue.shift();return e._queueTotalSize-=r.size,e._queueTotalSize<0&&(e._queueTotalSize=0),r.value}function Oe(e,r,t){if("number"!=typeof(o=t)||qe(o)||o<0||t===1/0)throw new RangeError("Size must be a finite, non-NaN, non-negative number.");var o;e._queue.push({value:r,size:t}),e._queueTotalSize+=t}function je(e){e._queue=new R,e._queueTotalSize=0}function Be(e){return e===DataView}function ke(e){return Be(e)?1:e.BYTES_PER_ELEMENT}var Ae=function(){function ReadableStreamBYOBRequest(){throw new TypeError("Illegal constructor")}return Object.defineProperty(ReadableStreamBYOBRequest.prototype,"view",{get:function(){if(!Fe(this))throw sr("view");return this._view},enumerable:!1,configurable:!0}),ReadableStreamBYOBRequest.prototype.respond=function(e){if(!Fe(this))throw sr("respond");if(Q(e,1,"respond"),e=U(e,"First parameter"),void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");if(be(this._view.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be used as a response");ar(this._associatedReadableByteStreamController,e)},ReadableStreamBYOBRequest.prototype.respondWithNewView=function(e){if(!Fe(this))throw sr("respondWithNewView");if(Q(e,1,"respondWithNewView"),!ArrayBuffer.isView(e))throw new TypeError("You can only respond with array buffer views");if(void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");if(be(e.buffer))throw new TypeError("The given view's buffer has been detached and so cannot be used as a response");ur(this._associatedReadableByteStreamController,e)},ReadableStreamBYOBRequest}();Object.defineProperties(Ae.prototype,{respond:{enumerable:!0},respondWithNewView:{enumerable:!0},view:{enumerable:!0}}),i(Ae.prototype.respond,"respond"),i(Ae.prototype.respondWithNewView,"respondWithNewView"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Ae.prototype,r.toStringTag,{value:"ReadableStreamBYOBRequest",configurable:!0});var ze=function(){function ReadableByteStreamController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(ReadableByteStreamController.prototype,"byobRequest",{get:function(){if(!De(this))throw cr("byobRequest");return nr(this)},enumerable:!1,configurable:!0}),Object.defineProperty(ReadableByteStreamController.prototype,"desiredSize",{get:function(){if(!De(this))throw cr("desiredSize");return ir(this)},enumerable:!1,configurable:!0}),ReadableByteStreamController.prototype.close=function(){if(!De(this))throw cr("close");if(this._closeRequested)throw new TypeError("The stream has already been closed; do not close it again!");var e=this._controlledReadableByteStream._state;if("readable"!==e)throw new TypeError("The stream (in ".concat(e," state) is not in the readable state and cannot be closed"));er(this)},ReadableByteStreamController.prototype.enqueue=function(e){if(!De(this))throw cr("enqueue");if(Q(e,1,"enqueue"),!ArrayBuffer.isView(e))throw new TypeError("chunk must be an array buffer view");if(0===e.byteLength)throw new TypeError("chunk must have non-zero byteLength");if(0===e.buffer.byteLength)throw new TypeError("chunk's buffer must have non-zero byteLength");if(this._closeRequested)throw new TypeError("stream is closed or draining");var r=this._controlledReadableByteStream._state;if("readable"!==r)throw new TypeError("The stream (in ".concat(r," state) is not in the readable state and cannot be enqueued to"));rr(this,e)},ReadableByteStreamController.prototype.error=function(e){if(void 0===e&&(e=void 0),!De(this))throw cr("error");tr(this,e)},ReadableByteStreamController.prototype[C]=function(e){Ie(this),je(this);var r=this._cancelAlgorithm(e);return $e(this),r},ReadableByteStreamController.prototype[q]=function(e){var r=this._controlledReadableByteStream;if(this._queueTotalSize>0)or(this,e);else{var t=this._autoAllocateChunkSize;if(void 0!==t){var o=void 0;try{o=new ArrayBuffer(t)}catch(r){return void e._errorSteps(r)}var n={buffer:o,bufferByteLength:t,byteOffset:0,byteLength:t,bytesFilled:0,minimumFill:1,elementSize:1,viewConstructor:Uint8Array,readerType:"default"};this._pendingPullIntos.push(n)}J(r,e),Le(this)}},ReadableByteStreamController.prototype[E]=function(){return this._queueTotalSize>0},ReadableByteStreamController.prototype[W]=function(){if(this._pendingPullIntos.length>0){var e=this._pendingPullIntos.peek();e.readerType="none",this._pendingPullIntos=new R,this._pendingPullIntos.push(e)}},ReadableByteStreamController}();function De(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableByteStream")&&e instanceof ze)}function Fe(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_associatedReadableByteStreamController")&&e instanceof Ae)}function Le(e){var r=function(e){var r=e._controlledReadableByteStream;if("readable"!==r._state)return!1;if(e._closeRequested)return!1;if(!e._started)return!1;if($(r)&&Z(r)>0)return!0;if(hr(r)&&br(r)>0)return!0;var t=ir(e);if(t>0)return!0;return!1}(e);r&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,h(e._pullAlgorithm(),function(){return e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,Le(e)),null},function(r){return tr(e,r),null})))}function Ie(e){Xe(e),e._pendingPullIntos=new R}function Me(e,r){var t=!1;"closed"===e._state&&(t=!0);var o=Ye(r);"default"===r.readerType?K(e,o,t):function(e,r,t){var o=e._reader,n=o._readIntoRequests.shift();t?n._closeSteps(r):n._chunkSteps(r)}(e,o,t)}function xe(e,r){for(var t=0;t0&&Ne(e,r.buffer,r.byteOffset,r.bytesFilled),Ze(e)}function Ve(e,r){var t=Math.min(e._queueTotalSize,r.byteLength-r.bytesFilled),o=r.bytesFilled+t,n=t,i=!1,a=o-o%r.elementSize;a>=r.minimumFill&&(n=a-r.bytesFilled,i=!0);for(var u=e._queue;n>0;){var l=u.peek(),s=Math.min(n,l.byteLength),c=r.byteOffset+r.bytesFilled;de(r.buffer,c,l.buffer,l.byteOffset,s),l.byteLength===s?u.shift():(l.byteOffset+=s,l.byteLength-=s),e._queueTotalSize-=s,Ue(e,s,r),n-=s}return i}function Ue(e,r,t){t.bytesFilled+=r}function Ge(e){0===e._queueTotalSize&&e._closeRequested?($e(e),ro(e._controlledReadableByteStream)):Le(e)}function Xe(e){null!==e._byobRequest&&(e._byobRequest._associatedReadableByteStreamController=void 0,e._byobRequest._view=null,e._byobRequest=null)}function Je(e){for(var r=[];e._pendingPullIntos.length>0&&0!==e._queueTotalSize;){var t=e._pendingPullIntos.peek();Ve(e,t)&&(Ze(e),r.push(t))}return r}function Ke(e,r){var t=e._pendingPullIntos.peek();Xe(e),"closed"===e._controlledReadableByteStream._state?function(e,r){"none"===r.readerType&&Ze(e);var t=e._controlledReadableByteStream;if(hr(t)){for(var o=[];o.length0){var n=t.byteOffset+t.bytesFilled;Ne(e,t.buffer,n-o,o)}t.bytesFilled-=o;var i=Je(e);Me(e._controlledReadableByteStream,t),xe(e._controlledReadableByteStream,i)}}else{He(e,t);var a=Je(e);xe(e._controlledReadableByteStream,a)}}(e,r,t),Le(e)}function Ze(e){return e._pendingPullIntos.shift()}function $e(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0}function er(e){var r=e._controlledReadableByteStream;if(!e._closeRequested&&"readable"===r._state)if(e._queueTotalSize>0)e._closeRequested=!0;else{if(e._pendingPullIntos.length>0){var t=e._pendingPullIntos.peek();if(t.bytesFilled%t.elementSize!==0){var o=new TypeError("Insufficient bytes to fill elements in the given buffer");throw tr(e,o),o}}$e(e),ro(r)}}function rr(e,r){var t=e._controlledReadableByteStream;if(!e._closeRequested&&"readable"===t._state){var o=r.buffer,n=r.byteOffset,i=r.byteLength;if(be(o))throw new TypeError("chunk's buffer is detached and so cannot be enqueued");var a=pe(o);if(e._pendingPullIntos.length>0){var u=e._pendingPullIntos.peek();if(be(u.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be filled with an enqueued chunk");Xe(e),u.buffer=pe(u.buffer),"none"===u.readerType&&He(e,u)}if($(t))if(function(e){for(var r=e._controlledReadableByteStream._reader;r._readRequests.length>0;){if(0===e._queueTotalSize)return;or(e,r._readRequests.shift())}}(e),0===Z(t))Qe(e,a,n,i);else e._pendingPullIntos.length>0&&Ze(e),K(t,new Uint8Array(a,n,i),!1);else if(hr(t)){Qe(e,a,n,i),xe(t,Je(e))}else Qe(e,a,n,i);Le(e)}}function tr(e,r){var t=e._controlledReadableByteStream;"readable"===t._state&&(Ie(e),je(e),$e(e),to(t,r))}function or(e,r){var t=e._queue.shift();e._queueTotalSize-=t.byteLength,Ge(e);var o=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);r._chunkSteps(o)}function nr(e){if(null===e._byobRequest&&e._pendingPullIntos.length>0){var r=e._pendingPullIntos.peek(),t=new Uint8Array(r.buffer,r.byteOffset+r.bytesFilled,r.byteLength-r.bytesFilled),o=Object.create(Ae.prototype);!function(e,r,t){e._associatedReadableByteStreamController=r,e._view=t}(o,e,t),e._byobRequest=o}return e._byobRequest}function ir(e){var r=e._controlledReadableByteStream._state;return"errored"===r?null:"closed"===r?0:e._strategyHWM-e._queueTotalSize}function ar(e,r){var t=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==r)throw new TypeError("bytesWritten must be 0 when calling respond() on a closed stream")}else{if(0===r)throw new TypeError("bytesWritten must be greater than 0 when calling respond() on a readable stream");if(t.bytesFilled+r>t.byteLength)throw new RangeError("bytesWritten out of range")}t.buffer=pe(t.buffer),Ke(e,r)}function ur(e,r){var t=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==r.byteLength)throw new TypeError("The view's length must be 0 when calling respondWithNewView() on a closed stream")}else if(0===r.byteLength)throw new TypeError("The view's length must be greater than 0 when calling respondWithNewView() on a readable stream");if(t.byteOffset+t.bytesFilled!==r.byteOffset)throw new RangeError("The region specified by view does not match byobRequest");if(t.bufferByteLength!==r.buffer.byteLength)throw new RangeError("The buffer of view has different capacity than byobRequest");if(t.bytesFilled+r.byteLength>t.byteLength)throw new RangeError("The region specified by view is larger than byobRequest");var o=r.byteLength;t.buffer=pe(r.buffer),Ke(e,o)}function lr(e,r,t,o,n,i,a){r._controlledReadableByteStream=e,r._pullAgain=!1,r._pulling=!1,r._byobRequest=null,r._queue=r._queueTotalSize=void 0,je(r),r._closeRequested=!1,r._started=!1,r._strategyHWM=i,r._pullAlgorithm=o,r._cancelAlgorithm=n,r._autoAllocateChunkSize=a,r._pendingPullIntos=new R,e._readableStreamController=r,h(d(t()),function(){return r._started=!0,Le(r),null},function(e){return tr(r,e),null})}function sr(e){return new TypeError("ReadableStreamBYOBRequest.prototype.".concat(e," can only be used on a ReadableStreamBYOBRequest"))}function cr(e){return new TypeError("ReadableByteStreamController.prototype.".concat(e," can only be used on a ReadableByteStreamController"))}function fr(e,r){if("byob"!==(e="".concat(e)))throw new TypeError("".concat(r," '").concat(e,"' is not a valid enumeration value for ReadableStreamReaderMode"));return e}function dr(e){return new _r(e)}function pr(e,r){e._reader._readIntoRequests.push(r)}function br(e){return e._reader._readIntoRequests.length}function hr(e){var r=e._reader;return void 0!==r&&!!yr(r)}Object.defineProperties(ze.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},byobRequest:{enumerable:!0},desiredSize:{enumerable:!0}}),i(ze.prototype.close,"close"),i(ze.prototype.enqueue,"enqueue"),i(ze.prototype.error,"error"),"symbol"==typeof r.toStringTag&&Object.defineProperty(ze.prototype,r.toStringTag,{value:"ReadableByteStreamController",configurable:!0});var _r=function(){function ReadableStreamBYOBReader(e){if(Q(e,1,"ReadableStreamBYOBReader"),G(e,"First parameter"),$t(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");if(!De(e._readableStreamController))throw new TypeError("Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source");O(this,e),this._readIntoRequests=new R}return Object.defineProperty(ReadableStreamBYOBReader.prototype,"closed",{get:function(){return yr(this)?this._closedPromise:p(wr("closed"))},enumerable:!1,configurable:!0}),ReadableStreamBYOBReader.prototype.cancel=function(e){return void 0===e&&(e=void 0),yr(this)?void 0===this._ownerReadableStream?p(k("cancel")):j(this,e):p(wr("cancel"))},ReadableStreamBYOBReader.prototype.read=function(e,r){if(void 0===r&&(r={}),!yr(this))return p(wr("read"));if(!ArrayBuffer.isView(e))return p(new TypeError("view must be an array buffer view"));if(0===e.byteLength)return p(new TypeError("view must have non-zero byteLength"));if(0===e.buffer.byteLength)return p(new TypeError("view's buffer must have non-zero byteLength"));if(be(e.buffer))return p(new TypeError("view's buffer has been detached"));var t;try{t=function(e,r){var t;return M(e,r),{min:U(null!==(t=null==e?void 0:e.min)&&void 0!==t?t:1,"".concat(r," has member 'min' that"))}}(r,"options")}catch(e){return p(e)}var o=t.min;if(0===o)return p(new TypeError("options.min must be greater than 0"));if(function(e){return Be(e.constructor)}(e)){if(o>e.byteLength)return p(new RangeError("options.min must be less than or equal to view's byteLength"))}else if(o>e.length)return p(new RangeError("options.min must be less than or equal to view's length"));if(void 0===this._ownerReadableStream)return p(k("read from"));var n=function(e,r,t){var o=e._ownerReadableStream;return"errored"===o._state||function(e,r,t){var o=e._controlledReadableByteStream,n=ke(r.constructor);r.byteLength;var i=t*n;return!(e._pendingPullIntos.length>0)&&("closed"===o._state||e._queueTotalSize>=i)}(o._readableStreamController,r,t)}(this,e,o)?new vr:new mr;return gr(this,e,o,n),n._promise},ReadableStreamBYOBReader.prototype.releaseLock=function(){if(!yr(this))throw wr("releaseLock");void 0!==this._ownerReadableStream&&function(e){B(e);var r=new TypeError("Reader was released");Sr(e,r)}(this)},ReadableStreamBYOBReader}();Object.defineProperties(_r.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),i(_r.prototype.cancel,"cancel"),i(_r.prototype.read,"read"),i(_r.prototype.releaseLock,"releaseLock"),"symbol"==typeof r.toStringTag&&Object.defineProperty(_r.prototype,r.toStringTag,{value:"ReadableStreamBYOBReader",configurable:!0});var mr=function(){function e(){var e=this;this._promise=f(function(r,t){e._resolvePromise=r,e._rejectPromise=t})}return e.prototype._chunkSteps=function(e){this._resolvePromise({value:e,done:!1})},e.prototype._closeSteps=function(e){this._resolvePromise({value:e,done:!0})},e.prototype._errorSteps=function(e){this._rejectPromise(e)},e}(),vr=function(){function e(){this._promise=void 0}return e.prototype._chunkSteps=function(e){this._promise=c({value:e,done:!1})},e.prototype._closeSteps=function(e){this._promise=c({value:e,done:!0})},e.prototype._errorSteps=function(e){this._promise=p(e)},e}();function yr(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readIntoRequests")&&e instanceof _r)}function gr(e,r,t,o){var n=e._ownerReadableStream;n._disturbed=!0,"errored"===n._state?o._errorSteps(n._storedError):function(e,r,t,o){var n,i=e._controlledReadableByteStream,a=r.constructor,u=ke(a),l=r.byteOffset,s=r.byteLength,c=t*u;try{n=pe(r.buffer)}catch(p){return void o._errorSteps(p)}var f={buffer:n,bufferByteLength:n.byteLength,byteOffset:l,byteLength:s,bytesFilled:0,minimumFill:c,elementSize:u,viewConstructor:a,readerType:"byob"};if(e._pendingPullIntos.length>0)return e._pendingPullIntos.push(f),void pr(i,o);if("closed"!==i._state){if(e._queueTotalSize>0){if(Ve(e,f)){var d=Ye(f);return Ge(e),void o._chunkSteps(d)}if(e._closeRequested){var p=new TypeError("Insufficient bytes to fill elements in the given buffer");return tr(e,p),void o._errorSteps(p)}}e._pendingPullIntos.push(f),pr(i,o),Le(e)}else{var b=new a(f.buffer,f.byteOffset,0);o._closeSteps(b)}}(n._readableStreamController,r,t,o)}function Sr(e,r){var t=e._readIntoRequests;e._readIntoRequests=new R,t.forEach(function(e){e._errorSteps(r)})}function wr(e){return new TypeError("ReadableStreamBYOBReader.prototype.".concat(e," can only be used on a ReadableStreamBYOBReader"))}function Rr(e,r){var t=e.highWaterMark;if(void 0===t)return r;if(qe(t)||t<0)throw new RangeError("Invalid highWaterMark");return t}function Tr(e){var r=e.size;return r||function(){return 1}}function Pr(e,r){M(e,r);var t=null==e?void 0:e.highWaterMark,o=null==e?void 0:e.size;return{highWaterMark:void 0===t?void 0:H(t),size:void 0===o?void 0:Cr(o,"".concat(r," has member 'size' that"))}}function Cr(e,r){return x(e,r),function(r){return H(e(r))}}function qr(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function Er(e,r,t){return x(e,t),function(){return w(e,r,[])}}function Wr(e,r,t){return x(e,t),function(t){return S(e,r,[t])}}function Or(e,r,t){return x(e,t),function(t,o){return w(e,r,[t,o])}}function jr(e,r){if(!zr(e))throw new TypeError("".concat(r," is not a WritableStream."))}var Br=function(){function WritableStream(e,r){void 0===e&&(e={}),void 0===r&&(r={}),void 0===e?e=null:Y(e,"First parameter");var t=Pr(r,"Second parameter"),o=function(e,r){M(e,r);var t=null==e?void 0:e.abort,o=null==e?void 0:e.close,n=null==e?void 0:e.start,i=null==e?void 0:e.type,a=null==e?void 0:e.write;return{abort:void 0===t?void 0:qr(t,e,"".concat(r," has member 'abort' that")),close:void 0===o?void 0:Er(o,e,"".concat(r," has member 'close' that")),start:void 0===n?void 0:Wr(n,e,"".concat(r," has member 'start' that")),write:void 0===a?void 0:Or(a,e,"".concat(r," has member 'write' that")),type:i}}(e,"First parameter");if(Ar(this),void 0!==o.type)throw new RangeError("Invalid type is specified");var n=Tr(t);!function(e,r,t,o){var n,i,a,u,l=Object.create($r.prototype);n=void 0!==r.start?function(){return r.start(l)}:function(){};i=void 0!==r.write?function(e){return r.write(e,l)}:function(){return d(void 0)};a=void 0!==r.close?function(){return r.close()}:function(){return d(void 0)};u=void 0!==r.abort?function(e){return r.abort(e)}:function(){return d(void 0)};rt(e,l,n,i,a,u,t,o)}(this,o,Rr(t,1),n)}return Object.defineProperty(WritableStream.prototype,"locked",{get:function(){if(!zr(this))throw lt("locked");return Dr(this)},enumerable:!1,configurable:!0}),WritableStream.prototype.abort=function(e){return void 0===e&&(e=void 0),zr(this)?Dr(this)?p(new TypeError("Cannot abort a stream that already has a writer")):Fr(this,e):p(lt("abort"))},WritableStream.prototype.close=function(){return zr(this)?Dr(this)?p(new TypeError("Cannot close a stream that already has a writer")):Yr(this)?p(new TypeError("Cannot close an already-closing stream")):Lr(this):p(lt("close"))},WritableStream.prototype.getWriter=function(){if(!zr(this))throw lt("getWriter");return kr(this)},WritableStream}();function kr(e){return new Hr(e)}function Ar(e){e._state="writable",e._storedError=void 0,e._writer=void 0,e._writableStreamController=void 0,e._writeRequests=new R,e._inFlightWriteRequest=void 0,e._closeRequest=void 0,e._inFlightCloseRequest=void 0,e._pendingAbortRequest=void 0,e._backpressure=!1}function zr(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_writableStreamController")&&e instanceof Br)}function Dr(e){return void 0!==e._writer}function Fr(e,r){var t;if("closed"===e._state||"errored"===e._state)return d(void 0);e._writableStreamController._abortReason=r,null===(t=e._writableStreamController._abortController)||void 0===t||t.abort(r);var o=e._state;if("closed"===o||"errored"===o)return d(void 0);if(void 0!==e._pendingAbortRequest)return e._pendingAbortRequest._promise;var n=!1;"erroring"===o&&(n=!0,r=void 0);var i=f(function(t,o){e._pendingAbortRequest={_promise:void 0,_resolve:t,_reject:o,_reason:r,_wasAlreadyErroring:n}});return e._pendingAbortRequest._promise=i,n||Mr(e,r),i}function Lr(e){var r=e._state;if("closed"===r||"errored"===r)return p(new TypeError("The stream (in ".concat(r," state) is not in the writable state and cannot be closed")));var t,o=f(function(r,t){var o={_resolve:r,_reject:t};e._closeRequest=o}),n=e._writer;return void 0!==n&&e._backpressure&&"writable"===r&>(n),Oe(t=e._writableStreamController,Zr,0),nt(t),o}function Ir(e,r){"writable"!==e._state?xr(e):Mr(e,r)}function Mr(e,r){var t=e._writableStreamController;e._state="erroring",e._storedError=r;var o=e._writer;void 0!==o&&Xr(o,r),!function(e){if(void 0===e._inFlightWriteRequest&&void 0===e._inFlightCloseRequest)return!1;return!0}(e)&&t._started&&xr(e)}function xr(e){e._state="errored",e._writableStreamController[P]();var r=e._storedError;if(e._writeRequests.forEach(function(e){e._reject(r)}),e._writeRequests=new R,void 0!==e._pendingAbortRequest){var t=e._pendingAbortRequest;if(e._pendingAbortRequest=void 0,t._wasAlreadyErroring)return t._reject(r),void Qr(e);h(e._writableStreamController[T](t._reason),function(){return t._resolve(),Qr(e),null},function(r){return t._reject(r),Qr(e),null})}else Qr(e)}function Yr(e){return void 0!==e._closeRequest||void 0!==e._inFlightCloseRequest}function Qr(e){void 0!==e._closeRequest&&(e._closeRequest._reject(e._storedError),e._closeRequest=void 0);var r=e._writer;void 0!==r&&bt(r,e._storedError)}function Nr(e,r){var t=e._writer;void 0!==t&&r!==e._backpressure&&(r?function(e){_t(e)}(t):gt(t)),e._backpressure=r}Object.defineProperties(Br.prototype,{abort:{enumerable:!0},close:{enumerable:!0},getWriter:{enumerable:!0},locked:{enumerable:!0}}),i(Br.prototype.abort,"abort"),i(Br.prototype.close,"close"),i(Br.prototype.getWriter,"getWriter"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Br.prototype,r.toStringTag,{value:"WritableStream",configurable:!0});var Hr=function(){function WritableStreamDefaultWriter(e){if(Q(e,1,"WritableStreamDefaultWriter"),jr(e,"First parameter"),Dr(e))throw new TypeError("This stream has already been locked for exclusive writing by another writer");this._ownerWritableStream=e,e._writer=this;var r,t=e._state;if("writable"===t)!Yr(e)&&e._backpressure?_t(this):vt(this),dt(this);else if("erroring"===t)mt(this,e._storedError),dt(this);else if("closed"===t)vt(this),dt(r=this),ht(r);else{var o=e._storedError;mt(this,o),pt(this,o)}}return Object.defineProperty(WritableStreamDefaultWriter.prototype,"closed",{get:function(){return Vr(this)?this._closedPromise:p(ct("closed"))},enumerable:!1,configurable:!0}),Object.defineProperty(WritableStreamDefaultWriter.prototype,"desiredSize",{get:function(){if(!Vr(this))throw ct("desiredSize");if(void 0===this._ownerWritableStream)throw ft("desiredSize");return function(e){var r=e._ownerWritableStream,t=r._state;if("errored"===t||"erroring"===t)return null;if("closed"===t)return 0;return ot(r._writableStreamController)}(this)},enumerable:!1,configurable:!0}),Object.defineProperty(WritableStreamDefaultWriter.prototype,"ready",{get:function(){return Vr(this)?this._readyPromise:p(ct("ready"))},enumerable:!1,configurable:!0}),WritableStreamDefaultWriter.prototype.abort=function(e){return void 0===e&&(e=void 0),Vr(this)?void 0===this._ownerWritableStream?p(ft("abort")):function(e,r){return Fr(e._ownerWritableStream,r)}(this,e):p(ct("abort"))},WritableStreamDefaultWriter.prototype.close=function(){if(!Vr(this))return p(ct("close"));var e=this._ownerWritableStream;return void 0===e?p(ft("close")):Yr(e)?p(new TypeError("Cannot close an already-closing stream")):Ur(this)},WritableStreamDefaultWriter.prototype.releaseLock=function(){if(!Vr(this))throw ct("releaseLock");void 0!==this._ownerWritableStream&&Jr(this)},WritableStreamDefaultWriter.prototype.write=function(e){return void 0===e&&(e=void 0),Vr(this)?void 0===this._ownerWritableStream?p(ft("write to")):Kr(this,e):p(ct("write"))},WritableStreamDefaultWriter}();function Vr(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_ownerWritableStream")&&e instanceof Hr)}function Ur(e){return Lr(e._ownerWritableStream)}function Gr(e,r){"pending"===e._closedPromiseState?bt(e,r):function(e,r){pt(e,r)}(e,r)}function Xr(e,r){"pending"===e._readyPromiseState?yt(e,r):function(e,r){mt(e,r)}(e,r)}function Jr(e){var r=e._ownerWritableStream,t=new TypeError("Writer was released and can no longer be used to monitor the stream's closedness");Xr(e,t),Gr(e,t),r._writer=void 0,e._ownerWritableStream=void 0}function Kr(e,r){var t=e._ownerWritableStream,o=t._writableStreamController,n=function(e,r){if(void 0===e._strategySizeAlgorithm)return 1;try{return e._strategySizeAlgorithm(r)}catch(r){return it(e,r),1}}(o,r);if(t!==e._ownerWritableStream)return p(ft("write to"));var i=t._state;if("errored"===i)return p(t._storedError);if(Yr(t)||"closed"===i)return p(new TypeError("The stream is closing or closed and cannot be written to"));if("erroring"===i)return p(t._storedError);var a=function(e){return f(function(r,t){var o={_resolve:r,_reject:t};e._writeRequests.push(o)})}(t);return function(e,r,t){try{Oe(e,r,t)}catch(r){return void it(e,r)}var o=e._controlledWritableStream;if(!Yr(o)&&"writable"===o._state){Nr(o,at(e))}nt(e)}(o,r,n),a}Object.defineProperties(Hr.prototype,{abort:{enumerable:!0},close:{enumerable:!0},releaseLock:{enumerable:!0},write:{enumerable:!0},closed:{enumerable:!0},desiredSize:{enumerable:!0},ready:{enumerable:!0}}),i(Hr.prototype.abort,"abort"),i(Hr.prototype.close,"close"),i(Hr.prototype.releaseLock,"releaseLock"),i(Hr.prototype.write,"write"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Hr.prototype,r.toStringTag,{value:"WritableStreamDefaultWriter",configurable:!0});var Zr={},$r=function(){function WritableStreamDefaultController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(WritableStreamDefaultController.prototype,"abortReason",{get:function(){if(!et(this))throw st("abortReason");return this._abortReason},enumerable:!1,configurable:!0}),Object.defineProperty(WritableStreamDefaultController.prototype,"signal",{get:function(){if(!et(this))throw st("signal");if(void 0===this._abortController)throw new TypeError("WritableStreamDefaultController.prototype.signal is not supported");return this._abortController.signal},enumerable:!1,configurable:!0}),WritableStreamDefaultController.prototype.error=function(e){if(void 0===e&&(e=void 0),!et(this))throw st("error");"writable"===this._controlledWritableStream._state&&ut(this,e)},WritableStreamDefaultController.prototype[T]=function(e){var r=this._abortAlgorithm(e);return tt(this),r},WritableStreamDefaultController.prototype[P]=function(){je(this)},WritableStreamDefaultController}();function et(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledWritableStream")&&e instanceof $r)}function rt(e,r,t,o,n,i,a,u){r._controlledWritableStream=e,e._writableStreamController=r,r._queue=void 0,r._queueTotalSize=void 0,je(r),r._abortReason=void 0,r._abortController=function(){if("function"==typeof AbortController)return new AbortController}(),r._started=!1,r._strategySizeAlgorithm=u,r._strategyHWM=a,r._writeAlgorithm=o,r._closeAlgorithm=n,r._abortAlgorithm=i;var l=at(r);Nr(e,l),h(d(t()),function(){return r._started=!0,nt(r),null},function(t){return r._started=!0,Ir(e,t),null})}function tt(e){e._writeAlgorithm=void 0,e._closeAlgorithm=void 0,e._abortAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function ot(e){return e._strategyHWM-e._queueTotalSize}function nt(e){var r=e._controlledWritableStream;if(e._started&&void 0===r._inFlightWriteRequest)if("erroring"!==r._state){if(0!==e._queue.length){var t=e._queue.peek().value;t===Zr?function(e){var r=e._controlledWritableStream;(function(e){e._inFlightCloseRequest=e._closeRequest,e._closeRequest=void 0})(r),We(e);var t=e._closeAlgorithm();tt(e),h(t,function(){return function(e){e._inFlightCloseRequest._resolve(void 0),e._inFlightCloseRequest=void 0,"erroring"===e._state&&(e._storedError=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._resolve(),e._pendingAbortRequest=void 0)),e._state="closed";var r=e._writer;void 0!==r&&ht(r)}(r),null},function(e){return function(e,r){e._inFlightCloseRequest._reject(r),e._inFlightCloseRequest=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._reject(r),e._pendingAbortRequest=void 0),Ir(e,r)}(r,e),null})}(e):function(e,r){var t=e._controlledWritableStream;!function(e){e._inFlightWriteRequest=e._writeRequests.shift()}(t);var o=e._writeAlgorithm(r);h(o,function(){!function(e){e._inFlightWriteRequest._resolve(void 0),e._inFlightWriteRequest=void 0}(t);var r=t._state;if(We(e),!Yr(t)&&"writable"===r){var o=at(e);Nr(t,o)}return nt(e),null},function(r){return"writable"===t._state&&tt(e),function(e,r){e._inFlightWriteRequest._reject(r),e._inFlightWriteRequest=void 0,Ir(e,r)}(t,r),null})}(e,t)}}else xr(r)}function it(e,r){"writable"===e._controlledWritableStream._state&&ut(e,r)}function at(e){return ot(e)<=0}function ut(e,r){var t=e._controlledWritableStream;tt(e),Mr(t,r)}function lt(e){return new TypeError("WritableStream.prototype.".concat(e," can only be used on a WritableStream"))}function st(e){return new TypeError("WritableStreamDefaultController.prototype.".concat(e," can only be used on a WritableStreamDefaultController"))}function ct(e){return new TypeError("WritableStreamDefaultWriter.prototype.".concat(e," can only be used on a WritableStreamDefaultWriter"))}function ft(e){return new TypeError("Cannot "+e+" a stream using a released writer")}function dt(e){e._closedPromise=f(function(r,t){e._closedPromise_resolve=r,e._closedPromise_reject=t,e._closedPromiseState="pending"})}function pt(e,r){dt(e),bt(e,r)}function bt(e,r){void 0!==e._closedPromise_reject&&(y(e._closedPromise),e._closedPromise_reject(r),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="rejected")}function ht(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="resolved")}function _t(e){e._readyPromise=f(function(r,t){e._readyPromise_resolve=r,e._readyPromise_reject=t}),e._readyPromiseState="pending"}function mt(e,r){_t(e),yt(e,r)}function vt(e){_t(e),gt(e)}function yt(e,r){void 0!==e._readyPromise_reject&&(y(e._readyPromise),e._readyPromise_reject(r),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="rejected")}function gt(e){void 0!==e._readyPromise_resolve&&(e._readyPromise_resolve(void 0),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="fulfilled")}Object.defineProperties($r.prototype,{abortReason:{enumerable:!0},signal:{enumerable:!0},error:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty($r.prototype,r.toStringTag,{value:"WritableStreamDefaultController",configurable:!0});var St="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof global?global:void 0;var wt,Rt=(function(e){if("function"!=typeof e&&"object"!=typeof e)return!1;if("DOMException"!==e.name)return!1;try{return new e,!0}catch(e){return!1}}(wt=null==St?void 0:St.DOMException)?wt:void 0)||function(){var e=function(e,r){this.message=e||"",this.name=r||"Error",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)};return i(e,"DOMException"),e.prototype=Object.create(Error.prototype),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,configurable:!0}),e}();function Tt(e,r,t,o,n,i){var a=X(e),u=kr(r);e._disturbed=!0;var l=new Pt(u),s=new qt(l);return f(function(c,m){var v,g,S,w;if(void 0!==i){if(v=function(){var t=void 0!==i.reason?i.reason:new Rt("Aborted","AbortError"),a=[];o||a.push(function(){return"writable"===r._state?Fr(r,t):d(void 0)}),n||a.push(function(){return"readable"===e._state?eo(e,t):d(void 0)}),P(function(){return Promise.all(a.map(function(e){return e()}))},!0,t)},i.aborted)return void v();i.addEventListener("abort",v)}function R(){for(;!l._shuttingDown&&!r._backpressure&&"writable"===r._state&&!Yr(r)&&"readable"===e._state&&le(a);)ue(a,s);if(l._shuttingDown)return d(!0);if(r._backpressure)return b(u._readyPromise,R);var t=new Ct(l);return ue(a,t),t._promise}if(Et(e,a._closedPromise,function(e){return o?C(!0,e):P(function(){return Fr(r,e)},!0,e),null}),Et(r,u._closedPromise,function(r){return n?C(!0,r):P(function(){return eo(e,r)},!0,r),null}),g=e,S=a._closedPromise,w=function(){return t?C():P(function(){return function(e){var r=e._ownerWritableStream,t=r._state;return Yr(r)||"closed"===t?d(void 0):"errored"===t?p(r._storedError):Ur(e)}(u)}),null},"closed"===g._state?w():_(S,w),Yr(r)||"closed"===r._state){var T=new TypeError("the destination writable stream closed before all data could be piped to it");n?C(!0,T):P(function(){return eo(e,T)},!0,T)}function P(e,t,o){function n(){return h(e(),function(){return q(t,o)},function(e){return q(!0,e)}),null}l._shuttingDown||(l._shuttingDown=!0,"writable"!==r._state||Yr(r)?n():_(l._waitForWritesToFinish(),n))}function C(e,t){l._shuttingDown||(l._shuttingDown=!0,"writable"!==r._state||Yr(r)?q(e,t):_(l._waitForWritesToFinish(),function(){return q(e,t)}))}function q(e,r){return Jr(u),B(a),void 0!==i&&i.removeEventListener("abort",v),e?m(r):c(void 0),null}y(f(function(e,r){!function t(o){o?e():b(R(),t,r)}(!1)}))})}var Pt=function(){function e(e){this._writer=e,this._shuttingDown=!1,this._currentWrite=d(void 0)}return e.prototype._waitForWritesToFinish=function(){var e=this,r=this._currentWrite;return b(this._currentWrite,function(){return r!==e._currentWrite?e._waitForWritesToFinish():void 0})},e}(),Ct=function(){function e(e){var r=this;this._state=e,this._promise=f(function(e,t){r._resolvePromise=e,r._rejectPromise=t})}return e.prototype._chunkSteps=function(e){this._state._currentWrite=b(Kr(this._state._writer,e),void 0,t),this._resolvePromise(!1)},e.prototype._closeSteps=function(){this._resolvePromise(!0)},e.prototype._errorSteps=function(e){this._rejectPromise(e)},e}(),qt=function(){function e(e){this._state=e}return e.prototype._chunkSteps=function(e){this._state._currentWrite=b(Kr(this._state._writer,e),void 0,t)},e.prototype._closeSteps=function(){},e.prototype._errorSteps=function(e){},e}();function Et(e,r,t){"errored"===e._state?t(e._storedError):m(r,t)}var Wt=function(){function ReadableStreamDefaultController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(ReadableStreamDefaultController.prototype,"desiredSize",{get:function(){if(!Ot(this))throw Mt("desiredSize");return Ft(this)},enumerable:!1,configurable:!0}),ReadableStreamDefaultController.prototype.close=function(){if(!Ot(this))throw Mt("close");if(!Lt(this))throw new TypeError("The stream is not in a state that permits close");At(this)},ReadableStreamDefaultController.prototype.enqueue=function(e){if(void 0===e&&(e=void 0),!Ot(this))throw Mt("enqueue");if(!Lt(this))throw new TypeError("The stream is not in a state that permits enqueue");return zt(this,e)},ReadableStreamDefaultController.prototype.error=function(e){if(void 0===e&&(e=void 0),!Ot(this))throw Mt("error");Dt(this,e)},ReadableStreamDefaultController.prototype[C]=function(e){je(this);var r=this._cancelAlgorithm(e);return kt(this),r},ReadableStreamDefaultController.prototype[q]=function(e){var r=this._controlledReadableStream;if(this._queue.length>0){var t=We(this);this._closeRequested&&0===this._queue.length?(kt(this),ro(r)):jt(this),e._chunkSteps(t)}else J(r,e),jt(this)},ReadableStreamDefaultController.prototype[E]=function(){return this._queue.length>0},ReadableStreamDefaultController.prototype[W]=function(){},ReadableStreamDefaultController}();function Ot(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableStream")&&e instanceof Wt)}function jt(e){Bt(e)&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,h(e._pullAlgorithm(),function(){return e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,jt(e)),null},function(r){return Dt(e,r),null})))}function Bt(e){var r=e._controlledReadableStream;return!!Lt(e)&&(!!e._started&&(!!($t(r)&&Z(r)>0)||Ft(e)>0))}function kt(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function At(e){if(Lt(e)){var r=e._controlledReadableStream;e._closeRequested=!0,0===e._queue.length&&(kt(e),ro(r))}}function zt(e,r){if(Lt(e)){var t=e._controlledReadableStream;if($t(t)&&Z(t)>0)K(t,r,!1);else{var o=void 0;try{o=e._strategySizeAlgorithm(r)}catch(r){throw Dt(e,r),r}try{Oe(e,r,o)}catch(r){throw Dt(e,r),r}}jt(e)}}function Dt(e,r){var t=e._controlledReadableStream;"readable"===t._state&&(je(e),kt(e),to(t,r))}function Ft(e){var r=e._controlledReadableStream._state;return"errored"===r?null:"closed"===r?0:e._strategyHWM-e._queueTotalSize}function Lt(e){var r=e._controlledReadableStream._state;return!e._closeRequested&&"readable"===r}function It(e,r,t,o,n,i,a){r._controlledReadableStream=e,r._queue=void 0,r._queueTotalSize=void 0,je(r),r._started=!1,r._closeRequested=!1,r._pullAgain=!1,r._pulling=!1,r._strategySizeAlgorithm=a,r._strategyHWM=i,r._pullAlgorithm=o,r._cancelAlgorithm=n,e._readableStreamController=r,h(d(t()),function(){return r._started=!0,jt(r),null},function(e){return Dt(r,e),null})}function Mt(e){return new TypeError("ReadableStreamDefaultController.prototype.".concat(e," can only be used on a ReadableStreamDefaultController"))}function xt(e,r){return De(e._readableStreamController)?function(e){var r,t,o,n,i,a=X(e),u=!1,l=!1,s=!1,c=!1,p=!1,b=f(function(e){i=e});function h(e){m(e._closedPromise,function(r){return e!==a||(tr(o._readableStreamController,r),tr(n._readableStreamController,r),c&&p||i(void 0)),null})}function _(){yr(a)&&(B(a),h(a=X(e))),ue(a,{_chunkSteps:function(r){g(function(){l=!1,s=!1;var t=r,a=r;if(!c&&!p)try{a=Ee(r)}catch(r){return tr(o._readableStreamController,r),tr(n._readableStreamController,r),void i(eo(e,r))}c||rr(o._readableStreamController,t),p||rr(n._readableStreamController,a),u=!1,l?y():s&&S()})},_closeSteps:function(){u=!1,c||er(o._readableStreamController),p||er(n._readableStreamController),o._readableStreamController._pendingPullIntos.length>0&&ar(o._readableStreamController,0),n._readableStreamController._pendingPullIntos.length>0&&ar(n._readableStreamController,0),c&&p||i(void 0)},_errorSteps:function(){u=!1}})}function v(r,t){ae(a)&&(B(a),h(a=dr(e)));var f=t?n:o,d=t?o:n;gr(a,r,1,{_chunkSteps:function(r){g(function(){l=!1,s=!1;var o=t?p:c;if(t?c:p)o||ur(f._readableStreamController,r);else{var n=void 0;try{n=Ee(r)}catch(r){return tr(f._readableStreamController,r),tr(d._readableStreamController,r),void i(eo(e,r))}o||ur(f._readableStreamController,r),rr(d._readableStreamController,n)}u=!1,l?y():s&&S()})},_closeSteps:function(e){u=!1;var r=t?p:c,o=t?c:p;r||er(f._readableStreamController),o||er(d._readableStreamController),void 0!==e&&(r||ur(f._readableStreamController,e),!o&&d._readableStreamController._pendingPullIntos.length>0&&ar(d._readableStreamController,0)),r&&o||i(void 0)},_errorSteps:function(){u=!1}})}function y(){if(u)return l=!0,d(void 0);u=!0;var e=nr(o._readableStreamController);return null===e?_():v(e._view,!1),d(void 0)}function S(){if(u)return s=!0,d(void 0);u=!0;var e=nr(n._readableStreamController);return null===e?_():v(e._view,!0),d(void 0)}function w(o){if(c=!0,r=o,p){var n=fe([r,t]),a=eo(e,n);i(a)}return b}function R(o){if(p=!0,t=o,c){var n=fe([r,t]),a=eo(e,n);i(a)}return b}function T(){}return o=Jt(T,y,w),n=Jt(T,S,R),h(a),[o,n]}(e):function(e){var r,t,o,n,i,a=X(e),u=!1,l=!1,s=!1,c=!1,p=f(function(e){i=e});function b(){return u?(l=!0,d(void 0)):(u=!0,ue(a,{_chunkSteps:function(e){g(function(){l=!1;var r=e,t=e;s||zt(o._readableStreamController,r),c||zt(n._readableStreamController,t),u=!1,l&&b()})},_closeSteps:function(){u=!1,s||At(o._readableStreamController),c||At(n._readableStreamController),s&&c||i(void 0)},_errorSteps:function(){u=!1}}),d(void 0))}function h(o){if(s=!0,r=o,c){var n=fe([r,t]),a=eo(e,n);i(a)}return p}function _(o){if(c=!0,t=o,s){var n=fe([r,t]),a=eo(e,n);i(a)}return p}function v(){}return o=Xt(v,b,h),n=Xt(v,b,_),m(a._closedPromise,function(e){return Dt(o._readableStreamController,e),Dt(n._readableStreamController,e),s&&c||i(void 0),null}),[o,n]}(e)}function Yt(e){return o(r=e)&&void 0!==r.getReader?function(e){var r;function n(){var t;try{t=e.read()}catch(e){return p(e)}return v(t,function(e){if(!o(e))throw new TypeError("The promise returned by the reader.read() method must fulfill with an object");if(e.done)At(r._readableStreamController);else{var t=e.value;zt(r._readableStreamController,t)}})}function i(r){try{return d(e.cancel(r))}catch(e){return p(e)}}return r=Xt(t,n,i,0),r}(e.getReader()):function(e){var r,n=ge(e,"async");function i(){var e;try{e=Se(n)}catch(e){return p(e)}return v(d(e),function(e){if(!o(e))throw new TypeError("The promise returned by the iterator.next() method must fulfill with an object");if(e.done)At(r._readableStreamController);else{var t=e.value;zt(r._readableStreamController,t)}})}function a(e){var r,t=n.iterator;try{r=_e(t,"return")}catch(e){return p(e)}return void 0===r?d(void 0):v(w(r,t,[e]),function(e){if(!o(e))throw new TypeError("The promise returned by the iterator.return() method must fulfill with an object")})}return r=Xt(t,i,a,0),r}(e);var r}function Qt(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function Nt(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function Ht(e,r,t){return x(e,t),function(t){return S(e,r,[t])}}function Vt(e,r){if("bytes"!==(e="".concat(e)))throw new TypeError("".concat(r," '").concat(e,"' is not a valid enumeration value for ReadableStreamType"));return e}function Ut(e,r){M(e,r);var t=null==e?void 0:e.preventAbort,o=null==e?void 0:e.preventCancel,n=null==e?void 0:e.preventClose,i=null==e?void 0:e.signal;return void 0!==i&&function(e,r){if(!function(e){if("object"!=typeof e||null===e)return!1;try{return"boolean"==typeof e.aborted}catch(e){return!1}}(e))throw new TypeError("".concat(r," is not an AbortSignal."))}(i,"".concat(r," has member 'signal' that")),{preventAbort:Boolean(t),preventCancel:Boolean(o),preventClose:Boolean(n),signal:i}}Object.defineProperties(Wt.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},desiredSize:{enumerable:!0}}),i(Wt.prototype.close,"close"),i(Wt.prototype.enqueue,"enqueue"),i(Wt.prototype.error,"error"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Wt.prototype,r.toStringTag,{value:"ReadableStreamDefaultController",configurable:!0});var Gt=function(){function ReadableStream(e,r){void 0===e&&(e={}),void 0===r&&(r={}),void 0===e?e=null:Y(e,"First parameter");var t=Pr(r,"Second parameter"),o=function(e,r){M(e,r);var t=e,o=null==t?void 0:t.autoAllocateChunkSize,n=null==t?void 0:t.cancel,i=null==t?void 0:t.pull,a=null==t?void 0:t.start,u=null==t?void 0:t.type;return{autoAllocateChunkSize:void 0===o?void 0:U(o,"".concat(r," has member 'autoAllocateChunkSize' that")),cancel:void 0===n?void 0:Qt(n,t,"".concat(r," has member 'cancel' that")),pull:void 0===i?void 0:Nt(i,t,"".concat(r," has member 'pull' that")),start:void 0===a?void 0:Ht(a,t,"".concat(r," has member 'start' that")),type:void 0===u?void 0:Vt(u,"".concat(r," has member 'type' that"))}}(e,"First parameter");if(Kt(this),"bytes"===o.type){if(void 0!==t.size)throw new RangeError("The strategy for a byte stream cannot have a size function");!function(e,r,t){var o,n,i,a=Object.create(ze.prototype);o=void 0!==r.start?function(){return r.start(a)}:function(){},n=void 0!==r.pull?function(){return r.pull(a)}:function(){return d(void 0)},i=void 0!==r.cancel?function(e){return r.cancel(e)}:function(){return d(void 0)};var u=r.autoAllocateChunkSize;if(0===u)throw new TypeError("autoAllocateChunkSize must be greater than 0");lr(e,a,o,n,i,t,u)}(this,o,Rr(t,0))}else{var n=Tr(t);!function(e,r,t,o){var n,i,a,u=Object.create(Wt.prototype);n=void 0!==r.start?function(){return r.start(u)}:function(){},i=void 0!==r.pull?function(){return r.pull(u)}:function(){return d(void 0)},a=void 0!==r.cancel?function(e){return r.cancel(e)}:function(){return d(void 0)},It(e,u,n,i,a,t,o)}(this,o,Rr(t,1),n)}}return Object.defineProperty(ReadableStream.prototype,"locked",{get:function(){if(!Zt(this))throw oo("locked");return $t(this)},enumerable:!1,configurable:!0}),ReadableStream.prototype.cancel=function(e){return void 0===e&&(e=void 0),Zt(this)?$t(this)?p(new TypeError("Cannot cancel a stream that already has a reader")):eo(this,e):p(oo("cancel"))},ReadableStream.prototype.getReader=function(e){if(void 0===e&&(e=void 0),!Zt(this))throw oo("getReader");return void 0===function(e,r){M(e,r);var t=null==e?void 0:e.mode;return{mode:void 0===t?void 0:fr(t,"".concat(r," has member 'mode' that"))}}(e,"First parameter").mode?X(this):dr(this)},ReadableStream.prototype.pipeThrough=function(e,r){if(void 0===r&&(r={}),!Zt(this))throw oo("pipeThrough");Q(e,1,"pipeThrough");var t=function(e,r){M(e,r);var t=null==e?void 0:e.readable;N(t,"readable","ReadableWritablePair"),G(t,"".concat(r," has member 'readable' that"));var o=null==e?void 0:e.writable;return N(o,"writable","ReadableWritablePair"),jr(o,"".concat(r," has member 'writable' that")),{readable:t,writable:o}}(e,"First parameter"),o=Ut(r,"Second parameter");if($t(this))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream");if(Dr(t.writable))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream");return y(Tt(this,t.writable,o.preventClose,o.preventAbort,o.preventCancel,o.signal)),t.readable},ReadableStream.prototype.pipeTo=function(e,r){if(void 0===r&&(r={}),!Zt(this))return p(oo("pipeTo"));if(void 0===e)return p("Parameter 1 is required in 'pipeTo'.");if(!zr(e))return p(new TypeError("ReadableStream.prototype.pipeTo's first argument must be a WritableStream"));var t;try{t=Ut(r,"Second parameter")}catch(e){return p(e)}return $t(this)?p(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream")):Dr(e)?p(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream")):Tt(this,e,t.preventClose,t.preventAbort,t.preventCancel,t.signal)},ReadableStream.prototype.tee=function(){if(!Zt(this))throw oo("tee");return fe(xt(this))},ReadableStream.prototype.values=function(e){if(void 0===e&&(e=void 0),!Zt(this))throw oo("values");var r,t,o,n,i,a=function(e,r){M(e,r);var t=null==e?void 0:e.preventCancel;return{preventCancel:Boolean(t)}}(e,"First parameter");return r=this,t=a.preventCancel,o=X(r),n=new we(o,t),(i=Object.create(Te))._asyncIteratorImpl=n,i},ReadableStream.prototype[ye]=function(e){return this.values(e)},ReadableStream.from=function(e){return Yt(e)},ReadableStream}();function Xt(e,r,t,o,n){void 0===o&&(o=1),void 0===n&&(n=function(){return 1});var i=Object.create(Gt.prototype);return Kt(i),It(i,Object.create(Wt.prototype),e,r,t,o,n),i}function Jt(e,r,t){var o=Object.create(Gt.prototype);return Kt(o),lr(o,Object.create(ze.prototype),e,r,t,0,void 0),o}function Kt(e){e._state="readable",e._reader=void 0,e._storedError=void 0,e._disturbed=!1}function Zt(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_readableStreamController")&&e instanceof Gt)}function $t(e){return void 0!==e._reader}function eo(e,r){if(e._disturbed=!0,"closed"===e._state)return d(void 0);if("errored"===e._state)return p(e._storedError);ro(e);var o=e._reader;if(void 0!==o&&yr(o)){var n=o._readIntoRequests;o._readIntoRequests=new R,n.forEach(function(e){e._closeSteps(void 0)})}return v(e._readableStreamController[C](r),t)}function ro(e){e._state="closed";var r=e._reader;if(void 0!==r&&(F(r),ae(r))){var t=r._readRequests;r._readRequests=new R,t.forEach(function(e){e._closeSteps()})}}function to(e,r){e._state="errored",e._storedError=r;var t=e._reader;void 0!==t&&(D(t,r),ae(t)?se(t,r):Sr(t,r))}function oo(e){return new TypeError("ReadableStream.prototype.".concat(e," can only be used on a ReadableStream"))}function no(e,r){M(e,r);var t=null==e?void 0:e.highWaterMark;return N(t,"highWaterMark","QueuingStrategyInit"),{highWaterMark:H(t)}}Object.defineProperties(Gt,{from:{enumerable:!0}}),Object.defineProperties(Gt.prototype,{cancel:{enumerable:!0},getReader:{enumerable:!0},pipeThrough:{enumerable:!0},pipeTo:{enumerable:!0},tee:{enumerable:!0},values:{enumerable:!0},locked:{enumerable:!0}}),i(Gt.from,"from"),i(Gt.prototype.cancel,"cancel"),i(Gt.prototype.getReader,"getReader"),i(Gt.prototype.pipeThrough,"pipeThrough"),i(Gt.prototype.pipeTo,"pipeTo"),i(Gt.prototype.tee,"tee"),i(Gt.prototype.values,"values"),"symbol"==typeof r.toStringTag&&Object.defineProperty(Gt.prototype,r.toStringTag,{value:"ReadableStream",configurable:!0}),Object.defineProperty(Gt.prototype,ye,{value:Gt.prototype.values,writable:!0,configurable:!0});var io=function(e){return e.byteLength};i(io,"size");var ao=function(){function ByteLengthQueuingStrategy(e){Q(e,1,"ByteLengthQueuingStrategy"),e=no(e,"First parameter"),this._byteLengthQueuingStrategyHighWaterMark=e.highWaterMark}return Object.defineProperty(ByteLengthQueuingStrategy.prototype,"highWaterMark",{get:function(){if(!lo(this))throw uo("highWaterMark");return this._byteLengthQueuingStrategyHighWaterMark},enumerable:!1,configurable:!0}),Object.defineProperty(ByteLengthQueuingStrategy.prototype,"size",{get:function(){if(!lo(this))throw uo("size");return io},enumerable:!1,configurable:!0}),ByteLengthQueuingStrategy}();function uo(e){return new TypeError("ByteLengthQueuingStrategy.prototype.".concat(e," can only be used on a ByteLengthQueuingStrategy"))}function lo(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_byteLengthQueuingStrategyHighWaterMark")&&e instanceof ao)}Object.defineProperties(ao.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty(ao.prototype,r.toStringTag,{value:"ByteLengthQueuingStrategy",configurable:!0});var so=function(){return 1};i(so,"size");var co=function(){function CountQueuingStrategy(e){Q(e,1,"CountQueuingStrategy"),e=no(e,"First parameter"),this._countQueuingStrategyHighWaterMark=e.highWaterMark}return Object.defineProperty(CountQueuingStrategy.prototype,"highWaterMark",{get:function(){if(!po(this))throw fo("highWaterMark");return this._countQueuingStrategyHighWaterMark},enumerable:!1,configurable:!0}),Object.defineProperty(CountQueuingStrategy.prototype,"size",{get:function(){if(!po(this))throw fo("size");return so},enumerable:!1,configurable:!0}),CountQueuingStrategy}();function fo(e){return new TypeError("CountQueuingStrategy.prototype.".concat(e," can only be used on a CountQueuingStrategy"))}function po(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_countQueuingStrategyHighWaterMark")&&e instanceof co)}function bo(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}function ho(e,r,t){return x(e,t),function(t){return S(e,r,[t])}}function _o(e,r,t){return x(e,t),function(t,o){return w(e,r,[t,o])}}function mo(e,r,t){return x(e,t),function(t){return w(e,r,[t])}}Object.defineProperties(co.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty(co.prototype,r.toStringTag,{value:"CountQueuingStrategy",configurable:!0});var vo=function(){function TransformStream(e,r,t){void 0===e&&(e={}),void 0===r&&(r={}),void 0===t&&(t={}),void 0===e&&(e=null);var o=Pr(r,"Second parameter"),n=Pr(t,"Third parameter"),i=function(e,r){M(e,r);var t=null==e?void 0:e.cancel,o=null==e?void 0:e.flush,n=null==e?void 0:e.readableType,i=null==e?void 0:e.start,a=null==e?void 0:e.transform,u=null==e?void 0:e.writableType;return{cancel:void 0===t?void 0:mo(t,e,"".concat(r," has member 'cancel' that")),flush:void 0===o?void 0:bo(o,e,"".concat(r," has member 'flush' that")),readableType:n,start:void 0===i?void 0:ho(i,e,"".concat(r," has member 'start' that")),transform:void 0===a?void 0:_o(a,e,"".concat(r," has member 'transform' that")),writableType:u}}(e,"First parameter");if(void 0!==i.readableType)throw new RangeError("Invalid readableType specified");if(void 0!==i.writableType)throw new RangeError("Invalid writableType specified");var a,u=Rr(n,0),l=Tr(n),s=Rr(o,1),c=Tr(o);!function(e,r,t,o,n,i){function a(){return r}function u(r){return function(e,r){var t=e._transformStreamController;if(e._backpressure){return v(e._backpressureChangePromise,function(){var o=e._writable;if("erroring"===o._state)throw o._storedError;return Eo(t,r)})}return Eo(t,r)}(e,r)}function l(r){return function(e,r){var t=e._transformStreamController;if(void 0!==t._finishPromise)return t._finishPromise;var o=e._readable;t._finishPromise=f(function(e,r){t._finishPromise_resolve=e,t._finishPromise_reject=r});var n=t._cancelAlgorithm(r);return Co(t),h(n,function(){return"errored"===o._state?jo(t,o._storedError):(Dt(o._readableStreamController,r),Oo(t)),null},function(e){return Dt(o._readableStreamController,e),jo(t,e),null}),t._finishPromise}(e,r)}function s(){return function(e){var r=e._transformStreamController;if(void 0!==r._finishPromise)return r._finishPromise;var t=e._readable;r._finishPromise=f(function(e,t){r._finishPromise_resolve=e,r._finishPromise_reject=t});var o=r._flushAlgorithm();return Co(r),h(o,function(){return"errored"===t._state?jo(r,t._storedError):(At(t._readableStreamController),Oo(r)),null},function(e){return Dt(t._readableStreamController,e),jo(r,e),null}),r._finishPromise}(e)}function c(){return function(e){return Ro(e,!1),e._backpressureChangePromise}(e)}function d(r){return function(e,r){var t=e._transformStreamController;if(void 0!==t._finishPromise)return t._finishPromise;var o=e._writable;t._finishPromise=f(function(e,r){t._finishPromise_resolve=e,t._finishPromise_reject=r});var n=t._cancelAlgorithm(r);return Co(t),h(n,function(){return"errored"===o._state?jo(t,o._storedError):(it(o._writableStreamController,r),wo(e),Oo(t)),null},function(r){return it(o._writableStreamController,r),wo(e),jo(t,r),null}),t._finishPromise}(e,r)}e._writable=function(e,r,t,o,n,i){void 0===n&&(n=1),void 0===i&&(i=function(){return 1});var a=Object.create(Br.prototype);return Ar(a),rt(a,Object.create($r.prototype),e,r,t,o,n,i),a}(a,u,s,l,t,o),e._readable=Xt(a,c,d,n,i),e._backpressure=void 0,e._backpressureChangePromise=void 0,e._backpressureChangePromise_resolve=void 0,Ro(e,!0),e._transformStreamController=void 0}(this,f(function(e){a=e}),s,c,u,l),function(e,r){var t,o,n,i=Object.create(To.prototype);t=void 0!==r.transform?function(e){return r.transform(e,i)}:function(e){try{return qo(i,e),d(void 0)}catch(e){return p(e)}};o=void 0!==r.flush?function(){return r.flush(i)}:function(){return d(void 0)};n=void 0!==r.cancel?function(e){return r.cancel(e)}:function(){return d(void 0)};!function(e,r,t,o,n){r._controlledTransformStream=e,e._transformStreamController=r,r._transformAlgorithm=t,r._flushAlgorithm=o,r._cancelAlgorithm=n,r._finishPromise=void 0,r._finishPromise_resolve=void 0,r._finishPromise_reject=void 0}(e,i,t,o,n)}(this,i),void 0!==i.start?a(i.start(this._transformStreamController)):a(void 0)}return Object.defineProperty(TransformStream.prototype,"readable",{get:function(){if(!yo(this))throw Bo("readable");return this._readable},enumerable:!1,configurable:!0}),Object.defineProperty(TransformStream.prototype,"writable",{get:function(){if(!yo(this))throw Bo("writable");return this._writable},enumerable:!1,configurable:!0}),TransformStream}();function yo(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_transformStreamController")&&e instanceof vo)}function go(e,r){Dt(e._readable._readableStreamController,r),So(e,r)}function So(e,r){Co(e._transformStreamController),it(e._writable._writableStreamController,r),wo(e)}function wo(e){e._backpressure&&Ro(e,!1)}function Ro(e,r){void 0!==e._backpressureChangePromise&&e._backpressureChangePromise_resolve(),e._backpressureChangePromise=f(function(r){e._backpressureChangePromise_resolve=r}),e._backpressure=r}Object.defineProperties(vo.prototype,{readable:{enumerable:!0},writable:{enumerable:!0}}),"symbol"==typeof r.toStringTag&&Object.defineProperty(vo.prototype,r.toStringTag,{value:"TransformStream",configurable:!0});var To=function(){function TransformStreamDefaultController(){throw new TypeError("Illegal constructor")}return Object.defineProperty(TransformStreamDefaultController.prototype,"desiredSize",{get:function(){if(!Po(this))throw Wo("desiredSize");return Ft(this._controlledTransformStream._readable._readableStreamController)},enumerable:!1,configurable:!0}),TransformStreamDefaultController.prototype.enqueue=function(e){if(void 0===e&&(e=void 0),!Po(this))throw Wo("enqueue");qo(this,e)},TransformStreamDefaultController.prototype.error=function(e){if(void 0===e&&(e=void 0),!Po(this))throw Wo("error");var r;r=e,go(this._controlledTransformStream,r)},TransformStreamDefaultController.prototype.terminate=function(){if(!Po(this))throw Wo("terminate");!function(e){var r=e._controlledTransformStream;At(r._readable._readableStreamController);var t=new TypeError("TransformStream terminated");So(r,t)}(this)},TransformStreamDefaultController}();function Po(e){return!!o(e)&&(!!Object.prototype.hasOwnProperty.call(e,"_controlledTransformStream")&&e instanceof To)}function Co(e){e._transformAlgorithm=void 0,e._flushAlgorithm=void 0,e._cancelAlgorithm=void 0}function qo(e,r){var t=e._controlledTransformStream,o=t._readable._readableStreamController;if(!Lt(o))throw new TypeError("Readable side is not in a state that permits enqueue");try{zt(o,r)}catch(e){throw So(t,e),t._readable._storedError}var n=function(e){return!Bt(e)}(o);n!==t._backpressure&&Ro(t,!0)}function Eo(e,r){return v(e._transformAlgorithm(r),void 0,function(r){throw go(e._controlledTransformStream,r),r})}function Wo(e){return new TypeError("TransformStreamDefaultController.prototype.".concat(e," can only be used on a TransformStreamDefaultController"))}function Oo(e){void 0!==e._finishPromise_resolve&&(e._finishPromise_resolve(),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}function jo(e,r){void 0!==e._finishPromise_reject&&(y(e._finishPromise),e._finishPromise_reject(r),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}function Bo(e){return new TypeError("TransformStream.prototype.".concat(e," can only be used on a TransformStream"))}Object.defineProperties(To.prototype,{enqueue:{enumerable:!0},error:{enumerable:!0},terminate:{enumerable:!0},desiredSize:{enumerable:!0}}),i(To.prototype.enqueue,"enqueue"),i(To.prototype.error,"error"),i(To.prototype.terminate,"terminate"),"symbol"==typeof r.toStringTag&&Object.defineProperty(To.prototype,r.toStringTag,{value:"TransformStreamDefaultController",configurable:!0}),e.ByteLengthQueuingStrategy=ao,e.CountQueuingStrategy=co,e.ReadableByteStreamController=ze,e.ReadableStream=Gt,e.ReadableStreamBYOBReader=_r,e.ReadableStreamBYOBRequest=Ae,e.ReadableStreamDefaultController=Wt,e.ReadableStreamDefaultReader=ee,e.TransformStream=vo,e.TransformStreamDefaultController=To,e.WritableStream=Br,e.WritableStreamDefaultController=$r,e.WritableStreamDefaultWriter=Hr}); diff --git a/Polyfills/Worker/CMakeLists.txt b/Polyfills/Worker/CMakeLists.txt new file mode 100644 index 00000000..4a33a7b8 --- /dev/null +++ b/Polyfills/Worker/CMakeLists.txt @@ -0,0 +1,31 @@ +set(SOURCES + "Include/Babylon/Polyfills/Worker.h" + "Source/Worker.cpp" + "Source/Worker.h" + "Source/WorkerScripts.h") + +add_library(Worker ${SOURCES}) +warnings_as_errors(Worker) + +target_include_directories(Worker PUBLIC "Include") + +target_link_libraries(Worker + PUBLIC AppRuntime + PRIVATE AbortController + PRIVATE Blob + PRIVATE Compression + PRIVATE Console + PRIVATE Fetch + PRIVATE File + PRIVATE IndexedDB + PRIVATE Performance + PRIVATE Scheduling + PRIVATE Streams + PRIVATE TextDecoder + PRIVATE TextEncoder + PRIVATE URL + PRIVATE WebSocket + PRIVATE XMLHttpRequest) + +set_property(TARGET Worker PROPERTY FOLDER Polyfills) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Polyfills/Worker/Include/Babylon/Polyfills/Worker.h b/Polyfills/Worker/Include/Babylon/Polyfills/Worker.h new file mode 100644 index 00000000..00a03921 --- /dev/null +++ b/Polyfills/Worker/Include/Babylon/Polyfills/Worker.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include +#include + +namespace Babylon::Polyfills::Worker +{ + struct Options + { + // Base directory used for relative paths and app:/// URLs. Files are + // read directly from this directory when ScriptResolver is not set. + std::string ScriptRoot{}; + + // Optional host asset resolver. The callback receives a normalized + // worker URL and returns its JavaScript source. It may be called from + // any Worker runtime thread and therefore must be thread-safe. + std::function ScriptResolver{}; + + // Optional worker-console sink. The callback is invoked on the Worker + // runtime thread and must be thread-safe. + std::function ConsoleCallback{}; + }; + + // Installs Worker, EventTarget, Event, MessageEvent, ErrorEvent and + // DOMException on the current global object. + void BABYLON_API Initialize(Napi::Env env, Options options = {}); +} diff --git a/Polyfills/Worker/Readme.md b/Polyfills/Worker/Readme.md new file mode 100644 index 00000000..ebc5fa6a --- /dev/null +++ b/Polyfills/Worker/Readme.md @@ -0,0 +1,63 @@ +# Worker + +Provides a browser-compatible dedicated `Worker` backed by one `AppRuntime` +(and therefore one engine realm and native thread) per Worker. + +Implemented surface: + +- `new Worker(url, { name, type })`, `postMessage()`, and `terminate()` +- `WorkerGlobalScope` / `DedicatedWorkerGlobalScope`, `self`, `location`, + `close()`, synchronous `importScripts()`, and worker `postMessage()` +- `EventTarget`, `Event`, `MessageEvent`, `ErrorEvent`, `DOMException`, + `onmessage`, `onmessageerror`, and `onerror` +- the standalone in-memory IndexedDB polyfill, including object stores, + indexes, cursors, key ranges, upgrade/versionchange handling, transactional + rollback, and storage structured clone +- worker-relative string and `URL` inputs to `fetch()`, plus the standalone + Streams, Blob, Fetch/Response, and Compression polyfills used by browser + application bundles +- structured cloning for cyclic objects, arrays, dates, regular expressions, + maps, sets, errors, ArrayBuffers, DataViews, typed arrays, BigInts, and + special number values +- transferable ArrayBuffers using the N-API v7 detach operation + +The default loader reads relative paths and `app:///` URLs below +`Options::ScriptRoot`. Native applications can instead supply a thread-safe +`ScriptResolver` for packaged assets. + +WHATWG `URL` serializes a hostless `app:///worker.js` URL as +`app:/worker.js`; both spellings resolve through `ScriptRoot`. Worker +`location` exposes the URL fields application bundles normally inspect +(`protocol`, `origin`, `pathname`, and related fields), not only `href`. +Native JavaScriptCore class objects are normalized inside the Worker realm so +browser-style constructor feature checks such as +`typeof AbortController === "function"` behave as expected. + +IndexedDB is intentionally in-memory and scoped to one JavaScript realm. It +provides the browser API and transactional semantics needed by worker bundles, +including the visualization integration fixture, but not durable storage or +cross-realm database sharing. A future persistent backend can replace it +without changing the Worker implementation because the initializer preserves a +host-provided `indexedDB`. + +Streams, Blob streaming, Fetch body handling, and gzip/deflate transforms come +from their independent polyfill targets. Worker only initializes them in +browser-compatible dependency order; it does not carry private, reduced +versions of those APIs. + +`type: "module"` accepts self-contained, script-compatible application bundles. +JavaScriptCore's public C API has no module-loader hook, so the application's +normal bundler must flatten top-level `import` and `export` declarations before +loading. Classic workers and bundled module workers share the same isolated +runtime. + +On system JavaScriptCore, `terminate()` uses the engine execution-time-limit +symbol when the installed build exports it and can interrupt a tight loop. +Other engines, and older WebKitGTK builds without that symbol, currently stop +between dispatches until their native interrupt hooks are connected. + +`WorkerGlobalScope.close()` stops future worker tasks but preserves messages +and uncaught errors produced later in the task that called it, as required by +WPT. Worker-owned runtime state is torn down on the worker thread; the final +strong reference to the parent-realm `Worker` object is released by a FIFO +dispatch on the parent runtime after worker engine/environment teardown. diff --git a/Polyfills/Worker/Source/Worker.cpp b/Polyfills/Worker/Source/Worker.cpp new file mode 100644 index 00000000..782044a4 --- /dev/null +++ b/Polyfills/Worker/Source/Worker.cpp @@ -0,0 +1,717 @@ +#include "Worker.h" +#include "WorkerScripts.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Babylon::Polyfills::Internal +{ + namespace + { + using Options = Babylon::Polyfills::Worker::Options; + + bool HasScheme(const std::string& value) + { + const auto separator = value.find(':'); + if (separator == std::string::npos || separator == 0) + { + return false; + } + for (std::size_t i = 0; i < separator; ++i) + { + const auto c = static_cast(value[i]); + if (!(std::isalnum(c) || c == '+' || c == '-' || c == '.')) + { + return false; + } + } + return true; + } + + std::string PercentDecode(const std::string& value) + { + std::string result; + result.reserve(value.size()); + for (std::size_t i = 0; i < value.size(); ++i) + { + if (value[i] == '%' && i + 2 < value.size()) + { + const auto hex = value.substr(i + 1, 2); + char* end{}; + const auto decoded = std::strtoul(hex.c_str(), &end, 16); + if (end != nullptr && *end == '\0') + { + result.push_back(static_cast(decoded)); + i += 2; + continue; + } + } + result.push_back(value[i]); + } + return result; + } + + std::string ResolveUrl(const std::string& root, + const std::string& base, + const std::string& requested) + { + if (requested.rfind("data:", 0) == 0 || requested.rfind("file://", 0) == 0 || + requested.rfind("app:", 0) == 0) + { + return requested; + } + + // A leading slash is origin-relative in Worker/importScripts, not + // a host-filesystem escape from ScriptRoot. + if (!requested.empty() && requested.front() == '/') + { + return (std::filesystem::path{root} / requested.substr(1)).lexically_normal().string(); + } + + if (HasScheme(requested)) + { + return requested; + } + + if (base.rfind("app:", 0) == 0) + { + // WHATWG URL serializes a hostless custom scheme such as + // app:///worker.js as app:/worker.js. Accept both that form + // and the app://host/path form used by existing hosts. + const bool hasAuthority = base.rfind("app://", 0) == 0; + const auto pathStart = hasAuthority ? base.find('/', 6) : base.find('/', 4); + const std::string prefix = pathStart == std::string::npos ? + (hasAuthority ? base + "/" : "app:/") : + base.substr(0, pathStart + 1); + const std::filesystem::path path = pathStart == std::string::npos ? + std::filesystem::path{} : + std::filesystem::path{base.substr(pathStart + 1)}.parent_path(); + return prefix + (path / requested).lexically_normal().generic_string(); + } + + if (base.rfind("file://", 0) == 0) + { + const auto path = std::filesystem::path{base.substr(7)}.parent_path() / requested; + return "file://" + path.lexically_normal().generic_string(); + } + + if (base.rfind("data:", 0) == 0) + { + throw std::runtime_error{"A relative importScripts URL cannot be resolved from a data URL"}; + } + + std::filesystem::path parent = root; + if (!base.empty() && !HasScheme(base)) + { + parent = std::filesystem::path{base}.parent_path(); + } + return (parent / requested).lexically_normal().string(); + } + + std::string ReadFile(const std::filesystem::path& path) + { + std::ifstream stream{path, std::ios::binary}; + if (!stream) + { + throw std::runtime_error{"Unable to load Worker script: " + path.string()}; + } + return {std::istreambuf_iterator{stream}, std::istreambuf_iterator{}}; + } + + std::filesystem::path ResolveWithinRoot(const std::string& root, + const std::filesystem::path& candidate) + { + const auto configuredRoot = root.empty() ? std::filesystem::current_path() : + std::filesystem::path{root}; + const auto rootPath = std::filesystem::weakly_canonical( + std::filesystem::absolute(configuredRoot)); + const auto candidatePath = std::filesystem::weakly_canonical( + std::filesystem::absolute(candidate)); + + auto rootPart = rootPath.begin(); + auto candidatePart = candidatePath.begin(); + for (; rootPart != rootPath.end(); ++rootPart, ++candidatePart) + { + if (candidatePart == candidatePath.end() || *candidatePart != *rootPart) + { + throw std::runtime_error{"Worker script path escapes ScriptRoot: " + candidate.string()}; + } + } + + return candidatePath; + } + + std::string LoadSource(const Options& options, const std::string& url) + { + if (options.ScriptResolver) + { + return options.ScriptResolver(url); + } + + if (url.rfind("data:", 0) == 0) + { + const auto comma = url.find(','); + if (comma == std::string::npos || url.substr(0, comma).find(";base64") != std::string::npos) + { + throw std::runtime_error{"Only percent-encoded JavaScript data URLs are supported"}; + } + return PercentDecode(url.substr(comma + 1)); + } + + if (url.rfind("app:", 0) == 0) + { + const bool hasAuthority = url.rfind("app://", 0) == 0; + const auto pathStart = hasAuthority ? url.find('/', 6) : url.find('/', 4); + const auto relative = pathStart == std::string::npos ? + std::string{} : + url.substr(pathStart + 1); + return ReadFile(ResolveWithinRoot( + options.ScriptRoot, std::filesystem::path{options.ScriptRoot} / relative)); + } + + if (url.rfind("file://", 0) == 0) + { + return ReadFile(PercentDecode(url.substr(7))); + } + + return ReadFile(ResolveWithinRoot(options.ScriptRoot, url)); + } + +#if NAPI_VERSION >= 7 + void ThrowStatus(Napi::Env env, napi_status status, const char* operation) + { + if (status == napi_pending_exception && env.IsExceptionPending()) + { + throw env.GetAndClearPendingException(); + } + throw Napi::Error::New(env, std::string{operation} + " failed with Node-API status " + + std::to_string(static_cast(status))); + } + + [[noreturn]] void ThrowDataCloneError(Napi::Env env, const char* message) + { + const auto exception = env.Global() + .Get("DOMException") + .As() + .New({Napi::String::New(env, message), + Napi::String::New(env, "DataCloneError")}); + throw Napi::Error{env, exception}; + } +#endif + } + + struct Worker::State + { + Options Config{}; + JsRuntime* ParentRuntime{}; + Napi::ObjectReference ParentObject{}; + std::unique_ptr Runtime{}; + std::atomic_bool Terminated{false}; + std::atomic_bool Closed{false}; + std::string ActiveUrl{}; + std::string Name{}; + bool Module{}; + std::mutex RuntimeMutex{}; + }; + + void Worker::Initialize(Napi::Env env, Options options) + { + if (!env.Global().Get("Worker").IsUndefined()) + { + return; + } + + Napi::Eval(env, WorkerScripts::Common, "jsruntimehost-worker-common.js"); + + auto* classOptions = new Options{std::move(options)}; + auto constructor = DefineClass( + env, + "Worker", + { + InstanceMethod("__jsrhNativePostMessage", &Worker::PostMessage), + InstanceMethod("terminate", &Worker::Terminate), + }, + classOptions); + + // Keep constructor data alive for exactly as long as the constructor. + constructor.Set( + "__jsrhOptions", + Napi::External::New(env, classOptions, [](Napi::Env, Options* value) { delete value; })); + + env.Global().Get("__jsrhInstallWorker").As().Call(env.Global(), {constructor}); + env.Global().Set("Worker", constructor); + } + + Worker::Worker(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + if (info.Length() == 0) + { + throw Napi::TypeError::New(info.Env(), "Worker requires a script URL"); + } + + auto state = std::make_shared(); + state->Config = *static_cast(info.Data()); + state->ParentRuntime = &JsRuntime::GetFromJavaScript(info.Env()); + // An active Worker is a browser "active object": it remains alive even + // if script drops its last reference, until terminate()/close(). A + // strong reference also works on engines such as QuickJS whose N-API + // weak-reference adapter cannot materialize a live weak value. + state->ParentObject = Napi::Persistent(info.This().As()); + + const auto requestedUrl = info[0].ToString().Utf8Value(); + const auto resolvedUrl = ResolveUrl(state->Config.ScriptRoot, {}, requestedUrl); + bool module = false; + if (info.Length() > 1 && info[1].IsObject()) + { + const auto options = info[1].As(); + if (options.Has("type")) + { + const auto type = options.Get("type").ToString().Utf8Value(); + if (type != "classic" && type != "module") + { + throw Napi::TypeError::New(info.Env(), "Worker type must be 'classic' or 'module'"); + } + module = type == "module"; + } + if (options.Has("name")) + { + state->Name = options.Get("name").ToString().Utf8Value(); + } + } + state->Module = module; + + AppRuntime::Options runtimeOptions{}; + const std::weak_ptr weakState{state}; + runtimeOptions.UnhandledExceptionHandler = [weakState](const Napi::Error& error) { + const auto locked = weakState.lock(); + if (!locked || locked->Terminated.load()) + { + return; + } + // Some engines expose a stack without its usual "Error: message" + // header (QuickJS is one). Always retain the exception message: + // parent Worker.onerror is the only startup diagnostic for a + // bundle that fails before installing its message protocol. + auto detail = Napi::GetErrorString(error); + const auto& message = error.Message(); + if (!message.empty() && detail.find(message) == std::string::npos) + { + detail = message + (detail.empty() ? "" : "\n" + detail); + } + DispatchErrorToParent(weakState, std::move(detail)); + }; + runtimeOptions.ThreadExitHandler = [weakState] { + const auto locked = weakState.lock(); + if (!locked) + { + return; + } + + // The Worker wrapper is a parent-realm JS object. Never release + // its strong reference from the worker thread: WebKit fixed this + // exact cross-thread destruction pattern after a terminate UAF. + // Queue this after all same-task message/error deliveries so + // WorkerGlobalScope.close() preserves their FIFO ordering. + // https://github.com/WebKit/WebKit/commit/4aaa3c1477e296e67b03e1461479b8caf57c37dd + locked->ParentRuntime->Dispatch([weakState](Napi::Env) { + const auto parentState = weakState.lock(); + if (parentState && !parentState->ParentObject.IsEmpty()) + { + parentState->ParentObject.Reset(); + } + }); + }; + + state->Runtime = std::make_unique(std::move(runtimeOptions)); + state->Runtime->Dispatch([state, resolvedUrl, module](Napi::Env env) { + InitializeWorker(state, env, resolvedUrl, module); + }); + + m_state = std::move(state); + } + + Worker::~Worker() + { + Stop(); + } + + void Worker::Stop() + { + auto state = std::move(m_state); + if (!state) + { + return; + } + + state->Terminated.store(true); + state->Closed.store(true); + { + std::scoped_lock lock{state->RuntimeMutex}; + if (state->Runtime) + { + state->Runtime->Terminate(); + } + } + state->ParentObject.Reset(); + state->Runtime.reset(); + } + + void Worker::Terminate(const Napi::CallbackInfo&) + { + if (!m_state || m_state->Terminated.exchange(true)) + { + return; + } + + m_state->Closed.store(true); + std::scoped_lock lock{m_state->RuntimeMutex}; + if (m_state->Runtime) + { + m_state->Runtime->Terminate(); + } + m_state->ParentObject.Reset(); + } + + Worker::Message Worker::Serialize(Napi::Env env, + const Napi::Value& value, + const Napi::Value& transfer) + { + const auto encoded = env.Global() + .Get("__jsrhSerialize") + .As() + .Call(env.Global(), {value, transfer}) + .As(); + + Message result{}; + result.Json = encoded.Get("json").As().Utf8Value(); + const auto buffers = encoded.Get("buffers").As(); + const auto transferBuffers = encoded.Get("transferBuffers").As(); +#if NAPI_VERSION >= 7 + // Validate all transferables before copying or detaching any of them. + // Some engines do not expose detached state as a JavaScript property, + // so the Node-API check is the portable source of truth. + for (std::uint32_t i = 0; i < transferBuffers.Length(); ++i) + { + bool detached{}; + const napi_status status = napi_is_detached_arraybuffer(env, transferBuffers.Get(i), &detached); + if (status != napi_ok) + { + ThrowStatus(env, status, "napi_is_detached_arraybuffer"); + } + if (detached) + { + ThrowDataCloneError(env, "An ArrayBuffer in the transfer list is already detached"); + } + } +#endif + + result.Buffers.reserve(buffers.Length()); + for (std::uint32_t i = 0; i < buffers.Length(); ++i) + { + const auto buffer = buffers.Get(i).As(); + std::vector bytes(buffer.ByteLength()); + if (!bytes.empty()) + { + std::memcpy(bytes.data(), buffer.Data(), bytes.size()); + } + result.Buffers.emplace_back(std::move(bytes)); + } + + // Duplicate and invalid entries were rejected by the JS serializer; + // after validation and copying, detach each original transfer target. + // The native byte-copy loop above only inspects JS-side snapshots, so + // JavaScriptCore never exposes the original backing pointer before its + // standards-track transfer() implementation detaches it. + for (std::uint32_t i = 0; i < transferBuffers.Length(); ++i) + { +#if NAPI_VERSION >= 7 + const napi_status status = napi_detach_arraybuffer(env, transferBuffers.Get(i)); + if (status != napi_ok) + { + ThrowStatus(env, status, "napi_detach_arraybuffer"); + } +#else + (void)i; + throw Napi::Error::New(env, "Transferable ArrayBuffers require N-API v7"); +#endif + } + + return result; + } + + Napi::Value Worker::Deserialize(Napi::Env env, const Message& message) + { + const auto buffers = Napi::Array::New(env, message.Buffers.size()); + for (std::size_t i = 0; i < message.Buffers.size(); ++i) + { + const auto& bytes = message.Buffers[i]; + auto buffer = Napi::ArrayBuffer::New(env, bytes.size()); + if (!bytes.empty()) + { + std::memcpy(buffer.Data(), bytes.data(), bytes.size()); + } + buffers.Set(static_cast(i), buffer); + } + + return env.Global() + .Get("__jsrhDeserialize") + .As() + .Call(env.Global(), {Napi::String::New(env, message.Json), buffers}); + } + + Napi::Value Worker::PostMessage(const Napi::CallbackInfo& info) + { + if (!m_state || m_state->Terminated.load() || m_state->Closed.load()) + { + return info.Env().Undefined(); + } + + const auto envelope = info[0].As(); + const auto value = envelope.Get("__jsrhMessage"); + const auto transfer = envelope.Get("__jsrhTransfer"); + auto message = Serialize(info.Env(), value, transfer); + const std::weak_ptr weakState{m_state}; + + std::scoped_lock lock{m_state->RuntimeMutex}; + if (m_state->Runtime && !m_state->Terminated.load()) + { + m_state->Runtime->Dispatch([weakState, message = std::move(message)](Napi::Env env) mutable { + const auto state = weakState.lock(); + if (!state || state->Terminated.load() || state->Closed.load()) + { + return; + } + const auto value = Deserialize(env, message); + env.Global().Get("__jsrhDispatchMessage").As().Call( + env.Global(), {env.Global(), value}); + }); + } + + return info.Env().Undefined(); + } + + void Worker::InitializeWorker(const std::shared_ptr& state, + Napi::Env env, + const std::string& url, + bool module) + { + (void)module; + Babylon::Polyfills::Console::Initialize(env, [weakState = std::weak_ptr{state}](const char* message, + Babylon::Polyfills::Console::LogLevel) { + const auto locked = weakState.lock(); + if (locked && locked->Config.ConsoleCallback) + { + locked->Config.ConsoleCallback(message); + } + }); + Babylon::Polyfills::AbortController::Initialize(env); + Babylon::Polyfills::Performance::Initialize(env); + Babylon::Polyfills::Scheduling::Initialize(env); + Babylon::Polyfills::URL::Initialize(env); + Babylon::Polyfills::WebSocket::Initialize(env); + Babylon::Polyfills::XMLHttpRequest::Initialize(env); + Babylon::Polyfills::Streams::Initialize(env); + Babylon::Polyfills::Blob::Initialize(env); + Babylon::Polyfills::File::Initialize(env); + Babylon::Polyfills::TextDecoder::Initialize(env); + Babylon::Polyfills::TextEncoder::Initialize(env); + Babylon::Polyfills::Fetch::Initialize(env); + Babylon::Polyfills::Compression::Initialize(env); + Babylon::Polyfills::IndexedDB::Initialize(env); + + const std::weak_ptr weakState{state}; + env.Global().Set("__jsrhNativePostMessage", Napi::Function::New( + env, + [weakState](const Napi::CallbackInfo& info) { + const auto locked = weakState.lock(); + if (!locked || locked->Terminated.load()) + { + return info.Env().Undefined(); + } + const auto envelope = info[0].As(); + const auto value = envelope.Get("__jsrhMessage"); + const auto transfer = envelope.Get("__jsrhTransfer"); + DispatchMessageToParent(weakState, Serialize(info.Env(), value, transfer)); + return info.Env().Undefined(); + }, + "postMessage")); + + env.Global().Set("__jsrhNativeClose", Napi::Function::New( + env, + [weakState](const Napi::CallbackInfo& info) { + const auto locked = weakState.lock(); + if (locked && !locked->Terminated.load() && !locked->Closed.exchange(true)) + { + std::scoped_lock lock{locked->RuntimeMutex}; + if (locked->Runtime) + { + // WorkerGlobalScope.close() discards future tasks but + // must let this task finish (including postMessage and + // an uncaught error). Parent terminate() remains the + // immediate-interrupt path for tight loops. + locked->Runtime->Close(); + } + } + return info.Env().Undefined(); + }, + "close")); + + env.Global().Set("__jsrhNativeImportScripts", Napi::Function::New( + env, + [weakState](const Napi::CallbackInfo& info) { + const auto locked = weakState.lock(); + if (!locked || locked->Terminated.load() || locked->Closed.load()) + { + return info.Env().Undefined(); + } + if (locked->Module) + { + throw Napi::TypeError::New(info.Env(), "importScripts is unavailable in a module Worker"); + } + + for (std::size_t i = 0; i < info.Length(); ++i) + { + const auto requested = info[i].ToString().Utf8Value(); + const auto resolved = ResolveUrl(locked->Config.ScriptRoot, locked->ActiveUrl, requested); + try + { + const auto source = LoadSource(locked->Config, resolved); + const auto previous = std::exchange(locked->ActiveUrl, resolved); + try + { + Napi::Eval(info.Env(), source.c_str(), resolved.c_str()); + } + catch (...) + { + locked->ActiveUrl = previous; + throw; + } + locked->ActiveUrl = previous; + } + catch (const Napi::Error&) + { + throw; + } + catch (const std::exception& error) + { + throw Napi::Error::New(info.Env(), error.what()); + } + } + return info.Env().Undefined(); + }, + "importScripts")); + + env.Global().Set("__jsrhWorkerLocation", Napi::String::New(env, url)); + env.Global().Set("__jsrhWorkerName", Napi::String::New(env, state->Name)); + Napi::Eval(env, WorkerScripts::Common, "jsruntimehost-worker-common.js"); + Napi::Eval(env, WorkerScripts::WorkerGlobal, "jsruntimehost-worker-global.js"); + + try + { + state->ActiveUrl = url; + const auto source = LoadSource(state->Config, url); + // JavaScriptCore's C API exposes script evaluation but no module + // loader. A script-compatible module bundle is already + // self-contained and can use the same isolated realm; remaining + // import/export declarations surface as a worker error. + Napi::Eval(env, source.c_str(), url.c_str()); + } + catch (const Napi::Error&) + { + throw; + } + catch (const std::exception& error) + { + throw Napi::Error::New(env, error.what()); + } + } + + void Worker::DispatchMessageToParent(const std::weak_ptr& weakState, Message message) + { + const auto state = weakState.lock(); + if (!state || state->Terminated.load()) + { + return; + } + + state->ParentRuntime->Dispatch([weakState, message = std::move(message)](Napi::Env env) mutable { + const auto locked = weakState.lock(); + if (!locked || locked->Terminated.load()) + { + return; + } + const auto target = locked->ParentObject.Value(); + if (target.IsEmpty()) + { + return; + } + const auto value = Deserialize(env, message); + env.Global().Get("__jsrhDispatchMessage").As().Call( + env.Global(), {target, value}); + }); + } + + void Worker::DispatchErrorToParent(const std::weak_ptr& weakState, std::string message) + { + const auto state = weakState.lock(); + if (!state || state->Terminated.load()) + { + return; + } + + state->ParentRuntime->Dispatch([weakState, message = std::move(message)](Napi::Env env) { + const auto locked = weakState.lock(); + if (!locked || locked->Terminated.load()) + { + return; + } + const auto target = locked->ParentObject.Value(); + if (!target.IsEmpty()) + { + env.Global().Get("__jsrhDispatchError").As().Call( + env.Global(), {target, Napi::String::New(env, message)}); + } + }); + } +} + +namespace Babylon::Polyfills::Worker +{ + void BABYLON_API Initialize(Napi::Env env, Options options) + { + Internal::Worker::Initialize(env, std::move(options)); + } +} diff --git a/Polyfills/Worker/Source/Worker.h b/Polyfills/Worker/Source/Worker.h new file mode 100644 index 00000000..ff76eab7 --- /dev/null +++ b/Polyfills/Worker/Source/Worker.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace Babylon::Polyfills::Internal +{ + class Worker final : public Napi::ObjectWrap + { + public: + static void Initialize(Napi::Env env, Babylon::Polyfills::Worker::Options options); + + explicit Worker(const Napi::CallbackInfo& info); + ~Worker() override; + + private: + struct Message + { + std::string Json; + std::vector> Buffers; + }; + + struct State; + + Napi::Value PostMessage(const Napi::CallbackInfo& info); + void Terminate(const Napi::CallbackInfo& info); + void Stop(); + + static Message Serialize(Napi::Env env, const Napi::Value& value, const Napi::Value& transfer); + static Napi::Value Deserialize(Napi::Env env, const Message& message); + static void InitializeWorker(const std::shared_ptr& state, + Napi::Env env, + const std::string& url, + bool module); + static void DispatchMessageToParent(const std::weak_ptr& state, Message message); + static void DispatchErrorToParent(const std::weak_ptr& state, std::string message); + + std::shared_ptr m_state{}; + }; +} diff --git a/Polyfills/Worker/Source/WorkerScripts.h b/Polyfills/Worker/Source/WorkerScripts.h new file mode 100644 index 00000000..adff1634 --- /dev/null +++ b/Polyfills/Worker/Source/WorkerScripts.h @@ -0,0 +1,570 @@ +#pragma once + +namespace Babylon::Polyfills::Internal::WorkerScripts +{ + inline constexpr char Common[] = R"JSRH( +(() => { + 'use strict'; + const g = globalThis; + if (g.__jsrhWorkerCommonInstalled) return; + + const listeners = new WeakMap(); + const handlers = new WeakMap(); + + const DOMException = typeof g.DOMException === 'function' + ? g.DOMException + : class DOMException extends Error { + constructor(message = '', name = 'Error') { + super(String(message)); + this.name = String(name); + const codes = { IndexSizeError: 1, HierarchyRequestError: 3, + WrongDocumentError: 4, InvalidCharacterError: 5, + NoModificationAllowedError: 7, NotFoundError: 8, + NotSupportedError: 9, InUseAttributeError: 10, + InvalidStateError: 11, SyntaxError: 12, InvalidModificationError: 13, + NamespaceError: 14, InvalidAccessError: 15, TypeMismatchError: 17, + SecurityError: 18, NetworkError: 19, AbortError: 20, + URLMismatchError: 21, QuotaExceededError: 22, TimeoutError: 23, + InvalidNodeTypeError: 24, DataCloneError: 25 }; + Object.defineProperty(this, 'code', { + value: codes[this.name] || 0, enumerable: true + }); + } + }; + + class Event { + constructor(type, init = {}) { + if (arguments.length === 0) throw new TypeError('Event type is required'); + this._type = String(type); + this._bubbles = Boolean(init.bubbles); + this._cancelable = Boolean(init.cancelable); + this._composed = Boolean(init.composed); + this._target = null; + this._currentTarget = null; + this._defaultPrevented = false; + this._stopped = false; + this._immediateStopped = false; + this._timeStamp = Date.now(); + } + get type() { return this._type; } + get target() { return this._target; } + get srcElement() { return this._target; } + get currentTarget() { return this._currentTarget; } + get eventPhase() { return this._currentTarget === null ? 0 : 2; } + get bubbles() { return this._bubbles; } + get cancelable() { return this._cancelable; } + get composed() { return this._composed; } + get defaultPrevented() { return this._defaultPrevented; } + get timeStamp() { return this._timeStamp; } + get isTrusted() { return false; } + preventDefault() { if (this._cancelable) this._defaultPrevented = true; } + stopPropagation() { this._stopped = true; } + stopImmediatePropagation() { + this._stopped = true; + this._immediateStopped = true; + } + composedPath() { return this._target === null ? [] : [this._target]; } + } + Event.NONE = 0; + Event.CAPTURING_PHASE = 1; + Event.AT_TARGET = 2; + Event.BUBBLING_PHASE = 3; + + class MessageEvent extends Event { + constructor(type, init = {}) { + super(type, init); + this.data = init.data === undefined ? null : init.data; + this.origin = init.origin === undefined ? '' : String(init.origin); + this.lastEventId = init.lastEventId === undefined ? '' : String(init.lastEventId); + this.source = init.source === undefined ? null : init.source; + this.ports = init.ports === undefined ? [] : Array.from(init.ports); + } + } + + class ErrorEvent extends Event { + constructor(type, init = {}) { + super(type, Object.assign({ cancelable: true }, init)); + this.message = init.message === undefined ? '' : String(init.message); + this.filename = init.filename === undefined ? '' : String(init.filename); + this.lineno = Number(init.lineno || 0); + this.colno = Number(init.colno || 0); + this.error = init.error === undefined ? null : init.error; + } + } + + function optionCapture(options) { + return typeof options === 'boolean' ? options : Boolean(options && options.capture); + } + + class EventTarget { + addEventListener(type, callback, options = false) { + if (callback === null || callback === undefined) return; + if (typeof callback !== 'function' && + (typeof callback !== 'object' || typeof callback.handleEvent !== 'function')) return; + type = String(type); + let byType = listeners.get(this); + if (!byType) listeners.set(this, byType = new Map()); + let list = byType.get(type); + if (!list) byType.set(type, list = []); + const capture = optionCapture(options); + if (list.some(x => x.callback === callback && x.capture === capture)) return; + const entry = { callback, capture, once: Boolean(options && options.once) }; + list.push(entry); + if (options && options.signal) { + if (options.signal.aborted) { + list.splice(list.indexOf(entry), 1); + } else if (typeof options.signal.addEventListener === 'function') { + options.signal.addEventListener('abort', () => { + this.removeEventListener(type, callback, capture); + }, { once: true }); + } + } + } + + removeEventListener(type, callback, options = false) { + const byType = listeners.get(this); + const list = byType && byType.get(String(type)); + if (!list) return; + const capture = optionCapture(options); + const index = list.findIndex(x => x.callback === callback && x.capture === capture); + if (index !== -1) list.splice(index, 1); + } + + dispatchEvent(event) { + if (!(event instanceof Event)) throw new TypeError('Argument 1 is not an Event'); + if (event._currentTarget !== null) throw new DOMException('Event is already being dispatched', 'InvalidStateError'); + event._target = this; + event._currentTarget = this; + event._immediateStopped = false; + const byType = listeners.get(this); + const snapshot = byType && byType.get(event.type) ? byType.get(event.type).slice() : []; + for (const entry of snapshot) { + if (event._immediateStopped) break; + const current = byType && byType.get(event.type); + if (!current || current.indexOf(entry) === -1) continue; + if (entry.once) this.removeEventListener(event.type, entry.callback, entry.capture); + if (typeof entry.callback === 'function') entry.callback.call(this, event); + else entry.callback.handleEvent.call(entry.callback, event); + } + if (!event._immediateStopped) { + const byTypeHandler = handlers.get(this); + const handler = byTypeHandler && byTypeHandler.get(event.type); + // EventHandler attributes retain arbitrary objects, but only callable + // values are invoked (matching Web IDL's EventHandler processing). + if (typeof handler === 'function') handler.call(this, event); + } + event._currentTarget = null; + return !event.defaultPrevented; + } + } + + function installHandler(target, type) { + const receiver = value => + ((typeof value === 'object' && value !== null) || typeof value === 'function') + ? value + : target; + Object.defineProperty(target, 'on' + type, { + configurable: true, + enumerable: true, + get() { + // JavaScriptCore can call an accessor installed directly on its + // engine-owned global object without an object receiver for an + // unqualified assignment such as `onmessage = callback`. + const byType = handlers.get(receiver(this)); + return byType && byType.has(type) ? byType.get(type) : null; + }, + set(value) { + const owner = receiver(this); + let byType = handlers.get(owner); + if (!byType) handlers.set(owner, byType = new Map()); + if ((typeof value === 'object' && value !== null) || typeof value === 'function') { + byType.set(type, value); + } else { + byType.delete(type); + } + } + }); + } + + function dataCloneError(message) { + return new DOMException(message, 'DataCloneError'); + } + + function normalizeTransfers(argument) { + if (argument === undefined || argument === null) return []; + const value = Array.isArray(argument) ? argument : argument.transfer; + if (value === undefined || value === null) return []; + try { return Array.from(value); } + catch (_) { throw new TypeError('transfer must be an iterable'); } + } + + function serialize(root, transferArgument) { + const transferList = normalizeTransfers(transferArgument); + const buffers = []; + const bufferIndexes = new Map(); + const transferSet = new Set(); + + function addBuffer(buffer) { + let index = bufferIndexes.get(buffer); + if (index === undefined) { + index = buffers.length; + bufferIndexes.set(buffer, index); + buffers.push(buffer); + } + return index; + } + + for (const value of transferList) { + if (!(value instanceof ArrayBuffer)) throw dataCloneError('Only ArrayBuffer transfer is supported'); + if (transferSet.has(value)) throw dataCloneError('Transfer list contains a duplicate ArrayBuffer'); + if (value.detached === true) throw dataCloneError('A detached ArrayBuffer cannot be transferred'); + transferSet.add(value); + addBuffer(value); + } + + const seen = new Map(); + const nodes = []; + const ref = id => ['r', id]; + const ownProperties = (value, skip) => { + const result = []; + for (const key of Object.keys(value)) { + if (!skip || !skip(key)) result.push([key, encode(value[key])]); + } + return result; + }; + + function encode(value) { + if (value === null || typeof value === 'string' || typeof value === 'boolean') return value; + if (value === undefined) return ['u']; + if (typeof value === 'number') { + if (Number.isNaN(value)) return ['n', 'nan']; + if (value === Infinity) return ['n', 'inf']; + if (value === -Infinity) return ['n', '-inf']; + if (Object.is(value, -0)) return ['n', '-0']; + return value; + } + if (typeof value === 'bigint') return ['bi', value.toString()]; + if (typeof value === 'symbol' || typeof value === 'function') { + throw dataCloneError('The value could not be cloned'); + } + if (typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer) { + throw dataCloneError('SharedArrayBuffer is not available across native runtimes'); + } + if (seen.has(value)) return ref(seen.get(value)); + + const id = nodes.length; + seen.set(value, id); + nodes.push(null); + + if (value instanceof ArrayBuffer) { + nodes[id] = { t: 'ab', b: addBuffer(value) }; + } else if (ArrayBuffer.isView(value)) { + if (value instanceof DataView) { + nodes[id] = { t: 'dv', b: encode(value.buffer), o: value.byteOffset, l: value.byteLength }; + } else { + nodes[id] = { t: 'ta', c: value.constructor.name, b: encode(value.buffer), + o: value.byteOffset, l: value.length }; + } + } else if (Array.isArray(value)) { + const items = []; + for (let i = 0; i < value.length; ++i) { + items.push(Object.prototype.hasOwnProperty.call(value, i) ? encode(value[i]) : ['h']); + } + nodes[id] = { t: 'a', i: items, + p: ownProperties(value, key => key === 'length' || /^(0|[1-9][0-9]*)$/.test(key)) }; + } else if (value instanceof Date) { + nodes[id] = { t: 'd', v: value.getTime(), p: ownProperties(value) }; + } else if (value instanceof RegExp) { + nodes[id] = { t: 're', s: value.source, f: value.flags, x: value.lastIndex, + p: ownProperties(value) }; + } else if (value instanceof Map) { + nodes[id] = { t: 'm', e: Array.from(value, pair => [encode(pair[0]), encode(pair[1])]), + p: ownProperties(value) }; + } else if (value instanceof Set) { + nodes[id] = { t: 's', e: Array.from(value, encode), p: ownProperties(value) }; + } else if (value instanceof Error) { + nodes[id] = { t: 'e', n: value.name, m: value.message, s: value.stack || '', + p: ownProperties(value, key => key === 'name' || key === 'message' || key === 'stack') }; + } else if (value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise) { + throw dataCloneError('The value could not be cloned'); + } else { + nodes[id] = { t: 'o', z: Object.getPrototypeOf(value) === null, p: ownProperties(value) }; + } + return ref(id); + } + + // Keep the buffers used for native byte extraction distinct from the + // transfer targets. In JavaScriptCore, exposing a source buffer's backing + // pointer through the public C API before ArrayBuffer.prototype.transfer() + // can prevent the original JS wrapper from observing the detach. Slicing + // first also makes the required copy-before-detach ordering explicit for + // every backend. + return { + json: JSON.stringify({ root: encode(root), nodes }), + buffers: buffers.map(buffer => buffer.slice(0)), + transferBuffers: transferList + }; + } + + function deserialize(json, buffers) { + const graph = JSON.parse(json); + const nodes = graph.nodes; + const values = new Array(nodes.length); + const built = new Array(nodes.length).fill(false); + + function decode(value) { + if (!Array.isArray(value)) return value; + switch (value[0]) { + case 'u': return undefined; + case 'h': return undefined; + case 'n': return value[1] === 'nan' ? NaN : value[1] === 'inf' ? Infinity : + value[1] === '-inf' ? -Infinity : -0; + case 'bi': return BigInt(value[1]); + case 'r': return build(value[1]); + default: throw dataCloneError('Invalid native structured-clone record'); + } + } + + function properties(target, entries) { + for (const [key, value] of entries || []) { + Object.defineProperty(target, key, { + value: decode(value), writable: true, enumerable: true, configurable: true + }); + } + } + + function build(id) { + if (built[id]) return values[id]; + const node = nodes[id]; + if (!node) throw dataCloneError('Invalid native structured-clone node'); + let value; + switch (node.t) { + case 'a': value = []; break; + case 'o': value = node.z ? Object.create(null) : {}; break; + case 'd': value = new Date(node.v); break; + case 're': value = new RegExp(node.s, node.f); value.lastIndex = node.x; break; + case 'm': value = new Map(); break; + case 's': value = new Set(); break; + case 'e': value = new Error(node.m); value.name = node.n; value.stack = node.s; break; + case 'ab': value = buffers[node.b]; break; + case 'dv': value = new DataView(decode(node.b), node.o, node.l); break; + case 'ta': { + const Constructor = g[node.c]; + if (typeof Constructor !== 'function') throw dataCloneError('Unknown TypedArray ' + node.c); + value = new Constructor(decode(node.b), node.o, node.l); + break; + } + default: throw dataCloneError('Unknown native structured-clone type'); + } + values[id] = value; + built[id] = true; + if (node.t === 'a') { + value.length = node.i.length; + node.i.forEach((item, index) => { if (!(Array.isArray(item) && item[0] === 'h')) value[index] = decode(item); }); + } else if (node.t === 'm') { + for (const pair of node.e) value.set(decode(pair[0]), decode(pair[1])); + } else if (node.t === 's') { + for (const entry of node.e) value.add(decode(entry)); + } + properties(value, node.p); + return value; + } + + return decode(graph.root); + } + + function dispatchMessage(target, value) { + target.dispatchEvent(new MessageEvent('message', { data: value })); + } + + function dispatchError(target, message) { + const error = new Error(String(message)); + target.dispatchEvent(new ErrorEvent('error', { message: String(message), error })); + } + + function installWorker(Constructor) { + const nativePostMessage = Constructor.prototype.__jsrhNativePostMessage; + Object.defineProperty(Constructor.prototype, 'postMessage', { + configurable: true, + writable: true, + value(message, transfer) { + return nativePostMessage.call(this, { + __jsrhMessage: message, + __jsrhTransfer: transfer + }); + } + }); + Object.setPrototypeOf(Constructor.prototype, EventTarget.prototype); + installHandler(Constructor.prototype, 'message'); + installHandler(Constructor.prototype, 'messageerror'); + installHandler(Constructor.prototype, 'error'); + Object.defineProperty(Constructor.prototype, Symbol.toStringTag, + { value: 'Worker', configurable: true }); + } + + Object.defineProperties(g, { + EventTarget: { value: EventTarget, writable: true, configurable: true }, + Event: { value: Event, writable: true, configurable: true }, + MessageEvent: { value: MessageEvent, writable: true, configurable: true }, + ErrorEvent: { value: ErrorEvent, writable: true, configurable: true }, + DOMException: { value: DOMException, writable: true, configurable: true }, + __jsrhInstallHandler: { value: installHandler }, + __jsrhInstallWorker: { value: installWorker }, + __jsrhSerialize: { value: serialize }, + __jsrhDeserialize: { value: deserialize }, + __jsrhDispatchMessage: { value: dispatchMessage }, + __jsrhDispatchError: { value: dispatchError }, + __jsrhWorkerCommonInstalled: { value: true } + }); +})(); +)JSRH"; + + inline constexpr char WorkerGlobal[] = R"JSRH( +(() => { + 'use strict'; + const g = globalThis; + + // JavaScriptCore's Node-API class adapter exposes native constructors as + // constructable exotic objects, so JavaScript's typeof reports "object". + // Browser feature detection commonly requires "function". Wrap those + // constructors in this realm while retaining native instances, prototypes, + // and inherited static methods. + function normalizeNativeConstructor(name) { + const nativeConstructor = g[name]; + if (!nativeConstructor || typeof nativeConstructor === 'function') return; + function BrowserConstructor(...args) { + if (!new.target) { + throw new TypeError(name + ' constructor must be called with new'); + } + const instance = new nativeConstructor(...args); + if (new.target !== BrowserConstructor) { + Object.setPrototypeOf(instance, new.target.prototype); + } + return instance; + } + BrowserConstructor.prototype = nativeConstructor.prototype; + Object.setPrototypeOf(BrowserConstructor, nativeConstructor); + try { + Object.defineProperty(BrowserConstructor, 'name', + { value: name, configurable: true }); + } catch (_) {} + g[name] = BrowserConstructor; + } + + for (const constructorName of [ + 'AbortController', 'AbortSignal', 'Blob', 'File', 'FileReader', + 'TextDecoder', 'TextEncoder', 'URL', 'URLSearchParams', 'WebSocket', + 'XMLHttpRequest' + ]) { + normalizeNativeConstructor(constructorName); + } + + class WorkerGlobalScope extends EventTarget {} + class DedicatedWorkerGlobalScope extends WorkerGlobalScope {} + __jsrhInstallHandler(WorkerGlobalScope.prototype, 'message'); + __jsrhInstallHandler(WorkerGlobalScope.prototype, 'messageerror'); + __jsrhInstallHandler(WorkerGlobalScope.prototype, 'error'); + Object.defineProperty(WorkerGlobalScope.prototype, Symbol.toStringTag, + { value: 'WorkerGlobalScope', configurable: true }); + Object.defineProperty(DedicatedWorkerGlobalScope.prototype, Symbol.toStringTag, + { value: 'DedicatedWorkerGlobalScope', configurable: true }); + + try { delete g.window; } catch (_) { g.window = undefined; } + try { + Object.setPrototypeOf(g, DedicatedWorkerGlobalScope.prototype); + } catch (_) { + // JavaScriptCore's global object has an immutable prototype. Preserve its + // engine-owned prototype and provide the observable worker-global shape + // through own accessors plus @@hasInstance instead. + Object.defineProperty(WorkerGlobalScope, Symbol.hasInstance, { + configurable: true, + value(instance) { + return instance === g || Function.prototype[Symbol.hasInstance].call(this, instance); + } + }); + } + __jsrhInstallHandler(g, 'message'); + __jsrhInstallHandler(g, 'messageerror'); + __jsrhInstallHandler(g, 'error'); + + function createWorkerLocation(href) { + const location = { + href, + origin: 'null', + protocol: '', + host: '', + hostname: '', + port: '', + pathname: '', + search: '', + hash: '', + toString() { return this.href; } + }; + try { + const parsed = new URL(href, 'app:///'); + for (const key of ['href', 'origin', 'protocol', 'host', 'hostname', + 'port', 'pathname', 'search', 'hash']) { + if (parsed[key] !== undefined) location[key] = String(parsed[key]); + } + } catch (_) { + location.pathname = href; + } + return Object.freeze(location); + } + + Object.defineProperties(g, { + WorkerGlobalScope: { value: WorkerGlobalScope, writable: true, configurable: true }, + DedicatedWorkerGlobalScope: { value: DedicatedWorkerGlobalScope, writable: true, configurable: true }, + self: { value: g, writable: false, enumerable: true, configurable: false }, + location: { value: createWorkerLocation(String(g.__jsrhWorkerLocation || '')), + writable: false, enumerable: true, configurable: false }, + name: { value: String(g.__jsrhWorkerName || ''), writable: false, configurable: true }, + navigator: { value: Object.freeze({ + hardwareConcurrency: 1, + language: 'en-US', + languages: Object.freeze(['en-US']), + onLine: true, + platform: '', + userAgent: 'JsRuntimeHost Worker' + }), + writable: false, configurable: true }, + addEventListener: { value: EventTarget.prototype.addEventListener.bind(g), + writable: true, configurable: true }, + removeEventListener: { value: EventTarget.prototype.removeEventListener.bind(g), + writable: true, configurable: true }, + dispatchEvent: { value: EventTarget.prototype.dispatchEvent.bind(g), + writable: true, configurable: true }, + postMessage: { value(message, transfer) { g.__jsrhNativePostMessage({ + __jsrhMessage: message, __jsrhTransfer: transfer + }); }, + writable: true, configurable: true }, + close: { value() { g.__jsrhNativeClose(); }, writable: true, configurable: true }, + importScripts: { value(...urls) { return g.__jsrhNativeImportScripts(...urls); }, + writable: true, configurable: true } + }); + + // Browser fetch resolves relative inputs against the worker's own location. + // The shared native fetch polyfill intentionally has no realm/base-URL + // knowledge, so add that worker-specific behavior here. + if (typeof g.fetch === 'function') { + const nativeFetch = g.fetch; + g.fetch = function(input, init) { + let candidate = input; + if (typeof input === 'string' || + (typeof URL === 'function' && input instanceof URL)) { + candidate = String(input); + } else if (input && typeof input === 'object' && + typeof input.url === 'string') { + candidate = input.url; + } + if (typeof candidate === 'string') { + try { candidate = new URL(candidate, g.location.href).href; } + catch (_) {} + } + return nativeFetch.call(g, candidate, init); + }; + } + +})(); +)JSRH"; +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 2cb5d26c..a7efe8f4 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(UnitTests) +add_subdirectory(NodeApi) npm(install --silent) diff --git a/Tests/NodeApi/.clang-format b/Tests/NodeApi/.clang-format new file mode 100644 index 00000000..b3fd9613 --- /dev/null +++ b/Tests/NodeApi/.clang-format @@ -0,0 +1,111 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 8 +UseTab: Never diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt new file mode 100644 index 00000000..4b36a9ac --- /dev/null +++ b/Tests/NodeApi/CMakeLists.txt @@ -0,0 +1,324 @@ +set(NODE_API_TEST_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) + +option(JSR_NODE_API_BUILD_NATIVE_TESTS "Build Node-API native addon test modules" ON) + +set(JSR_NODE_API_NATIVE_TEST_DIRS + 2_function_arguments + 3_callbacks + 4_object_factory + 5_function_factory + # Recently ported to the cross-platform node-api-cts. This is v5-clean and + # expands coverage to function calls, names, pending exceptions, native + # finalizers, and strong-reference teardown. + test_function + # v5-clean reference coverage. The rest of the reference/finalizer/wrap suite targets newer + # Node-API and is staged for the NAPI_VERSION bump: + # test_reference -> node_api_symbol_for (v9) + # test_finalizer/ -> napi_get_instance_data (v6), node_api_basic_env / node_api_post_finalizer (v9) + # 6_object_wrap -> napi_get_instance_data (v6), node_api_basic_env (v9) + test_reference_double_free +) + +# Vlad's hermes-windows#349 regression suite is specifically about Hermes's +# private metadata/weak-reference machinery. Keep its v5-clean cases on the +# Hermes adapter so future static_h bumps cannot reintroduce proxy traps, +# frozen-object rejection, prototype leakage, or cross-GC finalizer bugs. +if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes") + list(APPEND JSR_NODE_API_NATIVE_TEST_DIRS test_hermes_private_metadata) +endif() + +# v6/v7 conformance addons. The JSI backend (Core/Node-API-JSI) does not implement the v6/v7 C surface, +# so its addons are omitted (its build links only the v1-v5 set). V8 + JavaScriptCore execute these for +# real; Chakra links them (instance_data works; BigInt throws ENOTSUP via feature-detection). +# The current node-api-cts typed-array addon only needs the v7 API surface; +# node_api_basic_env is an ABI-compatible finalizer typedef here. Enable it +# now so detach/is-detached behavior is exercised instead of waiting for v9. +if(NOT NAPI_JAVASCRIPT_ENGINE STREQUAL "JSI") + list(APPEND JSR_NODE_API_NATIVE_TEST_DIRS test_instance_data) + # BigInt: the real conformance test where the engine has BigInt; an ENOTSUP feature-detection + # fallback where it doesn't -- the Win10 OS Chakra, and jsc-android (~2020), whose parser rejects + # `0n` literals so the standard test_bigint can't even be parsed there. + if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra" OR + (NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore" AND ANDROID)) + list(APPEND JSR_NODE_API_NATIVE_TEST_DIRS test_bigint_unsupported) + else() + list(APPEND JSR_NODE_API_NATIVE_TEST_DIRS test_bigint) + endif() + + # Frozen Chakra and the old Android JSC do not expose a real detach + # primitive. All modern engines covered by the v7 branch run the upstream + # CTS-derived typed-array/detach cases. + if(NOT NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra" AND + NOT (NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore" AND ANDROID)) + list(APPEND JSR_NODE_API_NATIVE_TEST_DIRS test_typedarray) + endif() +endif() + +function(node_api_copy_test_sources TARGET_NAME) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${NODE_API_TEST_ROOT}/test + $/test + COMMENT "Copying Node-API test assets for ${TARGET_NAME}" + ) +endfunction() + +if(ANDROID) + set(NODE_LITE_PLATFORM_SRC node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_posix.cpp) +elseif(APPLE) + set(NODE_LITE_PLATFORM_SRC node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_posix.cpp) +elseif(WIN32) + set(NODE_LITE_PLATFORM_SRC node_lite_windows.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process.cpp) +else() + set(NODE_LITE_PLATFORM_SRC node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_posix.cpp) + message(WARNING "Node-API node_lite platform not yet customized for ${CMAKE_SYSTEM_NAME}; using POSIX defaults.") +endif() + +add_executable(node_lite + ${NODE_LITE_CHILD_PROCESS_SRC} + child_process.h + compat.h + js_runtime_api.cpp + js_runtime_api.h + node_lite.cpp + node_lite.h + node_lite_jsruntimehost.cpp + ${NODE_LITE_PLATFORM_SRC} + string_utils.cpp + string_utils.h +) + +target_include_directories(node_lite + PRIVATE + ${NODE_API_TEST_ROOT} + ${NODE_API_TEST_ROOT}/include + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Shared + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Source +) + +target_compile_definitions(node_lite + PRIVATE + NODE_API_EXPERIMENTAL_NO_WARNING + NODE_API_BUILD_TYPE="$,Debug,Release>" +) + +target_link_libraries(node_lite + PRIVATE + napi +) + +if(UNIX AND NOT APPLE) + # Native test addons resolve napi_* against the embedding executable, like + # node itself. Export node_lite's symbols so dlopen can bind them on ELF. + set_target_properties(node_lite PROPERTIES ENABLE_EXPORTS ON) +endif() + +node_api_copy_test_sources(node_lite) + +add_executable(NodeApiTests + ${NODE_LITE_CHILD_PROCESS_SRC} + child_process.h + string_utils.cpp + string_utils.h + test_basics.cpp + test_main.cpp + main.cpp + test_main.h +) + +target_include_directories(NodeApiTests + PRIVATE + ${NODE_API_TEST_ROOT} + ${NODE_API_TEST_ROOT}/include +) + +target_link_libraries(NodeApiTests + PRIVATE + gtest_main +) + +node_api_copy_test_sources(NodeApiTests) + +add_dependencies(NodeApiTests node_lite) + +add_custom_target(NodeApiModules) + +# Always define which tests are available, regardless of whether we build the native modules +list(JOIN JSR_NODE_API_NATIVE_TEST_DIRS "," NODE_API_NATIVE_TESTS_STRING) +target_compile_definitions(node_lite + PRIVATE + NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" +) +target_compile_definitions(NodeApiTests + PRIVATE + NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" +) + +# Only define NODE_API_TESTS_HAVE_NATIVE_MODULES when actually building native modules +if(JSR_NODE_API_BUILD_NATIVE_TESTS) + target_compile_definitions(node_lite + PRIVATE + NODE_API_TESTS_HAVE_NATIVE_MODULES=1 + ) + target_compile_definitions(NodeApiTests + PRIVATE + NODE_API_TESTS_HAVE_NATIVE_MODULES=1 + ) +endif() + +function(add_node_api_module MODULE_TARGET) + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "" "SOURCES;DEFINES") + + get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + + if(NOT "${MODULE_TARGET}" STREQUAL "${FOLDER_NAME}") + set(MODULE_TARGET "${FOLDER_NAME}_${MODULE_TARGET}") + endif() + + # On Android the addon is a SHARED lib.so so AGP packages it into the APK's lib// (the + # app's nativeLibraryDir, the only place a native library may be dlopen'd from on API 29+). + # Elsewhere it is a MODULE (dlopen-only) .node loaded by the node_lite child-process runner. + if(ANDROID) + add_library(${MODULE_TARGET} SHARED) + else() + add_library(${MODULE_TARGET} MODULE) + endif() + target_sources(${MODULE_TARGET} PRIVATE ${ARG_SOURCES}) + target_include_directories(${MODULE_TARGET} + PRIVATE + ${NODE_API_TEST_ROOT}/include + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Shared + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Shared/napi + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE}/napi + ) + + target_compile_definitions(${MODULE_TARGET} + PRIVATE + NODE_API_EXPERIMENTAL_NO_WARNING + NODE_GYP_MODULE_NAME=\"${FOLDER_NAME}\" + ${ARG_DEFINES} + ) + + if(APPLE) + # The addon is dlopen'd into node_lite, which already provides the napi_* symbols; let them + # remain unresolved at link time. + target_link_options(${MODULE_TARGET} + PRIVATE + "-undefined" "dynamic_lookup" + ) + elseif(ANDROID) + # Link the shared napi (libnapi.so) so the addon's napi_* imports resolve at dlopen via a real + # DT_NEEDED -- the same libnapi.so the host depends on, so there is a single napi instance. + target_link_libraries(${MODULE_TARGET} PRIVATE napi) + elseif(WIN32) + # MSVC binds a DLL's imports at link time (there's no node.exe-style host export / delay-load + # wired up here), so the .node addon must resolve its napi_* against the napi library directly. + # napi is static on Windows, so the references bind into the addon; node_lite isn't executed by + # the Windows/UWP CI (build-only), so a single shared napi instance isn't required there. + target_link_libraries(${MODULE_TARGET} PRIVATE napi) + endif() + + if(ANDROID) + set_target_properties(${MODULE_TARGET} + PROPERTIES + PREFIX "lib" + SUFFIX ".so" + ) + else() + set(MODULE_OUTPUT_DIR + ${CMAKE_CURRENT_BINARY_DIR}/build/$,Debug,Release>) + set_target_properties(${MODULE_TARGET} + PROPERTIES + PREFIX "" + SUFFIX ".node" + ARCHIVE_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} + LIBRARY_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} + RUNTIME_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} + ) + endif() + + add_dependencies(NodeApiModules ${MODULE_TARGET}) + + if(NOT ANDROID) + # Stage the built .node next to the node_lite / NodeApiTests runners so they can dlopen it + # relative to the copied test files. + add_custom_command(TARGET ${MODULE_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + $/test/js-native-api/${FOLDER_NAME}/build + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_BINARY_DIR}/build + $/test/js-native-api/${FOLDER_NAME}/build + COMMAND ${CMAKE_COMMAND} -E make_directory + $/test/js-native-api/${FOLDER_NAME}/build + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_BINARY_DIR}/build + $/test/js-native-api/${FOLDER_NAME}/build + COMMENT "Copying Node-API module ${MODULE_TARGET} outputs" + ) + endif() + + if(APPLE) + # The copy_directory above runs as an Xcode "Run Script" phase, which executes BEFORE + # Xcode's implicit CodeSign phase signs the original module. The copied .node files (the + # ones node_lite/NodeApiTests actually dlopen) are therefore unsigned on a clean build, and + # macOS refuses to load them ("Trying to load an unsigned library"). Ad-hoc sign the copies + # directly so clean builds work without a second pass. + set(_node_api_cfg_dir $,Debug,Release>) + add_custom_command(TARGET ${MODULE_TARGET} POST_BUILD + COMMAND codesign --force --sign - + "$/test/js-native-api/${FOLDER_NAME}/build/${_node_api_cfg_dir}/$" + COMMAND codesign --force --sign - + "$/test/js-native-api/${FOLDER_NAME}/build/${_node_api_cfg_dir}/$" + COMMENT "Ad-hoc signing copied ${MODULE_TARGET}.node for dlopen on macOS" + VERBATIM + ) + endif() +endfunction() + +add_dependencies(NodeApiTests NodeApiModules) + +add_subdirectory(test) + +# Babel rewrites current CTS sources to import @babel/runtime helpers. With a +# single-config generator the runner and generated test tree share this binary +# directory, but Xcode and Visual Studio place each runner one configuration +# level deeper. Stage the runtime dependency beside those runners so module +# resolution is identical on every generator. +if(CMAKE_CONFIGURATION_TYPES) + foreach(nodeApiRunner node_lite NodeApiTests) + add_custom_command(TARGET ${nodeApiRunner} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/test/node_modules/@babel" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${NODE_API_TRANSFORMED_JS_BINARY_DIR}/node_modules/@babel/runtime" + "$/test/node_modules/@babel/runtime" + COMMENT "Copying Babel runtime for ${nodeApiRunner}" + VERBATIM) + endforeach() +endif() + +# node_api_copy_test_sources() first stages the complete source tree (native +# module layout and non-JS fixtures included). Overlay Babel's generated JS and +# source maps afterward. add_custom_command(TARGET) must be called from the +# directory that created the target, so this cannot live in test/CMakeLists.txt. +foreach(testJSFile IN LISTS NODE_API_TRANSFORMED_JS_RELATIVE_FILES) + get_filename_component(testJSDir ${testJSFile} DIRECTORY) + foreach(nodeApiRunner node_lite NodeApiTests) + add_custom_command(TARGET ${nodeApiRunner} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/test/${testJSDir}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${NODE_API_TRANSFORMED_JS_BINARY_DIR}/${testJSFile}" + "$/test/${testJSFile}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${NODE_API_TRANSFORMED_JS_BINARY_DIR}/${testJSFile}.map" + "$/test/${testJSFile}.map" + VERBATIM) + endforeach() +endforeach() diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md new file mode 100644 index 00000000..e102bb38 --- /dev/null +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -0,0 +1,193 @@ +# Node-API (N-API) Conformance & Version Roadmap + +_Last updated: 2026-07-22 · Tracks PR #116 (`napi-tests`), PR #189 (`napi-v7`), and the current node-api-cts._ + +## Scope & PR discipline + +This is a multi-PR effort. Keep the boundaries strict: + +- **PR #116 / `napi-tests`:** land the conformance test suite on the N-API v5 surface that upstream already supports. +- **PR #189 / this stacked branch:** raise the engine-facing surface to v7 and enable the matching portable cases per engine capability. +- **Out of scope here — each becomes its own follow-up PR:** + - **Bug fixes** against the current N-API implementation that the tests surface → *quarantine* the failing test via the allow-list and open a separate fix PR. Do not fix impl bugs in the test-suite PR. + - **Any later `NAPI_VERSION` bump** (8 → 9 → 10) and the per-engine native work it requires. + - **jsc-android engine bump** (v6 enabler — see below). + +> **Android packaging note:** to run the suite in-process the addons are `dlopen`'d as standalone +> `.node` modules, which requires `napi` to ship as a shared library (`libnapi.so`) on Android (see +> *Platform status*). That is a deliberate packaging change for all Android consumers; if upstream +> prefers it isolated, it can land as a small precursor commit/PR that this suite depends on. + +## Current state (2026-07-22) + +- PR #189 raises the engine-facing headers to **N-API v7**. `NAPI_HAS_THREADS` remains **0**; + Worker message delivery is a runtime queue and does not depend on Node-API thread-safe functions. +- Recommended early refactor (a later PR): replace the three hardcoded defines with a single + build-system knob (`target_compile_definitions(... NAPI_VERSION=${JSR_NAPI_VERSION})`), per-engine overridable. + +### Per-engine implementation completeness + +| Engine | Source (fns) | v5 | v6 bigint / instance-data | v7 detach AB | v8 type-tag / freeze-seal | Notes | +|---|---|:--:|:--:|:--:|:--:|---| +| **V8** | `js_native_api_v8.cc` | ✅ | ✅ | ✅ | ✅ | Current CTS caught and fixed a stale stub which reported every value as detached. | +| **JavaScriptCore** | `js_native_api_javascriptcore.cc` | ✅ | ✅* | ✅* | ❌ | System JSC is green-capable; old Android JSC feature-detects BigInt/detach and reports ENOTSUP. | +| **QuickJS** | `js_native_api_quickjs.cc` | ✅ | ✅ | ✅ | partial | Added by upstream after #189 branched; the merge update completes word BigInts, lossless conversion, instance data/finalizers, exact detach semantics, and escaped-handle lifetime. | +| **Hermes** | upstream `hermesNapi` | ✅ | ✅ | ✅ | ✅ | Hermes supplies its own N-API v10 implementation; JsRuntimeHost exposes only the selected public header level. | +| **Chakra** | `js_native_api_chakra.cc` | ✅ | ❌ (hard wall: BigInt) | ❌ | partial | Frozen OS engine on post-EOL Win10; it remains capability-gated. | +| **JSI** | `Core/Node-API-JSI` | ✅* | ❌ | ❌ | ❌ | Separate v1-v5 shim; excluded from v6/v7 addon coverage. | + +`*` capability depends on the concrete engine build (notably legacy Android JSC). + +### Platform status (this PR) + +| Platform | Engine | conformance suite | Runner | Notes | +|---|---|---|---|---| +| **macOS** | JavaScriptCore | v5 + supported v6/v7, CI-gated (plain + ASan/UBSan + TSan) | child-process | The reusable workflow now builds and runs `NodeApiTests`, not just `UnitTests`. | +| **Linux** | JavaScriptCore / QuickJS | CI-gated (GCC + Clang + ASan/UBSan + TSan) | child-process | Uses the OS JavaScriptCore package; the full `NodeApiTests` executable runs after `UnitTests`. | +| **Android** | V8 / JSC / QuickJS / Hermes | CI-gated (dynamic `.node` + `libnapi.so`, in-process) | in-process | App sandbox can't `fork`/`exec`; addons `dlopen`'d — see below. | + +**Android in-process addon loading (dynamic `.node` + shared `libnapi.so`).** The app sandbox can't `fork`/`exec`, so the conformance suite runs the addons *in-process*. The addons are built as standalone SHARED `lib.so` modules (matching nodejs/node-api-cts's `add_node_api_cts_addon`), packaged by AGP into `nativeLibraryDir`, and `dlopen`'d by `node_lite_android` by soname. For their `napi_*` imports to bind at load, **`napi` is built as a shared library (`libnapi.so`) on Android** — a real `DT_NEEDED` of both the host and every addon, so there is a single napi instance. This is the IoC / dynamic-self-registration model: `dlopen` the addon → its `DT_NEEDED libnapi.so` resolves the `napi_*` → `dlsym("napi_register_module_v1")` → call it. Verified on device: `lib2_function_arguments.so` carries `DT_NEEDED [libnapi.so]`, its 8 `napi_*` are imports (`U`), `libnapi.so` exports all 106 `napi_*` (`T`), and the 4 v5 tests pass. The harness also needs the `noexcept`-removal fix (`38864e4`) so a failing test surfaces as a `ProcessResult` rather than `std::terminate`, and stdout is routed to logcat via AndroidExtensions' `StdoutLogger` (tag `StdoutLogger`) for visible gtest output. + +> Why shared `napi` rather than static-linking the addons into the host: bionic will not surface a `System.loadLibrary`-loaded (RTLD_LOCAL) host's `napi_*` to a `dlopen`'d module, and post-hoc `RTLD_GLOBAL` host promotion is a no-op on bionic — so a `dlopen`'d addon can only resolve `napi` via a real shared-library `DT_NEEDED`. (An earlier iteration statically linked the addons into the host and passed too; the shared-lib model was adopted to align with node-api-cts's dynamic `.node` addons, easing a future migration.) + +> Emulator note: the unrelated `JavaScript.All` UnitTest (XMLHttpRequest/WebSocket/HTTP mocha tests) can fail in an offline emulator (status 0 vs 200/404, socket timeouts); it runs before and is independent of the js-native-api suite. + +## Chakra N-API ceiling + +The Windows-OS Chakra (`chakra.dll`, JSRT/`jsrt.h`) is frozen ~ES2017 and will never gain new VM +primitives; Windows 10 reached EOL 2025-10-14. OSS ChakraCore has more, but is itself archived (≈2021) +and shipping it would mean bundling an unmaintained, security-frozen engine. So Chakra's capability is set +by the frozen JSRT surface you link. + +- **Hard walls (a VM primitive is missing — cannot be coded around):** **BigInt** (v6 + `napi_create_bigint_words` / `napi_get_value_bigint_*`) → no faithful v6 on Chakra, ever; **ArrayBuffer + detach** (v7) unless `JsDetachArrayBuffer` exists (verify against the actual `ChakraCore.h`). +- **Soft (more native work on existing primitives):** type tags (private symbol / external data), + `object_freeze`/`seal` (call ES `Object.freeze`), instance data, `get_all_property_names`, + references/finalizers (already work via `JsSetObjectBeforeCollectCallback`). + +**Decision: do not let Chakra set a global ceiling.** N-API is per-engine by design — `napi_get_version` +reports the level *this* engine supports and addons feature-detect. Chakra reports the honest version it can +reach, returns `napi_generic_failure` for the walled functions, and the conformance allow-list is gated per +engine. Pragmatic Chakra target: keep v5 green, optionally cherry-pick the cheap v8 wins (freeze/seal, +type-tags), hard-stop at BigInt. Do not pour native effort into a frozen engine on a sunset OS. + +## jsc-android bump (v6 enabler — separate PR) + +Currently pinned at JSC `250231.0.0`; `294992.0.0` is available. Bumping brings a modern JSC with real +BigInt + ES2020 primitives (what v6/v7 need) and makes **Bun's mature N-API-on-JSC layer** +(`src/bun.js/bindings/napi.cpp` et al.) directly referenceable. Caveats: Bun rides its own WebKit fork +(API mostly matches, build differs), jsc-android-buildscripts is semi-stale, binary size grows. Sequence it +*after* this PR, as the first step of the JSC v6→v8 work. + +## Staged version roadmap (post-PR1) + +Each tier: bump the knob → recompile (V8 exposes the surface for free) → enable the matching test dirs → +run the suite per engine → implement the JSC/(Chakra) gaps → green. + +| Step | Target | Unlocks (test dirs) | V8 | JSC / Chakra work | +|---|---|---|:--:|---| +| B1 | **v6** | `test_bigint`, `test_instance_data`, `get_all_property_names` | ✅ | Implemented on system JSC and QuickJS; capability-gated on legacy JSC/Chakra. | +| B2 | **v7** | `test_typedarray` detached-ArrayBuffer cases | ✅ | Enabled on V8, system JSC, QuickJS, and Hermes; capability-gated on legacy JSC/Chakra. | +| B3 | **v8** | type-tag + freeze/seal in `test_object`/`test_general` | free | type tags + freeze/seal → **parity with hermes-windows** | +| B4* | **v9** | `symbol_for`, syntax-error, `module_file_name` | free | implement on JSC | +| B5* | **v10** | external strings, property keys (matches `facebook/hermes API/napi`) | mostly | implement on JSC | + +`*` stretch. Node-API thread-safe functions remain a separate runtime-layer axis. The browser Worker +polyfill uses `AppRuntime` dispatch queues directly, so it neither exposes nor depends on TSFN support. + +**Reference/finalizer test staging (initially measured at v5).** Of the vendored reference/finalizer/wrap dirs, only +`test_reference_double_free` is v5-clean and is enabled now (green on macOS/JSC incl. ASan, and Android/V8); its +`test_wrap.js` is quarantined (JSC `napi_remove_wrap` on an unwrapped object returns `napi_invalid_arg` — it +does not crash; separate fix). The rest are gated by symbols our v5 pin doesn't export and enable with the +bump: `test_reference` → `node_api_symbol_for` (v9, B4); `test_finalizer/` & `6_object_wrap` → +`napi_get_instance_data` (v6, B1) + `node_api_post_finalizer` (v9, B4). `node_api_basic_env` is an +ABI-compatible finalizer-environment typedef and does not itself require postponing v7 `test_typedarray`. `test_finalizer` +also surfaced a JSC finalizer-delivery timing case (`mustCall(1)`→0) to confirm at B1. + +**July 2026 CTS expansion.** The official cross-platform suite's `test_function` is v5-clean, was already +vendored here, and is now enabled. It covers function creation/calls and names, invalid arguments, pending +exceptions, native finalization, and strong-reference teardown. This v7 branch additionally enables the +instance-data, BigInt, and typed-array/detach cases where the selected engine can implement them faithfully. +`Tests/NodeApi/UPSTREAM_REVISIONS` records the exact audited hermes-windows and +node-api-cts commits so later resyncs are deliberate and reviewable. + +**GC-safety (re upstream [hermes-windows#321](https://github.com/microsoft/hermes-windows/pull/321)).** That +weak-ref-over-Proxy moving-GC bug is **not present here**. V8 uses `v8::Persistent` (an immediate, auto-relocated +GC root). JSC keeps the to-be-referenced object in the `value` argument on the C stack across the reference's +finalizer-setup allocation, so JSC's **conservative stack scan pins it against collection and relocation** — the +creation-time window is closed regardless of whether the collector moves cells — and weak liveness is an +object-id check, not a bare-pointer deref. Chakra is non-moving with a nullptr-returning weak read. Empirically, +`test_reference_double_free` runs **clean under ASan on the modern system JSC** (no use-after-free). + +The newer [hermes-windows#349](https://github.com/microsoft/hermes-windows/pull/349) moved weak-reference, +wrap/finalizer, and type-tag state into private own-object metadata. Its v5-clean failure modes now have a +Hermes-only regression module here: throwing Proxies must observe no metadata traps, frozen objects must accept +weak refs and wraps, prototype children must not inherit wraps, and a Proxy finalizer must survive collection. +The weak-reference creation case also directly guards #321. It is intentionally engine-gated: other adapters do +not use Hermes's private-metadata mechanism, and broadening it would turn a Hermes regression guard into an +unrelated implementation change in this test-integration PR. + +## Test-suite sourcing strategy + +- **Now (this PR):** vendored copy of vmoroz's hermes-windows `unittests/NodeApi/` (engine-layer, v8-capable + harness, `node_lite`). Audited at `3c6569e` (2026-07-17), including Vlad's private-metadata fix in #349. + The Babel tool graph follows that upstream's deterministic npm/lockfile stamp, but uses `npm ci`; both runner + targets explicitly depend on the transform target and receive the transformed assets after their source copy. +- **Evaluated (task 6) — `nodejs/node-api-cts` as a `FetchContent` `GIT_REPOSITORY` dep: not yet; track for later.** + Findings (HEAD `67b5e42`, 2026-07-13): + - **Maturity blocker:** the README states it "is currently a work-in-progress and shouldn't yet be relied on by + anyone" (v0.1.0, not on npm). Too early to take as an upstream dependency. + - **Harness-model mismatch:** the runner is Node.js + TypeScript (`node --test implementors/node/run-tests.ts`, + `amaro` for TS-strip); the only implementor is `node`. Adopting it means authoring an `implementors/jsruntimehost/` + harness (JS modules: `load-addon`, `assert`, `must-call`, `gc`, `napi-version`, `features`, `skip-test`) and + driving the test `.js` from our runtime — i.e. re-expressing what `node_lite` already does, in their contract. + - **Android now aligns:** `add_node_api_cts_addon()` builds SHARED `.node` (dlopen) — the same model our Android + suite now uses (dynamic `.node` + `libnapi.so`). Its CMake only handles Apple `-undefined dynamic_lookup` / + MSVC import-libs, so an Android port would still need our `libnapi.so` + soname-load glue, but the addon model + itself matches — a future migration is much smoother now. + - **One immediate v5 gain:** enable its now-ported `test_function` semantics from our existing vendored copy. + At v7, its current `test_typedarray` native test is content-equivalent to the vendored copy and exercises + the detach contract. It exposed the V8 always-true stub and QuickJS's zero-length/non-ArrayBuffer false + positives plus a double finalizer on detached external buffers. Its BigInt cases also require full word + conversion and correct `lossless` reporting, which are now implemented for QuickJS. + - **Harness evolution:** `spawnTest` landed in #54. That is useful for a future native CTS implementor, but it + does not remove the current runner/Android integration work needed to consume the repository directly. + - **Upside (why track it):** engine-agnostic, active (including `test_bigint`, `test_typedarray`, SharedArrayBuffer, + references, strings and dates), CMake-based, and + contributing a JsRuntimeHost implementor could upstream our Android in-process + static-link learnings. + - **Recommendation:** keep the pinned vendored subset while CTS is explicitly WIP, regularly diff every enabled + case against CTS, and add a first-party `implementors/jsruntimehost` adapter once its harness stabilizes. + +--- + +## Appendix: JS engine compatibility baseline + +_Folded from the original `engine-compat-baseline.md` (captured 2025-10-02 on macOS V8)._ + +**Environment:** Node.js v24.2.0 · V8 13.6.233.10-node.17 · macOS (darwin arm64). All 15 smoke checks passed. + +| Group | Checks | +|---|---| +| Engine detection | V8 detection; WebAssembly available | +| N-API compatibility | 1 MB strings < 100 ms; TypedArray aliasing/endianness; local + global Symbols | +| Unicode / encoding | UTF-16 surrogate pairs (emoji); NFC/NFD normalization; TextEncoder/TextDecoder UTF-8 | +| Memory | 10 MB array allocation; WeakMap/WeakSet | +| ES6+ | Proxy/Reflect; BigInt; async generators | +| Performance | 1000 timers < 100 ms; deep recursion to 8,907 frames | + +**Engine-upgrade-sensitive features (may fail on older Android V8/JSC):** TextEncoder/TextDecoder, BigInt +(needs V8 6.7+ / JSC with BigInt), async generators, deep recursion (lower Android stack limits), global +`v8` object detection. Tests should **feature-detect and `skip()`** rather than assume availability. + +**Android engine notes:** prebuilt Android V8 may lag Node's V8 (test against Android-XR system V8); +JavaScriptCore Android `250231.0.0` is older than current Safari JSC and may lack some ES2020+ — consider the +`294992.0.0` bump (see jsc-android section). + +## References + +- PR #116: https://github.com/BabylonJS/JsRuntimeHost/pull/116 +- hermes-windows N-API suite: https://github.com/microsoft/hermes-windows/tree/main/unittests/NodeApi +- node-api-cts: https://github.com/nodejs/node-api-cts (umbrella issue #15; publishing issue #35) +- facebook/hermes native Node-API (v10): https://github.com/facebook/hermes/tree/main/API/napi (`COMPATIBILITY.md`) +- Node-API version gating reference: https://github.com/nodejs/node-api-headers diff --git a/Tests/NodeApi/UPSTREAM_REVISIONS b/Tests/NodeApi/UPSTREAM_REVISIONS new file mode 100644 index 00000000..74d33b38 --- /dev/null +++ b/Tests/NodeApi/UPSTREAM_REVISIONS @@ -0,0 +1,4 @@ +# Audited upstream revisions for the vendored Node-API conformance tests. +# Bump deliberately after reviewing the diff and updating the roadmap. +hermes-windows 3c6569eccb99d1940fb29ebe5a24413fd954dd5c +node-api-cts 67b5e428a17b2cf31c8aacf9181840a111ba639a diff --git a/Tests/NodeApi/child_process.cpp b/Tests/NodeApi/child_process.cpp new file mode 100644 index 00000000..2fd22cd8 --- /dev/null +++ b/Tests/NodeApi/child_process.cpp @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Windows-specific implementation of the `spawnSync` function for creating +// child processes and capturing their output. This code is designed to work +// with the Windows API and is not portable to other platforms. It uses pipes +// to redirect the standard output and error streams of the child process back +// to the parent process, allowing the parent to read the output and error +// messages generated by the child process. +// +// The `spawnSync` function takes a command and a list of arguments, creates a +// child process to execute the command, and returns a `ProcessResult` structure +// containing the exit status and the captured output and error messages. +// + +#include "child_process.h" + +#include +#include +#include +#include +#include "string_utils.h" + +#ifndef VerifyElseExit +#define VerifyElseExit(condition) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition); \ + } \ + } while (false) +#endif + +namespace node_api_tests { + +namespace { + +std::string ReadFromPipe(HANDLE pipeHandle); +void ExitOnError(const char* message); + +struct AutoHandle { + HANDLE handle{NULL}; + + AutoHandle() = default; + AutoHandle(HANDLE handle) : handle(handle) {} + ~AutoHandle() { ::CloseHandle(handle); } + + AutoHandle(const AutoHandle&) = delete; + AutoHandle& operator=(const AutoHandle&) = delete; + + void Close() { + ::CloseHandle(handle); + handle = NULL; + } +}; +} // namespace + +// Create a child process that uses the previously created pipes for STDIN and +// STDOUT. +ProcessResult SpawnSync(std::string_view command, + std::vector args) { + ProcessResult result{}; + + // Set the bInheritHandle flag so pipe handles are inherited. + + SECURITY_ATTRIBUTES handles_are_inheritable = { + sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + + AutoHandle out_read_handle, out_write_handle; + VerifyElseExit(CreatePipe(&out_read_handle.handle, + &out_write_handle.handle, + &handles_are_inheritable, + 0)); + // Ensure the read handle to the pipe for STDOUT is not inherited. + VerifyElseExit( + SetHandleInformation(out_read_handle.handle, HANDLE_FLAG_INHERIT, 0)); + + AutoHandle err_read_handle, err_write_handle; + VerifyElseExit(CreatePipe(&err_read_handle.handle, + &err_write_handle.handle, + &handles_are_inheritable, + 0)); + // Ensure the read handle to the pipe for STDERR is not inherited. + VerifyElseExit( + SetHandleInformation(err_read_handle.handle, HANDLE_FLAG_INHERIT, 0)); + + // Set up members of the STARTUPINFO structure. + // This structure specifies the STDIN and STDOUT handles for redirection. + STARTUPINFOA startup_info{}; + startup_info.cb = sizeof(STARTUPINFOA); + startup_info.dwFlags |= STARTF_USESTDHANDLES; + startup_info.hStdInput = ::GetStdHandle(STD_INPUT_HANDLE); + startup_info.hStdOutput = out_write_handle.handle; + startup_info.hStdError = err_write_handle.handle; + + // Create the child process. + + std::string commandLine = std::string(command); + for (std::string& arg : args) { + commandLine += " " + arg; + } + PROCESS_INFORMATION process_info{}; + VerifyElseExit( + CreateProcessA(nullptr, + const_cast(commandLine.c_str()), // command line + nullptr, // process security attributes + nullptr, // primary thread security attributes + TRUE, // handles are inherited + CREATE_DEFAULT_ERROR_MODE, // creation flags + nullptr, // use parent's environment + nullptr, // use parent's current directory + &startup_info, // STARTUPINFO pointer + &process_info)); // receives PROCESS_INFORMATION + + VerifyElseExit(WAIT_OBJECT_0 == + ::WaitForSingleObject(process_info.hProcess, INFINITE)); + + DWORD exit_code; + VerifyElseExit(::GetExitCodeProcess(process_info.hProcess, &exit_code)); + + // Close handles to the child process and its primary thread. + // Some applications might keep these handles to monitor the status + // of the child process, for example. + ::CloseHandle(process_info.hProcess); + ::CloseHandle(process_info.hThread); + + // Close handles to the stdin and stdout pipes no longer needed by the child + // process. If they are not explicitly closed, there is no way to recognize + // that the child process has ended. + + out_write_handle.Close(); + err_write_handle.Close(); + + result.status = exit_code; + result.std_output = + ReplaceAll(ReadFromPipe(out_read_handle.handle), "\r\n", "\n"); + result.std_error = + ReplaceAll(ReadFromPipe(err_read_handle.handle), "\r\n", "\n"); + + return result; +} + +namespace { +std::string ReadFromPipe(HANDLE pipeHandle) { + std::string result; + constexpr size_t bufferSize = 4096; + char buffer[bufferSize]; + + for (;;) { + DWORD bytesRead; + BOOL isSuccess = + ::ReadFile(pipeHandle, buffer, bufferSize, &bytesRead, nullptr); + if (!isSuccess || bytesRead == 0) break; + + result.append(buffer, bytesRead); + } + + return result; +} + +// Format a readable error message, display a message box, +// and exit from the application. +void ExitOnError(const char* message) { + LPVOID lpMsgBuf; + LPVOID lpDisplayBuf; + DWORD dw = GetLastError(); + + ::FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + dw, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&lpMsgBuf, + 0, + nullptr); + + lpDisplayBuf = (LPVOID)LocalAlloc( + LMEM_ZEROINIT, (lstrlenA((LPCSTR)lpMsgBuf) + lstrlenA(message) + 40)); + ::StringCchPrintfA((LPSTR)lpDisplayBuf, + LocalSize(lpDisplayBuf), + "%s failed with error %d: %s", + message, + dw, + lpMsgBuf); + fprintf(stderr, "%s\n", (const char*)lpDisplayBuf); + + ::LocalFree(lpMsgBuf); + ::LocalFree(lpDisplayBuf); + ::ExitProcess(1); +} + +} // namespace +} // namespace node_api_tests \ No newline at end of file diff --git a/Tests/NodeApi/child_process.h b/Tests/NodeApi/child_process.h new file mode 100644 index 00000000..75ab8115 --- /dev/null +++ b/Tests/NodeApi/child_process.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef NODE_API_TEST_CHILD_PROCESS_H +#define NODE_API_TEST_CHILD_PROCESS_H + +#include +#include +#include +#include + +namespace node_api_tests { + +// Struct to hold the result of a child process execution. +struct ProcessResult { + uint32_t status; // Exit status of the child process. + std::string std_output; // Standard output from the child process. + std::string std_error; // Standard error from the child process. +}; + +// Creates a child process to run the given command with the specified +// arguments. +ProcessResult SpawnSync(std::string_view command, + std::vector args); + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_CHILD_PROCESS_H \ No newline at end of file diff --git a/Tests/NodeApi/child_process_android.cpp b/Tests/NodeApi/child_process_android.cpp new file mode 100644 index 00000000..cca60c3c --- /dev/null +++ b/Tests/NodeApi/child_process_android.cpp @@ -0,0 +1,15 @@ +#include "child_process.h" + +namespace node_api_tests { + +ProcessResult SpawnSync(std::string_view /*command*/, std::vector /*args*/) +{ + ProcessResult result{}; + // Non-zero failure (status is uint32_t); spawnSync is unsupported in the in-process Android runner. + result.status = 1; + result.std_error = "child_process.spawnSync is not supported on this platform."; + result.std_output.clear(); + return result; +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/child_process_posix.cpp b/Tests/NodeApi/child_process_posix.cpp new file mode 100644 index 00000000..ebc90913 --- /dev/null +++ b/Tests/NodeApi/child_process_posix.cpp @@ -0,0 +1,201 @@ +#include "child_process.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +#if !defined(__ANDROID__) +#include +#elif (__ANDROID_API__ >= 29) +#include +#endif + +#ifndef VerifyElseExit +#define VerifyElseExit(condition) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition, nullptr); \ + } \ + } while (false) +#endif + +#ifndef VerifyElseExitWithCleanup +#define VerifyElseExitWithCleanup(condition, actions_ptr) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition, actions_ptr); \ + } \ + } while (false) +#endif + +#if defined(__ANDROID__) && (__ANDROID_API__ < 29) + +namespace node_api_tests { + +ProcessResult SpawnSync(std::string_view /*command*/, + std::vector /*args*/) { + ProcessResult result{}; + result.status = -1; + result.std_error = "child_process.spawnSync is not supported on this platform."; + result.std_output.clear(); + return result; +} + +} // namespace node_api_tests + +#else + +extern char** environ; + +namespace node_api_tests { + +namespace { + +std::string ReadFromFd(int fd); +void ExitOnError(const char* message, posix_spawn_file_actions_t* actions); + +} // namespace + +ProcessResult SpawnSync(std::string_view command, + std::vector args) { + ProcessResult result{}; + + // These int arrays each comprise two file descriptors: { readEnd, writeEnd }. + int stdout_pipe[2], stderr_pipe[2]; + VerifyElseExit(pipe(stdout_pipe) == 0); + VerifyElseExit(pipe(stderr_pipe) == 0); + + posix_spawn_file_actions_t actions; + VerifyElseExit(posix_spawn_file_actions_init(&actions) == 0); + + VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( + &actions, stdout_pipe[1], STDOUT_FILENO) == 0, + &actions); + VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( + &actions, stderr_pipe[1], STDERR_FILENO) == 0, + &actions); + + VerifyElseExitWithCleanup( + posix_spawn_file_actions_addclose(&actions, stdout_pipe[0]) == 0, + &actions); + VerifyElseExitWithCleanup( + posix_spawn_file_actions_addclose(&actions, stderr_pipe[0]) == 0, + &actions); + + std::vector argv; + argv.push_back(strdup(std::string(command).c_str())); + for (const std::string& arg : args) { + argv.push_back(strdup(arg.c_str())); + } + argv.push_back(nullptr); + + pid_t pid; + VerifyElseExitWithCleanup( + posix_spawnp(&pid, argv[0], &actions, nullptr, argv.data(), environ) == 0, + &actions); + + posix_spawn_file_actions_destroy(&actions); + + // Close the write ends of the pipes. + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + // Drain both pipes while the child is running. Waiting first can deadlock + // once either pipe fills (sanitizer diagnostics routinely exceed the pipe + // capacity), because the child cannot exit and the parent never starts + // reading. + std::thread stdout_reader{[&result, fd = stdout_pipe[0]]() { + result.std_output = ReadFromFd(fd); + }}; + std::thread stderr_reader{[&result, fd = stderr_pipe[0]]() { + result.std_error = ReadFromFd(fd); + }}; + + int wait_status; + pid_t waited_pid; + do { + waited_pid = waitpid(pid, &wait_status, 0); + } while (waited_pid == -1 && errno == EINTR); + + VerifyElseExit(waited_pid == pid); + stdout_reader.join(); + stderr_reader.join(); + + if (WIFEXITED(wait_status)) { + result.status = WEXITSTATUS(wait_status); + } else if (WIFSIGNALED(wait_status)) { + result.status = 128 + WTERMSIG(wait_status); + } else { + result.status = 1; + } + + // Close the read ends of the pipes. + close(stdout_pipe[0]); + close(stderr_pipe[0]); + + for (char* arg : argv) { + free(arg); + } + + return result; +} + +namespace { + +std::string ReadFromFd(int fd) { + std::string result; + constexpr size_t bufferSize = 4096; + char buffer[bufferSize]; + ssize_t bytesRead; + while (true) { + bytesRead = read(fd, buffer, bufferSize); + if (bytesRead > 0) { + result.append(buffer, bytesRead); + continue; + } + + if (bytesRead == 0) { + break; + } + + if (errno == EINTR) { + continue; + } + + ExitOnError("read", nullptr); + } + return result; +} + +// Format a readable error message, print it to console, and exit from the +// application. +void ExitOnError(const char* message, posix_spawn_file_actions_t* actions) { + int err = errno; + const char* err_msg = strerror(err); + + fprintf(stderr, "%s failed with error %d: %s\n", message, err, err_msg); + + if (actions != nullptr) { + posix_spawn_file_actions_destroy(actions); + } + + exit(1); +} + +} // namespace + +} // namespace node_api_tests + +#endif // __ANDROID__ diff --git a/Tests/NodeApi/compat.h b/Tests/NodeApi/compat.h new file mode 100644 index 00000000..7974b34c --- /dev/null +++ b/Tests/NodeApi/compat.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#ifndef SRC_PUBLIC_COMPAT_H_ +#define SRC_PUBLIC_COMPAT_H_ + +// This file contains some useful datatypes recently introduced in C++17 and +// C++20. They must be removed after we switch the toolset to the newer C++ +// language version. + +#include +#ifdef __cpp_lib_span +#include +#endif + +namespace node_api_tests { + +#ifdef __cpp_lib_span +using std::span; +#else +/** + * @brief A span of values that can be used to pass arguments to function. + * + * For C++20 we should consider to replace it with std::span. + */ +template +struct span { + constexpr span(std::initializer_list il) noexcept + : data_{const_cast(il.begin())}, size_{il.size()} {} + constexpr span(T* data, size_t size) noexcept : data_{data}, size_{size} {} + + [[nodiscard]] constexpr T* data() const noexcept { return data_; } + + [[nodiscard]] constexpr size_t size() const noexcept { return size_; } + + [[nodiscard]] constexpr T* begin() const noexcept { return data_; } + + [[nodiscard]] constexpr T* end() const noexcept { return data_ + size_; } + + const T& operator[](size_t index) const noexcept { return *(data_ + index); } + + private: + T* data_; + size_t size_; +}; +#endif // __cpp_lib_span + +} // namespace node_api_tests + +#endif // SRC_PUBLIC_COMPAT_H_ diff --git a/Tests/NodeApi/include/node_api.h b/Tests/NodeApi/include/node_api.h new file mode 100644 index 00000000..0ba4bc6e --- /dev/null +++ b/Tests/NodeApi/include/node_api.h @@ -0,0 +1,67 @@ +#ifndef NODE_API_H_ +#define NODE_API_H_ + +#include +#include "node_api_types.h" + +#ifdef __cplusplus +#define NODE_API_EXTERN_C_START extern "C" { +#define NODE_API_EXTERN_C_END } +#else +#define NODE_API_EXTERN_C_START +#define NODE_API_EXTERN_C_END +#endif + +#ifdef _WIN32 +#define NAPI_MODULE_EXPORT __declspec(dllexport) +#else +#define NAPI_MODULE_EXPORT __attribute__((visibility("default"))) +#endif + +#ifndef NAPI_MODULE_VERSION +#define NAPI_MODULE_VERSION 1 +#endif + +typedef napi_value(NAPI_CDECL* napi_addon_register_func)(napi_env env, + napi_value exports); + +typedef struct napi_module_s { + int nm_version; + unsigned int nm_flags; + const char* nm_filename; + napi_addon_register_func nm_register_func; + const char* nm_modname; + void* nm_priv; + void* reserved[4]; +} napi_module; + +#define NODE_API_MODULE_GET_API_VERSION_FUNCTION node_api_module_get_api_version_v1 +#define NODE_API_MODULE_REGISTER_FUNCTION napi_register_module_v1 + +#define NAPI_MODULE_INIT() \ + NODE_API_EXTERN_C_START \ + NAPI_MODULE_EXPORT int32_t NODE_API_MODULE_GET_API_VERSION_FUNCTION(void) {\ + return NAPI_VERSION; \ + } \ + NAPI_MODULE_EXPORT napi_value NODE_API_MODULE_REGISTER_FUNCTION( \ + napi_env env, napi_value exports); \ + NODE_API_EXTERN_C_END \ + static napi_value napi_module_init_impl(napi_env env, napi_value exports); \ + NODE_API_EXTERN_C_START \ + NAPI_MODULE_EXPORT napi_value NODE_API_MODULE_REGISTER_FUNCTION( \ + napi_env env, napi_value exports) { \ + return napi_module_init_impl(env, exports); \ + } \ + NODE_API_EXTERN_C_END \ + static napi_value napi_module_init_impl(napi_env env, napi_value exports) + +#define NAPI_MODULE(modname, regfunc) \ + NAPI_MODULE_INIT() { \ + (void)(modname); \ + return regfunc(env, exports); \ + } + +#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ + NAPI_MODULE(modname, regfunc) + +#endif // NODE_API_H_ diff --git a/Tests/NodeApi/include/node_api_types.h b/Tests/NodeApi/include/node_api_types.h new file mode 100644 index 00000000..dc36f9d1 --- /dev/null +++ b/Tests/NodeApi/include/node_api_types.h @@ -0,0 +1,28 @@ +#ifndef NODE_API_TYPES_H_ +#define NODE_API_TYPES_H_ + +#include + +typedef struct napi_callback_scope__* napi_callback_scope; +typedef struct napi_async_context__* napi_async_context; +typedef struct napi_async_work__* napi_async_work; + +// Current node-api-cts uses the no-JS finalizer environment spelling in its +// v7 typed-array test. It is ABI-compatible with napi_env for this API level; +// the stricter nogc type distinction arrived later in the public headers. +typedef napi_env node_api_basic_env; + +typedef void(NAPI_CDECL* napi_async_execute_callback)(napi_env env, + void* data); +typedef void(NAPI_CDECL* napi_async_complete_callback)(napi_env env, + napi_status status, + void* data); + +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t patch; + const char* release; +} napi_node_version; + +#endif // NODE_API_TYPES_H_ diff --git a/Tests/NodeApi/js_runtime_api.cpp b/Tests/NodeApi/js_runtime_api.cpp new file mode 100644 index 00000000..fe7dc56a --- /dev/null +++ b/Tests/NodeApi/js_runtime_api.cpp @@ -0,0 +1,264 @@ +#include "js_runtime_api.h" + +#include +#include + +#include +#include + +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) +#include +#include "js_native_api_javascriptcore.h" + +// Exported by JavaScriptCore but intentionally kept out of the public header. +// It is the API used here to implement Node's explicit --expose-gc testing +// hook; JSGarbageCollect() no longer performs a synchronous collection. +extern "C" void JSSynchronousGarbageCollectForDebugging(JSContextRef); +#elif defined(JSR_NAPI_ENGINE_V8) +#include +#include "js_native_api_v8.h" +#elif defined(JSR_NAPI_ENGINE_QUICKJS) +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshorten-64-to-32" +#endif +#include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif +#include "js_native_api_quickjs.h" +#elif defined(JSR_NAPI_ENGINE_HERMES) +#include +#endif + +struct jsr_napi_env_scope_s { + napi_env env{nullptr}; +#if defined(JSR_NAPI_ENGINE_V8) + v8::Global context; +#endif +}; + +napi_status jsr_open_napi_env_scope(napi_env env, + jsr_napi_env_scope* scope) { + if (scope == nullptr) { + return napi_invalid_arg; + } + + auto* scope_impl = new jsr_napi_env_scope_s{}; + scope_impl->env = env; +#if defined(JSR_NAPI_ENGINE_V8) + // node_lite calls Node-API outside any napi callback, so V8 has no *current context*. The env + // holder already holds a Locker + Isolate::Scope; enter the env's context here (exited in + // jsr_close_napi_env_scope) so calls such as napi_create_object -> v8::Object::New(isolate), + // which use the isolate's current context, don't segfault. (On JSC this scope is a no-op -- the + // env carries its context explicitly.) + if (env != nullptr) { + v8::Isolate* isolate = env->isolate; + v8::HandleScope handle_scope(isolate); + v8::Local context = env->context(); + scope_impl->context.Reset(isolate, context); + context->Enter(); + } +#endif + *scope = scope_impl; + return napi_ok; +} + +napi_status jsr_close_napi_env_scope(napi_env /*env*/, + jsr_napi_env_scope scope) { + if (scope == nullptr) { + return napi_invalid_arg; + } + +#if defined(JSR_NAPI_ENGINE_V8) + if (scope->env != nullptr) { + v8::Isolate* isolate = scope->env->isolate; + v8::HandleScope handle_scope(isolate); + v8::Local context = scope->context.Get(isolate); + context->Exit(); + scope->context.Reset(); + } +#endif + delete scope; + return napi_ok; +} + +napi_status jsr_run_script(napi_env env, + napi_value source, + const char* source_url, + napi_value* result) { +#if defined(JSR_NAPI_ENGINE_HERMES) + if (env == nullptr || source == nullptr || result == nullptr) { + return napi_invalid_arg; + } + + size_t length{}; + napi_status status = + napi_get_value_string_utf8(env, source, nullptr, 0, &length); + if (status != napi_ok) { + return status; + } + + std::string script(length + 1, '\0'); + size_t written{}; + status = napi_get_value_string_utf8( + env, source, script.data(), script.size(), &written); + if (status != napi_ok) { + return status; + } + script.resize(written); + + try { + *result = Napi::Eval( + Napi::Env{env}, script.c_str(), source_url == nullptr ? "" : source_url); + return napi_ok; + } catch (const Napi::Error& error) { + error.ThrowAsJavaScriptException(); + return napi_pending_exception; + } catch (const std::exception& error) { + napi_throw_error(env, nullptr, error.what()); + return napi_pending_exception; + } +#else + return napi_run_script(env, source, source_url, result); +#endif +} + +napi_status jsr_collect_garbage(napi_env env) { +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) + if (env == nullptr) { + return napi_invalid_arg; + } + + JSGlobalContextRef context = env->context; + if (context == nullptr) { + return napi_invalid_arg; + } + + // JSGarbageCollect only calls reportAbandonedObjectGraph() in current + // WebKit; it does not synchronously collect. The Node-API suite expects one + // global.gc() call to run an unreachable napi_wrap finalizer. + // https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/API/JSBase.cpp + env->set_defer_finalizers(true); + JSSynchronousGarbageCollectForDebugging(context); + env->set_defer_finalizers(false); + env->drain_deferred_finalizers(); + return napi_ok; +#elif defined(JSR_NAPI_ENGINE_V8) + if (env == nullptr) { + return napi_invalid_arg; + } + + v8::Isolate* isolate = env->isolate; + if (isolate == nullptr) { + return napi_invalid_arg; + } + + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::kFullGarbageCollection); + return napi_ok; +#elif defined(JSR_NAPI_ENGINE_QUICKJS) + if (env == nullptr || env->context == nullptr) { + return napi_invalid_arg; + } + JS_RunGC(JS_GetRuntime(env->context)); + return napi_ok; +#elif defined(JSR_NAPI_ENGINE_HERMES) + if (env == nullptr) { + return napi_invalid_arg; + } + Napi::CollectGarbage(Napi::Env{env}); + return napi_ok; +#else + (void)env; + return napi_generic_failure; +#endif +} + +napi_status jsr_drain_microtasks(napi_env env, + int32_t max_count_hint, + bool* result) { + if (env == nullptr || result == nullptr) { + return napi_invalid_arg; + } + +#if defined(JSR_NAPI_ENGINE_QUICKJS) + if (env->context == nullptr) { + return napi_invalid_arg; + } + + JSRuntime* runtime = JS_GetRuntime(env->context); + JSContext* pending_context = nullptr; + int32_t count = 0; + while (max_count_hint <= 0 || count < max_count_hint) { + int status = JS_ExecutePendingJob(runtime, &pending_context); + if (status < 0) { + return napi_pending_exception; + } + if (status == 0) { + *result = true; + return napi_ok; + } + ++count; + } + + *result = !JS_IsJobPending(runtime); + return napi_ok; +#elif defined(JSR_NAPI_ENGINE_V8) + if (env->isolate == nullptr) { + return napi_invalid_arg; + } + env->isolate->PerformMicrotaskCheckpoint(); + *result = true; + return napi_ok; +#elif defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) + // JavaScriptCore drains promise jobs at the host call boundary. + (void)max_count_hint; + *result = true; + return napi_ok; +#elif defined(JSR_NAPI_ENGINE_HERMES) + (void)max_count_hint; + Napi::DrainJobs(Napi::Env{env}); + *result = true; + return napi_ok; +#else + (void)max_count_hint; + *result = false; + return napi_generic_failure; +#endif +} + +napi_status jsr_initialize_native_module( + napi_env env, + napi_addon_register_func register_module, + int32_t /*api_version*/, + napi_value* exports) { + if (env == nullptr || register_module == nullptr || exports == nullptr) { + return napi_invalid_arg; + } + + napi_value module_exports{}; + napi_status status = napi_create_object(env, &module_exports); + if (status != napi_ok) { + return status; + } + + napi_value returned_exports = register_module(env, module_exports); + + bool has_exception = false; + status = napi_is_exception_pending(env, &has_exception); + if (status != napi_ok) { + return status; + } + + if (has_exception) { + return napi_pending_exception; + } + + if (returned_exports != nullptr && returned_exports != module_exports) { + module_exports = returned_exports; + } + + *exports = module_exports; + return napi_ok; +} diff --git a/Tests/NodeApi/js_runtime_api.h b/Tests/NodeApi/js_runtime_api.h new file mode 100644 index 00000000..27b39461 --- /dev/null +++ b/Tests/NodeApi/js_runtime_api.h @@ -0,0 +1,219 @@ +#ifndef HERMES_JS_RUNTIME_API_H +#define HERMES_JS_RUNTIME_API_H + +#include "node_api.h" + +// +// Node-API extensions required for JavaScript engine hosting. +// +// It is a very early version of the APIs which we consider to be experimental. +// These APIs are not stable yet and are subject to change while we continue +// their development. After some time we will stabilize the APIs and make them +// "officially stable". +// + +#define JSR_API NAPI_EXTERN napi_status NAPI_CDECL + +EXTERN_C_START + +typedef struct jsr_runtime_s *jsr_runtime; +typedef struct jsr_config_s *jsr_config; +typedef struct jsr_prepared_script_s *jsr_prepared_script; +typedef struct jsr_napi_env_scope_s *jsr_napi_env_scope; + +typedef void(NAPI_CDECL *jsr_data_delete_cb)(void *data, void *deleter_data); + +//============================================================================= +// jsr_runtime +//============================================================================= + +JSR_API jsr_create_runtime(jsr_config config, jsr_runtime *runtime); +JSR_API jsr_delete_runtime(jsr_runtime runtime); +JSR_API jsr_runtime_get_node_api_env(jsr_runtime runtime, napi_env *env); + +//============================================================================= +// jsr_config +//============================================================================= + +JSR_API jsr_create_config(jsr_config *config); +JSR_API jsr_delete_config(jsr_config config); + +JSR_API jsr_config_enable_inspector(jsr_config config, bool value); +JSR_API jsr_config_set_inspector_runtime_name( + jsr_config config, + const char *name); +JSR_API jsr_config_set_inspector_port(jsr_config config, uint16_t port); +JSR_API jsr_config_set_inspector_break_on_start(jsr_config config, bool value); + +JSR_API jsr_config_enable_gc_api(jsr_config config, bool value); + +JSR_API jsr_config_set_explicit_microtasks(jsr_config config, bool value); + +// A callback to process unhandled JS error +typedef void(NAPI_CDECL *jsr_unhandled_error_cb)( + void *cb_data, + napi_env env, + napi_value error); + +JSR_API jsr_config_on_unhandled_error( + jsr_config config, + void *cb_data, + jsr_unhandled_error_cb unhandled_error_cb); + +//============================================================================= +// jsr_config task runner +//============================================================================= + +// A callback to run task +typedef void(NAPI_CDECL *jsr_task_run_cb)(void *task_data); + +// A callback to post task to the task runner +typedef void(NAPI_CDECL *jsr_task_runner_post_task_cb)( + void *task_runner_data, + void *task_data, + jsr_task_run_cb task_run_cb, + jsr_data_delete_cb task_data_delete_cb, + void *deleter_data); + +JSR_API jsr_config_set_task_runner( + jsr_config config, + void *task_runner_data, + jsr_task_runner_post_task_cb task_runner_post_task_cb, + jsr_data_delete_cb task_runner_data_delete_cb, + void *deleter_data); + +//============================================================================= +// jsr_config script cache +//============================================================================= + +typedef void(NAPI_CDECL *jsr_script_cache_load_cb)( + void *script_cache_data, + const char *source_url, + uint64_t source_hash, + const char *runtime_name, + uint64_t runtime_version, + const char *cache_tag, + const uint8_t **buffer, + size_t *buffer_size, + jsr_data_delete_cb *buffer_delete_cb, + void **deleter_data); + +typedef void(NAPI_CDECL *jsr_script_cache_store_cb)( + void *script_cache_data, + const char *source_url, + uint64_t source_hash, + const char *runtime_name, + uint64_t runtime_version, + const char *cache_tag, + const uint8_t *buffer, + size_t buffer_size, + jsr_data_delete_cb buffer_delete_cb, + void *deleter_data); + +JSR_API jsr_config_set_script_cache( + jsr_config config, + void *script_cache_data, + jsr_script_cache_load_cb script_cache_load_cb, + jsr_script_cache_store_cb script_cache_store_cb, + jsr_data_delete_cb script_cache_data_delete_cb, + void *deleter_data); + +//============================================================================= +// napi_env scope +//============================================================================= + +// Opens the napi_env scope in the current thread. +// Calling Node-API functions without the opened scope may cause a failure. +// The scope must be closed by the jsr_close_napi_env_scope call. +JSR_API jsr_open_napi_env_scope(napi_env env, jsr_napi_env_scope *scope); + +// Closes the napi_env scope in the current thread. It must match to the +// jsr_open_napi_env_scope call. +JSR_API jsr_close_napi_env_scope(napi_env env, jsr_napi_env_scope scope); + +//============================================================================= +// Additional functions to implement JSI +//============================================================================= + +// To implement JSI description() +JSR_API jsr_get_description(napi_env env, const char **result); + +// To implement JSI queueMicrotask() +JSR_API jsr_queue_microtask(napi_env env, napi_value callback); + +// To implement JSI drainMicrotasks() +JSR_API +jsr_drain_microtasks(napi_env env, int32_t max_count_hint, bool *result); + +// To implement JSI isInspectable() +JSR_API jsr_is_inspectable(napi_env env, bool *result); + +//============================================================================= +// Script preparing and running. +// +// Script is usually converted to byte code, or in other words - prepared - for +// execution. Then, we can run the prepared script. +//============================================================================= + +// Run script with source URL. +JSR_API jsr_run_script( + napi_env env, + napi_value source, + const char *source_url, + napi_value *result); + +// Prepare the script for running. +JSR_API jsr_create_prepared_script( + napi_env env, + const uint8_t *script_data, + size_t script_length, + jsr_data_delete_cb script_delete_cb, + void *deleter_data, + const char *source_url, + jsr_prepared_script *result); + +// Delete the prepared script. +JSR_API jsr_delete_prepared_script( + napi_env env, + jsr_prepared_script prepared_script); + +// Run the prepared script. +JSR_API jsr_prepared_script_run( + napi_env env, + jsr_prepared_script prepared_script, + napi_value *result); + +//============================================================================= +// Functions to support unit tests. +//============================================================================= + +// Provides a hint to run garbage collection. +// It is typically used for unit tests. +// It requires enabling GC by calling jsr_config_enable_gc_api. +JSR_API jsr_collect_garbage(napi_env env); + +// Checks if the environment has an unhandled promise rejection. +JSR_API jsr_has_unhandled_promise_rejection(napi_env env, bool *result); + +// Gets and clears the last unhandled promise rejection. +JSR_API jsr_get_and_clear_last_unhandled_promise_rejection( + napi_env env, + napi_value *result); + +// Create new napi_env for the runtime. +JSR_API +jsr_create_node_api_env(napi_env root_env, int32_t api_version, napi_env *env); + +// Run task in the environment context. +JSR_API jsr_run_task(napi_env env, jsr_task_run_cb task_cb, void *data); + +// Initializes native module. +JSR_API jsr_initialize_native_module( + napi_env env, + napi_addon_register_func register_module, + int32_t api_version, + napi_value *exports); + +EXTERN_C_END + +#endif // HERMES_JS_RUNTIME_API_H \ No newline at end of file diff --git a/Tests/NodeApi/main.cpp b/Tests/NodeApi/main.cpp new file mode 100644 index 00000000..1565693c --- /dev/null +++ b/Tests/NodeApi/main.cpp @@ -0,0 +1,81 @@ +#include "test_main.h" + +#include + +#include +#include +#include +#include + +#include "child_process.h" + +namespace fs = std::filesystem; + +namespace { + +fs::path ResolveNodeLitePath(const fs::path& exe_path) { + fs::path nodeLitePath = exe_path; + nodeLitePath.replace_filename("node_lite"); +#if defined(_WIN32) + nodeLitePath += ".exe"; +#endif + return nodeLitePath; +} + +fs::path ResolveTestsRoot(const fs::path& exe_path) { + fs::path testRootPath = exe_path.parent_path(); + fs::path js_root = testRootPath / "test"; + if (!fs::exists(js_root)) { + testRootPath = testRootPath.parent_path(); + js_root = testRootPath / "test"; + } + return js_root; +} + +std::unordered_set ParseEnabledNativeSuites() { + std::unordered_set suites; +#ifdef NODE_API_AVAILABLE_NATIVE_TESTS + std::stringstream stream(NODE_API_AVAILABLE_NATIVE_TESTS); + std::string entry; + while (std::getline(stream, entry, ',')) { + if (!entry.empty()) { + suites.insert(entry); + } + } +#endif + return suites; +} + +} // namespace + +int main(int argc, char** argv) { + fs::path exe_path = fs::canonical(argv[0]); + fs::path js_root = ResolveTestsRoot(exe_path); + if (!fs::exists(js_root)) { + std::cerr << "Error: Cannot find Node-API test directory." << std::endl; + return EXIT_FAILURE; + } + + fs::path node_lite_path = ResolveNodeLitePath(exe_path); + if (!fs::exists(node_lite_path)) { + std::cerr << "Error: Cannot find node_lite executable at " + << node_lite_path << std::endl; + return EXIT_FAILURE; + } + + node_api_tests::NodeApiTestConfig config{}; + config.js_root = js_root; + config.run_script = + [node_lite_path](const fs::path& script_path) + -> node_api_tests::ProcessResult { + return node_api_tests::SpawnSync(node_lite_path.string(), + {script_path.string()}); + }; + config.enabled_native_suites = ParseEnabledNativeSuites(); + + node_api_tests::InitializeNodeApiTests(config); + + ::testing::InitGoogleTest(&argc, argv); + node_api_tests::RegisterNodeApiTests(); + return RUN_ALL_TESTS(); +} diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp new file mode 100644 index 00000000..ca94b9fa --- /dev/null +++ b/Tests/NodeApi/node_lite.cpp @@ -0,0 +1,1478 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "node_lite.h" +#include "js_runtime_api.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "child_process.h" + +namespace fs = std::filesystem; + +namespace node_api_tests { + +namespace { + +std::mutex& ErrorHandlerMutex() { + static std::mutex mutex; + return mutex; +} + +void DefaultFatalErrorHandler(const NodeLiteFatalErrorInfo& info) { + if (!info.message.empty()) { + std::cerr << info.message; + if (!info.details.empty()) { + std::cerr << '\n' << info.details; + } + std::cerr << std::endl; + } else if (!info.details.empty()) { + std::cerr << info.details << std::endl; + } + std::exit(info.exit_code); +} + +NodeApiRef MakeNodeApiRef(napi_env env, napi_value value) { + napi_ref ref{}; + NODE_LITE_CALL(napi_create_reference(env, value, 1, &ref)); + return NodeApiRef(ref, NodeApiRefDeleter(env)); +} + +template +void ThrowJSErrorOnException(napi_env env, TCallback&& callback) { + try { + callback(); + } catch (const NodeLiteException& e) { + if (e.error_status() == napi_pending_exception) { + napi_value error = NodeApi::GetAndClearLastException(env); + NodeApi::ThrowError(env, error); + } else { + NodeApi::ThrowError(env, e.what()); + } + } catch (const std::exception& e) { + NodeApi::ThrowError(env, e.what()); + } +} + +template +void ExitOnException(napi_env env, TCallback&& callback) { + // NOT noexcept: the in-process runner (RunNodeLiteScript) installs a fatal-error handler that + // *throws* NodeLiteFatalError (to be caught and turned into a ProcessResult) rather than calling + // std::exit. ExitWithJSError/ExitWithMessage below invoke that handler, so this function must let + // the throw propagate -- if it were noexcept the throw would std::terminate the whole test + // process. (With the default handler these call std::exit and never throw.) + try { + callback(); + } catch (const NodeLiteException& e) { + if (e.error_status() == napi_pending_exception) { + napi_value error = NodeApi::GetAndClearLastException(env); + NodeLiteErrorHandler::ExitWithJSError(env, error); + } else { + NodeLiteErrorHandler::ExitWithMessage(e.what()); + } + } catch (const std::exception& e) { + NodeLiteErrorHandler::ExitWithMessage(e.what()); + } +} + +std::string ReadFileText(napi_env env, fs::path file_path) { + std::ifstream file_stream(file_path.string()); + NODE_LITE_ASSERT(file_stream.is_open(), + "Failed to open file: %s. Error: %s", + file_path.c_str(), + std::strerror(errno)); + std::ostringstream ss; + ss << file_stream.rdbuf(); + return ss.str(); +} + +class NodeApiCallbackInfo { + public: + NodeApiCallbackInfo(napi_env env, napi_callback_info info) { + size_t argc{inline_args_.size()}; + napi_value* argv = inline_args_.data(); + NODE_LITE_CALL( + napi_get_cb_info(env, info, &argc, argv, &this_arg_, &data_)); + if (argc > inline_args_.size()) { + dynamic_args_ = std::make_unique(argc); + argv = dynamic_args_.get(); + NODE_LITE_CALL( + napi_get_cb_info(env, info, &argc, argv, &this_arg_, &data_)); + } + args_ = span(argv, argc); + } + + span args() const { return args_; } + napi_value this_arg() const { return this_arg_; } + void* data() const { return data_; } + + private: + std::array inline_args_{}; + std::unique_ptr dynamic_args_{}; + span args_{}; + napi_value this_arg_{}; + void* data_{}; +}; + +} // namespace + +//============================================================================= +// NodeApiTest implementation +//============================================================================= + +std::unique_ptr CreateEnvHolder( + std::shared_ptr taskRunner, + std::function onUnhandledError); + +//============================================================================= +// NodeLiteModule implementation +//============================================================================= + +using ModuleRegisterFuncCallback = napi_value(NAPI_CDECL*)(napi_env env, + napi_value exports); +using ModuleApiVersionCallback = int32_t(NAPI_CDECL*)(); + +NodeLiteModule::NodeLiteModule(std::filesystem::path module_path) noexcept + : module_path_(std::move(module_path)) {} + +NodeLiteModule::NodeLiteModule(std::filesystem::path module_path, + InitModuleCallback init_module) noexcept + : module_path_(std::move(module_path)), + init_module_(std::move(init_module)) {} + +napi_value NodeLiteModule::LoadModule(napi_env env) { + if (state_ == State::kLoaded) { + return NodeApi::GetReferenceValue(env, exports_.get()); + } + if (state_ == State::kLoading) { + return NodeApi::GetUndefined(env); + } + NODE_LITE_ASSERT(state_ == State::kNotLoaded, + "Unexpected module '%s' state: %d", + module_path_.string().c_str(), + static_cast(state_)); + state_ = State::kLoading; + struct ResetStateIfFailed { + NodeLiteModule* module_; + ~ResetStateIfFailed() { + if (module_->state_ == State::kLoading) { + module_->state_ = State::kNotLoaded; + } + } + } reset_state_if_failed{this}; + + if (init_module_) { + napi_value exports = NodeApi::CreateObject(env); + napi_value init_exports = init_module_(env, exports); + if (init_exports != nullptr && + NodeApi::TypeOf(env, init_exports) != napi_undefined) { + exports = init_exports; + } + exports_ = MakeNodeApiRef(env, exports); + } else if (module_path_.extension() == ".js") { + exports_ = MakeNodeApiRef(env, LoadScriptModule(env)); + } else if (module_path_.extension() == ".node") { + exports_ = MakeNodeApiRef(env, LoadNativeModule(env)); + } else { + NODE_LITE_ASSERT( + false, "Unsupported module type: %s", module_path_.string().c_str()); + } + state_ = State::kLoaded; + return NodeApi::GetReferenceValue(env, exports_.get()); +} + +napi_value NodeLiteModule::LoadScriptModule(napi_env env) { + std::string module_func_wrapper = + "(function(module, exports, require, __filename, __dirname) {"; + module_func_wrapper += ReadModuleFileText(env); + + size_t source_map_index = module_func_wrapper.find("//# sourceMappingURL"); + constexpr const char* module_suffix = "\nreturn module.exports; })\n"; + if (source_map_index != std::string::npos) { + module_func_wrapper.insert(source_map_index, module_suffix); + } else { + module_func_wrapper += module_suffix; + } + + napi_value module_func = NodeApi::RunScript( + env, module_func_wrapper, module_path_.string().c_str()); + + NODE_LITE_ASSERT(NodeApi::TypeOf(env, module_func) == napi_function); + + napi_value exports = NodeApi::CreateObject(env); + napi_value file_name = NodeApi::CreateString(env, module_path_.string()); + napi_value dir_name = + NodeApi::CreateString(env, module_path_.parent_path().string()); + + napi_value module_obj = NodeApi::CreateObject(env); + NodeApi::SetProperty(env, module_obj, "exports", exports); + NodeApi::SetProperty(env, module_obj, "__filename", file_name); + NodeApi::SetProperty(env, module_obj, "__dirname", dir_name); + + napi_value require = NodeApi::CreateFunction( + env, "require", [this](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least one argument"); + std::string module_path = NodeApi::ToStdString(env, args[0]); + NodeLiteRuntime* runtime = NodeLiteRuntime::GetRuntime(env); + return runtime + ->ResolveModule(module_path_.parent_path().string(), module_path) + .LoadModule(env); + }); + + return NodeApi::CallFunction( + env, module_func, {module_obj, exports, require, file_name, dir_name}); +} + +napi_value NodeLiteModule::LoadNativeModule(napi_env env) { + ModuleApiVersionCallback getModuleApiVersion = + reinterpret_cast(NodeLitePlatform::LoadFunction( + env, module_path_.c_str(), "node_api_module_get_api_version_v1")); + int32_t moduleApiVersion = getModuleApiVersion ? getModuleApiVersion() : 8; + + ModuleRegisterFuncCallback moduleRegisterFunc = + reinterpret_cast( + NodeLitePlatform::LoadFunction( + env, module_path_.c_str(), "napi_register_module_v1")); + NODE_LITE_ASSERT(moduleRegisterFunc != nullptr, + "Failed to find 'napi_register_module_v1' in module: %s", + module_path_.c_str()); + + napi_value exports{}; + NODE_LITE_CALL(jsr_initialize_native_module( + env, moduleRegisterFunc, moduleApiVersion, &exports)); + return exports; +} + +std::string NodeLiteModule::ReadModuleFileText(napi_env env) { + return ReadFileText(env, module_path_); +} + +//============================================================================= +// NodeLiteRuntime implementation +//============================================================================= + +/*static*/ void NodeLiteRuntime::Run(std::vector argv) { + // Convert arguments to vector of strings and skip all options before the JS + // file name. + std::vector args; + args.reserve(argv.size()); + bool skipOptions = true; + if (argv.size() < 2) { + NodeLiteErrorHandler::ExitWithMessage("", [&](std::ostream& os) { + os << "Usage: " << argv[0] << " "; + }); + } + args.push_back(argv[0]); + for (int i = 1; i < argv.size(); i++) { + if (skipOptions && std::string_view(argv[i]).find("--") == 0) { + continue; + } + skipOptions = false; + args.push_back(argv[i]); + } + + std::shared_ptr taskRunner = + std::make_shared(); + + fs::path exe_path = fs::canonical(argv[0]); + + fs::path test_root_path = exe_path.parent_path(); + fs::path js_root = test_root_path / "test"; + if (!fs::exists(js_root)) { + test_root_path = test_root_path.parent_path(); + js_root = test_root_path / "test"; + } + if (!fs::exists(js_root)) { + NodeLiteErrorHandler::ExitWithMessage("Error: Cannot find test directory."); + } + + std::string jsFilePath = args[1]; + std::unique_ptr runtime = NodeLiteRuntime::Create( + std::move(taskRunner), + js_root.string(), + std::move(args), + NodeLiteRuntime::Callbacks{}); + runtime->RunTestScript(jsFilePath); +} + +/*static*/ std::unique_ptr NodeLiteRuntime::Create( + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks) { + std::unique_ptr runtime = + std::make_unique(PrivateTag{}, + std::move(task_runner), + std::move(js_root), + std::move(args), + std::move(callbacks)); + runtime->Initialize(); + return std::unique_ptr(runtime.release()); +} + +NodeLiteRuntime::NodeLiteRuntime( + PrivateTag, + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks) + : task_runner_(std::move(task_runner)), + js_root_(std::move(js_root)), + args_(std::move(args)), + callbacks_(std::move(callbacks)) {} + +void NodeLiteRuntime::Initialize() { + env_holder_ = + CreateEnvHolder(task_runner_, [this](napi_env env, napi_value error) { + NODE_LITE_ASSERT(env == env_, + "Unhandled error in different napi_env: %p != %p", + env, + env_); + OnUncaughtException(error); + }); + env_ = env_holder_->getEnv(); + NodeApiEnvScope env_scope{env_}; + NodeApiHandleScope handle_scope{env_}; + DefineBuiltInModules(); + DefineGlobalFunctions(); +} + +NodeLiteModule& NodeLiteRuntime::ResolveModule( + const std::string& parent_module_path, const std::string& module_path) { + napi_env env = env_; + fs::path fs_module_path = ResolveModulePath(parent_module_path, module_path); + if (auto it = registered_modules_.find(fs_module_path.string()); + it != registered_modules_.end()) { + return *it->second; + } + + if (auto [it, succeeded] = registered_modules_.try_emplace( + fs_module_path.string(), + std::make_unique(fs_module_path.string())); + succeeded) { + return *it->second; + } + + NODE_LITE_ASSERT( + false, "Failed to register module: %s", fs_module_path.string().c_str()); +} + +fs::path NodeLiteRuntime::ResolveModulePath( + const std::string& parent_module_path, const std::string& module_path) { + napi_env env = env_; + // 1. See if it is an embedded module such as "assert". + auto it = node_js_modules_.find(module_path); + if (it != node_js_modules_.end()) { + return fs::path(it->second); + } + + // 2. Check if it is a relative or an absolute path to a module. + { + fs::path fs_module_path = fs::path(module_path); + if (!fs_module_path.is_absolute()) { + fs::path fs_parent_module_path = fs::path(parent_module_path); + NODE_LITE_ASSERT(fs_parent_module_path.is_absolute(), + "Parent module path '%s' is not absolute", + parent_module_path.c_str()); + fs_module_path = fs_parent_module_path / fs_module_path; + } + fs_module_path = fs::weakly_canonical(fs_module_path); + + if (fs::exists(fs_module_path) && fs::is_regular_file(fs_module_path)) { + return fs_module_path; + } + if (fs::path result = fs::path(fs_module_path).replace_extension(".js"); + fs::exists(result)) { + return result; + } + if (fs::path result = fs_module_path / "index.js"; fs::exists(result)) { + return result; + } + // See if it is a native module. + fs::path node_module_path = + fs::path(fs_module_path).replace_extension(".node"); + if (fs::exists(node_module_path)) { + return node_module_path; + } +#if defined(__ANDROID__) + // On Android the addon ships as lib.so in the app's nativeLibraryDir rather than as a .node + // file on disk, so the existence check above fails. Resolve to the .node path anyway so + // LoadNativeModule runs; node_lite_android dlopens it by soname (lib.so), resolving its + // napi_* imports from the shared libnapi.so. + return node_module_path; +#endif + // See if the module was prefixed with the parent folder to disambiguate C++ + // project name. + fs::path fs_parent_folder = fs::path(parent_module_path).filename(); + node_module_path.replace_filename(fs_parent_folder.string() + "_" + + node_module_path.filename().string()); + if (fs::exists(node_module_path)) { + return node_module_path; + } + } + + // 3. Check if it is in the node_modules folder. + { + fs::path fs_module_path = fs::weakly_canonical( + fs::path(js_root_) / "node_modules" / fs::path(module_path)); + + if (fs::exists(fs_module_path) && fs::is_regular_file(fs_module_path)) { + return fs_module_path; + } + if (fs::path result = fs::path(fs_module_path).replace_extension(".js"); + fs::exists(result)) { + return result; + } + if (fs::path result = fs_module_path / "index.js"; fs::exists(result)) { + return result; + } + } + + NODE_LITE_ASSERT( + false, "Cannot resolve module path '%s'", module_path.c_str()); +} + +void NodeLiteRuntime::AddNativeModule( + const std::string& module_name, + std::function initModule) { + napi_env env = env_; + auto [_, succeeded] = registered_modules_.try_emplace( + module_name, + std::make_unique(module_name, std::move(initModule))); + NODE_LITE_ASSERT( + succeeded, "Failed to register module: %s", module_name.c_str()); +} + +void NodeLiteRuntime::RunTestScript(const std::string& script_path) { + NodeApiEnvScope env_scope{env_}; + NodeApiHandleScope handle_scope{env_}; + { + ExitOnException(env_, [this, &script_path]() { + NodeApiHandleScope scope{env_}; + NodeLiteModule& main_module = ResolveModule(js_root_, script_path); + main_module.LoadModule(env_); + }); + ExitOnException(env_, [this]() { + napi_env env = env_; + task_runner_->DrainTaskQueue(); + bool microtasks_drained{}; + NODE_LITE_CALL(jsr_drain_microtasks(env, -1, µtasks_drained)); + OnExit(); + on_exit_callbacks_.clear(); + on_uncaughtException_callbacks_.clear(); + }); + } +} + +void NodeLiteRuntime::OnExit() { + for (NodeApiRef& callback_ref : on_exit_callbacks_) { + napi_value callback = NodeApi::GetReferenceValue(env_, callback_ref.get()); + NodeApi::CallFunction(env_, callback, {NodeApi::CreateUInt32(env_, 0)}); + } +} + +void NodeLiteRuntime::OnUncaughtException(napi_value error) { + bool shouldExit = true; + for (NodeApiRef& callback_ref : on_uncaughtException_callbacks_) { + napi_value callback = NodeApi::GetReferenceValue(env_, callback_ref.get()); + napi_value result = NodeApi::CallFunction( + env_, + callback, + {error, NodeApi::CreateString(env_, "uncaughtException")}); + // If at least one callback returns false, we do not exit. + // TODO: (vmoroz) Investigate the Node.js behavior in that case + // if (shouldExit && NodeApi::TypeOf(env_, result) == napi_boolean) { + // shouldExit = NodeApi::GetBoolean(env_, result); + //} + shouldExit = false; + } + + if (shouldExit) { + NodeLiteErrorHandler::ExitWithJSError(env_, error); + } +} + +/*static*/ NodeLiteRuntime* NodeLiteRuntime::GetRuntime(napi_env env) { + napi_value global = NodeApi::GetGlobal(env); + return static_cast(NodeApi::GetValueExternal( + env, NodeApi::GetProperty(env, global, "__NodeLiteRuntime__"))); +} + +void NodeLiteRuntime::DefineBuiltInModules() { + napi_env env = env_; + // Define "assert" module + { + fs::path assert_path = + fs::weakly_canonical(fs::path(js_root_) / "common" / "assert.js"); + std::string assert_path_str = assert_path.string(); + NODE_LITE_ASSERT(fs::exists(assert_path), + "Failed to find assert.js file: %s", + assert_path_str.c_str()); + node_js_modules_.try_emplace(assert_path_str, assert_path_str); + node_js_modules_.try_emplace(assert_path.replace_extension().string(), + assert_path_str); + node_js_modules_.try_emplace("assert", assert_path_str); + node_js_modules_.try_emplace("node:assert", assert_path_str); + } + + // Define "child_process" module + { + node_js_modules_.try_emplace("child_process", "child_process"); + node_js_modules_.try_emplace("node:child_process", "child_process"); + AddNativeModule("child_process", [this](napi_env env, napi_value exports) { + NodeApi::SetMethod( + env_, exports, "spawnSync", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 2, + "Expected at least 2 arguments, but got: %zu", + args.size()); + std::string command = NodeApi::ToStdString(env, args[0]); + std::vector command_args = + NodeApi::ToStdStringArray(env, args[1]); + ProcessResult call_result = SpawnSync(command, command_args); + napi_value result = NodeApi::CreateObject(env); + NodeApi::SetPropertyUInt32( + env, result, "status", call_result.status); + NodeApi::SetPropertyString( + env, result, "stderr", call_result.std_error); + NodeApi::SetPropertyString( + env, result, "stdout", call_result.std_output); + NodeApi::SetPropertyNull(env, result, "signal"); + return result; + }); + return exports; + }); + } + + // Define "fs" module + { + node_js_modules_.try_emplace("fs", "fs"); + node_js_modules_.try_emplace("node:fs", "fs"); + AddNativeModule("fs", [this](napi_env env, napi_value exports) { + NodeApi::SetMethod( + env_, exports, "existsSync", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + fs::path path = fs::path{NodeApi::ToStdString(env, args[0])}; + return NodeApi::GetBoolean(env, fs::exists(path)); + }); + NodeApi::SetMethod( + env_, + exports, + "readFileSync", + [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + fs::path path = fs::path{NodeApi::ToStdString(env, args[0])}; + return NodeApi::CreateString(env, ReadFileText(env, path)); + }); + return exports; + }); + } + + // Define "path" module + { + node_js_modules_.try_emplace("path", "path"); + node_js_modules_.try_emplace("node:path", "path"); + AddNativeModule("path", [this](napi_env env, napi_value exports) { + NodeApi::SetMethod( + env_, exports, "join", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 2, + "Expected at least 2 arguments, but got: %zu", + args.size()); + fs::path path = fs::path{NodeApi::ToStdString(env, args[0])}; + for (size_t i = 1; i < args.size(); ++i) { + path /= NodeApi::ToStdString(env, args[i]); + } + return NodeApi::CreateString(env, path.string()); + }); + return exports; + }); + } +} + +void NodeLiteRuntime::DefineGlobalFunctions() { + NodeApiHandleScope scope{env_}; + napi_value global = NodeApi::GetGlobal(env_); + + // Add global.global + NodeApi::SetProperty(env_, global, "global", global); + + // Add global.__NodeLiteRuntime__ + NodeApi::SetProperty( + env_, global, "__NodeLiteRuntime__", NodeApi::CreateExternal(env_, this)); + + // Remove the global.require defined by Hermes + NodeApi::DeleteProperty(env_, global, "require"); + + // global.gc() + NodeApi::SetMethod( + env_, global, "gc", [](napi_env env, span /*args*/) { + NODE_LITE_CALL(jsr_collect_garbage(env)); +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) + // JSC conservatively scans the active native stack. The value made + // unreachable immediately before global.gc() can therefore remain + // pinned by the callback/evaluation frames that requested the + // collection. Queue one follow-up collection after those frames have + // unwound; RunTestScript drains this queue before process "exit" + // callbacks run their mustCall checks. + // + // This is deliberately a NodeLite/JSC test-host behavior, not a + // production scheduling change. Other engines complete their explicit + // collection synchronously and do not need the second pass. + GetRuntime(env)->task_runner_->PostTask([env]() { + ExitOnException(env, [env]() { + NodeApiHandleScope scope{env}; + NODE_LITE_CALL(jsr_collect_garbage(env)); + }); + }); +#endif + return nullptr; + }); + + auto set_immediate_cb = [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, + "Expected at least 1 argument, but got: %zu", + args.size()); + std::shared_ptr callback_ref = + std::make_shared(MakeNodeApiRef(env, args[0])); + uint32_t task_id = GetRuntime(env)->task_runner_->PostTask( + [env, callback_ref = std::move(callback_ref)]() { + ExitOnException(env, [env, &callback_ref]() { + NodeApiHandleScope scope{env}; + napi_value callback = + NodeApi::GetReferenceValue(env, callback_ref->get()); + NodeApi::CallFunction(env, callback, {}); + }); + }); + return NodeApi::CreateUInt32(env, task_id); + }; + + // global.setImmediate() + NodeApi::SetMethod(env_, global, "setImmediate", set_immediate_cb); + + // global.setTimeout() + NodeApi::SetMethod(env_, global, "setTimeout", set_immediate_cb); + + // global.clearTimeout() + NodeApi::SetMethod( + env_, global, "clearTimeout", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, + "Expected at least 1 argument, but got: %zu", + args.size()); + uint32_t task_id = NodeApi::GetValueUInt32(env, args[0]); + GetRuntime(env)->task_runner_->RemoveTask(task_id); + return nullptr; + }); + + // global.process + { + napi_value process_obj = NodeApi::CreateObject(env_); + NodeApi::SetProperty(env_, global, "process", process_obj); + + // process.argv + NodeApi::SetPropertyStringArray(env_, process_obj, "argv", args_); + + // process.execPath + NodeApi::SetPropertyString(env_, process_obj, "execPath", args_[0]); + + // process.target_config follows the directory where CMake staged addons. + NodeApi::SetPropertyString(env_, process_obj, "target_config", NODE_API_BUILD_TYPE); + +// process.platform +#ifdef WIN32 + NodeApi::SetPropertyString(env_, process_obj, "platform", "win32"); +#else + // TODO: (vmoroz) Add support for other platforms. + NodeApi::SetPropertyString(env_, process_obj, "platform", "other"); +#endif + + // process.exit(exit_code) + NodeApi::SetMethod( + env_, process_obj, "exit", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, + "Expected at least 1 argument, but got: " + "%zu", + args.size()); + int32_t exit_code = NodeApi::GetValueInt32(env, args[0]); + exit(exit_code); + return nullptr; + }); + + // process.on('event_name', callback) + NodeApi::SetMethod( + env_, process_obj, "on", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 2, + "Expected at least 2 arguments, but got: %zu", + args.size()); + std::string event_name = NodeApi::ToStdString(env, args[0]); + if (event_name == "exit") { + NODE_LITE_ASSERT(NodeApi::TypeOf(env, args[1]) == napi_function, + "Expected function as second argument"); + GetRuntime(env)->on_exit_callbacks_.push_back( + MakeNodeApiRef(env, args[1])); + } else if (event_name == "uncaughtException") { + NODE_LITE_ASSERT(NodeApi::TypeOf(env, args[1]) == napi_function, + "Expected function as second argument"); + GetRuntime(env)->on_uncaughtException_callbacks_.push_back( + MakeNodeApiRef(env, args[1])); + } else { + NODE_LITE_ASSERT(false, + "Unsupported process event name: %s", + event_name.c_str()); + } + return nullptr; + }); + } + + // global.console + { + napi_value console_obj = NodeApi::CreateObject(env_); + NodeApi::SetProperty(env_, global, "console", console_obj); + + // console.log() + NodeApi::SetMethod( + env_, + console_obj, + "log", + [this](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + EmitConsoleOutput(message, false); + return nullptr; + }); + + // console.error() + NodeApi::SetMethod( + env_, + console_obj, + "error", + [this](napi_env env, span args) -> napi_value { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + EmitConsoleOutput(message, true); + return nullptr; + }); + } +} + +void NodeLiteRuntime::EmitConsoleOutput(const std::string& message, + bool is_error) { + const auto& callback = is_error ? callbacks_.stderr_callback + : callbacks_.stdout_callback; + if (callback) { + callback(message); + return; + } + + if (is_error) { + std::cerr << message << std::endl; + } else { + std::cout << message << std::endl; + } +} + +std::string NodeLiteRuntime::ProcessStack(std::string const& stack, + std::string const& assertMethod) { + // Split up the stack string into an array of stack frames + auto stackStream = std::istringstream(stack); + std::string stackFrame; + std::vector stackFrames; + while (std::getline(stackStream, stackFrame, '\n')) { + stackFrames.push_back(std::move(stackFrame)); + } + + // Remove first and last stack frames: one is the error message + // and another is the module root call. + if (!stackFrames.empty()) { + stackFrames.pop_back(); + } + if (!stackFrames.empty()) { + stackFrames.erase(stackFrames.begin()); + } + + std::string processedStack; + bool assertFuncFound = false; + std::string assertFuncPattern = assertMethod + " ("; + const std::regex locationRE("(\\w+):(\\d+)"); + std::smatch locationMatch; + // for (auto const& frame : stackFrames) { + // if (assertFuncFound) { + // std::string processedFrame; + // if (std::regex_search(frame, locationMatch, locationRE)) { + // if (auto const* scriptInfo = + // GetTestScriptInfo(locationMatch[1].str())) { + // int32_t cppLine = + // scriptInfo->line + std::stoi(locationMatch[2].str()) - 1; + // processedFrame = locationMatch.prefix().str() + + // UseSrcFilePath(scriptInfo->filePath.string()) + + // ':' + std::to_string(cppLine) + + // locationMatch.suffix().str(); + // } + // } + // processedStack += + // (!processedFrame.empty() ? processedFrame : frame) + '\n'; + // } else { + // auto pos = frame.find(assertFuncPattern); + // if (pos != std::string::npos) { + // if (frame[pos - 1] == '.' || frame[pos - 1] == ' ') { + // assertFuncFound = true; + // } + // } + // } + // } + + return processedStack; +} + +//============================================================================= +// NodeApiRefDeleter implementation +//============================================================================= + +NodeApiRefDeleter::NodeApiRefDeleter() noexcept = default; + +NodeApiRefDeleter::NodeApiRefDeleter(napi_env env) noexcept : env_(env) {} + +void NodeApiRefDeleter::operator()(napi_ref ref) noexcept { + if (ref == nullptr || env_ == nullptr) { + return; + } + napi_env env = env_; + NODE_LITE_CALL(napi_delete_reference(env, ref)); +} + +//============================================================================= +// NodeLiteTaskRunner implementation +//============================================================================= + +uint32_t NodeLiteTaskRunner::PostTask(std::function&& task) noexcept { + uint32_t task_id = next_task_id_++; + task_queue_.emplace_back(task_id, std::move(task)); + return task_id; +} + +void NodeLiteTaskRunner::RemoveTask(uint32_t task_id) noexcept { + task_queue_.remove_if( + [task_id](const std::pair>& entry) { + return entry.first == task_id; + }); +} + +void NodeLiteTaskRunner::DrainTaskQueue() noexcept { + while (!task_queue_.empty()) { + std::pair> task = + std::move(task_queue_.front()); + task_queue_.pop_front(); + task.second(); + } +} + +/*static*/ void NodeLiteTaskRunner::PostTaskCallback( + void* task_runner_data, + void* task_data, + jsr_task_run_cb task_run_cb, + jsr_data_delete_cb task_data_delete_cb, + void* deleter_data) { + NodeLiteTaskRunner* taskRunnerPtr = + static_cast*>(task_runner_data) + ->get(); + taskRunnerPtr->PostTask( + [task_run_cb, task_data, task_data_delete_cb, deleter_data]() { + if (task_run_cb != nullptr) { + task_run_cb(task_data); + } + if (task_data_delete_cb != nullptr) { + task_data_delete_cb(task_data, deleter_data); + } + }); +} + +/*static*/ void NodeLiteTaskRunner::DeleteCallback(void* data, + void* /*deleter_data*/) { + delete static_cast*>(data); +} + +//============================================================================= +// NodeApiHandleScope implementation +//============================================================================= + +NodeApiHandleScope::NodeApiHandleScope(napi_env env) noexcept : env_{env} { + NODE_LITE_CALL(napi_open_handle_scope(env, &scope_)); +} + +NodeApiHandleScope::~NodeApiHandleScope() noexcept { + // Destructors must not throw: this can run while a NodeLiteFatalError unwinds (a failing test in + // the in-process runner). Ignore the status rather than NODE_LITE_CALL, which would throw and + // std::terminate during unwinding. + static_cast(napi_close_handle_scope(env_, scope_)); +} + +//============================================================================= +// NodeApiEnvScope implementation +//============================================================================= + +NodeApiEnvScope::NodeApiEnvScope(napi_env env) noexcept : env_{env} { + NODE_LITE_CALL(jsr_open_napi_env_scope(env, &scope_)); +} + +NodeApiEnvScope ::~NodeApiEnvScope() noexcept { + if (env_ != nullptr) { + // Destructors must not throw (see NodeApiHandleScope). Ignore the status. + static_cast(jsr_close_napi_env_scope(env_, scope_)); + } +} + +NodeApiEnvScope::NodeApiEnvScope(NodeApiEnvScope&& other) noexcept + : env_{std::exchange(other.env_, nullptr)}, + scope_{std::exchange(other.scope_, nullptr)} {} + +NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { + if (this != &other) { + NodeApiEnvScope temp(std::move(*this)); + env_ = std::exchange(other.env_, nullptr); + scope_ = std::exchange(other.scope_, nullptr); + } + return *this; +} + +//============================================================================= +// NodeLiteErrorHandler implementation +//============================================================================= + +/*static*/ NodeLiteErrorHandler::Handler NodeLiteErrorHandler::SetHandler( + Handler handler) noexcept { + std::lock_guard lock{ErrorHandlerMutex()}; + Handler previous = GetHandler(); + if (handler) { + GetHandler() = std::move(handler); + } else { + GetHandler() = DefaultFatalErrorHandler; + } + return previous; +} + +/*static*/ NodeLiteErrorHandler::Handler& NodeLiteErrorHandler::GetHandler() + noexcept { + static Handler handler = DefaultFatalErrorHandler; + return handler; +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::HandleFatalError( + NodeLiteFatalErrorInfo info) { + Handler handler_copy; + { + std::lock_guard lock{ErrorHandlerMutex()}; + handler_copy = GetHandler(); + } + handler_copy(info); + std::terminate(); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnNodeApiFailed( + napi_env env, napi_status error_code) { + const char* errorMessage = "An exception is pending"; + if (NodeApi::IsExceptionPending(env)) { + error_code = napi_pending_exception; + } else { + const napi_extended_error_info* error_info{}; + napi_status status = napi_get_last_error_info(env, &error_info); + if (status != napi_ok) { + NodeLiteErrorHandler::ExitWithMessage( + "", [&](std::ostream& os) { os << "Failed to get last error info: " << status; }); + } + errorMessage = error_info->error_message; + } + throw NodeLiteException(error_code, errorMessage); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnAssertFailed( + napi_env env, char const* expr, char const* message) { + std::string error_message = FormatString("Assert failed: %s.", expr); + if (message != nullptr) { + std::string message_str{message}; + if (!message_str.empty()) { + error_message += " " + message_str; + } + } + napi_status error_code = NodeApi::IsExceptionPending(env) + ? napi_pending_exception + : napi_generic_failure; + + throw NodeLiteException(error_code, error_message.c_str()); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSError( + napi_env env, napi_value error) { + // TODO: protect from stack overflow + napi_valuetype error_value_type = NodeApi::TypeOf(env, error); + if (error_value_type == napi_object) { + std::string name = NodeApi::GetPropertyString(env, error, "name"); + if (name == "AssertionError") { + ExitWithJSAssertError(env, error); + } + std::string message = NodeApi::GetPropertyString(env, error, "message"); + std::string stack = NodeApi::GetPropertyString(env, error, "stack"); + ExitWithMessage("JavaScript error", [&](std::ostream& os) { + os << "Exception: " << name << '\n' + << " Message: " << message << '\n' + << "Callstack: " << '\n' + << stack; + }); + } else { + std::string message = NodeApi::CoerceToString(env, error); + ExitWithMessage("JavaScript error", + [&](std::ostream& os) { os << " Message: " << message; }); + } +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSAssertError( + napi_env env, napi_value error) { + std::string message = NodeApi::GetPropertyString(env, error, "message"); + std::string method = NodeApi::GetPropertyString(env, error, "method"); + std::string expected = NodeApi::GetPropertyString(env, error, "expected"); + std::string actual = NodeApi::GetPropertyString(env, error, "actual"); + std::string source_file = + NodeApi::GetPropertyString(env, error, "sourceFile"); + int32_t source_line = NodeApi::GetPropertyInt32(env, error, "sourceLine"); + std::string error_stack = + NodeApi::GetPropertyString(env, error, "errorStack"); + if (error_stack.empty()) { + error_stack = NodeApi::GetPropertyString(env, error, "stack"); + } + std::string method_name = "assert." + method; + std::stringstream error_details; + if (method_name != "assert.fail") { + error_details << " Expected: " << expected << '\n' + << " Actual: " << actual << '\n'; + } + + ExitWithMessage("JavaScript assertion error", [&](std::ostream& os) { + os << "Exception: " + << "AssertionError" << '\n' + << " Method: " << method_name << '\n' + << " Message: " << message << '\n' + << error_details.str(/*a filler for formatting*/) + << "Callstack: " << '\n' + << error_stack; + }); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithMessage( + const std::string& message, + std::function get_error_details, + int exit_code) { + std::ostringstream details_stream; + if (get_error_details) { + get_error_details(details_stream); + } + std::string details = details_stream.str(); + + HandleFatalError(NodeLiteFatalErrorInfo{ + .message = message, + .details = details, + .exit_code = exit_code, + }); +} + +//============================================================================= +// NodeApi implementation +//============================================================================= + +/*static*/ bool NodeApi::IsExceptionPending(napi_env env) { + bool result{}; + NODE_LITE_CALL(napi_is_exception_pending(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetAndClearLastException(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_and_clear_last_exception(env, &result)); + return result; +} + +/*static*/ void NodeApi::ThrowError(napi_env env, napi_value error) { + NODE_LITE_CALL(napi_throw(env, error)); +} + +/*static*/ void NodeApi::ThrowError(napi_env env, const char* error_message) { + NODE_LITE_CALL(napi_throw_error(env, "", error_message)); +} + +/*static*/ napi_value NodeApi::GetNull(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_null(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetUndefined(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_undefined(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetGlobal(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_global(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetBoolean(napi_env env, bool value) { + napi_value result{}; + NODE_LITE_CALL(napi_get_boolean(env, value, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetReferenceValue(napi_env env, napi_ref ref) { + napi_value result{}; + NODE_LITE_CALL(napi_get_reference_value(env, ref, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateUInt32(napi_env env, std::uint32_t value) { + napi_value result{}; + NODE_LITE_CALL(napi_create_uint32(env, value, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateString(napi_env env, + std::string_view value) { + napi_value result{}; + NODE_LITE_CALL( + napi_create_string_utf8(env, value.data(), value.size(), &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateStringArray( + napi_env env, std::vector const& value) { + napi_value result{}; + NODE_LITE_CALL(napi_create_array(env, &result)); + + uint32_t index = 0; + for (const std::string& item : value) { + NODE_LITE_CALL( + napi_set_element(env, result, index++, CreateString(env, item))); + } + return result; +} + +/*static*/ napi_value NodeApi::CreateObject(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_create_object(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateExternal(napi_env env, void* data) { + napi_value result{}; + NODE_LITE_CALL(napi_create_external(env, data, nullptr, nullptr, &result)); + return result; +} + +/*static*/ int32_t NodeApi::GetValueInt32(napi_env env, napi_value value) { + int32_t result{}; + NODE_LITE_CALL(napi_get_value_int32(env, value, &result)); + return result; +} + +/*static*/ uint32_t NodeApi::GetValueUInt32(napi_env env, napi_value value) { + uint32_t result{}; + NODE_LITE_CALL(napi_get_value_uint32(env, value, &result)); + return result; +} + +/*static*/ void* NodeApi::GetValueExternal(napi_env env, napi_value value) { + void* result{}; + NODE_LITE_CALL(napi_get_value_external(env, value, &result)); + return result; +} + +/*static*/ bool NodeApi::HasProperty(napi_env env, + napi_value obj, + std::string_view utf8_name) { + bool result{}; + NODE_LITE_CALL(napi_has_named_property(env, obj, utf8_name.data(), &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name) { + napi_value result{}; + NODE_LITE_CALL(napi_get_named_property(env, obj, utf8_name.data(), &result)); + return result; +} + +/*static*/ std::string NodeApi::GetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name) { + if (HasProperty(env, obj, utf8_name)) { + return ToStdString(env, GetProperty(env, obj, utf8_name)); + } else { + return ""; + } +} + +/*static*/ int32_t NodeApi::GetPropertyInt32(napi_env env, + napi_value obj, + std::string_view utf8_name) { + return GetValueInt32(env, GetProperty(env, obj, utf8_name)); +} + +/*static*/ std::string NodeApi::CoerceToString(napi_env env, napi_value value) { + napi_value str_value; + NODE_LITE_CALL(napi_coerce_to_string(env, value, &str_value)); + return ToStdString(env, str_value); +} + +/*static*/ void NodeApi::SetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name, + napi_value value) { + NODE_LITE_CALL(napi_set_named_property(env, obj, utf8_name.data(), value)); +} + +/*static*/ void NodeApi::SetPropertyUInt32(napi_env env, + napi_value obj, + std::string_view utf8_name, + uint32_t value) { + SetProperty(env, obj, utf8_name, CreateUInt32(env, value)); +} + +/*static*/ void NodeApi::SetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name, + std::string_view value) { + SetProperty(env, obj, utf8_name, CreateString(env, value)); +} + +/*static*/ void NodeApi::SetPropertyStringArray( + napi_env env, + napi_value obj, + std::string_view utf8_name, + std::vector const& value) { + SetProperty(env, obj, utf8_name, CreateStringArray(env, value)); +} + +/*static*/ void NodeApi::SetPropertyNull(napi_env env, + napi_value obj, + std::string_view utf8_name) { + SetProperty(env, obj, utf8_name, GetNull(env)); +} + +/*static*/ void NodeApi::SetMethod(napi_env env, + napi_value obj, + std::string_view utf8_name, + NodeApiCallback cb) { + NodeApi::SetProperty(env, obj, utf8_name, CreateFunction(env, utf8_name, cb)); +} + +/*static*/ bool NodeApi::DeleteProperty(napi_env env, + napi_value obj, + std::string_view utf8_name) { + bool result{}; + NODE_LITE_CALL( + napi_delete_property(env, obj, CreateString(env, utf8_name), &result)); + return result; +} + +/*static*/ std::string NodeApi::ToStdString(napi_env env, napi_value value) { + size_t str_size{}; + NODE_LITE_CALL(napi_get_value_string_utf8(env, value, nullptr, 0, &str_size)); + std::string result(str_size, '\0'); + NODE_LITE_CALL(napi_get_value_string_utf8( + env, value, &result[0], str_size + 1, nullptr)); + return result; +} + +/*static*/ std::vector NodeApi::ToStdStringArray( + napi_env env, napi_value value) { + std::vector result; + bool is_array; + NODE_LITE_CALL(napi_is_array(env, value, &is_array)); + if (is_array) { + uint32_t length; + NODE_LITE_CALL(napi_get_array_length(env, value, &length)); + result.reserve(length); + for (uint32_t i = 0; i < length; i++) { + napi_value element; + NODE_LITE_CALL(napi_get_element(env, value, i, &element)); + result.push_back(CoerceToString(env, element)); + } + } + return result; +} + +/*static*/ napi_value NodeApi::RunScript(napi_env env, napi_value script) { + napi_value result{}; + NODE_LITE_CALL(napi_run_script(env, script, nullptr, &result)); + return result; +} + +/*static*/ napi_value NodeApi::RunScript(napi_env env, + const std::string& code, + char const* source_url) { + napi_value script = NodeApi::CreateString(env, code); + + if (source_url != nullptr) { + napi_value result{}; + NODE_LITE_CALL(jsr_run_script(env, script, source_url, &result)); + return result; + } + return RunScript(env, script); +} + +/*static*/ napi_valuetype NodeApi::TypeOf(napi_env env, napi_value value) { + napi_valuetype result{}; + NODE_LITE_CALL(napi_typeof(env, value, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CallFunction(napi_env env, + napi_value func, + std::initializer_list args) { + napi_value result{}; + NODE_LITE_CALL(napi_call_function( + env, GetUndefined(env), func, args.size(), args.begin(), &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateFunction(napi_env env, + std::string_view name, + NodeApiCallback cb) { + napi_value result{}; + auto callback = std::make_unique(std::move(cb)); + NODE_LITE_CALL(napi_create_function( + env, + name.data(), + name.size(), + [](napi_env env, napi_callback_info info) { + napi_value result{}; + ThrowJSErrorOnException(env, [env, info, &result]() { + NodeApiCallbackInfo callback_info{env, info}; + NodeApiCallback* cb = + static_cast(callback_info.data()); + result = (*cb)(env, callback_info.args()); + }); + return result; + }, + callback.get(), + &result)); + NODE_LITE_CALL(napi_add_finalizer( + env, + result, + callback.get(), + [](napi_env, void* data, void*) { + delete static_cast(data); + }, + nullptr, + nullptr)); + callback.release(); + return result; +} + +ProcessResult RunNodeLiteScript(const std::filesystem::path& js_root, + const std::filesystem::path& script_path, + NodeLiteRuntime::Callbacks callbacks) { + ProcessResult result{}; + std::ostringstream stdout_stream; + std::ostringstream stderr_stream; + + NodeLiteRuntime::Callbacks effective_callbacks; + auto stdout_cb = callbacks.stdout_callback; + auto stderr_cb = callbacks.stderr_callback; + effective_callbacks.stdout_callback = + [stdout_cb, &stdout_stream](const std::string& message) { + if (stdout_cb) { + stdout_cb(message); + } + stdout_stream << message << '\n'; + }; + effective_callbacks.stderr_callback = + [stderr_cb, &stderr_stream](const std::string& message) { + if (stderr_cb) { + stderr_cb(message); + } + stderr_stream << message << '\n'; + }; + + auto fatal_handler = [&result](const NodeLiteFatalErrorInfo& info) { + result.status = info.exit_code; + if (!info.message.empty()) { + result.std_error = info.message; + } + if (!info.details.empty()) { + if (!result.std_error.empty()) { + result.std_error += '\n'; + } + result.std_error += info.details; + } + // If this handler is reached while another exception is already unwinding (e.g. invoked from a + // teardown destructor after a failing test in the in-process runner), throwing again would be a + // second in-flight exception -> std::terminate (the "double exception" abort). The result is + // already captured above, so just return and let the original exception reach the catch below. + if (std::uncaught_exceptions() > 0) { + return; + } + throw NodeLiteFatalError(info); + }; + + NodeLiteErrorHandler::Handler previous_handler = + NodeLiteErrorHandler::SetHandler(fatal_handler); + + try { + auto task_runner = std::make_shared(); + std::vector args{"node_lite", script_path.string()}; + auto runtime = NodeLiteRuntime::Create(std::move(task_runner), + js_root.string(), + std::move(args), + std::move(effective_callbacks)); + runtime->RunTestScript(script_path.string()); + result.status = 0; + } catch (const NodeLiteFatalError&) { + // Fatal error captured in result + } catch (const std::exception& e) { + NodeLiteErrorHandler::SetHandler(previous_handler); + result.status = -1; + result.std_error = e.what(); + return result; + } catch (...) { + NodeLiteErrorHandler::SetHandler(previous_handler); + result.status = -1; + result.std_error = "Unknown error"; + return result; + } + + NodeLiteErrorHandler::SetHandler(previous_handler); + + result.std_output = stdout_stream.str(); + if (!result.std_output.empty() && result.std_output.back() == '\n') { + result.std_output.pop_back(); + } + + std::string stderr_logs = stderr_stream.str(); + if (!stderr_logs.empty() && stderr_logs.back() == '\n') { + stderr_logs.pop_back(); + } + if (!stderr_logs.empty()) { + if (!result.std_error.empty()) { + result.std_error += '\n'; + } + result.std_error += stderr_logs; + } + + return result; +} + +} // namespace node_api_tests + +int main(int argc, char* argv[]) { + node_api_tests::NodeLiteRuntime::Run( + std::vector(argv, argv + argc)); +} diff --git a/Tests/NodeApi/node_lite.h b/Tests/NodeApi/node_lite.h new file mode 100644 index 00000000..7235a34f --- /dev/null +++ b/Tests/NodeApi/node_lite.h @@ -0,0 +1,408 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// A simple Node.js-like runtime that runs Node-API test scripts. + +#ifndef NODE_API_TEST_NODE_LITE_H +#define NODE_API_TEST_NODE_LITE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "child_process.h" +#include "compat.h" +#include "string_utils.h" + +#define NAPI_EXPERIMENTAL +#include "js_runtime_api.h" + +#define NODE_LITE_CALL(expr) \ + do { \ + napi_status temp_status__ = (expr); \ + if (temp_status__ != napi_status::napi_ok) { \ + NodeLiteErrorHandler::OnNodeApiFailed(env, temp_status__); \ + } \ + } while (false) + +#define NODE_LITE_ASSERT(expr, ...) \ + do { \ + if (!(expr)) { \ + NodeLiteErrorHandler::OnAssertFailed( \ + env, #expr, FormatString("" __VA_ARGS__).c_str()); \ + } \ + } while (false) + +namespace node_api_tests { + +// Forward declarations +class NodeLiteModule; +class NodeLiteRuntime; +class NodeLiteTaskRunner; +class NodeApiRefDeleter; +class NodeApiHandleScope; +class NodeApiEnvScope; +class NodeLiteErrorHandler; + +struct IEnvHolder { + virtual ~IEnvHolder() {} + virtual napi_env getEnv() = 0; +}; + +class NodeLiteTaskRunner { + public: + using QueueEntry = std::pair>; + + uint32_t PostTask(std::function&& task) noexcept; + void RemoveTask(uint32_t task_id) noexcept; + void DrainTaskQueue() noexcept; + + static void PostTaskCallback(void* task_runner_data, + void* task_data, + jsr_task_run_cb task_run_cb, + jsr_data_delete_cb task_data_delete_cb, + void* deleter_data); + + static void DeleteCallback(void* data, void* /*deleter_data*/); + + private: + std::list task_queue_; + uint32_t next_task_id_{1}; +}; + +class NodeLiteException : public std::runtime_error { + public: + explicit NodeLiteException(napi_status error_status, + const char* message) noexcept + : runtime_error{message}, error_status_{error_status} {} + + napi_status error_status() const noexcept { return error_status_; } + + private: + napi_status error_status_; +}; + +struct NodeLiteFatalErrorInfo { + std::string message; + std::string details; + int exit_code{1}; +}; + +class NodeLiteFatalError : public std::runtime_error { + public: + explicit NodeLiteFatalError(NodeLiteFatalErrorInfo info) + : std::runtime_error{info.message.c_str()}, info_{std::move(info)} {} + + const NodeLiteFatalErrorInfo& info() const noexcept { return info_; } + + private: + NodeLiteFatalErrorInfo info_; +}; + +class NodeLiteErrorHandler { + public: + using Handler = std::function; + + static Handler SetHandler(Handler handler) noexcept; + + [[noreturn]] static void OnNodeApiFailed(napi_env env, + napi_status error_status); + + [[noreturn]] static void OnAssertFailed(napi_env env, + char const* expr, + char const* message); + + // NOTE: not noexcept. With the default fatal handler these call std::exit, but the in-process + // runner installs a handler that *throws* NodeLiteFatalError (to be caught as a ProcessResult). + // A throw crossing a noexcept boundary is an immediate std::terminate, so these must allow it. + [[noreturn]] static void ExitWithJSError(napi_env env, napi_value error); + + [[noreturn]] static void ExitWithJSAssertError(napi_env env, napi_value error); + + [[noreturn]] static void ExitWithMessage( + const std::string& message, + std::function get_error_details = nullptr, + int exit_code = 1); + + private: + static Handler& GetHandler() noexcept; + [[noreturn]] static void HandleFatalError(NodeLiteFatalErrorInfo info); +}; + +// Define NodeApiRef "smart pointer" for napi_ref as unique_ptr with a custom +// deleter. +class NodeApiRefDeleter { + public: + NodeApiRefDeleter() noexcept; + explicit NodeApiRefDeleter(napi_env env) noexcept; + + void operator()(napi_ref ref) noexcept; + + private: + napi_env env_{}; +}; + +using NodeApiRef = std::unique_ptr; + +class NodeApiHandleScope { + public: + explicit NodeApiHandleScope(napi_env env) noexcept; + ~NodeApiHandleScope() noexcept; + + private: + napi_env env_{}; + napi_handle_scope scope_{}; +}; + +class NodeApiEnvScope { + public: + explicit NodeApiEnvScope(napi_env env) noexcept; + + ~NodeApiEnvScope() noexcept; + + NodeApiEnvScope(NodeApiEnvScope&& other) noexcept; + NodeApiEnvScope& operator=(NodeApiEnvScope&& other) noexcept; + + NodeApiEnvScope(const NodeApiEnvScope&) = delete; + NodeApiEnvScope& operator=(const NodeApiEnvScope&) = delete; + + private: + napi_env env_{}; + jsr_napi_env_scope scope_{}; +}; + +class NodeLiteModule { + public: + using InitModuleCallback = + std::function; + + explicit NodeLiteModule(std::filesystem::path module_path) noexcept; + explicit NodeLiteModule(std::filesystem::path module_path, + InitModuleCallback init_module) noexcept; + + napi_value LoadModule(napi_env env); + + NodeLiteModule(const NodeLiteModule&) = delete; + NodeLiteModule& operator=(const NodeLiteModule&) = delete; + + private: + napi_value LoadScriptModule(napi_env env); + napi_value LoadNativeModule(napi_env env); + std::string ReadModuleFileText(napi_env env); + + private: + enum class State { + kNotLoaded, + kLoading, + kLoaded, + }; + + private: + State state_{State::kNotLoaded}; + std::filesystem::path module_path_; + InitModuleCallback init_module_; + NodeApiRef exports_; +}; + +// The Node.js-like runtime that is enough to run Node-API tests. +class NodeLiteRuntime { + struct PrivateTag {}; + + public: + struct Callbacks { + std::function stdout_callback{}; + std::function stderr_callback{}; + }; + + static std::unique_ptr Create( + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks); + + explicit NodeLiteRuntime(PrivateTag tag, + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks); + + static void Run(std::vector args); + + NodeLiteModule& ResolveModule(const std::string& parent_module_path, + const std::string& module_path); + + std::filesystem::path ResolveModulePath(const std::string& parent_module_path, + const std::string& module_path); + + void RunTestScript(const std::string& script_path); + + void AddNativeModule( + const std::string& module_name, + std::function initModule); + + void HandleUnhandledPromiseRejections(); + void OnExit(); + void OnUncaughtException(napi_value error); + + std::string ProcessStack(std::string const& stack, + std::string const& assertMethod); + + static NodeLiteRuntime* GetRuntime(napi_env env); + + private: + void Initialize(); + void DefineGlobalFunctions(); + void DefineBuiltInModules(); + void EmitConsoleOutput(const std::string& message, bool is_error); + + private: + std::shared_ptr task_runner_; + std::string js_root_; + std::vector args_; + Callbacks callbacks_{}; + std::unique_ptr env_holder_; + napi_env env_{}; + std::unordered_map> + registered_modules_; + std::unordered_map node_js_modules_; + std::vector on_exit_callbacks_; + std::vector on_uncaughtException_callbacks_; +}; + +class NodeLitePlatform { + public: + static void* LoadFunction(napi_env env, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept; +}; + +using NodeApiCallback = + std::function args)>; + +// Wraps up Node-API function calls. +// To simplify usage patterns it throws NodeApiException on errors. +class NodeApi { + public: + static bool IsExceptionPending(napi_env env); + + static napi_value GetAndClearLastException(napi_env env); + + static void ThrowError(napi_env env, napi_value error); + + static void ThrowError(napi_env env, const char* error_message); + + static napi_value GetNull(napi_env env); + + static napi_value GetUndefined(napi_env env); + + static napi_value GetGlobal(napi_env env); + + static napi_value GetBoolean(napi_env env, bool value); + + static napi_value GetReferenceValue(napi_env env, napi_ref ref); + + static napi_value CreateUInt32(napi_env env, std::uint32_t value); + + static napi_value CreateString(napi_env env, std::string_view value); + + static napi_value CreateStringArray(napi_env env, + std::vector const& value); + + static napi_value CreateObject(napi_env env); + + static napi_value CreateExternal(napi_env env, void* data); + + static int32_t GetValueInt32(napi_env env, napi_value value); + + static uint32_t GetValueUInt32(napi_env env, napi_value value); + + static void* GetValueExternal(napi_env env, napi_value value); + + static bool HasProperty(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static napi_value GetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static std::string GetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static int32_t GetPropertyInt32(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static void SetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name, + napi_value value); + + static void SetPropertyUInt32(napi_env env, + napi_value obj, + std::string_view utf8_name, + uint32_t value); + + static void SetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name, + std::string_view value); + + static void SetPropertyStringArray(napi_env env, + napi_value obj, + std::string_view utf8_name, + std::vector const& value); + + static void SetPropertyNull(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static void SetMethod(napi_env env, + napi_value obj, + std::string_view utf8_name, + NodeApiCallback cb); + + static bool DeleteProperty(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static std::string CoerceToString(napi_env env, napi_value value); + + static std::string ToStdString(napi_env env, napi_value value); + + static std::vector ToStdStringArray(napi_env env, + napi_value value); + + static napi_value RunScript(napi_env env, napi_value script); + + static napi_value RunScript(napi_env env, + const std::string& code, + char const* source_url); + + static napi_valuetype TypeOf(napi_env env, napi_value value); + + static napi_value CallFunction(napi_env env, + napi_value func, + std::initializer_list args); + + static napi_value CreateFunction(napi_env env, + std::string_view name, + NodeApiCallback cb); +}; + +ProcessResult RunNodeLiteScript( + const std::filesystem::path& js_root, + const std::filesystem::path& script_path, + NodeLiteRuntime::Callbacks callbacks = NodeLiteRuntime::Callbacks{}); + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_NODE_LITE_H diff --git a/Tests/NodeApi/node_lite_android.cpp b/Tests/NodeApi/node_lite_android.cpp new file mode 100644 index 00000000..0f34ac14 --- /dev/null +++ b/Tests/NodeApi/node_lite_android.cpp @@ -0,0 +1,27 @@ +#include "node_lite.h" + +#include + +namespace node_api_tests { + +/*static*/ void* NodeLitePlatform::LoadFunction( + napi_env /*env*/, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept +{ + // On Android the conformance addons are packaged as lib.so in the app's nativeLibraryDir -- + // the only location a native library may be dlopen'd from on API 29+. The resolved lib_path points + // at a (non-existent) .node under the copied test tree, so load by soname and let the dynamic + // linker resolve it from nativeLibraryDir. The addon's napi_* imports resolve from libnapi.so -- a + // DT_NEEDED of both the addon and the host -- so RTLD_NOW binds them at load time. + std::string soname = "lib" + lib_path.stem().string() + ".so"; + void* handle = dlopen(soname.c_str(), RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) + { + return nullptr; + } + + return dlsym(handle, function_name.c_str()); +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/node_lite_jsruntimehost.cpp b/Tests/NodeApi/node_lite_jsruntimehost.cpp new file mode 100644 index 00000000..92e37f2d --- /dev/null +++ b/Tests/NodeApi/node_lite_jsruntimehost.cpp @@ -0,0 +1,224 @@ +#include "node_lite.h" + +#include +#include + +#include +#include +#include + +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) +#include +#include "js_native_api_javascriptcore.h" +#elif defined(JSR_NAPI_ENGINE_V8) +#include +#include "js_native_api_v8.h" +#elif defined(JSR_NAPI_ENGINE_QUICKJS) +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshorten-64-to-32" +#endif +#include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif +#endif + +namespace node_api_tests { + +namespace { + +class JsRuntimeHostEnvHolder : public IEnvHolder { + public: + JsRuntimeHostEnvHolder( + std::shared_ptr /*taskRunner*/, + std::function onUnhandledError) + : onUnhandledError_(std::move(onUnhandledError)) { +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) + context_ = JSGlobalContextCreateInGroup(nullptr, nullptr); + env_ = Napi::Attach(context_); +#elif defined(JSR_NAPI_ENGINE_V8) + // V8's platform is process-global and is already initialized by JsRuntimeHost -- the host + // AppRuntime that UnitTestsJNI links and that runs (via the regular V8 unit tests) before these + // in-process Node-API tests. Initializing it a second time aborts V8 with "Wrong initialization + // order", so reuse the host's platform and only create our own isolate/context below. + allocator_.reset(v8::ArrayBuffer::Allocator::NewDefaultAllocator()); + v8::Isolate::CreateParams create_params; + create_params.array_buffer_allocator = allocator_.get(); + isolate_ = v8::Isolate::New(create_params); + + // The host runs its own V8 isolate in this process, so V8 enforces multi-isolate locking. + // Hold a Locker + Isolate::Scope for this holder's entire lifetime so all subsequent + // Node-API/V8 access on this thread (running the test script, native callbacks, teardown) is + // properly locked and scoped to our isolate -- otherwise V8 aborts with "Entering the V8 API + // without proper locking in place". + locker_ = std::make_unique(isolate_); + isolate_scope_ = std::make_unique(isolate_); + + v8::HandleScope handle_scope(isolate_); + v8::Local context = v8::Context::New(isolate_); + context_.Reset(isolate_, context); + v8::Context::Scope context_scope(context); + env_ = Napi::Attach(context); +#elif defined(JSR_NAPI_ENGINE_QUICKJS) + runtime_ = JS_NewRuntime(); + if (runtime_ == nullptr) { + throw std::runtime_error("Unable to create QuickJS runtime"); + } + context_ = JS_NewContext(runtime_); + if (context_ == nullptr) { + JS_FreeRuntime(runtime_); + runtime_ = nullptr; + throw std::runtime_error("Unable to create QuickJS context"); + } + env_ = Napi::Attach(context_); +#elif defined(JSR_NAPI_ENGINE_HERMES) + env_ = Napi::Attach(); +#else + (void)onUnhandledError_; + throw std::runtime_error( + "node_lite is not implemented for the selected JavaScript engine."); +#endif + } + + ~JsRuntimeHostEnvHolder() override { +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) + if (env_ != nullptr) { + Napi::Env napiEnv{env_}; + + if (onUnhandledError_) { + bool hasPending = false; + if (napi_is_exception_pending(env_, &hasPending) == napi_ok && + hasPending) { + napi_value error{}; + if (napi_get_and_clear_last_exception(env_, &error) == napi_ok) { + onUnhandledError_(env_, error); + } + } + } + + if (context_ != nullptr) { + JSGlobalContextRelease(context_); + context_ = nullptr; + } + + Napi::Detach(napiEnv); + env_ = nullptr; + } else if (context_ != nullptr) { + JSGlobalContextRelease(context_); + context_ = nullptr; + } +#elif defined(JSR_NAPI_ENGINE_V8) + if (env_ != nullptr && isolate_ != nullptr) { + // Still locked + isolate-scoped on this thread via locker_/isolate_scope_ (held members). + v8::HandleScope handle_scope(isolate_); + v8::Local context = context_.Get(isolate_); + v8::Context::Scope context_scope(context); + + if (onUnhandledError_) { + bool hasPending = false; + if (napi_is_exception_pending(env_, &hasPending) == napi_ok && hasPending) { + napi_value error{}; + if (napi_get_and_clear_last_exception(env_, &error) == napi_ok) { + // onUnhandledError_ may invoke the in-process fatal handler, which throws + // NodeLiteFatalError. A destructor must not let that escape (std::terminate). Real + // test errors are reported synchronously via ExitOnException; this is a best-effort + // fallback for anything still pending at teardown. + try { + onUnhandledError_(env_, error); + } catch (...) { + } + } + } + } + + Napi::Env napiEnv{env_}; + Napi::Detach(napiEnv); + env_ = nullptr; + } + + context_.Reset(); + + isolate_scope_.reset(); + locker_.reset(); + + if (isolate_ != nullptr) { + isolate_->Dispose(); + isolate_ = nullptr; + } + + allocator_.reset(); +#elif defined(JSR_NAPI_ENGINE_QUICKJS) + if (env_ != nullptr) { + if (onUnhandledError_) { + bool hasPending = false; + if (napi_is_exception_pending(env_, &hasPending) == napi_ok && hasPending) { + napi_value error{}; + if (napi_get_and_clear_last_exception(env_, &error) == napi_ok) { + try { + onUnhandledError_(env_, error); + } catch (...) { + } + } + } + } + Napi::Detach(Napi::Env{env_}); + env_ = nullptr; + } + if (context_ != nullptr) { + JS_FreeContext(context_); + context_ = nullptr; + } + if (runtime_ != nullptr) { + JS_FreeRuntime(runtime_); + runtime_ = nullptr; + } +#elif defined(JSR_NAPI_ENGINE_HERMES) + if (env_ != nullptr) { + if (onUnhandledError_) { + bool hasPending = false; + if (napi_is_exception_pending(env_, &hasPending) == napi_ok && hasPending) { + napi_value error{}; + if (napi_get_and_clear_last_exception(env_, &error) == napi_ok) { + try { + onUnhandledError_(env_, error); + } catch (...) { + } + } + } + } + Napi::Detach(Napi::Env{env_}); + env_ = nullptr; + } +#endif + } + + napi_env getEnv() override { return env_; } + + private: +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) + JSGlobalContextRef context_{}; +#elif defined(JSR_NAPI_ENGINE_V8) + v8::Isolate* isolate_{nullptr}; + std::unique_ptr locker_{}; + std::unique_ptr isolate_scope_{}; + v8::Global context_; + std::unique_ptr allocator_{}; +#elif defined(JSR_NAPI_ENGINE_QUICKJS) + JSRuntime* runtime_{}; + JSContext* context_{}; +#endif + napi_env env_{}; + std::function onUnhandledError_{}; +}; + +} // namespace + +std::unique_ptr CreateEnvHolder( + std::shared_ptr taskRunner, + std::function onUnhandledError) { + return std::make_unique( + std::move(taskRunner), std::move(onUnhandledError)); +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/node_lite_posix.cpp b/Tests/NodeApi/node_lite_posix.cpp new file mode 100644 index 00000000..c2968514 --- /dev/null +++ b/Tests/NodeApi/node_lite_posix.cpp @@ -0,0 +1,43 @@ +#include +#if defined(__ANDROID__) +#include +#endif +#include "node_lite.h" + +namespace node_api_tests { + +/*static*/ void* NodeLitePlatform::LoadFunction( + napi_env env, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept { +#if defined(__ANDROID__) && (__ANDROID_API__ < 29) + void* library_handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); + if (library_handle == nullptr) { + return nullptr; + } + + return dlsym(library_handle, function_name.c_str()); +#else + void* library_handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); + if (library_handle == nullptr) { + const char* error_message = dlerror(); + NODE_LITE_ASSERT(false, + "Failed to load dynamic library: %s. Error: %s", + lib_path.c_str(), + error_message != nullptr ? error_message : "Unknown error"); + return nullptr; + } + + dlerror(); // Clear any existing error state before dlsym. + void* symbol = dlsym(library_handle, function_name.c_str()); + const char* error_message = dlerror(); + NODE_LITE_ASSERT(error_message == nullptr, + "Failed to resolve symbol: %s in %s. Error: %s", + function_name.c_str(), + lib_path.c_str(), + error_message != nullptr ? error_message : "Unknown error"); + return symbol; +#endif +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/node_lite_windows.cpp b/Tests/NodeApi/node_lite_windows.cpp new file mode 100644 index 00000000..164487df --- /dev/null +++ b/Tests/NodeApi/node_lite_windows.cpp @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include "node_lite.h" + +namespace node_api_tests { + +//============================================================================= +// NodeLitePlatform implementation +//============================================================================= + +/*static*/ void* NodeLitePlatform::LoadFunction( + napi_env env, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept { + HMODULE dll_module = ::LoadLibraryA(lib_path.string().c_str()); + const DWORD load_error = ::GetLastError(); + NODE_LITE_ASSERT(dll_module != NULL, + "Failed to load DLL: %s. Error code: %lu", + lib_path.string().c_str(), + load_error); + return ::GetProcAddress(dll_module, function_name.c_str()); +} +} // namespace node_api_tests diff --git a/Tests/NodeApi/string_utils.cpp b/Tests/NodeApi/string_utils.cpp new file mode 100644 index 00000000..583f3e15 --- /dev/null +++ b/Tests/NodeApi/string_utils.cpp @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "string_utils.h" +#include + +namespace node_api_tests { + +std::string FormatString(const char *format, ...) noexcept { + va_list args1; + va_start(args1, format); + va_list args2; + va_copy(args2, args1); + std::string result = + std::string(std::vsnprintf(nullptr, 0, format, args1), '\0'); + va_end(args1); + std::vsnprintf(&result[0], result.size() + 1, format, args2); + va_end(args2); + return result; +} + +std::string ReplaceAll( + std::string str, + std::string_view from, + std::string_view to) noexcept { + std::string result = std::move(str); + if (from.empty()) + return result; + size_t start_pos = 0; + while ((start_pos = result.find(from, start_pos)) != std::string::npos) { + result.replace(start_pos, from.length(), to); + start_pos += to.length(); // In case if 'to' contains 'from', like + // replacing 'x' with 'yx' + } + return result; +} + +} // namespace node_api_tests \ No newline at end of file diff --git a/Tests/NodeApi/string_utils.h b/Tests/NodeApi/string_utils.h new file mode 100644 index 00000000..0d290208 --- /dev/null +++ b/Tests/NodeApi/string_utils.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef NODE_API_TEST_STRING_UTILS_H +#define NODE_API_TEST_STRING_UTILS_H + +#include +#include + +namespace node_api_tests { + +std::string FormatString(const char *format, ...) noexcept; + +std::string ReplaceAll( + std::string str, + std::string_view from, + std::string_view to) noexcept; + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_STRING_UTILS_H \ No newline at end of file diff --git a/Tests/NodeApi/test/.clang-format b/Tests/NodeApi/test/.clang-format new file mode 100644 index 00000000..b3fd9613 --- /dev/null +++ b/Tests/NodeApi/test/.clang-format @@ -0,0 +1,111 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 8 +UseTab: Never diff --git a/Tests/NodeApi/test/CMakeLists.txt b/Tests/NodeApi/test/CMakeLists.txt new file mode 100644 index 00000000..ec6256b4 --- /dev/null +++ b/Tests/NodeApi/test/CMakeLists.txt @@ -0,0 +1,114 @@ +if(WIN32) + # "npx" interrupts current shell script execution without the "call" + set(npx cmd /c npx) + set(npm cmd /c npm) +else() + set(npx "npx") + set(npm "npm") +endif() + +# copy JS Tools package files +add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/babel.config.js + ${CMAKE_CURRENT_BINARY_DIR}/package.json + ${CMAKE_CURRENT_BINARY_DIR}/package-lock.json + ${CMAKE_CURRENT_BINARY_DIR}/postinstall.mjs + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/babel.config.js + ${CMAKE_CURRENT_SOURCE_DIR}/package.json + ${CMAKE_CURRENT_SOURCE_DIR}/package-lock.json + ${CMAKE_CURRENT_SOURCE_DIR}/postinstall.mjs + ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/babel.config.js + ${CMAKE_CURRENT_SOURCE_DIR}/package.json + ${CMAKE_CURRENT_SOURCE_DIR}/package-lock.json + ${CMAKE_CURRENT_SOURCE_DIR}/postinstall.mjs +) +add_custom_target(copyNodeApiJSToolsFiles) + +# Install exactly the lockfile graph. postinstall.mjs writes the declared +# output from the lockfile itself, avoiding the old racy background tar/hash +# pipeline and making incremental rebuilds deterministic on every platform. +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/node_modules.sha256 + DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/package.json + ${CMAKE_CURRENT_BINARY_DIR}/package-lock.json + ${CMAKE_CURRENT_BINARY_DIR}/postinstall.mjs + COMMAND ${npm} ci + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +add_custom_target(installNodeApiTestJsTools + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/node_modules.sha256 +) +add_dependencies(installNodeApiTestJsTools copyNodeApiJSToolsFiles) + +# add the Babel transform commands for each test JS file +set(testJSRootDir ${CMAKE_CURRENT_SOURCE_DIR}) + +# Collect all .js files recursively +file(GLOB_RECURSE basicsTestJSFiles "basics/*.js") +file(GLOB_RECURSE commonTestJSFiles "common/*.js") +file(GLOB_RECURSE jsNativeApiTestJSFiles "js-native-api/*.js") +set(testJSFiles + ${basicsTestJSFiles} + ${commonTestJSFiles} + ${jsNativeApiTestJSFiles}) + +foreach(absoluteTestJSFile ${testJSFiles}) + # create target directory + file(RELATIVE_PATH testJSFile ${testJSRootDir} ${absoluteTestJSFile}) + get_filename_component(testJSDir ${testJSFile} DIRECTORY) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${testJSDir}) + + # generate Hermes-compatible JavaScript code + add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile} + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}.map + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${testJSFile} + COMMAND + ${npx} + "babel" + "--retain-lines" + "--source-maps" + "true" + "--out-file" + "${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}" + "--source-map-target" + "${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}.map" + "${CMAKE_CURRENT_SOURCE_DIR}/${testJSFile}" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + + # build a list of all outputs + list(APPEND transformedJSFiles + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile} + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}.map + ) + list(APPEND transformedJSRelativeFiles ${testJSFile}) +endforeach() + +# run the Babel transforms for all required output files +add_custom_target(transformJSFiles + DEPENDS ${transformedJSFiles} +) +add_dependencies(transformJSFiles installNodeApiTestJsTools) +add_dependencies(node_lite transformJSFiles) +add_dependencies(NodeApiTests transformJSFiles) + +# The parent directory owns the runner targets, so it must attach their +# POST_BUILD commands. Export the generated-file locations for that final +# source-tree overlay after this subdirectory returns. +set(NODE_API_TRANSFORMED_JS_BINARY_DIR + "${CMAKE_CURRENT_BINARY_DIR}" + PARENT_SCOPE) +set(NODE_API_TRANSFORMED_JS_RELATIVE_FILES + "${transformedJSRelativeFiles}" + PARENT_SCOPE) + +if(JSR_NODE_API_BUILD_NATIVE_TESTS) + add_subdirectory(js-native-api) +endif() diff --git a/Tests/NodeApi/test/babel.config.js b/Tests/NodeApi/test/babel.config.js new file mode 100644 index 00000000..c8cda4fe --- /dev/null +++ b/Tests/NodeApi/test/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['module:@react-native/babel-preset', + { "unstable_transformProfile": "hermes-canary"}] + ], +}; \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/async_rejected.js b/Tests/NodeApi/test/basics/async_rejected.js new file mode 100644 index 00000000..3c2d956a --- /dev/null +++ b/Tests/NodeApi/test/basics/async_rejected.js @@ -0,0 +1,19 @@ +function resolveAfterTimeout() { + return new Promise((_, reject) => { + setTimeout(() => { + reject("test async rejected"); + }, 0); + }); +} + +async function asyncCall() { + console.log("test async calling"); + try { + const result = await resolveAfterTimeout(); + console.log(`Unexpected: ${result}`); + } catch (error) { + console.error(`Expected: ${error}`); + } +} + +asyncCall(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/async_resolved.js b/Tests/NodeApi/test/basics/async_resolved.js new file mode 100644 index 00000000..d9b44145 --- /dev/null +++ b/Tests/NodeApi/test/basics/async_resolved.js @@ -0,0 +1,15 @@ +function resolveAfterTimeout() { + return new Promise((resolve) => { + setTimeout(() => { + resolve("test async resolved"); + }, 0); + }); +} + +async function asyncCall() { + console.log("test async calling"); + const result = await resolveAfterTimeout(); + console.log(`Expected: ${result}`); +} + +asyncCall(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/hello.js b/Tests/NodeApi/test/basics/hello.js new file mode 100644 index 00000000..7a2bb74e --- /dev/null +++ b/Tests/NodeApi/test/basics/hello.js @@ -0,0 +1 @@ +console.log("Hello"); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/large_output.js b/Tests/NodeApi/test/basics/large_output.js new file mode 100644 index 00000000..38fb5a09 --- /dev/null +++ b/Tests/NodeApi/test/basics/large_output.js @@ -0,0 +1,7 @@ +const output = 'O'.repeat(128 * 1024); +const error = 'E'.repeat(128 * 1024); + +console.log(output); +console.error(error); +console.log('stdout-end'); +console.error('stderr-end'); diff --git a/Tests/NodeApi/test/basics/mustcall_failure.js b/Tests/NodeApi/test/basics/mustcall_failure.js new file mode 100644 index 00000000..105650f8 --- /dev/null +++ b/Tests/NodeApi/test/basics/mustcall_failure.js @@ -0,0 +1,3 @@ +const common = require('../common'); + +common.mustCall(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/mustcall_success.js b/Tests/NodeApi/test/basics/mustcall_success.js new file mode 100644 index 00000000..cec9ac6a --- /dev/null +++ b/Tests/NodeApi/test/basics/mustcall_success.js @@ -0,0 +1,4 @@ +const common = require('../common'); + +var fn = common.mustCall(); +fn(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/mustnotcall_failure.js b/Tests/NodeApi/test/basics/mustnotcall_failure.js new file mode 100644 index 00000000..49db06cc --- /dev/null +++ b/Tests/NodeApi/test/basics/mustnotcall_failure.js @@ -0,0 +1,4 @@ +const common = require('../common'); + +var fn = common.mustNotCall(); +fn(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/mustnotcall_success.js b/Tests/NodeApi/test/basics/mustnotcall_success.js new file mode 100644 index 00000000..b933a30f --- /dev/null +++ b/Tests/NodeApi/test/basics/mustnotcall_success.js @@ -0,0 +1,3 @@ +const common = require('../common'); + +common.mustNotCall(); diff --git a/Tests/NodeApi/test/basics/throw_string.js b/Tests/NodeApi/test/basics/throw_string.js new file mode 100644 index 00000000..f6cc7b60 --- /dev/null +++ b/Tests/NodeApi/test/basics/throw_string.js @@ -0,0 +1 @@ +throw "Script failed"; \ No newline at end of file diff --git a/Tests/NodeApi/test/common/assert.js b/Tests/NodeApi/test/common/assert.js new file mode 100644 index 00000000..12bb4dac --- /dev/null +++ b/Tests/NodeApi/test/common/assert.js @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// The JavaScript code in this file is adopted from the Node.js project. +// See the src\napi\Readme.md about the Node.js copyright notice. + +"use strict"; + +class AssertionError extends Error { + constructor(options) { + const { message, actual, expected, method, errorStack } = options; + + super(String(message)); + + this.name = "AssertionError"; + this.method = String(method); + this.actual = String(actual); + this.expected = String(expected); + this.errorStack = errorStack || ""; + setAssertionSource(this, method); + } +} + +function setAssertionSource(error, method) { + let result = { sourceFile: "", sourceLine: 0 }; + const stackArray = (error.errorStack || error.stack).split("\n"); + const methodNamePattern = `${method} (`; + let methodNameFound = false; + for (const stackFrame of stackArray) { + if (methodNameFound) { + const stackFrameParts = stackFrame.split(":"); + if (stackFrameParts.length >= 2) { + let sourceFile = stackFrameParts[0]; + if (sourceFile.startsWith(" at ")) { + sourceFile = sourceFile.substr(7); + } + result = { sourceFile, sourceLine: Number(stackFrameParts[1]) }; + } + break; + } else { + methodNameFound = stackFrame.indexOf(methodNamePattern) >= 0; + } + } + Object.assign(error, result); +} + +const assert = (module.exports = ok); + +assert.fail = function fail(message) { + message = message || "Failed"; + let errorInfo = message; + if (typeof message !== "object") { + errorInfo = { message, method: fail.name }; + } + throw new AssertionError(errorInfo); +}; + +function innerOk(fn, argLen, value, message) { + if (!value) { + if (argLen === 0) { + message = "No value argument passed to `assert.ok()`"; + } else if (message == null) { + message = "The expression evaluated to a falsy value"; + } + + assert.fail({ + message, + actual: formatValue(value), + expected: formatValue(true), + method: fn.name, + }); + } +} + +// Pure assertion tests whether a value is truthy, as determined by !!value. +function ok(...args) { + innerOk(ok, args.length, ...args); +} +assert.ok = ok; + +let compareErrorMessage = undefined; +function innerComparison( + method, + compare, + defaultMessage, + argLen, + actual, + expected, + message +) { + if (!compare(actual, expected)) { + if (argLen < 2) { + message = `'assert.${method.name}' expects two or more arguments.`; + } else if (message == null) { + message = defaultMessage; + } + if (typeof compareErrorMessage === "string") { + message += "; " + compareErrorMessage; + compareErrorMessage = undefined; + } + assert.fail({ + message, + actual: formatValue(actual), + expected: formatValue(expected), + method: method.name, + }); + } +} + +assert.strictEqual = function strictEqual(...args) { + innerComparison( + strictEqual, + Object.is, + "Values are not strict equal", + args.length, + ...args + ); +}; + +assert.notStrictEqual = function notStrictEqual(...args) { + innerComparison( + notStrictEqual, + negate(Object.is), + "Values must not be strict equal", + args.length, + ...args + ); +}; + +assert.deepStrictEqual = function deepStrictEqual(...args) { + innerComparison( + deepStrictEqual, + isDeepStrictEqual, + "Values are not deep strict equal", + args.length, + ...args + ); +}; + +assert.notDeepStrictEqual = function notDeepStrictEqual(...args) { + innerComparison( + notDeepStrictEqual, + negate(isDeepStrictEqual), + "Values must not be deep strict equal", + args.length, + ...args + ); +}; + +function innerThrows(method, argLen, fn, expected, message) { + let actual = "Did not throw"; + function succeeds() { + try { + fn(); + return false; + } catch (error) { + if (typeof expected === "function") { + if (expected.prototype !== undefined && error instanceof expected) { + return true; + } else { + return expected(error); + } + } else if (expected instanceof RegExp) { + actual = `${error.name}: ${error.message}`; + return expected.test(actual); + } else if (expected) { + actual = `${error.name}: ${error.message}`; + if (expected.name && expected.name != error.name) { + return false; + } else if (expected.message && expected.message != error.message) { + return false; + } else if (expected.code && expected.code != error.code) { + return false; + } + } + return true; + } + } + + if (argLen < 1 || typeof fn !== "function") { + message = `'assert.${method.name}' expects a function parameter.`; + } else if (message == null) { + if (expected) { + message = `'assert.${method.name}' failed to throw an exception that matches '${expected}'.`; + } else { + message = `'assert.${method.name}' failed to throw an exception.`; + } + } + + if (!succeeds()) { + throw new AssertionError({ + message, + actual, + expected, + method: method.name, + }); + } +} + +assert.throws = function throws(...args) { + innerThrows(throws, args.length, ...args); +}; + +function innerMatch(method, argLen, value, expected, message) { + let succeeds = false; + if (argLen < 1 || typeof value !== "string") { + message = `'assert.${method.name}' expects a string parameter.`; + } else if (!(expected instanceof RegExp)) { + message = `'assert.${method.name}' expects a RegExp as a second parameter.`; + } else { + succeeds = expected.test(value); + if (!succeeds && message == null) { + message = `'assert.${method.name}' failed to match '${expected}'.`; + } + } + + if (!succeeds) { + throw new AssertionError({ + message, + actual: value, + expected, + method: method.name, + }); + } +} + +assert.match = function match(...args) { + innerMatch(match, args.length, ...args); +}; + +function negate(compare) { + return (...args) => !compare(...args); +} + +function isDeepStrictEqual(left, right) { + function check(left, right) { + if (left === right) { + return true; + } + if (typeof left !== typeof right) { + compareErrorMessage = `Different types: ${typeof left} vs ${typeof right}`; + return false; + } + if (Array.isArray(left)) { + return Array.isArray(right) && checkArray(left, right); + } + if (typeof left === "number") { + return isNaN(left) && isNaN(right); + } + if (typeof left === "object") { + return typeof right === "object" && checkObject(left, right); + } + return false; + } + + function checkArray(left, right) { + if (left.length !== right.length) { + compareErrorMessage = `Different array lengths: ${left.length} vs ${right.length}`; + return false; + } + for (let i = 0; i < left.length; ++i) { + if (!check(left[i], right[i])) { + compareErrorMessage = `Different values at index ${i}: ${left[i]} vs ${right[i]}`; + return false; + } + } + return true; + } + + function checkObject(left, right) { + const leftNames = Object.getOwnPropertyNames(left); + const rightNames = Object.getOwnPropertyNames(right); + if (leftNames.length !== rightNames.length) { + compareErrorMessage = `Different set of property names: ${leftNames.length} vs ${rightNames.length}`; + return false; + } + for (let i = 0; i < leftNames.length; ++i) { + if (!check(left[leftNames[i]], right[leftNames[i]])) { + compareErrorMessage = `Different values for property '${leftNames[i]}': ${left[leftNames[i]]} vs ${right[leftNames[i]]}`; + return false; + } + } + const leftSymbols = Object.getOwnPropertySymbols(left); + const rightSymbols = Object.getOwnPropertySymbols(right); + if (leftSymbols.length !== rightSymbols.length) { + compareErrorMessage = `Different set of symbol names: ${leftSymbols.length} vs ${rightSymbols.length}`; + return false; + } + for (let i = 0; i < leftSymbols.length; ++i) { + if (!check(left[leftSymbols[i]], right[leftSymbols[i]])) { + compareErrorMessage = `${leftSymbols[i].toString()}: different value`; + return false; + } + } + return check(Object.getPrototypeOf(left), Object.getPrototypeOf(right)); + } + + return check(left, right); +} + +const mustCallChecks = []; + +function runCallChecks() { + const failed = mustCallChecks.filter((context) => { + if ("minimum" in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + mustCallChecks.length = 0; + + failed.forEach((context) => { + assert.fail({ + message: `Mismatched ${context.name} function calls`, + actual: `${context.actual} calls`, + expected: `${context.messageSegment} calls`, + method: context.method.name, + errorStack: context.stack, + }); + }); +}; +assert.runCallChecks = runCallChecks; + +function getCallSite() { + try { + throw new Error(""); + } catch (err) { + return err.stack; + } +} + +assert.mustNotCall = function mustNotCall(msg) { + return function mustNotCall(...args) { + assert.fail({ + message: String(msg || "Function should not have been called"), + actual: + args.length > 0 + ? `Called with arguments: ${args.map(String).join(", ")}` + : "Called without arguments", + expected: "Not to be called", + method: mustNotCall.name, + }); + }; +}; + +assert.mustCall = function mustCall(fn, exact) { + return _mustCallInner(fn, exact, "exact", mustCall); +}; + +assert.mustCallAtLeast = function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, "minimum", mustCallAtLeast); +}; + +const noop = () => {}; + +function _mustCallInner(fn, criteria = 1, field, method) { + if (typeof fn === "number") { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== "number") { + throw new TypeError(`Invalid ${field} value: ${criteria}`); + } + + const context = { + [field]: criteria, + actual: 0, + stack: getCallSite(), + name: fn.name || "", + method, + }; + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) process.on('exit', runCallChecks); + + mustCallChecks.push(context); + + return function () { + context.actual++; + return fn.apply(this, arguments); + }; +} + +function formatValue(value) { + let type = typeof value; + if (type === "object") { + if (Array.isArray(value)) { + return " []"; + } else { + return " {}"; + } + } + return `<${type}> ${value}`; +} diff --git a/Tests/NodeApi/test/common/gc.js b/Tests/NodeApi/test/common/gc.js new file mode 100644 index 00000000..66f77055 --- /dev/null +++ b/Tests/NodeApi/test/common/gc.js @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// The JavaScript code in this file is adopted from the Node.js project. +// See the src\napi\Readme.md about the Node.js copyright notice. +"use strict"; + +function gcUntil(name, condition) { + if (typeof name === "function") { + condition = name; + name = undefined; + } + return new Promise((resolve, reject) => { + let count = 0; + function gcAndCheck() { + setImmediate(() => { + count++; + global.gc(); + if (condition()) { + resolve(); + } else if (count < 10) { + gcAndCheck(); + } else { + reject(name === undefined ? undefined : "Test " + name + " failed"); + } + }); + } + gcAndCheck(); + }); +} + +Object.assign(module.exports, { + gcUntil, +}); diff --git a/Tests/NodeApi/test/common/index.js b/Tests/NodeApi/test/common/index.js new file mode 100644 index 00000000..e4ec6151 --- /dev/null +++ b/Tests/NodeApi/test/common/index.js @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// The JavaScript code in this file is adopted from the Node.js project. +// See the src\napi\Readme.md about the Node.js copyright notice. +"use strict"; + +const { mustCall, mustCallAtLeast, mustNotCall } = require("assert"); +const { gcUntil } = require("gc"); + +const buildType = process.target_config; +const isWindows = process.platform === 'win32'; + +// Returns true if the exit code "exitCode" and/or signal name "signal" +// represent the exit code and/or signal name of a node process that aborted, +// false otherwise. +function nodeProcessAborted(exitCode, signal) { + // Depending on the compiler used, node will exit with either + // exit code 132 (SIGILL), 133 (SIGTRAP) or 134 (SIGABRT). + let expectedExitCodes = [132, 133, 134]; + + // On platforms using KSH as the default shell (like SmartOS), + // when a process aborts, KSH exits with an exit code that is + // greater than 256, and thus the exit code emitted with the 'exit' + // event is null and the signal is set to either SIGILL, SIGTRAP, + // or SIGABRT (depending on the compiler). + const expectedSignals = ['SIGILL', 'SIGTRAP', 'SIGABRT']; + + // On Windows, 'aborts' are of 2 types, depending on the context: + // (i) Exception breakpoint, if --abort-on-uncaught-exception is on + // which corresponds to exit code 2147483651 (0x80000003) + // (ii) Otherwise, _exit(134) which is called in place of abort() due to + // raising SIGABRT exiting with ambiguous exit code '3' by default + if (isWindows) + expectedExitCodes = [0x80000003, 134]; + + // When using --abort-on-uncaught-exception, V8 will use + // base::OS::Abort to terminate the process. + // Depending on the compiler used, the shell or other aspects of + // the platform used to build the node binary, this will actually + // make V8 exit by aborting or by raising a signal. In any case, + // one of them (exit code or signal) needs to be set to one of + // the expected exit codes or signals. + if (signal !== null) { + return expectedSignals.includes(signal); + } + return expectedExitCodes.includes(exitCode); +} + +Object.assign(module.exports, { + buildType, + gcUntil, + mustCall, + mustCallAtLeast, + mustNotCall, + nodeProcessAborted, +}); \ No newline at end of file diff --git a/Tests/NodeApi/test/js-native-api/.gitignore b/Tests/NodeApi/test/js-native-api/.gitignore new file mode 100644 index 00000000..6e29bde8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/.gitignore @@ -0,0 +1,7 @@ +.buildstamp +.docbuildstamp +Makefile +*.Makefile +*.mk +gyp-mac-tool +/*/build diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/2_function_arguments.c b/Tests/NodeApi/test/js-native-api/2_function_arguments/2_function_arguments.c new file mode 100644 index 00000000..c03085db --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/2_function_arguments.c @@ -0,0 +1,39 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value Add(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype0 == napi_number && valuetype1 == napi_number, + "Wrong argument type. Numbers expected."); + + double value0; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value0)); + + double value1; + NODE_API_CALL(env, napi_get_value_double(env, args[1], &value1)); + + napi_value sum; + NODE_API_CALL(env, napi_create_double(env, value0 + value1, &sum)); + + return sum; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor desc = DECLARE_NODE_API_PROPERTY("add", Add); + NODE_API_CALL(env, napi_define_properties(env, exports, 1, &desc)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/2_function_arguments/CMakeLists.txt new file mode 100644 index 00000000..258ed16e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(2_function_arguments + SOURCES + 2_function_arguments.c +) diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/binding.gyp b/Tests/NodeApi/test/js-native-api/2_function_arguments/binding.gyp new file mode 100644 index 00000000..8f89a61e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "2_function_arguments", + "sources": [ + "2_function_arguments.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/test.js b/Tests/NodeApi/test/js-native-api/2_function_arguments/test.js new file mode 100644 index 00000000..2966cc0b --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/test.js @@ -0,0 +1,6 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/2_function_arguments`); + +assert.strictEqual(addon.add(3, 5), 8); diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/3_callbacks.c b/Tests/NodeApi/test/js-native-api/3_callbacks/3_callbacks.c new file mode 100644 index 00000000..fd7b6618 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/3_callbacks.c @@ -0,0 +1,58 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value RunCallback(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, + "Wrong number of arguments. Expects a single argument."); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_function, + "Wrong type of arguments. Expects a function as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, valuetype1 == napi_undefined, + "Additional arguments should be undefined."); + + napi_value argv[1]; + const char* str = "hello world"; + size_t str_len = strlen(str); + NODE_API_CALL(env, napi_create_string_utf8(env, str, str_len, argv)); + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value cb = args[0]; + NODE_API_CALL(env, napi_call_function(env, global, cb, 1, argv, NULL)); + + return NULL; +} + +static napi_value RunCallbackWithRecv(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value cb = args[0]; + napi_value recv = args[1]; + NODE_API_CALL(env, napi_call_function(env, recv, cb, 0, NULL, NULL)); + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor desc[2] = { + DECLARE_NODE_API_PROPERTY("RunCallback", RunCallback), + DECLARE_NODE_API_PROPERTY("RunCallbackWithRecv", RunCallbackWithRecv), + }; + NODE_API_CALL(env, napi_define_properties(env, exports, 2, desc)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/3_callbacks/CMakeLists.txt new file mode 100644 index 00000000..02134621 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(3_callbacks + SOURCES + 3_callbacks.c +) diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/binding.gyp b/Tests/NodeApi/test/js-native-api/3_callbacks/binding.gyp new file mode 100644 index 00000000..d64b5e48 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "3_callbacks", + "sources": [ + "3_callbacks.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/test.js b/Tests/NodeApi/test/js-native-api/3_callbacks/test.js new file mode 100644 index 00000000..ace0f2a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/test.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/3_callbacks`); + +addon.RunCallback(function(msg) { + assert.strictEqual(msg, 'hello world'); +}); + +function testRecv(desiredRecv) { + addon.RunCallbackWithRecv(function() { + assert.strictEqual(this, desiredRecv); + }, desiredRecv); +} + +testRecv(undefined); +testRecv(null); +testRecv(5); +testRecv(true); +testRecv('Hello'); +testRecv([]); +testRecv({}); diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/4_object_factory.c b/Tests/NodeApi/test/js-native-api/4_object_factory/4_object_factory.c new file mode 100644 index 00000000..38169b0f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/4_object_factory.c @@ -0,0 +1,24 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value CreateObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value obj; + NODE_API_CALL(env, napi_create_object(env, &obj)); + + NODE_API_CALL(env, napi_set_named_property(env, obj, "msg", args[0])); + + return obj; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NODE_API_CALL(env, + napi_create_function(env, "exports", -1, CreateObject, NULL, &exports)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/4_object_factory/CMakeLists.txt new file mode 100644 index 00000000..b4a9917c --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(4_object_factory + SOURCES + 4_object_factory.c +) diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/binding.gyp b/Tests/NodeApi/test/js-native-api/4_object_factory/binding.gyp new file mode 100644 index 00000000..86f8b1f0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "4_object_factory", + "sources": [ + "4_object_factory.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/test.js b/Tests/NodeApi/test/js-native-api/4_object_factory/test.js new file mode 100644 index 00000000..fbfbd67f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/test.js @@ -0,0 +1,8 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/4_object_factory`); + +const obj1 = addon('hello'); +const obj2 = addon('world'); +assert.strictEqual(`${obj1.msg} ${obj2.msg}`, 'hello world'); diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/5_function_factory.c b/Tests/NodeApi/test/js-native-api/5_function_factory/5_function_factory.c new file mode 100644 index 00000000..744a8c72 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/5_function_factory.c @@ -0,0 +1,24 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value MyFunction(napi_env env, napi_callback_info info) { + napi_value str; + NODE_API_CALL(env, napi_create_string_utf8(env, "hello world", -1, &str)); + return str; +} + +static napi_value CreateFunction(napi_env env, napi_callback_info info) { + napi_value fn; + NODE_API_CALL(env, + napi_create_function(env, "theFunction", -1, MyFunction, NULL, &fn)); + return fn; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NODE_API_CALL(env, + napi_create_function(env, "exports", -1, CreateFunction, NULL, &exports)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/5_function_factory/CMakeLists.txt new file mode 100644 index 00000000..4a7e51a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(5_function_factory + SOURCES + 5_function_factory.c +) diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/binding.gyp b/Tests/NodeApi/test/js-native-api/5_function_factory/binding.gyp new file mode 100644 index 00000000..06bd385e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "5_function_factory", + "sources": [ + "5_function_factory.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/test.js b/Tests/NodeApi/test/js-native-api/5_function_factory/test.js new file mode 100644 index 00000000..bacb22ce --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/test.js @@ -0,0 +1,7 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/5_function_factory`); + +const fn = addon(); +assert.strictEqual(fn(), 'hello world'); // 'hello world' diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/6_object_wrap/CMakeLists.txt new file mode 100644 index 00000000..27486730 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/CMakeLists.txt @@ -0,0 +1,21 @@ +add_node_api_module(myobject + SOURCES + myobject.cc + myobject.h +) + +add_node_api_module(myobject_basic_finalizer + SOURCES + myobject.cc + myobject.h + DEFINES + NAPI_EXPERIMENTAL +) + +add_node_api_module(nested_wrap + SOURCES + nested_wrap.cc + nested_wrap.h + DEFINES + "NAPI_VERSION=10" +) diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/binding.gyp b/Tests/NodeApi/test/js-native-api/6_object_wrap/binding.gyp new file mode 100644 index 00000000..e7a9d7ba --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/binding.gyp @@ -0,0 +1,28 @@ +{ + "targets": [ + { + "target_name": "myobject", + "sources": [ + "myobject.cc", + "myobject.h", + ] + }, + { + "target_name": "myobject_basic_finalizer", + "defines": [ "NAPI_EXPERIMENTAL" ], + "sources": [ + "myobject.cc", + "myobject.h", + ] + }, + { + "target_name": "nested_wrap", + # Test without basic finalizers as it schedules differently. + "defines": [ "NAPI_VERSION=10" ], + "sources": [ + "nested_wrap.cc", + "nested_wrap.h", + ], + }, + ] +} diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.cc b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.cc new file mode 100644 index 00000000..5633d929 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.cc @@ -0,0 +1,270 @@ +#include "myobject.h" +#include "../common.h" +#include "../entry_point.h" +#include "assert.h" + +typedef int32_t FinalizerData; + +napi_ref MyObject::constructor; + +MyObject::MyObject(double value) + : value_(value), env_(nullptr), wrapper_(nullptr) {} + +MyObject::~MyObject() { + napi_delete_reference(env_, wrapper_); +} + +void MyObject::Destructor(node_api_basic_env env, + void* nativeObject, + void* /*finalize_hint*/) { + MyObject* obj = static_cast(nativeObject); + delete obj; + + FinalizerData* data; + NODE_API_BASIC_CALL_RETURN_VOID( + env, napi_get_instance_data(env, reinterpret_cast(&data))); + *data += 1; +} + +void MyObject::Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + {"value", nullptr, nullptr, GetValue, SetValue, 0, napi_default, 0}, + {"valueReadonly", + nullptr, + nullptr, + GetValue, + nullptr, + 0, + napi_default, + 0}, + DECLARE_NODE_API_PROPERTY("plusOne", PlusOne), + DECLARE_NODE_API_PROPERTY("multiply", Multiply), + }; + + napi_value cons; + NODE_API_CALL_RETURN_VOID( + env, + napi_define_class(env, + "MyObject", + -1, + New, + nullptr, + sizeof(properties) / sizeof(napi_property_descriptor), + properties, + &cons)); + + NODE_API_CALL_RETURN_VOID(env, + napi_create_reference(env, cons, 1, &constructor)); + + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "MyObject", cons)); +} + +napi_value MyObject::New(napi_env env, napi_callback_info info) { + napi_value new_target; + NODE_API_CALL(env, napi_get_new_target(env, info, &new_target)); + bool is_constructor = (new_target != nullptr); + + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + if (is_constructor) { + // Invoked as constructor: `new MyObject(...)` + double value = 0; + + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + if (valuetype != napi_undefined) { + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value)); + } + + MyObject* obj = new MyObject(value); + + obj->env_ = env; + NODE_API_CALL(env, + napi_wrap(env, + _this, + obj, + MyObject::Destructor, + nullptr /* finalize_hint */, + &obj->wrapper_)); + + return _this; + } + + // Invoked as plain function `MyObject(...)`, turn into construct call. + argc = 1; + napi_value argv[1] = {args[0]}; + + napi_value cons; + NODE_API_CALL(env, napi_get_reference_value(env, constructor, &cons)); + + napi_value instance; + NODE_API_CALL(env, napi_new_instance(env, cons, argc, argv, &instance)); + + return instance; +} + +napi_value MyObject::GetValue(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + napi_value num; + NODE_API_CALL(env, napi_create_double(env, obj->value_, &num)); + + return num; +} + +napi_value MyObject::SetValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + NODE_API_CALL(env, napi_get_value_double(env, args[0], &obj->value_)); + + return nullptr; +} + +napi_value MyObject::PlusOne(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + obj->value_ += 1; + + napi_value num; + NODE_API_CALL(env, napi_create_double(env, obj->value_, &num)); + + return num; +} + +napi_value MyObject::Multiply(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + double multiple = 1; + if (argc >= 1) { + NODE_API_CALL(env, napi_get_value_double(env, args[0], &multiple)); + } + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + napi_value cons; + NODE_API_CALL(env, napi_get_reference_value(env, constructor, &cons)); + + const int kArgCount = 1; + napi_value argv[kArgCount]; + NODE_API_CALL(env, napi_create_double(env, obj->value_ * multiple, argv)); + + napi_value instance; + NODE_API_CALL(env, napi_new_instance(env, cons, kArgCount, argv, &instance)); + + return instance; +} + +// This finalizer should never be invoked. +void ObjectWrapDanglingReferenceFinalizer(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + assert(0 && "unreachable"); +} + +napi_ref dangling_ref; +napi_value ObjectWrapDanglingReference(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + // Create a napi_wrap and remove it immediately, whilst leaving the out-param + // ref dangling (not deleted). + NODE_API_CALL(env, + napi_wrap(env, + args[0], + nullptr, + ObjectWrapDanglingReferenceFinalizer, + nullptr, + &dangling_ref)); + NODE_API_CALL(env, napi_remove_wrap(env, args[0], nullptr)); + + return args[0]; +} + +napi_value ObjectWrapDanglingReferenceTest(napi_env env, + napi_callback_info info) { + napi_value out; + napi_value ret; + NODE_API_CALL(env, napi_get_reference_value(env, dangling_ref, &out)); + + if (out == nullptr) { + // If the napi_ref has been invalidated, delete it. + NODE_API_CALL(env, napi_delete_reference(env, dangling_ref)); + NODE_API_CALL(env, napi_get_boolean(env, true, &ret)); + } else { + // The dangling napi_ref is still valid. + NODE_API_CALL(env, napi_get_boolean(env, false, &ret)); + } + return ret; +} + +static napi_value GetFinalizerCallCount(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + FinalizerData* data; + napi_value result; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr)); + NODE_API_CALL(env, + napi_get_instance_data(env, reinterpret_cast(&data))); + NODE_API_CALL(env, napi_create_int32(env, *data, &result)); + return result; +} + +static void finalizeData(napi_env env, void* data, void* hint) { + delete reinterpret_cast(data); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + FinalizerData* data = new FinalizerData; + *data = 0; + NODE_API_CALL(env, napi_set_instance_data(env, data, finalizeData, nullptr)); + + MyObject::Init(env, exports); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("objectWrapDanglingReference", + ObjectWrapDanglingReference), + DECLARE_NODE_API_PROPERTY("objectWrapDanglingReferenceTest", + ObjectWrapDanglingReferenceTest), + DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", GetFinalizerCallCount), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.h b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.h new file mode 100644 index 00000000..fcb2e575 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.h @@ -0,0 +1,28 @@ +#ifndef TEST_JS_NATIVE_API_6_OBJECT_WRAP_MYOBJECT_H_ +#define TEST_JS_NATIVE_API_6_OBJECT_WRAP_MYOBJECT_H_ + +#include + +class MyObject { + public: + static void Init(napi_env env, napi_value exports); + static void Destructor(node_api_basic_env env, + void* nativeObject, + void* finalize_hint); + + private: + explicit MyObject(double value_ = 0); + ~MyObject(); + + static napi_value New(napi_env env, napi_callback_info info); + static napi_value GetValue(napi_env env, napi_callback_info info); + static napi_value SetValue(napi_env env, napi_callback_info info); + static napi_value PlusOne(napi_env env, napi_callback_info info); + static napi_value Multiply(napi_env env, napi_callback_info info); + static napi_ref constructor; + double value_; + napi_env env_; + napi_ref wrapper_; +}; + +#endif // TEST_JS_NATIVE_API_6_OBJECT_WRAP_MYOBJECT_H_ diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.cc b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.cc new file mode 100644 index 00000000..1c8594c8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.cc @@ -0,0 +1,99 @@ +#include "nested_wrap.h" +#include "../common.h" +#include "../entry_point.h" + +napi_ref NestedWrap::constructor{}; +static int finalization_count = 0; + +NestedWrap::NestedWrap() {} + +NestedWrap::~NestedWrap() { + napi_delete_reference(env_, wrapper_); + + // Delete the nested reference as well. + napi_delete_reference(env_, nested_); +} + +void NestedWrap::Destructor(node_api_basic_env env, + void* nativeObject, + void* /*finalize_hint*/) { + // Once this destructor is called, it cancels all pending + // finalizers for the object by deleting the references. + NestedWrap* obj = static_cast(nativeObject); + delete obj; + + finalization_count++; +} + +void NestedWrap::Init(napi_env env, napi_value exports) { + napi_value cons; + NODE_API_CALL_RETURN_VOID( + env, + napi_define_class( + env, "NestedWrap", -1, New, nullptr, 0, nullptr, &cons)); + + NODE_API_CALL_RETURN_VOID(env, + napi_create_reference(env, cons, 1, &constructor)); + + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "NestedWrap", cons)); +} + +napi_value NestedWrap::New(napi_env env, napi_callback_info info) { + napi_value new_target; + NODE_API_CALL(env, napi_get_new_target(env, info, &new_target)); + bool is_constructor = (new_target != nullptr); + NODE_API_BASIC_ASSERT_BASE( + is_constructor, "Constructor called without new", nullptr); + + napi_value this_val; + NODE_API_CALL(env, + napi_get_cb_info(env, info, 0, nullptr, &this_val, nullptr)); + + NestedWrap* obj = new NestedWrap(); + + obj->env_ = env; + NODE_API_CALL(env, + napi_wrap(env, + this_val, + obj, + NestedWrap::Destructor, + nullptr /* finalize_hint */, + &obj->wrapper_)); + + // Create a second napi_ref to be deleted in the destructor. + NODE_API_CALL(env, + napi_add_finalizer(env, + this_val, + obj, + NestedWrap::Destructor, + nullptr /* finalize_hint */, + &obj->nested_)); + + return this_val; +} + +static napi_value GetFinalizerCallCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalization_count, &result)); + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NestedWrap::Init(env, exports); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", GetFinalizerCallCount), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.h b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.h new file mode 100644 index 00000000..584f24de --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.h @@ -0,0 +1,33 @@ +#ifndef TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_ +#define TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_ + +#include + +/** + * Test that an napi_ref can be nested inside another ObjectWrap. + * + * This test shows a critical case where a finalizer deletes an napi_ref + * whose finalizer is also scheduled. + */ + +class NestedWrap { + public: + static void Init(napi_env env, napi_value exports); + static void Destructor(node_api_basic_env env, + void* nativeObject, + void* finalize_hint); + + private: + explicit NestedWrap(); + ~NestedWrap(); + + static napi_value New(napi_env env, napi_callback_info info); + + static napi_ref constructor; + + napi_env env_{}; + napi_ref wrapper_{}; + napi_ref nested_{}; +}; + +#endif // TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_ diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.js new file mode 100644 index 00000000..726c6931 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.js @@ -0,0 +1,20 @@ +// Flags: --expose-gc + +'use strict'; +const common = require('../../common'); +const { gcUntil } = require('../../common/gc'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/nested_wrap`); + +// This test verifies that ObjectWrap and napi_ref can be nested and finalized +// correctly with a non-basic finalizer. +(() => { + let obj = new addon.NestedWrap(); + obj = null; + // Silent eslint about unused variables. + assert.strictEqual(obj, null); +})(); + +gcUntil('object-wrap-ref', () => { + return addon.getFinalizerCallCount() === 1; +}); diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/test-basic-finalizer.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-basic-finalizer.js new file mode 100644 index 00000000..5a7ccff4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-basic-finalizer.js @@ -0,0 +1,24 @@ +// Flags: --expose-gc + +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/myobject_basic_finalizer`); + +// This test verifies that ObjectWrap can be correctly finalized with a node_api_basic_finalizer +// in the current JS loop tick +(() => { + let obj = new addon.MyObject(9); + obj = null; + // Silent eslint about unused variables. + assert.strictEqual(obj, null); +})(); + +for (let i = 0; i < 10; ++i) { + global.gc(); + if (addon.getFinalizerCallCount() === 1) { + break; + } +} + +assert.strictEqual(addon.getFinalizerCallCount(), 1); diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/test-object-wrap-ref.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-object-wrap-ref.js new file mode 100644 index 00000000..8f236410 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-object-wrap-ref.js @@ -0,0 +1,14 @@ +// Flags: --expose-gc + +'use strict'; +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/myobject`); +const { gcUntil } = require('../../common/gc'); + +(function scope() { + addon.objectWrapDanglingReference({}); +})(); + +gcUntil('object-wrap-ref', () => { + return addon.objectWrapDanglingReferenceTest(); +}); diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/test.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/test.js new file mode 100644 index 00000000..809fddf2 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/test.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/myobject`); + +const getterOnlyErrorRE = + /^TypeError: Cannot (set|assign to) property .*( of #<.*>)? which has only a getter$/; + +const valueDescriptor = Object.getOwnPropertyDescriptor( + addon.MyObject.prototype, 'value'); +const valueReadonlyDescriptor = Object.getOwnPropertyDescriptor( + addon.MyObject.prototype, 'valueReadonly'); +const plusOneDescriptor = Object.getOwnPropertyDescriptor( + addon.MyObject.prototype, 'plusOne'); +assert.strictEqual(typeof valueDescriptor.get, 'function'); +assert.strictEqual(typeof valueDescriptor.set, 'function'); +assert.strictEqual(valueDescriptor.value, undefined); +assert.strictEqual(valueDescriptor.enumerable, false); +assert.strictEqual(valueDescriptor.configurable, false); +assert.strictEqual(typeof valueReadonlyDescriptor.get, 'function'); +assert.strictEqual(valueReadonlyDescriptor.set, undefined); +assert.strictEqual(valueReadonlyDescriptor.value, undefined); +assert.strictEqual(valueReadonlyDescriptor.enumerable, false); +assert.strictEqual(valueReadonlyDescriptor.configurable, false); + +assert.strictEqual(plusOneDescriptor.get, undefined); +assert.strictEqual(plusOneDescriptor.set, undefined); +assert.strictEqual(typeof plusOneDescriptor.value, 'function'); +assert.strictEqual(plusOneDescriptor.enumerable, false); +assert.strictEqual(plusOneDescriptor.configurable, false); + +const obj = new addon.MyObject(9); +assert.strictEqual(obj.value, 9); +obj.value = 10; +assert.strictEqual(obj.value, 10); +assert.strictEqual(obj.valueReadonly, 10); +assert.throws(() => { obj.valueReadonly = 14; }, getterOnlyErrorRE); +assert.strictEqual(obj.plusOne(), 11); +assert.strictEqual(obj.plusOne(), 12); +assert.strictEqual(obj.plusOne(), 13); + +assert.strictEqual(obj.multiply().value, 13); +assert.strictEqual(obj.multiply(10).value, 130); + +const newobj = obj.multiply(-1); +assert.strictEqual(newobj.value, -13); +assert.strictEqual(newobj.valueReadonly, -13); +assert.notStrictEqual(obj, newobj); diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/7_factory_wrap.cc b/Tests/NodeApi/test/js-native-api/7_factory_wrap/7_factory_wrap.cc new file mode 100644 index 00000000..b0ff0063 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/7_factory_wrap.cc @@ -0,0 +1,32 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "myobject.h" + +napi_value CreateObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + napi_value instance; + NODE_API_CALL(env, MyObject::NewInstance(env, args[0], &instance)); + + return instance; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NODE_API_CALL(env, MyObject::Init(env)); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", MyObject::GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createObject", CreateObject), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/7_factory_wrap/CMakeLists.txt new file mode 100644 index 00000000..1ca18670 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(7_factory_wrap + SOURCES + 7_factory_wrap.cc + myobject.cc +) diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/binding.gyp b/Tests/NodeApi/test/js-native-api/7_factory_wrap/binding.gyp new file mode 100644 index 00000000..f51f7823 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "7_factory_wrap", + "sources": [ + "7_factory_wrap.cc", + "myobject.cc" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.cc b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.cc new file mode 100644 index 00000000..142c2dab --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.cc @@ -0,0 +1,101 @@ +#include "myobject.h" +#include "../common.h" + +static int finalize_count = 0; + +MyObject::MyObject() : env_(nullptr), wrapper_(nullptr) {} + +MyObject::~MyObject() { napi_delete_reference(env_, wrapper_); } + +void MyObject::Destructor(node_api_basic_env env, + void* nativeObject, + void* /*finalize_hint*/) { + ++finalize_count; + MyObject* obj = static_cast(nativeObject); + delete obj; +} + +napi_value MyObject::GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} + +napi_ref MyObject::constructor; + +napi_status MyObject::Init(napi_env env) { + napi_status status; + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("plusOne", PlusOne), + }; + + napi_value cons; + status = napi_define_class( + env, "MyObject", -1, New, nullptr, 1, properties, &cons); + if (status != napi_ok) return status; + + status = napi_create_reference(env, cons, 1, &constructor); + if (status != napi_ok) return status; + + return napi_ok; +} + +napi_value MyObject::New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + MyObject* obj = new MyObject(); + + if (valuetype == napi_undefined) { + obj->counter_ = 0; + } else { + NODE_API_CALL(env, napi_get_value_uint32(env, args[0], &obj->counter_)); + } + + obj->env_ = env; + NODE_API_CALL(env, + napi_wrap( + env, _this, obj, MyObject::Destructor, nullptr /* finalize_hint */, + &obj->wrapper_)); + + return _this; +} + +napi_status MyObject::NewInstance(napi_env env, + napi_value arg, + napi_value* instance) { + napi_status status; + + const int argc = 1; + napi_value argv[argc] = {arg}; + + napi_value cons; + status = napi_get_reference_value(env, constructor, &cons); + if (status != napi_ok) return status; + + status = napi_new_instance(env, cons, argc, argv, instance); + if (status != napi_ok) return status; + + return napi_ok; +} + +napi_value MyObject::PlusOne(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + obj->counter_ += 1; + + napi_value num; + NODE_API_CALL(env, napi_create_uint32(env, obj->counter_, &num)); + + return num; +} diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.h b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.h new file mode 100644 index 00000000..aa2b199a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.h @@ -0,0 +1,27 @@ +#ifndef TEST_JS_NATIVE_API_7_FACTORY_WRAP_MYOBJECT_H_ +#define TEST_JS_NATIVE_API_7_FACTORY_WRAP_MYOBJECT_H_ + +#include + +class MyObject { + public: + static napi_status Init(napi_env env); + static void + Destructor(node_api_basic_env env, void *nativeObject, void *finalize_hint); + static napi_value GetFinalizeCount(napi_env env, napi_callback_info info); + static napi_status + NewInstance(napi_env env, napi_value arg, napi_value *instance); + + private: + MyObject(); + ~MyObject(); + + static napi_ref constructor; + static napi_value New(napi_env env, napi_callback_info info); + static napi_value PlusOne(napi_env env, napi_callback_info info); + uint32_t counter_; + napi_env env_; + napi_ref wrapper_; +}; + +#endif // TEST_JS_NATIVE_API_7_FACTORY_WRAP_MYOBJECT_H_ diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/test.js b/Tests/NodeApi/test/js-native-api/7_factory_wrap/test.js new file mode 100644 index 00000000..23840b36 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/test.js @@ -0,0 +1,27 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); +const test = require(`./build/${common.buildType}/7_factory_wrap`); +const { gcUntil } = require('../../common/gc'); + +assert.strictEqual(test.finalizeCount, 0); +async function runGCTests() { + (() => { + const obj = test.createObject(10); + assert.strictEqual(obj.plusOne(), 11); + assert.strictEqual(obj.plusOne(), 12); + assert.strictEqual(obj.plusOne(), 13); + })(); + await gcUntil('test 1', () => (test.finalizeCount === 1)); + + (() => { + const obj2 = test.createObject(20); + assert.strictEqual(obj2.plusOne(), 21); + assert.strictEqual(obj2.plusOne(), 22); + assert.strictEqual(obj2.plusOne(), 23); + })(); + await gcUntil('test 2', () => (test.finalizeCount === 2)); +} +runGCTests(); diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc new file mode 100644 index 00000000..f3328d93 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc @@ -0,0 +1,61 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "myobject.h" + +extern size_t finalize_count; + +static napi_value CreateObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + napi_value instance; + NODE_API_CALL(env, MyObject::NewInstance(env, args[0], &instance)); + + return instance; +} + +static napi_value Add(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + MyObject* obj1; + NODE_API_CALL(env, + napi_unwrap(env, args[0], reinterpret_cast(&obj1))); + + MyObject* obj2; + NODE_API_CALL(env, + napi_unwrap(env, args[1], reinterpret_cast(&obj2))); + + napi_value sum; + NODE_API_CALL(env, napi_create_double(env, obj1->Val() + obj2->Val(), &sum)); + + return sum; +} + +static napi_value FinalizeCount(napi_env env, napi_callback_info info) { + napi_value return_value; + NODE_API_CALL(env, napi_create_uint32(env, finalize_count, &return_value)); + return return_value; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + MyObject::Init(env); + + napi_property_descriptor desc[] = { + DECLARE_NODE_API_PROPERTY("createObject", CreateObject), + DECLARE_NODE_API_PROPERTY("add", Add), + DECLARE_NODE_API_PROPERTY("finalizeCount", FinalizeCount), + }; + + NODE_API_CALL(env, + napi_define_properties(env, exports, sizeof(desc) / sizeof(*desc), desc)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/CMakeLists.txt new file mode 100644 index 00000000..3ee22eb8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(8_passing_wrapped + SOURCES + 8_passing_wrapped.cc + myobject.cc +) diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/binding.gyp b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/binding.gyp new file mode 100644 index 00000000..d043d0f5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "8_passing_wrapped", + "sources": [ + "8_passing_wrapped.cc", + "myobject.cc" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.cc b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.cc new file mode 100644 index 00000000..ff352d3f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.cc @@ -0,0 +1,91 @@ +#include "myobject.h" +#include "../common.h" + +size_t finalize_count = 0; + +MyObject::MyObject() : env_(nullptr), wrapper_(nullptr) {} + +MyObject::~MyObject() { + finalize_count++; + napi_delete_reference(env_, wrapper_); +} + +void MyObject::Destructor( + node_api_basic_env env, + void *nativeObject, + void * /*finalize_hint*/) { + MyObject *obj = static_cast(nativeObject); + delete obj; +} + +napi_ref MyObject::constructor; + +napi_status MyObject::Init(napi_env env) { + napi_status status; + + napi_value cons; + status = + napi_define_class(env, "MyObject", -1, New, nullptr, 0, nullptr, &cons); + if (status != napi_ok) + return status; + + status = napi_create_reference(env, cons, 1, &constructor); + if (status != napi_ok) + return status; + + return napi_ok; +} + +napi_value MyObject::New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + MyObject *obj = new MyObject(); + + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + if (valuetype == napi_undefined) { + obj->val_ = 0; + } else { + NODE_API_CALL(env, napi_get_value_double(env, args[0], &obj->val_)); + } + + obj->env_ = env; + + // The below call to napi_wrap() must request a reference to the wrapped + // object via the out-parameter, because this ensures that we test the code + // path that deals with a reference that is destroyed from its own finalizer. + NODE_API_CALL( + env, + napi_wrap( + env, + _this, + obj, + MyObject::Destructor, + nullptr /* finalize_hint */, + &obj->wrapper_)); + + return _this; +} + +napi_status +MyObject::NewInstance(napi_env env, napi_value arg, napi_value *instance) { + napi_status status; + + const int argc = 1; + napi_value argv[argc] = {arg}; + + napi_value cons; + status = napi_get_reference_value(env, constructor, &cons); + if (status != napi_ok) + return status; + + status = napi_new_instance(env, cons, argc, argv, instance); + if (status != napi_ok) + return status; + + return napi_ok; +} diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.h b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.h new file mode 100644 index 00000000..bdde3fb4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.h @@ -0,0 +1,28 @@ +#ifndef TEST_JS_NATIVE_API_8_PASSING_WRAPPED_MYOBJECT_H_ +#define TEST_JS_NATIVE_API_8_PASSING_WRAPPED_MYOBJECT_H_ + +#include + +class MyObject { + public: + static napi_status Init(napi_env env); + static void + Destructor(node_api_basic_env env, void *nativeObject, void *finalize_hint); + static napi_status + NewInstance(napi_env env, napi_value arg, napi_value *instance); + double Val() const { + return val_; + } + + private: + MyObject(); + ~MyObject(); + + static napi_ref constructor; + static napi_value New(napi_env env, napi_callback_info info); + double val_; + napi_env env_; + napi_ref wrapper_; +}; + +#endif // TEST_JS_NATIVE_API_8_PASSING_WRAPPED_MYOBJECT_H_ diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/test.js b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/test.js new file mode 100644 index 00000000..145828e6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/test.js @@ -0,0 +1,21 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/8_passing_wrapped`); +const { gcUntil } = require('../../common/gc'); + +async function runTest() { + let obj1 = addon.createObject(10); + let obj2 = addon.createObject(20); + const result = addon.add(obj1, obj2); + assert.strictEqual(result, 30); + + // Make sure the native destructor gets called. + obj1 = null; + obj2 = null; + await gcUntil('8_passing_wrapped', + () => (addon.finalizeCount() === 2)); +} +runTest(); diff --git a/Tests/NodeApi/test/js-native-api/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/CMakeLists.txt new file mode 100644 index 00000000..a2426f51 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/CMakeLists.txt @@ -0,0 +1,7 @@ +if(JSR_NODE_API_BUILD_NATIVE_TESTS) + foreach(NODE_API_TEST_DIR ${JSR_NODE_API_NATIVE_TEST_DIRS}) + if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/${NODE_API_TEST_DIR}/CMakeLists.txt) + add_subdirectory(${NODE_API_TEST_DIR}) + endif() + endforeach() +endif() diff --git a/Tests/NodeApi/test/js-native-api/common-inl.h b/Tests/NodeApi/test/js-native-api/common-inl.h new file mode 100644 index 00000000..2a1a8fa6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/common-inl.h @@ -0,0 +1,71 @@ +#ifndef JS_NATIVE_API_COMMON_INL_H_ +#define JS_NATIVE_API_COMMON_INL_H_ + +#include +#include "common.h" + +#include + +inline void add_returned_status(napi_env env, + const char* key, + napi_value object, + const char* expected_message, + napi_status expected_status, + napi_status actual_status) { + char napi_message_string[100] = ""; + napi_value prop_value; + + if (actual_status != expected_status) { + snprintf(napi_message_string, + sizeof(napi_message_string), + "Invalid status [%d]", + actual_status); + } + + NODE_API_CALL_RETURN_VOID( + env, + napi_create_string_utf8( + env, + (actual_status == expected_status ? expected_message + : napi_message_string), + NAPI_AUTO_LENGTH, + &prop_value)); + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, object, key, prop_value)); +} + +inline void add_last_status(napi_env env, + const char* key, + napi_value return_value) { + napi_value prop_value; + napi_value exception; + const napi_extended_error_info* p_last_error; + NODE_API_CALL_RETURN_VOID(env, napi_get_last_error_info(env, &p_last_error)); + // Content of p_last_error can be updated in subsequent node-api calls. + // Retrieve it immediately. + const char* error_message = p_last_error->error_message == NULL + ? "napi_ok" + : p_last_error->error_message; + + bool is_exception_pending; + NODE_API_CALL_RETURN_VOID( + env, napi_is_exception_pending(env, &is_exception_pending)); + if (is_exception_pending) { + NODE_API_CALL_RETURN_VOID( + env, napi_get_and_clear_last_exception(env, &exception)); + char exception_key[50]; + snprintf(exception_key, sizeof(exception_key), "%s%s", key, "Exception"); + NODE_API_CALL_RETURN_VOID( + env, + napi_set_named_property(env, return_value, exception_key, exception)); + } + + NODE_API_CALL_RETURN_VOID( + env, + napi_create_string_utf8( + env, error_message, NAPI_AUTO_LENGTH, &prop_value)); + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, return_value, key, prop_value)); +} + +#endif // JS_NATIVE_API_COMMON_INL_H_ diff --git a/Tests/NodeApi/test/js-native-api/common.h b/Tests/NodeApi/test/js-native-api/common.h new file mode 100644 index 00000000..7c99da88 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/common.h @@ -0,0 +1,132 @@ +#ifndef JS_NATIVE_API_COMMON_H_ +#define JS_NATIVE_API_COMMON_H_ + +#include +#include // abort() + +// Empty value so that macros here are able to return NULL or void +#define NODE_API_RETVAL_NOTHING // Intentionally blank #define + +#define GET_AND_THROW_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info *error_info; \ + napi_get_last_error_info((env), &error_info); \ + bool is_pending; \ + const char* err_message = error_info->error_message; \ + napi_is_exception_pending((env), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + const char* error_message = err_message != NULL ? \ + err_message : \ + "empty error message"; \ + napi_throw_error((env), NULL, error_message); \ + } \ + } while (0) + +// The basic version of GET_AND_THROW_LAST_ERROR. We cannot access any +// exceptions and we cannot fail by way of JS exception, so we abort. +#define FATALLY_FAIL_WITH_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info* error_info; \ + napi_get_last_error_info((env), &error_info); \ + const char* err_message = error_info->error_message; \ + const char* error_message = \ + err_message != NULL ? err_message : "empty error message"; \ + fprintf(stderr, "%s\n", error_message); \ + abort(); \ + } while (0) + +#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + napi_throw_error( \ + (env), \ + NULL, \ + "assertion (" #assertion ") failed: " message); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_ASSERT_BASE(assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + fprintf(stderr, "assertion (" #assertion ") failed: " message); \ + abort(); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL on failed assertion. +// This is meant to be used inside napi_callback methods. +#define NODE_API_ASSERT(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NULL) + +// Returns empty on failed assertion. +// This is meant to be used inside functions with void return type. +#define NODE_API_ASSERT_RETURN_VOID(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_ASSERT_RETURN_VOID(assertion, message) \ + NODE_API_BASIC_ASSERT_BASE(assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + GET_AND_THROW_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + FATALLY_FAIL_WITH_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL if the_call doesn't return napi_ok. +#define NODE_API_CALL(env, the_call) \ + NODE_API_CALL_BASE(env, the_call, NULL) + +// Returns empty if the_call doesn't return napi_ok. +#define NODE_API_CALL_RETURN_VOID(env, the_call) \ + NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_CALL_RETURN_VOID(env, the_call) \ + NODE_API_BASIC_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CHECK_STATUS(the_call) \ + do { \ + napi_status status = (the_call); \ + if (status != napi_ok) { \ + return status; \ + } \ + } while (0) + +#define NODE_API_ASSERT_STATUS(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) + +#define DECLARE_NODE_API_PROPERTY(name, func) \ + { (name), NULL, (func), NULL, NULL, NULL, napi_default, NULL } + +#define DECLARE_NODE_API_GETTER(name, func) \ + { (name), NULL, NULL, (func), NULL, NULL, napi_default, NULL } + +#define DECLARE_NODE_API_PROPERTY_VALUE(name, value) \ + { (name), NULL, NULL, NULL, NULL, (value), napi_default, NULL } + +static inline void add_returned_status(napi_env env, + const char* key, + napi_value object, + const char* expected_message, + napi_status expected_status, + napi_status actual_status); + +static inline void add_last_status(napi_env env, + const char* key, + napi_value return_value); + +#include "common-inl.h" + +#endif // JS_NATIVE_API_COMMON_H_ diff --git a/Tests/NodeApi/test/js-native-api/entry_point.h b/Tests/NodeApi/test/js-native-api/entry_point.h new file mode 100644 index 00000000..5ba5aaff --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/entry_point.h @@ -0,0 +1,12 @@ +#ifndef JS_NATIVE_API_ENTRY_POINT_H_ +#define JS_NATIVE_API_ENTRY_POINT_H_ + +#include + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports); +EXTERN_C_END + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) + +#endif // JS_NATIVE_API_ENTRY_POINT_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_array/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_array/CMakeLists.txt new file mode 100644 index 00000000..bda269e6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_array + SOURCES + test_array.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_array/binding.gyp b/Tests/NodeApi/test/js-native-api/test_array/binding.gyp new file mode 100644 index 00000000..69545b66 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_array", + "sources": [ + "test_array.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_array/test.js b/Tests/NodeApi/test/js-native-api/test_array/test.js new file mode 100644 index 00000000..26bcb18f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/test.js @@ -0,0 +1,61 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for arrays +const test_array = require(`./build/${common.buildType}/test_array`); + +const array = [ + 1, + 9, + 48, + 13493, + 9459324, + { name: 'hello' }, + [ + 'world', + 'node', + 'abi', + ], +]; + +assert.throws( + () => { + test_array.TestGetElement(array, array.length + 1); + }, + /^Error: assertion \(\(\(uint32_t\)index < length\)\) failed: Index out of bounds!$/, +); + +assert.throws( + () => { + test_array.TestGetElement(array, -2); + }, + /^Error: assertion \(index >= 0\) failed: Invalid index\. Expects a positive integer\.$/, +); + +array.forEach(function(element, index) { + assert.strictEqual(test_array.TestGetElement(array, index), element); +}); + + +assert.deepStrictEqual(test_array.New(array), array); + +assert(test_array.TestHasElement(array, 0)); +assert.strictEqual(test_array.TestHasElement(array, array.length + 1), false); + +assert(test_array.NewWithLength(0) instanceof Array); +assert(test_array.NewWithLength(1) instanceof Array); +// Check max allowed length for an array 2^32 -1 +// TODO: Hermes does not allow such big arrays +// assert(test_array.NewWithLength(4294967295) instanceof Array); + +{ + // Verify that array elements can be deleted. + const arr = ['a', 'b', 'c', 'd']; + + assert.strictEqual(arr.length, 4); + assert.strictEqual(2 in arr, true); + assert.strictEqual(test_array.TestDeleteElement(arr, 2), true); + assert.strictEqual(arr.length, 4); + assert.strictEqual(2 in arr, false); +} diff --git a/Tests/NodeApi/test/js-native-api/test_array/test_array.c b/Tests/NodeApi/test/js-native-api/test_array/test_array.c new file mode 100644 index 00000000..7a34af20 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/test_array.c @@ -0,0 +1,188 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestGetElement(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects an integer as second argument."); + + napi_value array = args[0]; + int32_t index; + NODE_API_CALL(env, napi_get_value_int32(env, args[1], &index)); + + NODE_API_ASSERT(env, index >= 0, "Invalid index. Expects a positive integer."); + + bool isarray; + NODE_API_CALL(env, napi_is_array(env, array, &isarray)); + + if (!isarray) { + return NULL; + } + + uint32_t length; + NODE_API_CALL(env, napi_get_array_length(env, array, &length)); + + NODE_API_ASSERT(env, ((uint32_t)index < length), "Index out of bounds!"); + + napi_value ret; + NODE_API_CALL(env, napi_get_element(env, array, index, &ret)); + + return ret; +} + +static napi_value TestHasElement(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects an integer as second argument."); + + napi_value array = args[0]; + int32_t index; + NODE_API_CALL(env, napi_get_value_int32(env, args[1], &index)); + + bool isarray; + NODE_API_CALL(env, napi_is_array(env, array, &isarray)); + + if (!isarray) { + return NULL; + } + + bool has_element; + NODE_API_CALL(env, napi_has_element(env, array, index, &has_element)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_element, &ret)); + + return ret; +} + +static napi_value TestDeleteElement(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects an integer as second argument."); + + napi_value array = args[0]; + int32_t index; + bool result; + napi_value ret; + + NODE_API_CALL(env, napi_get_value_int32(env, args[1], &index)); + NODE_API_CALL(env, napi_is_array(env, array, &result)); + + if (!result) { + return NULL; + } + + NODE_API_CALL(env, napi_delete_element(env, array, index, &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &ret)); + + return ret; +} + +static napi_value New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_value ret; + NODE_API_CALL(env, napi_create_array(env, &ret)); + + uint32_t i, length; + NODE_API_CALL(env, napi_get_array_length(env, args[0], &length)); + + for (i = 0; i < length; i++) { + napi_value e; + NODE_API_CALL(env, napi_get_element(env, args[0], i, &e)); + NODE_API_CALL(env, napi_set_element(env, ret, i, e)); + } + + return ret; +} + +static napi_value NewWithLength(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects an integer the first argument."); + + int32_t array_length; + NODE_API_CALL(env, napi_get_value_int32(env, args[0], &array_length)); + + napi_value ret; + NODE_API_CALL(env, napi_create_array_with_length(env, array_length, &ret)); + + return ret; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("TestGetElement", TestGetElement), + DECLARE_NODE_API_PROPERTY("TestHasElement", TestHasElement), + DECLARE_NODE_API_PROPERTY("TestDeleteElement", TestDeleteElement), + DECLARE_NODE_API_PROPERTY("New", New), + DECLARE_NODE_API_PROPERTY("NewWithLength", NewWithLength), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_bigint/CMakeLists.txt new file mode 100644 index 00000000..7027e3be --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_bigint + SOURCES + test_bigint.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/binding.gyp b/Tests/NodeApi/test/js-native-api/test_bigint/binding.gyp new file mode 100644 index 00000000..6dc71015 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_bigint", + "sources": [ + "test_bigint.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/test.js b/Tests/NodeApi/test/js-native-api/test_bigint/test.js new file mode 100644 index 00000000..50febf14 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/test.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const { + IsLossless, + TestInt64, + TestUint64, + TestWords, + CreateTooBigBigInt, + MakeBigIntWordsThrow, +} = require(`./build/${common.buildType}/test_bigint`); + +[ + 0n, + -0n, + 1n, + -1n, + 100n, + 2121n, + -1233n, + 986583n, + -976675n, + 98765432213456789876546896323445679887645323232436587988766545658n, + -4350987086545760976737453646576078997096876957864353245245769809n, +].forEach((num) => { + if (num > -(2n ** 63n) && num < 2n ** 63n) { + assert.strictEqual(TestInt64(num), num); + assert.strictEqual(IsLossless(num, true), true); + } else { + assert.strictEqual(IsLossless(num, true), false); + } + + if (num >= 0 && num < 2n ** 64n) { + assert.strictEqual(TestUint64(num), num); + assert.strictEqual(IsLossless(num, false), true); + } else { + assert.strictEqual(IsLossless(num, false), false); + } + + assert.strictEqual(num, TestWords(num)); +}); + +assert.throws(() => CreateTooBigBigInt(), { + name: 'Error', + message: 'Invalid argument', +}); + +// Test that we correctly forward exceptions from the engine. +assert.throws(() => MakeBigIntWordsThrow(), { + name: 'RangeError', + message: 'Maximum BigInt size exceeded', +}); diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/test_bigint.c b/Tests/NodeApi/test/js-native-api/test_bigint/test_bigint.c new file mode 100644 index 00000000..203bc3a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/test_bigint.c @@ -0,0 +1,159 @@ +#include +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value IsLossless(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool is_signed; + NODE_API_CALL(env, napi_get_value_bool(env, args[1], &is_signed)); + + bool lossless; + + if (is_signed) { + int64_t input; + NODE_API_CALL(env, napi_get_value_bigint_int64(env, args[0], &input, &lossless)); + } else { + uint64_t input; + NODE_API_CALL(env, napi_get_value_bigint_uint64(env, args[0], &input, &lossless)); + } + + napi_value output; + NODE_API_CALL(env, napi_get_boolean(env, lossless, &output)); + + return output; +} + +static napi_value TestInt64(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_bigint, + "Wrong type of arguments. Expects a bigint as first argument."); + + int64_t input; + bool lossless; + NODE_API_CALL(env, napi_get_value_bigint_int64(env, args[0], &input, &lossless)); + + napi_value output; + NODE_API_CALL(env, napi_create_bigint_int64(env, input, &output)); + + return output; +} + +static napi_value TestUint64(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_bigint, + "Wrong type of arguments. Expects a bigint as first argument."); + + uint64_t input; + bool lossless; + NODE_API_CALL(env, napi_get_value_bigint_uint64( + env, args[0], &input, &lossless)); + + napi_value output; + NODE_API_CALL(env, napi_create_bigint_uint64(env, input, &output)); + + return output; +} + +static napi_value TestWords(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_bigint, + "Wrong type of arguments. Expects a bigint as first argument."); + + size_t expected_word_count; + NODE_API_CALL(env, napi_get_value_bigint_words( + env, args[0], NULL, &expected_word_count, NULL)); + + int sign_bit; + size_t word_count = 10; + uint64_t words[10]; + + NODE_API_CALL(env, napi_get_value_bigint_words( + env, args[0], &sign_bit, &word_count, words)); + + NODE_API_ASSERT(env, word_count == expected_word_count, + "word counts do not match"); + + napi_value output; + NODE_API_CALL(env, napi_create_bigint_words( + env, sign_bit, word_count, words, &output)); + + return output; +} + +// throws RangeError +static napi_value CreateTooBigBigInt(napi_env env, napi_callback_info info) { + int sign_bit = 0; + size_t word_count = SIZE_MAX; + uint64_t words[10] = {0}; + + napi_value output; + + NODE_API_CALL(env, napi_create_bigint_words( + env, sign_bit, word_count, words, &output)); + + return output; +} + +// Test that we correctly forward exceptions from the engine. +static napi_value MakeBigIntWordsThrow(napi_env env, napi_callback_info info) { + uint64_t words[10] = {0}; + napi_value output; + + napi_status status = napi_create_bigint_words(env, + 0, + INT_MAX, + words, + &output); + if (status != napi_pending_exception) + napi_throw_error(env, NULL, "Expected status `napi_pending_exception`"); + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("IsLossless", IsLossless), + DECLARE_NODE_API_PROPERTY("TestInt64", TestInt64), + DECLARE_NODE_API_PROPERTY("TestUint64", TestUint64), + DECLARE_NODE_API_PROPERTY("TestWords", TestWords), + DECLARE_NODE_API_PROPERTY("CreateTooBigBigInt", CreateTooBigBigInt), + DECLARE_NODE_API_PROPERTY("MakeBigIntWordsThrow", MakeBigIntWordsThrow), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/CMakeLists.txt new file mode 100644 index 00000000..8ef5603d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_bigint_unsupported + SOURCES + test_bigint_unsupported.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/binding.gyp b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/binding.gyp new file mode 100644 index 00000000..897ff4bb --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_bigint_unsupported", + "sources": [ + "test_bigint_unsupported.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/test.js b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/test.js new file mode 100644 index 00000000..fe550dd0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/test.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const { + CreateBigIntExpectThrow, +} = require(`./build/${common.buildType}/test_bigint_unsupported`); + +// On engines without BigInt (jsc-android ~2020, Win10 Chakra) the BigInt create API throws a +// JS-catchable ENOTSUP. The standard test_bigint can't run on these engines at all -- its `0n` +// literals raise a SyntaxError at parse time -- so this is the feature-detection fallback. +let threw = false; +try { + CreateBigIntExpectThrow(); +} catch (err) { + threw = true; + assert.strictEqual(err.code, 'ENOTSUP'); +} +assert.strictEqual(threw, true, 'expected napi_create_bigint_int64 to throw ENOTSUP'); diff --git a/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/test_bigint_unsupported.c b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/test_bigint_unsupported.c new file mode 100644 index 00000000..75fa35fc --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint_unsupported/test_bigint_unsupported.c @@ -0,0 +1,30 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +// Feature-detection fallback for the standard test_bigint. Engines without BigInt (jsc-android ~2020, +// whose parser even rejects `0n` literals, and the Win10 OS Chakra) must report the capability gap via +// a JS-catchable ENOTSUP exception from the BigInt create API rather than failing silently or crashing. +static napi_value CreateBigIntExpectThrow(napi_env env, napi_callback_info info) { + (void)info; + napi_value result = NULL; + // On a BigInt-less engine this throws ENOTSUP (a pending exception); let it propagate to JS land, + // where test.js asserts on the error code. (No `0n` literal here, so the script itself parses.) + napi_create_bigint_int64(env, 42, &result); + (void)result; + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("CreateBigIntExpectThrow", CreateBigIntExpectThrow), + }; + + NODE_API_CALL(env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/CMakeLists.txt new file mode 100644 index 00000000..c708e9cb --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/CMakeLists.txt @@ -0,0 +1,13 @@ +add_node_api_module(test_cannot_run_js + SOURCES + test_cannot_run_js.c + DEFINES + "NAPI_VERSION=10" +) + +add_node_api_module(test_pending_exception + SOURCES + test_cannot_run_js.c + DEFINES + "NAPI_VERSION=9" +) diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/binding.gyp b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/binding.gyp new file mode 100644 index 00000000..51ff8ccb --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/binding.gyp @@ -0,0 +1,18 @@ +{ + "targets": [ + { + "target_name": "test_cannot_run_js", + "sources": [ + "test_cannot_run_js.c" + ], + "defines": [ "NAPI_VERSION=10" ], + }, + { + "target_name": "test_pending_exception", + "sources": [ + "test_cannot_run_js.c" + ], + "defines": [ "NAPI_VERSION=9" ], + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test.js b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test.js new file mode 100644 index 00000000..31c82480 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test.js @@ -0,0 +1,24 @@ +'use strict'; + +// Test that `napi_call_function()` returns `napi_cannot_run_js` in experimental +// mode and `napi_pending_exception` otherwise. This test calls the add-on's +// `createRef()` method, which creates a strong reference to a JS function. When +// the process exits, it calls all reference finalizers. The finalizer for the +// strong reference created herein will attempt to call `napi_get_property()` on +// a property of the global object and will abort the process if the API doesn't +// return the correct status. + +const { buildType, mustNotCall } = require('../../common'); +const addon_v8 = require(`./build/${buildType}/test_pending_exception`); +const addon_new = require(`./build/${buildType}/test_cannot_run_js`); + +function runTests(addon, isVersion8) { + addon.createRef(mustNotCall()); +} + +function runAllTests() { + runTests(addon_v8, /* isVersion8 */ true); + runTests(addon_new, /* isVersion8 */ false); +} + +runAllTests(); diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c new file mode 100644 index 00000000..8ca44c23 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c @@ -0,0 +1,66 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "stdlib.h" + +static void Finalize(napi_env env, void* data, void* hint) { + napi_value global, set_timeout; + napi_ref* ref = data; + + NODE_API_BASIC_ASSERT_RETURN_VOID( + napi_delete_reference(env, *ref) == napi_ok, + "deleting reference in finalizer should succeed"); + NODE_API_BASIC_ASSERT_RETURN_VOID( + napi_get_global(env, &global) == napi_ok, + "getting global reference in finalizer should succeed"); + napi_status result = + napi_get_named_property(env, global, "setTimeout", &set_timeout); + + // The finalizer could be invoked either from check callbacks (as native + // immediates) if the event loop is still running (where napi_ok is returned) + // or during environment shutdown (where napi_cannot_run_js or + // napi_pending_exception is returned). This is not deterministic from + // the point of view of the addon. + +#if NAPI_VERSION > 9 + NODE_API_BASIC_ASSERT_RETURN_VOID( + result == napi_cannot_run_js || result == napi_ok, + "getting named property from global in finalizer should succeed " + "or return napi_cannot_run_js"); +#else + NODE_API_BASIC_ASSERT_RETURN_VOID( + result == napi_pending_exception || result == napi_ok, + "getting named property from global in finalizer should succeed " + "or return napi_pending_exception"); +#endif // NAPI_VERSION > 9 + free(ref); +} + +static napi_value CreateRef(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value cb; + napi_valuetype value_type; + napi_ref* ref = malloc(sizeof(*ref)); + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &cb, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Function takes only one argument"); + NODE_API_CALL(env, napi_typeof(env, cb, &value_type)); + NODE_API_ASSERT( + env, value_type == napi_function, "argument must be function"); + NODE_API_CALL(env, napi_add_finalizer(env, cb, ref, Finalize, NULL, ref)); + return cb; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("createRef", CreateRef), + }; + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_constructor/CMakeLists.txt new file mode 100644 index 00000000..0582345e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(test_constructor + SOURCES + test_constructor.c + test_null.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/binding.gyp b/Tests/NodeApi/test/js-native-api/test_constructor/binding.gyp new file mode 100644 index 00000000..af0c5d10 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_constructor", + "sources": [ + "test_constructor.c", + "test_null.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test.js b/Tests/NodeApi/test/js-native-api/test_constructor/test.js new file mode 100644 index 00000000..4ef41794 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test.js @@ -0,0 +1,62 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +const getterOnlyErrorRE = + /^TypeError: Cannot (set|assign to) property .*( of #<.*>)? which has only a getter$/; + +// Testing api calls for a constructor that defines properties +const TestConstructor = require(`./build/${common.buildType}/test_constructor`); +const test_object = new TestConstructor(); + +assert.strictEqual(test_object.echo('hello'), 'hello'); + +test_object.readwriteValue = 1; +assert.strictEqual(test_object.readwriteValue, 1); +test_object.readwriteValue = 2; +assert.strictEqual(test_object.readwriteValue, 2); + +assert.throws(() => { test_object.readonlyValue = 3; }, + /^TypeError: Cannot assign to read(-| )only property 'readonlyValue'.*(of object '#')?/); + +assert.ok(test_object.hiddenValue); + +// Properties with napi_enumerable attribute should be enumerable. +const propertyNames = []; +for (const name in test_object) { + propertyNames.push(name); +} +assert.ok(propertyNames.includes('echo')); +assert.ok(propertyNames.includes('readwriteValue')); +assert.ok(propertyNames.includes('readonlyValue')); +assert.ok(!propertyNames.includes('hiddenValue')); +assert.ok(!propertyNames.includes('readwriteAccessor1')); +assert.ok(!propertyNames.includes('readwriteAccessor2')); +assert.ok(!propertyNames.includes('readonlyAccessor1')); +assert.ok(!propertyNames.includes('readonlyAccessor2')); + +// The napi_writable attribute should be ignored for accessors. +test_object.readwriteAccessor1 = 1; +assert.strictEqual(test_object.readwriteAccessor1, 1); +assert.strictEqual(test_object.readonlyAccessor1, 1); +assert.throws(() => { test_object.readonlyAccessor1 = 3; }, getterOnlyErrorRE); +test_object.readwriteAccessor2 = 2; +assert.strictEqual(test_object.readwriteAccessor2, 2); +assert.strictEqual(test_object.readonlyAccessor2, 2); +assert.throws(() => { test_object.readonlyAccessor2 = 3; }, getterOnlyErrorRE); + +// Validate that static properties are on the class as opposed +// to the instance +assert.strictEqual(TestConstructor.staticReadonlyAccessor1, 10); +assert.strictEqual(test_object.staticReadonlyAccessor1, undefined); + +// Verify that passing NULL to napi_define_class() results in the correct +// error. +assert.deepStrictEqual(TestConstructor.TestDefineClass(), { + envIsNull: 'Invalid argument', + nameIsNull: 'Invalid argument', + cbIsNull: 'Invalid argument', + cbDataIsNull: 'napi_ok', + propertiesIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument' +}); diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test2.js b/Tests/NodeApi/test/js-native-api/test_constructor/test2.js new file mode 100644 index 00000000..125af81c --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test2.js @@ -0,0 +1,8 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for a constructor that defines properties +const TestConstructor = + require(`./build/${common.buildType}/test_constructor`).constructorName; +assert.strictEqual(TestConstructor.name, 'MyObject'); diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_constructor.c b/Tests/NodeApi/test/js-native-api/test_constructor/test_constructor.c new file mode 100644 index 00000000..0c52bc31 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_constructor.c @@ -0,0 +1,200 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static double value_ = 1; +static double static_value_ = 10; + +static napi_value TestDefineClass(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value result, return_value; + + napi_property_descriptor property_descriptor = { + "TestDefineClass", + NULL, + TestDefineClass, + NULL, + NULL, + NULL, + napi_enumerable | napi_static, + NULL}; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + status = napi_define_class(NULL, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + &result); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + status); + + napi_define_class(env, + NULL, + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + &result); + + add_last_status(env, "nameIsNull", return_value); + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + NULL, + NULL, + 1, + &property_descriptor, + &result); + + add_last_status(env, "cbIsNull", return_value); + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + &result); + + add_last_status(env, "cbDataIsNull", return_value); + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + NULL, + &result); + + add_last_status(env, "propertiesIsNull", return_value); + + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + NULL); + + add_last_status(env, "resultIsNull", return_value); + + return return_value; +} + +static napi_value GetValue(napi_env env, napi_callback_info info) { + size_t argc = 0; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, NULL, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 0, "Wrong number of arguments"); + + napi_value number; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + return number; +} + +static napi_value SetValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value_)); + + return NULL; +} + +static napi_value Echo(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + return args[0]; +} + +static napi_value New(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, NULL, NULL, &_this, NULL)); + + return _this; +} + +static napi_value GetStaticValue(napi_env env, napi_callback_info info) { + size_t argc = 0; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, NULL, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 0, "Wrong number of arguments"); + + napi_value number; + NODE_API_CALL(env, napi_create_double(env, static_value_, &number)); + + return number; +} + + +static napi_value NewExtra(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, NULL, NULL, &_this, NULL)); + + return _this; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value number, cons; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + NODE_API_CALL(env, napi_define_class( + env, "MyObject_Extra", 8, NewExtra, NULL, 0, NULL, &cons)); + + napi_property_descriptor properties[] = { + { "echo", NULL, Echo, NULL, NULL, NULL, napi_enumerable, NULL }, + { "readwriteValue", NULL, NULL, NULL, NULL, number, + napi_enumerable | napi_writable, NULL }, + { "readonlyValue", NULL, NULL, NULL, NULL, number, napi_enumerable, + NULL }, + { "hiddenValue", NULL, NULL, NULL, NULL, number, napi_default, NULL }, + { "readwriteAccessor1", NULL, NULL, GetValue, SetValue, NULL, napi_default, + NULL }, + { "readwriteAccessor2", NULL, NULL, GetValue, SetValue, NULL, + napi_writable, NULL }, + { "readonlyAccessor1", NULL, NULL, GetValue, NULL, NULL, napi_default, + NULL }, + { "readonlyAccessor2", NULL, NULL, GetValue, NULL, NULL, napi_writable, + NULL }, + { "staticReadonlyAccessor1", NULL, NULL, GetStaticValue, NULL, NULL, + napi_default | napi_static, NULL}, + { "constructorName", NULL, NULL, NULL, NULL, cons, + napi_enumerable | napi_static, NULL }, + { "TestDefineClass", NULL, TestDefineClass, NULL, NULL, NULL, + napi_enumerable | napi_static, NULL }, + }; + + NODE_API_CALL(env, napi_define_class(env, "MyObject", NAPI_AUTO_LENGTH, New, + NULL, sizeof(properties)/sizeof(*properties), properties, &cons)); + + init_test_null(env, cons); + + return cons; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_null.c b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.c new file mode 100644 index 00000000..acbe5982 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.c @@ -0,0 +1,111 @@ +#include + +#include "../common.h" +#include "test_null.h" + +static int some_data = 0; + +static napi_value TestConstructor(napi_env env, napi_callback_info info) { + return NULL; +} + +static napi_value TestDefineClass(napi_env env, napi_callback_info info) { + napi_value return_value, cons; + + const napi_property_descriptor prop = + DECLARE_NODE_API_PROPERTY("testConstructor", TestConstructor); + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_define_class(NULL, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + &prop, + &cons)); + + napi_define_class(env, + NULL, + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + &prop, + &cons); + add_last_status(env, "nameIsNull", return_value); + + napi_define_class( + env, "TestClass", 0, TestConstructor, &some_data, 1, &prop, &cons); + add_last_status(env, "lengthIsZero", return_value); + + napi_define_class( + env, "TestClass", NAPI_AUTO_LENGTH, NULL, &some_data, 1, &prop, &cons); + add_last_status(env, "nativeSideIsNull", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + NULL, + 1, + &prop, + &cons); + add_last_status(env, "dataIsNull", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 0, + &prop, + &cons); + add_last_status(env, "propsLengthIsZero", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + NULL, + &cons); + add_last_status(env, "propsIsNull", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + &prop, + NULL); + add_last_status(env, "resultIsNull", return_value); + + return return_value; +} + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("testDefineClass", TestDefineClass), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID( + env, + napi_define_properties(env, + test_null, + sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "testNull", test_null)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_null.h b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.h new file mode 100644 index 00000000..b142570d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_null.js b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.js new file mode 100644 index 00000000..f944953e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Test passing NULL to object-related N-APIs. +const { testNull } = require(`./build/${common.buildType}/test_constructor`); +const expectedResult = { + envIsNull: 'Invalid argument', + nameIsNull: 'Invalid argument', + lengthIsZero: 'napi_ok', + nativeSideIsNull: 'Invalid argument', + dataIsNull: 'napi_ok', + propsLengthIsZero: 'napi_ok', + propsIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}; + +assert.deepStrictEqual(testNull.testDefineClass(), expectedResult); diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_conversions/CMakeLists.txt new file mode 100644 index 00000000..732de7c6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(test_conversions + SOURCES + test_conversions.c + test_null.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/binding.gyp b/Tests/NodeApi/test/js-native-api/test_conversions/binding.gyp new file mode 100644 index 00000000..a7be5290 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_conversions", + "sources": [ + "test_conversions.c", + "test_null.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test.js b/Tests/NodeApi/test/js-native-api/test_conversions/test.js new file mode 100644 index 00000000..b5d047a4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test.js @@ -0,0 +1,218 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const test = require(`./build/${common.buildType}/test_conversions`); + +const boolExpected = /boolean was expected/; +const numberExpected = /number was expected/; +const stringExpected = /string was expected/; + +const testSym = Symbol('test'); + +assert.strictEqual(test.asBool(false), false); +assert.strictEqual(test.asBool(true), true); +assert.throws(() => test.asBool(undefined), boolExpected); +assert.throws(() => test.asBool(null), boolExpected); +assert.throws(() => test.asBool(Number.NaN), boolExpected); +assert.throws(() => test.asBool(0), boolExpected); +assert.throws(() => test.asBool(''), boolExpected); +assert.throws(() => test.asBool('0'), boolExpected); +assert.throws(() => test.asBool(1), boolExpected); +assert.throws(() => test.asBool('1'), boolExpected); +assert.throws(() => test.asBool('true'), boolExpected); +assert.throws(() => test.asBool({}), boolExpected); +assert.throws(() => test.asBool([]), boolExpected); +assert.throws(() => test.asBool(testSym), boolExpected); + +[test.asInt32, test.asUInt32, test.asInt64].forEach((asInt) => { + assert.strictEqual(asInt(0), 0); + assert.strictEqual(asInt(1), 1); + assert.strictEqual(asInt(1.0), 1); + assert.strictEqual(asInt(1.1), 1); + assert.strictEqual(asInt(1.9), 1); + assert.strictEqual(asInt(0.9), 0); + assert.strictEqual(asInt(999.9), 999); + assert.strictEqual(asInt(Number.NaN), 0); + assert.throws(() => asInt(undefined), numberExpected); + assert.throws(() => asInt(null), numberExpected); + assert.throws(() => asInt(false), numberExpected); + assert.throws(() => asInt(''), numberExpected); + assert.throws(() => asInt('1'), numberExpected); + assert.throws(() => asInt({}), numberExpected); + assert.throws(() => asInt([]), numberExpected); + assert.throws(() => asInt(testSym), numberExpected); +}); + +assert.strictEqual(test.asInt32(-1), -1); +assert.strictEqual(test.asInt64(-1), -1); +assert.strictEqual(test.asUInt32(-1), Math.pow(2, 32) - 1); + +assert.strictEqual(test.asDouble(0), 0); +assert.strictEqual(test.asDouble(1), 1); +assert.strictEqual(test.asDouble(1.0), 1.0); +assert.strictEqual(test.asDouble(1.1), 1.1); +assert.strictEqual(test.asDouble(1.9), 1.9); +assert.strictEqual(test.asDouble(0.9), 0.9); +assert.strictEqual(test.asDouble(999.9), 999.9); +assert.strictEqual(test.asDouble(-1), -1); +assert.ok(Number.isNaN(test.asDouble(Number.NaN))); +assert.throws(() => test.asDouble(undefined), numberExpected); +assert.throws(() => test.asDouble(null), numberExpected); +assert.throws(() => test.asDouble(false), numberExpected); +assert.throws(() => test.asDouble(''), numberExpected); +assert.throws(() => test.asDouble('1'), numberExpected); +assert.throws(() => test.asDouble({}), numberExpected); +assert.throws(() => test.asDouble([]), numberExpected); +assert.throws(() => test.asDouble(testSym), numberExpected); + +assert.strictEqual(test.asString(''), ''); +assert.strictEqual(test.asString('test'), 'test'); +assert.throws(() => test.asString(undefined), stringExpected); +assert.throws(() => test.asString(null), stringExpected); +assert.throws(() => test.asString(false), stringExpected); +assert.throws(() => test.asString(1), stringExpected); +assert.throws(() => test.asString(1.1), stringExpected); +assert.throws(() => test.asString(Number.NaN), stringExpected); +assert.throws(() => test.asString({}), stringExpected); +assert.throws(() => test.asString([]), stringExpected); +assert.throws(() => test.asString(testSym), stringExpected); + +assert.strictEqual(test.toBool(true), true); +assert.strictEqual(test.toBool(1), true); +assert.strictEqual(test.toBool(-1), true); +assert.strictEqual(test.toBool('true'), true); +assert.strictEqual(test.toBool('false'), true); +assert.strictEqual(test.toBool({}), true); +assert.strictEqual(test.toBool([]), true); +assert.strictEqual(test.toBool(testSym), true); +assert.strictEqual(test.toBool(false), false); +assert.strictEqual(test.toBool(undefined), false); +assert.strictEqual(test.toBool(null), false); +assert.strictEqual(test.toBool(0), false); +assert.strictEqual(test.toBool(Number.NaN), false); +assert.strictEqual(test.toBool(''), false); + +assert.strictEqual(test.toNumber(0), 0); +assert.strictEqual(test.toNumber(1), 1); +assert.strictEqual(test.toNumber(1.1), 1.1); +assert.strictEqual(test.toNumber(-1), -1); +assert.strictEqual(test.toNumber('0'), 0); +assert.strictEqual(test.toNumber('1'), 1); +assert.strictEqual(test.toNumber('1.1'), 1.1); +assert.strictEqual(test.toNumber([]), 0); +assert.strictEqual(test.toNumber(false), 0); +assert.strictEqual(test.toNumber(null), 0); +assert.strictEqual(test.toNumber(''), 0); +assert.ok(Number.isNaN(test.toNumber(Number.NaN))); +assert.ok(Number.isNaN(test.toNumber({}))); +assert.ok(Number.isNaN(test.toNumber(undefined))); +assert.throws(() => test.toNumber(testSym), TypeError); + +assert.deepStrictEqual({}, test.toObject({})); +assert.deepStrictEqual({ 'test': 1 }, test.toObject({ 'test': 1 })); +assert.deepStrictEqual([], test.toObject([])); +assert.deepStrictEqual([ 1, 2, 3 ], test.toObject([ 1, 2, 3 ])); +assert.deepStrictEqual(new Boolean(false), test.toObject(false)); +assert.deepStrictEqual(new Boolean(true), test.toObject(true)); +assert.deepStrictEqual(new String(''), test.toObject('')); +assert.deepStrictEqual(new Number(0), test.toObject(0)); +assert.deepStrictEqual(new Number(Number.NaN), test.toObject(Number.NaN)); +assert.deepStrictEqual(new Object(testSym), test.toObject(testSym)); +assert.notStrictEqual(test.toObject(false), false); +assert.notStrictEqual(test.toObject(true), true); +assert.notStrictEqual(test.toObject(''), ''); +assert.notStrictEqual(test.toObject(0), 0); +assert.ok(!Number.isNaN(test.toObject(Number.NaN))); + +assert.strictEqual(test.toString(''), ''); +assert.strictEqual(test.toString('test'), 'test'); +assert.strictEqual(test.toString(undefined), 'undefined'); +assert.strictEqual(test.toString(null), 'null'); +assert.strictEqual(test.toString(false), 'false'); +assert.strictEqual(test.toString(true), 'true'); +assert.strictEqual(test.toString(0), '0'); +assert.strictEqual(test.toString(1.1), '1.1'); +assert.strictEqual(test.toString(Number.NaN), 'NaN'); +assert.strictEqual(test.toString({}), '[object Object]'); +assert.strictEqual(test.toString({ toString: () => 'test' }), 'test'); +assert.strictEqual(test.toString([]), ''); +assert.strictEqual(test.toString([ 1, 2, 3 ]), '1,2,3'); +assert.throws(() => test.toString(testSym), TypeError); + +assert.deepStrictEqual(test.testNull.getValueBool(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A boolean was expected', +}); + +assert.deepStrictEqual(test.testNull.getValueInt32(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + +assert.deepStrictEqual(test.testNull.getValueUint32(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + +assert.deepStrictEqual(test.testNull.getValueInt64(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + + +assert.deepStrictEqual(test.testNull.getValueDouble(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + +assert.deepStrictEqual(test.testNull.coerceToBool(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'napi_ok', +}); + +assert.deepStrictEqual(test.testNull.coerceToObject(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'napi_ok', +}); + +assert.deepStrictEqual(test.testNull.coerceToString(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'napi_ok', +}); + +assert.deepStrictEqual(test.testNull.getValueStringUtf8(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + wrongTypeIn: 'A string was expected', + bufAndOutLengthIsNull: 'Invalid argument', +}); + +assert.deepStrictEqual(test.testNull.getValueStringLatin1(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + wrongTypeIn: 'A string was expected', + bufAndOutLengthIsNull: 'Invalid argument', +}); + +assert.deepStrictEqual(test.testNull.getValueStringUtf16(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + wrongTypeIn: 'A string was expected', + bufAndOutLengthIsNull: 'Invalid argument', +}); diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test_conversions.c b/Tests/NodeApi/test/js-native-api/test_conversions/test_conversions.c new file mode 100644 index 00000000..2db42970 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test_conversions.c @@ -0,0 +1,158 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static napi_value AsBool(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool value; + NODE_API_CALL(env, napi_get_value_bool(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_get_boolean(env, value, &output)); + + return output; +} + +static napi_value AsInt32(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t value; + NODE_API_CALL(env, napi_get_value_int32(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_int32(env, value, &output)); + + return output; +} + +static napi_value AsUInt32(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + uint32_t value; + NODE_API_CALL(env, napi_get_value_uint32(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, value, &output)); + + return output; +} + +static napi_value AsInt64(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int64_t value; + NODE_API_CALL(env, napi_get_value_int64(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_int64(env, (double)value, &output)); + + return output; +} + +static napi_value AsDouble(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double value; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_double(env, value, &output)); + + return output; +} + +static napi_value AsString(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char value[100]; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[0], value, sizeof(value), NULL)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_utf8( + env, value, NAPI_AUTO_LENGTH, &output)); + + return output; +} + +static napi_value ToBool(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_bool(env, args[0], &output)); + + return output; +} + +static napi_value ToNumber(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_number(env, args[0], &output)); + + return output; +} + +static napi_value ToObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_object(env, args[0], &output)); + + return output; +} + +static napi_value ToString(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_string(env, args[0], &output)); + + return output; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("asBool", AsBool), + DECLARE_NODE_API_PROPERTY("asInt32", AsInt32), + DECLARE_NODE_API_PROPERTY("asUInt32", AsUInt32), + DECLARE_NODE_API_PROPERTY("asInt64", AsInt64), + DECLARE_NODE_API_PROPERTY("asDouble", AsDouble), + DECLARE_NODE_API_PROPERTY("asString", AsString), + DECLARE_NODE_API_PROPERTY("toBool", ToBool), + DECLARE_NODE_API_PROPERTY("toNumber", ToNumber), + DECLARE_NODE_API_PROPERTY("toObject", ToObject), + DECLARE_NODE_API_PROPERTY("toString", ToString), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + init_test_null(env, exports); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test_null.c b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.c new file mode 100644 index 00000000..e08b986a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.c @@ -0,0 +1,102 @@ +#include + +#include "../common.h" +#include "test_null.h" + +#define GEN_NULL_CHECK_BINDING(binding_name, output_type, api) \ + static napi_value binding_name(napi_env env, napi_callback_info info) { \ + napi_value return_value; \ + output_type result; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + api(NULL, return_value, &result)); \ + api(env, NULL, &result); \ + add_last_status(env, "valueIsNull", return_value); \ + api(env, return_value, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + api(env, return_value, &result); \ + add_last_status(env, "inputTypeCheck", return_value); \ + return return_value; \ + } + +GEN_NULL_CHECK_BINDING(GetValueBool, bool, napi_get_value_bool) +GEN_NULL_CHECK_BINDING(GetValueInt32, int32_t, napi_get_value_int32) +GEN_NULL_CHECK_BINDING(GetValueUint32, uint32_t, napi_get_value_uint32) +GEN_NULL_CHECK_BINDING(GetValueInt64, int64_t, napi_get_value_int64) +GEN_NULL_CHECK_BINDING(GetValueDouble, double, napi_get_value_double) +GEN_NULL_CHECK_BINDING(CoerceToBool, napi_value, napi_coerce_to_bool) +GEN_NULL_CHECK_BINDING(CoerceToObject, napi_value, napi_coerce_to_object) +GEN_NULL_CHECK_BINDING(CoerceToString, napi_value, napi_coerce_to_string) + +#define GEN_NULL_CHECK_STRING_BINDING(binding_name, arg_type, api) \ + static napi_value binding_name(napi_env env, napi_callback_info info) { \ + napi_value return_value; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + arg_type buf1[4]; \ + size_t length1 = 3; \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + api(NULL, return_value, buf1, length1, &length1)); \ + arg_type buf2[4]; \ + size_t length2 = 3; \ + api(env, NULL, buf2, length2, &length2); \ + add_last_status(env, "valueIsNull", return_value); \ + api(env, return_value, NULL, 3, NULL); \ + add_last_status(env, "wrongTypeIn", return_value); \ + napi_value string; \ + NODE_API_CALL(env, \ + napi_create_string_utf8(env, \ + "Something", \ + NAPI_AUTO_LENGTH, \ + &string)); \ + api(env, string, NULL, 3, NULL); \ + add_last_status(env, "bufAndOutLengthIsNull", return_value); \ + return return_value; \ + } + +GEN_NULL_CHECK_STRING_BINDING(GetValueStringUtf8, + char, + napi_get_value_string_utf8) +GEN_NULL_CHECK_STRING_BINDING(GetValueStringLatin1, + char, + napi_get_value_string_latin1) +GEN_NULL_CHECK_STRING_BINDING(GetValueStringUtf16, + char16_t, + napi_get_value_string_utf16) + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("getValueBool", GetValueBool), + DECLARE_NODE_API_PROPERTY("getValueInt32", GetValueInt32), + DECLARE_NODE_API_PROPERTY("getValueUint32", GetValueUint32), + DECLARE_NODE_API_PROPERTY("getValueInt64", GetValueInt64), + DECLARE_NODE_API_PROPERTY("getValueDouble", GetValueDouble), + DECLARE_NODE_API_PROPERTY("coerceToBool", CoerceToBool), + DECLARE_NODE_API_PROPERTY("coerceToObject", CoerceToObject), + DECLARE_NODE_API_PROPERTY("coerceToString", CoerceToString), + DECLARE_NODE_API_PROPERTY("getValueStringUtf8", GetValueStringUtf8), + DECLARE_NODE_API_PROPERTY("getValueStringLatin1", GetValueStringLatin1), + DECLARE_NODE_API_PROPERTY("getValueStringUtf16", GetValueStringUtf16), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID(env, napi_define_properties( + env, test_null, sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + const napi_property_descriptor test_null_set = { + "testNull", NULL, NULL, NULL, NULL, test_null, napi_enumerable, NULL + }; + + NODE_API_CALL_RETURN_VOID(env, + napi_define_properties(env, exports, 1, &test_null_set)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test_null.h b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.h new file mode 100644 index 00000000..fe6ad77a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_CONVERSIONS_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_CONVERSIONS_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_CONVERSIONS_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_dataview/CMakeLists.txt new file mode 100644 index 00000000..d809ba9a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_dataview + SOURCES + test_dataview.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/binding.gyp b/Tests/NodeApi/test/js-native-api/test_dataview/binding.gyp new file mode 100644 index 00000000..a8b4f1d4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_dataview", + "sources": [ + "test_dataview.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/test.js b/Tests/NodeApi/test/js-native-api/test_dataview/test.js new file mode 100644 index 00000000..2bfd109d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/test.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for arrays +const test_dataview = require(`./build/${common.buildType}/test_dataview`); + +// Test for creating dataview +{ + const buffer = new ArrayBuffer(128); + const template = Reflect.construct(DataView, [buffer]); + + const theDataview = test_dataview.CreateDataViewFromJSDataView(template); + assert.ok(theDataview instanceof DataView, + `Expect ${theDataview} to be a DataView`); +} + +// Test for creating dataview with invalid range +{ + const buffer = new ArrayBuffer(128); + assert.throws(() => { + test_dataview.CreateDataView(buffer, 10, 200); + }, RangeError); +} diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/test_dataview.c b/Tests/NodeApi/test/js-native-api/test_dataview/test_dataview.c new file mode 100644 index 00000000..20a840de --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/test_dataview.c @@ -0,0 +1,102 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value CreateDataView(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args [3]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 3, "Wrong number of arguments"); + + napi_valuetype valuetype0; + napi_value arraybuffer = args[0]; + + NODE_API_CALL(env, napi_typeof(env, arraybuffer, &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects a ArrayBuffer as the first " + "argument."); + + bool is_arraybuffer; + NODE_API_CALL(env, napi_is_arraybuffer(env, arraybuffer, &is_arraybuffer)); + NODE_API_ASSERT(env, is_arraybuffer, + "Wrong type of arguments. Expects a ArrayBuffer as the first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects a number as second argument."); + + size_t byte_offset = 0; + NODE_API_CALL(env, napi_get_value_uint32(env, args[1], (uint32_t*)(&byte_offset))); + + napi_valuetype valuetype2; + NODE_API_CALL(env, napi_typeof(env, args[2], &valuetype2)); + + NODE_API_ASSERT(env, valuetype2 == napi_number, + "Wrong type of arguments. Expects a number as third argument."); + + size_t length = 0; + NODE_API_CALL(env, napi_get_value_uint32(env, args[2], (uint32_t*)(&length))); + + napi_value output_dataview; + NODE_API_CALL(env, + napi_create_dataview(env, length, arraybuffer, + byte_offset, &output_dataview)); + + return output_dataview; +} + +static napi_value CreateDataViewFromJSDataView(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args [1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + napi_valuetype valuetype; + napi_value input_dataview = args[0]; + + NODE_API_CALL(env, napi_typeof(env, input_dataview, &valuetype)); + NODE_API_ASSERT(env, valuetype == napi_object, + "Wrong type of arguments. Expects a DataView as the first " + "argument."); + + bool is_dataview; + NODE_API_CALL(env, napi_is_dataview(env, input_dataview, &is_dataview)); + NODE_API_ASSERT(env, is_dataview, + "Wrong type of arguments. Expects a DataView as the first " + "argument."); + size_t byte_offset = 0; + size_t length = 0; + napi_value buffer; + NODE_API_CALL(env, + napi_get_dataview_info(env, input_dataview, &length, NULL, + &buffer, &byte_offset)); + + napi_value output_dataview; + NODE_API_CALL(env, + napi_create_dataview(env, length, buffer, + byte_offset, &output_dataview)); + + + return output_dataview; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("CreateDataView", CreateDataView), + DECLARE_NODE_API_PROPERTY("CreateDataViewFromJSDataView", + CreateDataViewFromJSDataView) + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_date/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_date/CMakeLists.txt new file mode 100644 index 00000000..9c9736c8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_date + SOURCES + test_date.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_date/binding.gyp b/Tests/NodeApi/test/js-native-api/test_date/binding.gyp new file mode 100644 index 00000000..e08eaf6d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_date", + "sources": [ + "test_date.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_date/test.js b/Tests/NodeApi/test/js-native-api/test_date/test.js new file mode 100644 index 00000000..637f9f94 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/test.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../../common'); + +// This tests the date-related n-api calls + +const assert = require('assert'); +const test_date = require(`./build/${common.buildType}/test_date`); + +const dateTypeTestDate = test_date.createDate(1549183351); +assert.strictEqual(test_date.isDate(dateTypeTestDate), true); + +assert.strictEqual(test_date.isDate(new Date(1549183351)), true); + +assert.strictEqual(test_date.isDate(2.4), false); +assert.strictEqual(test_date.isDate('not a date'), false); +assert.strictEqual(test_date.isDate(undefined), false); +assert.strictEqual(test_date.isDate(null), false); +assert.strictEqual(test_date.isDate({}), false); + +assert.strictEqual(test_date.getDateValue(new Date(1549183351)), 1549183351); diff --git a/Tests/NodeApi/test/js-native-api/test_date/test_date.c b/Tests/NodeApi/test/js-native-api/test_date/test_date.c new file mode 100644 index 00000000..a9eeb4f0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/test_date.c @@ -0,0 +1,64 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value createDate(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + double time; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &time)); + + napi_value date; + NODE_API_CALL(env, napi_create_date(env, time, &date)); + + return date; +} + +static napi_value isDate(napi_env env, napi_callback_info info) { + napi_value date, result; + size_t argc = 1; + bool is_date; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &date, NULL, NULL)); + NODE_API_CALL(env, napi_is_date(env, date, &is_date)); + NODE_API_CALL(env, napi_get_boolean(env, is_date, &result)); + + return result; +} + +static napi_value getDateValue(napi_env env, napi_callback_info info) { + napi_value date, result; + size_t argc = 1; + double value; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &date, NULL, NULL)); + NODE_API_CALL(env, napi_get_date_value(env, date, &value)); + NODE_API_CALL(env, napi_create_double(env, value, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("createDate", createDate), + DECLARE_NODE_API_PROPERTY("isDate", isDate), + DECLARE_NODE_API_PROPERTY("getDateValue", getDateValue), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_error/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_error/CMakeLists.txt new file mode 100644 index 00000000..955fd2a0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/CMakeLists.txt @@ -0,0 +1,6 @@ +add_node_api_module(test_error + SOURCES + test_error.c + DEFINES + "NAPI_VERSION=9" +) diff --git a/Tests/NodeApi/test/js-native-api/test_error/binding.gyp b/Tests/NodeApi/test/js-native-api/test_error/binding.gyp new file mode 100644 index 00000000..f0448028 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_error", + "sources": [ + "test_error.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_error/test.js b/Tests/NodeApi/test/js-native-api/test_error/test.js new file mode 100644 index 00000000..f6ba3799 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/test.js @@ -0,0 +1,148 @@ +'use strict'; + +const common = require('../../common'); +const test_error = require(`./build/${common.buildType}/test_error`); +const assert = require('assert'); +const theError = new Error('Some error'); +const theTypeError = new TypeError('Some type error'); +const theSyntaxError = new SyntaxError('Some syntax error'); +const theRangeError = new RangeError('Some type error'); +const theReferenceError = new ReferenceError('Some reference error'); +const theURIError = new URIError('Some URI error'); +const theEvalError = new EvalError('Some eval error'); + +class MyError extends Error { } +const myError = new MyError('Some MyError'); + +// Test that native error object is correctly classed +assert.strictEqual(test_error.checkError(theError), true); + +// Test that native type error object is correctly classed +assert.strictEqual(test_error.checkError(theTypeError), true); + +// Test that native syntax error object is correctly classed +assert.strictEqual(test_error.checkError(theSyntaxError), true); + +// Test that native range error object is correctly classed +assert.strictEqual(test_error.checkError(theRangeError), true); + +// Test that native reference error object is correctly classed +assert.strictEqual(test_error.checkError(theReferenceError), true); + +// Test that native URI error object is correctly classed +assert.strictEqual(test_error.checkError(theURIError), true); + +// Test that native eval error object is correctly classed +assert.strictEqual(test_error.checkError(theEvalError), true); + +// Test that class derived from native error is correctly classed +assert.strictEqual(test_error.checkError(myError), true); + +// Test that non-error object is correctly classed +assert.strictEqual(test_error.checkError({}), false); + +// Test that non-error primitive is correctly classed +assert.strictEqual(test_error.checkError('non-object'), false); + +assert.throws(() => { + test_error.throwExistingError(); +}, /^Error: existing error$/); + +assert.throws(() => { + test_error.throwError(); +}, /^Error: error$/); + +assert.throws(() => { + test_error.throwRangeError(); +}, /^RangeError: range error$/); + +assert.throws(() => { + test_error.throwTypeError(); +}, /^TypeError: type error$/); + +assert.throws(() => { + test_error.throwSyntaxError(); +}, /^SyntaxError: syntax error$/); + +[42, {}, [], Symbol('xyzzy'), true, 'ball', undefined, null, NaN] + .forEach((value) => assert.throws( + () => test_error.throwArbitrary(value), + (err) => { + assert.strictEqual(err, value); + return true; + }, + )); + +assert.throws( + () => test_error.throwErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'Error [error]', + }); + +assert.throws( + () => test_error.throwRangeErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'RangeError [range error]', + }); + +assert.throws( + () => test_error.throwTypeErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'TypeError [type error]', + }); + +assert.throws( + () => test_error.throwSyntaxErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'SyntaxError [syntax error]', + }); + +let error = test_error.createError(); +assert.ok(error instanceof Error, 'expected error to be an instance of Error'); +assert.strictEqual(error.message, 'error'); + +error = test_error.createRangeError(); +assert.ok(error instanceof RangeError, + 'expected error to be an instance of RangeError'); +assert.strictEqual(error.message, 'range error'); + +error = test_error.createTypeError(); +assert.ok(error instanceof TypeError, + 'expected error to be an instance of TypeError'); +assert.strictEqual(error.message, 'type error'); + +error = test_error.createSyntaxError(); +assert.ok(error instanceof SyntaxError, + 'expected error to be an instance of SyntaxError'); +assert.strictEqual(error.message, 'syntax error'); + +error = test_error.createErrorCode(); +assert.ok(error instanceof Error, 'expected error to be an instance of Error'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.message, 'Error [error]'); +assert.strictEqual(error.name, 'Error'); + +error = test_error.createRangeErrorCode(); +assert.ok(error instanceof RangeError, + 'expected error to be an instance of RangeError'); +assert.strictEqual(error.message, 'RangeError [range error]'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.name, 'RangeError'); + +error = test_error.createTypeErrorCode(); +assert.ok(error instanceof TypeError, + 'expected error to be an instance of TypeError'); +assert.strictEqual(error.message, 'TypeError [type error]'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.name, 'TypeError'); + +error = test_error.createSyntaxErrorCode(); +assert.ok(error instanceof SyntaxError, + 'expected error to be an instance of SyntaxError'); +assert.strictEqual(error.message, 'SyntaxError [syntax error]'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.name, 'SyntaxError'); diff --git a/Tests/NodeApi/test/js-native-api/test_error/test_error.c b/Tests/NodeApi/test/js-native-api/test_error/test_error.c new file mode 100644 index 00000000..fc4b8758 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/test_error.c @@ -0,0 +1,197 @@ +#define NAPI_VERSION 9 +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value checkError(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool r; + NODE_API_CALL(env, napi_is_error(env, args[0], &r)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, r, &result)); + + return result; +} + +static napi_value throwExistingError(napi_env env, napi_callback_info info) { + napi_value message; + napi_value error; + NODE_API_CALL(env, napi_create_string_utf8( + env, "existing error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_error(env, NULL, message, &error)); + NODE_API_CALL(env, napi_throw(env, error)); + return NULL; +} + +static napi_value throwError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_error(env, NULL, "error")); + return NULL; +} + +static napi_value throwRangeError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_range_error(env, NULL, "range error")); + return NULL; +} + +static napi_value throwTypeError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_type_error(env, NULL, "type error")); + return NULL; +} + +static napi_value throwSyntaxError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, node_api_throw_syntax_error(env, NULL, "syntax error")); + return NULL; +} + +static napi_value throwErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_error(env, "ERR_TEST_CODE", "Error [error]")); + return NULL; +} + +static napi_value throwRangeErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, + napi_throw_range_error(env, "ERR_TEST_CODE", "RangeError [range error]")); + return NULL; +} + +static napi_value throwTypeErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, + napi_throw_type_error(env, "ERR_TEST_CODE", "TypeError [type error]")); + return NULL; +} + +static napi_value throwSyntaxErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, + node_api_throw_syntax_error(env, "ERR_TEST_CODE", "SyntaxError [syntax error]")); + return NULL; +} + +static napi_value createError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_error(env, NULL, message, &result)); + return result; +} + +static napi_value createRangeError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "range error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_range_error(env, NULL, message, &result)); + return result; +} + +static napi_value createTypeError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "type error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_type_error(env, NULL, message, &result)); + return result; +} + +static napi_value createSyntaxError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "syntax error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, node_api_create_syntax_error(env, NULL, message, &result)); + return result; +} + +static napi_value createErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, napi_create_string_utf8( + env, "Error [error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_error(env, code, message, &result)); + return result; +} + +static napi_value createRangeErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "RangeError [range error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_range_error(env, code, message, &result)); + return result; +} + +static napi_value createTypeErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "TypeError [type error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_type_error(env, code, message, &result)); + return result; +} + +static napi_value createSyntaxErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "SyntaxError [syntax error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, node_api_create_syntax_error(env, code, message, &result)); + return result; +} + +static napi_value throwArbitrary(napi_env env, napi_callback_info info) { + napi_value arbitrary; + size_t argc = 1; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arbitrary, NULL, NULL)); + NODE_API_CALL(env, napi_throw(env, arbitrary)); + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("checkError", checkError), + DECLARE_NODE_API_PROPERTY("throwExistingError", throwExistingError), + DECLARE_NODE_API_PROPERTY("throwError", throwError), + DECLARE_NODE_API_PROPERTY("throwRangeError", throwRangeError), + DECLARE_NODE_API_PROPERTY("throwTypeError", throwTypeError), + DECLARE_NODE_API_PROPERTY("throwSyntaxError", throwSyntaxError), + DECLARE_NODE_API_PROPERTY("throwErrorCode", throwErrorCode), + DECLARE_NODE_API_PROPERTY("throwRangeErrorCode", throwRangeErrorCode), + DECLARE_NODE_API_PROPERTY("throwTypeErrorCode", throwTypeErrorCode), + DECLARE_NODE_API_PROPERTY("throwSyntaxErrorCode", throwSyntaxErrorCode), + DECLARE_NODE_API_PROPERTY("throwArbitrary", throwArbitrary), + DECLARE_NODE_API_PROPERTY("createError", createError), + DECLARE_NODE_API_PROPERTY("createRangeError", createRangeError), + DECLARE_NODE_API_PROPERTY("createTypeError", createTypeError), + DECLARE_NODE_API_PROPERTY("createSyntaxError", createSyntaxError), + DECLARE_NODE_API_PROPERTY("createErrorCode", createErrorCode), + DECLARE_NODE_API_PROPERTY("createRangeErrorCode", createRangeErrorCode), + DECLARE_NODE_API_PROPERTY("createTypeErrorCode", createTypeErrorCode), + DECLARE_NODE_API_PROPERTY("createSyntaxErrorCode", createSyntaxErrorCode), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_exception/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_exception/CMakeLists.txt new file mode 100644 index 00000000..4d8494f9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_exception + SOURCES + test_exception.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_exception/binding.gyp b/Tests/NodeApi/test/js-native-api/test_exception/binding.gyp new file mode 100644 index 00000000..a453505d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_exception", + "sources": [ + "test_exception.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_exception/test.js b/Tests/NodeApi/test/js-native-api/test_exception/test.js new file mode 100644 index 00000000..3e070fed --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/test.js @@ -0,0 +1,115 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); +const theError = new Error('Some error'); + +// The test module throws an error during Init, but in order for its exports to +// not be lost, it attaches them to the error's "bindings" property. This way, +// we can make sure that exceptions thrown during the module initialization +// phase are propagated through require() into JavaScript. +// https://github.com/nodejs/node/issues/19437 +const test_exception = (function() { + let resultingException; + try { + require(`./build/${common.buildType}/test_exception`); + } catch (anException) { + resultingException = anException; + } + assert.strictEqual(resultingException.message, 'Error during Init'); + return resultingException.binding; +})(); + +{ + const throwTheError = () => { throw theError; }; + + // Test that the native side successfully captures the exception + let returnedError = test_exception.returnException(throwTheError); + assert.strictEqual(returnedError, theError); + + // Test that the native side passes the exception through + assert.throws( + () => { test_exception.allowException(throwTheError); }, + (err) => err === theError, + ); + + // Test that the exception thrown above was marked as pending + // before it was handled on the JS side + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, true, + 'Exception not pending as expected,' + + ` .wasPending() returned ${exception_pending}`); + + // Test that the native side does not capture a non-existing exception + returnedError = test_exception.returnException(common.mustCall()); + assert.strictEqual(returnedError, undefined, + 'Returned error should be undefined when no exception is' + + ` thrown, but ${returnedError} was passed`); +} + + +{ + const throwTheError = class { constructor() { throw theError; } }; + + // Test that the native side successfully captures the exception + let returnedError = test_exception.constructReturnException(throwTheError); + assert.strictEqual(returnedError, theError); + + // Test that the native side passes the exception through + assert.throws( + () => { test_exception.constructAllowException(throwTheError); }, + (err) => err === theError, + ); + + // Test that the exception thrown above was marked as pending + // before it was handled on the JS side + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, true, + 'Exception not pending as expected,' + + ` .wasPending() returned ${exception_pending}`); + + // Test that the native side does not capture a non-existing exception + returnedError = test_exception.constructReturnException(common.mustCall()); + assert.strictEqual(returnedError, undefined, + 'Returned error should be undefined when no exception is' + + ` thrown, but ${returnedError} was passed`); +} + +{ + // Test that no exception appears that was not thrown by us + let caughtError; + try { + test_exception.allowException(common.mustCall()); + } catch (anError) { + caughtError = anError; + } + assert.strictEqual(caughtError, undefined, + 'No exception originated on the native side, but' + + ` ${caughtError} was passed`); + + // Test that the exception state remains clear when no exception is thrown + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, false, + 'Exception state did not remain clear as expected,' + + ` .wasPending() returned ${exception_pending}`); +} + +{ + // Test that no exception appears that was not thrown by us + let caughtError; + try { + test_exception.constructAllowException(common.mustCall()); + } catch (anError) { + caughtError = anError; + } + assert.strictEqual(caughtError, undefined, + 'No exception originated on the native side, but' + + ` ${caughtError} was passed`); + + // Test that the exception state remains clear when no exception is thrown + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, false, + 'Exception state did not remain clear as expected,' + + ` .wasPending() returned ${exception_pending}`); +} diff --git a/Tests/NodeApi/test/js-native-api/test_exception/testFinalizerException.js b/Tests/NodeApi/test/js-native-api/test_exception/testFinalizerException.js new file mode 100644 index 00000000..dce63624 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/testFinalizerException.js @@ -0,0 +1,31 @@ +'use strict'; +if (process.argv[2] === 'child') { + const common = require('../../common'); + // Trying, catching the exception, and finding the bindings at the `Error`'s + // `binding` property is done intentionally, because we're also testing what + // happens when the add-on entry point throws. See test.js. + try { + require(`./build/${common.buildType}/test_exception`); + } catch (anException) { + anException.binding.createExternal(); + } + + // Collect garbage 10 times. At least one of those should throw the exception + // and cause the whole process to bail with it, its text printed to stderr and + // asserted by the parent process to match expectations. + let gcCount = 10; + (function gcLoop() { + global.gc(); + if (--gcCount > 0) { + setImmediate(() => gcLoop()); + } + })(); +} else { + const assert = require('assert'); + const { spawnSync } = require('child_process'); + const child = spawnSync(process.execPath, [ + '--expose-gc', __filename, 'child', + ]); + assert.strictEqual(child.signal, null); + assert.match(child.stderr.toString(), /Error during Finalize/m); +} \ No newline at end of file diff --git a/Tests/NodeApi/test/js-native-api/test_exception/test_exception.c b/Tests/NodeApi/test/js-native-api/test_exception/test_exception.c new file mode 100644 index 00000000..de1eb42a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/test_exception.c @@ -0,0 +1,116 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static bool exceptionWasPending = false; +static int num = 0x23432; + +static napi_value returnException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value result; + napi_status status = napi_call_function(env, global, args[0], 0, 0, &result); + if (status == napi_pending_exception) { + napi_value ex; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &ex)); + return ex; + } + + return NULL; +} + +static napi_value constructReturnException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + napi_status status = napi_new_instance(env, args[0], 0, 0, &result); + if (status == napi_pending_exception) { + napi_value ex; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &ex)); + return ex; + } + + return NULL; +} + +static napi_value allowException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value result; + napi_call_function(env, global, args[0], 0, 0, &result); + // Ignore status and check napi_is_exception_pending() instead. + + NODE_API_CALL(env, napi_is_exception_pending(env, &exceptionWasPending)); + return NULL; +} + +static napi_value constructAllowException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + napi_new_instance(env, args[0], 0, 0, &result); + // Ignore status and check napi_is_exception_pending() instead. + + NODE_API_CALL(env, napi_is_exception_pending(env, &exceptionWasPending)); + return NULL; +} + +static napi_value wasPending(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, exceptionWasPending, &result)); + + return result; +} + +static void finalizer(napi_env env, void *data, void *hint) { + NODE_API_CALL_RETURN_VOID(env, + napi_throw_error(env, NULL, "Error during Finalize")); +} + +static napi_value createExternal(napi_env env, napi_callback_info info) { + napi_value external; + + NODE_API_CALL(env, + napi_create_external(env, &num, finalizer, NULL, &external)); + + return external; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("returnException", returnException), + DECLARE_NODE_API_PROPERTY("allowException", allowException), + DECLARE_NODE_API_PROPERTY("constructReturnException", constructReturnException), + DECLARE_NODE_API_PROPERTY("constructAllowException", constructAllowException), + DECLARE_NODE_API_PROPERTY("wasPending", wasPending), + DECLARE_NODE_API_PROPERTY("createExternal", createExternal), + }; + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + napi_value error, code, message; + NODE_API_CALL(env, napi_create_string_utf8(env, "Error during Init", + NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_error(env, code, message, &error)); + NODE_API_CALL(env, napi_set_named_property(env, error, "binding", exports)); + NODE_API_CALL(env, napi_throw(env, error)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_finalizer/CMakeLists.txt new file mode 100644 index 00000000..ce560dad --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/CMakeLists.txt @@ -0,0 +1,7 @@ +add_node_api_module(test_finalizer + SOURCES + test_finalizer.c + DEFINES + NAPI_EXPERIMENTAL + NODE_API_EXPERIMENTAL_NO_WARNING +) diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/binding.gyp b/Tests/NodeApi/test/js-native-api/test_finalizer/binding.gyp new file mode 100644 index 00000000..8553fd2d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_finalizer", + "defines": [ "NAPI_EXPERIMENTAL" ], + "sources": [ + "test_finalizer.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/test.js b/Tests/NodeApi/test/js-native-api/test_finalizer/test.js new file mode 100644 index 00000000..3edf53ce --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/test.js @@ -0,0 +1,45 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const test_finalizer = require(`./build/${common.buildType}/test_finalizer`); +const assert = require('assert'); + +const { gcUntil } = require('../../common/gc'); + +// The goal of this test is to show that we can run "pure" finalizers in the +// current JS loop tick. Thus, we do not use gcUntil function works +// asynchronously using micro tasks. +// We use IIFE for the obj scope instead of {} to be compatible with +// non-V8 JS engines that do not support scoped variables. +(() => { + const obj = {}; + test_finalizer.addFinalizer(obj); +})(); + +for (let i = 0; i < 10; ++i) { + global.gc(); + if (test_finalizer.getFinalizerCallCount() === 1) { + break; + } +} + +assert.strictEqual(test_finalizer.getFinalizerCallCount(), 1); + +// The finalizer that access JS cannot run synchronously. They are run in the +// next JS loop tick. Thus, we must use gcUntil. +async function runAsyncTests() { + // We do not use common.mustCall() because we want to see the finalizer + // called in response to GC and not as a part of env destruction. + let js_is_called = false; + // We use IIFE for the obj scope instead of {} to be compatible with + // non-V8 JS engines that do not support scoped variables. + (() => { + const obj = {}; + test_finalizer.addFinalizerWithJS(obj, () => { js_is_called = true; }); + })(); + await gcUntil('ensure JS finalizer called', + () => (test_finalizer.getFinalizerCallCount() === 2)); + assert(js_is_called); +} +runAsyncTests(); diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/test_fatal_finalize.js b/Tests/NodeApi/test/js-native-api/test_finalizer/test_fatal_finalize.js new file mode 100644 index 00000000..6b725414 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/test_fatal_finalize.js @@ -0,0 +1,35 @@ +"use strict"; +const common = require("../../common"); + +if (process.argv[2] === "child") { + const test_finalizer = require(`./build/${common.buildType}/test_finalizer`); + + (() => { + const obj = {}; + test_finalizer.addFinalizerFailOnJS(obj); + })(); + + // Collect garbage 10 times. At least one of those should throw the exception + // and cause the whole process to bail with it, its text printed to stderr and + // asserted by the parent process to match expectations. + let gcCount = 10; + (function gcLoop() { + global.gc(); + if (--gcCount > 0) { + setImmediate(() => gcLoop()); + } + })(); +} else { + const assert = require("assert"); + const { spawnSync } = require("child_process"); + const child = spawnSync(process.execPath, [ + "--expose-gc", + __filename, + "child", + ]); + assert(common.nodeProcessAborted(child.status, child.signal)); + assert.match( + child.stderr.toString(), + /Finalizer is calling a function that may affect GC state/ + ); +} diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/test_finalizer.c b/Tests/NodeApi/test/js-native-api/test_finalizer/test_finalizer.c new file mode 100644 index 00000000..0d829eee --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/test_finalizer.c @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +typedef struct { + int32_t finalize_count; + napi_ref js_func; +} FinalizerData; + +static void finalizerOnlyCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + FinalizerData* data = (FinalizerData*)finalize_data; + int32_t count = ++data->finalize_count; + + // It is safe to access instance data + NODE_API_BASIC_CALL_RETURN_VOID(env, + napi_get_instance_data(env, (void**)&data)); + NODE_API_BASIC_ASSERT_RETURN_VOID(count == data->finalize_count, + "Expected to be the same FinalizerData"); +} + +static void finalizerCallingJSCallback(napi_env env, + void* finalize_data, + void* finalize_hint) { + napi_value js_func, undefined; + FinalizerData* data = (FinalizerData*)finalize_data; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, data->js_func, &js_func)); + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, undefined, js_func, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_func)); + data->js_func = NULL; + ++data->finalize_count; +} + +// Schedule async finalizer to run JavaScript-touching code. +static void finalizerWithJSCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + NODE_API_BASIC_CALL_RETURN_VOID( + env, + node_api_post_finalizer( + env, finalizerCallingJSCallback, finalize_data, finalize_hint)); +} + +static void finalizerWithFailedJSCallback(node_api_basic_env basic_env, + void* finalize_data, + void* finalize_hint) { + // Intentionally cast to a napi_env to test the fatal failure. + napi_env env = (napi_env)basic_env; + napi_value obj; + FinalizerData* data = (FinalizerData*)finalize_data; + ++data->finalize_count; + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &obj)); +} + +static napi_value addFinalizer(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1] = {0}; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], data, finalizerOnlyCallback, NULL, NULL)); + return NULL; +} + +// This finalizer is going to call JavaScript from finalizer and succeed. +static napi_value addFinalizerWithJS(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2] = {0}; + napi_valuetype arg_type; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_typeof(env, argv[1], &arg_type)); + NODE_API_ASSERT( + env, arg_type == napi_function, "Expected function as the second arg"); + NODE_API_CALL(env, napi_create_reference(env, argv[1], 1, &data->js_func)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], data, finalizerWithJSCallback, NULL, NULL)); + return NULL; +} + +// This finalizer is going to call JavaScript from finalizer and fail. +static napi_value addFinalizerFailOnJS(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1] = {0}; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL( + env, + napi_add_finalizer( + env, argv[0], data, finalizerWithFailedJSCallback, NULL, NULL)); + return NULL; +} + +static napi_value getFinalizerCallCount(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + FinalizerData* data; + napi_value result; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_create_int32(env, data->finalize_count, &result)); + return result; +} + +static void finalizeData(napi_env env, void* data, void* hint) { + free(data); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + FinalizerData* data = (FinalizerData*)malloc(sizeof(FinalizerData)); + NODE_API_ASSERT(env, data != NULL, "Failed to allocate memory"); + memset(data, 0, sizeof(FinalizerData)); + NODE_API_CALL(env, napi_set_instance_data(env, data, finalizeData, NULL)); + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("addFinalizer", addFinalizer), + DECLARE_NODE_API_PROPERTY("addFinalizerWithJS", addFinalizerWithJS), + DECLARE_NODE_API_PROPERTY("addFinalizerFailOnJS", addFinalizerFailOnJS), + DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", + getFinalizerCallCount)}; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_function/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_function/CMakeLists.txt new file mode 100644 index 00000000..ee290680 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_function + SOURCES + test_function.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_function/binding.gyp b/Tests/NodeApi/test/js-native-api/test_function/binding.gyp new file mode 100644 index 00000000..7cd97f9d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_function", + "sources": [ + "test_function.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_function/test.js b/Tests/NodeApi/test/js-native-api/test_function/test.js new file mode 100644 index 00000000..3e669bd2 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/test.js @@ -0,0 +1,51 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for function +const test_function = require(`./build/${common.buildType}/test_function`); + +function func1() { + return 1; +} +assert.strictEqual(test_function.TestCall(func1), 1); + +function func2() { + return null; +} +assert.strictEqual(test_function.TestCall(func2), null); + +function func3(input) { + return input + 1; +} +assert.strictEqual(test_function.TestCall(func3, 1), 2); + +function func4(input) { + return func3(input); +} +assert.strictEqual(test_function.TestCall(func4, 1), 2); + +assert.strictEqual(test_function.TestName.name, 'Name'); +assert.strictEqual(test_function.TestNameShort.name, 'Name_'); + +let tracked_function = test_function.MakeTrackedFunction(common.mustCall()); +assert(!!tracked_function); +tracked_function = null; +global.gc(); + +assert.deepStrictEqual(test_function.TestCreateFunctionParameters(), { + envIsNull: 'Invalid argument', + nameIsNull: 'napi_ok', + cbIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +assert.throws( + () => test_function.TestBadReturnExceptionPending(), + { + code: 'throwing exception', + name: 'Error', + }, +); diff --git a/Tests/NodeApi/test/js-native-api/test_function/test_function.c b/Tests/NodeApi/test/js-native-api/test_function/test_function.c new file mode 100644 index 00000000..be660034 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/test_function.c @@ -0,0 +1,204 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestCreateFunctionParameters(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value result, return_value; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + status = napi_create_function(NULL, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, + NULL, + &result); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + status); + + napi_create_function(env, + NULL, + NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, + NULL, + &result); + + add_last_status(env, "nameIsNull", return_value); + + napi_create_function(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + NULL, + NULL, + &result); + + add_last_status(env, "cbIsNull", return_value); + + napi_create_function(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, + NULL, + NULL); + + add_last_status(env, "resultIsNull", return_value); + + return return_value; +} + +static napi_value TestCallFunction(napi_env env, napi_callback_info info) { + size_t argc = 10; + napi_value args[10]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc > 0, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_function, + "Wrong type of arguments. Expects a function as first argument."); + + napi_value* argv = args + 1; + argc = argc - 1; + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value result; + NODE_API_CALL(env, napi_call_function(env, global, args[0], argc, argv, &result)); + + return result; +} + +static napi_value TestFunctionName(napi_env env, napi_callback_info info) { + return NULL; +} + +static void finalize_function(napi_env env, void* data, void* hint) { + napi_ref ref = data; + + // Retrieve the JavaScript undefined value. + napi_value undefined; + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + + // Retrieve the JavaScript function we must call. + napi_value js_function; + NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, ref, &js_function)); + + // Call the JavaScript function to indicate that the generated JavaScript + // function is about to be gc-ed. + NODE_API_CALL_RETURN_VOID(env, + napi_call_function(env, undefined, js_function, 0, NULL, NULL)); + + // Destroy the persistent reference to the function we just called so as to + // properly clean up. + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, ref)); +} + +static napi_value MakeTrackedFunction(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value js_finalize_cb; + napi_valuetype arg_type; + + // Retrieve and validate from the arguments the function we will use to + // indicate to JavaScript that the function we are about to create is about to + // be gc-ed. + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &js_finalize_cb, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + NODE_API_CALL(env, napi_typeof(env, js_finalize_cb, &arg_type)); + NODE_API_ASSERT(env, arg_type == napi_function, "Argument must be a function"); + + // Dynamically create a function. + napi_value result; + NODE_API_CALL(env, + napi_create_function( + env, "TrackedFunction", NAPI_AUTO_LENGTH, TestFunctionName, NULL, + &result)); + + // Create a strong reference to the function we will call when the tracked + // function is about to be gc-ed. + napi_ref js_finalize_cb_ref; + NODE_API_CALL(env, + napi_create_reference(env, js_finalize_cb, 1, &js_finalize_cb_ref)); + + // Attach a finalizer to the dynamically created function and pass it the + // strong reference we created in the previous step. + NODE_API_CALL(env, + napi_wrap( + env, result, js_finalize_cb_ref, finalize_function, NULL, NULL)); + + return result; +} + +static napi_value TestBadReturnExceptionPending(napi_env env, napi_callback_info info) { + napi_throw_error(env, "throwing exception", "throwing exception"); + + // addons should only ever return a valid napi_value even if an + // exception occurs, but we have seen that the C++ wrapper + // with exceptions enabled sometimes returns an invalid value + // when an exception is thrown. Test that we ignore the return + // value then an exception is pending. We use 0xFFFFFFFF as a value + // that should never be a valid napi_value and node seems to + // crash if it is not ignored indicating that it is indeed invalid. + return (napi_value)(0xFFFFFFFFF); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value fn1; + NODE_API_CALL(env, napi_create_function( + env, NULL, NAPI_AUTO_LENGTH, TestCallFunction, NULL, &fn1)); + + napi_value fn2; + NODE_API_CALL(env, napi_create_function( + env, "Name", NAPI_AUTO_LENGTH, TestFunctionName, NULL, &fn2)); + + napi_value fn3; + NODE_API_CALL(env, napi_create_function( + env, "Name_extra", 5, TestFunctionName, NULL, &fn3)); + + napi_value fn4; + NODE_API_CALL(env, + napi_create_function( + env, "MakeTrackedFunction", NAPI_AUTO_LENGTH, MakeTrackedFunction, + NULL, &fn4)); + + napi_value fn5; + NODE_API_CALL(env, + napi_create_function( + env, "TestCreateFunctionParameters", NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, NULL, &fn5)); + + napi_value fn6; + NODE_API_CALL(env, + napi_create_function( + env, "TestBadReturnExceptionPending", NAPI_AUTO_LENGTH, + TestBadReturnExceptionPending, NULL, &fn6)); + + NODE_API_CALL(env, napi_set_named_property(env, exports, "TestCall", fn1)); + NODE_API_CALL(env, napi_set_named_property(env, exports, "TestName", fn2)); + NODE_API_CALL(env, + napi_set_named_property(env, exports, "TestNameShort", fn3)); + NODE_API_CALL(env, + napi_set_named_property(env, exports, "MakeTrackedFunction", fn4)); + + NODE_API_CALL(env, + napi_set_named_property( + env, exports, "TestCreateFunctionParameters", fn5)); + + NODE_API_CALL(env, + napi_set_named_property( + env, exports, "TestBadReturnExceptionPending", fn6)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_general/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_general/CMakeLists.txt new file mode 100644 index 00000000..3b3532fd --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_general + SOURCES + test_general.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_general/binding.gyp b/Tests/NodeApi/test/js-native-api/test_general/binding.gyp new file mode 100644 index 00000000..71a4fb6b --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_general", + "sources": [ + "test_general.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/test.js b/Tests/NodeApi/test/js-native-api/test_general/test.js new file mode 100644 index 00000000..3bf87a55 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/test.js @@ -0,0 +1,97 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const test_general = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +const val1 = '1'; +const val2 = 1; +const val3 = 1; + +class BaseClass { +} + +class ExtendedClass extends BaseClass { +} + +const baseObject = new BaseClass(); +const extendedObject = new ExtendedClass(); + +// Test napi_strict_equals +assert.ok(test_general.testStrictEquals(val1, val1)); +assert.strictEqual(test_general.testStrictEquals(val1, val2), false); +assert.ok(test_general.testStrictEquals(val2, val3)); + +// Test napi_get_prototype +assert.strictEqual(test_general.testGetPrototype(baseObject), + Object.getPrototypeOf(baseObject)); +assert.strictEqual(test_general.testGetPrototype(extendedObject), + Object.getPrototypeOf(extendedObject)); +// Prototypes for base and extended should be different. +assert.notStrictEqual(test_general.testGetPrototype(baseObject), + test_general.testGetPrototype(extendedObject)); + +// Test version management functions +assert.strictEqual(test_general.testGetVersion(), 8); + +[ + 123, + 'test string', + function() {}, + new Object(), + true, + undefined, + Symbol(), +].forEach((val) => { + assert.strictEqual(test_general.testNapiTypeof(val), typeof val); +}); + +// Since typeof in js return object need to validate specific case +// for null +assert.strictEqual(test_general.testNapiTypeof(null), 'null'); + +// Assert that wrapping twice fails. +const x = {}; +test_general.wrap(x); +assert.throws(() => test_general.wrap(x), + { name: 'Error', message: 'Invalid argument' }); +// Clean up here, otherwise derefItemWasCalled() will be polluted. +test_general.removeWrap(x); + +// Ensure that wrapping, removing the wrap, and then wrapping again works. +const y = {}; +test_general.wrap(y); +test_general.removeWrap(y); +// Wrapping twice succeeds if a remove_wrap() separates the instances +test_general.wrap(y); +// Clean up here, otherwise derefItemWasCalled() will be polluted. +test_general.removeWrap(y); + +// Test napi_adjust_external_memory +// TODO: (vmoroz) Hermes does not implement that API. +// const adjustedValue = test_general.testAdjustExternalMemory(); +// assert.strictEqual(typeof adjustedValue, 'number'); +// assert(adjustedValue > 0); + +async function runGCTests() { + // Ensure that garbage collecting an object with a wrapped native item results + // in the finalize callback being called. + // TODO: (vmoroz) Restore after Hermes GC is fixed. + // assert.strictEqual(test_general.derefItemWasCalled(), false); + // (() => test_general.wrap({}))(); + // await common.gcUntil('deref_item() was called upon garbage collecting a ' + + // 'wrapped object.', + // () => test_general.derefItemWasCalled()); + + // Ensure that removing a wrap and garbage collecting does not fire the + // finalize callback. + let z = {}; + test_general.testFinalizeWrap(z); + test_general.removeWrap(z); + z = null; + await common.gcUntil( + 'finalize callback was not called upon garbage collection.', + () => (!test_general.finalizeWasCalled())); +} +runGCTests(); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testEnvCleanup.js b/Tests/NodeApi/test/js-native-api/test_general/testEnvCleanup.js new file mode 100644 index 00000000..ce59768b --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testEnvCleanup.js @@ -0,0 +1,57 @@ +'use strict'; + +if (process.argv[2] === 'child') { + const common = require('../../common'); + const test_general = require(`./build/${common.buildType}/test_general`); + + // The second argument to `envCleanupWrap()` is an index into the global + // static string array named `env_cleanup_finalizer_messages` on the native + // side. A reverse mapping is reproduced here for clarity. + const finalizerMessages = { + 'simple wrap': 0, + 'wrap, removeWrap': 1, + 'first wrap': 2, + 'second wrap': 3, + }; + + // We attach the three objects we will test to `module.exports` to ensure they + // will not be garbage-collected before the process exits. + + // Make sure the finalizer for a simple wrap will be called at env cleanup. + module.exports['simple wrap'] = + test_general.envCleanupWrap({}, finalizerMessages['simple wrap']); + + // Make sure that a removed wrap does not result in a call to its finalizer at + // env cleanup. + module.exports['wrap, removeWrap'] = + test_general.envCleanupWrap({}, finalizerMessages['wrap, removeWrap']); + test_general.removeWrap(module.exports['wrap, removeWrap']); + + // Make sure that only the latest attached version of a re-wrapped item's + // finalizer gets called at env cleanup. + module.exports['first wrap'] = + test_general.envCleanupWrap({}, finalizerMessages['first wrap']); + test_general.removeWrap(module.exports['first wrap']); + test_general.envCleanupWrap(module.exports['first wrap'], + finalizerMessages['second wrap']); +} else { + const assert = require('assert'); + const { spawnSync } = require('child_process'); + + const child = spawnSync(process.execPath, [__filename, 'child'], { + stdio: [ process.stdin, 'pipe', process.stderr ], + }); + + // Grab the child's output and construct an object whose keys are the rows of + // the output and whose values are `true`, so we can compare the output while + // ignoring the order in which the lines of it were produced. + assert.deepStrictEqual( + child.stdout.toString().split(/\r\n|\r|\n/g).reduce((obj, item) => + Object.assign(obj, item ? { [item]: true } : {}), {}), { + 'finalize at env cleanup for simple wrap': true, + 'finalize at env cleanup for second wrap': true, + }); + + // Ensure that the child exited successfully. + assert.strictEqual(child.status, 0); +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/testFinalizer.js b/Tests/NodeApi/test/js-native-api/test_general/testFinalizer.js new file mode 100644 index 00000000..3eefe142 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testFinalizer.js @@ -0,0 +1,38 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const test_general = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +(function() { + let finalized = {}; + const callback = common.mustCall(2); + + // Add two items to be finalized and ensure the callback is called for each. + test_general.addFinalizerOnly(finalized, callback); + test_general.addFinalizerOnly(finalized, callback); + + // Ensure attached items cannot be retrieved. + assert.throws(() => test_general.unwrap(finalized), + { name: 'Error', message: 'Invalid argument' }); + + // Ensure attached items cannot be removed. + assert.throws(() => test_general.removeWrap(finalized), + { name: 'Error', message: 'Invalid argument' }); +})(); +global.gc(); + +// Add an item to an object that is already wrapped, and ensure that its +// finalizer as well as the wrap finalizer gets called. +async function testFinalizeAndWrap() { + assert.strictEqual(test_general.derefItemWasCalled(), false); + (function() { + let finalizeAndWrap = {}; + test_general.wrap(finalizeAndWrap); + test_general.addFinalizerOnly(finalizeAndWrap, common.mustCall()); + })(); + await common.gcUntil('test finalize and wrap', + () => test_general.derefItemWasCalled()); +} +testFinalizeAndWrap(); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testGlobals.js b/Tests/NodeApi/test/js-native-api/test_general/testGlobals.js new file mode 100644 index 00000000..34188e08 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testGlobals.js @@ -0,0 +1,8 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +const test_globals = require(`./build/${common.buildType}/test_general`); + +assert.strictEqual(test_globals.getUndefined(), undefined); +assert.strictEqual(test_globals.getNull(), null); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testInstanceOf.js b/Tests/NodeApi/test/js-native-api/test_general/testInstanceOf.js new file mode 100644 index 00000000..c9b98fa8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testInstanceOf.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Addon is referenced through the eval expression in testFile +const addon = require(`./build/${common.buildType}/test_general`); + +// We can only perform this test if we have a working Symbol.hasInstance +if (typeof Symbol !== 'undefined' && 'hasInstance' in Symbol && + typeof Symbol.hasInstance === 'symbol') { + + function compareToNative(theObject, theConstructor) { + assert.strictEqual( + addon.doInstanceOf(theObject, theConstructor), + (theObject instanceof theConstructor), + ); + } + + function MyClass() {} + Object.defineProperty(MyClass, Symbol.hasInstance, { + value: function(candidate) { + return 'mark' in candidate; + }, + }); + + function MySubClass() {} + MySubClass.prototype = new MyClass(); + + let x = new MySubClass(); + let y = new MySubClass(); + x.mark = true; + + compareToNative(x, MySubClass); + compareToNative(y, MySubClass); + compareToNative(x, MyClass); + compareToNative(y, MyClass); + + x = new MyClass(); + y = new MyClass(); + x.mark = true; + + compareToNative(x, MySubClass); + compareToNative(y, MySubClass); + compareToNative(x, MyClass); + compareToNative(y, MyClass); +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/testNapiRun.js b/Tests/NodeApi/test/js-native-api/test_general/testNapiRun.js new file mode 100644 index 00000000..6d4f4662 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testNapiRun.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../../common'); +const assert = require('assert'); + +// `addon` is referenced through the eval expression in testFile +const addon = require(`./build/${common.buildType}/test_general`); + +const testCase = '(41.92 + 0.08);'; +const expected = 42; +const actual = addon.testNapiRun(testCase); + +assert.strictEqual(actual, expected); +assert.throws(() => addon.testNapiRun({ abc: 'def' }), /string was expected/); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testNapiStatus.js b/Tests/NodeApi/test/js-native-api/test_general/testNapiStatus.js new file mode 100644 index 00000000..5ad97a34 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testNapiStatus.js @@ -0,0 +1,8 @@ +'use strict'; + +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +addon.createNapiError(); +assert(addon.testNapiErrorCleanup(), 'napi_status cleaned up for second call'); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof.js b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof.js new file mode 100644 index 00000000..0b476e1e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof.js @@ -0,0 +1,121 @@ +// Copyright 2008 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +// The following assert functions are referenced by v8's unit tests +// See for instance deps/v8/test/mjsunit/instanceof.js +// eslint-disable-next-line no-unused-vars +function assertTrue(assertion) { + return assert.strictEqual(assertion, true); +} + +// eslint-disable-next-line no-unused-vars +function assertFalse(assertion) { + assert.strictEqual(assertion, false); +} + +// eslint-disable-next-line no-unused-vars +function assertEquals(leftHandSide, rightHandSide) { + assert.strictEqual(leftHandSide, rightHandSide); +} + +// eslint-disable-next-line no-unused-vars +function assertThrows(statement) { + assert.throws(function() { + eval(statement); + }, Error); +} + +assertTrue(addon.doInstanceOf({}, Object)); +assertTrue(addon.doInstanceOf([], Object)); + +assertFalse(addon.doInstanceOf({}, Array)); +assertTrue(addon.doInstanceOf([], Array)); + +function TestChains() { + var A = {}; + var B = {}; + var C = {}; + B.__proto__ = A; + C.__proto__ = B; + + function F() { } + F.prototype = A; + assertTrue(addon.doInstanceOf(C, F)); + assertTrue(addon.doInstanceOf(B, F)); + assertFalse(addon.doInstanceOf(A, F)); + + F.prototype = B; + assertTrue(addon.doInstanceOf(C, F)); + assertFalse(addon.doInstanceOf(B, F)); + assertFalse(addon.doInstanceOf(A, F)); + + F.prototype = C; + assertFalse(addon.doInstanceOf(C, F)); + assertFalse(addon.doInstanceOf(B, F)); + assertFalse(addon.doInstanceOf(A, F)); +} + +TestChains(); + + +function TestExceptions() { + function F() { } + var items = [ 1, new Number(42), + true, + 'string', new String('hest'), + {}, [], + F, new F(), + Object, String ]; + + var exceptions = 0; + var instanceofs = 0; + + for (var i = 0; i < items.length; i++) { + for (var j = 0; j < items.length; j++) { + try { + if (addon.doInstanceOf(items[i], items[j])) instanceofs++; + } catch (e) { + assertTrue(addon.doInstanceOf(e, TypeError)); + exceptions++; + } + } + } + assertEquals(10, instanceofs); + assertEquals(88, exceptions); + + // Make sure to throw an exception if the function prototype + // isn't a proper JavaScript object. + function G() { } + G.prototype = undefined; + assertThrows("addon.doInstanceOf({}, G)"); +} + +TestExceptions(); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof2.js b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof2.js new file mode 100644 index 00000000..360b28c8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof2.js @@ -0,0 +1,341 @@ +// Copyright 2010 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +function assertTrue(assertion) { + return assert.strictEqual(assertion, true); +} + +function assertEquals(leftHandSide, rightHandSide) { + assert.strictEqual(leftHandSide, rightHandSide); +} + +var except = "exception"; + +var correct_answer_index = 0; +var correct_answers = [ + false, false, true, true, false, false, true, true, + true, false, false, true, true, false, false, true, + false, true, true, false, false, true, true, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, false, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, true, true, false, + true, true, false, false, false, true, true, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, true, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, false, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, true, true, false, + true, false, false, true, true, true, false, false, + false, true, true, false, false, true, true, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, true, true, false, + true, false, false, true, false, true, true, false, + false, true, true, false, false, true, true, false, + true, true, false, false, false, true, true, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, true, false, + false, false, except, except, false, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, true, true, false, false, + false, true, true, false, false, false, true, true, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, false, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, false, true, true, + true, true, false, false, false, false, true, true, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, true, true, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, true, true, false, false, + false, true, true, false, false, false, true, true, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, false, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, false, true, true, + true, true, false, false, false, false, true, true, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, true, true, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except]; + +for (var i = 0; i < 256; i++) { + Test(i & 1, i & 2, i & 4, i & 8, i & 0x10, i & 0x20, i & 0x40, i & 0x80); +} + + +function InstanceTest(x, func) { + try { + var answer = addon.doInstanceOf(x, func); + assertEquals(correct_answers[correct_answer_index], answer); + } catch (e) { + assertTrue(/prototype/.test(e)); + assertEquals(correct_answers[correct_answer_index], except); + } + correct_answer_index++; +} + + +function Test(a, b, c, d, e, f, g, h) { + var Foo = function() { } + var Bar = function() { } + + if (c) Foo.prototype = 12; + if (d) Bar.prototype = 13; + var x = a ? new Foo() : new Bar(); + var y = b ? new Foo() : new Bar(); + InstanceTest(x, Foo); + InstanceTest(y, Foo); + InstanceTest(x, Bar); + InstanceTest(y, Bar); + if (e) x.__proto__ = Bar.prototype; + if (f) y.__proto__ = Foo.prototype; + if (g) { + x.__proto__ = y; + } else { + if (h) y.__proto__ = x + } + InstanceTest(x, Foo); + InstanceTest(y, Foo); + InstanceTest(x, Bar); + InstanceTest(y, Bar); +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/test_general.c b/Tests/NodeApi/test/js-native-api/test_general/test_general.c new file mode 100644 index 00000000..2e130af6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/test_general.c @@ -0,0 +1,315 @@ +// we define NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED here to +// validate that it can be used as a form of test itself. It is +// not related to any of the other tests +// defined in the file +#define NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +#include +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value testStrictEquals(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool bool_result; + napi_value result; + NODE_API_CALL(env, napi_strict_equals(env, args[0], args[1], &bool_result)); + NODE_API_CALL(env, napi_get_boolean(env, bool_result, &result)); + + return result; +} + +static napi_value testGetPrototype(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + NODE_API_CALL(env, napi_get_prototype(env, args[0], &result)); + + return result; +} + +static napi_value testGetVersion(napi_env env, napi_callback_info info) { + uint32_t version; + napi_value result; + NODE_API_CALL(env, napi_get_version(env, &version)); + NODE_API_CALL(env, napi_create_uint32(env, version, &result)); + return result; +} + +static napi_value doInstanceOf(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool instanceof; + NODE_API_CALL(env, napi_instanceof(env, args[0], args[1], &instanceof)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, instanceof, &result)); + + return result; +} + +static napi_value getNull(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_null(env, &result)); + return result; +} + +static napi_value getUndefined(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +static napi_value createNapiError(napi_env env, napi_callback_info info) { + napi_value value; + NODE_API_CALL(env, napi_create_string_utf8(env, "xyz", 3, &value)); + + double double_value; + napi_status status = napi_get_value_double(env, value, &double_value); + + NODE_API_ASSERT(env, status != napi_ok, "Failed to produce error condition"); + + const napi_extended_error_info *error_info = 0; + NODE_API_CALL(env, napi_get_last_error_info(env, &error_info)); + + NODE_API_ASSERT(env, error_info->error_code == status, + "Last error info code should match last status"); + NODE_API_ASSERT(env, error_info->error_message, + "Last error info message should not be null"); + + return NULL; +} + +static napi_value testNapiErrorCleanup(napi_env env, napi_callback_info info) { + const napi_extended_error_info *error_info = 0; + NODE_API_CALL(env, napi_get_last_error_info(env, &error_info)); + + napi_value result; + bool is_ok = error_info->error_code == napi_ok; + NODE_API_CALL(env, napi_get_boolean(env, is_ok, &result)); + + return result; +} + +static napi_value testNapiTypeof(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_valuetype argument_type; + NODE_API_CALL(env, napi_typeof(env, args[0], &argument_type)); + + napi_value result = NULL; + if (argument_type == napi_number) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "number", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_string) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "string", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_function) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "function", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_object) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "object", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_boolean) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "boolean", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_undefined) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "undefined", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_symbol) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "symbol", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_null) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "null", NAPI_AUTO_LENGTH, &result)); + } + return result; +} + +static bool deref_item_called = false; +static void deref_item(napi_env env, void* data, void* hint) { + (void) hint; + + NODE_API_ASSERT_RETURN_VOID(env, data == &deref_item_called, + "Finalize callback was called with the correct pointer"); + + deref_item_called = true; +} + +static napi_value deref_item_was_called(napi_env env, napi_callback_info info) { + napi_value it_was_called; + + NODE_API_CALL(env, napi_get_boolean(env, deref_item_called, &it_was_called)); + + return it_was_called; +} + +static napi_value wrap_first_arg(napi_env env, + napi_callback_info info, + napi_finalize finalizer, + void* data) { + size_t argc = 1; + napi_value to_wrap; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &to_wrap, NULL, NULL)); + NODE_API_CALL(env, napi_wrap(env, to_wrap, data, finalizer, NULL, NULL)); + + return to_wrap; +} + +static napi_value wrap(napi_env env, napi_callback_info info) { + deref_item_called = false; + return wrap_first_arg(env, info, deref_item, &deref_item_called); +} + +static napi_value unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value wrapped; + void* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL)); + NODE_API_CALL(env, napi_unwrap(env, wrapped, &data)); + + return NULL; +} + +static napi_value remove_wrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value wrapped; + void* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL)); + NODE_API_CALL(env, napi_remove_wrap(env, wrapped, &data)); + + return NULL; +} + +static bool finalize_called = false; +static void test_finalize(napi_env env, void* data, void* hint) { + finalize_called = true; +} + +static napi_value test_finalize_wrap(napi_env env, napi_callback_info info) { + return wrap_first_arg(env, info, test_finalize, NULL); +} + +static napi_value finalize_was_called(napi_env env, napi_callback_info info) { + napi_value it_was_called; + + NODE_API_CALL(env, napi_get_boolean(env, finalize_called, &it_was_called)); + + return it_was_called; +} + +static napi_value testAdjustExternalMemory(napi_env env, napi_callback_info info) { + napi_value result; + int64_t adjustedValue; + + NODE_API_CALL(env, napi_adjust_external_memory(env, 1, &adjustedValue)); + NODE_API_CALL(env, napi_create_double(env, (double)adjustedValue, &result)); + + return result; +} + +static napi_value testNapiRun(napi_env env, napi_callback_info info) { + napi_value script, result; + size_t argc = 1; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &script, NULL, NULL)); + + NODE_API_CALL(env, napi_run_script(env, script, &result)); + + return result; +} + +static void finalizer_only_callback(napi_env env, void* data, void* hint) { + napi_ref js_cb_ref = data; + napi_value js_cb, undefined; + NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, js_cb_ref, &js_cb)); + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NODE_API_CALL_RETURN_VOID(env, + napi_call_function(env, undefined, js_cb, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, js_cb_ref)); +} + +static napi_value add_finalizer_only(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + napi_ref js_cb_ref; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_create_reference(env, argv[1], 1, &js_cb_ref)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], js_cb_ref, finalizer_only_callback, NULL, NULL)); + return NULL; +} + +static const char* env_cleanup_finalizer_messages[] = { + "simple wrap", + "wrap, removeWrap", + "first wrap", + "second wrap" +}; + +static void cleanup_env_finalizer(napi_env env, void* data, void* hint) { + (void) env; + (void) hint; + + printf("finalize at env cleanup for %s\n", + env_cleanup_finalizer_messages[(uintptr_t)data]); +} + +static napi_value env_cleanup_wrap(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + uint32_t value; + uintptr_t ptr_value; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + + NODE_API_CALL(env, napi_get_value_uint32(env, argv[1], &value)); + + ptr_value = value; + return wrap_first_arg(env, info, cleanup_env_finalizer, (void*)ptr_value); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("testStrictEquals", testStrictEquals), + DECLARE_NODE_API_PROPERTY("testGetPrototype", testGetPrototype), + DECLARE_NODE_API_PROPERTY("testGetVersion", testGetVersion), + DECLARE_NODE_API_PROPERTY("testNapiRun", testNapiRun), + DECLARE_NODE_API_PROPERTY("doInstanceOf", doInstanceOf), + DECLARE_NODE_API_PROPERTY("getUndefined", getUndefined), + DECLARE_NODE_API_PROPERTY("getNull", getNull), + DECLARE_NODE_API_PROPERTY("createNapiError", createNapiError), + DECLARE_NODE_API_PROPERTY("testNapiErrorCleanup", testNapiErrorCleanup), + DECLARE_NODE_API_PROPERTY("testNapiTypeof", testNapiTypeof), + DECLARE_NODE_API_PROPERTY("wrap", wrap), + DECLARE_NODE_API_PROPERTY("envCleanupWrap", env_cleanup_wrap), + DECLARE_NODE_API_PROPERTY("unwrap", unwrap), + DECLARE_NODE_API_PROPERTY("removeWrap", remove_wrap), + DECLARE_NODE_API_PROPERTY("addFinalizerOnly", add_finalizer_only), + DECLARE_NODE_API_PROPERTY("testFinalizeWrap", test_finalize_wrap), + DECLARE_NODE_API_PROPERTY("finalizeWasCalled", finalize_was_called), + DECLARE_NODE_API_PROPERTY("derefItemWasCalled", deref_item_was_called), + DECLARE_NODE_API_PROPERTY("testAdjustExternalMemory", testAdjustExternalMemory) + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_handle_scope/CMakeLists.txt new file mode 100644 index 00000000..579119a3 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_handle_scope + SOURCES + test_handle_scope.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/binding.gyp b/Tests/NodeApi/test/js-native-api/test_handle_scope/binding.gyp new file mode 100644 index 00000000..dd6dd63a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_handle_scope", + "sources": [ + "test_handle_scope.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/test.js b/Tests/NodeApi/test/js-native-api/test_handle_scope/test.js new file mode 100644 index 00000000..46aa12f6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/test.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing handle scope api calls +const testHandleScope = + require(`./build/${common.buildType}/test_handle_scope`); + +testHandleScope.NewScope(); + +assert.ok(testHandleScope.NewScopeEscape() instanceof Object); + +testHandleScope.NewScopeEscapeTwice(); + +assert.throws( + () => { + testHandleScope.NewScopeWithException(() => { throw new RangeError(); }); + }, + RangeError); diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/test_handle_scope.c b/Tests/NodeApi/test/js-native-api/test_handle_scope/test_handle_scope.c new file mode 100644 index 00000000..7c5eb4a4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/test_handle_scope.c @@ -0,0 +1,86 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +// these tests validate the handle scope functions in the normal +// flow. Forcing gc behavior to fully validate they are doing +// the right right thing would be quite hard so we keep it +// simple for now. + +static napi_value NewScope(napi_env env, napi_callback_info info) { + napi_handle_scope scope; + napi_value output = NULL; + + NODE_API_CALL(env, napi_open_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + NODE_API_CALL(env, napi_close_handle_scope(env, scope)); + return NULL; +} + +static napi_value NewScopeEscape(napi_env env, napi_callback_info info) { + napi_escapable_handle_scope scope; + napi_value output = NULL; + napi_value escapee = NULL; + + NODE_API_CALL(env, napi_open_escapable_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + NODE_API_CALL(env, napi_escape_handle(env, scope, output, &escapee)); + NODE_API_CALL(env, napi_close_escapable_handle_scope(env, scope)); + return escapee; +} + +static napi_value NewScopeEscapeTwice(napi_env env, napi_callback_info info) { + napi_escapable_handle_scope scope; + napi_value output = NULL; + napi_value escapee = NULL; + napi_status status; + + NODE_API_CALL(env, napi_open_escapable_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + NODE_API_CALL(env, napi_escape_handle(env, scope, output, &escapee)); + status = napi_escape_handle(env, scope, output, &escapee); + NODE_API_ASSERT(env, status == napi_escape_called_twice, "Escaping twice fails"); + NODE_API_CALL(env, napi_close_escapable_handle_scope(env, scope)); + return NULL; +} + +static napi_value NewScopeWithException(napi_env env, napi_callback_info info) { + napi_handle_scope scope; + size_t argc; + napi_value exception_function; + napi_status status; + napi_value output = NULL; + + NODE_API_CALL(env, napi_open_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + + argc = 1; + NODE_API_CALL(env, napi_get_cb_info( + env, info, &argc, &exception_function, NULL, NULL)); + + status = napi_call_function( + env, output, exception_function, 0, NULL, NULL); + NODE_API_ASSERT(env, status == napi_pending_exception, + "Function should have thrown."); + + // Closing a handle scope should still work while an exception is pending. + NODE_API_CALL(env, napi_close_handle_scope(env, scope)); + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("NewScope", NewScope), + DECLARE_NODE_API_PROPERTY("NewScopeEscape", NewScopeEscape), + DECLARE_NODE_API_PROPERTY("NewScopeEscapeTwice", NewScopeEscapeTwice), + DECLARE_NODE_API_PROPERTY("NewScopeWithException", NewScopeWithException), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/CMakeLists.txt new file mode 100644 index 00000000..45c58f67 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_hermes_private_metadata + SOURCES + test_hermes_private_metadata.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/test.js b/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/test.js new file mode 100644 index 00000000..386b4525 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/test.js @@ -0,0 +1,62 @@ +'use strict'; +// Flags: --expose-gc + +// Regression coverage adapted from microsoft/hermes-windows#349, with the +// weak-reference creation case from #321 kept on the N-API v5 surface. +// https://github.com/microsoft/hermes-windows/pull/349 +// https://github.com/microsoft/hermes-windows/pull/321 +const { buildType } = require('../../common'); +const { gcUntil } = require('../../common/gc'); +const assert = require('assert'); + +const metadata = require( + `./build/${buildType}/test_hermes_private_metadata`); + +function makeThrowingProxy() { + let trapCount = 0; + const unexpected = () => { + trapCount++; + throw new Error('Node-API metadata must not invoke a Proxy trap'); + }; + const proxy = new Proxy({}, { + get: unexpected, + getOwnPropertyDescriptor: unexpected, + defineProperty: unexpected, + deleteProperty: unexpected, + has: unexpected, + set: unexpected, + }); + return { proxy, getTrapCount: () => trapCount }; +} + +{ + const { proxy, getTrapCount } = makeThrowingProxy(); + assert.strictEqual(metadata.weakReferenceRoundTrip(proxy), true); + assert.strictEqual(metadata.wrapRoundTrip(proxy), true); + assert.strictEqual(getTrapCount(), 0); +} + +{ + const object = Object.freeze({}); + assert.strictEqual(metadata.weakReferenceRoundTrip(object), true); + assert.strictEqual(metadata.wrapRoundTrip(object), true); +} + +{ + const parent = {}; + const child = Object.create(parent); + assert.strictEqual(metadata.wrapPrototypeIsolation(parent, child), true); +} + +(async function testProxyFinalizer() { + const { getTrapCount } = (() => { + const { proxy, getTrapCount } = makeThrowingProxy(); + metadata.addFinalizer(proxy); + return { getTrapCount }; + })(); + + await gcUntil( + 'Node-API finalizer on proxy', + () => metadata.finalizeCount === 1); + assert.strictEqual(getTrapCount(), 0); +})(); diff --git a/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/test_hermes_private_metadata.c b/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/test_hermes_private_metadata.c new file mode 100644 index 00000000..fa466b35 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_hermes_private_metadata/test_hermes_private_metadata.c @@ -0,0 +1,126 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +// Portable N-API v5 subset of microsoft/hermes-windows#349. That fix moved +// weak-reference and wrap metadata out of JavaScript-visible properties so it +// cannot invoke Proxy traps, reject frozen objects, or leak through prototypes. +// It also closes the GC-unsafe creation window found in hermes-windows#321. +// https://github.com/microsoft/hermes-windows/pull/349 +// https://github.com/microsoft/hermes-windows/pull/321 + +static int wrapped_value = 42; +static int finalize_count = 0; + +static void CountFinalizer(napi_env env, void* data, void* hint) { + (void)env; + (void)data; + (void)hint; + finalize_count++; +} + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + (void)info; + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} + +static napi_value WeakReferenceRoundTrip(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value object; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &object, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Expected one object."); + + napi_ref reference; + NODE_API_CALL(env, napi_create_reference(env, object, 0, &reference)); + + napi_value referenced; + NODE_API_CALL(env, napi_get_reference_value(env, reference, &referenced)); + bool equal = false; + NODE_API_CALL(env, napi_strict_equals(env, object, referenced, &equal)); + NODE_API_CALL(env, napi_delete_reference(env, reference)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, equal, &result)); + return result; +} + +static napi_value WrapRoundTrip(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value object; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &object, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Expected one object."); + + NODE_API_CALL(env, napi_wrap(env, object, &wrapped_value, NULL, NULL, NULL)); + + void* unwrapped = NULL; + NODE_API_CALL(env, napi_unwrap(env, object, &unwrapped)); + NODE_API_ASSERT(env, unwrapped == &wrapped_value, "Unexpected wrapped value."); + + void* removed = NULL; + NODE_API_CALL(env, napi_remove_wrap(env, object, &removed)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, removed == &wrapped_value, &result)); + return result; +} + +static napi_value WrapPrototypeIsolation(napi_env env, + napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Expected parent and child objects."); + + NODE_API_CALL(env, napi_wrap(env, args[0], &wrapped_value, NULL, NULL, NULL)); + + void* child_data = NULL; + napi_status child_status = napi_unwrap(env, args[1], &child_data); + + void* parent_data = NULL; + NODE_API_CALL(env, napi_remove_wrap(env, args[0], &parent_data)); + + napi_value result; + NODE_API_CALL( + env, + napi_get_boolean(env, + child_status == napi_invalid_arg && child_data == NULL && + parent_data == &wrapped_value, + &result)); + return result; +} + +static napi_value AddFinalizer(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value object; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &object, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Expected one object."); + + NODE_API_CALL( + env, napi_add_finalizer(env, object, NULL, CountFinalizer, NULL, NULL)); + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("weakReferenceRoundTrip", + WeakReferenceRoundTrip), + DECLARE_NODE_API_PROPERTY("wrapRoundTrip", WrapRoundTrip), + DECLARE_NODE_API_PROPERTY("wrapPrototypeIsolation", + WrapPrototypeIsolation), + DECLARE_NODE_API_PROPERTY("addFinalizer", AddFinalizer), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_instance_data/CMakeLists.txt new file mode 100644 index 00000000..feda8f59 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_instance_data + SOURCES + test_instance_data.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/binding.gyp b/Tests/NodeApi/test/js-native-api/test_instance_data/binding.gyp new file mode 100644 index 00000000..f1c77b30 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_instance_data", + "sources": [ + "test_instance_data.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/test.js b/Tests/NodeApi/test/js-native-api/test_instance_data/test.js new file mode 100644 index 00000000..043da9f1 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/test.js @@ -0,0 +1,41 @@ +'use strict'; +// Test API calls for instance data. + +const common = require('../../common'); +const assert = require('assert'); + +if (module !== require.main) { + // When required as a module, run the tests. + const test_instance_data = + require(`./build/${common.buildType}/test_instance_data`); + + // Print to stdout when the environment deletes the instance data. This output + // is checked by the parent process. + test_instance_data.setPrintOnDelete(); + + // Test that instance data can be accessed from a binding. + assert.strictEqual(test_instance_data.increment(), 42); + + // Test that the instance data can be accessed from a finalizer. + // TODO: (vmoroz) Restore after Hermes fixes GC. + // test_instance_data.objectWithFinalizer(common.mustCall()); + // global.gc(); +} else { + // When launched as a script, run tests in either a child process or in a + // worker thread. + const requireAs = require('../../common/require-as'); + const runOptions = { stdio: ['inherit', 'pipe', 'inherit'] }; + + function checkOutput(child) { + assert.strictEqual(child.status, 0); + assert.strictEqual( + (child.stdout.toString().split(/\r\n?|\n/) || [])[0], + 'deleting addon data'); + } + + // Run tests in a child process. + checkOutput(requireAs(__filename, ['--expose-gc'], runOptions, 'child')); + + // Run tests in a worker thread in a child process. + checkOutput(requireAs(__filename, ['--expose-gc'], runOptions, 'worker')); +} diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/test_instance_data.c b/Tests/NodeApi/test/js-native-api/test_instance_data/test_instance_data.c new file mode 100644 index 00000000..bf354983 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/test_instance_data.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +typedef struct { + size_t value; + bool print; + napi_ref js_cb_ref; +} AddonData; + +static napi_value Increment(napi_env env, napi_callback_info info) { + AddonData* data; + napi_value result; + + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_create_uint32(env, ++data->value, &result)); + + return result; +} + +static void DeleteAddonData(napi_env env, void* raw_data, void* hint) { + AddonData* data = raw_data; + if (data->print) { + printf("deleting addon data\n"); + } + if (data->js_cb_ref != NULL) { + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref)); + } + free(data); +} + +static napi_value SetPrintOnDelete(napi_env env, napi_callback_info info) { + AddonData* data; + + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + data->print = true; + + return NULL; +} + +static void TestFinalizer(napi_env env, void* raw_data, void* hint) { + (void) raw_data; + (void) hint; + + AddonData* data; + NODE_API_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data)); + napi_value js_cb, undefined; + NODE_API_CALL_RETURN_VOID(env, + napi_get_reference_value(env, data->js_cb_ref, &js_cb)); + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NODE_API_CALL_RETURN_VOID(env, + napi_call_function(env, undefined, js_cb, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref)); + data->js_cb_ref = NULL; +} + +static napi_value ObjectWithFinalizer(napi_env env, napi_callback_info info) { + AddonData* data; + napi_value result, js_cb; + size_t argc = 1; + + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_ASSERT(env, data->js_cb_ref == NULL, "reference must be NULL"); + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL)); + NODE_API_CALL(env, napi_create_object(env, &result)); + NODE_API_CALL(env, + napi_add_finalizer(env, result, NULL, TestFinalizer, NULL, NULL)); + NODE_API_CALL(env, napi_create_reference(env, js_cb, 1, &data->js_cb_ref)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + AddonData* data = malloc(sizeof(*data)); + data->value = 41; + data->print = false; + data->js_cb_ref = NULL; + + NODE_API_CALL(env, napi_set_instance_data(env, data, DeleteAddonData, NULL)); + + napi_property_descriptor props[] = { + DECLARE_NODE_API_PROPERTY("increment", Increment), + DECLARE_NODE_API_PROPERTY("setPrintOnDelete", SetPrintOnDelete), + DECLARE_NODE_API_PROPERTY("objectWithFinalizer", ObjectWithFinalizer), + }; + + NODE_API_CALL(env, + napi_define_properties( + env, exports, sizeof(props) / sizeof(*props), props)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_new_target/CMakeLists.txt new file mode 100644 index 00000000..1cee2625 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_new_target + SOURCES + test_new_target.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/binding.gyp b/Tests/NodeApi/test/js-native-api/test_new_target/binding.gyp new file mode 100644 index 00000000..baa0e3c6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/binding.gyp @@ -0,0 +1,11 @@ +{ + 'targets': [ + { + 'target_name': 'test_new_target', + 'defines': [ 'V8_DEPRECATION_WARNINGS=1' ], + 'sources': [ + 'test_new_target.c' + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/test.js b/Tests/NodeApi/test/js-native-api/test_new_target/test.js new file mode 100644 index 00000000..06d5ef36 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/test.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../../common'); +const assert = require('assert'); +const binding = require(`./build/${common.buildType}/test_new_target`); + +class Class extends binding.BaseClass { + constructor() { + super(); + this.method(); + } + method() { + this.ok = true; + } +} + +assert.ok(new Class() instanceof binding.BaseClass); +assert.ok(new Class().ok); +assert.ok(binding.OrdinaryFunction()); +assert.ok( + new binding.Constructor(binding.Constructor) instanceof binding.Constructor); diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/test_new_target.c b/Tests/NodeApi/test/js-native-api/test_new_target/test_new_target.c new file mode 100644 index 00000000..02ceb992 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/test_new_target.c @@ -0,0 +1,92 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value BaseClass(napi_env env, napi_callback_info info) { + napi_value newTargetArg; + NODE_API_CALL(env, napi_get_new_target(env, info, &newTargetArg)); + napi_value thisArg; + NODE_API_CALL(env, napi_get_cb_info(env, info, NULL, NULL, &thisArg, NULL)); + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + + // this !== new.target since we are being invoked through super() + bool result; + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, thisArg, &result)); + NODE_API_ASSERT(env, !result, "this !== new.target"); + + // new.target !== undefined because we should be called as a new expression + NODE_API_ASSERT(env, newTargetArg != NULL, "newTargetArg != NULL"); + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, undefined, &result)); + NODE_API_ASSERT(env, !result, "new.target !== undefined"); + + return thisArg; +} + +static napi_value Constructor(napi_env env, napi_callback_info info) { + bool result; + napi_value newTargetArg; + NODE_API_CALL(env, napi_get_new_target(env, info, &newTargetArg)); + size_t argc = 1; + napi_value argv; + napi_value thisArg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &argv, &thisArg, NULL)); + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + + // new.target !== undefined because we should be called as a new expression + NODE_API_ASSERT(env, newTargetArg != NULL, "newTargetArg != NULL"); + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, undefined, &result)); + NODE_API_ASSERT(env, !result, "new.target !== undefined"); + + // arguments[0] should be Constructor itself (test harness passed it) + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, argv, &result)); + NODE_API_ASSERT(env, result, "new.target === Constructor"); + + return thisArg; +} + +static napi_value OrdinaryFunction(napi_env env, napi_callback_info info) { + napi_value newTargetArg; + NODE_API_CALL(env, napi_get_new_target(env, info, &newTargetArg)); + + NODE_API_ASSERT(env, newTargetArg == NULL, "newTargetArg == NULL"); + + napi_value _true; + NODE_API_CALL(env, napi_get_boolean(env, true, &_true)); + return _true; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value baseClass, constructor; + NODE_API_CALL( + env, + napi_define_class( + env, + "BaseClass", + NAPI_AUTO_LENGTH, + BaseClass, + NULL, + 0, + NULL, + &baseClass)); + NODE_API_CALL( + env, + napi_define_class( + env, + "Constructor", + NAPI_AUTO_LENGTH, + Constructor, + NULL, + 0, + NULL, + &constructor)); + const napi_property_descriptor desc[] = { + DECLARE_NODE_API_PROPERTY_VALUE("BaseClass", baseClass), + DECLARE_NODE_API_PROPERTY("OrdinaryFunction", OrdinaryFunction), + DECLARE_NODE_API_PROPERTY_VALUE("Constructor", constructor)}; + NODE_API_CALL(env, napi_define_properties(env, exports, 3, desc)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_number/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_number/CMakeLists.txt new file mode 100644 index 00000000..87946947 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(test_number + SOURCES + test_number.c + test_null.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_number/binding.gyp b/Tests/NodeApi/test/js-native-api/test_number/binding.gyp new file mode 100644 index 00000000..31707404 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_number", + "sources": [ + "test_number.c", + "test_null.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_number/test.js b/Tests/NodeApi/test/js-native-api/test_number/test.js new file mode 100644 index 00000000..6c0f0f3e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test.js @@ -0,0 +1,134 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const test_number = require(`./build/${common.buildType}/test_number`); + + +// Testing api calls for number +function testNumber(num) { + assert.strictEqual(num, test_number.Test(num)); +} + +testNumber(0); +testNumber(-0); +testNumber(1); +testNumber(-1); +testNumber(100); +testNumber(2121); +testNumber(-1233); +testNumber(986583); +testNumber(-976675); + +/* eslint-disable no-loss-of-precision */ +testNumber( + 98765432213456789876546896323445679887645323232436587988766545658); +testNumber( + -4350987086545760976737453646576078997096876957864353245245769809); +/* eslint-enable no-loss-of-precision */ +testNumber(Number.MIN_SAFE_INTEGER); +testNumber(Number.MAX_SAFE_INTEGER); +testNumber(Number.MAX_SAFE_INTEGER + 10); + +testNumber(Number.MIN_VALUE); +testNumber(Number.MAX_VALUE); +testNumber(Number.MAX_VALUE + 10); + +testNumber(Number.POSITIVE_INFINITY); +testNumber(Number.NEGATIVE_INFINITY); +testNumber(Number.NaN); + +function testUint32(input, expected = input) { + assert.strictEqual(expected, test_number.TestUint32Truncation(input)); +} + +// Test zero +testUint32(0.0, 0); +testUint32(-0.0, 0); + +// Test overflow scenarios +testUint32(4294967295); +testUint32(4294967296, 0); +testUint32(4294967297, 1); +testUint32(17 * 4294967296 + 1, 1); +testUint32(-1, 0xffffffff); + +// Validate documented behavior when value is retrieved as 32-bit integer with +// `napi_get_value_int32` +function testInt32(input, expected = input) { + assert.strictEqual(expected, test_number.TestInt32Truncation(input)); +} + +// Test zero +testInt32(0.0, 0); +testInt32(-0.0, 0); + +// Test min/max int32 range +testInt32(-Math.pow(2, 31)); +testInt32(Math.pow(2, 31) - 1); + +// Test overflow scenarios +testInt32(4294967297, 1); +testInt32(4294967296, 0); +testInt32(4294967295, -1); +testInt32(4294967296 * 5 + 3, 3); + +// Test min/max safe integer range +testInt32(Number.MIN_SAFE_INTEGER, 1); +testInt32(Number.MAX_SAFE_INTEGER, -1); + +// Test within int64_t range (with precision loss) +testInt32(-Math.pow(2, 63) + (Math.pow(2, 9) + 1), 1024); +testInt32(Math.pow(2, 63) - (Math.pow(2, 9) + 1), -1024); + +// Test min/max double value +testInt32(-Number.MIN_VALUE, 0); +testInt32(Number.MIN_VALUE, 0); +testInt32(-Number.MAX_VALUE, 0); +testInt32(Number.MAX_VALUE, 0); + +// Test outside int64_t range +testInt32(-Math.pow(2, 63) + (Math.pow(2, 9)), 0); +testInt32(Math.pow(2, 63) - (Math.pow(2, 9)), 0); + +// Test non-finite numbers +testInt32(Number.POSITIVE_INFINITY, 0); +testInt32(Number.NEGATIVE_INFINITY, 0); +testInt32(Number.NaN, 0); + +// Validate documented behavior when value is retrieved as 64-bit integer with +// `napi_get_value_int64` +function testInt64(input, expected = input) { + assert.strictEqual(expected, test_number.TestInt64Truncation(input)); +} + +// Both V8 and ChakraCore return a sentinel value of `0x8000000000000000` when +// the conversion goes out of range, but V8 treats it as unsigned in some cases. +const RANGEERROR_POSITIVE = Math.pow(2, 63); +const RANGEERROR_NEGATIVE = -Math.pow(2, 63); + +// Test zero +testInt64(0.0, 0); +testInt64(-0.0, 0); + +// Test min/max safe integer range +testInt64(Number.MIN_SAFE_INTEGER); +testInt64(Number.MAX_SAFE_INTEGER); + +// Test within int64_t range (with precision loss) +testInt64(-Math.pow(2, 63) + (Math.pow(2, 9) + 1)); +testInt64(Math.pow(2, 63) - (Math.pow(2, 9) + 1)); + +// Test min/max double value +testInt64(-Number.MIN_VALUE, 0); +testInt64(Number.MIN_VALUE, 0); +testInt64(-Number.MAX_VALUE, RANGEERROR_NEGATIVE); +testInt64(Number.MAX_VALUE, RANGEERROR_POSITIVE); + +// Test outside int64_t range +testInt64(-Math.pow(2, 63) + (Math.pow(2, 9)), RANGEERROR_NEGATIVE); +testInt64(Math.pow(2, 63) - (Math.pow(2, 9)), RANGEERROR_POSITIVE); + +// Test non-finite numbers +testInt64(Number.POSITIVE_INFINITY, 0); +testInt64(Number.NEGATIVE_INFINITY, 0); +testInt64(Number.NaN, 0); diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_null.c b/Tests/NodeApi/test/js-native-api/test_number/test_null.c new file mode 100644 index 00000000..20d479c9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_null.c @@ -0,0 +1,77 @@ +#include + +#include "../common.h" + +// Unifies the way the macros declare values. +typedef double double_t; + +#define BINDING_FOR_CREATE(initial_capital, lowercase) \ + static napi_value Create##initial_capital(napi_env env, \ + napi_callback_info info) { \ + napi_value return_value, call_result; \ + lowercase##_t value = 42; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + napi_create_##lowercase(NULL, value, &call_result)); \ + napi_create_##lowercase(env, value, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + return return_value; \ + } + +#define BINDING_FOR_GET_VALUE(initial_capital, lowercase) \ + static napi_value GetValue##initial_capital(napi_env env, \ + napi_callback_info info) { \ + napi_value return_value, call_result; \ + lowercase##_t value = 42; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + NODE_API_CALL(env, napi_create_##lowercase(env, value, &call_result)); \ + add_returned_status( \ + env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + napi_get_value_##lowercase(NULL, call_result, &value)); \ + napi_get_value_##lowercase(env, NULL, &value); \ + add_last_status(env, "valueIsNull", return_value); \ + napi_get_value_##lowercase(env, call_result, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + return return_value; \ + } + +BINDING_FOR_CREATE(Double, double) +BINDING_FOR_CREATE(Int32, int32) +BINDING_FOR_CREATE(Uint32, uint32) +BINDING_FOR_CREATE(Int64, int64) +BINDING_FOR_GET_VALUE(Double, double) +BINDING_FOR_GET_VALUE(Int32, int32) +BINDING_FOR_GET_VALUE(Uint32, uint32) +BINDING_FOR_GET_VALUE(Int64, int64) + +void init_test_null(napi_env env, napi_value exports) { + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("createDouble", CreateDouble), + DECLARE_NODE_API_PROPERTY("createInt32", CreateInt32), + DECLARE_NODE_API_PROPERTY("createUint32", CreateUint32), + DECLARE_NODE_API_PROPERTY("createInt64", CreateInt64), + DECLARE_NODE_API_PROPERTY("getValueDouble", GetValueDouble), + DECLARE_NODE_API_PROPERTY("getValueInt32", GetValueInt32), + DECLARE_NODE_API_PROPERTY("getValueUint32", GetValueUint32), + DECLARE_NODE_API_PROPERTY("getValueInt64", GetValueInt64), + }; + napi_value test_null; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID( + env, + napi_define_properties(env, + test_null, + sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "testNull", test_null)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_null.h b/Tests/NodeApi/test/js-native-api/test_number/test_null.h new file mode 100644 index 00000000..695d8971 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_NUMBER_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_NUMBER_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_NUMBER_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_null.js b/Tests/NodeApi/test/js-native-api/test_number/test_null.js new file mode 100644 index 00000000..c09801ac --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_null.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const { testNull } = require(`./build/${common.buildType}/test_number`); + +const expectedCreateResult = { + envIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}; +const expectedGetValueResult = { + envIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; +[ 'Double', 'Int32', 'Uint32', 'Int64' ].forEach((typeName) => { + assert.deepStrictEqual(testNull['create' + typeName](), expectedCreateResult); + assert.deepStrictEqual(testNull['getValue' + typeName](), expectedGetValueResult); +}); diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_number.c b/Tests/NodeApi/test/js-native-api/test_number/test_number.c new file mode 100644 index 00000000..3b3ba29f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_number.c @@ -0,0 +1,110 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static napi_value Test(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + double input; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_double(env, input, &output)); + + return output; +} + +static napi_value TestUint32Truncation(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + uint32_t input; + NODE_API_CALL(env, napi_get_value_uint32(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, input, &output)); + + return output; +} + +static napi_value TestInt32Truncation(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + int32_t input; + NODE_API_CALL(env, napi_get_value_int32(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_int32(env, input, &output)); + + return output; +} + +static napi_value TestInt64Truncation(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + int64_t input; + NODE_API_CALL(env, napi_get_value_int64(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_int64(env, input, &output)); + + return output; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("Test", Test), + DECLARE_NODE_API_PROPERTY("TestInt32Truncation", TestInt32Truncation), + DECLARE_NODE_API_PROPERTY("TestUint32Truncation", TestUint32Truncation), + DECLARE_NODE_API_PROPERTY("TestInt64Truncation", TestInt64Truncation), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + init_test_null(env, exports); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_object/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_object/CMakeLists.txt new file mode 100644 index 00000000..e8f81166 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/CMakeLists.txt @@ -0,0 +1,10 @@ +add_node_api_module(test_object + SOURCES + test_null.c + test_object.c +) + +add_node_api_module(test_exceptions + SOURCES + test_exceptions.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_object/binding.gyp b/Tests/NodeApi/test/js-native-api/test_object/binding.gyp new file mode 100644 index 00000000..37ea4931 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/binding.gyp @@ -0,0 +1,17 @@ +{ + "targets": [ + { + "target_name": "test_object", + "sources": [ + "test_null.c", + "test_object.c" + ] + }, + { + "target_name": "test_exceptions", + "sources": [ + "test_exceptions.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_object/test.js b/Tests/NodeApi/test/js-native-api/test_object/test.js new file mode 100644 index 00000000..8ca961a1 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test.js @@ -0,0 +1,393 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for objects +const test_object = require(`./build/${common.buildType}/test_object`); + + +const object = { + hello: 'world', + array: [ + 1, 94, 'str', 12.321, { test: 'obj in arr' }, + ], + newObject: { + test: 'obj in obj', + }, +}; + +assert.strictEqual(test_object.Get(object, 'hello'), 'world'); +assert.strictEqual(test_object.GetNamed(object, 'hello'), 'world'); +assert.deepStrictEqual(test_object.Get(object, 'array'), + [1, 94, 'str', 12.321, { test: 'obj in arr' }]); +assert.deepStrictEqual(test_object.Get(object, 'newObject'), + { test: 'obj in obj' }); + +assert(test_object.Has(object, 'hello')); +assert(test_object.HasNamed(object, 'hello')); +assert(test_object.Has(object, 'array')); +assert(test_object.Has(object, 'newObject')); + +const newObject = test_object.New(); +assert(test_object.Has(newObject, 'test_number')); +assert.strictEqual(newObject.test_number, 987654321); +assert.strictEqual(newObject.test_string, 'test string'); + +{ + // Verify that napi_get_property() walks the prototype chain. + function MyObject() { + this.foo = 42; + this.bar = 43; + } + + MyObject.prototype.bar = 44; + MyObject.prototype.baz = 45; + + const obj = new MyObject(); + + assert.strictEqual(test_object.Get(obj, 'foo'), 42); + assert.strictEqual(test_object.Get(obj, 'bar'), 43); + assert.strictEqual(test_object.Get(obj, 'baz'), 45); + assert.strictEqual(test_object.Get(obj, 'toString'), + Object.prototype.toString); +} + +{ + // Verify that napi_has_own_property() fails if property is not a name. + [true, false, null, undefined, {}, [], 0, 1, () => { }].forEach((value) => { + assert.throws(() => { + test_object.HasOwn({}, value); + }, /^Error: A string or symbol was expected$/); + }); +} + +{ + // Verify that napi_has_own_property() does not walk the prototype chain. + const symbol1 = Symbol(); + const symbol2 = Symbol(); + + function MyObject() { + this.foo = 42; + this.bar = 43; + this[symbol1] = 44; + } + + MyObject.prototype.bar = 45; + MyObject.prototype.baz = 46; + MyObject.prototype[symbol2] = 47; + + const obj = new MyObject(); + + assert.strictEqual(test_object.HasOwn(obj, 'foo'), true); + assert.strictEqual(test_object.HasOwn(obj, 'bar'), true); + assert.strictEqual(test_object.HasOwn(obj, symbol1), true); + assert.strictEqual(test_object.HasOwn(obj, 'baz'), false); + assert.strictEqual(test_object.HasOwn(obj, 'toString'), false); + assert.strictEqual(test_object.HasOwn(obj, symbol2), false); +} + +{ + // test_object.Inflate increases all properties by 1 + const cube = { + x: 10, + y: 10, + z: 10, + }; + + assert.deepStrictEqual(test_object.Inflate(cube), { x: 11, y: 11, z: 11 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 12, y: 12, z: 12 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 13, y: 13, z: 13 }); + cube.t = 13; + assert.deepStrictEqual( + test_object.Inflate(cube), { x: 14, y: 14, z: 14, t: 14 }); + + const sym1 = Symbol('1'); + const sym2 = Symbol('2'); + const sym3 = Symbol('3'); + const sym4 = Symbol('4'); + const object2 = { + [sym1]: '@@iterator', + [sym2]: sym3, + }; + + assert(test_object.Has(object2, sym1)); + assert(test_object.Has(object2, sym2)); + assert.strictEqual(test_object.Get(object2, sym1), '@@iterator'); + assert.strictEqual(test_object.Get(object2, sym2), sym3); + assert(test_object.Set(object2, 'string', 'value')); + assert(test_object.SetNamed(object2, 'named_string', 'value')); + assert(test_object.Set(object2, sym4, 123)); + assert(test_object.Has(object2, 'string')); + assert(test_object.HasNamed(object2, 'named_string')); + assert(test_object.Has(object2, sym4)); + assert.strictEqual(test_object.Get(object2, 'string'), 'value'); + assert.strictEqual(test_object.Get(object2, sym4), 123); +} + +{ + // Wrap a pointer in a JS object, then verify the pointer can be unwrapped. + const wrapper = {}; + test_object.Wrap(wrapper); + + assert(test_object.Unwrap(wrapper)); +} + +{ + // Verify that wrapping doesn't break an object's prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + assert(test_object.Unwrap(wrapper)); + assert(wrapper.protoA); +} + +{ + // Verify the pointer can be unwrapped after inserting in the prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + const protoB = { protoB: true }; + Object.setPrototypeOf(protoB, Object.getPrototypeOf(wrapper)); + Object.setPrototypeOf(wrapper, protoB); + + assert(test_object.Unwrap(wrapper)); + assert(wrapper.protoA, true); + assert(wrapper.protoB, true); +} + +{ + // Verify that objects can be type-tagged and type-tag-checked. + const obj1 = test_object.TypeTaggedInstance(0); + const obj2 = test_object.TypeTaggedInstance(1); + const obj3 = test_object.TypeTaggedInstance(2); + const obj4 = test_object.TypeTaggedInstance(3); + const external = test_object.TypeTaggedExternal(2); + const plainExternal = test_object.PlainExternal(); + + // Verify that we do not allow type tag indices greater than the largest + // available index. + assert.throws(() => test_object.TypeTaggedInstance(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + assert.throws(() => test_object.TypeTaggedExternal(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + + // Verify that type tags are correctly accepted. + assert.strictEqual(test_object.CheckTypeTag(0, obj1), true); + assert.strictEqual(test_object.CheckTypeTag(1, obj2), true); + assert.strictEqual(test_object.CheckTypeTag(2, obj3), true); + assert.strictEqual(test_object.CheckTypeTag(3, obj4), true); + assert.strictEqual(test_object.CheckTypeTag(2, external), true); + + // Verify that wrongly tagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, obj2), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj1), false); + assert.strictEqual(test_object.CheckTypeTag(0, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj4), false); + assert.strictEqual(test_object.CheckTypeTag(2, obj4), false); + assert.strictEqual(test_object.CheckTypeTag(3, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(4, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(0, external), false); + assert.strictEqual(test_object.CheckTypeTag(1, external), false); + assert.strictEqual(test_object.CheckTypeTag(3, external), false); + assert.strictEqual(test_object.CheckTypeTag(4, external), false); + + // Verify that untagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, {}), false); + assert.strictEqual(test_object.CheckTypeTag(1, {}), false); + assert.strictEqual(test_object.CheckTypeTag(0, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(1, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(2, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(3, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(4, plainExternal), false); +} + +{ + // Verify that normal and nonexistent properties can be deleted. + const sym = Symbol(); + const obj = { foo: 'bar', [sym]: 'baz' }; + + assert.strictEqual('foo' in obj, true); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, sym), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, false); + assert.strictEqual('does_not_exist' in obj, false); +} + +{ + // Verify that non-configurable properties are not deleted. + const obj = {}; + + Object.defineProperty(obj, 'foo', { configurable: false }); + assert.strictEqual(test_object.Delete(obj, 'foo'), false); + assert.strictEqual('foo' in obj, true); +} + +{ + // Verify that prototype properties are not deleted. + function Foo() { + this.foo = 'bar'; + } + + Foo.prototype.foo = 'baz'; + + const obj = new Foo(); + + assert.strictEqual(obj.foo, 'bar'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); +} + +{ + // Verify that napi_get_property_names gets the right set of property names, + // i.e.: includes prototypes, only enumerable properties, skips symbols, + // and includes indices and converts them to strings. + + const object = { __proto__: { + inherited: 1, + } }; + + const fooSymbol = Symbol('foo'); + + object.normal = 2; + object[fooSymbol] = 3; + Object.defineProperty(object, 'unenumerable', { + value: 4, + enumerable: false, + writable: true, + configurable: true, + }); + Object.defineProperty(object, 'writable', { + value: 4, + enumerable: true, + writable: true, + configurable: false, + }); + Object.defineProperty(object, 'configurable', { + value: 4, + enumerable: true, + writable: false, + configurable: true, + }); + object[5] = 5; + + assert.deepStrictEqual(test_object.GetPropertyNames(object), + ['5', + 'normal', + 'writable', + 'configurable', + 'inherited']); + + assert.deepStrictEqual(test_object.GetSymbolNames(object), + [fooSymbol]); + + assert.deepStrictEqual(test_object.GetEnumerableWritableNames(object), + ['5', + 'normal', + 'writable', + fooSymbol, + 'inherited']); + + assert.deepStrictEqual(test_object.GetOwnWritableNames(object), + ['5', + 'normal', + 'unenumerable', + 'writable', + fooSymbol]); + + assert.deepStrictEqual(test_object.GetEnumerableConfigurableNames(object), + ['5', + 'normal', + 'configurable', + fooSymbol, + 'inherited']); + + assert.deepStrictEqual(test_object.GetOwnConfigurableNames(object), + ['5', + 'normal', + 'unenumerable', + 'configurable', + fooSymbol]); +} + +// Verify that passing NULL to napi_set_property() results in the correct +// error. +assert.deepStrictEqual(test_object.TestSetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}); + +// Verify that passing NULL to napi_has_property() results in the correct +// error. +assert.deepStrictEqual(test_object.TestHasProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +// Verify that passing NULL to napi_get_property() results in the correct +// error. +assert.deepStrictEqual(test_object.TestGetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +{ + const obj = { x: 'a', y: 'b', z: 'c' }; + + test_object.TestSeal(obj); + + assert.strictEqual(Object.isSealed(obj), true); + + assert.throws(() => { + obj.w = 'd'; + }, /(Cannot add property w, object is not extensible)|(TypeError: Cannot add new property 'w')/); + + assert.throws(() => { + delete obj.x; + }, /(Cannot delete property 'x' of #)|(TypeError: Property is not configurable)/); + + // Sealed objects allow updating existing properties, + // so this should not throw. + obj.x = 'd'; +} + +{ + const obj = { x: 10, y: 10, z: 10 }; + + test_object.TestFreeze(obj); + + assert.strictEqual(Object.isFrozen(obj), true); + + assert.throws(() => { + obj.x = 10; + }, /(Cannot assign to read only property 'x' of object '#)|(TypeError: Cannot assign to read-only property 'x')/); + + assert.throws(() => { + obj.w = 15; + }, /(Cannot add property w, object is not extensible)|(TypeError: Cannot add new property 'w')/); + + assert.throws(() => { + delete obj.x; + }, /(Cannot delete property 'x' of #)|(TypeError: Property is not configurable)/); +} diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.c b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.c new file mode 100644 index 00000000..7474d49e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.c @@ -0,0 +1,82 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestExceptions(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value target = args[0]; + napi_value exception, key, value; + napi_status status; + bool is_exception_pending; + bool bool_result; + + NODE_API_CALL(env, + napi_create_string_utf8(env, "key", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL( + env, napi_create_string_utf8(env, "value", NAPI_AUTO_LENGTH, &value)); + +#define PROCEDURE(call) \ + { \ + status = (call); \ + NODE_API_ASSERT( \ + env, status == napi_pending_exception, "expect exception pending"); \ + NODE_API_CALL(env, napi_is_exception_pending(env, &is_exception_pending)); \ + NODE_API_ASSERT(env, is_exception_pending, "expect exception pending"); \ + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); \ + } + // discard the exception values. + + // properties + PROCEDURE(napi_set_property(env, target, key, value)); + PROCEDURE(napi_set_named_property(env, target, "key", value)); + PROCEDURE(napi_has_property(env, target, key, &bool_result)); + PROCEDURE(napi_has_own_property(env, target, key, &bool_result)); + PROCEDURE(napi_has_named_property(env, target, "key", &bool_result)); + PROCEDURE(napi_get_property(env, target, key, &value)); + PROCEDURE(napi_get_named_property(env, target, "key", &value)); + PROCEDURE(napi_delete_property(env, target, key, &bool_result)); + + // elements + PROCEDURE(napi_set_element(env, target, 0, value)); + PROCEDURE(napi_has_element(env, target, 0, &bool_result)); + PROCEDURE(napi_get_element(env, target, 0, &value)); + PROCEDURE(napi_delete_element(env, target, 0, &bool_result)); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY_VALUE("key", value), + }; + PROCEDURE(napi_define_properties( + env, target, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + PROCEDURE(napi_get_all_property_names(env, + target, + napi_key_own_only, + napi_key_enumerable, + napi_key_keep_numbers, + &value)); + PROCEDURE(napi_get_property_names(env, target, &value)); + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("testExceptions", TestExceptions), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.js b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.js new file mode 100644 index 00000000..2d5f10ab --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common'); + +// Test +const { testExceptions } = require(`./build/${common.buildType}/test_exceptions`); + +function throws() { + throw new Error('foobar'); +} +testExceptions(new Proxy({}, { + get: common.mustCallAtLeast(throws, 1), + getOwnPropertyDescriptor: common.mustCallAtLeast(throws, 1), + defineProperty: common.mustCallAtLeast(throws, 1), + deleteProperty: common.mustCallAtLeast(throws, 1), + has: common.mustCallAtLeast(throws, 1), + set: common.mustCallAtLeast(throws, 1), + ownKeys: common.mustCallAtLeast(throws, 1), +})); diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_null.c b/Tests/NodeApi/test/js-native-api/test_object/test_null.c new file mode 100644 index 00000000..4fd4e95e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_null.c @@ -0,0 +1,400 @@ +#include + +#include "../common.h" +#include "test_null.h" + +static napi_value SetProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, key; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_property(NULL, object, key, object)); + + napi_set_property(env, NULL, key, object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_property(env, object, NULL, object); + add_last_status(env, "keyIsNull", return_value); + + napi_set_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, key, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_property(NULL, object, key, &prop)); + + napi_get_property(env, NULL, key, &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property(env, object, NULL, &prop); + add_last_status(env, "keyIsNull", return_value); + + napi_get_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value TestBoolValuedPropApi(napi_env env, + napi_status (*api)(napi_env, napi_value, napi_value, bool*)) { + napi_value return_value, object, key; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + api(NULL, object, key, &result)); + + api(env, NULL, key, &result); + add_last_status(env, "objectIsNull", return_value); + + api(env, object, NULL, &result); + add_last_status(env, "keyIsNull", return_value); + + api(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_has_property); +} + +static napi_value HasOwnProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_has_own_property); +} + +static napi_value DeleteProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_delete_property); +} + +static napi_value SetNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_named_property(NULL, object, "key", object)); + + napi_set_named_property(env, NULL, "key", object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_named_property(env, object, NULL, object); + add_last_status(env, "keyIsNull", return_value); + + napi_set_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_named_property(NULL, object, "key", &prop)); + + napi_get_named_property(env, NULL, "key", &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_named_property(env, object, NULL, &prop); + add_last_status(env, "keyIsNull", return_value); + + napi_get_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_has_named_property(NULL, object, "key", &result)); + + napi_has_named_property(env, NULL, "key", &result); + add_last_status(env, "objectIsNull", return_value); + + napi_has_named_property(env, object, NULL, &result); + add_last_status(env, "keyIsNull", return_value); + + napi_has_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value SetElement(napi_env env, napi_callback_info info) { + napi_value return_value, object; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_element(NULL, object, 0, object)); + + napi_set_element(env, NULL, 0, object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_property(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetElement(napi_env env, napi_callback_info info) { + napi_value return_value, object, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_element(NULL, object, 0, &prop)); + + napi_get_property(env, NULL, 0, &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value TestBoolValuedElementApi(napi_env env, + napi_status (*api)(napi_env, napi_value, uint32_t, bool*)) { + napi_value return_value, object; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + api(NULL, object, 0, &result)); + + api(env, NULL, 0, &result); + add_last_status(env, "objectIsNull", return_value); + + api(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasElement(napi_env env, napi_callback_info info) { + return TestBoolValuedElementApi(env, napi_has_element); +} + +static napi_value DeleteElement(napi_env env, napi_callback_info info) { + return TestBoolValuedElementApi(env, napi_delete_element); +} + +static napi_value DefineProperties(napi_env env, napi_callback_info info) { + napi_value object, return_value; + + napi_property_descriptor desc = { + "prop", NULL, DefineProperties, NULL, NULL, NULL, napi_enumerable, NULL + }; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_define_properties(NULL, object, 1, &desc)); + + napi_define_properties(env, NULL, 1, &desc); + add_last_status(env, "objectIsNull", return_value); + + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "descriptorListIsNull", return_value); + + desc.utf8name = NULL; + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "utf8nameIsNull", return_value); + desc.utf8name = "prop"; + + desc.method = NULL; + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "methodIsNull", return_value); + desc.method = DefineProperties; + + return return_value; +} + +static napi_value GetPropertyNames(napi_env env, napi_callback_info info) { + napi_value return_value, props; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_property_names(NULL, return_value, &props)); + + napi_get_property_names(env, NULL, &props); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property_names(env, return_value, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetAllPropertyNames(napi_env env, napi_callback_info info) { + napi_value return_value, props; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_all_property_names(NULL, + return_value, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + &props)); + + napi_get_all_property_names(env, + NULL, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + &props); + add_last_status(env, "objectIsNull", return_value); + + napi_get_all_property_names(env, + return_value, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetPrototype(napi_env env, napi_callback_info info) { + napi_value return_value, proto; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_prototype(NULL, return_value, &proto)); + + napi_get_prototype(env, NULL, &proto); + add_last_status(env, "objectIsNull", return_value); + + napi_get_prototype(env, return_value, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("setProperty", SetProperty), + DECLARE_NODE_API_PROPERTY("getProperty", GetProperty), + DECLARE_NODE_API_PROPERTY("hasProperty", HasProperty), + DECLARE_NODE_API_PROPERTY("hasOwnProperty", HasOwnProperty), + DECLARE_NODE_API_PROPERTY("deleteProperty", DeleteProperty), + DECLARE_NODE_API_PROPERTY("setNamedProperty", SetNamedProperty), + DECLARE_NODE_API_PROPERTY("getNamedProperty", GetNamedProperty), + DECLARE_NODE_API_PROPERTY("hasNamedProperty", HasNamedProperty), + DECLARE_NODE_API_PROPERTY("setElement", SetElement), + DECLARE_NODE_API_PROPERTY("getElement", GetElement), + DECLARE_NODE_API_PROPERTY("hasElement", HasElement), + DECLARE_NODE_API_PROPERTY("deleteElement", DeleteElement), + DECLARE_NODE_API_PROPERTY("defineProperties", DefineProperties), + DECLARE_NODE_API_PROPERTY("getPropertyNames", GetPropertyNames), + DECLARE_NODE_API_PROPERTY("getAllPropertyNames", GetAllPropertyNames), + DECLARE_NODE_API_PROPERTY("getPrototype", GetPrototype), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID(env, napi_define_properties( + env, test_null, sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + const napi_property_descriptor test_null_set = { + "testNull", NULL, NULL, NULL, NULL, test_null, napi_enumerable, NULL + }; + + NODE_API_CALL_RETURN_VOID(env, + napi_define_properties(env, exports, 1, &test_null_set)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_null.h b/Tests/NodeApi/test/js-native-api/test_object/test_null.h new file mode 100644 index 00000000..b142570d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_null.js b/Tests/NodeApi/test/js-native-api/test_object/test_null.js new file mode 100644 index 00000000..dcf688c5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_null.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Test passing NULL to object-related N-APIs. +const { testNull } = require(`./build/${common.buildType}/test_object`); + +const expectedForProperty = { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; +assert.deepStrictEqual(testNull.setProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.getProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.hasProperty(), expectedForProperty); +// eslint-disable-next-line no-prototype-builtins +assert.deepStrictEqual(testNull.hasOwnProperty(), expectedForProperty); +// It's OK not to want the result of a deletion. +assert.deepStrictEqual(testNull.deleteProperty(), + Object.assign({}, + expectedForProperty, + { valueIsNull: 'napi_ok' })); +assert.deepStrictEqual(testNull.setNamedProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.getNamedProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.hasNamedProperty(), expectedForProperty); + +const expectedForElement = { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; +assert.deepStrictEqual(testNull.setElement(), expectedForElement); +assert.deepStrictEqual(testNull.getElement(), expectedForElement); +assert.deepStrictEqual(testNull.hasElement(), expectedForElement); +// It's OK not to want the result of a deletion. +assert.deepStrictEqual(testNull.deleteElement(), + Object.assign({}, + expectedForElement, + { valueIsNull: 'napi_ok' })); + +assert.deepStrictEqual(testNull.defineProperties(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + descriptorListIsNull: 'Invalid argument', + utf8nameIsNull: 'Invalid argument', + methodIsNull: 'Invalid argument', +}); + +// `expectedForElement` also works for the APIs below. +assert.deepStrictEqual(testNull.getPropertyNames(), expectedForElement); +assert.deepStrictEqual(testNull.getAllPropertyNames(), expectedForElement); +assert.deepStrictEqual(testNull.getPrototype(), expectedForElement); diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_object.c b/Tests/NodeApi/test/js-native-api/test_object/test_object.c new file mode 100644 index 00000000..4d53e09f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_object.c @@ -0,0 +1,755 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static int test_value = 3; + +static napi_value Get(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + napi_value object = args[0]; + napi_value output; + NODE_API_CALL(env, napi_get_property(env, object, args[1], &output)); + + return output; +} + +static napi_value GetNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + napi_value object = args[0]; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT(env, key_length <= 255, + "Cannot accommodate keys longer than 255 bytes"); + napi_value output; + NODE_API_CALL(env, napi_get_named_property(env, object, key, &output)); + + return output; +} + +static napi_value GetPropertyNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, napi_get_property_names(env, args[0], &output)); + + return output; +} + +static napi_value GetSymbolNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names( + env, args[0], napi_key_include_prototypes, napi_key_skip_strings, + napi_key_numbers_to_strings, &output)); + + return output; +} + +static napi_value GetEnumerableWritableNames(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL( + env, + napi_get_all_property_names(env, + args[0], + napi_key_include_prototypes, + napi_key_enumerable | napi_key_writable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetOwnWritableNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names(env, + args[0], + napi_key_own_only, + napi_key_writable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetEnumerableConfigurableNames(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL( + env, + napi_get_all_property_names(env, + args[0], + napi_key_include_prototypes, + napi_key_enumerable | napi_key_configurable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetOwnConfigurableNames(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names(env, + args[0], + napi_key_own_only, + napi_key_configurable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value Set(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + NODE_API_CALL(env, napi_set_property(env, args[0], args[1], args[2])); + + napi_value valuetrue; + NODE_API_CALL(env, napi_get_boolean(env, true, &valuetrue)); + + return valuetrue; +} + +static napi_value SetNamed(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT(env, key_length <= 255, + "Cannot accommodate keys longer than 255 bytes"); + + NODE_API_CALL(env, napi_set_named_property(env, args[0], key, args[2])); + + napi_value value_true; + NODE_API_CALL(env, napi_get_boolean(env, true, &value_true)); + + return value_true; +} + +static napi_value Has(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + bool has_property; + NODE_API_CALL(env, napi_has_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, value_type1 == napi_string || value_type1 == napi_symbol, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT(env, key_length <= 255, + "Cannot accommodate keys longer than 255 bytes"); + + bool has_property; + NODE_API_CALL(env, napi_has_named_property(env, args[0], key, &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasOwn(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + // napi_valuetype valuetype1; + // NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + // + // NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + // "Wrong type of arguments. Expects a string or symbol as second."); + + bool has_property; + NODE_API_CALL(env, napi_has_own_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value Delete(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + bool result; + napi_value ret; + NODE_API_CALL(env, napi_delete_property(env, args[0], args[1], &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &ret)); + + return ret; +} + +static napi_value New(napi_env env, napi_callback_info info) { + napi_value ret; + NODE_API_CALL(env, napi_create_object(env, &ret)); + + napi_value num; + NODE_API_CALL(env, napi_create_int32(env, 987654321, &num)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_number", num)); + + napi_value str; + const char* str_val = "test string"; + size_t str_len = strlen(str_val); + NODE_API_CALL(env, napi_create_string_utf8(env, str_val, str_len, &str)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_string", str)); + + return ret; +} + +static napi_value Inflate(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value obj = args[0]; + napi_value propertynames; + NODE_API_CALL(env, napi_get_property_names(env, obj, &propertynames)); + + uint32_t i, length; + NODE_API_CALL(env, napi_get_array_length(env, propertynames, &length)); + + for (i = 0; i < length; i++) { + napi_value property_str; + NODE_API_CALL(env, napi_get_element(env, propertynames, i, &property_str)); + + napi_value value; + NODE_API_CALL(env, napi_get_property(env, obj, property_str, &value)); + + double double_val; + NODE_API_CALL(env, napi_get_value_double(env, value, &double_val)); + NODE_API_CALL(env, napi_create_double(env, double_val + 1, &value)); + NODE_API_CALL(env, napi_set_property(env, obj, property_str, value)); + } + + return obj; +} + +static napi_value Wrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_CALL(env, napi_wrap(env, arg, &test_value, NULL, NULL, NULL)); + return NULL; +} + +static napi_value Unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + void* data; + NODE_API_CALL(env, napi_unwrap(env, arg, &data)); + + bool is_expected = (data != NULL && *(int*)data == 3); + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, is_expected, &result)); + return result; +} + +static napi_value TestSetProperty(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value object, key, value; + + NODE_API_CALL(env, napi_create_object(env, &object)); + + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + NODE_API_CALL(env, napi_create_object(env, &value)); + + status = napi_set_property(NULL, object, key, value); + + add_returned_status(env, + "envIsNull", + object, + "Invalid argument", + napi_invalid_arg, + status); + + napi_set_property(env, NULL, key, value); + + add_last_status(env, "objectIsNull", object); + + napi_set_property(env, object, NULL, value); + + add_last_status(env, "keyIsNull", object); + + napi_set_property(env, object, key, NULL); + + add_last_status(env, "valueIsNull", object); + + return object; +} + +static napi_value TestHasProperty(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value object, key; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + status = napi_has_property(NULL, object, key, &result); + + add_returned_status(env, + "envIsNull", + object, + "Invalid argument", + napi_invalid_arg, + status); + + napi_has_property(env, NULL, key, &result); + + add_last_status(env, "objectIsNull", object); + + napi_has_property(env, object, NULL, &result); + + add_last_status(env, "keyIsNull", object); + + napi_has_property(env, object, key, NULL); + + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestGetProperty(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value object, key, result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + NODE_API_CALL(env, napi_create_object(env, &result)); + + status = napi_get_property(NULL, object, key, &result); + + add_returned_status(env, + "envIsNull", + object, + "Invalid argument", + napi_invalid_arg, + status); + + napi_get_property(env, NULL, key, &result); + + add_last_status(env, "objectIsNull", object); + + napi_get_property(env, object, NULL, &result); + + add_last_status(env, "keyIsNull", object); + + napi_get_property(env, object, key, NULL); + + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestFreeze(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value object = args[0]; + NODE_API_CALL(env, napi_object_freeze(env, object)); + + return object; +} + +static napi_value TestSeal(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value object = args[0]; + NODE_API_CALL(env, napi_object_seal(env, object)); + + return object; +} + +// We create two type tags. They are basically 128-bit UUIDs. +#define TYPE_TAG_COUNT 5 +static const napi_type_tag type_tags[TYPE_TAG_COUNT] = { + {0xdaf987b3cc62481a, 0xb745b0497f299531}, + {0xbb7936c374084d9b, 0xa9548d0762eeedb9}, + {0xa5ed9ce2e4c00c38, 0}, + {0, 0}, + {0xa5ed9ce2e4c00c38, 0xdaf987b3cc62481a}, +}; +#define VALIDATE_TYPE_INDEX(env, type_index) \ + do { \ + if ((type_index) >= TYPE_TAG_COUNT) { \ + NODE_API_CALL((env), \ + napi_throw_range_error((env), \ + "NODE_API_TEST_INVALID_TYPE_INDEX", \ + "Invalid type index")); \ + } \ + } while (0) + +static napi_value +TypeTaggedInstance(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + napi_type_tag tag; + + // Below we copy the tag before setting it to prevent bugs where a pointer + // to the tag (instead of the 128-bit tag value) is stored. + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL(env, napi_create_object(env, &instance)); + tag = type_tags[type_index]; + NODE_API_CALL(env, napi_type_tag_object(env, instance, &tag)); + + // Since the tag passed to napi_type_tag_object() was copied to the stack, + // a type tagging implementation that uses a pointer instead of the + // tag value would end up pointing to stack memory. + // When CheckTypeTag() is called later on, it might be the case that this + // stack address has been left untouched by accident (if no subsequent + // function call has clobbered it), which means the pointer would still + // point to valid data. + // To make sure that tags are stored by value and not by reference, + // clear this copy; any implementation using a pointer would end up with + // random stack data or { 0, 0 }, but not the original tag value, and fail. + memset(&tag, 0, sizeof(tag)); + + return instance; +} + +// V8 will not allow us to construct an external with a NULL data value. +#define IN_LIEU_OF_NULL ((void*)0x1) + +static napi_value PlainExternal(napi_env env, napi_callback_info info) { + napi_value instance; + + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + + return instance; +} + +static napi_value TypeTaggedExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + napi_type_tag tag; + + // See TypeTaggedInstance() for an explanation about why we copy the tag + // to the stack and why we call memset on it after the external is tagged. + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + tag = type_tags[type_index]; + NODE_API_CALL(env, napi_type_tag_object(env, instance, &tag)); + + memset(&tag, 0, sizeof(tag)); + + return instance; +} + +static napi_value +CheckTypeTag(napi_env env, napi_callback_info info) { + size_t argc = 2; + bool result; + napi_value argv[2], js_result; + uint32_t type_index; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, argv[0], &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL(env, napi_check_object_type_tag(env, + argv[1], + &type_tags[type_index], + &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &js_result)); + + return js_result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("Get", Get), + DECLARE_NODE_API_PROPERTY("GetNamed", GetNamed), + DECLARE_NODE_API_PROPERTY("GetPropertyNames", GetPropertyNames), + DECLARE_NODE_API_PROPERTY("GetSymbolNames", GetSymbolNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableWritableNames", + GetEnumerableWritableNames), + DECLARE_NODE_API_PROPERTY("GetOwnWritableNames", GetOwnWritableNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableConfigurableNames", + GetEnumerableConfigurableNames), + DECLARE_NODE_API_PROPERTY("GetOwnConfigurableNames", + GetOwnConfigurableNames), + DECLARE_NODE_API_PROPERTY("Set", Set), + DECLARE_NODE_API_PROPERTY("SetNamed", SetNamed), + DECLARE_NODE_API_PROPERTY("Has", Has), + DECLARE_NODE_API_PROPERTY("HasNamed", HasNamed), + DECLARE_NODE_API_PROPERTY("HasOwn", HasOwn), + DECLARE_NODE_API_PROPERTY("Delete", Delete), + DECLARE_NODE_API_PROPERTY("New", New), + DECLARE_NODE_API_PROPERTY("Inflate", Inflate), + DECLARE_NODE_API_PROPERTY("Wrap", Wrap), + DECLARE_NODE_API_PROPERTY("Unwrap", Unwrap), + DECLARE_NODE_API_PROPERTY("TestSetProperty", TestSetProperty), + DECLARE_NODE_API_PROPERTY("TestHasProperty", TestHasProperty), + DECLARE_NODE_API_PROPERTY("TypeTaggedInstance", TypeTaggedInstance), + DECLARE_NODE_API_PROPERTY("TypeTaggedExternal", TypeTaggedExternal), + DECLARE_NODE_API_PROPERTY("PlainExternal", PlainExternal), + DECLARE_NODE_API_PROPERTY("CheckTypeTag", CheckTypeTag), + DECLARE_NODE_API_PROPERTY("TestGetProperty", TestGetProperty), + DECLARE_NODE_API_PROPERTY("TestFreeze", TestFreeze), + DECLARE_NODE_API_PROPERTY("TestSeal", TestSeal), + }; + + init_test_null(env, exports); + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_promise/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_promise/CMakeLists.txt new file mode 100644 index 00000000..0e82dff9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_promise + SOURCES + test_promise.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_promise/binding.gyp b/Tests/NodeApi/test/js-native-api/test_promise/binding.gyp new file mode 100644 index 00000000..c2b65f5a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_promise", + "sources": [ + "test_promise.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_promise/test.js b/Tests/NodeApi/test/js-native-api/test_promise/test.js new file mode 100644 index 00000000..695fdcc2 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/test.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../../common'); + +// This tests the promise-related n-api calls + +const assert = require('assert'); +const test_promise = require(`./build/${common.buildType}/test_promise`); + +// A resolution +{ + const expected_result = 42; + const promise = test_promise.createPromise(); + promise.then( + common.mustCall(function(result) { + assert.strictEqual(result, expected_result); + }), + common.mustNotCall()); + test_promise.concludeCurrentPromise(expected_result, true); +} + +// A rejection +{ + const expected_result = 'It\'s not you, it\'s me.'; + const promise = test_promise.createPromise(); + promise.then( + common.mustNotCall(), + common.mustCall(function(result) { + assert.strictEqual(result, expected_result); + })); + test_promise.concludeCurrentPromise(expected_result, false); +} + +// Chaining +{ + const expected_result = 'chained answer'; + const promise = test_promise.createPromise(); + promise.then( + common.mustCall(function(result) { + assert.strictEqual(result, expected_result); + }), + common.mustNotCall()); + test_promise.concludeCurrentPromise(Promise.resolve('chained answer'), true); +} + +const promiseTypeTestPromise = test_promise.createPromise(); +assert.strictEqual(test_promise.isPromise(promiseTypeTestPromise), true); +test_promise.concludeCurrentPromise(undefined, true); + +const rejectPromise = Promise.reject(-1); +const expected_reason = -1; +assert.strictEqual(test_promise.isPromise(rejectPromise), true); +rejectPromise.catch((reason) => { + assert.strictEqual(reason, expected_reason); +}); + +assert.strictEqual(test_promise.isPromise(2.4), false); +assert.strictEqual(test_promise.isPromise('I promise!'), false); +assert.strictEqual(test_promise.isPromise(undefined), false); +assert.strictEqual(test_promise.isPromise(null), false); +assert.strictEqual(test_promise.isPromise({}), false); diff --git a/Tests/NodeApi/test/js-native-api/test_promise/test_promise.c b/Tests/NodeApi/test/js-native-api/test_promise/test_promise.c new file mode 100644 index 00000000..1f0b5507 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/test_promise.c @@ -0,0 +1,64 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +napi_deferred deferred = NULL; + +static napi_value createPromise(napi_env env, napi_callback_info info) { + napi_value promise; + + // We do not overwrite an existing deferred. + if (deferred != NULL) { + return NULL; + } + + NODE_API_CALL(env, napi_create_promise(env, &deferred, &promise)); + + return promise; +} + +static napi_value +concludeCurrentPromise(napi_env env, napi_callback_info info) { + napi_value argv[2]; + size_t argc = 2; + bool resolution; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_bool(env, argv[1], &resolution)); + if (resolution) { + NODE_API_CALL(env, napi_resolve_deferred(env, deferred, argv[0])); + } else { + NODE_API_CALL(env, napi_reject_deferred(env, deferred, argv[0])); + } + + deferred = NULL; + + return NULL; +} + +static napi_value isPromise(napi_env env, napi_callback_info info) { + napi_value promise, result; + size_t argc = 1; + bool is_promise; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &promise, NULL, NULL)); + NODE_API_CALL(env, napi_is_promise(env, promise, &is_promise)); + NODE_API_CALL(env, napi_get_boolean(env, is_promise, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("createPromise", createPromise), + DECLARE_NODE_API_PROPERTY("concludeCurrentPromise", concludeCurrentPromise), + DECLARE_NODE_API_PROPERTY("isPromise", isPromise), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_properties/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_properties/CMakeLists.txt new file mode 100644 index 00000000..18dcbf18 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/CMakeLists.txt @@ -0,0 +1,6 @@ +add_node_api_module(test_properties + SOURCES + test_properties.c + DEFINES + "NAPI_VERSION=9" +) diff --git a/Tests/NodeApi/test/js-native-api/test_properties/binding.gyp b/Tests/NodeApi/test/js-native-api/test_properties/binding.gyp new file mode 100644 index 00000000..ab9d58db --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_properties", + "sources": [ + "test_properties.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_properties/test.js b/Tests/NodeApi/test/js-native-api/test_properties/test.js new file mode 100644 index 00000000..6e035d67 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/test.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const readonlyErrorRE = + /^TypeError: Cannot assign to read(-| )only property '.*'( of object '#')?$/; +const getterOnlyErrorRE = + /^TypeError: Cannot (set|assign to) property .*( of #)? which has only a getter$/; + +// Testing api calls for defining properties +const test_object = require(`./build/${common.buildType}/test_properties`); + +assert.strictEqual(test_object.echo('hello'), 'hello'); + +test_object.readwriteValue = 1; +assert.strictEqual(test_object.readwriteValue, 1); +test_object.readwriteValue = 2; +assert.strictEqual(test_object.readwriteValue, 2); + +assert.throws(() => { test_object.readonlyValue = 3; }, readonlyErrorRE); + +assert.ok(test_object.hiddenValue); + +// Properties with napi_enumerable attribute should be enumerable. +const propertyNames = []; +for (const name in test_object) { + propertyNames.push(name); +} +assert.ok(propertyNames.includes('echo')); +assert.ok(propertyNames.includes('readwriteValue')); +assert.ok(propertyNames.includes('readonlyValue')); +assert.ok(!propertyNames.includes('hiddenValue')); +assert.ok(propertyNames.includes('NameKeyValue')); +assert.ok(!propertyNames.includes('readwriteAccessor1')); +assert.ok(!propertyNames.includes('readwriteAccessor2')); +assert.ok(!propertyNames.includes('readonlyAccessor1')); +assert.ok(!propertyNames.includes('readonlyAccessor2')); + +// Validate property created with symbol +const start = 'Symbol('.length; +const end = start + 'NameKeySymbol'.length; +const symbolDescription = + String(Object.getOwnPropertySymbols(test_object)[0]).slice(start, end); +assert.strictEqual(symbolDescription, 'NameKeySymbol'); + +// The napi_writable attribute should be ignored for accessors. +const readwriteAccessor1Descriptor = + Object.getOwnPropertyDescriptor(test_object, 'readwriteAccessor1'); +const readonlyAccessor1Descriptor = + Object.getOwnPropertyDescriptor(test_object, 'readonlyAccessor1'); +assert.ok(readwriteAccessor1Descriptor.get != null); +assert.ok(readwriteAccessor1Descriptor.set != null); +assert.ok(readwriteAccessor1Descriptor.value === undefined); +assert.ok(readonlyAccessor1Descriptor.get != null); +assert.ok(readonlyAccessor1Descriptor.set === undefined); +assert.ok(readonlyAccessor1Descriptor.value === undefined); +test_object.readwriteAccessor1 = 1; +assert.strictEqual(test_object.readwriteAccessor1, 1); +assert.strictEqual(test_object.readonlyAccessor1, 1); +assert.throws(() => { test_object.readonlyAccessor1 = 3; }, getterOnlyErrorRE); +test_object.readwriteAccessor2 = 2; +assert.strictEqual(test_object.readwriteAccessor2, 2); +assert.strictEqual(test_object.readonlyAccessor2, 2); +assert.throws(() => { test_object.readonlyAccessor2 = 3; }, getterOnlyErrorRE); + +assert.strictEqual(test_object.hasNamedProperty(test_object, 'echo'), true); +assert.strictEqual(test_object.hasNamedProperty(test_object, 'hiddenValue'), + true); +assert.strictEqual(test_object.hasNamedProperty(test_object, 'doesnotexist'), + false); diff --git a/Tests/NodeApi/test/js-native-api/test_properties/test_properties.c b/Tests/NodeApi/test/js-native-api/test_properties/test_properties.c new file mode 100644 index 00000000..7b8e67a9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/test_properties.c @@ -0,0 +1,113 @@ +#define NAPI_VERSION 9 +#include +#include "../common.h" +#include "../entry_point.h" + +static double value_ = 1; + +static napi_value GetValue(napi_env env, napi_callback_info info) { + size_t argc = 0; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, NULL, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 0, "Wrong number of arguments"); + + napi_value number; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + return number; +} + +static napi_value SetValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value_)); + + return NULL; +} + +static napi_value Echo(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + return args[0]; +} + +static napi_value HasNamedProperty(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + // Extract the name of the property to check + char buffer[128]; + size_t copied; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], buffer, sizeof(buffer), &copied)); + + // do the check and create the boolean return value + bool value; + napi_value result; + NODE_API_CALL(env, napi_has_named_property(env, args[0], buffer, &value)); + NODE_API_CALL(env, napi_get_boolean(env, value, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value number; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + napi_value name_value; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "NameKeyValue", NAPI_AUTO_LENGTH, &name_value)); + + napi_value symbol_description; + napi_value name_symbol; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "NameKeySymbol", NAPI_AUTO_LENGTH, &symbol_description)); + NODE_API_CALL(env, + napi_create_symbol(env, symbol_description, &name_symbol)); + + napi_value name_symbol_descriptionless; + NODE_API_CALL(env, + napi_create_symbol(env, NULL, &name_symbol_descriptionless)); + + napi_value name_symbol_for; + NODE_API_CALL(env, node_api_symbol_for(env, + "NameKeySymbolFor", + NAPI_AUTO_LENGTH, + &name_symbol_for)); + + napi_property_descriptor properties[] = { + { "echo", 0, Echo, 0, 0, 0, napi_enumerable, 0 }, + { "readwriteValue", 0, 0, 0, 0, number, napi_enumerable | napi_writable, 0 }, + { "readonlyValue", 0, 0, 0, 0, number, napi_enumerable, 0}, + { "hiddenValue", 0, 0, 0, 0, number, napi_default, 0}, + { NULL, name_value, 0, 0, 0, number, napi_enumerable, 0}, + { NULL, name_symbol, 0, 0, 0, number, napi_enumerable, 0}, + { NULL, name_symbol_descriptionless, 0, 0, 0, number, napi_enumerable, 0}, + { NULL, name_symbol_for, 0, 0, 0, number, napi_enumerable, 0}, + { "readwriteAccessor1", 0, 0, GetValue, SetValue, 0, napi_default, 0}, + { "readwriteAccessor2", 0, 0, GetValue, SetValue, 0, napi_writable, 0}, + { "readonlyAccessor1", 0, 0, GetValue, NULL, 0, napi_default, 0}, + { "readonlyAccessor2", 0, 0, GetValue, NULL, 0, napi_writable, 0}, + { "hasNamedProperty", 0, HasNamedProperty, 0, 0, 0, napi_default, 0 }, + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_reference/CMakeLists.txt new file mode 100644 index 00000000..2c0dd7b5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/CMakeLists.txt @@ -0,0 +1,11 @@ +add_node_api_module(test_reference + SOURCES + test_reference.c + DEFINES + "NAPI_VERSION=9" +) + +add_node_api_module(test_finalizer + SOURCES + test_finalizer.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_reference/binding.gyp b/Tests/NodeApi/test/js-native-api/test_reference/binding.gyp new file mode 100644 index 00000000..2f2acb3a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/binding.gyp @@ -0,0 +1,16 @@ +{ + "targets": [ + { + "target_name": "test_reference", + "sources": [ + "test_reference.c" + ] + }, + { + "target_name": "test_finalizer", + "sources": [ + "test_finalizer.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test.js b/Tests/NodeApi/test/js-native-api/test_reference/test.js new file mode 100644 index 00000000..aa5c9953 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test.js @@ -0,0 +1,158 @@ +'use strict'; +// Flags: --expose-gc + +const { buildType } = require('../../common'); +const { gcUntil } = require('../../common/gc'); +const assert = require('assert'); + +const test_reference = require(`./build/${buildType}/test_reference`); + +// This test script uses external values with finalizer callbacks +// in order to track when values get garbage-collected. Each invocation +// of a finalizer callback increments the finalizeCount property. +assert.strictEqual(test_reference.finalizeCount, 0); + +// Run each test function in sequence, +// with an async delay and GC call between each. +async function runTests() { + (() => { + const symbol = test_reference.createSymbol('testSym'); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor('testSymFor'); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor('testSymFor'); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for('testSymFor')); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, Symbol.for('')); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for('')); + })(); + test_reference.deleteReference(); + + assert.throws(() => test_reference.createSymbolForIncorrectLength(), + /Invalid argument/); + + (() => { + const value = test_reference.createExternal(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, 'object'); + test_reference.checkExternal(value); + })(); + await gcUntil('External value without a finalizer', + () => (test_reference.finalizeCount === 0)); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, 'object'); + test_reference.checkExternal(value); + })(); + await gcUntil('External value with a finalizer', + () => (test_reference.finalizeCount === 1)); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 0); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should be GC'd because there is only a weak ref + await gcUntil('Weak reference', + () => (test_reference.referenceValue === undefined && + test_reference.finalizeCount === 1)); + test_reference.deleteReference(); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil('Strong reference', + () => (test_reference.finalizeCount === 0)); + test_reference.deleteReference(); + await gcUntil('Strong reference (cont.d)', + () => (test_reference.finalizeCount === 1)); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil('Strong reference, increment then decrement to weak reference', + () => (test_reference.finalizeCount === 0)); + assert.strictEqual(test_reference.incrementRefcount(), 2); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-1)', + () => (test_reference.finalizeCount === 0)); + assert.strictEqual(test_reference.decrementRefcount(), 1); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-2)', + () => (test_reference.finalizeCount === 0)); + assert.strictEqual(test_reference.decrementRefcount(), 0); + // Value should be GC'd because the ref is now weak! + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-3)', + () => (test_reference.finalizeCount === 1)); + test_reference.deleteReference(); + // Value was already GC'd + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-4)', + () => (test_reference.finalizeCount === 1)); +} +runTests(); + +// This test creates a napi_ref on an object that has +// been wrapped by napi_wrap and for which the finalizer +// for the wrap calls napi_delete_ref on that napi_ref. +// +// Since both the wrap and the reference use the same +// object the finalizer for the wrap and reference +// may run in the same gc and in any order. +// +// It does that to validate that napi_delete_ref can be +// called before the finalizer has been run for the +// reference (there is a finalizer behind the scenes even +// though it cannot be passed to napi_create_reference). +// +// Since the order is not guaranteed, run the +// test a number of times maximize the chance that we +// get a run with the desired order for the test. +// +// 1000 reliably recreated the problem without the fix +// required to ensure delete could be called before +// the finalizer in manual testing. +for (let i = 0; i < 1000; i++) { + (() => { + const wrapObject = new Object(); + test_reference.validateDeleteBeforeFinalize(wrapObject); + })(); + global.gc(); +} diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.c b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.c new file mode 100644 index 00000000..0ce671b6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.c @@ -0,0 +1,79 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} +static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) { + finalize_count++; + + int* actual_value = data; + NODE_API_ASSERT_RETURN_VOID( + env, + actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + + napi_ref finalizer_ref = (napi_ref)hint; + napi_value js_finalizer; + napi_value recv; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, finalizer_ref, &js_finalizer)); + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref)); +} + +static napi_value CreateExternalWithJsFinalize(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + napi_value finalizer = args[0]; + napi_valuetype finalizer_valuetype; + NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype)); + NODE_API_ASSERT(env, + finalizer_valuetype == napi_function, + "Wrong type of first argument"); + napi_ref finalizer_ref; + NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref)); + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternalCallJs, + finalizer_ref, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize", + CreateExternalWithJsFinalize), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.js b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.js new file mode 100644 index 00000000..0b973163 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.js @@ -0,0 +1,24 @@ +'use strict'; +// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy + +const common = require('../../common'); +const binding = require(`./build/${common.buildType}/test_finalizer`); +const assert = require('assert'); +const { gcUntil } = require('../../common/gc'); + +process.on('uncaughtException', common.mustCall((err) => { + assert.throws(() => { throw err; }, /finalizer error/); +})); + +(async function() { + (() => { + binding.createExternalWithJsFinalize( + common.mustCall(() => { + throw new Error('finalizer error'); + }) + ); + })(); + await gcUntil('External value calls finalizer', + () => (binding.finalizeCount === 1)); + +})().then(common.mustCall()); diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test_reference.c b/Tests/NodeApi/test/js-native-api/test_reference/test_reference.c new file mode 100644 index 00000000..a66b8068 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test_reference.c @@ -0,0 +1,252 @@ +#define NAPI_VERSION 9 +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; +static napi_ref test_reference = NULL; + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} + +static void FinalizeExternal(napi_env env, void* data, void* hint) { + int *actual_value = data; + NODE_API_ASSERT_RETURN_VOID(env, actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + finalize_count++; +} + +static napi_value CreateExternal(napi_env env, napi_callback_info info) { + int* data = &test_value; + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + data, + NULL, /* finalize_cb */ + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CreateSymbol(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + napi_value result_symbol; + + NODE_API_CALL(env, napi_create_symbol(env, args[0], &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolFor(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + + char description[256]; + size_t description_length; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + NODE_API_CALL( + env, + napi_get_value_string_utf8( + env, args[0], description, sizeof(description), &description_length)); + NODE_API_ASSERT(env, + description_length <= 255, + "Cannot accommodate descriptions longer than 255 bytes"); + + napi_value result_symbol; + + NODE_API_CALL(env, + node_api_symbol_for( + env, description, description_length, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForEmptyString(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 0, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForIncorrectLength(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 5, &result_symbol)); + return result_symbol; +} + +static napi_value +CreateExternalWithFinalize(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternal, + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CheckExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Expected one argument."); + + napi_valuetype argtype; + NODE_API_CALL(env, napi_typeof(env, arg, &argtype)); + + NODE_API_ASSERT(env, argtype == napi_external, "Expected an external value."); + + void* data; + NODE_API_CALL(env, napi_get_value_external(env, arg, &data)); + + NODE_API_ASSERT(env, data != NULL && *(int*)data == test_value, + "An external data value of 1 was expected."); + + return NULL; +} + +static napi_value CreateReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference == NULL, + "The test allows only one reference at a time."); + + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Expected two arguments."); + + uint32_t initial_refcount; + NODE_API_CALL(env, napi_get_value_uint32(env, args[1], &initial_refcount)); + + NODE_API_CALL(env, + napi_create_reference(env, args[0], initial_refcount, &test_reference)); + + NODE_API_ASSERT(env, test_reference != NULL, + "A reference should have been created."); + + return NULL; +} + +static napi_value DeleteReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + NODE_API_CALL(env, napi_delete_reference(env, test_reference)); + test_reference = NULL; + return NULL; +} + +static napi_value IncrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_ref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value DecrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_unref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value GetReferenceValue(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + napi_value result; + NODE_API_CALL(env, napi_get_reference_value(env, test_reference, &result)); + return result; +} + +static void DeleteBeforeFinalizeFinalizer( + napi_env env, void* finalize_data, void* finalize_hint) { + napi_ref* ref = (napi_ref*)finalize_data; + napi_value value; + assert(napi_get_reference_value(env, *ref, &value) == napi_ok); + assert(value == NULL); + napi_delete_reference(env, *ref); + free(ref); +} + +static napi_value ValidateDeleteBeforeFinalize(napi_env env, napi_callback_info info) { + napi_value wrapObject; + size_t argc = 1; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapObject, NULL, NULL)); + + napi_ref* ref_t = malloc(sizeof(napi_ref)); + NODE_API_CALL(env, + napi_wrap( + env, wrapObject, ref_t, DeleteBeforeFinalizeFinalizer, NULL, NULL)); + + // Create a reference that will be eligible for collection at the same + // time as the wrapped object by passing in the same wrapObject. + // This means that the FinalizeOrderValidation callback may be run + // before the finalizer for the newly created reference (there is a finalizer + // behind the scenes even though it cannot be passed to napi_create_reference) + // The Finalizer for the wrap (which is different than the finalizer + // for the reference) calls napi_delete_reference validating that + // napi_delete_reference can be called before the finalizer for the + // reference runs. + NODE_API_CALL(env, napi_create_reference(env, wrapObject, 0, ref_t)); + return wrapObject; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal), + DECLARE_NODE_API_PROPERTY("createExternalWithFinalize", + CreateExternalWithFinalize), + DECLARE_NODE_API_PROPERTY("checkExternal", CheckExternal), + DECLARE_NODE_API_PROPERTY("createReference", CreateReference), + DECLARE_NODE_API_PROPERTY("createSymbol", CreateSymbol), + DECLARE_NODE_API_PROPERTY("createSymbolFor", CreateSymbolFor), + DECLARE_NODE_API_PROPERTY("createSymbolForEmptyString", + CreateSymbolForEmptyString), + DECLARE_NODE_API_PROPERTY("createSymbolForIncorrectLength", + CreateSymbolForIncorrectLength), + DECLARE_NODE_API_PROPERTY("deleteReference", DeleteReference), + DECLARE_NODE_API_PROPERTY("incrementRefcount", IncrementRefcount), + DECLARE_NODE_API_PROPERTY("decrementRefcount", DecrementRefcount), + DECLARE_NODE_API_GETTER("referenceValue", GetReferenceValue), + DECLARE_NODE_API_PROPERTY("validateDeleteBeforeFinalize", + ValidateDeleteBeforeFinalize), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_reference_double_free/CMakeLists.txt new file mode 100644 index 00000000..3fe097a0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_reference_double_free + SOURCES + test_reference_double_free.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/binding.gyp b/Tests/NodeApi/test/js-native-api/test_reference_double_free/binding.gyp new file mode 100644 index 00000000..8e49ef22 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_reference_double_free", + "sources": [ + "test_reference_double_free.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/test.js b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test.js new file mode 100644 index 00000000..f9a465c5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test.js @@ -0,0 +1,11 @@ +'use strict'; + +// This test makes no assertions. It tests a fix without which it will crash +// with a double free. + +const { buildType } = require('../../common'); + +const addon = require(`./build/${buildType}/test_reference_double_free`); + +{ new addon.MyObject(true); } +{ new addon.MyObject(false); } diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_reference_double_free.c b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_reference_double_free.c new file mode 100644 index 00000000..e99667a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_reference_double_free.c @@ -0,0 +1,90 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static size_t g_call_count = 0; + +static void Destructor(napi_env env, void* data, void* nothing) { + napi_ref* ref = data; + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, *ref)); + free(ref); +} + +static void NoDeleteDestructor(napi_env env, void* data, void* hint) { + napi_ref* ref = data; + size_t* call_count = hint; + + // This destructor must be called exactly once. + if ((*call_count) > 0) abort(); + *call_count = ((*call_count) + 1); + free(ref); +} + +static napi_value New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value js_this, js_delete; + bool delete; + napi_ref* ref = malloc(sizeof(*ref)); + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &js_delete, &js_this, NULL)); + NODE_API_CALL(env, napi_get_value_bool(env, js_delete, &delete)); + + if (delete) { + NODE_API_CALL(env, + napi_wrap(env, js_this, ref, Destructor, NULL, ref)); + } else { + NODE_API_CALL(env, + napi_wrap(env, js_this, ref, NoDeleteDestructor, &g_call_count, ref)); + } + NODE_API_CALL(env, napi_reference_ref(env, *ref, NULL)); + + return js_this; +} + +static void NoopDeleter(napi_env env, void* data, void* hint) {} + +// Tests that calling napi_remove_wrap and napi_delete_reference consecutively +// doesn't crash the process. +// This is analogous to the test https://github.com/nodejs/node-addon-api/blob/main/test/objectwrap_constructor_exception.cc. +// In which the Napi::ObjectWrap<> is being destructed immediately after napi_wrap. +// As Napi::ObjectWrap<> is a subclass of Napi::Reference<>, napi_remove_wrap +// in the destructor of Napi::ObjectWrap<> is called before napi_delete_reference +// in the destructor of Napi::Reference<>. +static napi_value DeleteImmediately(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value js_obj; + napi_ref ref; + napi_valuetype type; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &js_obj, NULL, NULL)); + + NODE_API_CALL(env, napi_typeof(env, js_obj, &type)); + NODE_API_ASSERT(env, type == napi_object, "Expected object parameter"); + + NODE_API_CALL(env, napi_wrap(env, js_obj, NULL, NoopDeleter, NULL, &ref)); + NODE_API_CALL(env, napi_remove_wrap(env, js_obj, NULL)); + NODE_API_CALL(env, napi_delete_reference(env, ref)); + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value myobj_ctor; + NODE_API_CALL(env, + napi_define_class( + env, "MyObject", NAPI_AUTO_LENGTH, New, NULL, 0, NULL, &myobj_ctor)); + NODE_API_CALL(env, + napi_set_named_property(env, exports, "MyObject", myobj_ctor)); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("deleteImmediately", DeleteImmediately), + }; + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_wrap.js b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_wrap.js new file mode 100644 index 00000000..f7f75094 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_wrap.js @@ -0,0 +1,10 @@ +'use strict'; + +// This test makes no assertions. It tests that calling napi_remove_wrap and +// napi_delete_reference consecutively doesn't crash the process. + +const { buildType } = require('../../common'); + +const addon = require(`./build/${buildType}/test_reference_double_free`); + +addon.deleteImmediately({}); diff --git a/Tests/NodeApi/test/js-native-api/test_string/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_string/CMakeLists.txt new file mode 100644 index 00000000..5244fdba --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/CMakeLists.txt @@ -0,0 +1,7 @@ +add_node_api_module(test_string + SOURCES + test_string.c + test_null.c + DEFINES + "NAPI_VERSION=10" +) diff --git a/Tests/NodeApi/test/js-native-api/test_string/binding.gyp b/Tests/NodeApi/test/js-native-api/test_string/binding.gyp new file mode 100644 index 00000000..550e33b4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/binding.gyp @@ -0,0 +1,14 @@ +{ + "targets": [ + { + "target_name": "test_string", + "sources": [ + "test_string.c", + "test_null.c", + ], + "defines": [ + "NAPI_VERSION=10", + ], + }, + ], +} diff --git a/Tests/NodeApi/test/js-native-api/test_string/test.js b/Tests/NodeApi/test/js-native-api/test_string/test.js new file mode 100644 index 00000000..5d03ba9d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test.js @@ -0,0 +1,91 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for string +const test_string = require(`./build/${common.buildType}/test_string`); +// The insufficient buffer test case allocates a buffer of size 4, including +// the null terminator. +const kInsufficientIdx = 3; + +const asciiCases = [ + '', + 'hello world', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + '?!@#$%^&*()_+-=[]{}/.,<>\'"\\', +]; + +const latin1Cases = [ + { + str: '¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿', + utf8Length: 62, + utf8InsufficientIdx: 1, + }, + { + str: 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ', + utf8Length: 126, + utf8InsufficientIdx: 1, + }, +]; + +const unicodeCases = [ + { + str: '\u{2003}\u{2101}\u{2001}\u{202}\u{2011}', + utf8Length: 14, + utf8InsufficientIdx: 1, + }, +]; + +function testLatin1Cases(str) { + assert.strictEqual(test_string.TestLatin1(str), str); + assert.strictEqual(test_string.TestLatin1AutoLength(str), str); + assert.strictEqual(test_string.TestLatin1External(str), str); + assert.strictEqual(test_string.TestLatin1ExternalAutoLength(str), str); + assert.strictEqual(test_string.TestPropertyKeyLatin1(str), str); + assert.strictEqual(test_string.TestPropertyKeyLatin1AutoLength(str), str); + assert.strictEqual(test_string.Latin1Length(str), str.length); + + if (str !== '') { + assert.strictEqual(test_string.TestLatin1Insufficient(str), str.slice(0, kInsufficientIdx)); + } +} + +function testUnicodeCases(str, utf8Length, utf8InsufficientIdx) { + assert.strictEqual(test_string.TestUtf8(str), str); + assert.strictEqual(test_string.TestUtf16(str), str); + assert.strictEqual(test_string.TestUtf8AutoLength(str), str); + assert.strictEqual(test_string.TestUtf16AutoLength(str), str); + assert.strictEqual(test_string.TestUtf16External(str), str); + assert.strictEqual(test_string.TestUtf16ExternalAutoLength(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf8(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf8AutoLength(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf16(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf16AutoLength(str), str); + assert.strictEqual(test_string.Utf8Length(str), utf8Length); + assert.strictEqual(test_string.Utf16Length(str), str.length); + + if (str !== '') { + assert.strictEqual(test_string.TestUtf8Insufficient(str), str.slice(0, utf8InsufficientIdx)); + assert.strictEqual(test_string.TestUtf16Insufficient(str), str.slice(0, kInsufficientIdx)); + } +} + +asciiCases.forEach(testLatin1Cases); +asciiCases.forEach((str) => testUnicodeCases(str, str.length, kInsufficientIdx)); +latin1Cases.forEach((it) => testLatin1Cases(it.str)); +latin1Cases.forEach((it) => testUnicodeCases(it.str, it.utf8Length, it.utf8InsufficientIdx)); +unicodeCases.forEach((it) => testUnicodeCases(it.str, it.utf8Length, it.utf8InsufficientIdx)); + +assert.throws(() => { + test_string.TestLargeUtf8(); +}, /^Error: Invalid argument$/); + +assert.throws(() => { + test_string.TestLargeLatin1(); +}, /^Error: Invalid argument$/); + +assert.throws(() => { + test_string.TestLargeUtf16(); +}, /^Error: Invalid argument$/); + +test_string.TestMemoryCorruption(' '.repeat(64 * 1024)); diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_null.c b/Tests/NodeApi/test/js-native-api/test_string/test_null.c new file mode 100644 index 00000000..84c1fc40 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_null.c @@ -0,0 +1,71 @@ +#include + +#include "../common.h" +#include "test_null.h" + +#define DECLARE_TEST(charset, str_arg) \ + static napi_value \ + test_create_##charset(napi_env env, napi_callback_info info) { \ + napi_value return_value, result; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + napi_create_string_##charset(NULL, \ + (str_arg), \ + NAPI_AUTO_LENGTH, \ + &result)); \ + \ + napi_create_string_##charset(env, NULL, NAPI_AUTO_LENGTH, &result); \ + add_last_status(env, "stringIsNullNonZeroLength", return_value); \ + \ + napi_create_string_##charset(env, NULL, 0, &result); \ + add_last_status(env, "stringIsNullZeroLength", return_value); \ + \ + napi_create_string_##charset(env, (str_arg), NAPI_AUTO_LENGTH, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + \ + return return_value; \ + } + +static const char16_t something[] = { + (char16_t)'s', + (char16_t)'o', + (char16_t)'m', + (char16_t)'e', + (char16_t)'t', + (char16_t)'h', + (char16_t)'i', + (char16_t)'n', + (char16_t)'g', + (char16_t)'\0' +}; + +DECLARE_TEST(utf8, "something") +DECLARE_TEST(latin1, "something") +DECLARE_TEST(utf16, something) + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("test_create_utf8", test_create_utf8), + DECLARE_NODE_API_PROPERTY("test_create_latin1", test_create_latin1), + DECLARE_NODE_API_PROPERTY("test_create_utf16", test_create_utf16), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID(env, napi_define_properties( + env, test_null, sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + const napi_property_descriptor test_null_set = { + "testNull", NULL, NULL, NULL, NULL, test_null, napi_enumerable, NULL + }; + + NODE_API_CALL_RETURN_VOID(env, + napi_define_properties(env, exports, 1, &test_null_set)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_null.h b/Tests/NodeApi/test/js-native-api/test_string/test_null.h new file mode 100644 index 00000000..95be6359 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_STRING_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_STRING_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_STRING_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_null.js b/Tests/NodeApi/test/js-native-api/test_string/test_null.js new file mode 100644 index 00000000..71963009 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_null.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Test passing NULL to object-related N-APIs. +const { testNull } = require(`./build/${common.buildType}/test_string`); + +const expectedResult = { + envIsNull: 'Invalid argument', + stringIsNullNonZeroLength: 'Invalid argument', + stringIsNullZeroLength: 'napi_ok', + resultIsNull: 'Invalid argument', +}; + +assert.deepStrictEqual(expectedResult, testNull.test_create_latin1()); +assert.deepStrictEqual(expectedResult, testNull.test_create_utf8()); +assert.deepStrictEqual(expectedResult, testNull.test_create_utf16()); diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_string.c b/Tests/NodeApi/test/js-native-api/test_string/test_string.c new file mode 100644 index 00000000..c6874dc7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_string.c @@ -0,0 +1,498 @@ +#include +#include // INT_MAX +#include +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +enum length_type { actual_length, auto_length }; + +static napi_status validate_and_retrieve_single_string_arg( + napi_env env, napi_callback_info info, napi_value* arg) { + size_t argc = 1; + NODE_API_CHECK_STATUS(napi_get_cb_info(env, info, &argc, arg, NULL, NULL)); + + NODE_API_ASSERT_STATUS(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype; + NODE_API_CHECK_STATUS(napi_typeof(env, *arg, &valuetype)); + + NODE_API_ASSERT_STATUS(env, + valuetype == napi_string, + "Wrong type of argment. Expects a string."); + + return napi_ok; +} + +// These help us factor out code that is common between the bindings. +typedef napi_status (*OneByteCreateAPI)(napi_env, + const char*, + size_t, + napi_value*); +typedef napi_status (*OneByteGetAPI)( + napi_env, napi_value, char*, size_t, size_t*); +typedef napi_status (*TwoByteCreateAPI)(napi_env, + const char16_t*, + size_t, + napi_value*); +typedef napi_status (*TwoByteGetAPI)( + napi_env, napi_value, char16_t*, size_t, size_t*); + +// Test passing back the one-byte string we got from JS. +static napi_value TestOneByteImpl(napi_env env, + napi_callback_info info, + OneByteGetAPI get_api, + OneByteCreateAPI create_api, + enum length_type length_mode) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char buffer[128]; + size_t buffer_size = 128; + size_t copied; + + NODE_API_CALL(env, get_api(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + if (length_mode == auto_length) { + copied = NAPI_AUTO_LENGTH; + } + NODE_API_CALL(env, create_api(env, buffer, copied, &output)); + + return output; +} + +// Test passing back the two-byte string we got from JS. +static napi_value TestTwoByteImpl(napi_env env, + napi_callback_info info, + TwoByteGetAPI get_api, + TwoByteCreateAPI create_api, + enum length_type length_mode) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char16_t buffer[128]; + size_t buffer_size = 128; + size_t copied; + + NODE_API_CALL(env, get_api(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + if (length_mode == auto_length) { + copied = NAPI_AUTO_LENGTH; + } + NODE_API_CALL(env, create_api(env, buffer, copied, &output)); + + return output; +} + +static void free_string(node_api_basic_env env, void* data, void* hint) { + free(data); +} + +static napi_status create_external_latin1(napi_env env, + const char* string, + size_t length, + napi_value* result) { + napi_status status; + // Initialize to true, because that is the value we don't want. + bool copied = true; + char* string_copy; + const size_t actual_length = + (length == NAPI_AUTO_LENGTH ? strlen(string) : length); + const size_t length_bytes = (actual_length + 1) * sizeof(*string_copy); + string_copy = malloc(length_bytes); + memcpy(string_copy, string, length_bytes); + string_copy[actual_length] = 0; + + status = node_api_create_external_string_latin1( + env, string_copy, length, free_string, NULL, result, &copied); + // We do not want the string to be copied. + if (copied) { + return napi_generic_failure; + } + if (status != napi_ok) { + free(string_copy); + return status; + } + return napi_ok; +} + +// strlen for char16_t. Needed in case we're copying a string of length +// NAPI_AUTO_LENGTH. +static size_t strlen16(const char16_t* string) { + for (const char16_t* iter = string;; iter++) { + if (*iter == 0) { + return iter - string; + } + } + // We should never get here. + abort(); +} + +static napi_status create_external_utf16(napi_env env, + const char16_t* string, + size_t length, + napi_value* result) { + napi_status status; + // Initialize to true, because that is the value we don't want. + bool copied = true; + char16_t* string_copy; + const size_t actual_length = + (length == NAPI_AUTO_LENGTH ? strlen16(string) : length); + const size_t length_bytes = (actual_length + 1) * sizeof(*string_copy); + string_copy = malloc(length_bytes); + memcpy(string_copy, string, length_bytes); + string_copy[actual_length] = 0; + + status = node_api_create_external_string_utf16( + env, string_copy, length, free_string, NULL, result, &copied); + if (status != napi_ok) { + free(string_copy); + return status; + } + + return napi_ok; +} + +static napi_value TestLatin1(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + napi_create_string_latin1, + actual_length); +} + +static napi_value TestUtf8(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + napi_create_string_utf8, + actual_length); +} + +static napi_value TestUtf16(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + napi_create_string_utf16, + actual_length); +} + +static napi_value TestLatin1AutoLength(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + napi_create_string_latin1, + auto_length); +} + +static napi_value TestUtf8AutoLength(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + napi_create_string_utf8, + auto_length); +} + +static napi_value TestUtf16AutoLength(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + napi_create_string_utf16, + auto_length); +} + +static napi_value TestLatin1External(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + create_external_latin1, + actual_length); +} + +static napi_value TestUtf16External(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + create_external_utf16, + actual_length); +} + +static napi_value TestLatin1ExternalAutoLength(napi_env env, + napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + create_external_latin1, + auto_length); +} + +static napi_value TestUtf16ExternalAutoLength(napi_env env, + napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + create_external_utf16, + auto_length); +} + +static napi_value TestLatin1Insufficient(napi_env env, + napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char buffer[4]; + size_t buffer_size = 4; + size_t copied; + + NODE_API_CALL( + env, + napi_get_value_string_latin1(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_latin1(env, buffer, copied, &output)); + + return output; +} + +static napi_value TestUtf8Insufficient(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char buffer[4]; + size_t buffer_size = 4; + size_t copied; + + NODE_API_CALL( + env, + napi_get_value_string_utf8(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_utf8(env, buffer, copied, &output)); + + return output; +} + +static napi_value TestUtf16Insufficient(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char16_t buffer[4]; + size_t buffer_size = 4; + size_t copied; + + NODE_API_CALL( + env, + napi_get_value_string_utf16(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_utf16(env, buffer, copied, &output)); + + return output; +} + +static napi_value TestPropertyKeyLatin1(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + node_api_create_property_key_latin1, + actual_length); +} + +static napi_value TestPropertyKeyLatin1AutoLength(napi_env env, + napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + node_api_create_property_key_latin1, + auto_length); +} + +static napi_value TestPropertyKeyUtf8(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + node_api_create_property_key_utf8, + actual_length); +} + +static napi_value TestPropertyKeyUtf8AutoLength(napi_env env, + napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + node_api_create_property_key_utf8, + auto_length); +} + +static napi_value TestPropertyKeyUtf16(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + node_api_create_property_key_utf16, + actual_length); +} + +static napi_value TestPropertyKeyUtf16AutoLength(napi_env env, + napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + node_api_create_property_key_utf16, + auto_length); +} + +static napi_value Latin1Length(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + size_t length; + NODE_API_CALL(env, + napi_get_value_string_latin1(env, args[0], NULL, 0, &length)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); + + return output; +} + +static napi_value Utf16Length(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + size_t length; + NODE_API_CALL(env, + napi_get_value_string_utf16(env, args[0], NULL, 0, &length)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); + + return output; +} + +static napi_value Utf8Length(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + size_t length; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[0], NULL, 0, &length)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); + + return output; +} + +static napi_value TestLargeUtf8(napi_env env, napi_callback_info info) { + napi_value output; + if (SIZE_MAX > INT_MAX) { + NODE_API_CALL( + env, napi_create_string_utf8(env, "", ((size_t)INT_MAX) + 1, &output)); + } else { + // just throw the expected error as there is nothing to test + // in this case since we can't overflow + NODE_API_CALL(env, napi_throw_error(env, NULL, "Invalid argument")); + } + + return output; +} + +static napi_value TestLargeLatin1(napi_env env, napi_callback_info info) { + napi_value output; + if (SIZE_MAX > INT_MAX) { + NODE_API_CALL( + env, + napi_create_string_latin1(env, "", ((size_t)INT_MAX) + 1, &output)); + } else { + // just throw the expected error as there is nothing to test + // in this case since we can't overflow + NODE_API_CALL(env, napi_throw_error(env, NULL, "Invalid argument")); + } + + return output; +} + +static napi_value TestLargeUtf16(napi_env env, napi_callback_info info) { + napi_value output; + if (SIZE_MAX > INT_MAX) { + NODE_API_CALL( + env, + napi_create_string_utf16( + env, ((const char16_t*)""), ((size_t)INT_MAX) + 1, &output)); + } else { + // just throw the expected error as there is nothing to test + // in this case since we can't overflow + NODE_API_CALL(env, napi_throw_error(env, NULL, "Invalid argument")); + } + + return output; +} + +static napi_value TestMemoryCorruption(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + char buf[10] = {0}; + NODE_API_CALL(env, napi_get_value_string_utf8(env, args[0], buf, 0, NULL)); + + char zero[10] = {0}; + if (memcmp(buf, zero, sizeof(buf)) != 0) { + NODE_API_CALL(env, napi_throw_error(env, NULL, "Buffer overwritten")); + } + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("TestLatin1", TestLatin1), + DECLARE_NODE_API_PROPERTY("TestLatin1AutoLength", TestLatin1AutoLength), + DECLARE_NODE_API_PROPERTY("TestLatin1External", TestLatin1External), + DECLARE_NODE_API_PROPERTY("TestLatin1ExternalAutoLength", + TestLatin1ExternalAutoLength), + DECLARE_NODE_API_PROPERTY("TestLatin1Insufficient", + TestLatin1Insufficient), + DECLARE_NODE_API_PROPERTY("TestUtf8", TestUtf8), + DECLARE_NODE_API_PROPERTY("TestUtf8AutoLength", TestUtf8AutoLength), + DECLARE_NODE_API_PROPERTY("TestUtf8Insufficient", TestUtf8Insufficient), + DECLARE_NODE_API_PROPERTY("TestUtf16", TestUtf16), + DECLARE_NODE_API_PROPERTY("TestUtf16AutoLength", TestUtf16AutoLength), + DECLARE_NODE_API_PROPERTY("TestUtf16External", TestUtf16External), + DECLARE_NODE_API_PROPERTY("TestUtf16ExternalAutoLength", + TestUtf16ExternalAutoLength), + DECLARE_NODE_API_PROPERTY("TestUtf16Insufficient", TestUtf16Insufficient), + DECLARE_NODE_API_PROPERTY("Latin1Length", Latin1Length), + DECLARE_NODE_API_PROPERTY("Utf16Length", Utf16Length), + DECLARE_NODE_API_PROPERTY("Utf8Length", Utf8Length), + DECLARE_NODE_API_PROPERTY("TestLargeUtf8", TestLargeUtf8), + DECLARE_NODE_API_PROPERTY("TestLargeLatin1", TestLargeLatin1), + DECLARE_NODE_API_PROPERTY("TestLargeUtf16", TestLargeUtf16), + DECLARE_NODE_API_PROPERTY("TestMemoryCorruption", TestMemoryCorruption), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyLatin1", TestPropertyKeyLatin1), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyLatin1AutoLength", + TestPropertyKeyLatin1AutoLength), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf8", TestPropertyKeyUtf8), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf8AutoLength", + TestPropertyKeyUtf8AutoLength), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf16", TestPropertyKeyUtf16), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf16AutoLength", + TestPropertyKeyUtf16AutoLength), + }; + + init_test_null(env, exports); + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_symbol/CMakeLists.txt new file mode 100644 index 00000000..7e937424 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_symbol + SOURCES + test_symbol.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/binding.gyp b/Tests/NodeApi/test/js-native-api/test_symbol/binding.gyp new file mode 100644 index 00000000..6a5a7cad --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_symbol", + "sources": [ + "test_symbol.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test1.js b/Tests/NodeApi/test/js-native-api/test_symbol/test1.js new file mode 100644 index 00000000..3a28437a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test1.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for symbol +const test_symbol = require(`./build/${common.buildType}/test_symbol`); + +const sym = test_symbol.New('test'); +assert.strictEqual(sym.toString(), 'Symbol(test)'); + +const myObj = {}; +const fooSym = test_symbol.New('foo'); +const otherSym = test_symbol.New('bar'); +myObj.foo = 'bar'; +myObj[fooSym] = 'baz'; +myObj[otherSym] = 'bing'; +assert.strictEqual(myObj.foo, 'bar'); +assert.strictEqual(myObj[fooSym], 'baz'); +assert.strictEqual(myObj[otherSym], 'bing'); diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test2.js b/Tests/NodeApi/test/js-native-api/test_symbol/test2.js new file mode 100644 index 00000000..026f2c68 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test2.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for symbol +const test_symbol = require(`./build/${common.buildType}/test_symbol`); + +const fooSym = test_symbol.New('foo'); +assert.strictEqual(fooSym.toString(), 'Symbol(foo)'); + +const myObj = {}; +myObj.foo = 'bar'; +myObj[fooSym] = 'baz'; + +assert.deepStrictEqual(Object.keys(myObj), ['foo']); +assert.deepStrictEqual(Object.getOwnPropertyNames(myObj), ['foo']); +assert.deepStrictEqual(Object.getOwnPropertySymbols(myObj), [fooSym]); diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test3.js b/Tests/NodeApi/test/js-native-api/test_symbol/test3.js new file mode 100644 index 00000000..186c561e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test3.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for symbol +const test_symbol = require(`./build/${common.buildType}/test_symbol`); + +assert.notStrictEqual(test_symbol.New(), test_symbol.New()); +assert.notStrictEqual(test_symbol.New('foo'), test_symbol.New('foo')); +assert.notStrictEqual(test_symbol.New('foo'), test_symbol.New('bar')); + +const foo1 = test_symbol.New('foo'); +const foo2 = test_symbol.New('foo'); +const object = { + [foo1]: 1, + [foo2]: 2, +}; +assert.strictEqual(object[foo1], 1); +assert.strictEqual(object[foo2], 2); diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test_symbol.c b/Tests/NodeApi/test/js-native-api/test_symbol/test_symbol.c new file mode 100644 index 00000000..58fcb85a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test_symbol.c @@ -0,0 +1,38 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value description = NULL; + if (argc >= 1) { + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + NODE_API_ASSERT(env, valuetype == napi_string, + "Wrong type of arguments. Expects a string."); + + description = args[0]; + } + + napi_value symbol; + NODE_API_CALL(env, napi_create_symbol(env, description, &symbol)); + + return symbol; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("New", New), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_typedarray/CMakeLists.txt new file mode 100644 index 00000000..093e92d0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_typedarray + SOURCES + test_typedarray.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/binding.gyp b/Tests/NodeApi/test/js-native-api/test_typedarray/binding.gyp new file mode 100644 index 00000000..a5ae5741 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_typedarray", + "sources": [ + "test_typedarray.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/test.js b/Tests/NodeApi/test/js-native-api/test_typedarray/test.js new file mode 100644 index 00000000..673bb5ce --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/test.js @@ -0,0 +1,109 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for arrays +const test_typedarray = require(`./build/${common.buildType}/test_typedarray`); + +const byteArray = new Uint8Array(3); +byteArray[0] = 0; +byteArray[1] = 1; +byteArray[2] = 2; +assert.strictEqual(byteArray.length, 3); + +const doubleArray = new Float64Array(3); +doubleArray[0] = 0.0; +doubleArray[1] = 1.1; +doubleArray[2] = 2.2; +assert.strictEqual(doubleArray.length, 3); + +const byteResult = test_typedarray.Multiply(byteArray, 3); +assert.ok(byteResult instanceof Uint8Array); +assert.strictEqual(byteResult.length, 3); +assert.strictEqual(byteResult[0], 0); +assert.strictEqual(byteResult[1], 3); +assert.strictEqual(byteResult[2], 6); + +const doubleResult = test_typedarray.Multiply(doubleArray, -3); +assert.ok(doubleResult instanceof Float64Array); +assert.strictEqual(doubleResult.length, 3); +assert.strictEqual(doubleResult[0], -0); +assert.strictEqual(Math.round(10 * doubleResult[1]) / 10, -3.3); +assert.strictEqual(Math.round(10 * doubleResult[2]) / 10, -6.6); + +const externalResult = test_typedarray.External(); +assert.ok(externalResult instanceof Int8Array); +assert.strictEqual(externalResult.length, 3); +assert.strictEqual(externalResult[0], 0); +assert.strictEqual(externalResult[1], 1); +assert.strictEqual(externalResult[2], 2); + +// Validate creation of all kinds of TypedArrays +const buffer = new ArrayBuffer(128); +const arrayTypes = [ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, + Uint16Array, Int32Array, Uint32Array, Float32Array, + Float64Array, BigInt64Array, BigUint64Array ]; + +arrayTypes.forEach((currentType) => { + const template = Reflect.construct(currentType, buffer); + const theArray = test_typedarray.CreateTypedArray(template, buffer); + + assert.ok(theArray instanceof currentType, + 'Type of new array should match that of the template. ' + + `Expected type: ${currentType.name}, ` + + `actual type: ${template.constructor.name}`); + assert.notStrictEqual(theArray, template); + assert.strictEqual(theArray.buffer, buffer); +}); + +arrayTypes.forEach((currentType) => { + const template = Reflect.construct(currentType, buffer); + assert.throws(() => { + test_typedarray.CreateTypedArray(template, buffer, 0, 136); + }, RangeError); +}); + +const nonByteArrayTypes = [ Int16Array, Uint16Array, Int32Array, Uint32Array, + Float32Array, Float64Array, + BigInt64Array, BigUint64Array ]; +nonByteArrayTypes.forEach((currentType) => { + const template = Reflect.construct(currentType, buffer); + assert.throws(() => { + test_typedarray.CreateTypedArray(template, buffer, + currentType.BYTES_PER_ELEMENT + 1, 1); + console.log(`start of offset ${currentType}`); + }, RangeError); +}); + +// Test detaching +arrayTypes.forEach((currentType) => { + const buffer = Reflect.construct(currentType, [8]); + assert.strictEqual(buffer.length, 8); + assert.ok(!test_typedarray.IsDetached(buffer.buffer)); + test_typedarray.Detach(buffer); + assert.ok(test_typedarray.IsDetached(buffer.buffer)); + assert.strictEqual(buffer.length, 0); +}); +{ + const buffer = test_typedarray.External(); + assert.ok(externalResult instanceof Int8Array); + assert.strictEqual(externalResult.length, 3); + assert.strictEqual(externalResult.byteLength, 3); + assert.ok(!test_typedarray.IsDetached(buffer.buffer)); + test_typedarray.Detach(buffer); + assert.ok(test_typedarray.IsDetached(buffer.buffer)); + assert.ok(externalResult instanceof Int8Array); + assert.strictEqual(buffer.length, 0); + assert.strictEqual(buffer.byteLength, 0); +} + +{ + const buffer = new ArrayBuffer(128); + assert.ok(!test_typedarray.IsDetached(buffer)); +} + +{ + const buffer = test_typedarray.NullArrayBuffer(); + assert.ok(buffer instanceof ArrayBuffer); + assert.ok(test_typedarray.IsDetached(buffer)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/test_typedarray.c b/Tests/NodeApi/test/js-native-api/test_typedarray/test_typedarray.c new file mode 100644 index 00000000..8aac9b52 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/test_typedarray.c @@ -0,0 +1,249 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value Multiply(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects a typed array as first argument."); + + napi_value input_array = args[0]; + bool is_typedarray; + NODE_API_CALL(env, napi_is_typedarray(env, input_array, &is_typedarray)); + + NODE_API_ASSERT(env, is_typedarray, + "Wrong type of arguments. Expects a typed array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects a number as second argument."); + + double multiplier; + NODE_API_CALL(env, napi_get_value_double(env, args[1], &multiplier)); + + napi_typedarray_type type; + napi_value input_buffer; + size_t byte_offset; + size_t i, length; + NODE_API_CALL(env, napi_get_typedarray_info( + env, input_array, &type, &length, NULL, &input_buffer, &byte_offset)); + + void* data; + size_t byte_length; + NODE_API_CALL(env, napi_get_arraybuffer_info( + env, input_buffer, &data, &byte_length)); + + napi_value output_buffer; + void* output_ptr = NULL; + NODE_API_CALL(env, napi_create_arraybuffer( + env, byte_length, &output_ptr, &output_buffer)); + + napi_value output_array; + NODE_API_CALL(env, napi_create_typedarray( + env, type, length, output_buffer, byte_offset, &output_array)); + + if (type == napi_uint8_array) { + uint8_t* input_bytes = (uint8_t*)(data) + byte_offset; + uint8_t* output_bytes = (uint8_t*)(output_ptr); + for (i = 0; i < length; i++) { + output_bytes[i] = (uint8_t)(input_bytes[i] * multiplier); + } + } else if (type == napi_float64_array) { + double* input_doubles = (double*)((uint8_t*)(data) + byte_offset); + double* output_doubles = (double*)(output_ptr); + for (i = 0; i < length; i++) { + output_doubles[i] = input_doubles[i] * multiplier; + } + } else { + napi_throw_error(env, NULL, + "Typed array was of a type not expected by test."); + return NULL; + } + + return output_array; +} + +static void FinalizeCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) +{ + free(finalize_data); +} + +static napi_value External(napi_env env, napi_callback_info info) { + const uint8_t nElem = 3; + int8_t* externalData = malloc(nElem*sizeof(int8_t)); + externalData[0] = 0; + externalData[1] = 1; + externalData[2] = 2; + + napi_value output_buffer; + NODE_API_CALL(env, napi_create_external_arraybuffer( + env, + externalData, + nElem*sizeof(int8_t), + FinalizeCallback, + NULL, // finalize_hint + &output_buffer)); + + napi_value output_array; + NODE_API_CALL(env, napi_create_typedarray(env, + napi_int8_array, + nElem, + output_buffer, + 0, + &output_array)); + + return output_array; +} + + +static napi_value NullArrayBuffer(napi_env env, napi_callback_info info) { + static void* data = NULL; + napi_value arraybuffer; + NODE_API_CALL(env, + napi_create_external_arraybuffer(env, data, 0, NULL, NULL, &arraybuffer)); + return arraybuffer; +} + +static napi_value CreateTypedArray(napi_env env, napi_callback_info info) { + size_t argc = 4; + napi_value args[4]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2 || argc == 4, "Wrong number of arguments"); + + napi_value input_array = args[0]; + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, input_array, &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects a typed array as first argument."); + + bool is_typedarray; + NODE_API_CALL(env, napi_is_typedarray(env, input_array, &is_typedarray)); + + NODE_API_ASSERT(env, is_typedarray, + "Wrong type of arguments. Expects a typed array as first argument."); + + napi_valuetype valuetype1; + napi_value input_buffer = args[1]; + NODE_API_CALL(env, napi_typeof(env, input_buffer, &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_object, + "Wrong type of arguments. Expects an array buffer as second argument."); + + bool is_arraybuffer; + NODE_API_CALL(env, napi_is_arraybuffer(env, input_buffer, &is_arraybuffer)); + + NODE_API_ASSERT(env, is_arraybuffer, + "Wrong type of arguments. Expects an array buffer as second argument."); + + napi_typedarray_type type; + napi_value in_array_buffer; + size_t byte_offset; + size_t length; + NODE_API_CALL(env, napi_get_typedarray_info( + env, input_array, &type, &length, NULL, &in_array_buffer, &byte_offset)); + + if (argc == 4) { + napi_valuetype valuetype2; + NODE_API_CALL(env, napi_typeof(env, args[2], &valuetype2)); + + NODE_API_ASSERT(env, valuetype2 == napi_number, + "Wrong type of arguments. Expects a number as third argument."); + + uint32_t uint32_length; + NODE_API_CALL(env, napi_get_value_uint32(env, args[2], &uint32_length)); + length = uint32_length; + + napi_valuetype valuetype3; + NODE_API_CALL(env, napi_typeof(env, args[3], &valuetype3)); + + NODE_API_ASSERT(env, valuetype3 == napi_number, + "Wrong type of arguments. Expects a number as third argument."); + + uint32_t uint32_byte_offset; + NODE_API_CALL(env, napi_get_value_uint32(env, args[3], &uint32_byte_offset)); + byte_offset = uint32_byte_offset; + } + + napi_value output_array; + NODE_API_CALL(env, napi_create_typedarray( + env, type, length, input_buffer, byte_offset, &output_array)); + + return output_array; +} + +static napi_value Detach(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments."); + + bool is_typedarray; + NODE_API_CALL(env, napi_is_typedarray(env, args[0], &is_typedarray)); + NODE_API_ASSERT( + env, is_typedarray, + "Wrong type of arguments. Expects a typedarray as first argument."); + + napi_value arraybuffer; + NODE_API_CALL(env, + napi_get_typedarray_info( + env, args[0], NULL, NULL, NULL, &arraybuffer, NULL)); + NODE_API_CALL(env, napi_detach_arraybuffer(env, arraybuffer)); + + return NULL; +} + +static napi_value IsDetached(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments."); + + napi_value array_buffer = args[0]; + bool is_arraybuffer; + NODE_API_CALL(env, napi_is_arraybuffer(env, array_buffer, &is_arraybuffer)); + NODE_API_ASSERT(env, is_arraybuffer, + "Wrong type of arguments. Expects an array buffer as first argument."); + + bool is_detached; + NODE_API_CALL(env, + napi_is_detached_arraybuffer(env, array_buffer, &is_detached)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, is_detached, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("Multiply", Multiply), + DECLARE_NODE_API_PROPERTY("External", External), + DECLARE_NODE_API_PROPERTY("NullArrayBuffer", NullArrayBuffer), + DECLARE_NODE_API_PROPERTY("CreateTypedArray", CreateTypedArray), + DECLARE_NODE_API_PROPERTY("Detach", Detach), + DECLARE_NODE_API_PROPERTY("IsDetached", IsDetached), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/package-lock.json b/Tests/NodeApi/test/package-lock.json new file mode 100644 index 00000000..17619de8 --- /dev/null +++ b/Tests/NodeApi/test/package-lock.json @@ -0,0 +1,2467 @@ +{ + "name": "hermes-node-api-test-packages", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hermes-node-api-test-packages", + "hasInstallScript": true, + "devDependencies": { + "@babel/cli": "^7.28.0", + "@babel/core": "^7.28.0", + "@babel/runtime": "^7.28.2", + "@react-native/babel-preset": "^0.80.2" + } + }, + "node_modules/@babel/cli": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.29.7.tgz", + "integrity": "sha512-/75HwRbAYPqXv/Ax1h7Fg3IZfXgdU98jnA8H93/m/QBaPV3Hp5ICoLqzGYye1yHBCgpmXvtqgSUN8oOKX5tojQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.28", + "commander": "^6.2.0", + "convert-source-map": "^2.0.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.6.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.29.7.tgz", + "integrity": "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.29.7.tgz", + "integrity": "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.80.3.tgz", + "integrity": "sha512-Ys4lC5DoobBWYDPwOZmyNFudddRgMaYMnt3bSxK++Z+mOuIxGxC8ue3Iupy09x0KEEAejpkaQYBoVMpO5YwUKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.80.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.80.3.tgz", + "integrity": "sha512-ROSWmuabB+8vho57Lrc/nFhU82Sd9/I5RI1OslE+JCnOUMdt6kz2JKd6hA7GV3UsJbkbYRXOF9KfjzX804JweQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.80.3", + "babel-plugin-syntax-hermes-parser": "0.28.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.80.3.tgz", + "integrity": "sha512-dHwjy54tNUsL/2My1LXwKmr02SNiwy9Zo48LE3OFfTY6LdIl6jMTB0ejzQTAyOzLHyEoam756U3kvNWPB1Kp0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.28.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.28.1.tgz", + "integrity": "sha512-meT17DOuUElMNsL5LZN56d+KBp22hb0EfxWfuPUeoSi54e40v1W4C2V36P75FpsH9fVEfDKpw5Nnkahc8haSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-parser": "0.28.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz", + "integrity": "sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.28.1.tgz", + "integrity": "sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.28.1.tgz", + "integrity": "sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.28.1" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/Tests/NodeApi/test/package.json b/Tests/NodeApi/test/package.json new file mode 100644 index 00000000..a0703806 --- /dev/null +++ b/Tests/NodeApi/test/package.json @@ -0,0 +1,13 @@ +{ + "name": "hermes-node-api-test-packages", + "private": true, + "scripts": { + "postinstall": "node postinstall.mjs" + }, + "devDependencies": { + "@babel/cli": "^7.28.0", + "@babel/core": "^7.28.0", + "@babel/runtime": "^7.28.2", + "@react-native/babel-preset": "^0.80.2" + } +} diff --git a/Tests/NodeApi/test/postinstall.mjs b/Tests/NodeApi/test/postinstall.mjs new file mode 100644 index 00000000..a31b2a98 --- /dev/null +++ b/Tests/NodeApi/test/postinstall.mjs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// CMake needs a deterministic output file for the npm install edge. Hashing +// the lockfile is sufficient: `npm ci` guarantees node_modules exactly matches +// it, and unlike the former background tar pipeline this cannot race with npm. +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; + +const directory = new URL(".", import.meta.url); +const lockfile = new URL("package-lock.json", directory); +const hash = createHash("sha256").update(readFileSync(lockfile)).digest("hex"); +writeFileSync(new URL("node_modules.sha256", directory), `${hash}\n`); diff --git a/Tests/NodeApi/test_basics.cpp b/Tests/NodeApi/test_basics.cpp new file mode 100644 index 00000000..42aefba1 --- /dev/null +++ b/Tests/NodeApi/test_basics.cpp @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +#include +#include +#include "child_process.h" +#include "test_main.h" + +namespace fs = std::filesystem; + +namespace node_api_tests { + +class BasicsTest : public TestFixtureBase { + protected: + void SetUp() override { + const auto& config = Config(); + ASSERT_FALSE(config.js_root.empty()) + << "Node-API test root directory is not configured."; + basics_js_dir_ = config.js_root / "basics"; + } + + ProcessResult RunScript(std::string_view script_filename) noexcept { + const auto& config = Config(); + if (config.run_script) { + return config.run_script(basics_js_dir_ / fs::path{script_filename}); + } + + ProcessResult fallback{}; + fallback.status = 1; + fallback.std_error = "Node-API test runner is not configured."; + return fallback; + } + + bool StringContains(std::string_view str, std::string_view substr) { + return str.find(substr) != std::string::npos; + } + + private: + fs::path basics_js_dir_; +}; + +TEST_F(BasicsTest, TestHello) { + ProcessResult result = RunScript("hello.js"); + ASSERT_TRUE(StringContains(result.std_output, "Hello")); +} + +TEST_F(BasicsTest, TestThrowString) { + ProcessResult result = RunScript("throw_string.js"); + ASSERT_TRUE(StringContains(result.std_error, "Script failed")); +} + +TEST_F(BasicsTest, TestLargeOutputDoesNotDeadlock) { + ProcessResult result = RunScript("large_output.js"); + ASSERT_EQ(result.status, 0); + ASSERT_TRUE(StringContains(result.std_output, "stdout-end")); + ASSERT_TRUE(StringContains(result.std_error, "stderr-end")); +} + +TEST_F(BasicsTest, TestAsyncResolved) { + ProcessResult result = RunScript("async_resolved.js"); + ASSERT_TRUE(StringContains(result.std_output, "test async calling")); + ASSERT_TRUE( + StringContains(result.std_output, "Expected: test async resolved")); +} + +TEST_F(BasicsTest, TestAsyncRejected) { + ProcessResult result = RunScript("async_rejected.js"); + ASSERT_TRUE(StringContains(result.std_output, "test async calling")); + ASSERT_TRUE( + StringContains(result.std_error, "Expected: test async rejected")); +} + +TEST_F(BasicsTest, TestMustCallSuccess) { + ProcessResult result = RunScript("mustcall_success.js"); + ASSERT_TRUE(result.status == 0); +} + +TEST_F(BasicsTest, TestMustCallFailure) { + ProcessResult result = RunScript("mustcall_failure.js"); + ASSERT_TRUE(result.status != 0); + ASSERT_TRUE( + StringContains(result.std_error, "Mismatched noop function calls")); +} + +TEST_F(BasicsTest, TestMustNotCallSuccess) { + ProcessResult result = RunScript("mustnotcall_success.js"); + ASSERT_TRUE(result.status == 0); +} + +TEST_F(BasicsTest, TestMustNotCallFailure) { + ProcessResult result = RunScript("mustnotcall_failure.js"); + ASSERT_TRUE(result.status != 0); + ASSERT_TRUE( + StringContains(result.std_error, "Function should not have been called")); +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/test_main.cpp b/Tests/NodeApi/test_main.cpp new file mode 100644 index 00000000..d7edb1a1 --- /dev/null +++ b/Tests/NodeApi/test_main.cpp @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "test_main.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include "string_utils.h" + +namespace fs = std::filesystem; + +namespace node_api_tests { + +namespace { + +NodeApiTestConfig g_test_config{}; +bool g_test_config_initialized = false; + +const NodeApiTestConfig& RequireConfig() noexcept { + if (!g_test_config_initialized) { + std::cerr << "[NodeApiTests] configuration not initialized." << std::endl; + std::abort(); + } + return g_test_config; +} + +std::string SanitizeName(const std::string& name) { + return ReplaceAll(ReplaceAll(name, "-", "_"), ".", "_"); +} + +} // namespace + +void InitializeNodeApiTests(const NodeApiTestConfig& config) noexcept { + g_test_config = config; + g_test_config_initialized = true; +} + +const NodeApiTestConfig& GetNodeApiTestConfig() noexcept { + return RequireConfig(); +} + +const NodeApiTestConfig& TestFixtureBase::Config() noexcept { + return RequireConfig(); +} + +class NodeApiTestFixture : public TestFixtureBase { + public: + explicit NodeApiTestFixture(fs::path jsFilePath) + : m_jsFilePath(std::move(jsFilePath)) {} + + void TestBody() override { + const auto& config = Config(); + ASSERT_TRUE(static_cast(config.run_script)) + << "Node-API test runner is not configured."; + + ProcessResult result = config.run_script(m_jsFilePath); + if (result.status == 0) { + return; + } + + if (!result.std_error.empty()) { + std::stringstream errorStream(result.std_error); + std::vector errorLines; + std::string line; + while (std::getline(errorStream, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + errorLines.push_back(line); + } + if (errorLines.size() >= 3) { + std::string file = errorLines[0].rfind("file:", 0) == 0 + ? errorLines[0].substr(5) + : ""; + int lineNumber = errorLines[1].rfind("line:", 0) == 0 + ? std::stoi(errorLines[1].substr(5)) + : 0; + std::string message = errorLines[2]; + std::stringstream details; + for (size_t i = 3; i < errorLines.size(); ++i) { + details << errorLines[i] << std::endl; + } + GTEST_MESSAGE_AT_(file.c_str(), + lineNumber, + message.c_str(), + ::testing::TestPartResult::kFatalFailure) + << details.str(); + return; + } + } + + ADD_FAILURE() << "node_lite exited with status " << result.status + << "\nstdout:\n" << result.std_output + << "\nstderr:\n" << result.std_error; + } + + static void Register() { + const auto& config = Config(); + const fs::path& js_root = config.js_root; + if (js_root.empty()) { + std::cerr << "[NodeApiTests] JS root directory not configured." << std::endl; + std::abort(); + } + + for (const fs::directory_entry& dir_entry : + fs::recursive_directory_iterator(js_root)) { + if (!dir_entry.is_regular_file() || + dir_entry.path().extension() != ".js") { + continue; + } + + fs::path jsFilePath = dir_entry.path(); + fs::path suitePath = jsFilePath.parent_path().parent_path(); + std::string suiteFolder = suitePath.filename().string(); + + bool includeTest = false; + if (suiteFolder == "basics") { + includeTest = true; + } else if (suiteFolder == "js-native-api") { + if (config.enabled_native_suites.empty()) { + continue; + } + std::string moduleName = jsFilePath.parent_path().filename().string(); + includeTest = + config.enabled_native_suites.find(moduleName) != + config.enabled_native_suites.end(); + } else { + continue; + } + + if (!includeTest) { + continue; + } + + std::string testSuiteName = SanitizeName(suiteFolder); + std::string testName = SanitizeName( + jsFilePath.parent_path().filename().string() + "_" + + jsFilePath.filename().string()); + + ::testing::RegisterTest( + testSuiteName.c_str(), + testName.c_str(), + nullptr, + nullptr, + jsFilePath.string().c_str(), + 1, + [jsFilePath]() { return new NodeApiTestFixture(jsFilePath); }); + } + } + + private: + fs::path m_jsFilePath; +}; + +void RegisterNodeApiTests() { + NodeApiTestFixture::Register(); +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/test_main.h b/Tests/NodeApi/test_main.h new file mode 100644 index 00000000..a4fe4532 --- /dev/null +++ b/Tests/NodeApi/test_main.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef NODE_API_TEST_TEST_MAIN_H +#define NODE_API_TEST_TEST_MAIN_H + +#include +#include +#include +#include + +#include "child_process.h" + +namespace node_api_tests { + +struct NodeApiTestConfig { + std::filesystem::path js_root; + std::function run_script; + std::unordered_set enabled_native_suites; +}; + +void InitializeNodeApiTests(const NodeApiTestConfig& config) noexcept; +const NodeApiTestConfig& GetNodeApiTestConfig() noexcept; +void RegisterNodeApiTests(); + +class TestFixtureBase : public ::testing::Test { + protected: + static const NodeApiTestConfig& Config() noexcept; +}; + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_TEST_MAIN_H diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index c6137034..0546fc24 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -7,6 +7,8 @@ if (project.hasProperty("jsEngine")) { jsEngine = project.property("jsEngine") } +def nodeApiAssetsDir = "${project.buildDir}/generated/nodeapiassets" +def enableAsan = project.hasProperty("enableAsan") def cmakeArguments = [ "-DANDROID_STL=c++_shared", "-DNAPI_JAVASCRIPT_ENGINE=${jsEngine}", @@ -21,11 +23,14 @@ def cmakeArguments = [ if (project.hasProperty("importHostCompilers")) { cmakeArguments.add("-DIMPORT_HOST_COMPILERS=${project.property('importHostCompilers')}") } +if (enableAsan) { + cmakeArguments.add("-DENABLE_SANITIZERS=ON") +} android { namespace 'com.jsruntimehost.unittests' compileSdk 33 - ndkVersion = "23.1.7779620" + ndkVersion = "28.2.13676358" if (project.hasProperty("ndkVersion")) { ndkVersion = project.property("ndkVersion") } @@ -72,6 +77,22 @@ android { buildFeatures { viewBinding true } + + packagingOptions { + if (enableAsan) { + doNotStrip "**/*.so" + jniLibs.useLegacyPackaging true + } + } + + sourceSets { + main { + assets.srcDirs += [nodeApiAssetsDir] + if (enableAsan) { + jniLibs.srcDir "${buildDir}/generated/asanRuntime/jniLibs" + } + } + } } dependencies { @@ -108,6 +129,27 @@ task copyScripts { } } +task copyNodeApiTests(type: Copy) { + from '../../../NodeApi/test' + into "${nodeApiAssetsDir}/NodeApi/test" + // Always re-run so the manifest is regenerated even when the copied sources are unchanged. + outputs.upToDateWhen { false } + doLast { + // AAssetManager cannot enumerate subdirectories at runtime, so emit a manifest listing + // every copied file (one path relative to NodeApi/test per line). Consumed by Shared.cpp. + // Must NOT start with a dot -- aapt ignores dotfiles when packaging assets. + def testRoot = file("${nodeApiAssetsDir}/NodeApi/test") + def manifestFile = new File(testRoot, 'manifest.txt') + manifestFile.withWriter('UTF-8') { writer -> + testRoot.eachFileRecurse(groovy.io.FileType.FILES) { f -> + if (f != manifestFile) { + writer.writeLine(testRoot.toPath().relativize(f.toPath()).toString().replace(File.separator, '/')) + } + } + } + } +} + // Run copyScripts task after CMake external build // And make sure merging assets into output is performed after the scripts copy tasks.configureEach { task -> @@ -116,5 +158,77 @@ tasks.configureEach { task -> } if (task.name == 'mergeDebugAssets') { task.dependsOn(copyScripts) + task.dependsOn(copyNodeApiTests) + } + if (task.name == 'mergeReleaseAssets') { + task.dependsOn(copyScripts) + task.dependsOn(copyNodeApiTests) + } +} + +preBuild.dependsOn(copyNodeApiTests) + +if (enableAsan) { + def hostTag = { + def osName = System.getProperty("os.name").toLowerCase() + if (osName.contains("mac") || osName.contains("darwin")) { + return "darwin-x86_64" + } else if (osName.contains("windows")) { + return "windows-x86_64" + } else { + return "linux-x86_64" + } + }.call() + + def asanRuntimeProvider = providers.provider { + def prebuiltRoot = new File(android.ndkDirectory, "toolchains/llvm/prebuilt/${hostTag}/lib64/clang") + if (!prebuiltRoot.exists()) { + throw new GradleException("Unable to locate clang libraries under ${prebuiltRoot}") + } + def versionDir = prebuiltRoot.listFiles().find { it.isDirectory() } + if (versionDir == null) { + throw new GradleException("Unable to determine clang version directory within ${prebuiltRoot}") + } + def runtimeFile = new File(versionDir, "lib/linux/libclang_rt.asan-aarch64-android.so") + if (!runtimeFile.exists()) { + throw new GradleException("Unable to locate ASan runtime library at ${runtimeFile}") + } + return runtimeFile + } + + def generatedAsanDir = layout.buildDirectory.dir("generated/asanRuntime/jniLibs/arm64-v8a") + + def prepareAsanRuntime = tasks.register("prepareAsanRuntime", Copy) { + from(asanRuntimeProvider) + into(generatedAsanDir) + } + + tasks.matching { it.name in ["mergeDebugNativeLibs", "mergeReleaseNativeLibs"] }.configureEach { + dependsOn(prepareAsanRuntime) + } + + def wrapScript = file("${projectDir}/../tools/wrap.sh") + + def pushAsanRuntime = tasks.register("pushAsanRuntime", Exec) { + dependsOn(prepareAsanRuntime) + commandLine("adb", "push", + generatedAsanDir.get().file("libclang_rt.asan-aarch64-android.so").asFile.absolutePath, + "/data/local/tmp/libclang_rt.asan-aarch64-android.so") + } + + def pushAsanWrapScript = tasks.register("pushAsanWrapScript", Exec) { + commandLine("adb", "push", wrapScript.absolutePath, "/data/local/tmp/wrap.sh") + } + + tasks.register("pushAsanArtifacts") { + dependsOn(pushAsanRuntime, pushAsanWrapScript) + doLast { + exec { + commandLine("adb", "shell", "chcon", "u:object_r:zygote_exec:s0", "/data/local/tmp/wrap.sh") + } + exec { + commandLine("adb", "shell", "chmod", "+x", "/data/local/tmp/wrap.sh") + } + } } } diff --git a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java index 23c22307..069c698d 100644 --- a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java +++ b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java @@ -23,6 +23,7 @@ public void javaScriptTests() { Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.jsruntimehost.unittests", appContext.getPackageName()); - assertEquals(0, Native.javaScriptTests(appContext)); + Context applicationContext = appContext.getApplicationContext(); + assertEquals(0, Native.javaScriptTests(applicationContext)); } -} \ No newline at end of file +} diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 0af5caa8..6800708e 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -16,16 +16,42 @@ FetchContent_MakeAvailable_With_Message(googletest) npm(install --silent WORKING_DIRECTORY ${TESTS_DIR}) +if(WIN32) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_windows.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process.cpp) +elseif(APPLE) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_posix.cpp) +elseif(ANDROID) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_android.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_android.cpp) +else() + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_posix.cpp) +endif() + add_library(UnitTestsJNI SHARED JNI.cpp ${UNIT_TESTS_DIR}/Shared/Shared.h - ${UNIT_TESTS_DIR}/Shared/Shared.cpp) + ${UNIT_TESTS_DIR}/Shared/Shared.cpp + ${TESTS_DIR}/NodeApi/node_lite.cpp + ${NODE_LITE_PLATFORM_SRC} + ${NODE_LITE_CHILD_PROCESS_SRC} + ${TESTS_DIR}/NodeApi/node_lite_jsruntimehost.cpp + ${TESTS_DIR}/NodeApi/js_runtime_api.cpp + ${TESTS_DIR}/NodeApi/string_utils.cpp + ${TESTS_DIR}/NodeApi/test_main.cpp) -target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") -target_compile_definitions(UnitTestsJNI PRIVATE ARCANA_TEST_HOOKS) +target_compile_definitions(UnitTestsJNI + PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}" + PRIVATE ARCANA_TEST_HOOKS + PRIVATE NODE_API_BUILD_TYPE="$,Debug,Release>") target_include_directories(UnitTestsJNI - PRIVATE ${UNIT_TESTS_DIR}) + PRIVATE ${UNIT_TESTS_DIR} + PRIVATE ${TESTS_DIR}/NodeApi + PRIVATE ${TESTS_DIR}/NodeApi/include + PRIVATE ${REPO_ROOT_DIR}/Core/Node-API/Source) target_link_libraries(UnitTestsJNI PRIVATE log @@ -45,4 +71,27 @@ target_link_libraries(UnitTestsJNI PRIVATE File PRIVATE TextDecoder PRIVATE TextEncoder - PRIVATE Performance) + PRIVATE Performance + PRIVATE IndexedDB + PRIVATE napi) + +if(ANDROID) + target_link_libraries(UnitTestsJNI PRIVATE dl) +endif() +# Keep the in-process Android list aligned with JSR_NODE_API_NATIVE_TEST_DIRS +# in Tests/NodeApi/CMakeLists.txt. +set(_jsr_node_api_tests + "2_function_arguments,3_callbacks,4_object_factory,5_function_factory,test_reference_double_free,test_instance_data") +# Frozen jsc-android has no BigInt (its parser rejects `0n`); the maintained +# V8, QuickJS, and Hermes adapters run the real conformance addon. +if(NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore") + string(APPEND _jsr_node_api_tests ",test_bigint_unsupported") +else() + string(APPEND _jsr_node_api_tests ",test_bigint") +endif() +target_compile_definitions(UnitTestsJNI + PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="${_jsr_node_api_tests}") + +# The conformance addons are built as standalone SHARED lib.so by Tests/NodeApi/CMakeLists.txt, +# packaged by AGP into nativeLibraryDir, and dlopen'd in-process by node_lite_android. Both they and +# this host link the shared napi (libnapi.so), so the addons' napi_* imports resolve at load. diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index 4415ce87..13a87e41 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "Babylon/DebugTrace.h" #include @@ -17,9 +18,55 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz jclass webSocketClass{env->FindClass("com/jsruntimehost/unittests/WebSocket")}; java::websocket::WebSocketClient::InitializeJavaWebSocketClass(webSocketClass, env); + // Route stdout (the in-process gtest output -- [RUN]/[OK]/[FAILED] + failure file:line:message) + // to logcat so test results are visible; stopped after RunTests below. android::StdoutLogger::Start(); - android::global::Initialize(javaVM, context); + jclass contextClass = env->GetObjectClass(context); + jmethodID getApplicationContext = env->GetMethodID(contextClass, "getApplicationContext", "()Landroid/content/Context;"); + jobject applicationContext = env->CallObjectMethod(context, getApplicationContext); + env->DeleteLocalRef(contextClass); + + jclass appContextClass = env->GetObjectClass(applicationContext); + jmethodID getAssets = env->GetMethodID(appContextClass, "getAssets", "()Landroid/content/res/AssetManager;"); + jobject assetManagerObj = env->CallObjectMethod(applicationContext, getAssets); + env->DeleteLocalRef(appContextClass); + + android::global::Initialize(javaVM, applicationContext, assetManagerObj); + +#if defined(NODE_API_AVAILABLE_NATIVE_TESTS) + // Wire the in-process Node-API test harness to a native AssetManager and a writable base dir + // derived from the (still-valid) instrumentation Context, so it does not fall back to + // android::global::GetAppContext() during the run -- that global ref is not valid here and + // dereferencing it aborts with "use of deleted global reference". + if (assetManagerObj != nullptr) + { + AAssetManager* nativeAssetManager = AAssetManager_fromJava(env, assetManagerObj); + + jclass ctxClass = env->GetObjectClass(context); + jmethodID getFilesDir = env->GetMethodID(ctxClass, "getFilesDir", "()Ljava/io/File;"); + jobject filesDir = env->CallObjectMethod(context, getFilesDir); + jclass fileClass = env->GetObjectClass(filesDir); + jmethodID getAbsolutePath = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;"); + auto pathString = static_cast(env->CallObjectMethod(filesDir, getAbsolutePath)); + const char* rawPath = env->GetStringUTFChars(pathString, nullptr); + std::filesystem::path baseDir = std::filesystem::path{rawPath} / "node_api_tests"; + env->ReleaseStringUTFChars(pathString, rawPath); + env->DeleteLocalRef(pathString); + env->DeleteLocalRef(fileClass); + env->DeleteLocalRef(filesDir); + env->DeleteLocalRef(ctxClass); + + SetNodeApiTestEnvironment(nativeAssetManager, baseDir); + } +#endif + + if (assetManagerObj != nullptr) + { + env->DeleteLocalRef(assetManagerObj); + } + + env->DeleteLocalRef(applicationContext); Babylon::DebugTrace::EnableDebugTrace(true); Babylon::DebugTrace::SetTraceOutput([](const char* trace) { printf("%s\n", trace); fflush(stdout); }); diff --git a/Tests/UnitTests/Android/gradle.properties b/Tests/UnitTests/Android/gradle.properties index 25ceb3e4..3ec776e3 100644 --- a/Tests/UnitTests/Android/gradle.properties +++ b/Tests/UnitTests/Android/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/Tests/UnitTests/Android/tools/wrap.sh b/Tests/UnitTests/Android/tools/wrap.sh new file mode 100644 index 00000000..cac9a91a --- /dev/null +++ b/Tests/UnitTests/Android/tools/wrap.sh @@ -0,0 +1,16 @@ +#!/system/bin/sh + +# Ensure ASan runtime is available; prefer copy in /data/local/tmp if present. +ASAN_RT_BASENAME=${ASAN_RT_BASENAME:-libclang_rt.asan-aarch64-android.so} +ASAN_RT_LOCAL="/data/local/tmp/${ASAN_RT_BASENAME}" + +if [ -f "${ASAN_RT_LOCAL}" ]; then + ASAN_RT_PATH="${ASAN_RT_LOCAL}" +else + ASAN_RT_PATH="${ASAN_RT_BASENAME}" +fi + +export ASAN_OPTIONS=${ASAN_OPTIONS:-log_to_syslog=1:allow_user_segv_handler=1:disable_core=1:abort_on_error=0} +export LD_PRELOAD="${ASAN_RT_PATH}" + +exec "$@" diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 2dbc7619..e02481e9 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -70,13 +70,34 @@ target_link_libraries(UnitTests PRIVATE Performance PRIVATE TextDecoder PRIVATE TextEncoder + PRIVATE Streams + PRIVATE Compression + PRIVATE IndexedDB ${ADDITIONAL_LIBRARIES}) +if(TARGET Worker) + target_link_libraries(UnitTests PRIVATE Worker) + target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_TEST_WORKER=1) +endif() + # See https://gitlab.kitware.com/cmake/cmake/-/issues/23543 # If we can set minimum required to 3.26+, then we can use the `copy -t` syntax instead. add_custom_command(TARGET UnitTests POST_BUILD COMMAND ${CMAKE_COMMAND} -E $>,copy,true> $ $ COMMAND_EXPAND_LISTS) +if(TARGET Worker) + add_custom_command(TARGET UnitTests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/WebPlatformTests" + "$/WebPlatformTests" + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/workers/support" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/WebPlatformTests/workers/support/visualization-worker-cache.json" + "$/workers/support/visualization-worker-cache.json" + COMMENT "Copying focused Worker conformance regressions") +endif() + if(APPLE) enable_objc_arc(UnitTests) endif() diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index b286edf8..2219bbe9 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -88,6 +88,12 @@ describe("AbortController", function () { expect((controller.signal as any).reason).to.equal(reason); }); + it("abort(reason) records a primitive reason", function () { + const controller = new AbortController(); + controller.abort("custom reason"); + expect((controller.signal as any).reason).to.equal("custom reason"); + }); + it("abort() with no reason defaults to an AbortError", function () { const controller = new AbortController(); controller.abort(); @@ -293,11 +299,321 @@ describe("XMLHTTPRequest", function () { }); }); +describe("Headers", function () { + // Focused ports from WPT fetch/api/headers. + it("normalizes names and values and combines repeated fields", function () { + const headers = new Headers([ + ["X-Test", " first\t"], + ["x-test", "second"], + ["X-Other", "\r\n value \n"] + ]); + expect(headers.get("X-TEST")).to.equal("first, second"); + expect(headers.get("x-other")).to.equal("value"); + expect(Array.from(headers.keys())).to.deep.equal(["x-other", "x-test"]); + }); + + it("validates sequence shape and HTTP ByteStrings", function () { + expect(() => new Headers(null as any)).to.throw(); + expect(() => new Headers([["missing-value"]] as any)).to.throw(); + expect(() => new Headers([["too", "many", "values"]] as any)).to.throw(); + expect(() => new Headers([["invalid name", "value"]])).to.throw(); + expect(() => new Headers([["valid", "a\0b"]])).to.throw(); + expect(() => new Headers([["invalidĀ", "value"]])).to.throw(); + }); + + it("preserves Set-Cookie fields while combining other duplicates", function () { + const headers = new Headers([ + ["set-cookie", "a=1"], + ["x-value", "first"], + ["Set-Cookie", "b=2"], + ["X-Value", "second"] + ]); + expect(headers.getSetCookie()).to.deep.equal(["a=1", "b=2"]); + expect(Array.from(headers)).to.deep.equal([ + ["set-cookie", "a=1"], + ["set-cookie", "b=2"], + ["x-value", "first, second"] + ]); + }); + + it("keeps iteration live when headers are changed", function () { + const headers = new Headers({ bar: "0", baz: "1", foo: "2" }); + const seen: string[] = []; + for (const [name] of headers) { + seen.push(name); + headers.delete("foo"); + } + expect(seen).to.deep.equal(["bar", "baz"]); + }); + + it("copies an initializer and honors a custom iterator", function () { + const source = new Headers({ ignored: "value" }); + source[Symbol.iterator] = function* () { + yield ["custom", "value"]; + }; + const copy = new Headers(source); + source.set("custom", "changed"); + expect(Array.from(copy)).to.deep.equal([["custom", "value"]]); + }); +}); + +describe("Response", function () { + // Focused ports from WPT fetch/api/response. + it("exposes browser defaults and validates response metadata", function () { + const response = new Response(); + expect(String(response)).to.equal("[object Response]"); + expect(response.status).to.equal(200); + expect(response.statusText).to.equal(""); + expect(response.ok).to.equal(true); + expect(response.type).to.equal("default"); + expect(response.url).to.equal(""); + expect(response.body).to.equal(null); + expect(response.headers).to.equal(response.headers); + + expect(() => new Response("", { status: 199 })).to.throw(RangeError); + expect(() => new Response("", { status: 600 })).to.throw(RangeError); + expect(() => new Response("", { statusText: "bad\ntext" })).to.throw(TypeError); + expect(() => new Response("body", { status: 204 })).to.throw(TypeError); + }); + + it("streams strings and consumes a body only once", async function () { + const response = new Response("streamed text"); + expect(response.body).to.be.instanceOf(ReadableStream); + expect(response.bodyUsed).to.equal(false); + expect(await response.text()).to.equal("streamed text"); + expect(response.bodyUsed).to.equal(true); + + let rejected = false; + try { + await response.arrayBuffer(); + } catch (error) { + rejected = error instanceof TypeError; + } + expect(rejected).to.equal(true); + }); + + // Ported from WPT fetch/api/response/response-consume-empty.any.js. + it("consumes a null body as empty without disturbing it", async function () { + const textResponse = new Response(); + expect(await textResponse.text()).to.equal(""); + expect(textResponse.bodyUsed).to.equal(false); + + const bufferResponse = new Response(); + expect((await bufferResponse.arrayBuffer()).byteLength).to.equal(0); + expect(bufferResponse.bodyUsed).to.equal(false); + + const bytesResponse = new Response(); + expect((await bytesResponse.bytes()).byteLength).to.equal(0); + expect(bytesResponse.bodyUsed).to.equal(false); + + const blobResponse = new Response(); + expect((await blobResponse.blob()).size).to.equal(0); + expect(blobResponse.bodyUsed).to.equal(false); + + const jsonResponse = new Response(); + let jsonRejected = false; + try { + await jsonResponse.json(); + } catch { + jsonRejected = true; + } + expect(jsonRejected).to.equal(true); + expect(jsonResponse.bodyUsed).to.equal(false); + }); + + // Byte streams reject zero-length enqueues. An empty BufferSource still + // represents a non-null body, but its stream must close without a chunk. + it("consumes an empty BufferSource without enqueueing an empty byte chunk", async function () { + const response = new Response(new Uint8Array(0)); + expect(response.body).to.be.instanceOf(ReadableStream); + expect(response.bodyUsed).to.equal(false); + expect(new Uint8Array(await response.arrayBuffer())).to.eql(new Uint8Array(0)); + expect(response.bodyUsed).to.equal(true); + }); + + it("snapshots mutable BufferSource input once", async function () { + const input = new Uint8Array([80, 65, 83, 83]); + const response = new Response(input); + input.fill(0); + expect(await response.text()).to.equal("PASS"); + }); + + // Adapted from WebKit LayoutTests/fetch/body-init.html. + it("stringifies integer and object bodies", async function () { + expect(await new Response(1 as any).text()).to.equal("1"); + expect(await new Response({} as any).text()).to.equal("[object Object]"); + }); + + it("sets inferred content types without replacing explicit headers", async function () { + const text = new Response("text"); + expect(text.headers.get("content-type")).to.equal("text/plain;charset=UTF-8"); + + const blob = new Response(new Blob(["blob"], { type: "application/example" })); + expect(blob.headers.get("content-type")).to.equal("application/example"); + + const explicit = new Response("text", { headers: { "content-type": "text/custom" } }); + expect(explicit.headers.get("content-type")).to.equal("text/custom"); + }); + + it("clones stream branches for independent consumption", async function () { + const response = new Response("clone body", { + headers: { "x-test": "value" }, + status: 201, + statusText: "Created" + }); + const clone = response.clone(); + expect(clone.status).to.equal(201); + expect(clone.headers.get("x-test")).to.equal("value"); + expect(await response.text()).to.equal("clone body"); + expect(await clone.text()).to.equal("clone body"); + }); + + function withoutPrivateDisturbedState(stream: ReadableStream): ReadableStream { + Object.defineProperty(stream, "_disturbed", { + configurable: true, + get() { return undefined; }, + set() {} + }); + return stream; + } + + it("tracks direct stream reads and rejects invalid body chunks", async function () { + const directStream = withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("body")); + controller.close(); + } + })); + const direct = new Response(directStream); + const reader = direct.body!.getReader(); + expect(direct.bodyUsed).to.equal(false); + await reader.read(); + expect(direct.bodyUsed).to.equal(true); + + const invalid = new Response(new ReadableStream({ + start(controller) { + controller.enqueue("not bytes"); + controller.close(); + } + }) as any); + let rejected = false; + try { + await invalid.bytes(); + } catch (error) { + rejected = error instanceof TypeError; + } + expect(rejected).to.equal(true); + }); + + it("rejects a host-shaped stream disturbed before Response construction", async function () { + const stream = withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1])); + controller.close(); + } + })); + const reader = stream.getReader(); + await reader.read(); + reader.releaseLock(); + + expect(() => new Response(stream)).to.throw(TypeError); + }); + + // Focused ports from WPT response-stream-disturbed-6.any.js and + // response-stream-disturbed-by-pipe.any.js. These must not depend on a + // private field supplied by one particular Streams implementation. + it("tracks cancellation and piping through standard stream methods", async function () { + const cancelled = new Response(withoutPrivateDisturbedState(new ReadableStream())); + const cancelledReader = cancelled.body!.getReader(); + expect(cancelled.bodyUsed).to.equal(false); + await cancelledReader.cancel(); + expect(cancelled.bodyUsed).to.equal(true); + + const piped = new Response(withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.close(); + } + }))); + const pipePromise = piped.body!.pipeTo(new WritableStream({}, { highWaterMark: 0 })); + expect(piped.bodyUsed).to.equal(true); + await pipePromise; + + const pipedThrough = new Response(withoutPrivateDisturbedState(new ReadableStream({ + start(controller) { + controller.close(); + } + }))); + const output = pipedThrough.body!.pipeThrough(new TransformStream()); + expect(pipedThrough.bodyUsed).to.equal(true); + await output.cancel(); + }); + + // Adapted from Firefox dom/fetch/tests/crashtests/1939295.html. The + // unresolved read must remain safe through runtime teardown. + it("does not crash while consuming an open empty stream", function () { + const pending = new Response(new ReadableStream()).text(); + expect(pending).to.be.instanceOf(Promise); + }); + + // Adapted from WebKit's imported many-empty-chunks-crash.html. + it("consumes many empty chunks without retaining growing byte buffers", async function () { + const response = new Response(new ReadableStream({ + start(controller) { + for (let index = 0; index < 40000; ++index) { + controller.enqueue(new Uint8Array()); + } + controller.close(); + } + })); + expect((await response.arrayBuffer()).byteLength).to.equal(0); + }); + + // Bounded adaptation of Chromium's call-extra-crash-is-disturbed.html. + // QuickJS terminates before a JS-defined getter can run after a real + // native stack overflow, so retain the deep-call regression portably. + it("reads bodyUsed from a deep call stack without recursion", function () { + const response = new Response(new ReadableStream()); + function readAtDepth(depth: number): boolean { + return depth === 0 ? response.bodyUsed : readAtDepth(depth - 1); + } + expect(readAtDepth(128)).to.equal(false); + }); + + it("provides error, redirect, and JSON factories", async function () { + const error = Response.error(); + expect(error.type).to.equal("error"); + expect(error.status).to.equal(0); + + const redirect = Response.redirect("https://example.com/path", 307); + expect(redirect.status).to.equal(307); + expect(redirect.headers.get("location")).to.equal("https://example.com/path"); + + const json = Response.json({ value: 42 }); + expect(json.headers.get("content-type")).to.equal("application/json"); + expect(await json.json()).to.deep.equal({ value: 42 }); + }); + + it("filters forbidden response Set-Cookie fields", function () { + const response = new Response(null, { + headers: { + "set-cookie": "secret=value", + "set-cookie2": "legacy=value" + } + }); + response.headers.append("Set-Cookie", "other=value"); + expect(response.headers.getSetCookie()).to.deep.equal([]); + expect(response.headers.has("set-cookie2")).to.equal(false); + }); +}); + describe("fetch", function () { this.timeout(30000); it("should resolve with ok=true and status=200 for a resource that exists", async function () { const response = await fetch("https://github.com/"); + expect(response).to.be.instanceOf(Response); + expect(response.headers).to.be.instanceOf(Headers); + expect(response.body).to.be.instanceOf(ReadableStream); expect(response.ok).to.equal(true); expect(response.status).to.equal(200); }); @@ -320,6 +636,71 @@ describe("fetch", function () { expect(await response.text()).to.equal("var symlink_target_js = true;"); }); + it("should resolve percent-encoded data URLs locally", async function () { + const url = "data:text/plain;charset=utf-8,hello%20native%20fetch%21"; + const response = await fetch(url); + expect(response.ok).to.equal(true); + expect(response.status).to.equal(200); + expect(response.url).to.equal(url); + expect(response.headers.get("content-type")).to.equal("text/plain;charset=utf-8"); + expect(await response.text()).to.equal("hello native fetch!"); + }); + + it("should decode base64 data URLs without using the network transport", async function () { + const response = await fetch("data:application/octet-stream;base64,AAEC/w=="); + const clone = response.clone(); + expect(new Uint8Array(await response.arrayBuffer())).to.eql(new Uint8Array([0, 1, 2, 255])); + const blob = await clone.blob(); + expect(blob.type).to.equal("application/octet-stream"); + expect(new Uint8Array(await blob.arrayBuffer())).to.eql(new Uint8Array([0, 1, 2, 255])); + }); + + // Adapted from WPT fetch/data-urls/processing.any.js and resources/data-urls.json. + const dataUrlCases: Array<[string, string, number[]]> = [ + ["data:,", "text/plain;charset=US-ASCII", []], + ["data:,%FF", "text/plain;charset=US-ASCII", [255]], + ["data:text/plain,X", "text/plain", [88]], + ["data:,X#fragment", "text/plain;charset=US-ASCII", [88]], + ["data:;BASe64,WA", "text/plain;charset=US-ASCII", [88]], + ["data: ;charset=x ; base64,W%20A", "text/plain;charset=x", [88]] + ]; + + for (const [url, expectedType, expectedBody] of dataUrlCases) { + it(`should process WPT data URL case ${JSON.stringify(url)}`, async function () { + const response = await fetch(url); + expect(response.headers.get("content-type")).to.equal(expectedType); + expect(Array.from(new Uint8Array(await response.arrayBuffer()))).to.eql(expectedBody); + }); + } + + // Adapted from WPT's forgiving-base64 vectors and Chromium's DataURL tests. + const base64Cases: Array<[string, number[]]> = [ + ["abcd", [105, 183, 29]], + ["ab%09%0A%0C%0D%20cd", [105, 183, 29]], + ["ab==", [105]], + ["/A", [252]], + ["YR", [97]] + ]; + + for (const [encoded, expectedBody] of base64Cases) { + it(`should forgiving-base64 decode ${JSON.stringify(encoded)}`, async function () { + const response = await fetch(`data:application/octet-stream;base64,${encoded}`); + expect(Array.from(new Uint8Array(await response.arrayBuffer()))).to.eql(expectedBody); + }); + } + + for (const encoded of ["a", "ab===", "ab%0Bcd", "=a", "a=b"]) { + it(`should reject invalid WPT base64 case ${JSON.stringify(encoded)}`, async function () { + let error: unknown; + try { + await fetch(`data:application/octet-stream;base64,${encoded}`); + } catch (caught) { + error = caught; + } + expect(error).to.be.instanceOf(TypeError); + }); + } + it("arrayBuffer() should return the body as bytes", async function () { const response = await fetch("app:///Scripts/symlink_target.js"); const expected = new Uint8Array("var symlink_target_js = true;".split("").map(x => x.charCodeAt(0))); @@ -451,6 +832,21 @@ describe("fetch", function () { expect(error, "fetch should have rejected").to.not.equal(undefined); expect(error.name).to.equal("AbortError"); }); + + it("should preserve a primitive abort reason when aborted in-flight", async function () { + this.timeout(30000); + const controller = new AbortController(); + const promise = fetch("https://github.com/", { signal: controller.signal } as any); + controller.abort("visualization-stop"); + + let error: any; + try { + await promise; + } catch (e) { + error = e; + } + expect(error).to.equal("visualization-stop"); + }); }); describe("setTimeout", function () { @@ -1371,9 +1767,453 @@ describe("Console", function () { }); }); +describe("Web Streams", function () { + // Focused ports from the WHATWG Streams WPT suites at c05b4473: + // readable-streams/general.any.js and tee.any.js, + // writable-streams/write.any.js, and transform-streams/general.any.js. + it("installs the standard stream constructors", function () { + expect(ReadableStream).to.be.a("function"); + expect(WritableStream).to.be.a("function"); + expect(TransformStream).to.be.a("function"); + expect(ByteLengthQueuingStrategy).to.be.a("function"); + expect(CountQueuingStrategy).to.be.a("function"); + }); + + it("delivers queued chunks in order and closes the reader", async function () { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue("a"); + controller.enqueue("b"); + controller.close(); + } + }); + const reader = stream.getReader(); + + expect(await reader.read()).to.deep.equal({ value: "a", done: false }); + expect(await reader.read()).to.deep.equal({ value: "b", done: false }); + expect(await reader.read()).to.deep.equal({ value: undefined, done: true }); + await reader.closed; + }); + + it("propagates a rejected pull to read and closed", async function () { + const failure = new Error("pull failed"); + const reader = new ReadableStream({ + pull() { + return Promise.reject(failure); + } + }).getReader(); + + let readFailure: unknown; + let closedFailure: unknown; + try { await reader.read(); } catch (error) { readFailure = error; } + try { await reader.closed; } catch (error) { closedFailure = error; } + expect(readFailure).to.equal(failure); + expect(closedFailure).to.equal(failure); + }); + + it("tees without one branch consuming the other", async function () { + const [first, second] = new ReadableStream({ + start(controller) { + controller.enqueue("a"); + controller.enqueue("b"); + controller.close(); + } + }).tee(); + const firstReader = first.getReader(); + const secondReader = second.getReader(); + + expect(await firstReader.read()).to.deep.equal({ value: "a", done: false }); + expect(await firstReader.read()).to.deep.equal({ value: "b", done: false }); + expect(await firstReader.read()).to.deep.equal({ value: undefined, done: true }); + expect(await secondReader.read()).to.deep.equal({ value: "a", done: false }); + }); + + it("waits for asynchronous writes before closing", async function () { + const stored: number[] = []; + const writable = new WritableStream({ + write(chunk) { + return Promise.resolve().then(() => stored.push(chunk)); + } + }); + const writer = writable.getWriter(); + + writer.write(1); + writer.write(2); + await writer.close(); + expect(stored).to.deep.equal([1, 2]); + }); + + it("applies transform output and backpressure", async function () { + const transform = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk.toUpperCase()); + } + }); + const writer = transform.writable.getWriter(); + const reader = transform.readable.getReader(); + const write = writer.write("native"); + + expect(await reader.read()).to.deep.equal({ value: "NATIVE", done: false }); + await write; + await writer.close(); + expect(await reader.read()).to.deep.equal({ value: undefined, done: true }); + }); + + it("supports BYOB reads from byte streams", async function () { + let sent = false; + const stream = new ReadableStream({ + type: "bytes", + pull(controller) { + if (!sent) { + sent = true; + controller.enqueue(new Uint8Array([8, 241, 48, 123, 151])); + controller.close(); + } + } + } as any); + const reader = stream.getReader({ mode: "byob" }); + const result = await reader.read(new Uint8Array(8)); + + expect(result.done).to.equal(false); + expect(Array.from(result.value!)).to.deep.equal([8, 241, 48, 123, 151]); + expect((await reader.read(new Uint8Array(8))).done).to.equal(true); + }); + + // Ported from Firefox's dom/streams/test/xpcshell/subclassing.js. + it("supports subclassed streams, readers, and queuing strategies", async function () { + class SubclassedStream extends ReadableStream {} + class SubclassedStrategy extends CountQueuingStrategy {} + const stream = new SubclassedStream({ + start(controller) { + controller.enqueue("first"); + controller.close(); + } + }); + const Reader = stream.getReader().constructor as typeof ReadableStreamDefaultReader; + class SubclassedReader extends Reader {} + + expect(stream).to.be.instanceOf(ReadableStream); + expect(new SubclassedStrategy({ highWaterMark: 4 }).highWaterMark).to.equal(4); + + const secondStream = new ReadableStream({ + start(controller) { + controller.enqueue("second"); + controller.close(); + } + }); + const reader = new SubclassedReader(secondStream); + expect(await reader.read()).to.deep.equal({ value: "second", done: false }); + }); + + // Ported from Chromium's http/tests/streams/chromium/transform-stream-enqueue.html. + it("rejects enqueues after a transform is terminated or errored", function () { + expect(() => new TransformStream({ + start(controller) { + controller.terminate(); + controller.enqueue("late"); + } + })).to.throw(TypeError); + + expect(() => new TransformStream({ + start(controller) { + controller.error(new Error("failed")); + controller.enqueue("late"); + } + })).to.throw(TypeError); + }); +}); + +describe("Compression streams", function () { + this.timeout(20000); + + type SupportedCompressionFormat = "deflate" | "deflate-raw" | "gzip"; + type CompressionTransform = { + readable: ReadableStream; + writable: WritableStream; + }; + + const formats: SupportedCompressionFormat[] = ["deflate", "deflate-raw", "gzip"]; + const expectedOutput = new TextEncoder().encode("expected output"); + const compressedFixtures: Array<[SupportedCompressionFormat, Uint8Array]> = [ + ["deflate", new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36])], + ["gzip", new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0])], + ["deflate-raw", new Uint8Array([75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0])], + ]; + + async function concatenateStream(readable: ReadableStream): Promise { + const reader = readable.getReader(); + const chunks: Uint8Array[] = []; + let byteLength = 0; + while (true) { + const result = await reader.read(); + if (result.done) { + break; + } + expect(result.value).to.be.instanceOf(Uint8Array); + chunks.push(result.value); + byteLength += result.value.byteLength; + } + + const output = new Uint8Array(byteLength); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.byteLength; + } + return output; + } + + async function transformChunks(transform: CompressionTransform, chunks: BufferSource[]): Promise { + const output = concatenateStream(transform.readable); + const writer = transform.writable.getWriter(); + for (const chunk of chunks) { + await writer.write(chunk); + } + await writer.close(); + return output; + } + + async function roundTrip(input: Uint8Array, format: SupportedCompressionFormat): Promise { + const compressed = await transformChunks(new CompressionStream(format), [input]); + return transformChunks(new DecompressionStream(format), [compressed]); + } + + it("exposes browser-shaped constructors, properties, and brand checks", function () { + const compression = new CompressionStream("gzip"); + const decompression = new DecompressionStream("gzip"); + expect(compression.readable).to.be.instanceOf(ReadableStream); + expect(compression.writable).to.be.instanceOf(WritableStream); + expect(decompression.readable).to.be.instanceOf(ReadableStream); + expect(decompression.writable).to.be.instanceOf(WritableStream); + expect(String(compression)).to.equal("[object CompressionStream]"); + expect(String(decompression)).to.equal("[object DecompressionStream]"); + + const compressionReadable = Object.getOwnPropertyDescriptor(CompressionStream.prototype, "readable")!.get!; + const decompressionWritable = Object.getOwnPropertyDescriptor(DecompressionStream.prototype, "writable")!.get!; + expect(() => compressionReadable.call({})).to.throw(TypeError); + expect(() => decompressionWritable.call({})).to.throw(TypeError); + }); + + // Focused ports from WPT compression/*-constructor-error.any.js. + it("validates and converts constructor formats", function () { + expect(() => new CompressionStream()).to.throw(TypeError); + expect(() => new DecompressionStream()).to.throw(TypeError); + expect(() => new CompressionStream("invalid" as any)).to.throw(TypeError); + expect(() => new DecompressionStream("GZIP" as any)).to.throw(TypeError); + expect(() => new CompressionStream(Symbol("gzip") as any)).to.throw(TypeError); + + const failure = new Error("format conversion failed"); + let thrown: unknown; + try { + new DecompressionStream({ toString() { throw failure; } } as any); + } catch (error) { + thrown = error; + } + expect(thrown).to.equal(failure); + }); + + // Focused ports from WPT compression-stream.any.js, + // compression-multiple-chunks.any.js, and including-empty-chunk.any.js. + it("round-trips all supported formats across multiple and empty chunks", async function () { + const middleBacking = new Uint8Array(new TextEncoder().encode("!from browser-shaped streams!")); + const inputChunks = [ + new TextEncoder().encode("Hello "), + new Uint8Array(), + middleBacking.subarray(1, middleBacking.byteLength - 1), + ]; + const expected = new TextEncoder().encode("Hello from browser-shaped streams"); + + for (const format of formats) { + const compressed = await transformChunks(new CompressionStream(format), inputChunks); + const output = await transformChunks(new DecompressionStream(format), [compressed]); + expect(Array.from(output)).to.deep.equal(Array.from(expected)); + } + }); + + // Ported from WPT decompression-buffersource.any.js. + it("accepts ArrayBuffer, typed-array, and DataView input", async function () { + const fixtures: Array<[SupportedCompressionFormat, number[], string]> = [ + ["deflate", [120, 156, 75, 52, 48, 52, 50, 54, 49, 53, 3, 0, 8, 136, 1, 199], "a0123456"], + ["gzip", [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 52, 48, 52, 2, 0, 216, 252, 63, 136, 4, 0, 0, 0], "a012"], + ["deflate-raw", [0, 6, 0, 249, 255, 65, 66, 67, 68, 69, 70, 1, 0, 0, 255, 255], "ABCDEF"], + ]; + + for (const [format, values, expected] of fixtures) { + const expectedBytes = Array.from(new TextEncoder().encode(expected)); + const makeBuffer = () => new Uint8Array(values).buffer as ArrayBuffer; + const padded = new Uint8Array(values.length + 2); + padded.set(values, 1); + const inputs: BufferSource[] = [ + makeBuffer(), + new Int8Array(makeBuffer()), + new Uint16Array(makeBuffer()), + new DataView(makeBuffer()), + new DataView(padded.buffer, 1, values.length), + ]; + for (const input of inputs) { + const output = await transformChunks(new DecompressionStream(format), [input]); + expect(Array.from(output)).to.deep.equal(expectedBytes); + } + } + }); + + // Focused ports from WPT decompression-split-chunk.any.js and + // decompression-uint8array-output.any.js. + it("decompresses input split at every small chunk boundary", async function () { + for (const [format, fixture] of compressedFixtures) { + for (let chunkSize = 1; chunkSize < 16; ++chunkSize) { + const chunks: Uint8Array[] = []; + for (let offset = 0; offset < fixture.byteLength; offset += chunkSize) { + chunks.push(fixture.slice(offset, offset + chunkSize)); + } + const output = await transformChunks(new DecompressionStream(format), chunks); + expect(Array.from(output)).to.deep.equal(Array.from(expectedOutput)); + } + } + }); + + // Ported from WPT compression-large-flush-output.any.js. + it("does not truncate output produced while closing", async function () { + const encoded = new TextEncoder().encode(JSON.stringify(Array.from({ length: 10000 }, (_, index) => index))); + const input = encoded.subarray(0, 35579); + for (const format of formats) { + expect(Array.from(await roundTrip(input, format))).to.deep.equal(Array.from(input)); + } + }); + + // WPT decompression-extra-input.any.js requires already-produced output + // to be observable before the stream reports the trailing-data error. + it("emits valid output before rejecting extra compressed input", async function () { + for (const [format, fixture] of compressedFixtures) { + const stream = new DecompressionStream(format); + const reader = stream.readable.getReader(); + const writer = stream.writable.getWriter(); + const firstRead = reader.read(); + const write = writer.write(new Uint8Array([...fixture, 0])).catch(error => error); + const first = await firstRead; + expect(first.done).to.equal(false); + expect(Array.from(first.value!)).to.deep.equal(Array.from(expectedOutput)); + + let readFailure: unknown; + try { await reader.read(); } catch (error) { readFailure = error; } + expect(readFailure).to.be.instanceOf(TypeError); + expect(await write).to.be.instanceOf(TypeError); + } + }); + + // Focused ports from WPT compression-bad-chunks.any.js and + // decompression-bad-chunks.any.js. + it("errors both sides of the transform for non-BufferSource chunks", async function () { + for (const create of [ + () => new CompressionStream("gzip"), + () => new DecompressionStream("gzip"), + ]) { + const stream = create(); + const reader = stream.readable.getReader(); + const writer = stream.writable.getWriter(); + const read = reader.read().catch(error => error); + const write = writer.write({} as any).catch(error => error); + expect(await write).to.be.instanceOf(TypeError); + expect(await read).to.be.instanceOf(TypeError); + } + }); + + // Focused ports from WPT decompression-corrupt-input.any.js and the + // truncated-input check in Chromium's InflateTransformer. + it("rejects corrupt and truncated compressed input", async function () { + for (const [format, fixture] of compressedFixtures) { + const inputs = [ + fixture.subarray(0, fixture.byteLength - 1), + new Uint8Array(fixture.map((value, index) => index === 0 ? value ^ 0xff : value)), + ]; + for (const input of inputs) { + const stream = new DecompressionStream(format); + const output = concatenateStream(stream.readable).catch(error => error); + const writer = stream.writable.getWriter(); + await writer.write(input).catch(() => undefined); + const close = writer.close().catch(error => error); + expect(await close).to.be.instanceOf(TypeError); + expect(await output).to.be.instanceOf(TypeError); + } + } + }); + + // Chromium buffers output before enqueue because enqueue may execute + // JavaScript that mutates or detaches the input still being consumed. + // This regression test forces that reentrancy without needing postMessage. + it("finishes consuming input before an enqueue callback can mutate it", async function () { + const input = new Uint8Array(256 * 1024); + let state = 0x12345678; + for (let index = 0; index < input.length; ++index) { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + input[index] = state & 0xff; + } + const expected = input.slice(); + + const prototype = TransformStreamDefaultController.prototype as any; + const originalEnqueue = prototype.enqueue; + let mutated = false; + prototype.enqueue = function(chunk: unknown) { + if (!mutated) { + mutated = true; + input.fill(0); + } + return originalEnqueue.call(this, chunk); + }; + + let compressed: Uint8Array; + try { + compressed = await transformChunks(new CompressionStream("deflate"), [input]); + } finally { + prototype.enqueue = originalEnqueue; + } + + expect(mutated).to.equal(true); + const output = await transformChunks(new DecompressionStream("deflate"), [compressed!]); + expect(Array.from(output)).to.deep.equal(Array.from(expected)); + }); + + // Empty chunks are common in WebKit and WPT stream regressions. They must + // not allocate codec output or accumulate retained per-write state. + it("handles a long sequence of empty writes without retained output", async function () { + const chunks = Array.from({ length: 1024 }, () => new Uint8Array()); + const compressed = await transformChunks(new CompressionStream("gzip"), chunks); + const output = await transformChunks(new DecompressionStream("gzip"), [compressed]); + expect(output.byteLength).to.equal(0); + }); + + // Repeated short-lived streams model asset-heavy native applications. A + // completed stream must release zlib and scratch storage before JS GC. + it("releases completed codec state across repeated streams", async function () { + const fixture = compressedFixtures.find(([format]) => format === "gzip")![1]; + for (let iteration = 0; iteration < 128; ++iteration) { + const output = await transformChunks(new DecompressionStream("gzip"), [fixture]); + expect(Array.from(output)).to.deep.equal(Array.from(expectedOutput)); + } + }); +}); + describe("Blob", function () { + this.timeout(10000); + let emptyBlobs: Blob[], helloBlobs: Blob[], stringBlob: Blob, typedArrayBlob: Blob, arrayBufferBlob: Blob, blobBlob: Blob; + async function readStream(stream: ReadableStream, mode?: "byob"): Promise { + const reader: any = stream.getReader(mode === undefined ? undefined : { mode }); + const bytes: number[] = []; + while (true) { + const result = mode === "byob" + ? await reader.read(new Uint8Array(64)) + : await reader.read(); + if (result.done) { + return bytes; + } + bytes.push(...Array.from(result.value as Uint8Array)); + } + } + before(function () { emptyBlobs = [new Blob([]), new Blob([])]; stringBlob = new Blob(["Hello"]); @@ -1416,6 +2256,120 @@ describe("Blob", function () { expect(modelGltfJson.type).to.equal("model/gltf+json"); }); + // Focused ports from WPT FileAPI/blob/Blob-constructor.any.js. + it("accepts iterable parts and preserves their order", async function () { + const parts = { + *[Symbol.iterator]() { + yield "foo"; + yield new Uint8Array([98, 97, 114]); + yield new Blob(["baz"]); + } + }; + const blob = new Blob(parts as any); + expect(blob.size).to.equal(9); + expect(await blob.text()).to.equal("foobarbaz"); + }); + + it("uses an Array's overridden iterator", async function () { + const parts = ["ignored"]; + parts[Symbol.iterator] = function* () { + yield "custom"; + }; + + expect(await new Blob(parts).text()).to.equal("custom"); + }); + + it("closes an iterator when BlobPart conversion throws", function () { + let closed = false; + const badPart = { + toString() { + throw new Error("part conversion failed"); + } + }; + const parts = (function* () { + try { + yield badPart; + } finally { + closed = true; + } + })(); + + // QuickJS currently wraps an exception rethrown through a native + // constructor as its generic JS error type, so assert the observable + // iterator-close behavior separately from the adapter's error text. + expect(() => new Blob(parts as any)).to.throw(); + expect(closed).to.equal(true); + }); + + it("observes BlobPart array mutations during iteration", async function () { + const parts: any[] = [ + { + toString() { + parts.pop(); + return "PASS"; + } + }, + { + toString() { + throw new Error("removed part was converted"); + } + } + ]; + + expect(await new Blob(parts).text()).to.equal("PASS"); + }); + + it("converts parts before reading options in WebIDL order", function () { + const accesses: string[] = []; + const part = { + toString() { + accesses.push("part"); + return "data"; + } + }; + new Blob([part], { + get type() { + accesses.push("type"); + return "TEXT/PLAIN"; + }, + get endings() { + accesses.push("endings"); + return "transparent" as EndingType; + } + }); + + expect(accesses).to.deep.equal(["part", "endings", "type"]); + }); + + it("validates the endings enum and options dictionary", function () { + expect(() => new Blob([], { endings: "NATIVE" as EndingType })).to.throw(); + expect(() => new Blob([], { endings: "invalid" as EndingType })).to.throw(); + for (const value of [123, true, "abc"]) { + expect(() => new Blob([], value as any)).to.throw(); + } + expect(() => new Blob([], null as any)).not.to.throw(); + expect(() => new Blob([], undefined)).not.to.throw(); + }); + + it("exposes browser-compatible class tags", function () { + expect(String(new Blob())).to.equal("[object Blob]"); + expect(String(new File([], "empty.txt"))).to.equal("[object File]"); + }); + + it("rejects primitive parts containers", function () { + for (const value of [null, true, 7, "not a sequence"]) { + // Some Node-API adapters currently wrap a Napi::TypeError thrown + // by a constructor as their generic JS error type. + expect(() => new Blob(value as any)).to.throw(); + } + }); + + it("normalizes valid MIME types and clears invalid types", function () { + expect(new Blob([], { type: "TEXT/PLAIN" }).type).to.equal("text/plain"); + expect(new Blob([], { type: "te\x09xt/plain" }).type).to.equal(""); + expect(new Blob([], { type: "text/\x7fplain" }).type).to.equal(""); + }); + // -------------------------------- Blob.text() -------------------------------- it("returns empty string for empty blobs", async function () { for (const blob of emptyBlobs) { @@ -1437,6 +2391,11 @@ describe("Blob", function () { expect(text).to.equal("你好, 世界"); }); + it("replaces invalid UTF-8 bytes", async function () { + const invalid = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]); + expect(await new Blob([invalid]).text()).to.equal("\ufffd".repeat(invalid.length)); + }); + it("preserves line endings like default transparent mode", async function () { const lineEndingsBlob = new Blob(["Hello\nWorld"]); const text = await lineEndingsBlob.text(); @@ -1483,6 +2442,109 @@ describe("Blob", function () { } }); + + // Focused ports from WPT FileAPI/blob/Blob-slice.any.js. + it("slices across part boundaries and applies clamp rounding", async function () { + const blob = new Blob(["foo", new Blob(["bar"]), "baz"]); + expect(await blob.slice(2, 7).text()).to.equal("obarb"); + expect(await new Blob(["abcd"]).slice(1.5).text()).to.equal("cd"); + expect(await new Blob(["abcd"]).slice(2.5).text()).to.equal("cd"); + expect(await blob.slice(-3, undefined, "TEXT/PLAIN").text()).to.equal("baz"); + expect(blob.slice(-3, undefined, "TEXT/PLAIN").type).to.equal("text/plain"); + }); + + // Focused ports from WPT FileAPI/blob/Blob-stream.any.js. + it("streams binary data through default and BYOB readers", async function () { + const input = [8, 241, 48, 123, 151]; + const blob = new Blob([new Uint8Array(input)]); + expect(await readStream(blob.stream())).to.deep.equal(input); + expect(await readStream(blob.stream(), "byob")).to.deep.equal(input); + expect(await readStream(new Blob().stream())).to.deep.equal([]); + }); + + // Adapted from WPT streams/readable-byte-streams/general.any.js BYOB + // coverage. Small, offset views exercise the caller-owned output path. + it("fills bounded BYOB views across Blob segment boundaries", async function () { + const input = new Uint8Array(97); + for (let index = 0; index < input.length; ++index) { + input[index] = (index * 31) & 0xff; + } + + const blob = new Blob([input.subarray(0, 11), input.subarray(11, 53), input.subarray(53)]); + const reader = blob.stream().getReader({ mode: "byob" }); + const output: number[] = []; + const requestSizes = [1, 3, 7, 16]; + let requestIndex = 0; + while (true) { + const requestSize = requestSizes[requestIndex++ % requestSizes.length]; + const request = new Uint8Array(new ArrayBuffer(requestSize + 4), 2, requestSize); + const result = await reader.read(request); + if (result.done) { + break; + } + expect(result.value!.byteLength).to.be.at.most(requestSize); + output.push(...Array.from(result.value!)); + } + + expect(output).to.deep.equal(Array.from(input)); + }); + + it("keeps independent stream cursors after the Blob reference is dropped", async function () { + let blob: Blob | null = new Blob(["PASS"]); + const first = blob.stream(); + const second = blob.stream(); + blob = null; + expect(await readStream(first)).to.deep.equal([80, 65, 83, 83]); + expect(await readStream(second)).to.deep.equal([80, 65, 83, 83]); + }); + + // Adapted from WebKit's fast/files/blob-stream-chunks.html. + it("chunks a large Blob and supports cancellation without retaining work", async function () { + const blob = new Blob([new Uint8Array(5 * 1024 * 1024)]); + const reader = blob.stream().getReader(); + const first = await reader.read(); + expect(first.done).to.equal(false); + expect(first.value!.byteLength).to.be.at.most(64 * 1024); + await reader.cancel(); + await reader.closed; + }); + + // Adapted from WebKit's blob-stream crash regression and exercises + // teardown of the C++ pull-state closures under repeated construction. + it("constructs and cancels many empty streams without crashing", async function () { + for (let index = 0; index < 1000; ++index) { + await new Blob().stream().cancel(); + } + }); + + // Scaled port of Firefox's dom/streams/test/xpcshell/large-pipeto.js. + it("pipes nested shared Blob parts without corrupting chunk boundaries", async function () { + const pattern = new Uint8Array(256 * 1024); + for (let index = 0; index < pattern.length; ++index) { + pattern[index] = index % 256; + } + const pair = new Blob([pattern, pattern]); + const nested = new Blob([pair, pair, pair, pair, pair, pair]); + let position = 0; + + await nested.stream().pipeTo(new WritableStream({ + write(chunk: Uint8Array) { + for (const value of chunk) { + const expected = position % pattern.length % 256; + if (value !== expected) { + throw new Error(`Blob stream byte ${position}: expected ${expected}, received ${value}`); + } + ++position; + } + } + })); + expect(position).to.equal(pattern.length * 12); + }); + + it("uses a File's Blob bytes when composing parts", async function () { + const file = new File(["a", "b"], "letters.txt"); + expect(await new Blob(["<", file, ">"]).text()).to.equal(""); + }); }); describe("napi class prototype isolation (#172)", function () { @@ -1804,6 +2866,22 @@ describe("File", function () { expect(text).to.equal("你好, 世界"); }); + // Adapted from WebKit's fast/files/blob-stream-crash-2.html. + it("streams and slices multiple File parts through the Blob API", async function () { + const file = new File(["a", new Blob(), "b", new Blob(), "c", new Blob(), "d"], "letters.txt"); + const reader = file.stream().getReader(); + const bytes: number[] = []; + while (true) { + const result = await reader.read(); + if (result.done) { + break; + } + bytes.push(...Array.from(result.value as Uint8Array)); + } + expect(bytes).to.deep.equal([97, 98, 99, 100]); + expect(await file.slice(1, 3).text()).to.equal("bc"); + }); + // -------------------------------- Blob inheritance -------------------------------- it("is an instance of Blob (prototype chain wired up)", function () { // BJS core (fileTools, Offline/database, abstractEngine, @@ -2033,6 +3111,285 @@ describe("FileReader", function () { }); }); +describe("IndexedDB", function () { + this.timeout(10000); + + let databaseCounter = 0; + const databaseName = (test: string) => + `jsruntimehost-${test}-${++databaseCounter}`; + + const requestResult = (request: IDBRequest): Promise => + new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + + const transactionDone = (transaction: IDBTransaction): Promise => + new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onabort = () => + reject(transaction.error || new Error("IndexedDB transaction aborted")); + transaction.onerror = () => { + // The cancelable request error determines whether the + // transaction aborts; onabort carries the final result. + }; + }); + + const openDatabase = ( + name: string, + version: number, + upgrade: (database: IDBDatabase, transaction: IDBTransaction) => void + ): Promise => + new Promise((resolve, reject) => { + const request = indexedDB.open(name, version); + request.onupgradeneeded = () => + upgrade(request.result, request.transaction!); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + + it("supports key paths, generated keys, indexes, and storage clones", async function () { + // Adapted from WPT IndexedDB/keygenerator.any.js, + // clone-before-keypath-eval.any.js, and structured-clone.any.js. + const name = databaseName("records"); + const database = await openDatabase(name, 1, db => { + const store = db.createObjectStore("records", { + keyPath: "id", + autoIncrement: true, + }); + store.createIndex("by-email", "email", { unique: true }); + }); + + const bytes = new Uint8Array([1, 2, 3, 4]); + const record: any = { + email: "first@example.test", + bytes, + alias: bytes, + }; + record.self = record; + + const write = database.transaction("records", "readwrite"); + const writeDone = transactionDone(write); + const key = await requestResult( + write.objectStore("records").add(record) + ); + expect(key).to.equal(1); + expect(record.id).to.equal(undefined); + bytes[0] = 99; + await writeDone; + + const read = database.transaction("records"); + const stored: any = await requestResult( + read.objectStore("records").index("by-email").get("first@example.test") + ); + expect(stored.id).to.equal(1); + expect(stored.self).to.equal(stored); + expect(stored.bytes).to.equal(stored.alias); + expect(Array.from(stored.bytes)).to.eql([1, 2, 3, 4]); + + stored.bytes[1] = 88; + const readAgain = database.transaction("records"); + const storedAgain: any = await requestResult( + readAgain.objectStore("records").get(1) + ); + expect(Array.from(storedAgain.bytes)).to.eql([1, 2, 3, 4]); + database.close(); + }); + + it("stores Blob values without stalling an unrelated transaction", async function () { + // Regression coverage for Chromium crbug.com/475947902: + // https://chromium.googlesource.com/chromium/src/+/4e5f6ab9fcf10d98dadb6861184b686d244635fb + // Chromium regressed by coupling an in-progress Blob write to an + // unrelated object-store read. Keep disjoint scopes independent and + // preserve the Blob while cloning it for storage. + const name = databaseName("blob-concurrency"); + const database = await openDatabase(name, 1, db => { + db.createObjectStore("blobs"); + db.createObjectStore("other"); + }); + + const write = database.transaction("blobs", "readwrite"); + const writeDone = transactionDone(write); + write.objectStore("blobs").put( + { blob: new Blob(["abc"], { type: "text/plain" }) }, + "key" + ); + + const read = database.transaction("other", "readonly"); + const readDone = transactionDone(read); + expect(await requestResult(read.objectStore("other").get("missing"))) + .to.equal(undefined); + read.commit(); + await readDone; + await writeDone; + + const stored: any = await requestResult( + database.transaction("blobs").objectStore("blobs").get("key") + ); + expect(stored.blob).to.be.an.instanceof(Blob); + expect(stored.blob.type).to.equal("text/plain"); + expect(await stored.blob.text()).to.equal("abc"); + database.close(); + }); + + it("rolls back writes and generated keys when a transaction aborts", async function () { + // Adapted from WPT IndexedDB/idbtransaction_abort.any.js and + // transaction-abort-generator-revert.any.js. Applying writes directly + // to a backing Map is a tempting in-memory implementation shortcut; + // it violates IndexedDB atomicity when the transaction aborts. + const name = databaseName("rollback"); + const database = await openDatabase(name, 1, db => { + db.createObjectStore("records", { autoIncrement: true }); + }); + + const transaction = database.transaction("records", "readwrite"); + const aborted = new Promise((resolve, reject) => { + transaction.onabort = () => resolve(); + transaction.oncomplete = () => + reject(new Error("aborted transaction completed")); + }); + transaction.objectStore("records").add("discarded"); + transaction.abort(); + await aborted; + + const replacement = database.transaction("records", "readwrite"); + const replacementDone = transactionDone(replacement); + const key = await requestResult( + replacement.objectStore("records").add("committed") + ); + await replacementDone; + expect(key).to.equal(1); + expect( + await requestResult( + database.transaction("records").objectStore("records").get(1) + ) + ).to.equal("committed"); + database.close(); + }); + + it("continues a transaction when a request error is canceled", async function () { + // Adapted from WPT IndexedDB/idbtransaction.any.js and + // idbobjectstore-put-unique-index-constraint-is-atomic.any.js. + // Chromium's abort/error ordering regression tests landed in: + // https://chromium.googlesource.com/chromium/src/+/7af4cd7effc0e86f7f4a13b740f9315b243bf469 + // Do not abort the whole transaction before the cancelable request + // error can suppress its default action. + const name = databaseName("cancel-error"); + const database = await openDatabase(name, 1, db => { + const store = db.createObjectStore("records", { keyPath: "id" }); + store.createIndex("unique-value", "value", { unique: true }); + }); + + const seed = database.transaction("records", "readwrite"); + const seedDone = transactionDone(seed); + seed.objectStore("records").add({ id: 1, value: "duplicate" }); + await seedDone; + + const transaction = database.transaction("records", "readwrite"); + const done = transactionDone(transaction); + const duplicate = transaction + .objectStore("records") + .add({ id: 2, value: "duplicate" }); + duplicate.onerror = event => event.preventDefault(); + transaction + .objectStore("records") + .add({ id: 3, value: "survives" }); + await done; + + expect( + await requestResult( + database.transaction("records").objectStore("records").get(2) + ) + ).to.equal(undefined); + expect( + ( + await requestResult( + database.transaction("records").objectStore("records").get(3) + ) + ).value + ).to.equal("survives"); + database.close(); + }); + + it("iterates bounded key ranges in key order", async function () { + // Adapted from WPT IndexedDB/idbcursor-continue.any.js and + // idbkeyrange-includes.any.js. + const name = databaseName("cursor"); + const database = await openDatabase(name, 1, db => { + db.createObjectStore("records"); + }); + + const write = database.transaction("records", "readwrite"); + const writeDone = transactionDone(write); + for (let key = 5; key >= 1; --key) { + write.objectStore("records").put(`value-${key}`, key); + } + await writeDone; + + const keys: number[] = []; + const cursorRequest = database + .transaction("records") + .objectStore("records") + .openCursor(IDBKeyRange.bound(2, 4)); + await new Promise((resolve, reject) => { + cursorRequest.onerror = () => reject(cursorRequest.error); + cursorRequest.onsuccess = () => { + const cursor = cursorRequest.result; + if (!cursor) { + resolve(); + return; + } + keys.push(cursor.key as number); + cursor.continue(); + }; + }); + expect(keys).to.eql([2, 3, 4]); + database.close(); + }); + + it("dispatches versionchange before a blocked upgrade can finish", async function () { + // Adapted from WPT IndexedDB/idbfactory-open-request-error.any.js and + // open-request-queue.any.js. + const name = databaseName("versionchange"); + const first = await openDatabase(name, 1, db => { + db.createObjectStore("records"); + }); + + let oldVersion = -1; + let newVersion: number | null = -1; + first.onversionchange = event => { + oldVersion = event.oldVersion; + newVersion = event.newVersion; + first.close(); + }; + + const second = await openDatabase(name, 2, () => {}); + expect(oldVersion).to.equal(1); + expect(newVersion).to.equal(2); + expect(second.version).to.equal(2); + second.close(); + }); + + it("throws DataCloneError synchronously for unsupported values", async function () { + // Adapted from WPT IndexedDB/structured-clone.any.js. + const name = databaseName("clone-error"); + const database = await openDatabase(name, 1, db => { + db.createObjectStore("records"); + }); + const transaction = database.transaction("records", "readwrite"); + let error: any; + try { + transaction.objectStore("records").put(() => {}, 1); + } catch (caught) { + error = caught; + } + expect(error).to.be.an.instanceof(DOMException); + expect(error.name).to.equal("DataCloneError"); + transaction.abort(); + database.close(); + }); +}); + function runTests() { mocha.run((failures: number) => { // Test program will wait for code to be set before exiting diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index a920fa1f..b70e0940 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -11,19 +11,212 @@ #include #include #include +#include #include #include +#include +#include +#if defined(JSRUNTIMEHOST_TEST_WORKER) +#include +#endif #include #include #include #include #include +#include +#include #include #include #include +#include +#include +#include +#include + +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) +#include +#include +#include +#include +#include + +#include +#include + +#include "../../NodeApi/node_lite.h" +#include "../../NodeApi/test_main.h" +#endif namespace { +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) + namespace + { + using namespace std::filesystem; + + void CopyAssetsRecursive(AAssetManager* manager, const std::string& asset_path, const path& destination) + { + // The NDK AAssetManager cannot enumerate subdirectories -- AAssetDir_getNextFileName + // returns files in a single directory only, never nested directories -- so the test + // tree cannot be discovered at runtime. Instead read a build-time manifest (one + // relative path per line, produced by the copyNodeApiTests Gradle task) and copy each + // listed file individually (AAssetManager_open works fine for a known file path). + std::string manifest_asset = asset_path + "/manifest.txt"; + AAsset* manifest = AAssetManager_open(manager, manifest_asset.c_str(), AASSET_MODE_BUFFER); + if (manifest == nullptr) + { + return; + } + + off_t manifest_length = AAsset_getLength(manifest); + std::string manifest_text(static_cast(manifest_length), '\0'); + AAsset_read(manifest, manifest_text.data(), manifest_length); + AAsset_close(manifest); + + std::stringstream manifest_stream(manifest_text); + std::string relative_path; + while (std::getline(manifest_stream, relative_path)) + { + if (!relative_path.empty() && relative_path.back() == '\r') + { + relative_path.pop_back(); + } + if (relative_path.empty()) + { + continue; + } + + std::string child_asset = asset_path + "/" + relative_path; + AAsset* asset = AAssetManager_open(manager, child_asset.c_str(), AASSET_MODE_STREAMING); + if (asset == nullptr) + { + continue; + } + + path output_path = destination / relative_path; + create_directories(output_path.parent_path()); + std::ofstream output(output_path, std::ios::binary); + char buffer[8192]; + int read = 0; + while ((read = AAsset_read(asset, buffer, sizeof(buffer))) > 0) + { + output.write(buffer, read); + } + AAsset_close(asset); + } + } + + path GetFilesDir() + { + JNIEnv* env = android::global::GetEnvForCurrentThread(); + jobject context = android::global::GetAppContext(); + jclass contextClass = env->GetObjectClass(context); + jmethodID getFilesDir = env->GetMethodID(contextClass, "getFilesDir", "()Ljava/io/File;"); + jobject filesDir = env->CallObjectMethod(context, getFilesDir); + env->DeleteLocalRef(contextClass); + + jclass fileClass = env->GetObjectClass(filesDir); + jmethodID getAbsolutePath = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;"); + jstring pathString = static_cast(env->CallObjectMethod(filesDir, getAbsolutePath)); + env->DeleteLocalRef(fileClass); + + const char* rawPath = env->GetStringUTFChars(pathString, nullptr); + path resultPath{rawPath}; + env->ReleaseStringUTFChars(pathString, rawPath); + env->DeleteLocalRef(pathString); + env->DeleteLocalRef(filesDir); + + return resultPath; + } + + std::unordered_set ParseNativeSuiteList() + { + std::unordered_set suites; +#ifdef NODE_API_AVAILABLE_NATIVE_TESTS + std::stringstream stream(NODE_API_AVAILABLE_NATIVE_TESTS); + std::string entry; + while (std::getline(stream, entry, ',')) + { + if (!entry.empty()) + { + suites.insert(entry); + } + } +#endif + return suites; + } + + std::optional& OverrideBaseDir() + { + static std::optional baseDirOverride{}; + return baseDirOverride; + } + + AAssetManager*& OverrideAssetManager() + { + static AAssetManager* assetManager{}; + return assetManager; + } + + void ConfigureNodeApiTests() + { + static std::once_flag onceFlag; + std::call_once(onceFlag, []() { + path baseDir; + if (OverrideBaseDir()) + { + baseDir = *OverrideBaseDir(); + } + else + { + baseDir = GetFilesDir() / "node_api_tests"; + } + std::error_code ec; + std::filesystem::remove_all(baseDir, ec); + std::filesystem::create_directories(baseDir); + + AAssetManager* assetManagerNative = OverrideAssetManager(); + if (assetManagerNative == nullptr) + { + auto assetManagerWrapper = android::global::GetAppContext().getAssets(); + assetManagerNative = assetManagerWrapper; + } + + if (assetManagerNative != nullptr) + { + CopyAssetsRecursive(assetManagerNative, "NodeApi/test", baseDir); + } + + node_api_tests::NodeApiTestConfig config{}; + config.js_root = baseDir; + config.run_script = [baseDir](const path& script) { + node_api_tests::NodeLiteRuntime::Callbacks callbacks; + callbacks.stdout_callback = [](const std::string& message) { + __android_log_write(ANDROID_LOG_INFO, "NodeApiTests", message.c_str()); + }; + callbacks.stderr_callback = [](const std::string& message) { + __android_log_write(ANDROID_LOG_ERROR, "NodeApiTests", message.c_str()); + }; + auto result = node_api_tests::RunNodeLiteScript(baseDir, script, std::move(callbacks)); + // Surface the in-process failure detail to logcat. The runner keeps the assertion / + // exception message + stack in result.std_error; without this it never reaches the + // device log, making on-device conformance failures undebuggable. + if (result.status != 0) { + std::string detail = result.std_error.empty() ? "(no std_error captured)" : result.std_error; + __android_log_write(ANDROID_LOG_ERROR, "NodeApiTests", + ("[node_lite status=" + std::to_string(result.status) + "] " + detail).c_str()); + } + return result; + }; + config.enabled_native_suites = ParseNativeSuiteList(); + + node_api_tests::InitializeNodeApiTests(config); + }); + } + } +#endif + const char* EnumToString(Babylon::Polyfills::Console::LogLevel logLevel) { switch (logLevel) @@ -85,11 +278,20 @@ TEST(JavaScript, All) Babylon::Polyfills::URL::Initialize(env); Babylon::Polyfills::WebSocket::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); - Babylon::Polyfills::Fetch::Initialize(env); + Babylon::Polyfills::Streams::Initialize(env); Babylon::Polyfills::Blob::Initialize(env); Babylon::Polyfills::File::Initialize(env); Babylon::Polyfills::TextDecoder::Initialize(env); Babylon::Polyfills::TextEncoder::Initialize(env); + Babylon::Polyfills::Fetch::Initialize(env); + Babylon::Polyfills::Compression::Initialize(env); + Babylon::Polyfills::IndexedDB::Initialize(env); + +#if defined(JSRUNTIMEHOST_TEST_WORKER) + Babylon::Polyfills::Worker::Options workerOptions{}; + workerOptions.ScriptRoot = std::filesystem::current_path().string(); + Babylon::Polyfills::Worker::Initialize(env, std::move(workerOptions)); +#endif auto setExitCodeCallback = Napi::Function::New( env, [&exitCodePromise](const Napi::CallbackInfo& info) { @@ -111,6 +313,221 @@ TEST(JavaScript, All) EXPECT_EQ(exitCode, 0); } +TEST(Streams, ReplacesPartialHostSuiteAndIsIdempotent) +{ + std::promise done; + std::atomic_bool completed{}; + const auto complete = [&done, &completed](std::string result) { + if (!completed.exchange(true)) + { + done.set_value(std::move(result)); + } + }; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [&complete](const Napi::Error& error) { + complete(Napi::GetErrorString(error)); + }; + Babylon::AppRuntime runtime{options}; + + runtime.Dispatch([&complete](Napi::Env env) { + auto global = env.Global(); + const auto hostReadableStream = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostReadableStream"); + global.Set("ReadableStream", hostReadableStream); + global.Set("TransformStream", env.Null()); + + Babylon::Polyfills::Streams::Initialize(env); + const auto installedReadableStream = global.Get("ReadableStream"); + const auto installedTransformStream = global.Get("TransformStream"); + EXPECT_FALSE(installedReadableStream.StrictEquals(hostReadableStream)); + if (!installedReadableStream.IsFunction() || !installedTransformStream.IsFunction()) + { + complete("Streams::Initialize did not install a complete constructor suite."); + return; + } + + const auto transform = installedTransformStream.As().New({}); + EXPECT_TRUE(transform.Get("readable").As().InstanceOf(installedReadableStream.As())); + + Babylon::Polyfills::Streams::Initialize(env); + EXPECT_TRUE(global.Get("ReadableStream").StrictEquals(installedReadableStream)); + EXPECT_TRUE(global.Get("TransformStream").StrictEquals(installedTransformStream)); + complete({}); + }); + + const auto error = done.get_future().get(); + EXPECT_TRUE(error.empty()) << error; +} + +TEST(Compression, PreservesHostConstructorsAndIsIdempotent) +{ + Babylon::AppRuntime runtime{}; + std::promise done; + + runtime.Dispatch([&done](Napi::Env env) { + auto global = env.Global(); + const auto hostCompressionStream = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostCompressionStream"); + global.Set("CompressionStream", hostCompressionStream); + + Babylon::Polyfills::Compression::Initialize(env); + EXPECT_TRUE(global.Get("CompressionStream").StrictEquals(hostCompressionStream)); + EXPECT_TRUE(global.Get("DecompressionStream").IsFunction()); + + const auto installedDecompressionStream = global.Get("DecompressionStream"); + Babylon::Polyfills::Compression::Initialize(env); + EXPECT_TRUE(global.Get("CompressionStream").StrictEquals(hostCompressionStream)); + EXPECT_TRUE(global.Get("DecompressionStream").StrictEquals(installedDecompressionStream)); + done.set_value(); + }); + + done.get_future().get(); +} + +TEST(IndexedDB, PreservesHostImplementation) +{ + std::promise done; + Babylon::AppRuntime runtime{}; + + runtime.Dispatch([&done](Napi::Env env) { + auto global = env.Global(); + auto hostIndexedDB = Napi::Object::New(env); + global.Set("indexedDB", hostIndexedDB); + + Babylon::Polyfills::IndexedDB::Initialize(env); + EXPECT_TRUE(global.Get("indexedDB").StrictEquals(hostIndexedDB)); + + Babylon::Polyfills::IndexedDB::Initialize(env); + EXPECT_TRUE(global.Get("indexedDB").StrictEquals(hostIndexedDB)); + done.set_value(); + }); + + done.get_future().get(); +} + +TEST(Fetch, PreservesCompleteHostClassesAndIsIdempotent) +{ + std::promise done; + std::atomic_bool completed{}; + const auto complete = [&done, &completed](std::string result) { + if (!completed.exchange(true)) + { + done.set_value(std::move(result)); + } + }; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [&complete](const Napi::Error& error) { + complete(Napi::GetErrorString(error)); + }; + Babylon::AppRuntime runtime{options}; + + runtime.Dispatch([&complete](Napi::Env env) { + auto global = env.Global(); + const auto hostHeaders = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostHeaders"); + const auto hostResponse = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostResponse"); + global.Set("Headers", hostHeaders); + global.Set("Response", hostResponse); + + Babylon::Polyfills::Fetch::Initialize(env); + EXPECT_TRUE(global.Get("Headers").StrictEquals(hostHeaders)); + EXPECT_TRUE(global.Get("Response").StrictEquals(hostResponse)); + + const auto installedFetch = global.Get("fetch"); + Babylon::Polyfills::Fetch::Initialize(env); + EXPECT_TRUE(global.Get("Headers").StrictEquals(hostHeaders)); + EXPECT_TRUE(global.Get("Response").StrictEquals(hostResponse)); + EXPECT_TRUE(global.Get("fetch").StrictEquals(installedFetch)); + complete({}); + }); + + const auto error = done.get_future().get(); + EXPECT_TRUE(error.empty()) << error; +} + +TEST(Fetch, ReplacesPartialOrNullHostClassesAsACompletePair) +{ + std::promise done; + std::atomic_bool completed{}; + const auto complete = [&done, &completed](std::string result) { + if (!completed.exchange(true)) + { + done.set_value(std::move(result)); + } + }; + + Babylon::AppRuntime::Options options{}; + options.UnhandledExceptionHandler = [&complete](const Napi::Error& error) { + complete(Napi::GetErrorString(error)); + }; + Babylon::AppRuntime runtime{options}; + + runtime.Dispatch([&complete](Napi::Env env) { + auto global = env.Global(); + const auto hostHeaders = Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostHeaders"); + global.Set("Headers", hostHeaders); + global.Set("Response", env.Null()); + + Babylon::Polyfills::Fetch::Initialize(env); + const auto installedHeaders = global.Get("Headers"); + const auto installedResponse = global.Get("Response"); + EXPECT_FALSE(installedHeaders.StrictEquals(hostHeaders)); + if (!installedHeaders.IsFunction() || !installedResponse.IsFunction()) + { + complete("Fetch::Initialize did not install a complete Headers/Response pair."); + return; + } + + const auto response = installedResponse.As().New({}); + EXPECT_TRUE(response.Get("headers").As().InstanceOf(installedHeaders.As())); + + Babylon::Polyfills::Fetch::Initialize(env); + EXPECT_TRUE(global.Get("Headers").StrictEquals(installedHeaders)); + EXPECT_TRUE(global.Get("Response").StrictEquals(installedResponse)); + complete({}); + }); + + const auto error = done.get_future().get(); + EXPECT_TRUE(error.empty()) << error; +} + +TEST(IndexedDB, InstallsBrowserGlobals) +{ + std::promise done; + Babylon::AppRuntime runtime{}; + + runtime.Dispatch([&done](Napi::Env env) { + try + { + auto global = env.Global(); + Babylon::Polyfills::IndexedDB::Initialize(env); + + EXPECT_TRUE(global.Get("globalThis").StrictEquals(global)); + auto indexedDB = global.Get("indexedDB"); + EXPECT_TRUE(indexedDB.IsObject()); + if (indexedDB.IsObject()) + { + EXPECT_TRUE(indexedDB.As().Get("open").IsFunction()); + } + EXPECT_TRUE(global.Get("IDBKeyRange").IsFunction()); + EXPECT_TRUE(global.Get("IDBTransaction").IsFunction()); + + Babylon::Polyfills::IndexedDB::Initialize(env); + EXPECT_TRUE(global.Get("indexedDB").StrictEquals(indexedDB)); + } + catch (const std::exception& error) + { + ADD_FAILURE() << "IndexedDB initialization failed: " << error.what(); + } + catch (...) + { + ADD_FAILURE() << "IndexedDB initialization failed"; + } + done.set_value(); + }); + + done.get_future().get(); +} + TEST(Console, Log) { Babylon::AppRuntime runtime{}; @@ -279,6 +696,106 @@ TEST(AppRuntime, DestroyDoesNotDeadlock) testThread.join(); } +#if defined(JSRUNTIMEHOST_TEST_WORKER) && !defined(__ANDROID__) && \ + (defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) || defined(JSR_NAPI_ENGINE_QUICKJS) || \ + defined(JSR_NAPI_ENGINE_V8) || defined(JSR_NAPI_ENGINE_HERMES)) +TEST(Worker, PreservesHostDOMException) +{ + std::promise done; + Babylon::AppRuntime runtime{}; + + runtime.Dispatch([&done](Napi::Env env) { + auto global = env.Global(); + const auto hostDOMException = + Napi::Function::New(env, [](const Napi::CallbackInfo&) {}, "HostDOMException"); + global.Set("DOMException", hostDOMException); + + Babylon::Polyfills::Worker::Options options{}; + options.ScriptRoot = std::filesystem::current_path().string(); + Babylon::Polyfills::Worker::Initialize(env, std::move(options)); + + // Worker is composed with independent browser polyfills. Installing + // its lifecycle/event glue must not invalidate exceptions created by + // IndexedDB (or another host implementation) before Worker starts. + EXPECT_TRUE(global.Get("DOMException").StrictEquals(hostDOMException)); + done.set_value(); + }); + + done.get_future().get(); +} + +TEST(Worker, WebPlatformTests) +{ + struct Result + { + bool Passed{}; + std::string Detail{}; + }; + + std::promise completion; + std::atomic_bool completed{false}; + + Babylon::AppRuntime::Options runtimeOptions{}; + runtimeOptions.UnhandledExceptionHandler = [&completion, &completed](const Napi::Error& error) { + if (!completed.exchange(true)) + { + completion.set_value({false, Napi::GetErrorString(error)}); + } + }; + + Babylon::AppRuntime runtime{std::move(runtimeOptions)}; + runtime.Dispatch([&completion, &completed](Napi::Env env) { + Babylon::Polyfills::Scheduling::Initialize(env); + Babylon::Polyfills::URL::Initialize(env); + + Babylon::Polyfills::Worker::Options options{}; + options.ScriptRoot = (std::filesystem::current_path() / "WebPlatformTests").string(); + options.ConsoleCallback = [](const char* message) { + std::cerr << "[Worker] " << message << std::endl; + }; + Babylon::Polyfills::Worker::Initialize(env, std::move(options)); + +#if defined(JSR_NAPI_ENGINE_JAVASCRIPTCORE) + // System JSC exposes the execution-time-limit hook used to interrupt + // a worker stuck in top-level evaluation. Other adapters currently + // terminate cooperatively between dispatches, so the infinite-loop + // WPT regression is intentionally JSC-only for now. + env.Global().Set("__jsrhCanInterruptWorker", Napi::Boolean::New(env, true)); +#else + env.Global().Set("__jsrhCanInterruptWorker", Napi::Boolean::New(env, false)); +#endif + + env.Global().Set("__jsrhWptDone", Napi::Function::New( + env, + [&completion, &completed](const Napi::CallbackInfo& info) { + if (!completed.exchange(true)) + { + completion.set_value({ + info[0].ToBoolean().Value(), + info[1].ToString().Utf8Value(), + }); + } + }, + "__jsrhWptDone")); + env.Global().Set("__jsrhWptProgress", Napi::Function::New( + env, + [](const Napi::CallbackInfo& info) { + std::cerr << "[Worker WPT] " << info[0].ToString().Utf8Value() << std::endl; + }, + "__jsrhWptProgress")); + }); + + Babylon::ScriptLoader loader{runtime}; + loader.LoadScript("app:///WebPlatformTests/runner.js"); + + auto future = completion.get_future(); + ASSERT_EQ(future.wait_for(std::chrono::seconds{90}), std::future_status::ready) + << "Worker WPT subset timed out"; + const auto result = future.get(); + EXPECT_TRUE(result.Passed) << result.Detail; +} +#endif + // The V8JSI Node-API shim does not implement napi_create_dataview / // napi_get_dataview_info (its DataView::New throws "TODO"), so this native test // only builds on the Chakra, V8, and JavaScriptCore backends. The size_t-width @@ -351,6 +868,102 @@ TEST(NodeApi, CreateDataViewRejectsOverflowingRange) } #endif +#if NAPI_VERSION >= 7 && !defined(JSRUNTIMEHOST_NAPI_ENGINE_JSI) && !defined(JSR_NAPI_ENGINE_CHAKRA) +TEST(NodeApi, ArrayBufferWrapperRefreshesInfoAfterDetach) +{ + // Regression: Napi::ArrayBuffer used to cache Data()/ByteLength() under + // the assumption that they could never change. Detachment disproves that + // assumption, and WebGPU keeps wrapper instances alive across unmap(). + Babylon::AppRuntime runtime{}; + std::promise result; + + runtime.Dispatch([&result](Napi::Env env) { + auto arrayBuffer = Napi::ArrayBuffer::New(env, 16); + const auto* beforeData = arrayBuffer.Data(); + const auto beforeLength = arrayBuffer.ByteLength(); + + const auto detachStatus = napi_detach_arraybuffer(env, arrayBuffer); + if (detachStatus != napi_ok) + { + // Frozen JavaScriptCore builds have no public detach primitive, + // and transitional JSC may refuse detachment after Node-API has + // exposed the backing pointer. Node-API explicitly permits those + // engine-specific detachability conditions. + napi_value pendingException{nullptr}; + napi_get_and_clear_last_exception(env, &pendingException); + result.set_value(0x3Fu); + return; + } + + bool detached{false}; + const auto detachedStatus = napi_is_detached_arraybuffer(env, arrayBuffer, &detached); + const auto* afterData = arrayBuffer.Data(); + const auto afterLength = arrayBuffer.ByteLength(); + + result.set_value( + (beforeData != nullptr ? 1u << 0 : 0u) | + (beforeLength == 16 ? 1u << 1 : 0u) | + (detachedStatus == napi_ok ? 1u << 2 : 0u) | + (detached ? 1u << 3 : 0u) | + (afterData == nullptr ? 1u << 4 : 0u) | + (afterLength == 0 ? 1u << 5 : 0u)); + }); + + EXPECT_EQ(0x3Fu, result.get_future().get()) + << "bits: initial data, initial length, detached status, detached state, " + "detached data, detached length"; +} +#endif + +#if !defined(JSRUNTIMEHOST_NAPI_ENGINE_JSI) +TEST(NodeApi, ThrowApisReturnOkAndLeaveExceptionPending) +{ + // Node-API's throw functions return napi_ok when the exception was + // successfully scheduled. In particular, they must not return + // napi_pending_exception: node-addon-api treats any non-ok return from + // Error::ThrowAsJavaScriptException as a second C++ error while translating + // the original exception. + Babylon::AppRuntime runtime{}; + std::promise result; + + runtime.Dispatch([&result](Napi::Env env) { + napi_env nenv{env}; + bool passed{true}; + + const auto clearPending = [&]() { + bool pending{false}; + napi_value exception{nullptr}; + passed = passed && + napi_is_exception_pending(nenv, &pending) == napi_ok && + pending && + napi_get_and_clear_last_exception(nenv, &exception) == napi_ok && + exception != nullptr; + }; + + napi_value message{nullptr}; + napi_value error{nullptr}; + passed = passed && + napi_create_string_utf8(nenv, "boom", NAPI_AUTO_LENGTH, &message) == napi_ok && + napi_create_error(nenv, nullptr, message, &error) == napi_ok && + napi_throw(nenv, error) == napi_ok; + clearPending(); + + passed = passed && napi_throw_error(nenv, "ERR_TEST", "boom") == napi_ok; + clearPending(); + + passed = passed && napi_throw_type_error(nenv, "ERR_TEST", "boom") == napi_ok; + clearPending(); + + passed = passed && napi_throw_range_error(nenv, "ERR_TEST", "boom") == napi_ok; + clearPending(); + + result.set_value(passed); + }); + + EXPECT_TRUE(result.get_future().get()); +} +#endif + // The V8JSI Node-API shim does not expose napi_get_value_string_utf16, so this // native test only builds on the Chakra, V8, and JavaScriptCore backends. #if !defined(JSRUNTIMEHOST_NAPI_ENGINE_JSI) @@ -405,6 +1018,19 @@ TEST(NodeApi, GetValueStringUtf16HandlesZeroBufsize) int RunTests() { +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) + ConfigureNodeApiTests(); +#endif testing::InitGoogleTest(); +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) + node_api_tests::RegisterNodeApiTests(); +#endif return RUN_ALL_TESTS(); } +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) +void SetNodeApiTestEnvironment(AAssetManager* assetManager, const std::filesystem::path& baseDir) +{ + OverrideAssetManager() = assetManager; + OverrideBaseDir() = baseDir; +} +#endif diff --git a/Tests/UnitTests/Shared/Shared.h b/Tests/UnitTests/Shared/Shared.h index b1610fa8..e5fdd7e8 100644 --- a/Tests/UnitTests/Shared/Shared.h +++ b/Tests/UnitTests/Shared/Shared.h @@ -1,3 +1,15 @@ #pragma once -int RunTests(); \ No newline at end of file +#include + +int RunTests(); + +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) +#include + +// Supplies the in-process Node-API test harness with a native AssetManager and a writable base +// directory (derived from the instrumentation Context in the JNI layer). Without this the harness +// falls back to android::global::GetAppContext(), whose JNI global ref is not valid during the +// instrumented run and aborts with "use of deleted global reference". +void SetNodeApiTestEnvironment(AAssetManager* assetManager, const std::filesystem::path& baseDir); +#endif diff --git a/Tests/UnitTests/WebPlatformTests/LICENSE.md b/Tests/UnitTests/WebPlatformTests/LICENSE.md new file mode 100644 index 00000000..39c46d03 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/LICENSE.md @@ -0,0 +1,11 @@ +# The 3-Clause BSD License + +Copyright © web-platform-tests contributors + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Tests/UnitTests/WebPlatformTests/README.md b/Tests/UnitTests/WebPlatformTests/README.md new file mode 100644 index 00000000..23893b34 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/README.md @@ -0,0 +1,43 @@ +# Focused Worker conformance regressions + +`UPSTREAM_REVISION` records the exact +[web-platform-tests/wpt](https://github.com/web-platform-tests/wpt) commit used +when the broader suite is run outside this repository. The WPT checkout and +`testharness.js` are deliberately not vendored. + +`workers/focused-api.js` is a small host-native port of eight assertions that +found real gaps in the initial implementation: worker-global identity and +readonly behavior, EventTarget removal/targeting, `onmessage` normalization and +dispatch, `postMessage()`'s return value, and zero-argument `importScripts()`. +The source links are kept in that file. `workers/support/Worker-structure-message.js` +and `workers/constructors/Worker/terminate.js` are two additional focused WPT +ports for structured transfer and termination. + +The remaining cases are regressions adapted from browser-engine fixes; each +test carries the exact upstream link: + +- `workers/support/WorkerGlobalScope-close.js` checks that `close()` preserves + same-task messages/errors while discarding later tasks. +- `workers/support/Worker-early-message.js` checks the startup queue and the + complementary terminate-before-start cleanup path. +- `workers/support/Worker-run-forever.js` checks JSC interruption during + top-level evaluation, based on WPT's terminate-during-evaluation regression. +- `workers/support/Worker-termination-stress.js` repeats shutdown with an + inbound task pending, guarding the cross-thread destruction pattern fixed by + WebKit in July 2026. +- `runner.js` checks that a throwing structured-clone getter propagates the + original exception and leaves transferables attached, guarding Servo's 2025 + exception-clearing regression. +- `workers/support/visualization-worker-smoke.js` is a small, non-proprietary + reproduction of the deployed rebeckerspecialties visualization worker's + startup contract. It constructs a named module-compatible worker from a + WHATWG `URL`, opens the IndexedDB cache used by `pipelineCreate`, queues the + interop startup messages, and sends multiple `Date`/`Map`/`Set`-rich + playback streams through structured clone. + +`runner.js` launches these focused tests directly, without WPT infrastructure, +and adapts the document-side structured-clone, transfer, startup, close, and +termination checks to the JsRuntimeHost unit-test host. The infinite-evaluation +case runs only when the selected engine has a native execution-interrupt hook. +The visualization smoke case runs last so it exercises a fresh worker after +the lifecycle stress cases. diff --git a/Tests/UnitTests/WebPlatformTests/UPSTREAM_REVISION b/Tests/UnitTests/WebPlatformTests/UPSTREAM_REVISION new file mode 100644 index 00000000..b7616c05 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/UPSTREAM_REVISION @@ -0,0 +1 @@ +4809b72f863e05ab1df710d3390547dd86694239 diff --git a/Tests/UnitTests/WebPlatformTests/runner.js b/Tests/UnitTests/WebPlatformTests/runner.js new file mode 100644 index 00000000..a4abf3c9 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/runner.js @@ -0,0 +1,504 @@ +(() => { + const failures = []; + const cases = [ + "/workers/focused-api.js" + ]; + + let index = 0; + let timer = 0; + + function progress(name) { + if (typeof __jsrhWptProgress === "function") __jsrhWptProgress(name); + } + + function fail(name, detail) { + failures.push(name + ": " + detail); + } + + function finish() { + progress("finish"); + clearTimeout(timer); + __jsrhWptDone(failures.length === 0, failures.join("\n")); + } + + function runStructuredMessageCase() { + const name = "/workers/support/Worker-structure-message.js"; + progress(name); + const worker = new Worker(name); + const input = new ArrayBuffer(20); + let sawPass = false; + + timer = setTimeout(() => { + worker.terminate(); + fail(name, "timed out"); + finish(); + }, 10000); + + worker.onerror = event => { + clearTimeout(timer); + worker.terminate(); + fail(name, event.message || "worker error"); + finish(); + }; + + worker.onmessage = event => { + if (typeof event.data === "string") { + sawPass = event.data.indexOf("PASS:") === 0; + if (!sawPass) fail(name, event.data); + return; + } + + clearTimeout(timer); + const value = event.data; + if (!sawPass || !value || value.operation !== "find-edges" || + !(value.input instanceof ArrayBuffer) || value.input.byteLength !== 20 || + value.threshold !== 0.6) { + fail(name, "structured clone did not preserve the WPT payload"); + } + worker.terminate(); + runCloseCases(); + }; + + worker.postMessage({ operation: "find-edges", input, threshold: 0.6 }, [input]); + // Current WebKitGTK exposes the standards-track detached getter before + // its plain ArrayBuffer byteLength reflection catches up. Either signal + // confirms the sender backing store is detached; the Node-API adapter + // independently validates detached state before and after transfer. + if (input.detached !== true && input.byteLength !== 0) { + fail(name, "transfer did not detach the sender ArrayBuffer"); + } + } + + function runThrowingGetterCase() { + const name = "structured clone preserves a throwing getter exception"; + progress(name); + const worker = new Worker("/workers/support/Worker-early-message.js"); + const expected = new Error("getter sentinel"); + const transfer = new ArrayBuffer(8); + let thrown; + + // Servo used to clear the pending JS exception raised while reading an + // enumerable property and replace it with DataCloneError. Serialization + // must propagate the original exception and must not detach transferables + // after serialization has already failed. + // https://github.com/servo/servo/commit/26f4da824907946569fb673a249a2c9035c1d1e4 + try { + worker.postMessage({ + get value() { + throw expected; + } + }, [transfer]); + } catch (error) { + thrown = error; + } + + worker.terminate(); + if (thrown !== expected) { + fail(name, "postMessage replaced or swallowed the getter exception"); + } + if (transfer.detached === true || transfer.byteLength === 0) { + fail(name, "postMessage detached a transfer after serialization failed"); + } + runStructuredMessageCase(); + } + + function runCloseCases() { + const name = "/workers/support/WorkerGlobalScope-close.js"; + const modes = ['close', 'closeWithPendingEvents', 'closeWithError']; + let closeIndex = 0; + + function runNextClose() { + clearTimeout(timer); + if (closeIndex === modes.length) { + runEarlyMessageCase(); + return; + } + + const mode = modes[closeIndex++]; + progress(name + " / " + mode); + const worker = new Worker(name); + let messages = 0; + let settled = false; + + function complete() { + if (settled) return; + settled = true; + clearTimeout(timer); + worker.terminate(); + runNextClose(); + } + + worker.onmessage = event => { + messages++; + if (mode === 'close' && messages === 1 && event.data === 'Should be delivered') { + // Parent-to-worker tasks posted after close() must be discarded too. + worker.postMessage('Should not be observed'); + clearTimeout(timer); + timer = setTimeout(() => { + if (messages !== 1) fail(name + ' / ' + mode, 'a later task was delivered'); + complete(); + }, 100); + return; + } + fail(name + ' / ' + mode, 'unexpected message: ' + String(event.data)); + complete(); + }; + + worker.onerror = event => { + if (mode === 'closeWithError') { + if (String(event.message).indexOf('Error after close should be delivered') === -1) { + fail(name + ' / ' + mode, event.message || 'wrong worker error'); + } + } else { + fail(name + ' / ' + mode, event.message || 'worker error'); + } + complete(); + }; + + timer = setTimeout(() => { + if (mode !== 'closeWithPendingEvents') { + fail(name + ' / ' + mode, 'timed out'); + } + complete(); + }, mode === 'closeWithPendingEvents' ? 150 : 5000); + worker.postMessage(mode); + } + + runNextClose(); + } + + function runEarlyMessageCase() { + const name = "/workers/support/Worker-early-message.js"; + progress(name); + const worker = new Worker(name); + const payload = { phase: 'queued-before-start' }; + let postMessageReturned = false; + + timer = setTimeout(() => { + worker.terminate(); + fail(name, 'timed out'); + runEarlyTerminationCase(); + }, 5000); + + worker.onerror = event => { + clearTimeout(timer); + worker.terminate(); + fail(name, event.message || 'worker error'); + runEarlyTerminationCase(); + }; + worker.onmessage = event => { + clearTimeout(timer); + if (!postMessageReturned) fail(name, 'message delivery was synchronous'); + if (!event.data || event.data.phase !== payload.phase) { + fail(name, 'message queued during startup was lost or corrupted'); + } + worker.terminate(); + runEarlyTerminationCase(); + }; + + worker.postMessage(payload); + postMessageReturned = true; + } + + function runEarlyTerminationCase() { + const name = "/workers/support/Worker-early-message.js / terminate-before-start"; + progress(name); + const worker = new Worker("/workers/support/Worker-early-message.js"); + let delivered = false; + + worker.onmessage = () => { delivered = true; }; + worker.onerror = () => { delivered = true; }; + worker.postMessage('must be discarded'); + worker.terminate(); + + timer = setTimeout(() => { + if (delivered) fail(name, 'a pre-start event ran after terminate()'); + runTerminationDuringEvaluationCase(); + }, 150); + } + + function runTerminationDuringEvaluationCase() { + const name = "/workers/support/Worker-run-forever.js"; + progress(name); + if (globalThis.__jsrhCanInterruptWorker !== true) { + runTerminationStressCase(); + return; + } + + const worker = new Worker(name); + timer = setTimeout(() => { + worker.terminate(); + fail(name, 'worker did not begin top-level evaluation'); + runTerminationStressCase(); + }, 5000); + + worker.onerror = event => { + clearTimeout(timer); + worker.terminate(); + fail(name, event.message || 'worker error'); + runTerminationStressCase(); + }; + worker.onmessage = event => { + if (event.data !== 'start') { + fail(name, 'unexpected message before termination'); + return; + } + clearTimeout(timer); + worker.terminate(); + timer = setTimeout(runTerminationStressCase, 150); + }; + } + + function runTerminationStressCase() { + const name = "/workers/support/Worker-termination-stress.js"; + let iteration = 0; + + function runIteration() { + clearTimeout(timer); + if (iteration === 20) { + runTerminateCase(); + return; + } + + const current = iteration; + progress(name + " / iteration " + current); + const worker = new Worker(name); + let retired = false; + timer = setTimeout(() => { + retired = true; + worker.terminate(); + fail(name + ' / iteration ' + current, 'timed out'); + iteration++; + runIteration(); + }, 5000); + + worker.onerror = event => { + if (retired) { + fail(name + ' / iteration ' + current, 'error arrived after terminate()'); + return; + } + clearTimeout(timer); + retired = true; + worker.terminate(); + fail(name + ' / iteration ' + current, event.message || 'worker error'); + iteration++; + runIteration(); + }; + worker.onmessage = event => { + if (retired) { + fail(name + ' / iteration ' + current, 'message arrived after terminate()'); + return; + } + if (event.data !== 'ready') { + fail(name + ' / iteration ' + current, 'unexpected startup message'); + return; + } + + clearTimeout(timer); + // Leave an inbound task pending while shutdown starts. Repetition is + // intentional: WebKit's 2026 regression needed a lifecycle stress + // test to expose cross-thread destruction of worker-owned state. + worker.postMessage({ iteration: current }); + retired = true; + worker.terminate(); + iteration++; + timer = setTimeout(runIteration, 0); + }; + } + + runIteration(); + } + + function runTerminateCase() { + const name = "/workers/constructors/Worker/terminate.js"; + progress(name); + const worker = new Worker(name); + let messages = 0; + + worker.onerror = event => { + clearTimeout(timer); + worker.terminate(); + fail(name, event.message || "worker error"); + finish(); + }; + worker.onmessage = () => { messages++; }; + + timer = setTimeout(() => { + const expected = messages; + // Adapt the WPT document harness: hold the parent turn while the Worker + // queues additional messages, then verify terminate() discards them. + const start = Date.now(); + while (Date.now() - start < 50) {} + worker.terminate(); + + timer = setTimeout(() => { + if (messages !== expected) { + fail(name, "message events queued before terminate() were delivered"); + } + runVisualizationStartupCase(); + }, 100); + }, 100); + } + + function runVisualizationStartupCase() { + const name = "/workers/support/visualization-worker-smoke.js"; + progress(name); + const workerUrl = new URL(name, "app:///"); + const worker = new Worker(workerUrl, { + type: "module", + name: "github-portfolio" + }); + const config = { owner: "BabylonJS", repo: "Babylon.js" }; + const expectedKey = config.owner + "/" + config.repo; + let sawBootstrap = false; + let sawPipeline = false; + let sawData = false; + let sawPlaybackDate = false; + const initialTypes = new Set(); + + function completeIfReady() { + if (!sawBootstrap || !sawPipeline || !sawData || !sawPlaybackDate || + initialTypes.size !== 5) { + return; + } + clearTimeout(timer); + worker.terminate(); + finish(); + } + + timer = setTimeout(() => { + worker.terminate(); + fail(name, "timed out during app-derived startup"); + finish(); + }, 10000); + + worker.onerror = event => { + clearTimeout(timer); + worker.terminate(); + fail(name, event.message || "worker error"); + finish(); + }; + worker.onmessageerror = () => { + clearTimeout(timer); + worker.terminate(); + fail(name, "messageerror while cloning a visualization stream"); + finish(); + }; + worker.onmessage = event => { + const value = event.data; + if (!value || typeof value.type !== "string") return; + switch (value.type) { + case "integrationError": + fail(name, "app-derived worker integration failed: " + value.message); + clearTimeout(timer); + worker.terminate(); + finish(); + return; + case "bootstrap": + if (value.workerName !== "github-portfolio" || + !value.location || value.location.protocol !== "app:" || + value.location.pathname !== name || + !value.globals || value.globals.indexedDB !== "object" || + value.globals.fetch !== "function" || + value.globals.AbortController !== true || + value.globals.AbortControllerType !== "function" || + value.globals.AbortControllerPrimitiveReason !== true || + value.globals.TextEncoder !== true || + value.globals.TextEncoderType !== "function" || + value.globals.BlobType !== "function" || + value.globals.URLType !== "function" || + value.globals.NativeConstructorStatics !== true || + value.globals.NativeConstructorInstanceof !== true || + value.globals.ReadableStream !== "function" || + value.globals.Response !== "function" || + value.globals.ResponseError !== true || + value.globals.DecompressionStream !== "function" || + value.globals.blobStream !== "function") { + fail(name, "worker-global bootstrap surface is incomplete: " + + JSON.stringify(value)); + } + sawBootstrap = true; + break; + case "pipelineCreated": + if (value.key !== expectedKey || value.cacheHydrated !== true) { + fail(name, "relative/gzip cache hydration or IndexedDB pipeline setup failed"); + } + sawPipeline = true; + worker.postMessage({ type: "pipelinePlay", key: expectedKey }); + break; + case "clone": + case "currentDate": + case "startFrom": + case "playRate": + case "configs": + if (value.type === "currentDate" && value.date instanceof Date) { + if (value.date.toISOString() !== "2026-07-21T00:00:00.000Z") { + fail(name, "playback Date changed across postMessage"); + } + sawPlaybackDate = true; + } else { + initialTypes.add(value.type); + } + break; + case "data": { + const streams = value.data; + if (!Array.isArray(streams) || streams.length !== 3 || + !(value.updatedItems instanceof Map) || + value.updatedItems.size !== 384 || + !(value.metadata.activeTypes instanceof Set) || + value.metadata.activeTypes.size !== 3 || + !(streams[0].currentDate instanceof Date) || + streams[0].currentDate !== streams[1].currentDate || + streams[0].items[0].self !== streams[0].items[0]) { + fail(name, "multi-stream structured clone lost Date/Map/Set/alias fidelity"); + } + sawData = true; + break; + } + } + completeIfReady(); + }; + + // Match the interop's startup protocol: messages may be queued while the + // module-compatible bundle is still evaluating. + worker.postMessage({ type: "pipelineCreate", config }); + worker.postMessage({ type: "subscribe" }); + } + + function next() { + clearTimeout(timer); + if (index === cases.length) { + runThrowingGetterCase(); + return; + } + + const name = cases[index++]; + progress(name); + const worker = new Worker(name); + timer = setTimeout(() => { + worker.terminate(); + fail(name, "timed out"); + next(); + }, 10000); + + worker.onerror = event => { + clearTimeout(timer); + worker.terminate(); + fail(name, event.message || "worker error"); + next(); + }; + + worker.onmessage = event => { + const value = event.data; + if (!value || value.type !== "focused-results") return; + clearTimeout(timer); + for (const test of value.failures || []) { + fail(name + " / " + test.name, test.message || "failed"); + } + worker.terminate(); + next(); + }; + } + + next(); +})(); diff --git a/Tests/UnitTests/WebPlatformTests/workers/constructors/Worker/terminate.js b/Tests/UnitTests/WebPlatformTests/workers/constructors/Worker/terminate.js new file mode 100644 index 00000000..6adb3786 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/constructors/Worker/terminate.js @@ -0,0 +1,4 @@ +(function f() { + postMessage(1); + setTimeout(f, 0); +})(); diff --git a/Tests/UnitTests/WebPlatformTests/workers/focused-api.js b/Tests/UnitTests/WebPlatformTests/workers/focused-api.js new file mode 100644 index 00000000..fabc5e6a --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/focused-api.js @@ -0,0 +1,85 @@ +// These are small ports of only the Worker WPT assertions that exposed gaps in +// JsRuntimeHost. The upstream files remain pinned by UPSTREAM_REVISION and are +// run from an external WPT checkout during broader conformance work. +// +// https://github.com/web-platform-tests/wpt/blob/4809b72f863e05ab1df710d3390547dd86694239/workers/interfaces/DedicatedWorkerGlobalScope/EventTarget.worker.js +// https://github.com/web-platform-tests/wpt/blob/4809b72f863e05ab1df710d3390547dd86694239/workers/interfaces/DedicatedWorkerGlobalScope/onmessage.worker.js +// https://github.com/web-platform-tests/wpt/blob/4809b72f863e05ab1df710d3390547dd86694239/workers/interfaces/DedicatedWorkerGlobalScope/postMessage/return-value.worker.js +// https://github.com/web-platform-tests/wpt/blob/4809b72f863e05ab1df710d3390547dd86694239/workers/interfaces/WorkerGlobalScope/self.any.js +// https://github.com/web-platform-tests/wpt/blob/4809b72f863e05ab1df710d3390547dd86694239/workers/interfaces/WorkerUtils/importScripts/001.worker.js + +const failures = []; + +function check(name, callback) { + try { + callback(); + } catch (error) { + failures.push({ + name, + message: error && error.message ? error.message : String(error) + }); + } +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +check('self is the worker global', () => { + assert(self === globalThis, 'self and globalThis differ'); + assert(self instanceof WorkerGlobalScope, 'self is not a WorkerGlobalScope'); +}); + +check('self is readonly', () => { + const original = self; + self = 1; + assert(self === original, 'assigning self replaced the worker global'); +}); + +check('removeEventListener removes a capturing listener', () => { + let calls = 0; + function listener() { + calls++; + removeEventListener('message', listener, true); + } + addEventListener('message', listener, true); + dispatchEvent(new Event('message')); + dispatchEvent(new Event('message')); + assert(calls === 1, 'listener ran ' + calls + ' times'); +}); + +check('dispatched event targets the worker global', () => { + let target; + function listener(event) { + target = event.target; + } + addEventListener('message', listener, true); + dispatchEvent(new Event('message')); + removeEventListener('message', listener, true); + assert(target === self, 'event.target was not self'); +}); + +check('onmessage rejects primitive handlers', () => { + self.onmessage = 1; + assert(self.onmessage === null, 'primitive handler did not normalize to null'); +}); + +check('onmessage invokes function handlers', () => { + let called = false; + self.onmessage = () => { + called = true; + }; + dispatchEvent(new Event('message')); + assert(called, 'function handler was not invoked'); +}); + +check('postMessage returns undefined', () => { + assert(postMessage({ type: 'probe' }) === undefined, + 'postMessage returned a non-undefined value'); +}); + +check('importScripts with no arguments is a no-op', () => { + importScripts(); +}); + +postMessage({ type: 'focused-results', failures }); diff --git a/Tests/UnitTests/WebPlatformTests/workers/support/Worker-early-message.js b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-early-message.js new file mode 100644 index 00000000..8c5389fa --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-early-message.js @@ -0,0 +1,12 @@ +'use strict'; + +// WPT added this ordering regression after browsers raced Worker startup with +// an immediate postMessage. Gecko later fixed the complementary dead-worker +// path so pre-start/pending events are cleared when initialization never runs. +// https://github.com/web-platform-tests/wpt/commit/2060611f666a08629a55d5d594a0188c49c9ef5e +// https://github.com/mozilla/gecko-dev/commit/fd5b902f9f2ee1f9ed90e90a5843808422382987 +// https://github.com/mozilla/gecko-dev/commit/b68bc791d026930001a3afbd2b3139ba58822435 +const initializationEnds = Date.now() + 25; +while (Date.now() < initializationEnds) {} + +onmessage = event => postMessage(event.data); diff --git a/Tests/UnitTests/WebPlatformTests/workers/support/Worker-run-forever.js b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-run-forever.js new file mode 100644 index 00000000..97439e7c --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-run-forever.js @@ -0,0 +1,9 @@ +'use strict'; + +// Used by WPT to prove terminate() can interrupt top-level script evaluation. +// Chromium's later termination fix is a reminder that callbacks must re-check +// their execution context rather than dereference it after shutdown begins. +// https://github.com/web-platform-tests/wpt/commit/d1c32457e97d9147803705abc4ffac424733dd8d +// https://github.com/chromium/chromium/commit/3115ace01f79a4a1181a82d70f7777a9ddafd8c2 +postMessage('start'); +while (true) {} diff --git a/Tests/UnitTests/WebPlatformTests/workers/support/Worker-structure-message.js b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-structure-message.js new file mode 100644 index 00000000..81cd9824 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-structure-message.js @@ -0,0 +1,15 @@ +self.onmessage = function(evt) { + if (evt.data.operation == 'find-edges' && + ArrayBuffer.prototype.isPrototypeOf(evt.data.input) && + evt.data.input.byteLength == 20 && + evt.data.threshold == 0.6) { + self.postMessage("PASS: Worker receives correct structure message."); + self.postMessage({ + operation: evt.data.operation, + input: evt.data.input, + threshold: evt.data.threshold + }); + } + else + self.postMessage("FAIL: Worker receives error structure message."); +} diff --git a/Tests/UnitTests/WebPlatformTests/workers/support/Worker-termination-stress.js b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-termination-stress.js new file mode 100644 index 00000000..18dc72dc --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/support/Worker-termination-stress.js @@ -0,0 +1,8 @@ +'use strict'; + +// WebKit fixed a terminate-time UAF caused by destroying a worker-owned JS +// object on the main thread when a cross-thread postTask failed. The runner +// repeatedly terminates with an inbound task pending to exercise that shape. +// https://github.com/WebKit/WebKit/commit/4aaa3c1477e296e67b03e1461479b8caf57c37dd +postMessage('ready'); +onmessage = event => postMessage(event.data); diff --git a/Tests/UnitTests/WebPlatformTests/workers/support/WorkerGlobalScope-close.js b/Tests/UnitTests/WebPlatformTests/workers/support/WorkerGlobalScope-close.js new file mode 100644 index 00000000..0a660c56 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/support/WorkerGlobalScope-close.js @@ -0,0 +1,24 @@ +'use strict'; + +// Adapted from the current WPT close() coverage. In particular, close() must +// discard later tasks without suppressing messages or errors produced by the +// task that called close(). +// https://github.com/web-platform-tests/wpt/blob/57e48fbf38927e10e86e049b9b03c0e7a1686878/workers/support/WorkerGlobalScope-close.js +onmessage = event => { + switch (event.data) { + case 'close': + close(); + postMessage('Should be delivered'); + setTimeout(() => postMessage('Should not be delivered'), 0); + break; + + case 'closeWithPendingEvents': + setTimeout(() => postMessage('Pending event should be discarded'), 0); + close(); + break; + + case 'closeWithError': + close(); + throw new Error('Error after close should be delivered'); + } +}; diff --git a/Tests/UnitTests/WebPlatformTests/workers/support/visualization-worker-cache.json b/Tests/UnitTests/WebPlatformTests/workers/support/visualization-worker-cache.json new file mode 100644 index 00000000..e22670d1 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/support/visualization-worker-cache.json @@ -0,0 +1 @@ +{"source":"relative-worker-fetch","revision":1} diff --git a/Tests/UnitTests/WebPlatformTests/workers/support/visualization-worker-smoke.js b/Tests/UnitTests/WebPlatformTests/workers/support/visualization-worker-smoke.js new file mode 100644 index 00000000..88973fc1 --- /dev/null +++ b/Tests/UnitTests/WebPlatformTests/workers/support/visualization-worker-smoke.js @@ -0,0 +1,219 @@ +'use strict'; + +// A small, non-proprietary reproduction of the Worker contract used by +// rebeckerspecialties/webapp's GithubPortfolio.worker.ts at commit +// 3765b131bca72057a4331ed7f466b9121cf24d1f. The deployed app constructs an +// ES-module Worker, creates an IndexedDB-backed pipeline, and moves multiple +// Date/Map-rich playback streams through postMessage. + +const databaseOpenPromise = new Promise((resolve, reject) => { + const request = indexedDB.open('visualization-worker-smoke'); + request.onupgradeneeded = () => { + if (!request.result.objectStoreNames.contains('rebeckerLoaderCacheStore')) { + request.result.createObjectStore('rebeckerLoaderCacheStore') + .put({ schema: 1 }, '__schema__'); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); +}); + +const gzipBytes = new Uint8Array([ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 171, 86, 42, 74, 77, 74, + 77, 206, 78, 45, 242, 201, 79, 76, 73, 45, 114, 78, 76, 206, + 72, 13, 46, 201, 47, 74, 85, 178, 170, 86, 74, 6, 241, 172, + 12, 64, 204, 148, 196, 146, 68, 37, 171, 104, 67, 29, 35, 29, + 227, 216, 90, 29, 168, 148, 33, 146, 148, 137, 142, 169, 142, + 89, 108, 109, 109, 45, 0, 105, 118, 117, 71, 84, 0, 0, 0 +]); + +const cacheHydrationPromise = (async () => { + // Match the production cache path: its `${owner}.${repo}.gz` request is + // relative to the module worker bundle, rather than an absolute app URL. + const metadataResponse = await fetch('./visualization-worker-cache.json'); + const metadata = await metadataResponse.json(); + + const compressedBlob = new Blob([gzipBytes]); + const decompressedStream = compressedBlob.stream().pipeThrough( + new DecompressionStream('gzip')); + const decompressedBlob = await new Response(decompressedStream).blob(); + const prerecorded = JSON.parse(await decompressedBlob.text()); + + // WPT requires truncated, checksum-corrupt, and trailing-junk inputs to + // reject instead of returning a plausible prefix. Keep the native inflater + // from adopting that historical implementation failure mode. + // https://github.com/web-platform-tests/wpt/blob/57e48fbf38927e10e86e049b9b03c0e7a1686878/compression/decompression-corrupt-input.any.js + for (const invalidBytes of [ + gzipBytes.slice(0, gzipBytes.length - 4), + new Uint8Array([...gzipBytes, 0]) + ]) { + let rejected = false; + try { + const invalid = new Blob([invalidBytes]); + await new Response(invalid.stream().pipeThrough( + new DecompressionStream('gzip'))).arrayBuffer(); + } catch (_) { + rejected = true; + } + if (!rejected) { + throw new Error('invalid gzip cache data was accepted'); + } + } + + return { metadata, prerecorded }; +})(); + +const databasePromise = Promise.all([ + databaseOpenPromise, + cacheHydrationPromise +]).then(async ([database, cache]) => { + // Match IndexedDBCache.setMany(): all hydrated repository records are + // queued in one readwrite transaction and completion, not individual put + // success events, resolves the batch. + await new Promise((resolve, reject) => { + const transaction = database.transaction( + 'rebeckerLoaderCacheStore', 'readwrite'); + const store = transaction.objectStore('rebeckerLoaderCacheStore'); + for (const [key, value] of Object.entries( + cache.prerecorded.rebeckerLoaderCacheStore)) { + store.put(value, key); + } + transaction.oncomplete = resolve; + transaction.onerror = () => reject(transaction.error); + transaction.onabort = () => reject(transaction.error); + }); + + return { database, cache }; +}); + +const keyFor = config => `${config.owner}/${config.repo}`; +const itemsPerStream = 128; +const makeStream = (recordType, weight) => ({ + recordType, + items: Array.from({ length: itemsPerStream }, (_, index) => ({ + key: `${recordType}-${index}`, + weight + })) +}); + +onmessage = event => { + const message = event.data; + switch (message.type) { + case 'pipelineCreate': + databasePromise.then(({ database, cache }) => { + const transaction = database.transaction( + 'rebeckerLoaderCacheStore', 'readwrite'); + transaction.objectStore('rebeckerLoaderCacheStore').put({ + currentDate: new Date('2026-07-21T00:00:00.000Z'), + streams: [ + makeStream('commits', 3), + makeStream('pulls', 2), + makeStream('issues', 1) + ] + }, keyFor(message.config)); + transaction.oncomplete = () => { + postMessage({ + type: 'pipelineCreated', + key: keyFor(message.config), + cacheHydrated: + cache.metadata.source === 'relative-worker-fetch' && + cache.prerecorded.rebeckerLoaderCacheStore['cache:1'] + .data.length === 3 + }); + }; + }).catch(error => { + postMessage({ + type: 'integrationError', + message: error && error.message ? error.message : String(error) + }); + }); + return; + + case 'subscribe': + postMessage({ type: 'clone', value: false }); + postMessage({ type: 'currentDate', date: null }); + postMessage({ type: 'startFrom', date: undefined }); + postMessage({ type: 'playRate', playRate: 24 }); + postMessage({ type: 'configs', configs: [] }); + return; + + case 'pipelinePlay': + databasePromise.then(({ database }) => { + const request = database.transaction('rebeckerLoaderCacheStore') + .objectStore('rebeckerLoaderCacheStore').get(message.key); + request.onsuccess = () => { + const cached = request.result; + const updatedItems = new Map(); + for (const stream of cached.streams) { + for (const item of stream.items) updatedItems.set(item.key, item); + } + // Preserve an alias across the structured-clone graph, just as the + // production diff envelope can reference an item from both data and + // updatedItems. + cached.streams[0].items[0].self = cached.streams[0].items[0]; + postMessage({ + type: 'data', + data: cached.streams.map(stream => ({ + ...stream, + currentDate: cached.currentDate + })), + updatedItems, + metadata: { + activeTypes: new Set(cached.streams.map(stream => stream.recordType)) + } + }); + postMessage({ type: 'currentDate', date: cached.currentDate }); + }; + }).catch(error => { + postMessage({ + type: 'integrationError', + message: error && error.message ? error.message : String(error) + }); + }); + return; + } +}; + +const abortController = new AbortController(); +abortController.abort('visualization-stop'); + +postMessage({ + type: 'bootstrap', + workerName: name, + location: { + href: location.href, + protocol: location.protocol, + pathname: location.pathname + }, + globals: { + indexedDB: typeof indexedDB, + fetch: typeof fetch, + // The JavaScriptCore Node-API adapter reports some native constructors as + // typeof "object"; exercise their browser behavior instead of asserting a + // backend-specific typeof result. + AbortController: + new AbortController().signal.aborted === false, + AbortControllerType: typeof AbortController, + AbortControllerPrimitiveReason: + abortController.signal.aborted === true && + abortController.signal.reason === 'visualization-stop', + TextEncoder: + new TextEncoder().encode('A')[0] === 65, + TextEncoderType: typeof TextEncoder, + BlobType: typeof Blob, + URLType: typeof URL, + NativeConstructorStatics: + URL.canParse('app:///cache.gz') && + AbortSignal.abort().aborted === true, + NativeConstructorInstanceof: + new Blob([]) instanceof Blob && + new TextEncoder() instanceof TextEncoder, + ReadableStream: typeof ReadableStream, + Response: typeof Response, + ResponseError: + Response.error().status === 0 && + Response.error().type === 'error', + DecompressionStream: typeof DecompressionStream, + blobStream: typeof Blob.prototype.stream + } +}); diff --git a/Tests/package-lock.json b/Tests/package-lock.json index eccda69f..50d2b9cc 100644 --- a/Tests/package-lock.json +++ b/Tests/package-lock.json @@ -97,6 +97,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2395,6 +2396,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2421,6 +2423,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2709,6 +2712,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5207,6 +5211,7 @@ "integrity": "sha512-EW8af29ak8Oaf4T8k8YsajjrDBDYgnKZ5er6ljWFJsXABfTNowQfvHLftwcepVgdz+IoLSdEAbBiM9DFXoll9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -5256,6 +5261,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1",