Skip to content

Commit

Permalink
feat: duplicate worklets code the smart way (#6827)
Browse files Browse the repository at this point in the history
## Summary

In the transitional period of `react-native-worklets` coming as a
separate package, Reanimated should work with or without
`react-native-worklets` installed. This requires the code of Worklets to
be present both in `react-native-reanimated` and
`react-native-worklets`.

This PR moves Worklets' files that were in Reanimated to
`react-native-worklets` package and adds a script that makes hard links
to these files. The linking happens during the `yarn build` step of
`react-native-reanimated` - this guarantees that
`react-native-reanimated` would still work without
`react-native-worklets` and that its' npm package would still contain
all the necessary files.

For Android some files had to be duplicated. This is because of codegen
conflicts and namespace generation. However, that code is relatively
small and put into respective source sets `externalWorklets/with` and
`externalWorklets/without`.

Also:
- I moved `ReanimatedVersion.cpp/h` files back to Reanimated. We'll
implement version checking for Worklets when we actually need it.
- I adjusted the CI to always do `yarn build` in Reanimated when
necessary.
- Enabled detection of `react-native-worklets` in
`react-native-reanimated`.
- I improved the detection mechanism of `react-native-worklets` for iOS.
- Renamed `NativeWorkletsModuleSpec.ts` to
`NativeReaWorkletsModuleSpec.ts` to avoid codegen conflicts.
- Removed `DummyWorkletsModule` from `react-native-worklets` since now
it has `WorkletsModule`.



## Test plan

Run an Example with these changes.

Then do `yarn add react-native-worklets` in `common-app` (it will use
workspace version by default) in the example directory and run it again.
  • Loading branch information
tjzel authored Jan 29, 2025
1 parent ff2ca92 commit 1c66e51
Show file tree
Hide file tree
Showing 93 changed files with 361 additions and 180 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/example-android-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
env:
WORKING_DIRECTORY: apps/fabric-example
COMMON_APP_DIR: apps/common-app
REANIMATED_DIR: packages/react-native-reanimated
concurrency:
group: android-${{ github.ref }}-${{ inputs.use-external-worklets }}
cancel-in-progress: true
Expand All @@ -59,6 +60,14 @@ jobs:
# TODO: Add caching for node_modules and artifacts that will work with monorepo setup.
- name: Install monorepo node dependencies
run: yarn install --immutable
- name: Build Reanimated package
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn build

- name: Use external worklets
if: ${{ inputs.use-external-worklets == 'true' }}
working-directory: ${{ env.COMMON_APP_DIR }}
run: yarn add react-native-worklets@workspace:"*"

- name: Use external worklets
if: ${{ inputs.use-external-worklets == 'true' }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/example-ios-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
env:
WORKING_DIRECTORY: apps/fabric-example
COMMON_APP_DIR: apps/common-app
REANIMATED_DIR: packages/react-native-reanimated
concurrency:
group: ios-${{ github.ref }}-${{ inputs.use-external-worklets }}
cancel-in-progress: true
Expand All @@ -60,6 +61,9 @@ jobs:

- name: Install monorepo node dependencies
run: yarn install --immutable
- name: Build Reanimated package
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn build

- name: Use external worklets
if: ${{ inputs.use-external-worklets == 'true' }}
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/example-macos-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
env:
WORKING_DIRECTORY: apps/macos-example
COMMON_APP_DIR: apps/common-app
REANIMATED_DIR: packages/react-native-reanimated
concurrency:
group: macos-${{ github.ref }}-${{ inputs.use-external-worklets }}
cancel-in-progress: true
Expand All @@ -63,6 +64,14 @@ jobs:
# TODO: Add caching for node_modules and artifacts that will work with monorepo setup.
- name: Install monorepo node dependencies
run: yarn install --immutable
- name: Build Reanimated package
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn build

- name: Use external worklets
if: ${{ inputs.use-external-worklets == 'true' }}
working-directory: ${{ env.COMMON_APP_DIR }}
run: yarn add react-native-worklets@workspace:"*"

- name: Use external worklets
if: ${{ inputs.use-external-worklets == 'true' }}
Expand Down
15 changes: 8 additions & 7 deletions .github/workflows/example-tvos-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
env:
WORKING_DIRECTORY: apps/tvos-example
COMMON_APP_DIR: apps/common-app
REANIMATED_DIR: packages/react-native-reanimated
concurrency:
group: tvos-${{ github.ref }}-${{ inputs.use-external-worklets }}
cancel-in-progress: true
Expand All @@ -56,14 +57,14 @@ jobs:

- name: Install monorepo node dependencies
run: yarn install --immutable
# TODO: Add caching for node_modules and artifacts that will work with monorepo setup.
- name: Install Reanimated node_modules
working-directory: packages/react-native-reanimated
run: yarn install --immutable
- name: Build Reanimated package
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn build

- name: Install app node_modules
working-directory: ${{ env.WORKING_DIRECTORY }}
run: yarn install --immutable
- name: Use external worklets
if: ${{ inputs.use-external-worklets == 'true' }}
working-directory: ${{ env.COMMON_APP_DIR }}
run: yarn add react-native-worklets@workspace:"*"

- name: Use external worklets
if: ${{ inputs.use-external-worklets == 'true' }}
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/reanimated-android-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
concurrency:
group: validate-java-${{ github.ref }}
cancel-in-progress: true
env:
REANIMATED_DIR: packages/react-native-reanimated
steps:
- name: checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -62,7 +64,10 @@ jobs:

- name: Install monorepo node dependencies
run: yarn install --immutable
- name: Build Reanimated package
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn build

- name: Lint Android
working-directory: packages/react-native-reanimated
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn lint:android
7 changes: 6 additions & 1 deletion .github/workflows/reanimated-apple-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ on:
jobs:
check:
if: github.repository == 'software-mansion/react-native-reanimated'
env:
REANIMATED_DIR: packages/react-native-reanimated
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install monorepo node dependencies
run: yarn install --immutable
- name: Build Reanimated package
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn build

- name: Lint apple
working-directory: packages/react-native-reanimated
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn lint:apple
11 changes: 8 additions & 3 deletions .github/workflows/reanimated-common-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ jobs:
strategy:
matrix:
python-version: [3.13]

env:
REANIMATED_DIR: packages/react-native-reanimated
steps:
- uses: actions/checkout@v4

Expand All @@ -42,11 +43,15 @@ jobs:
pip install cpplint==1.6.1
- name: Install monorepo node dependencies
run: yarn install --immutable
- name: Build Reanimated
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn build

- name: Lint Common
working-directory: packages/react-native-reanimated
working-directory: ${{ env.REANIMATED_DIR }}
run: yarn lint:common

- name: Disallow DEBUG macros
working-directory: ${{ env.REANIMATED_DIR }}
run: |
! egrep -r '(#if DEBUG|#ifdef DEBUG)' packages/react-native-reanimated/Common/cpp/ packages/react-native-reanimated/apple/ packages/react-native-reanimated/android/src/main/cpp/
! egrep -r '(#if DEBUG|#ifdef DEBUG)' Common/cpp/ apple/ android/src/main/cpp/
6 changes: 6 additions & 0 deletions packages/react-native-reanimated/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,9 @@ plugin/types

# library bundle
*.tgz

# Worklets code (duplicated)
apple/worklets
Common/cpp/worklets
android/src/worklets
android/src/main/cpp/worklets
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include <reanimated/RuntimeDecorators/RNRuntimeDecorator.h>
#include <worklets/Tools/ReanimatedVersion.h>
#include <reanimated/Tools/ReanimatedVersion.h>

namespace reanimated {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include <reanimated/Tools/ReanimatedVersion.h>
#include <worklets/Tools/JSLogger.h>
#include <worklets/Tools/ReanimatedVersion.h>

#include <memory>
#include <regex>
Expand All @@ -13,7 +13,7 @@

using namespace facebook;

namespace worklets {
namespace reanimated {

std::string getReanimatedCppVersion() {
return std::string(REANIMATED_VERSION_STRING);
Expand Down Expand Up @@ -54,7 +54,7 @@ bool matchVersion(const std::string &version1, const std::string &version2) {

void checkJSVersion(
jsi::Runtime &rnRuntime,
const std::shared_ptr<JSLogger> &jsLogger) {
const std::shared_ptr<worklets::JSLogger> &jsLogger) {
auto cppVersion = getReanimatedCppVersion();

auto maybeJSVersion =
Expand Down Expand Up @@ -92,4 +92,4 @@ bool matchVersion(const std::string &version1, const std::string &version2) {
}
#endif // NDEBUG

}; // namespace worklets
}; // namespace reanimated
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

using namespace facebook;

namespace worklets {
namespace reanimated {

std::string getReanimatedCppVersion();
void injectReanimatedCppVersion(jsi::Runtime &);
bool matchVersion(const std::string &, const std::string &);
void checkJSVersion(jsi::Runtime &, const std::shared_ptr<JSLogger> &);
void checkJSVersion(
jsi::Runtime &,
const std::shared_ptr<worklets::JSLogger> &);

}; // namespace worklets
}; // namespace reanimated
10 changes: 4 additions & 6 deletions packages/react-native-reanimated/RNReanimated.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,9 @@ Pod::Spec.new do |s|
s.platforms = { :ios => ios_min_version, :tvos => "9.0", :osx => "10.14", :visionos => "1.0" }
s.source = { :git => "https://github.com/software-mansion/react-native-reanimated.git", :tag => "#{s.version}" }

# TODO: Uncomment me when dynamic worklets linking is ready
# if File.directory?(File.join(__dir__, "../react-native-worklets"))
# # This condition is really naïve...
# s.dependency "RNWorklets"
# else
if $config[:has_external_worklets]
s.dependency "RNWorklets"
else
s.subspec "worklets" do |ss|
ss.source_files = "Common/cpp/worklets/**/*.{cpp,h}"
ss.header_dir = "worklets"
Expand All @@ -94,7 +92,7 @@ Pod::Spec.new do |s|
sss.source_files = "apple/worklets/**/*.{mm,h,m}"
sss.header_dir = "worklets"
sss.header_mappings_dir = "apple/worklets"
# end
end
end
end

Expand Down
12 changes: 7 additions & 5 deletions packages/react-native-reanimated/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,7 @@ def thirdPartyNdkDir = new File("$buildDir/third-party-ndk")
def reactNativeThirdParty = new File("$reactNativeRootDir/ReactAndroid/src/main/jni/third-party")
def reactNativeAndroidDownloadDir = new File("$reactNativeRootDir/ReactAndroid/build/downloads")

// TODO: Uncomment me when when dynamic worklets linking is ready
// def hasExternalWorklets = rootProject.subprojects.find { it.name == 'react-native-worklets' } != null
def hasExternalWorklets = false
def hasExternalWorklets = rootProject.subprojects.find { it.name == 'react-native-worklets' } != null

def workletsPrefabHeadersDir = project.file("$buildDir/prefab-headers/worklets")
def reanimatedPrefabHeadersDir = project.file("$buildDir/prefab-headers/reanimated")
Expand Down Expand Up @@ -260,7 +258,11 @@ android {
"-DREANIMATED_VERSION=${REANIMATED_VERSION}",
"-DHAS_EXTERNAL_WORKLETS=${hasExternalWorklets}"
abiFilters (*reactNativeArchitectures())
targets("reanimated", "worklets")
if(hasExternalWorklets){
targets("reanimated")
} else {
targets("reanimated", "worklets")
}
}
}

Expand Down Expand Up @@ -343,7 +345,7 @@ android {
sourceSets.main {
java {
if(hasExternalWorklets){
// Nothing for now.
srcDirs += "src/externalWorklets/with"
} else {
srcDirs += "src/worklets/main"
srcDirs += "src/externalWorklets/without"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.swmansion.reanimated;

import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_END;
import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_START;

import androidx.annotation.NonNull;
import com.facebook.react.BaseReactPackage;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.module.annotations.ReactModuleList;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.uimanager.ReanimatedUIManager;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.systrace.Systrace;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@ReactModuleList(
nativeModules = {
ReanimatedModule.class,
ReanimatedUIManager.class,
})
public class ReanimatedPackage extends BaseReactPackage implements ReactPackage {
@Override
public NativeModule getModule(
@NonNull String name, @NonNull ReactApplicationContext reactContext) {
return switch (name) {
case ReanimatedModule.NAME -> new ReanimatedModule(reactContext);
case ReanimatedUIManager.NAME -> createUIManager(reactContext);
default -> null;
};
}

@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
Class<? extends NativeModule>[] moduleList =
new Class[] {
ReanimatedModule.class, ReanimatedUIManager.class,
};

final Map<String, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
for (Class<? extends NativeModule> moduleClass : moduleList) {
ReactModule reactModule =
Objects.requireNonNull(moduleClass.getAnnotation(ReactModule.class));

reactModuleInfoMap.put(
reactModule.name(),
new ReactModuleInfo(
reactModule.name(),
moduleClass.getName(),
true, // override UIManagerModule
reactModule.needsEagerInit(),
reactModule.isCxxModule(),
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED));
}

return () -> reactModuleInfoMap;
}

private UIManagerModule createUIManager(final ReactApplicationContext reactContext) {
ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_START);
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createUIManagerModule");
final ReactInstanceManager reactInstanceManager = getReactInstanceManager(reactContext);
List<ViewManager> viewManagers = reactInstanceManager.getOrCreateViewManagers(reactContext);
int minTimeLeftInFrameForNonBatchedOperationMs = -1;
try {
return ReanimatedUIManagerFactory.create(
reactContext, viewManagers, minTimeLeftInFrameForNonBatchedOperationMs);
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_END);
}
}

/**
* Get the {@link ReactInstanceManager} used by this app. By default, assumes {@link
* ReactApplicationContext#getApplicationContext()} is an instance of {@link ReactApplication} and
* calls {@link ReactApplication#getReactNativeHost().getReactInstanceManager()}. Override this
* method if your application class does not implement {@code ReactApplication} or you simply have
* a different mechanism for storing a {@code ReactInstanceManager}, e.g. as a static field
* somewhere.
*/
public ReactInstanceManager getReactInstanceManager(ReactApplicationContext reactContext) {
return ((ReactApplication) reactContext.getApplicationContext())
.getReactNativeHost()
.getReactInstanceManager();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@
import java.util.Map;
import java.util.Objects;

// `WorkletsModule` should be included from a separate Java package, called `WorkletsPackage`.
// However, it's not possible with the current state of the Gradle Tools provided by
// RNC CLI - all packages besides the first found are ignored.
// Therefore, until we extract `react-native-worklets` to a separate package,
// we will host this module in Reanimated's package.
// `WorkletsPackage` must be registered within `ReanimatedPackage` when
// `react-native-worklets` isn't installed . Extra `...Package` files
// are ignored by React Native tools.
@ReactModuleList(
nativeModules = {
WorkletsModule.class,
Expand Down
Loading

0 comments on commit 1c66e51

Please sign in to comment.