From 3bfa14abf8eca8cbc170d2f04bde9e0179017e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 11 Dec 2025 09:51:23 +0100 Subject: [PATCH 1/2] feat(example): add Rive to Reanimated shared value demo Demonstrates Rive animation driving React Native UI via data binding listeners. --- example/assets/rive/bouncing_ball.riv | Bin 0 -> 376 bytes .../src/pages/RiveToReactNativeExample.tsx | 220 ++++++++++++++++++ expo-example/assets/rive/bouncing_ball.riv | Bin 0 -> 376 bytes 3 files changed, 220 insertions(+) create mode 100644 example/assets/rive/bouncing_ball.riv create mode 100644 example/src/pages/RiveToReactNativeExample.tsx create mode 100644 expo-example/assets/rive/bouncing_ball.riv diff --git a/example/assets/rive/bouncing_ball.riv b/example/assets/rive/bouncing_ball.riv new file mode 100644 index 0000000000000000000000000000000000000000..d83155fa7251731bbbc68491b679c4d6cef6b2c3 GIT binary patch literal 376 zcmY*V%}PRH5Iy5PH#Nv0tT4h{)B{{Z!bLy+MM7;V+TZL#LTczGaM`wf^a8B{Nr+su zX(Jfw1M~uF{vhbGO??^!4V*b=hBGrTN6FKK6E`EfDgzm6k1Vmm8tZJZ$rjt}@RMEk z*yn&Fj)_T#!mpZ^SjMwfqT@P)C&esY)}xD)gazHX0epQcF|%P9#y1|+ zT#M4CbSR&cE~Q85QwEd~WlVurhk{@gYn!tt>5O%m&RPK@Tz1131Xe;G_~Qb9?1PGY zLHXZuBwTyyW=t8L+*4oK9sg^FHs_CR)3ySKX6j + {isLoading ? ( + + ) : riveFile ? ( + + ) : ( + {error || 'Unexpected error'} + )} + + ); +} + +function WithViewModelSetup({ file }: { file: RiveFile }) { + const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]); + const instance = useMemo( + () => viewModel?.createDefaultInstance(), + [viewModel] + ); + + if (!instance || !viewModel) { + return ( + + + {!viewModel + ? 'No view model found.' + : 'Failed to create view model instance'} + + + This demo requires a Rive file (bouncing_ball.riv) with:{'\n'} + {'\n'}• A ViewModel with a "ypos" number property{'\n'}• A bouncing + ball animation{'\n'}• Target-to-source binding from ball Y position to + ypos{'\n'} + {'\n'} + See Rive docs for data binding setup. + + + ); + } + + return ; +} + +function BouncingBallTracker({ + instance, + file, +}: { + instance: ViewModelInstance; + file: RiveFile; +}) { + const pointerY = useSharedValue(0); + + const yposProperty = useMemo( + () => instance.numberProperty('ypos'), + [instance] + ); + + useEffect(() => { + if (!yposProperty) return; + + yposProperty.addListener((value) => { + 'worklet'; + console.log('worklet:', _WORKLET, __RUNTIME_KIND); + pointerY.value = value; + return true; + }); + + return () => { + yposProperty.removeListeners(); + }; + }, [yposProperty, pointerY]); + + const pointerStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: pointerY.value }], + })); + + if (!yposProperty) { + return ( + + Property "ypos" not found + + Make sure the Rive file has a "ypos" number property in its ViewModel + with target-to-source binding from the ball's Y position. + + + ); + } + + return ( + + + Rive animation drives the ball position.{'\n'}React Native listens and + moves the blue pointer to track it. + + + No re-renders - using direct addListener + + + + + + + RN + + + + ); +} + +RiveToReactNativeExample.metadata = { + name: 'Rive → React Native', + description: + 'Demonstrates Rive animation driving React Native UI through data binding listeners', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + subtitle: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginVertical: 10, + paddingHorizontal: 20, + }, + valueText: { + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 10, + color: '#333', + }, + contentContainer: { + position: 'relative', + height: 600, + width: 200, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: '#ccc', + }, + rive: { + width: 100, + height: 600, + }, + pointer: { + position: 'absolute', + top: -10, + right: 40, + flexDirection: 'row', + alignItems: 'center', + }, + pointerArrow: { + width: 0, + height: 0, + borderTopWidth: 10, + borderBottomWidth: 10, + borderRightWidth: 15, + borderTopColor: 'transparent', + borderBottomColor: 'transparent', + borderRightColor: '#007AFF', + }, + pointerText: { + backgroundColor: '#007AFF', + color: '#fff', + fontSize: 12, + fontWeight: 'bold', + paddingHorizontal: 6, + paddingVertical: 4, + borderTopRightRadius: 4, + borderBottomRightRadius: 4, + }, + errorText: { + color: 'red', + textAlign: 'center', + fontSize: 16, + fontWeight: 'bold', + marginBottom: 10, + }, + instructionText: { + color: '#666', + textAlign: 'left', + fontSize: 14, + lineHeight: 22, + }, +}); diff --git a/expo-example/assets/rive/bouncing_ball.riv b/expo-example/assets/rive/bouncing_ball.riv new file mode 100644 index 0000000000000000000000000000000000000000..d83155fa7251731bbbc68491b679c4d6cef6b2c3 GIT binary patch literal 376 zcmY*V%}PRH5Iy5PH#Nv0tT4h{)B{{Z!bLy+MM7;V+TZL#LTczGaM`wf^a8B{Nr+su zX(Jfw1M~uF{vhbGO??^!4V*b=hBGrTN6FKK6E`EfDgzm6k1Vmm8tZJZ$rjt}@RMEk z*yn&Fj)_T#!mpZ^SjMwfqT@P)C&esY)}xD)gazHX0epQcF|%P9#y1|+ zT#M4CbSR&cE~Q85QwEd~WlVurhk{@gYn!tt>5O%m&RPK@Tz1131Xe;G_~Qb9?1PGY zLHXZuBwTyyW=t8L+*4oK9sg^FHs_CR)3ySKX6j Date: Mon, 22 Dec 2025 14:08:28 +0100 Subject: [PATCH 2/2] feat: add UI thread listener support for Reanimated shared values Add RiveWorkletBridge HybridObject that installs Nitro's dispatcher on Reanimated's UI runtime, enabling ViewModel property listeners to run on the UI thread and update SharedValues without blocking when JS is busy. - iOS: Uses GCD dispatch_async/dispatch_sync to main queue - Android: Uses Handler(Looper.getMainLooper()) via JNI bridge - Export installWorkletDispatcher() function from package - Update example to demonstrate JS thread blocking test --- RNRive.podspec | 11 +- android/CMakeLists.txt | 5 +- .../src/main/cpp/JRiveWorkletDispatcher.cpp | 81 +++++++++ .../src/main/cpp/JRiveWorkletDispatcher.hpp | 49 ++++++ android/src/main/cpp/cpp-adapter.cpp | 5 +- .../nitro/rive/RiveWorkletDispatcher.kt | 49 ++++++ cpp/HybridRiveWorkletBridge.hpp | 77 +++++++++ example/src/App.tsx | 5 + .../src/pages/RiveToReactNativeExample.tsx | 162 +++++++++++++++--- expo-example/app/_layout.tsx | 5 + expo-example/metro.config.js | 20 +++ expo-example/package.json | 4 +- nitro.json | 3 + .../generated/android/rive+autolinking.cmake | 1 + nitrogen/generated/android/riveOnLoad.cpp | 10 ++ nitrogen/generated/ios/RNRiveAutolinking.mm | 10 ++ .../c++/HybridRiveWorkletBridgeSpec.cpp | 21 +++ .../c++/HybridRiveWorkletBridgeSpec.hpp | 62 +++++++ src/core/WorkletBridge.ts | 47 +++++ src/index.tsx | 1 + src/specs/RiveWorkletBridge.nitro.ts | 13 ++ yarn.lock | 104 ++++++++--- 22 files changed, 693 insertions(+), 52 deletions(-) create mode 100644 android/src/main/cpp/JRiveWorkletDispatcher.cpp create mode 100644 android/src/main/cpp/JRiveWorkletDispatcher.hpp create mode 100644 android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt create mode 100644 cpp/HybridRiveWorkletBridge.hpp create mode 100644 nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp create mode 100644 nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp create mode 100644 src/core/WorkletBridge.ts create mode 100644 src/specs/RiveWorkletBridge.nitro.ts diff --git a/RNRive.podspec b/RNRive.podspec index 99664968..3b896068 100644 --- a/RNRive.podspec +++ b/RNRive.podspec @@ -41,13 +41,20 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/rive-app/rive-nitro-react-native.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm,swift}" + s.source_files = "ios/**/*.{h,m,mm,swift}", "cpp/**/*.{hpp,cpp}" s.public_header_files = ['ios/RCTSwiftLog.h'] + s.private_header_files = ['cpp/**/*.hpp'] + + # Set pod_target_xcconfig BEFORE add_nitrogen_files so it gets merged with Nitro's settings + s.pod_target_xcconfig = { + 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"' + } + load 'nitrogen/generated/ios/RNRive+autolinking.rb' add_nitrogen_files(s) s.dependency "RiveRuntime", rive_ios_version - install_modules_dependencies(s) + install_modules_dependencies(s) end diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 038c7a6e..8bbf93e1 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -6,7 +6,10 @@ set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_CXX_STANDARD 20) # Define C++ library and add all sources -add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) +add_library(${PACKAGE_NAME} SHARED + src/main/cpp/cpp-adapter.cpp + src/main/cpp/JRiveWorkletDispatcher.cpp +) # Add Nitrogen specs :) include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/rive+autolinking.cmake) diff --git a/android/src/main/cpp/JRiveWorkletDispatcher.cpp b/android/src/main/cpp/JRiveWorkletDispatcher.cpp new file mode 100644 index 00000000..f0745bfa --- /dev/null +++ b/android/src/main/cpp/JRiveWorkletDispatcher.cpp @@ -0,0 +1,81 @@ +#include "JRiveWorkletDispatcher.hpp" +#include + +namespace margelo::nitro::rive { + +using namespace facebook; + +JRiveWorkletDispatcher::JRiveWorkletDispatcher( + jni::alias_ref jThis) + : _javaPart(jni::make_global(jThis)) {} + +jni::local_ref JRiveWorkletDispatcher::initHybrid( + jni::alias_ref jThis) { + return makeCxxInstance(jThis); +} + +jni::local_ref JRiveWorkletDispatcher::create() { + return newObjectJavaArgs(); +} + +void JRiveWorkletDispatcher::trigger() { + std::unique_lock lock(_mutex); + while (!_jobs.empty()) { + auto job = std::move(_jobs.front()); + _jobs.pop(); + lock.unlock(); + job(); + lock.lock(); + } +} + +void JRiveWorkletDispatcher::scheduleTrigger() { + static const auto method = _javaPart->getClass()->getMethod("scheduleTrigger"); + method(_javaPart.get()); +} + +void JRiveWorkletDispatcher::runAsync(std::function&& function) { + std::unique_lock lock(_mutex); + _jobs.push(std::move(function)); + lock.unlock(); + scheduleTrigger(); +} + +void JRiveWorkletDispatcher::runSync(std::function&& function) { + std::mutex mtx; + std::condition_variable cv; + bool done = false; + + runAsync([&]() { + function(); + { + std::lock_guard lock(mtx); + done = true; + } + cv.notify_one(); + }); + + std::unique_lock lock(mtx); + cv.wait(lock, [&]{ return done; }); +} + +void JRiveWorkletDispatcher::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JRiveWorkletDispatcher::initHybrid), + makeNativeMethod("trigger", JRiveWorkletDispatcher::trigger), + }); +} + +AndroidMainThreadDispatcher::AndroidMainThreadDispatcher( + jni::local_ref javaDispatcher) + : _javaDispatcher(jni::make_global(javaDispatcher)) {} + +void AndroidMainThreadDispatcher::runAsync(std::function&& function) { + _javaDispatcher->cthis()->runAsync(std::move(function)); +} + +void AndroidMainThreadDispatcher::runSync(std::function&& function) { + _javaDispatcher->cthis()->runSync(std::move(function)); +} + +} // namespace margelo::nitro::rive diff --git a/android/src/main/cpp/JRiveWorkletDispatcher.hpp b/android/src/main/cpp/JRiveWorkletDispatcher.hpp new file mode 100644 index 00000000..280d1dd4 --- /dev/null +++ b/android/src/main/cpp/JRiveWorkletDispatcher.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace margelo::nitro::rive { + +using namespace facebook; + +class JRiveWorkletDispatcher : public jni::HybridClass { +public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/RiveWorkletDispatcher;"; + + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + static jni::local_ref create(); + + void runAsync(std::function&& function); + void runSync(std::function&& function); + +private: + friend HybridBase; + + void trigger(); + void scheduleTrigger(); + + jni::global_ref _javaPart; + std::queue> _jobs; + std::recursive_mutex _mutex; + + explicit JRiveWorkletDispatcher(jni::alias_ref jThis); +}; + +class AndroidMainThreadDispatcher : public Dispatcher { +public: + explicit AndroidMainThreadDispatcher(jni::local_ref javaDispatcher); + + void runAsync(std::function&& function) override; + void runSync(std::function&& function) override; + +private: + jni::global_ref _javaDispatcher; +}; + +} // namespace margelo::nitro::rive diff --git a/android/src/main/cpp/cpp-adapter.cpp b/android/src/main/cpp/cpp-adapter.cpp index 5116d53c..4c5af328 100644 --- a/android/src/main/cpp/cpp-adapter.cpp +++ b/android/src/main/cpp/cpp-adapter.cpp @@ -1,6 +1,9 @@ #include #include "riveOnLoad.hpp" +#include "JRiveWorkletDispatcher.hpp" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { - return margelo::nitro::rive::initialize(vm); + auto result = margelo::nitro::rive::initialize(vm); + margelo::nitro::rive::JRiveWorkletDispatcher::registerNatives(); + return result; } diff --git a/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt b/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt new file mode 100644 index 00000000..d6f08688 --- /dev/null +++ b/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt @@ -0,0 +1,49 @@ +package com.margelo.nitro.rive + +import android.os.Handler +import android.os.Looper +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import java.util.concurrent.atomic.AtomicBoolean + +@Suppress("JavaJniMissingFunction") +@Keep +@DoNotStrip +class RiveWorkletDispatcher { + @DoNotStrip + @Suppress("unused") + private val mHybridData: HybridData = initHybrid() + + private val mainHandler = Handler(Looper.getMainLooper()) + private val active = AtomicBoolean(true) + + private val triggerRunnable = Runnable { + synchronized(active) { + if (active.get()) { + trigger() + } + } + } + + private external fun initHybrid(): HybridData + private external fun trigger() + + @DoNotStrip + @Suppress("unused") + private fun scheduleTrigger() { + mainHandler.post(triggerRunnable) + } + + fun deactivate() { + synchronized(active) { + active.set(false) + } + } + + companion object { + init { + System.loadLibrary("rive") + } + } +} diff --git a/cpp/HybridRiveWorkletBridge.hpp b/cpp/HybridRiveWorkletBridge.hpp new file mode 100644 index 00000000..f58d8cc2 --- /dev/null +++ b/cpp/HybridRiveWorkletBridge.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "HybridRiveWorkletBridgeSpec.hpp" +#include + +#if __APPLE__ +#include +#include +#elif __ANDROID__ +#include "JRiveWorkletDispatcher.hpp" +#endif + +namespace margelo::nitro::rive { + +#if __APPLE__ + +/** + * iOS: A dispatcher that runs work on the main thread using GCD. + */ +class MainThreadDispatcher : public Dispatcher { +public: + void runAsync(std::function&& function) override { + __block auto func = std::move(function); + dispatch_async(dispatch_get_main_queue(), ^{ + func(); + }); + } + + void runSync(std::function&& function) override { + if (pthread_main_np() != 0) { + function(); + } else { + __block auto func = std::move(function); + dispatch_sync(dispatch_get_main_queue(), ^{ + func(); + }); + } + } +}; + +#endif + +class HybridRiveWorkletBridge : public HybridRiveWorkletBridgeSpec { +public: + HybridRiveWorkletBridge() : HybridObject(TAG) {} + + void install() override { + throw std::runtime_error("install() requires runtime access - use raw method"); + } + +protected: + void loadHybridMethods() override { + HybridObject::loadHybridMethods(); + registerHybrids(this, [](Prototype& prototype) { + prototype.registerRawHybridMethod("install", 0, &HybridRiveWorkletBridge::installRaw); + }); + } + +private: + jsi::Value installRaw(jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* args, + size_t count) { +#if __APPLE__ + auto dispatcher = std::make_shared(); + Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher); +#elif __ANDROID__ + // Create the Java dispatcher instance and wrap it in the C++ dispatcher + auto javaDispatcher = JRiveWorkletDispatcher::create(); + auto dispatcher = std::make_shared(javaDispatcher); + Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher); +#endif + return jsi::Value::undefined(); + } +}; + +} // namespace margelo::nitro::rive diff --git a/example/src/App.tsx b/example/src/App.tsx index 8291510c..e820f737 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,8 +7,13 @@ import { } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; +import { runOnUI } from 'react-native-reanimated'; +import { installWorkletDispatcher } from '@rive-app/react-native'; import { PagesList } from './PagesList'; +// Install dispatcher on Reanimated's UI runtime for worklet-based listeners +installWorkletDispatcher(runOnUI); + type RootStackParamList = { Home: undefined; } & { diff --git a/example/src/pages/RiveToReactNativeExample.tsx b/example/src/pages/RiveToReactNativeExample.tsx index 336c88cd..cf7cfaf1 100644 --- a/example/src/pages/RiveToReactNativeExample.tsx +++ b/example/src/pages/RiveToReactNativeExample.tsx @@ -1,18 +1,75 @@ -import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; +import { + View, + Text, + StyleSheet, + ActivityIndicator, + Pressable, + Switch, +} from 'react-native'; +import { useEffect, useMemo, useState } from 'react'; import Animated, { + runOnUI, useSharedValue, useAnimatedStyle, + type SharedValue, } from 'react-native-reanimated'; -import { useEffect, useMemo } from 'react'; +import { NitroModules } from 'react-native-nitro-modules'; import { Fit, RiveView, useRiveFile, type RiveFile, type ViewModelInstance, + type ViewModelNumberProperty, } from '@rive-app/react-native'; import { type Metadata } from '../helpers/metadata'; +declare global { + var __callMicrotasks: () => void; +} + +/** + * Syncs a Rive ViewModel number property to a Reanimated SharedValue. + * @param useUIThread - If true, runs listener on UI thread (won't freeze when JS blocked). + * If false, runs on JS thread (will freeze when JS blocked). + */ +function useRiveNumberListener( + property: ViewModelNumberProperty | undefined, + sharedValue: SharedValue, + useUIThread: boolean +) { + useEffect(() => { + if (!property) return; + + if (useUIThread) { + // UI thread version - won't freeze when JS thread is blocked + const boxedProperty = NitroModules.box(property); + const sv = sharedValue; + + runOnUI(() => { + 'worklet'; + const prop = boxedProperty.unbox(); + prop.addListener((value: number) => { + 'worklet'; + sv.value = value; + global.__callMicrotasks(); + }); + })(); + + return () => { + property.removeListeners(); + }; + } else { + // JS thread version - will freeze when JS thread is blocked + const removeListener = property.addListener((value: number) => { + sharedValue.value = value; + }); + + return removeListener; + } + }, [property, sharedValue, useUIThread]); +} + export default function RiveToReactNativeExample() { const { riveFile, isLoading, error } = useRiveFile( require('../../assets/rive/bouncing_ball.riv') @@ -37,6 +94,7 @@ function WithViewModelSetup({ file }: { file: RiveFile }) { () => viewModel?.createDefaultInstance(), [viewModel] ); + const [useUIThread, setUseUIThread] = useState(true); if (!instance || !viewModel) { return ( @@ -58,15 +116,26 @@ function WithViewModelSetup({ file }: { file: RiveFile }) { ); } - return ; + return ( + + ); } function BouncingBallTracker({ instance, file, + useUIThread, + onToggle, }: { instance: ViewModelInstance; file: RiveFile; + useUIThread: boolean; + onToggle: (value: boolean) => void; }) { const pointerY = useSharedValue(0); @@ -75,20 +144,7 @@ function BouncingBallTracker({ [instance] ); - useEffect(() => { - if (!yposProperty) return; - - yposProperty.addListener((value) => { - 'worklet'; - console.log('worklet:', _WORKLET, __RUNTIME_KIND); - pointerY.value = value; - return true; - }); - - return () => { - yposProperty.removeListeners(); - }; - }, [yposProperty, pointerY]); + useRiveNumberListener(yposProperty, pointerY, useUIThread); const pointerStyle = useAnimatedStyle(() => ({ transform: [{ translateY: pointerY.value }], @@ -109,13 +165,16 @@ function BouncingBallTracker({ return ( - Rive animation drives the ball position.{'\n'}React Native listens and - moves the blue pointer to track it. - - - No re-renders - using direct addListener + Rive drives the ball position via data binding.{'\n'}React Native tracks + it with the blue pointer using addListener. + + JS Thread + + UI Thread + + RN + + ); } +function BlockJSThreadButton() { + const [isBlocking, setIsBlocking] = useState(false); + + const handlePress = () => { + setIsBlocking(true); + + // Use setTimeout to let the state update render before blocking + setTimeout(() => { + const start = Date.now(); + while (Date.now() - start < 2000) { + // Busy poll - blocks JS thread for 2 seconds + } + setIsBlocking(false); + }, 50); + }; + + return ( + + + {isBlocking ? 'JS Thread Blocked...' : 'Block JS Thread (2s)'} + + + ); +} + RiveToReactNativeExample.metadata = { name: 'Rive → React Native', description: @@ -157,11 +247,15 @@ const styles = StyleSheet.create({ marginVertical: 10, paddingHorizontal: 20, }, - valueText: { - fontSize: 18, - fontWeight: 'bold', - textAlign: 'center', + switchContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 10, marginBottom: 10, + }, + switchLabel: { + fontSize: 14, color: '#333', }, contentContainer: { @@ -217,4 +311,20 @@ const styles = StyleSheet.create({ fontSize: 14, lineHeight: 22, }, + blockButton: { + backgroundColor: '#4CAF50', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + marginTop: 20, + alignSelf: 'center', + }, + blockButtonActive: { + backgroundColor: '#f44336', + }, + blockButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + }, }); diff --git a/expo-example/app/_layout.tsx b/expo-example/app/_layout.tsx index d10c2b6e..752b4568 100644 --- a/expo-example/app/_layout.tsx +++ b/expo-example/app/_layout.tsx @@ -5,9 +5,14 @@ import { } from '@react-navigation/native'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; +import { runOnUI } from 'react-native-reanimated'; +import { installWorkletDispatcher } from '@rive-app/react-native'; import { useColorScheme } from '@/hooks/use-color-scheme'; +// Install dispatcher on Reanimated's UI runtime for worklet-based listeners +installWorkletDispatcher(runOnUI); + export default function RootLayout() { const colorScheme = useColorScheme(); diff --git a/expo-example/metro.config.js b/expo-example/metro.config.js index 874dce7a..ac65f857 100644 --- a/expo-example/metro.config.js +++ b/expo-example/metro.config.js @@ -13,6 +13,26 @@ const finalConfig = getConfig(config, { project: __dirname, }); +// Block resolution from example/node_modules to avoid version conflicts +// (expo-example uses different reanimated/worklets versions than example app) +const escapeRegex = (str) => str.replace(/[/\\]/g, '[/\\\\]'); +const exampleNodeModules = path.join(root, 'example', 'node_modules'); +const blockPatterns = [ + new RegExp( + escapeRegex(path.join(exampleNodeModules, 'react-native-reanimated')) + '.*' + ), + new RegExp( + escapeRegex(path.join(exampleNodeModules, 'react-native-worklets')) + '.*' + ), +]; +const existingBlockList = finalConfig.resolver.blockList; +if (existingBlockList) { + blockPatterns.push(existingBlockList); +} +finalConfig.resolver.blockList = new RegExp( + blockPatterns.map((r) => r.source).join('|') +); + /** * Resolves @example/* path aliases to the example/src/* directory. * Metro doesn't natively understand TypeScript path mappings, so this diff --git a/expo-example/package.json b/expo-example/package.json index 8595c4f0..4ddda499 100644 --- a/expo-example/package.json +++ b/expo-example/package.json @@ -35,11 +35,11 @@ "react-native": "0.81.5", "react-native-gesture-handler": "2.29.1", "react-native-nitro-modules": "0.31.10", - "react-native-reanimated": "4.1.5", + "react-native-reanimated": "4.2.0", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", - "react-native-worklets": "0.6.1" + "react-native-worklets": "0.7.1" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/nitro.json b/nitro.json index 2213016c..c0bf4f1d 100644 --- a/nitro.json +++ b/nitro.json @@ -28,6 +28,9 @@ "RiveImageFactory": { "swift": "HybridRiveImageFactory", "kotlin": "HybridRiveImageFactory" + }, + "RiveWorkletBridge": { + "cpp": "HybridRiveWorkletBridge" } }, "ignorePaths": ["node_modules"] diff --git a/nitrogen/generated/android/rive+autolinking.cmake b/nitrogen/generated/android/rive+autolinking.cmake index d3d7c065..147b978a 100644 --- a/nitrogen/generated/android/rive+autolinking.cmake +++ b/nitrogen/generated/android/rive+autolinking.cmake @@ -41,6 +41,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridRiveImageFactorySpec.cpp ../nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp ../nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp + ../nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelPropertySpec.cpp diff --git a/nitrogen/generated/android/riveOnLoad.cpp b/nitrogen/generated/android/riveOnLoad.cpp index 9c74c45d..abcc07ef 100644 --- a/nitrogen/generated/android/riveOnLoad.cpp +++ b/nitrogen/generated/android/riveOnLoad.cpp @@ -42,6 +42,7 @@ #include "JHybridViewModelListPropertySpec.hpp" #include "JHybridViewModelArtboardPropertySpec.hpp" #include +#include "HybridRiveWorkletBridge.hpp" namespace margelo::nitro::rive { @@ -120,6 +121,15 @@ int initialize(JavaVM* vm) { return instance->cthis()->shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RiveWorkletBridge", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRiveWorkletBridge\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); }); } diff --git a/nitrogen/generated/ios/RNRiveAutolinking.mm b/nitrogen/generated/ios/RNRiveAutolinking.mm index e904f1c3..fb155897 100644 --- a/nitrogen/generated/ios/RNRiveAutolinking.mm +++ b/nitrogen/generated/ios/RNRiveAutolinking.mm @@ -15,6 +15,7 @@ #include "HybridRiveFileSpecSwift.hpp" #include "HybridRiveViewSpecSwift.hpp" #include "HybridRiveImageFactorySpecSwift.hpp" +#include "HybridRiveWorkletBridge.hpp" @interface RNRiveAutolinking : NSObject @end @@ -60,6 +61,15 @@ + (void) load { return hybridObject; } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RiveWorkletBridge", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRiveWorkletBridge\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); } @end diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp new file mode 100644 index 00000000..49a955c1 --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp @@ -0,0 +1,21 @@ +/// +/// HybridRiveWorkletBridgeSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridRiveWorkletBridgeSpec.hpp" + +namespace margelo::nitro::rive { + + void HybridRiveWorkletBridgeSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("install", &HybridRiveWorkletBridgeSpec::install); + }); + } + +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp new file mode 100644 index 00000000..df955d2a --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp @@ -0,0 +1,62 @@ +/// +/// HybridRiveWorkletBridgeSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + + + +namespace margelo::nitro::rive { + + using namespace margelo::nitro; + + /** + * An abstract base class for `RiveWorkletBridge` + * Inherit this class to create instances of `HybridRiveWorkletBridgeSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridRiveWorkletBridge: public HybridRiveWorkletBridgeSpec { + * public: + * HybridRiveWorkletBridge(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridRiveWorkletBridgeSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridRiveWorkletBridgeSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridRiveWorkletBridgeSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual void install() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "RiveWorkletBridge"; + }; + +} // namespace margelo::nitro::rive diff --git a/src/core/WorkletBridge.ts b/src/core/WorkletBridge.ts new file mode 100644 index 00000000..9bf82706 --- /dev/null +++ b/src/core/WorkletBridge.ts @@ -0,0 +1,47 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { RiveWorkletBridge } from '../specs/RiveWorkletBridge.nitro'; + +let isInstalled = false; + +/** + * Install the Nitro Dispatcher on Reanimated's UI runtime. + * This enables using HybridObject callbacks (like addListener) from worklets + * and having shared value updates trigger useAnimatedStyle. + * + * Call this once at app startup. It will schedule the installation on the UI thread. + * + * @param runOnUI - The runOnUI function from react-native-reanimated + * + * @example + * ```tsx + * import { installWorkletDispatcher } from '@rive-app/react-native'; + * import { runOnUI } from 'react-native-reanimated'; + * + * // Call once at app startup + * installWorkletDispatcher(runOnUI); + * ``` + */ +export function installWorkletDispatcher( + runOnUI: ( + worklet: (...args: Args) => ReturnValue + ) => (...args: Args) => void +): void { + if (isInstalled) { + return; + } + isInstalled = true; + + // Create bridge on JS thread + const bridge = + NitroModules.createHybridObject('RiveWorkletBridge'); + + // Box it so we can use it in worklet + const boxedBridge = NitroModules.box(bridge); + + // Call install on Reanimated's UI runtime so dispatcher is installed there + runOnUI(() => { + 'worklet'; + const b = boxedBridge.unbox(); + b.install(); + })(); +} diff --git a/src/index.tsx b/src/index.tsx index f37bd9eb..0277ed52 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -56,3 +56,4 @@ export { useRiveFile } from './hooks/useRiveFile'; export { type RiveFileInput } from './hooks/useRiveFile'; export { type SetValueAction } from './types'; export { DataBindMode }; +export { installWorkletDispatcher } from './core/WorkletBridge'; diff --git a/src/specs/RiveWorkletBridge.nitro.ts b/src/specs/RiveWorkletBridge.nitro.ts new file mode 100644 index 00000000..fa26a331 --- /dev/null +++ b/src/specs/RiveWorkletBridge.nitro.ts @@ -0,0 +1,13 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +/** + * Bridge for installing Nitro Dispatcher on the worklets UI runtime. + */ +export interface RiveWorkletBridge + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + /** + * Install the dispatcher on the current runtime. + * Must be called from the UI runtime (via scheduleOnUI). + */ + install(): void; +} diff --git a/yarn.lock b/yarn.lock index 435febbf..8de8ee42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -680,7 +680,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.0.0-0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": +"@babel/plugin-transform-arrow-functions@npm:7.27.1, @babel/plugin-transform-arrow-functions@npm:^7.0.0-0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" dependencies: @@ -739,7 +739,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": +"@babel/plugin-transform-class-properties@npm:7.27.1, @babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-class-properties@npm:7.27.1" dependencies: @@ -763,7 +763,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": +"@babel/plugin-transform-classes@npm:7.28.4, @babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-classes@npm:7.28.4" dependencies: @@ -1037,7 +1037,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": +"@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1" dependencies: @@ -1097,6 +1097,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-chaining@npm:7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c4428d31f182d724db6f10575669aad3dbccceb0dea26aa9071fa89f11b3456278da3097fcc78937639a13c105a82cd452dc0218ce51abdbcf7626a013b928a5 + languageName: node + linkType: hard + "@babel/plugin-transform-optional-chaining@npm:^7.0.0-0, @babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.27.1, @babel/plugin-transform-optional-chaining@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5" @@ -1277,7 +1289,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.0.0-0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": +"@babel/plugin-transform-shorthand-properties@npm:7.27.1, @babel/plugin-transform-shorthand-properties@npm:^7.0.0-0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.27.1" dependencies: @@ -1322,7 +1334,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.0.0-0, @babel/plugin-transform-template-literals@npm:^7.27.1": +"@babel/plugin-transform-template-literals@npm:7.27.1, @babel/plugin-transform-template-literals@npm:^7.0.0-0, @babel/plugin-transform-template-literals@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-template-literals@npm:7.27.1" dependencies: @@ -1344,7 +1356,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.28.5": +"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.27.1, @babel/plugin-transform-typescript@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-transform-typescript@npm:7.28.5" dependencies: @@ -1382,7 +1394,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.0.0-0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": +"@babel/plugin-transform-unicode-regex@npm:7.27.1, @babel/plugin-transform-unicode-regex@npm:^7.0.0-0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-unicode-regex@npm:7.27.1" dependencies: @@ -1515,6 +1527,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:7.27.1": + version: 7.27.1 + resolution: "@babel/preset-typescript@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-validator-option": ^7.27.1 + "@babel/plugin-syntax-jsx": ^7.27.1 + "@babel/plugin-transform-modules-commonjs": ^7.27.1 + "@babel/plugin-transform-typescript": ^7.27.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 38020f1b23e88ec4fbffd5737da455d8939244bddfb48a2516aef93fb5947bd9163fb807ce6eff3e43fa5ffe9113aa131305fef0fb5053998410bbfcfe6ce0ec + languageName: node + linkType: hard + "@babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.24.7": version: 7.28.5 resolution: "@babel/preset-typescript@npm:7.28.5" @@ -6736,7 +6763,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^2.0.0": +"convert-source-map@npm:2.0.0, convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 @@ -8207,11 +8234,11 @@ __metadata: react-native: 0.81.5 react-native-gesture-handler: 2.29.1 react-native-nitro-modules: 0.31.10 - react-native-reanimated: 4.1.5 + react-native-reanimated: 4.2.0 react-native-safe-area-context: ~5.6.0 react-native-screens: ~4.16.0 react-native-web: ~0.21.0 - react-native-worklets: 0.6.1 + react-native-worklets: 0.7.1 typescript: ~5.9.2 languageName: unknown linkType: soft @@ -13828,7 +13855,7 @@ __metadata: languageName: node linkType: hard -"react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": +"react-native-is-edge-to-edge@npm:1.2.1, react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": version: 1.2.1 resolution: "react-native-is-edge-to-edge@npm:1.2.1" peerDependencies: @@ -13873,6 +13900,20 @@ __metadata: languageName: node linkType: hard +"react-native-reanimated@npm:4.2.0": + version: 4.2.0 + resolution: "react-native-reanimated@npm:4.2.0" + dependencies: + react-native-is-edge-to-edge: 1.2.1 + semver: 7.7.3 + peerDependencies: + react: "*" + react-native: "*" + react-native-worklets: ">=0.7.0" + checksum: a8a4c321513cdca93a66b90c284c86eb56b05ac7d8989cd36b3e090055b59b9613870891868432835a13d1ddcaad3b1763bb9c7ed267616d90e138625d15ef23 + languageName: node + linkType: hard + "react-native-rive-example@workspace:example": version: 0.0.0-use.local resolution: "react-native-rive-example@workspace:example" @@ -13968,6 +14009,29 @@ __metadata: languageName: node linkType: hard +"react-native-worklets@npm:0.7.1": + version: 0.7.1 + resolution: "react-native-worklets@npm:0.7.1" + dependencies: + "@babel/plugin-transform-arrow-functions": 7.27.1 + "@babel/plugin-transform-class-properties": 7.27.1 + "@babel/plugin-transform-classes": 7.28.4 + "@babel/plugin-transform-nullish-coalescing-operator": 7.27.1 + "@babel/plugin-transform-optional-chaining": 7.27.1 + "@babel/plugin-transform-shorthand-properties": 7.27.1 + "@babel/plugin-transform-template-literals": 7.27.1 + "@babel/plugin-transform-unicode-regex": 7.27.1 + "@babel/preset-typescript": 7.27.1 + convert-source-map: 2.0.0 + semver: 7.7.3 + peerDependencies: + "@babel/core": "*" + react: "*" + react-native: "*" + checksum: d6ca920ce53cad6ad45ac8379914adfaf73a92d76dc7c68d9b8a8a2913f7042ec8d60bbb6fcf72afe593993d087cbe6277c3f81927d2c0ade9a94acaf58b5dc3 + languageName: node + linkType: hard + "react-native@npm:0.79.2": version: 0.79.2 resolution: "react-native@npm:0.79.2" @@ -14721,21 +14785,21 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": - version: 6.3.1 - resolution: "semver@npm:6.3.1" +"semver@npm:7.7.3, semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1": + version: 7.7.3 + resolution: "semver@npm:7.7.3" bin: semver: bin/semver.js - checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 languageName: node linkType: hard -"semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1": - version: 7.7.3 - resolution: "semver@npm:7.7.3" +"semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" bin: semver: bin/semver.js - checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 languageName: node linkType: hard