diff --git a/apps/common-app/package.json b/apps/common-app/package.json index 18ee878637ef..fc4fb2308299 100644 --- a/apps/common-app/package.json +++ b/apps/common-app/package.json @@ -16,6 +16,7 @@ "react-native": "*" }, "dependencies": { + "@exodus/react-native-reanimated": "workspace:*", "@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.5.2", "@fortawesome/react-native-fontawesome": "0.3.2", @@ -38,7 +39,6 @@ "react-native-gesture-handler": "2.28.0", "react-native-mmkv": "4.0.0", "react-native-nitro-modules": "0.31.9", - "react-native-reanimated": "workspace:*", "react-native-safe-area-context": "5.6.1", "react-native-screens": "4.19.0-nightly-20251125-46052f31e", "react-native-svg": "patch:react-native-svg@npm%3A15.15.3#~/.yarn/patches/react-native-svg-npm-15.15.3-0699a4dc13.patch", diff --git a/apps/macos-example/package.json b/apps/macos-example/package.json index eb98024c32a6..cf0e9921c99d 100644 --- a/apps/macos-example/package.json +++ b/apps/macos-example/package.json @@ -11,11 +11,11 @@ "type:check": "tsc --noEmit" }, "dependencies": { + "@exodus/react-native-reanimated": "workspace:*", "common-app": "workspace:*", "react": "19.1.4", "react-native": "0.81.4", "react-native-macos": "patch:react-native-macos@npm%3A0.81.4#~/.yarn/patches/react-native-macos-npm-0.81.4.patch", - "react-native-reanimated": "workspace:*", "react-native-worklets": "0.8.1" }, "devDependencies": { diff --git a/apps/next-example/package.json b/apps/next-example/package.json index 6f34fa9f78c9..be52af2dbe35 100644 --- a/apps/next-example/package.json +++ b/apps/next-example/package.json @@ -15,12 +15,12 @@ "format": "prettier --write --list-different ." }, "dependencies": { + "@exodus/react-native-reanimated": "workspace:*", "expo": "54.0.13", "next": "15.5.4", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.85.0-rc.1", - "react-native-reanimated": "workspace:*", "react-native-web": "0.21.1", "react-native-worklets": "0.8.1", "webpack": "5.102.1" diff --git a/apps/tvos-example/package.json b/apps/tvos-example/package.json index 19e9387fa357..9ec679e5ac48 100644 --- a/apps/tvos-example/package.json +++ b/apps/tvos-example/package.json @@ -13,9 +13,9 @@ "type:check": "tsc --noEmit" }, "dependencies": { + "@exodus/react-native-reanimated": "workspace:*", "react": "19.2.3", "react-native": "npm:react-native-tvos@0.84.1-0", - "react-native-reanimated": "workspace:*", "react-native-worklets": "0.8.1" }, "devDependencies": { diff --git a/package.json b/package.json index 3df798d778da..e69489fe27fa 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "private": true, "scripts": { "build": "husky && yarn build-packages", - "build-packages": "yarn workspace react-native-reanimated build", + "build-packages": "yarn workspace @exodus/react-native-reanimated build", "build-all": "husky && yarn workspaces foreach --all --parallel --topological-dev run build", "format": "yarn format:root && yarn format:workspaces", "format:root": "prettier --cache --write --list-different --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .prettiereslintignore .", diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp index 6d0d7204c522..c08672e6aea5 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp @@ -23,7 +23,7 @@ void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operation const jsi::Value &updates = item.getProperty(rt, "updates"); addUpdatesToBatch(shadowNode, jsi::dynamicFromValue(rt, updates)); - if constexpr (StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS")) { + if (RuntimeFeatureFlags::forceReactRenderForSettledAnimations()) { timestampMap_[shadowNode->getTag()] = timestamp; } } diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp index 313c66d175d7..d382a76092e1 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp @@ -343,7 +343,12 @@ jsi::Value ReanimatedModuleProxy::getStaticFeatureFlag(jsi::Runtime &rt, const j jsi::Value ReanimatedModuleProxy::setDynamicFeatureFlag(jsi::Runtime &rt, const jsi::Value &name, const jsi::Value &value) { - reanimated::DynamicFeatureFlags::setFlag(name.asString(rt).utf8(rt), value.asBool()); + const auto flagName = name.asString(rt).utf8(rt); + const auto flagValue = value.asBool(); + if (flagName == "FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS") { + reanimated::RuntimeFeatureFlags::setForceReactRenderForSettledAnimations(flagValue); + } + reanimated::DynamicFeatureFlags::setFlag(flagName, flagValue); return jsi::Value::undefined(); } @@ -528,9 +533,9 @@ void ReanimatedModuleProxy::unregisterCSSTransition(jsi::Runtime &rt, const jsi: } jsi::Value ReanimatedModuleProxy::getSettledUpdates(jsi::Runtime &rt) { - react_native_assert( - StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS") && - "getSettledUpdates requires FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS static feature flag to be enabled"); + if (!RuntimeFeatureFlags::forceReactRenderForSettledAnimations()) { + return jsi::Array(rt, 0); + } // TODO(future): use unified timestamp const auto currentTimestamp = getAnimationTimestamp_(); diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/RuntimeDecorators/RNRuntimeDecorator.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/RuntimeDecorators/RNRuntimeDecorator.cpp index e5fea15207d1..62ef6ce0cde5 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/RuntimeDecorators/RNRuntimeDecorator.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/RuntimeDecorators/RNRuntimeDecorator.cpp @@ -10,14 +10,7 @@ void RNRuntimeDecorator::decorate( jsi::Runtime &rnRuntime, jsi::Runtime &uiRuntime, const std::shared_ptr &reanimatedModuleProxy) { - auto workletRuntimeValue = rnRuntime.global() - .getPropertyAsObject(rnRuntime, "ArrayBuffer") - .asFunction(rnRuntime) - .callAsConstructor(rnRuntime, {static_cast(sizeof(void *))}); - uintptr_t *workletRuntimeData = - reinterpret_cast(workletRuntimeValue.getObject(rnRuntime).getArrayBuffer(rnRuntime).data(rnRuntime)); - workletRuntimeData[0] = reinterpret_cast(&uiRuntime); - rnRuntime.global().setProperty(rnRuntime, "_WORKLET_RUNTIME", workletRuntimeValue); + // Security: _WORKLET_RUNTIME pointer leak removed (0056) #ifndef NDEBUG checkJSVersion(rnRuntime); diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.cpp index 307681d48936..84fd38bc62cc 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -15,4 +16,15 @@ void DynamicFeatureFlags::setFlag(const std::string &name, bool value) { flags_[name] = value; } +std::atomic RuntimeFeatureFlags::forceReactRenderForSettledAnimations_{ + StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS")}; + +bool RuntimeFeatureFlags::forceReactRenderForSettledAnimations() { + return forceReactRenderForSettledAnimations_.load(std::memory_order_relaxed); +} + +void RuntimeFeatureFlags::setForceReactRenderForSettledAnimations(bool value) { + forceReactRenderForSettledAnimations_.store(value, std::memory_order_relaxed); +} + } // namespace reanimated diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.h b/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.h index b42d68554d13..85f7be9a833f 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.h +++ b/packages/react-native-reanimated/Common/cpp/reanimated/Tools/FeatureFlags.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -41,4 +42,15 @@ class DynamicFeatureFlags { static std::unordered_map flags_; }; +// Per-flag runtime override for select flags that we need to toggle at runtime. +// Defaults to the compile-time StaticFeatureFlags value. Thread-safe via atomic. +class RuntimeFeatureFlags { + public: + static bool forceReactRenderForSettledAnimations(); + static void setForceReactRenderForSettledAnimations(bool value); + + private: + static std::atomic forceReactRenderForSettledAnimations_; +}; + } // namespace reanimated diff --git a/packages/react-native-reanimated/android/CMakeLists.txt b/packages/react-native-reanimated/android/CMakeLists.txt index 3f87cab8570b..f5ff690373f4 100644 --- a/packages/react-native-reanimated/android/CMakeLists.txt +++ b/packages/react-native-reanimated/android/CMakeLists.txt @@ -45,7 +45,7 @@ file(GLOB_RECURSE REANIMATED_ANDROID_CPP_SOURCES CONFIGURE_DEPENDS find_package(fbjni REQUIRED CONFIG) find_package(ReactAndroid REQUIRED CONFIG) -find_package(react-native-worklets REQUIRED CONFIG) +find_package(exodus_react-native-worklets REQUIRED CONFIG) add_library(reanimated SHARED ${REANIMATED_COMMON_CPP_SOURCES} ${REANIMATED_ANDROID_CPP_SOURCES}) @@ -99,4 +99,4 @@ target_link_libraries( ReactAndroid::jsi fbjni::fbjni android - react-native-worklets::worklets) + exodus_react-native-worklets::worklets) diff --git a/packages/react-native-reanimated/android/build.gradle b/packages/react-native-reanimated/android/build.gradle index 8167d16df2b2..c8909ce49933 100644 --- a/packages/react-native-reanimated/android/build.gradle +++ b/packages/react-native-reanimated/android/build.gradle @@ -317,8 +317,8 @@ dependencies { if (project == rootProject) { // This is needed for linting in Reanimated's repo. } else { - if (rootProject.subprojects.find { it.name == "react-native-worklets" }) { - implementation project(":react-native-worklets") + if (rootProject.subprojects.find { it.name == "exodus_react-native-worklets" }) { + implementation project(":exodus_react-native-worklets") } else { throw new GradleException("[Reanimated] `react-native-worklets` library not found. Please install it as a dependency in your project. Install `react-native-worklets` with your package manager, i.e. `yarn add react-native-worklets` or `npm i react-native-worklets`. Read the documentation for more details: https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#unable-to-find-a-specification-for-rnworklets-depended-upon-by-rnreanimated") } @@ -328,14 +328,14 @@ dependencies { preBuild.dependsOn(prepareReanimatedHeadersForPrefabs) if (project != rootProject) { - evaluationDependsOn(":react-native-worklets") + evaluationDependsOn(":exodus_react-native-worklets") afterEvaluate { tasks.named("externalNativeBuildDebug").configure { - dependsOn(findProject(":react-native-worklets").tasks.named("externalNativeBuildDebug")) + dependsOn(findProject(":exodus_react-native-worklets").tasks.named("externalNativeBuildDebug")) } tasks.named("externalNativeBuildRelease").configure { - dependsOn(findProject(":react-native-worklets").tasks.named("externalNativeBuildRelease")) + dependsOn(findProject(":exodus_react-native-worklets").tasks.named("externalNativeBuildRelease")) } tasks.named("clean") { it.finalizedBy(cleanCMakeCache) diff --git a/packages/react-native-reanimated/android/src/main/cpp/reanimated/android/SensorSetter.h b/packages/react-native-reanimated/android/src/main/cpp/reanimated/android/SensorSetter.h index fc4b9d8b4ffc..50ab56a25669 100644 --- a/packages/react-native-reanimated/android/src/main/cpp/reanimated/android/SensorSetter.h +++ b/packages/react-native-reanimated/android/src/main/cpp/reanimated/android/SensorSetter.h @@ -17,7 +17,9 @@ class SensorSetter : public HybridClass { size_t size = value->size(); auto elements = value->getRegion(0, size); double array[7]; - for (size_t i = 0; i < size; i++) { + // Security: bounds check to prevent buffer overflow (0080) + size_t max = sizeof(array) / sizeof(array[0]); + for (size_t i = 0; i < size && i < max; i++) { array[i] = elements[i]; } callback_(array, orientationDegrees); diff --git a/packages/react-native-reanimated/package.json b/packages/react-native-reanimated/package.json index fb9706ea7bcf..2cf509b62736 100644 --- a/packages/react-native-reanimated/package.json +++ b/packages/react-native-reanimated/package.json @@ -1,6 +1,6 @@ { - "name": "react-native-reanimated", - "version": "4.3.0", + "name": "@exodus/react-native-reanimated", + "version": "4.3.0-exodus.3", "description": "More powerful alternative to Animated library for React Native.", "keywords": [ "react-native", @@ -34,7 +34,7 @@ "type:check:strict": "yarn type:check:strict:src && yarn type:check:strict:app", "type:check:strict:src": "yarn tsc --noEmit --customConditions react-native-strict-api", "type:check:strict:app": "yarn workspace common-app type:check:strict", - "build": "yarn workspace react-native-worklets build && bob build", + "build": "yarn workspace @exodus/react-native-worklets build && bob build", "circular-dependency-check": "yarn madge --extensions js,jsx --circular lib", "tree-shake:check:web": "yarn is-tree-shakable --resolution web", "validate-peers": "node ../../scripts/validate-compatibility-peer-dependencies.js" @@ -96,9 +96,9 @@ "semver": "^7.7.3" }, "peerDependencies": { + "@exodus/react-native-worklets": "0.9.x", "react": "*", - "react-native": "0.81 - 0.85", - "react-native-worklets": "0.8.x" + "react-native": "0.81 - 0.85" }, "devDependencies": { "@babel/core": "7.28.4", diff --git a/packages/react-native-reanimated/plugin/index.js b/packages/react-native-reanimated/plugin/index.js index 0f5fd05847d5..0790631d578b 100644 --- a/packages/react-native-reanimated/plugin/index.js +++ b/packages/react-native-reanimated/plugin/index.js @@ -1,4 +1,4 @@ // @ts-ignore plugin type isn't exposed -const plugin = require('react-native-worklets/plugin'); +const plugin = require('@exodus/react-native-worklets/plugin'); module.exports = plugin; diff --git a/packages/react-native-reanimated/src/Colors.ts b/packages/react-native-reanimated/src/Colors.ts index 3d23c133aa92..76ebca569686 100644 --- a/packages/react-native-reanimated/src/Colors.ts +++ b/packages/react-native-reanimated/src/Colors.ts @@ -19,51 +19,6 @@ interface HSV { v: number; } -const NUMBER: string = '[-+]?\\d*\\.?\\d+'; -const PERCENTAGE = NUMBER + '%'; - -function call(...args: (RegExp | string)[]) { - return '\\(\\s*(' + args.join(')\\s*,?\\s*(') + ')\\s*\\)'; -} - -function callWithSlashSeparator(...args: (RegExp | string)[]) { - return ( - '\\(\\s*(' + - args.slice(0, args.length - 1).join(')\\s*,?\\s*(') + - ')\\s*/\\s*(' + - args[args.length - 1] + - ')\\s*\\)' - ); -} - -function commaSeparatedCall(...args: (RegExp | string)[]) { - return '\\(\\s*(' + args.join(')\\s*,\\s*(') + ')\\s*\\)'; -} - -const MATCHERS = { - rgb: new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)), - rgba: new RegExp( - 'rgba(' + - commaSeparatedCall(NUMBER, NUMBER, NUMBER, NUMBER) + - '|' + - callWithSlashSeparator(NUMBER, NUMBER, NUMBER, NUMBER) + - ')' - ), - hsl: new RegExp('hsl' + call(NUMBER, PERCENTAGE, PERCENTAGE)), - hsla: new RegExp( - 'hsla(' + - commaSeparatedCall(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) + - '|' + - callWithSlashSeparator(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) + - ')' - ), - hwb: new RegExp('hwb' + call(NUMBER, PERCENTAGE, PERCENTAGE)), - hex3: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, - hex4: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, - hex6: /^#([0-9a-fA-F]{6})$/, - hex8: /^#([0-9a-fA-F]{8})$/, -}; - function hue2rgb(p: number, q: number, t: number): number { 'worklet'; if (t < 0) { @@ -368,136 +323,314 @@ export function normalizeColor(color: unknown): number | null { return null; } - let match: RegExpExecArray | null | undefined; + let inputUntrimmed = color; + while (inputUntrimmed.includes(' ')) { + inputUntrimmed = inputUntrimmed.replace(' ', ' '); + } - // Ordered based on occurrences on Facebook codebase - if ((match = MATCHERS.hex6.exec(color))) { - return Number.parseInt(match[1] + 'ff', 16) >>> 0; + const input = inputUntrimmed.trim(); + if (input.length > 0 && inputUntrimmed[0] === ' ' && input[0] === '#') { + return null; } - if (color in names) { - return names[color]; + function isAllHexDigits(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const c = str[i]; + const isHex = + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); + if (!isHex) { + return false; + } + } + return true; } - if ((match = MATCHERS.rgb.exec(color))) { - return ( - // b - ((parse255(match[1]) << 24) | // r - (parse255(match[2]) << 16) | // g - (parse255(match[3]) << 8) | - 0x000000ff) >>> // a - 0 - ); + function isAllDigits(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const c = str[i]; + if (i === 0 && (c === '-' || c === '+')) { + continue; + } + + const isNum = c >= '0' && c <= '9'; + if (!isNum) { + return false; + } + } + return true; } - if ((match = MATCHERS.rgba.exec(color))) { - // rgba(R G B / A) notation - if (match[6] !== undefined) { - return ( - ((parse255(match[6]) << 24) | // r - (parse255(match[7]) << 16) | // g - (parse255(match[8]) << 8) | // b - parse1(match[9])) >>> // a - 0 - ); + function isAllDigitsDot(str: string): boolean { + const newStr = str.replace('.', ''); // only remove one '.' + return isAllDigits(newStr); + } + + function isPercentage(str: string): boolean { + if (!str.includes('%')) { + return false; } + const digitDot = str.replace('%', ''); + if (isAllDigitsDot(digitDot)) { + return true; + } + return false; + } + + if (names[input] !== undefined) { + return names[input]; + } - // rgba(R, G, B, A) notation - return ( - ((parse255(match[2]) << 24) | // r - (parse255(match[3]) << 16) | // g - (parse255(match[4]) << 8) | // b - parse1(match[5])) >>> // a - 0 - ); - } - - if ((match = MATCHERS.hex3.exec(color))) { - return ( - Number.parseInt( - match[1] + - match[1] + // r - match[2] + - match[2] + // g - match[3] + - match[3] + // b - 'ff', // a - 16 - ) >>> 0 - ); - } - - // https://drafts.csswg.org/css-color-4/#hex-notation - if ((match = MATCHERS.hex8.exec(color))) { - return Number.parseInt(match[1], 16) >>> 0; - } - - if ((match = MATCHERS.hex4.exec(color))) { - return ( - Number.parseInt( - match[1] + - match[1] + // r - match[2] + - match[2] + // g - match[3] + - match[3] + // b - match[4] + - match[4], // a - 16 - ) >>> 0 - ); - } - - if ((match = MATCHERS.hsl.exec(color))) { - return ( - (hslToRgb( - parse360(match[1]), // h - parsePercentage(match[2]), // s - parsePercentage(match[3]) // l - ) | - 0x000000ff) >>> // a - 0 - ); - } - - if ((match = MATCHERS.hsla.exec(color))) { - // hsla(H S L / A) notation - if (match[6] !== undefined) { - return ( - (hslToRgb( - parse360(match[6]), // h - parsePercentage(match[7]), // s - parsePercentage(match[8]) // l - ) | - parse1(match[9])) >>> // a - 0 - ); + // #RRGGBB => 7 chars total, e.g. "#1a2B3C" + if (input.startsWith('#') && input.length === 7) { + const hexPart = input.slice(1); // e.g. "1a2B3C" + if (isAllHexDigits(hexPart)) { + return Number.parseInt(hexPart + 'ff', 16) >>> 0; } + } + // rgb(R, G, B) or rgb(R G B) + if (input.startsWith('rgb(') && input.endsWith(')')) { + const inside = input.slice(4, -1).trim(); + let parts = inside.split(',').map((p) => p.trim()); - // hsla(H, S, L, A) notation - return ( - (hslToRgb( - parse360(match[2]), // h - parsePercentage(match[3]), // s - parsePercentage(match[4]) // l - ) | - parse1(match[5])) >>> // a - 0 - ); - } - - if ((match = MATCHERS.hwb.exec(color))) { - return ( - (hwbToRgb( - parse360(match[1]), // h - parsePercentage(match[2]), // w - parsePercentage(match[3]) // b - ) | - 0x000000ff) >>> // a - 0 - ); + if (parts.length !== 3) { + parts = inside.split(' ').map((p) => p.trim()); + if (parts.length !== 3) { + return null; + } + } + for (const part of parts) { + if (!isAllDigitsDot(part)) { + return null; + } + } + + const r = parse255(parts[0]); + const g = parse255(parts[1]); + const b = parse255(parts[2]); + if (r != null && g != null && b != null) { + return ((r << 24) | (g << 16) | (b << 8) | 0xff) >>> 0; + } + } + + // rgba(R, G, B, A) or rgba(R G B / A) + if (input.startsWith('rgba(') && input.endsWith(')')) { + const inside = input.slice(5, -1).trim(); + if (inside.includes('/')) { + // slash form + const [beforeSlash, alphaPart] = inside.split('/'); + if (beforeSlash && alphaPart) { + const rgbParts = beforeSlash + .trim() + .split(' ') + .map((x) => x.trim()); + if (rgbParts.length === 3) { + for (const rgbPart of rgbParts) { + if (!isAllDigitsDot(rgbPart)) { + return null; + } + } + if (!isAllDigitsDot(alphaPart.trim())) { + return null; + } + + const r = parse255(rgbParts[0]); + const g = parse255(rgbParts[1]); + const b = parse255(rgbParts[2]); + const a = parse1(alphaPart.trim()); + if (r != null && g != null && b != null && a != null) { + return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0; + } + } + } + } else { + // comma form + const parts = inside.split(',').map((p) => p.trim()); + if (parts.length === 4) { + for (const part of parts) { + if (!isAllDigitsDot(part)) { + return null; + } + } + const r = parse255(parts[0]); + const g = parse255(parts[1]); + const b = parse255(parts[2]); + const a = parse1(parts[3]); + if (r != null && g != null && b != null && a != null) { + return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0; + } + } + } + } + + // #RGB => length=4, e.g. "#F0c" + if (input.startsWith('#') && input.length === 4) { + const shortHex = input.slice(1); // e.g. "F0c" + if (shortHex.length === 3 && isAllHexDigits(shortHex)) { + // Expand => "FF00cc" + "ff" + const expanded = + shortHex[0] + + shortHex[0] + + shortHex[1] + + shortHex[1] + + shortHex[2] + + shortHex[2] + + 'ff'; + return Number.parseInt(expanded, 16) >>> 0; + } + } + + // #RRGGBBAA => length=9 + if (input.startsWith('#') && input.length === 9) { + const hexPart = input.slice(1); // e.g. "1a2b3cFF" + if (hexPart.length === 8 && isAllHexDigits(hexPart)) { + return Number.parseInt(hexPart, 16) >>> 0; + } + } + + // #RGBA => length=5 + if (input.startsWith('#') && input.length === 5) { + const shortHex = input.slice(1); // e.g. "F0cF" + if (shortHex.length === 4 && isAllHexDigits(shortHex)) { + const expanded = + shortHex[0] + + shortHex[0] + + shortHex[1] + + shortHex[1] + + shortHex[2] + + shortHex[2] + + shortHex[3] + + shortHex[3]; + return Number.parseInt(expanded, 16) >>> 0; + } + } + + // hsl(H, S%, L%) or hsl(H S% L%) + if (input.startsWith('hsl(') && input.endsWith(')')) { + const inside = input.slice(4, -1).trim(); + let parts = inside.split(',').map((p) => p.trim()); + + if (parts.length !== 3) { + parts = inside.split(' ').map((p) => p.trim()); + if (parts.length !== 3) { + return null; + } + } + if (!isAllDigitsDot(parts[0])) { + return null; + } + if (!isPercentage(parts[1])) { + return null; + } + if (!isPercentage(parts[2])) { + return null; + } + + const h = parse360(parts[0]); // can be negative, wraps via mod + const s = parsePercentage(parts[1]); + const l = parsePercentage(parts[2]); + if (h != null && s != null && l != null) { + const rgb = hslToRgb(h, s, l); + return (rgb | 0xff) >>> 0; // alpha=255 + } + } + + // hsla(H, S%, L%, A) or hsla(H S% L% / A) + if (input.startsWith('hsla(') && input.endsWith(')')) { + const inside = input.slice(5, -1).trim(); + if (inside.includes('/')) { + // slash form => "H, S%, L% / A" + const [beforeSlash, alphaPart] = inside.split('/'); + if (beforeSlash && alphaPart) { + const hslParts = beforeSlash + .trim() + .split(' ') + .map((p) => p.trim()); + if (hslParts.length === 3) { + if (!isAllDigitsDot(hslParts[0])) { + return null; + } + if (!isPercentage(hslParts[1])) { + return null; + } + if (!isPercentage(hslParts[2])) { + return null; + } + if (!isAllDigitsDot(alphaPart.trim())) { + return null; + } + + const h = parse360(hslParts[0]); + const s = parsePercentage(hslParts[1]); + const l = parsePercentage(hslParts[2]); + const a = parse1(alphaPart.trim()); + if (h != null && s != null && l != null && a != null) { + const rgb = hslToRgb(h, s, l); + return (rgb | a) >>> 0; + } + } + } + } else { + // comma form => "H, S%, L%, A" + const parts = inside.split(',').map((p) => p.trim()); + if (parts.length === 4) { + if (!isAllDigitsDot(parts[0])) { + return null; + } + if (!isPercentage(parts[1])) { + return null; + } + if (!isPercentage(parts[2])) { + return null; + } + if (!isAllDigitsDot(parts[3])) { + return null; + } + + const h = parse360(parts[0]); + const s = parsePercentage(parts[1]); + const l = parsePercentage(parts[2]); + const a = parse1(parts[3]); + if (h != null && s != null && l != null && a != null) { + const rgb = hslToRgb(h, s, l); + return (rgb | a) >>> 0; + } + } + } + } + + // hwb(H, W%, B%) or hwb(H W% B%) -- angle can be negative + if (input.startsWith('hwb(') && input.endsWith(')')) { + const inside = input.slice(4, -1).trim(); + let parts = inside.split(',').map((p) => p.trim()); + + if (parts.length !== 3) { + parts = inside.split(' ').map((p) => p.trim()); + if (parts.length !== 3) { + return null; + } + } + if (!isAllDigitsDot(parts[0])) { + return null; + } + if (!isPercentage(parts[1])) { + return null; + } + if (!isPercentage(parts[2])) { + return null; + } + + const h = parse360(parts[0]); + const w = parsePercentage(parts[1]); + const b = parsePercentage(parts[2]); + if (h != null && w != null && b != null) { + const rgb = hwbToRgb(h, w, b); + return (rgb | 0xff) >>> 0; // alpha=255 + } } + // Nothing matched => invalid return null; } diff --git a/packages/react-native-reanimated/src/PropsRegistryGarbageCollector.ts b/packages/react-native-reanimated/src/PropsRegistryGarbageCollector.ts index f917ce5a8586..1a1507dc7bef 100644 --- a/packages/react-native-reanimated/src/PropsRegistryGarbageCollector.ts +++ b/packages/react-native-reanimated/src/PropsRegistryGarbageCollector.ts @@ -6,6 +6,7 @@ import { } from './common/style/processors/colors'; import type { StyleProps } from './commonTypes'; import type { IAnimatedComponentInternal } from './createAnimatedComponent/commonTypes'; +import { DynamicFlags } from './featureFlags'; import { ReanimatedModule } from './ReanimatedModule'; const FLUSH_INTERVAL_MS = 500; @@ -40,6 +41,9 @@ export const PropsRegistryGarbageCollector = { }, syncPropsBackToReact() { + if (!DynamicFlags.FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS) { + return; + } const settledUpdates = ReanimatedModule.getSettledUpdates(); for (const { viewTag, styleProps } of settledUpdates) { if (styleProps === null) { diff --git a/packages/react-native-reanimated/src/animation/styleAnimation.ts b/packages/react-native-reanimated/src/animation/styleAnimation.ts index 16aa75c1f230..e7ca7c2f8432 100644 --- a/packages/react-native-reanimated/src/animation/styleAnimation.ts +++ b/packages/react-native-reanimated/src/animation/styleAnimation.ts @@ -49,6 +49,14 @@ function setPath( const keys: Path = Array.isArray(path) ? path : [path]; let currObj: NestedObjectValues = obj; for (let i = 0; i < keys.length - 1; i++) { + // Security: guard dangerous keys to prevent prototype pollution (0058) + if ( + keys[i] === '__proto__' || + keys[i] === 'constructor' || + keys[i] === 'prototype' + ) { + return; + } // creates entry if there isn't one currObj = currObj as { [key: string]: NestedObjectValues }; if (!(keys[i] in currObj)) { @@ -62,8 +70,16 @@ function setPath( currObj = currObj[keys[i]]; } - (currObj as { [key: string]: NestedObjectValues })[keys[keys.length - 1]] = - value; + // Security: guard final key as well (0058) + const lastKey = keys[keys.length - 1]; + if ( + lastKey === '__proto__' || + lastKey === 'constructor' || + lastKey === 'prototype' + ) { + return; + } + (currObj as { [key: string]: NestedObjectValues })[lastKey] = value; } interface NestedObjectEntry { diff --git a/packages/react-native-reanimated/src/animation/util.ts b/packages/react-native-reanimated/src/animation/util.ts index 2df3a28be50f..dbdaf38fd183 100644 --- a/packages/react-native-reanimated/src/animation/util.ts +++ b/packages/react-native-reanimated/src/animation/util.ts @@ -203,7 +203,7 @@ function decorateAnimation( return; } - const animationCopy = Object.assign({}, animation); + const animationCopy = Object.assign(Object.create(null), animation); delete animationCopy.callback; const prefNumberSuffOnStart = ( @@ -283,7 +283,7 @@ function decorateAnimation( } } tab.forEach((i, index) => { - animation[i] = Object.assign({}, animationCopy); + animation[i] = Object.assign(Object.create(null), animationCopy); animation[i].current = RGBACurrent[index]; animation[i].toValue = RGBAToValue ? RGBAToValue[index] : undefined; animation[i].onStart( @@ -344,7 +344,7 @@ function decorateAnimation( // We set limits from 0 to 100 (instead of 0-1) to make spring look good // with default thresholds. - animation[0] = Object.assign({}, animationCopy); + animation[0] = Object.assign(Object.create(null), animationCopy); animation[0].current = 0; animation[0].toValue = 100; animation[0].onStart( @@ -424,7 +424,7 @@ function decorateAnimation( previousAnimation: Animation ): void => { value.forEach((v, i) => { - animation[i] = Object.assign({}, animationCopy); + animation[i] = Object.assign(Object.create(null), animationCopy); animation[i].current = v; animation[i].toValue = (animation.toValue as Array)[i]; animation[i].onStart( @@ -459,7 +459,7 @@ function decorateAnimation( previousAnimation: Animation ): void => { for (const key in value) { - animation[key] = Object.assign({}, animationCopy); + animation[key] = Object.assign(Object.create(null), animationCopy); animation[key].onStart = animation.onStart; animation[key].current = value[key]; diff --git a/packages/react-native-reanimated/src/common/style/processors/filter.ts b/packages/react-native-reanimated/src/common/style/processors/filter.ts index 7f8b714d2915..807b5d72bec3 100644 --- a/packages/react-native-reanimated/src/common/style/processors/filter.ts +++ b/packages/react-native-reanimated/src/common/style/processors/filter.ts @@ -11,12 +11,13 @@ import type { import { isLength, isNumber } from '../../utils/guards'; import { processColor } from './colors'; -// Capture filter functions and their content eg "brightness(0.5) opacity(1)" => [["brightness(0.5)", "brightness", "0.5"], ["opacity(1)", "opacity", "1"]] -const FILTER_REGEX = /([\w-]+)\(([^()]*|\([^()]*\)|[^()]*\([^()]*\)[^()]*)\)/g; -// Capture two groups: current transform value and optional unit -> "21.37px" => ["21.37px", "21.37", "px"] + accepts scientific notation like 'e-14' -const FILTER_VALUE_REGEX = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)([a-z%]*)$/; -// Capture drop-shadow parts "10px 5px 5px #888888" => ["10px", "5px", "5px", "#888888"] -const DROP_SHADOW_REGEX = /[^,\s()]+(?:\([^()]*\))?/g; +// NOTE: The RegExp literals previously declared here at module level +// (FILTER_REGEX, FILTER_VALUE_REGEX, DROP_SHADOW_REGEX) are now declared +// inside the worklet bodies that use them. The Exodus AppSec hardening in +// `@exodus/react-native-worklets` blocks cloning of captured RegExp +// instances during worklet serialization; keeping the literal inside the +// worklet keeps it in the worklet's serialized source so each runtime +// evaluates a fresh RegExp instance. type SingleFilterValue = { numberValue: number; @@ -54,6 +55,10 @@ const LENGTH_MAPPINGS = ['offsetX', 'offsetY', 'standardDeviation'] as const; const parseDropShadowString = (value: string): DropShadowValue | null => { 'worklet'; + // Capture two groups: current transform value and optional unit -> "21.37px" => ["21.37px", "21.37", "px"] + accepts scientific notation like 'e-14' + const FILTER_VALUE_REGEX = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)([a-z%]*)$/; + // Capture drop-shadow parts "10px 5px 5px #888888" => ["10px", "5px", "5px", "#888888"] + const DROP_SHADOW_REGEX = /[^,\s()]+(?:\([^()]*\))?/g; const match = value.match(DROP_SHADOW_REGEX) ?? []; const result: DropShadowValue = { offsetX: 0, offsetY: 0 }; let foundLengthsCount = 0; @@ -114,6 +119,8 @@ const parseFilterProperty = ( context: ValueProcessorContext | undefined ): ParsedFilterFunction | null => { 'worklet'; + // Capture two groups: current transform value and optional unit -> "21.37px" => ["21.37px", "21.37", "px"] + accepts scientific notation like 'e-14' + const FILTER_VALUE_REGEX = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)([a-z%]*)$/; // We need to handle dropShadow separately because of its complex structure if (filterName === 'dropShadow') { const dropShadow = parseDropShadow( @@ -180,6 +187,9 @@ const parseFilterString = ( context: ValueProcessorContext | undefined ): FilterArray => { 'worklet'; + // Capture filter functions and their content eg "brightness(0.5) opacity(1)" => [["brightness(0.5)", "brightness", "0.5"], ["opacity(1)", "opacity", "1"]] + const FILTER_REGEX = + /([\w-]+)\(([^()]*|\([^()]*\)|[^()]*\([^()]*\)[^()]*)\)/g; const matches = Array.from(value.matchAll(FILTER_REGEX)); const filterArray: FilterArray = []; diff --git a/packages/react-native-reanimated/src/common/utils/parsers.ts b/packages/react-native-reanimated/src/common/utils/parsers.ts index cca1a5c15b9d..5316d8b08b25 100644 --- a/packages/react-native-reanimated/src/common/utils/parsers.ts +++ b/packages/react-native-reanimated/src/common/utils/parsers.ts @@ -10,11 +10,14 @@ const LENGTH_MAPPINGS = [ 'spreadDistance', ] as const; -const SHADOW_PARTS_REGEX = /(?:[^\s()]+|\([^()]*\))+/g; -const SHADOW_SPLIT_REGEX = /(?:[^,()]+|\([^)]*\))+(?=\s*,|$)/g; - export function parseBoxShadowString(value: string) { 'worklet'; + // NOTE: Captured RegExp literals cannot be cloned by the Exodus AppSec + // `cloneRegExp` hardening in `@exodus/react-native-worklets`. Declaring + // them inside the worklet body keeps the literal in the worklet's + // serialized source so each runtime evaluates a fresh RegExp instance. + const SHADOW_PARTS_REGEX = /(?:[^\s()]+|\([^()]*\))+/g; + const SHADOW_SPLIT_REGEX = /(?:[^,()]+|\([^)]*\))+(?=\s*,|$)/g; if (value === 'none') { return []; } diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/InlinePropManager.ts b/packages/react-native-reanimated/src/createAnimatedComponent/InlinePropManager.ts index 3f698bfccafe..c5ad78b8cf07 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/InlinePropManager.ts +++ b/packages/react-native-reanimated/src/createAnimatedComponent/InlinePropManager.ts @@ -47,7 +47,8 @@ function getInlinePropsUpdate(styleValue: StyleProps): unknown { return styleValue.map(getInlinePropsUpdate); } if (styleValue && typeof styleValue === 'object') { - const update: Record = {}; + // Security: use null-prototype object to prevent prototype pollution (0074) + const update: Record = Object.create(null); for (const [key, value] of Object.entries(styleValue)) { update[key] = getInlinePropsUpdate(value); } @@ -61,10 +62,11 @@ function extractSharedValuesMapFromProps( Record /* Initial component props */ > ): Record { - const inlineProps: Record = {}; + // Security: use null-prototype object + Object.entries to avoid prototype + // chain traversal (0074) + const inlineProps: Record = Object.create(null); - for (const key in props) { - const value = props[key]; + for (const [key, value] of Object.entries(props)) { if (key === 'style') { const styles = flattenArray(props.style ?? []); styles.forEach((style) => { diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx b/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx index 1daef1a62caa..fe334d3cbe38 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx +++ b/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx @@ -28,7 +28,8 @@ export class PropsFilter implements IPropsFilter { ): Record { const inputProps = component.props as AnimatedComponentProps; - const props: Record = {}; + // Security: use null-prototype object to prevent prototype pollution (0078) + const props: Record = Object.create(null); for (const key in inputProps) { const value = inputProps[key]; diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/utils.ts b/packages/react-native-reanimated/src/createAnimatedComponent/utils.ts index feb44ab54fbc..daff9a95d035 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/utils.ts +++ b/packages/react-native-reanimated/src/createAnimatedComponent/utils.ts @@ -3,25 +3,29 @@ import type { StyleProps } from '../commonTypes'; import type { CSSStyle } from '../css'; import type { NestedArray } from './commonTypes'; +// Security: iterative stack-based flatten to prevent stack overflow via +// deeply nested arrays (0073) export function flattenArray(array: NestedArray): T[] { if (!Array.isArray(array)) { return [array]; } - const resultArr: T[] = []; - - const _flattenArray = (arr: NestedArray[]): void => { - arr.forEach((item) => { - if (Array.isArray(item)) { - _flattenArray(item); - } else { - resultArr.push(item); + const result: T[] = []; + const stack = [...array]; + while (stack.length > 0) { + const item = stack.pop(); + if (Array.isArray(item)) { + for (let i = item.length - 1; i >= 0; i--) { + stack.push(item[i]); } - }); - }; - _flattenArray(array); - return resultArr; + } else { + result.push(item as T); + } + } + return result; } +// Security: use Object.hasOwn instead of `in` to avoid prototype chain +// lookups (0074) export const has = ( key: K, x: unknown @@ -30,7 +34,7 @@ export const has = ( if (x === null || x === undefined) { return false; } else { - return key in x; + return Object.hasOwn(x as object, key); } } return false; diff --git a/packages/react-native-reanimated/src/featureFlags/index.ts b/packages/react-native-reanimated/src/featureFlags/index.ts index 6eaf47da9c1d..0879d5b04808 100644 --- a/packages/react-native-reanimated/src/featureFlags/index.ts +++ b/packages/react-native-reanimated/src/featureFlags/index.ts @@ -5,6 +5,7 @@ import type StaticFeatureFlagsJSON from './staticFlags.json'; type DynamicFlagsType = { EXAMPLE_DYNAMIC_FLAG: boolean; + FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS: boolean; init(): void; setFlag(name: DynamicFlagName, value: boolean): void; getFlag(name: DynamicFlagName): boolean; @@ -17,6 +18,7 @@ type DynamicFlagName = keyof Omit< /** @knipIgnore */ export const DynamicFlags: DynamicFlagsType = { EXAMPLE_DYNAMIC_FLAG: true, + FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS: true, init() { Object.keys(DynamicFlags).forEach((key) => { diff --git a/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts b/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts index 24a806c61322..d48604b96689 100644 --- a/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts +++ b/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts @@ -127,11 +127,28 @@ function runAnimations( forceCopyAnimation?: boolean ): boolean { 'worklet'; + + // Security: use defineProperty instead of bracket assignment to avoid + // prototype pollution via __proto__ or constructor keys (0057 + 0067) + function safeAssign( + obj: Record, + k: string, + value: unknown + ) { + Object.defineProperty(obj, k, { + value, + writable: true, + enumerable: true, + configurable: true, + __proto__: null, + } as PropertyDescriptor); + } + if (!animationsActive.value) { return true; } if (Array.isArray(animation)) { - result[key] = []; + safeAssign(result, String(key), []); let allFinished = true; forceCopyAnimation = key === 'boxShadow'; animation.forEach((entry, index) => { @@ -169,13 +186,13 @@ function runAnimations( * in rgba format, allowing the animation to run correctly. Additionally we need to check if user animated the whole boxShadow object or only one of its properties. */ if (forceCopyAnimation && typeof animation.current === 'object') { - result[key] = { ...animation.current }; + safeAssign(result, String(key), { ...animation.current }); } else { - result[key] = animation.current; + safeAssign(result, String(key), animation.current); } return finished; } else if (typeof animation === 'object') { - result[key] = {}; + safeAssign(result, String(key), {}); let allFinished = true; Object.keys(animation).forEach((k) => { if ( @@ -193,7 +210,7 @@ function runAnimations( }); return allFinished; } else { - result[key] = animation; + safeAssign(result, String(key), animation); return true; } } diff --git a/packages/react-native-reanimated/src/index.ts b/packages/react-native-reanimated/src/index.ts index 743549f34ca7..75e60186f6e0 100644 --- a/packages/react-native-reanimated/src/index.ts +++ b/packages/react-native-reanimated/src/index.ts @@ -212,7 +212,8 @@ export { FlipOutYLeft, FlipOutYRight, JumpingTransition, - Keyframe, + // Security: Keyframe export disabled to prevent prototype pollution (0074) + // Keyframe, // Transitions Layout, LightSpeedInLeft, diff --git a/packages/react-native-reanimated/src/platform-specific/jsVersion.ts b/packages/react-native-reanimated/src/platform-specific/jsVersion.ts index bc8488607682..705cc477152e 100644 --- a/packages/react-native-reanimated/src/platform-specific/jsVersion.ts +++ b/packages/react-native-reanimated/src/platform-specific/jsVersion.ts @@ -1,7 +1,8 @@ 'use strict'; +import { version } from '../../package.json'; /** * We hardcode the version of Reanimated here in order to compare it with the * version used to build the native part of the library in runtime. Remember to * keep this in sync with the version declared in `package.json` */ -export const jsVersion = '4.3.0'; +export const jsVersion = version; diff --git a/packages/react-native-worklets/Common/cpp/worklets/Resources/ValueUnpacker.cpp b/packages/react-native-worklets/Common/cpp/worklets/Resources/ValueUnpacker.cpp index 661cf4aa3072..bac876e1535b 100644 --- a/packages/react-native-worklets/Common/cpp/worklets/Resources/ValueUnpacker.cpp +++ b/packages/react-native-worklets/Common/cpp/worklets/Resources/ValueUnpacker.cpp @@ -10,21 +10,14 @@ const char ValueUnpackerCode[] = R"DELIMITER__((function () { var workletsCache = new Map(); var handleCache = new WeakMap(); - function valueUnpacker(objectToUnpack, category, remoteFunctionName) { + function valueUnpacker(objectToUnpack, category, remoteFunctionName, evaluateWorkletFunction) { 'use strict'; var workletHash = objectToUnpack.__workletHash; if (workletHash !== undefined) { var workletFun = workletsCache.get(workletHash); if (workletFun === undefined) { - var initData = objectToUnpack.__initData; - if (globalThis.evalWithSourceMap) { - workletFun = globalThis.evalWithSourceMap('(' + initData.code + '\n)', initData.location, initData.sourceMap); - } else if (globalThis.evalWithSourceUrl) { - workletFun = globalThis.evalWithSourceUrl('(' + initData.code + '\n)', "worklet_".concat(workletHash)); - } else { - workletFun = eval('(' + initData.code + '\n)'); - } + workletFun = evaluateWorkletFunction === null || evaluateWorkletFunction === void 0 ? void 0 : evaluateWorkletFunction(); workletsCache.set(workletHash, workletFun); } var functionInstance = workletFun.bind(objectToUnpack); diff --git a/packages/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.cpp b/packages/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.cpp index 62672f75b852..dadeb3158a97 100644 --- a/packages/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.cpp +++ b/packages/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.cpp @@ -10,6 +10,15 @@ using namespace facebook; namespace worklets { +jsi::PropNameID workletCodePropName(jsi::Runtime &rt) { + jsi::Function symbolFor = rt.global() + .getPropertyAsObject(rt, "Symbol") + .getPropertyAsFunction(rt, "for"); + jsi::Value val = symbolFor.call(rt, jsi::String::createFromAscii(rt, "__reanimated_workletCode")); + jsi::Symbol sym = val.asSymbol(rt); + return jsi::PropNameID::forSymbol(rt, sym); +} + jsi::Function getValueUnpacker(jsi::Runtime &rt) { auto valueUnpacker = rt.global().getProperty(rt, "__valueUnpacker"); react_native_assert(valueUnpacker.isObject() && "valueUnpacker not found"); @@ -24,7 +33,11 @@ jsi::Value makeSerializableClone( std::shared_ptr serializable; if (value.isObject()) { auto object = value.asObject(rt); - if (!object.getProperty(rt, "__workletHash").isUndefined()) { + jsi::PropNameID prop = workletCodePropName(rt); + if (object.hasProperty(rt, prop)) { + jsi::Value code = object.getProperty(rt, prop); + serializable = std::make_shared(code.asString(rt).utf8(rt)); + } else if (!object.getProperty(rt, "__workletHash").isUndefined()) { // We pass `false` because this function is invoked only // by `makeSerializableCloneOnUIRecursive` which doesn't // make Retaining Serializables. @@ -221,7 +234,24 @@ jsi::Value SerializableWorklet::toJSValue(jsi::Runtime &rt) { std::any_of(data_.cbegin(), data_.cend(), [](const auto &item) { return item.first == "__workletHash"; }) && "SerializableWorklet doesn't have `__workletHash` property"); jsi::Value obj = SerializableObject::toJSValue(rt); - return getValueUnpacker(rt).call(rt, obj, jsi::String::createFromAscii(rt, "Worklet")); + auto initData = obj.asObject(rt).getProperty(rt, "__initData").asObject(rt); + auto code = std::make_shared( + "(" + initData.getProperty(rt, "__reanimated_workletCode").asString(rt).utf8(rt) + "\n)"); + + auto locationValue = initData.getProperty(rt, "location"); + std::string sourceURL = locationValue.isString() ? locationValue.asString(rt).utf8(rt) : "worklet"; + auto evaluateWorkletFunction = jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "evaluateWorkletFunction"), + 0, + [&](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) + -> jsi::Value { return rt.evaluateJavaScript(code, sourceURL); }); + return getValueUnpacker(rt).call( + rt, + obj, + jsi::String::createFromAscii(rt, "Worklet"), + jsi::Value::undefined(), + evaluateWorkletFunction); } jsi::Value SerializableImport::toJSValue(jsi::Runtime &rt) { diff --git a/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletHermesRuntime.cpp b/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletHermesRuntime.cpp index 22388b6b7136..2bd07a8e4ec6 100644 --- a/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletHermesRuntime.cpp +++ b/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletHermesRuntime.cpp @@ -67,27 +67,6 @@ WorkletHermesRuntime::WorkletHermesRuntime( debugToken_ = chrome::enableDebugging(std::move(adapter), name); #endif // HERMES_ENABLE_DEBUGGER && !defined(HERMES_V1_ENABLED) -#ifndef NDEBUG - facebook::hermes::HermesRuntime *wrappedRuntime = runtime_.get(); - jsi::Value evalWithSourceMap = jsi::Function::createFromHostFunction( - *runtime_, - jsi::PropNameID::forAscii(*runtime_, "evalWithSourceMap"), - 3, - [wrappedRuntime]( - jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { - auto code = std::make_shared(args[0].asString(rt).utf8(rt)); - std::string sourceURL; - if (count > 1 && args[1].isString()) { - sourceURL = args[1].asString(rt).utf8(rt); - } - std::shared_ptr sourceMap; - if (count > 2 && args[2].isString()) { - sourceMap = std::make_shared(args[2].asString(rt).utf8(rt)); - } - return wrappedRuntime->evaluateJavaScriptWithSourceMap(code, sourceMap, sourceURL); - }); - runtime_->global().setProperty(*runtime_, "evalWithSourceMap", evalWithSourceMap); -#endif // NDEBUG } WorkletHermesRuntime::~WorkletHermesRuntime() { diff --git a/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletRuntimeDecorator.cpp b/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletRuntimeDecorator.cpp index 914811d73916..a7f3bee9d4fa 100644 --- a/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletRuntimeDecorator.cpp +++ b/packages/react-native-worklets/Common/cpp/worklets/WorkletRuntime/WorkletRuntimeDecorator.cpp @@ -91,23 +91,6 @@ void WorkletRuntimeDecorator::decorate( rt.global().setProperty(rt, "__workletsModuleProxy", std::move(jsiWorkletsModuleProxy)); -#ifndef NDEBUG - auto evalWithSourceUrl = - [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { - auto code = std::make_shared(args[0].asString(rt).utf8(rt)); - std::string url; - if (count > 1 && args[1].isString()) { - url = args[1].asString(rt).utf8(rt); - } - return rt.evaluateJavaScript(code, url); - }; - rt.global().setProperty( - rt, - "evalWithSourceUrl", - jsi::Function::createFromHostFunction( - rt, jsi::PropNameID::forAscii(rt, "evalWithSourceUrl"), 1, evalWithSourceUrl)); -#endif // NDEBUG - jsi_utils::installJsiFunction( rt, "_log", [](jsi::Runtime &rt, const jsi::Value &value) { PlatformLogger::log(stringifyJSIValue(rt, value)); }); diff --git a/packages/react-native-worklets/package.json b/packages/react-native-worklets/package.json index 04f6f329a22e..572dc8582e8e 100644 --- a/packages/react-native-worklets/package.json +++ b/packages/react-native-worklets/package.json @@ -1,6 +1,6 @@ { - "name": "react-native-worklets", - "version": "0.9.0-main", + "name": "@exodus/react-native-worklets", + "version": "0.9.0-exodus.1", "description": "The React Native multithreading library", "keywords": [ "react-native", diff --git a/packages/react-native-worklets/plugin/index.js b/packages/react-native-worklets/plugin/index.js index 1eb7f4025505..476d185d108b 100644 --- a/packages/react-native-worklets/plugin/index.js +++ b/packages/react-native-worklets/plugin/index.js @@ -964,8 +964,11 @@ var require_workletFactory = __commonJS({ (0, assert_1.strict)(pathForStringDefinitions, "[Reanimated] `pathForStringDefinitions` is null."); (0, assert_1.strict)(pathForStringDefinitions.parentPath, "[Reanimated] `pathForStringDefinitions.parentPath` is null."); const initDataId = pathForStringDefinitions.parentPath.scope.generateUidIdentifier(`worklet_${workletHash}_init_data`); + const symbolForWorkletCode = (0, types_12.callExpression)((0, types_12.memberExpression)((0, types_12.identifier)("Symbol"), (0, types_12.identifier)("for")), [(0, types_12.stringLiteral)("__reanimated_workletCode")]); + const workletCodeProperty = (0, types_12.objectProperty)(symbolForWorkletCode, (0, types_12.stringLiteral)(funString), true); + const reanimatedWorkletCodeProperty = (0, types_12.objectProperty)((0, types_12.identifier)("__reanimated_workletCodeWrapper"), (0, types_12.objectExpression)([workletCodeProperty])); const initDataObjectExpression = (0, types_12.objectExpression)([ - (0, types_12.objectProperty)((0, types_12.identifier)("code"), (0, types_12.stringLiteral)(funString)) + reanimatedWorkletCodeProperty ]); const shouldInjectLocation = !(0, utils_1.isRelease)(); if (shouldInjectLocation) { diff --git a/packages/react-native-worklets/plugin/src/workletFactory.ts b/packages/react-native-worklets/plugin/src/workletFactory.ts index efd10fc0aa8e..decb1900cdee 100644 --- a/packages/react-native-worklets/plugin/src/workletFactory.ts +++ b/packages/react-native-worklets/plugin/src/workletFactory.ts @@ -12,6 +12,7 @@ import { arrayExpression, assignmentExpression, blockStatement, + callExpression, cloneNode, expressionStatement, functionExpression, @@ -160,8 +161,24 @@ export function makeWorkletFactory( `worklet_${workletHash}_init_data` ); + const symbolForWorkletCode = callExpression( + memberExpression(identifier('Symbol'), identifier('for')), + [stringLiteral('__reanimated_workletCode')] + ); + + const workletCodeProperty = objectProperty( + symbolForWorkletCode, + stringLiteral(funString), + true + ); + + const reanimatedWorkletCodeProperty = objectProperty( + identifier('__reanimated_workletCodeWrapper'), + objectExpression([workletCodeProperty]) + ); + const initDataObjectExpression = objectExpression([ - objectProperty(identifier('code'), stringLiteral(funString)), + reanimatedWorkletCodeProperty, ]); // When testing with jest I noticed that environment variables are set later diff --git a/packages/react-native-worklets/src/debug/jsVersion.ts b/packages/react-native-worklets/src/debug/jsVersion.ts index b7476668b3a4..84b99af9519b 100644 --- a/packages/react-native-worklets/src/debug/jsVersion.ts +++ b/packages/react-native-worklets/src/debug/jsVersion.ts @@ -1,8 +1,10 @@ 'use strict'; +import { version } from '../../package.json'; + /** * We hardcode the version of Worklets here in order to compare it with the * version used to build the native part of the library in runtime. Remember to * keep this in sync with the version declared in `package.json` */ -export const jsVersion = '0.9.0-main'; +export const jsVersion = version; diff --git a/packages/react-native-worklets/src/memory/serializable.native.ts b/packages/react-native-worklets/src/memory/serializable.native.ts index a48d2fd9a076..39718892eea8 100644 --- a/packages/react-native-worklets/src/memory/serializable.native.ts +++ b/packages/react-native-worklets/src/memory/serializable.native.ts @@ -247,7 +247,7 @@ if (globalThis._WORKLETS_BUNDLE_MODE_ENABLED) { // TODO: Do it programmatically. createSerializable.__bundleData = { imported: 'createSerializable', - source: require.resolveWeak('react-native-worklets'), + source: require.resolveWeak('@exodus/react-native-worklets'), }; } @@ -379,12 +379,28 @@ function cloneNull(): SerializableRef { return WorkletsModule.createSerializableNull(); } +const assignReadOnly = ( + obj: Record, + key: string, + value: unknown +) => { + const descriptor = { + __proto__: null, + value, + writable: false, + enumerable: true, + configurable: false, + }; + Object.defineProperty(obj, key, descriptor); + return obj; +}; + function cloneObjectProperties( value: T, shouldPersistRemote: boolean, depth: number ): Record { - const clonedProps: Record = {}; + const clonedProps: Record = Object.create(null); for (const [key, element] of Object.entries(value)) { // We don't need to clone __initData field as it contains long strings // representing the worklet code, source map, and location, and we will @@ -392,10 +408,22 @@ function cloneObjectProperties( if (key === '__initData' && clonedProps.__initData !== undefined) { continue; } - clonedProps[key] = createSerializable( - element, - shouldPersistRemote, - depth + 1 + if (key === '__reanimated_workletCodeWrapper') { + assignReadOnly( + clonedProps, + '__reanimated_workletCode', + WorkletsModule.createSerializable( + element, + shouldPersistRemote, + value + ) + ); + continue; + } + assignReadOnly( + clonedProps, + key, + createSerializable(element, shouldPersistRemote, depth + 1) ); } return clonedProps; @@ -491,10 +519,10 @@ function cloneWorklet( // that the __initData field that contains long strings representing the // worklet code, source map, and location, will always be // serialized/deserialized once. - clonedProps.__initData = createSerializable( - value.__initData, - true, - depth + 1 + assignReadOnly( + clonedProps, + '__initData', + createSerializable(value.__initData, true, depth + 1) ); const clone = WorkletsModule.createSerializableWorklet( @@ -603,19 +631,12 @@ function cloneSet>( } function cloneRegExp( - value: TValue + _value: TValue ): SerializableRef { - const pattern = value.source; - const flags = value.flags; - const handle = cloneInitializer({ - __init: () => { - 'worklet'; - return new RegExp(pattern, flags); - }, - }) as unknown as SerializableRef; - serializableMappingCache.set(value, handle); - - return handle; + // disabled, contact appsec if needed: https://github.com/ExodusMovement/exodus-mobile/pull/24699#issuecomment-2709694172 + throw new WorkletsError( + 'RegExp has been disabled. Contact AppSec if needed.' + ); } function cloneError( @@ -727,7 +748,15 @@ function inaccessibleObject( const WORKLET_CODE_THRESHOLD = 255; function getWorkletCode(value: WorkletFunction) { - const code = value?.__initData?.code; + const initData = value?.__initData; + if (!initData) { + return 'unknown'; + } + // With Symbol-wrapped code, extract from __reanimated_workletCodeWrapper + const wrapper = initData.__reanimated_workletCodeWrapper; + const code = wrapper + ? wrapper[Symbol.for('__reanimated_workletCode')] + : initData.code; if (!code) { return 'unknown'; } diff --git a/packages/react-native-worklets/src/memory/valueUnpacker.native.ts b/packages/react-native-worklets/src/memory/valueUnpacker.native.ts index 5850da48f99f..b1c9c9ab76e3 100644 --- a/packages/react-native-worklets/src/memory/valueUnpacker.native.ts +++ b/packages/react-native-worklets/src/memory/valueUnpacker.native.ts @@ -2,15 +2,6 @@ import type { ValueUnpacker, WorkletFunction } from '../types'; -declare global { - var evalWithSourceMap: - | ((js: string, sourceURL: string, sourceMap: string) => () => unknown) - | undefined; - var evalWithSourceUrl: - | ((js: string, sourceURL: string) => () => unknown) - | undefined; -} - function __installUnpacker() { const workletsCache = new Map unknown>(); const handleCache = new WeakMap(); @@ -18,7 +9,8 @@ function __installUnpacker() { function valueUnpacker( objectToUnpack: ObjectToUnpack, category?: string, - remoteFunctionName?: string + remoteFunctionName?: string, + evaluateWorkletFunction?: () => (() => unknown) ): unknown { // eslint-disable-next-line strict 'use strict'; @@ -26,31 +18,7 @@ function __installUnpacker() { if (workletHash !== undefined) { let workletFun = workletsCache.get(workletHash); if (workletFun === undefined) { - const initData = objectToUnpack.__initData; - if (globalThis.evalWithSourceMap) { - // if the runtime (hermes only for now) supports loading source maps - // we want to use the proper filename for the location as it guarantees - // that debugger understands and loads the source code of the file where - // the worklet is defined. - workletFun = globalThis.evalWithSourceMap( - '(' + initData!.code + '\n)', - initData!.location!, - initData!.sourceMap! - ); - } else if (globalThis.evalWithSourceUrl) { - // if the runtime doesn't support loading source maps, in dev mode we - // can pass source url when evaluating the worklet. Now, instead of using - // the actual file location we use worklet hash, as it the allows us to - // properly symbolicate traces (see errors.ts for details) - workletFun = globalThis.evalWithSourceUrl( - '(' + initData!.code + '\n)', - `worklet_${workletHash}` - ); - } else { - // in release we use the regular eval to save on JSI calls - // eslint-disable-next-line no-eval - workletFun = eval('(' + initData!.code + '\n)'); - } + workletFun = evaluateWorkletFunction?.() as (() => unknown) | undefined; workletsCache.set(workletHash, workletFun!); } const functionInstance = workletFun!.bind(objectToUnpack); diff --git a/packages/react-native-worklets/src/types.ts b/packages/react-native-worklets/src/types.ts index be3b269ac177..b877dc5c395b 100644 --- a/packages/react-native-worklets/src/types.ts +++ b/packages/react-native-worklets/src/types.ts @@ -51,7 +51,8 @@ export type WorkletStackDetails = [ export type WorkletClosure = Record; interface WorkletInitData { - code: string; + code?: string; + __reanimated_workletCodeWrapper?: Record; /** Only in dev builds. */ location?: string; /** Only in dev builds. */ @@ -85,7 +86,12 @@ export interface WorkletFactory< } export type ValueUnpacker = WorkletFunction< - [objectToUnpack: unknown, category?: string], + [ + objectToUnpack: unknown, + category?: string, + remoteFunctionName?: string, + evaluateWorkletFunction?: () => (() => unknown), + ], unknown >; diff --git a/tsconfig.json b/tsconfig.json index c2bfab94ea60..86a34d670c45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "skipLibCheck": true, "allowSyntheticDefaultImports": true, "moduleResolution": "bundler", - "lib": ["es6", "dom", "ES2021"], + "lib": ["es6", "dom", "ES2022"], "esModuleInterop": true, "strict": true, "forceConsistentCasingInFileNames": true, diff --git a/yarn.lock b/yarn.lock index 3b3f10c1efa6..b59cb881e09d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4089,6 +4089,98 @@ __metadata: languageName: node linkType: hard +"@exodus/react-native-reanimated@workspace:*, @exodus/react-native-reanimated@workspace:packages/react-native-reanimated": + version: 0.0.0-use.local + resolution: "@exodus/react-native-reanimated@workspace:packages/react-native-reanimated" + dependencies: + "@babel/core": "npm:7.28.4" + "@babel/preset-env": "npm:7.28.3" + "@react-native/babel-preset": "npm:0.85.0-rc.1" + "@react-native/eslint-config": "npm:0.85.0-rc.1" + "@react-native/jest-preset": "patch:@react-native/jest-preset@npm%3A0.85.0-rc.1#~/.yarn/patches/@react-native-jest-preset-npm-0.85.0-rc.1-b8e9692393.patch" + "@react-native/metro-config": "npm:0.85.0-rc.1" + "@react-native/typescript-config": "npm:0.85.0-rc.1" + "@shopify/flash-list": "npm:2.1.0" + "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-native": "npm:5.4.3" + "@testing-library/react": "npm:16.1.0" + "@testing-library/react-hooks": "npm:8.0.1" + "@testing-library/react-native": "npm:13.3.3" + "@types/convert-source-map": "npm:2.0.3" + "@types/jest": "npm:30.0.0" + "@types/node": "npm:24.0.14" + "@types/react": "npm:19.2.2" + "@types/react-test-renderer": "npm:19.1.0" + "@types/semver": "npm:7.7.1" + clang-format-node: "npm:1.3.5" + eslint: "npm:9.37.0" + is-tree-shakable: "npm:0.5.0" + jest: "npm:30.2.0" + jest-expo: "npm:52.0.5" + knip: "npm:5.61.3" + madge: "npm:8.0.0" + prettier: "npm:3.6.2" + react: "npm:19.2.3" + react-dom: "npm:19.2.3" + react-native: "npm:0.85.0-rc.1" + react-native-builder-bob: "npm:0.40.13" + react-native-is-edge-to-edge: "npm:^1.3.1" + react-native-svg: "patch:react-native-svg@npm%3A15.15.3#~/.yarn/patches/react-native-svg-npm-15.15.3-0699a4dc13.patch" + react-native-web: "npm:0.21.1" + react-native-worklets: "npm:0.8.1" + react-test-renderer: "npm:19.2.3" + semver: "npm:^7.7.3" + typescript: "npm:5.8.3" + peerDependencies: + "@exodus/react-native-worklets": 0.9.x + react: "*" + react-native: 0.81 - 0.85 + languageName: unknown + linkType: soft + +"@exodus/react-native-worklets@workspace:packages/react-native-worklets": + version: 0.0.0-use.local + resolution: "@exodus/react-native-worklets@workspace:packages/react-native-worklets" + dependencies: + "@babel/cli": "npm:7.28.3" + "@babel/core": "npm:7.28.4" + "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" + "@babel/plugin-transform-class-properties": "npm:^7.28.6" + "@babel/plugin-transform-classes": "npm:^7.28.6" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.28.6" + "@babel/plugin-transform-optional-chaining": "npm:^7.28.6" + "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1" + "@babel/plugin-transform-template-literals": "npm:^7.27.1" + "@babel/plugin-transform-unicode-regex": "npm:^7.27.1" + "@babel/preset-typescript": "npm:^7.28.5" + "@react-native-community/cli": "npm:20.1.0" + "@react-native/eslint-config": "npm:0.83.0" + "@react-native/jest-preset": "patch:@react-native/jest-preset@npm%3A0.85.0-rc.1#~/.yarn/patches/@react-native-jest-preset-npm-0.85.0-rc.1-b8e9692393.patch" + "@types/jest": "npm:30.0.0" + "@types/node": "npm:24.7.0" + "@types/react": "npm:19.2.2" + clang-format-node: "npm:1.3.5" + code-tag: "npm:1.2.0" + convert-source-map: "npm:^2.0.0" + eslint: "npm:9.37.0" + is-tree-shakable: "npm:0.5.0" + jest: "npm:30.2.0" + knip: "npm:5.61.3" + madge: "npm:8.0.0" + prettier: "npm:3.6.2" + react: "npm:19.2.3" + react-native: "npm:0.85.0-rc.1" + react-native-builder-bob: "npm:0.40.13" + semver: "npm:^7.7.4" + typescript: "npm:5.8.3" + peerDependencies: + "@babel/core": "*" + "@react-native/metro-config": "*" + react: "*" + react-native: 0.81 - 0.85 + languageName: unknown + linkType: soft + "@expo/cli@npm:54.0.11": version: 54.0.11 resolution: "@expo/cli@npm:54.0.11" @@ -13698,6 +13790,7 @@ __metadata: "@babel/core": "npm:7.28.4" "@babel/preset-env": "npm:7.28.3" "@babel/runtime": "npm:7.28.4" + "@exodus/react-native-reanimated": "workspace:*" "@fortawesome/fontawesome-svg-core": "npm:6.5.2" "@fortawesome/free-solid-svg-icons": "npm:6.5.2" "@fortawesome/react-native-fontawesome": "npm:0.3.2" @@ -13737,7 +13830,6 @@ __metadata: react-native-gesture-handler: "npm:2.28.0" react-native-mmkv: "npm:4.0.0" react-native-nitro-modules: "npm:0.31.9" - react-native-reanimated: "workspace:*" react-native-safe-area-context: "npm:5.6.1" react-native-screens: "npm:4.19.0-nightly-20251125-46052f31e" react-native-svg: "patch:react-native-svg@npm%3A15.15.3#~/.yarn/patches/react-native-svg-npm-15.15.3-0699a4dc13.patch" @@ -22089,6 +22181,7 @@ __metadata: "@babel/core": "npm:7.28.4" "@babel/preset-env": "npm:7.28.3" "@babel/runtime": "npm:7.28.4" + "@exodus/react-native-reanimated": "workspace:*" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-ios": "npm:20.0.0" "@react-native/babel-preset": "npm:0.81.4" @@ -22101,7 +22194,6 @@ __metadata: react: "npm:19.1.4" react-native: "npm:0.81.4" react-native-macos: "patch:react-native-macos@npm%3A0.81.4#~/.yarn/patches/react-native-macos-npm-0.81.4.patch" - react-native-reanimated: "workspace:*" react-native-worklets: "npm:0.8.1" typescript: "npm:5.8.3" languageName: unknown @@ -24989,6 +25081,7 @@ __metadata: resolution: "next-example@workspace:apps/next-example" dependencies: "@babel/core": "npm:7.28.4" + "@exodus/react-native-reanimated": "workspace:*" "@expo/next-adapter": "npm:6.0.0" "@next/bundle-analyzer": "npm:15.5.4" cypress: "patch:cypress@npm%3A15.4.0#~/.yarn/patches/cypress-npm-15.4.0-c92384506d.patch" @@ -25000,7 +25093,6 @@ __metadata: react: "npm:19.2.3" react-dom: "npm:19.2.3" react-native: "npm:0.85.0-rc.1" - react-native-reanimated: "workspace:*" react-native-web: "npm:0.21.1" react-native-worklets: "npm:0.8.1" start-server-and-test: "npm:2.1.2" @@ -28103,55 +28195,6 @@ __metadata: languageName: node linkType: hard -"react-native-reanimated@workspace:*, react-native-reanimated@workspace:packages/react-native-reanimated": - version: 0.0.0-use.local - resolution: "react-native-reanimated@workspace:packages/react-native-reanimated" - dependencies: - "@babel/core": "npm:7.28.4" - "@babel/preset-env": "npm:7.28.3" - "@react-native/babel-preset": "npm:0.85.0-rc.1" - "@react-native/eslint-config": "npm:0.85.0-rc.1" - "@react-native/jest-preset": "patch:@react-native/jest-preset@npm%3A0.85.0-rc.1#~/.yarn/patches/@react-native-jest-preset-npm-0.85.0-rc.1-b8e9692393.patch" - "@react-native/metro-config": "npm:0.85.0-rc.1" - "@react-native/typescript-config": "npm:0.85.0-rc.1" - "@shopify/flash-list": "npm:2.1.0" - "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-native": "npm:5.4.3" - "@testing-library/react": "npm:16.1.0" - "@testing-library/react-hooks": "npm:8.0.1" - "@testing-library/react-native": "npm:13.3.3" - "@types/convert-source-map": "npm:2.0.3" - "@types/jest": "npm:30.0.0" - "@types/node": "npm:24.0.14" - "@types/react": "npm:19.2.2" - "@types/react-test-renderer": "npm:19.1.0" - "@types/semver": "npm:7.7.1" - clang-format-node: "npm:1.3.5" - eslint: "npm:9.37.0" - is-tree-shakable: "npm:0.5.0" - jest: "npm:30.2.0" - jest-expo: "npm:52.0.5" - knip: "npm:5.61.3" - madge: "npm:8.0.0" - prettier: "npm:3.6.2" - react: "npm:19.2.3" - react-dom: "npm:19.2.3" - react-native: "npm:0.85.0-rc.1" - react-native-builder-bob: "npm:0.40.13" - react-native-is-edge-to-edge: "npm:^1.3.1" - react-native-svg: "patch:react-native-svg@npm%3A15.15.3#~/.yarn/patches/react-native-svg-npm-15.15.3-0699a4dc13.patch" - react-native-web: "npm:0.21.1" - react-native-worklets: "npm:0.8.1" - react-test-renderer: "npm:19.2.3" - semver: "npm:^7.7.3" - typescript: "npm:5.8.3" - peerDependencies: - react: "*" - react-native: 0.81 - 0.85 - react-native-worklets: 0.8.x - languageName: unknown - linkType: soft - "react-native-safe-area-context@npm:5.6.1": version: 5.6.1 resolution: "react-native-safe-area-context@npm:5.6.1" @@ -28288,49 +28331,6 @@ __metadata: languageName: node linkType: hard -"react-native-worklets@workspace:packages/react-native-worklets": - version: 0.0.0-use.local - resolution: "react-native-worklets@workspace:packages/react-native-worklets" - dependencies: - "@babel/cli": "npm:7.28.3" - "@babel/core": "npm:7.28.4" - "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" - "@babel/plugin-transform-class-properties": "npm:^7.28.6" - "@babel/plugin-transform-classes": "npm:^7.28.6" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.28.6" - "@babel/plugin-transform-optional-chaining": "npm:^7.28.6" - "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1" - "@babel/plugin-transform-template-literals": "npm:^7.27.1" - "@babel/plugin-transform-unicode-regex": "npm:^7.27.1" - "@babel/preset-typescript": "npm:^7.28.5" - "@react-native-community/cli": "npm:20.1.0" - "@react-native/eslint-config": "npm:0.83.0" - "@react-native/jest-preset": "patch:@react-native/jest-preset@npm%3A0.85.0-rc.1#~/.yarn/patches/@react-native-jest-preset-npm-0.85.0-rc.1-b8e9692393.patch" - "@types/jest": "npm:30.0.0" - "@types/node": "npm:24.7.0" - "@types/react": "npm:19.2.2" - clang-format-node: "npm:1.3.5" - code-tag: "npm:1.2.0" - convert-source-map: "npm:^2.0.0" - eslint: "npm:9.37.0" - is-tree-shakable: "npm:0.5.0" - jest: "npm:30.2.0" - knip: "npm:5.61.3" - madge: "npm:8.0.0" - prettier: "npm:3.6.2" - react: "npm:19.2.3" - react-native: "npm:0.85.0-rc.1" - react-native-builder-bob: "npm:0.40.13" - semver: "npm:^7.7.4" - typescript: "npm:5.8.3" - peerDependencies: - "@babel/core": "*" - "@react-native/metro-config": "*" - react: "*" - react-native: 0.81 - 0.85 - languageName: unknown - linkType: soft - "react-native@npm:0.81.4": version: 0.81.4 resolution: "react-native@npm:0.81.4" @@ -31854,6 +31854,7 @@ __metadata: "@babel/core": "npm:7.28.4" "@babel/preset-env": "npm:7.28.3" "@babel/runtime": "npm:7.28.4" + "@exodus/react-native-reanimated": "workspace:*" "@react-native/babel-preset": "npm:0.84.1" "@react-native/eslint-config": "npm:0.84.1" "@react-native/metro-config": "npm:0.84.1" @@ -31866,7 +31867,6 @@ __metadata: prettier: "npm:3.6.2" react: "npm:19.2.3" react-native: "npm:react-native-tvos@0.84.1-0" - react-native-reanimated: "workspace:*" react-native-worklets: "npm:0.8.1" react-test-renderer: "npm:19.2.3" typescript: "npm:5.8.3"