diff --git a/.gitattributes b/.gitattributes index 41ab1b0c8295..188dd60ad12c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ -/.yarn/** linguist-vendored -/.yarn/releases/* binary -/.yarn/plugins/**/* binary +/.yarn/** linguist-vendored=true +/.yarn/releases/* binary=true +/.yarn/plugins/**/* binary=true +/packages/eslint-plugin-reanimated/index.js linguist-generated=true +/packages/eslint-plugin-reanimated/types linguist-generated=true diff --git a/.github/workflows/example-android-build-check.yml b/.github/workflows/example-android-build-check.yml index 4896439e3f6e..b1c1908b9e25 100644 --- a/.github/workflows/example-android-build-check.yml +++ b/.github/workflows/example-android-build-check.yml @@ -69,11 +69,6 @@ jobs: working-directory: ${{ env.COMMON_APP_DIR }} run: yarn add react-native-worklets@workspace:"*" - - name: Use external worklets - if: ${{ inputs.use-external-worklets == 'true' }} - working-directory: ${{ env.COMMON_APP_DIR }} - run: yarn add react-native-worklets@workspace:"*" - - name: Build app working-directory: ${{ env.WORKING_DIRECTORY }}/android run: ./gradlew assembleDebug --build-cache -PreactNativeArchitectures=arm64-v8a diff --git a/.github/workflows/example-macos-build-check.yml b/.github/workflows/example-macos-build-check.yml index f21c2b32499b..9dc743964dc2 100644 --- a/.github/workflows/example-macos-build-check.yml +++ b/.github/workflows/example-macos-build-check.yml @@ -73,11 +73,6 @@ jobs: working-directory: ${{ env.COMMON_APP_DIR }} run: yarn add react-native-worklets@workspace:"*" - - name: Use external worklets - if: ${{ inputs.use-external-worklets == 'true' }} - working-directory: ${{ env.COMMON_APP_DIR }} - run: yarn add react-native-worklets@workspace:"*" - - name: Install Pods working-directory: ${{ env.WORKING_DIRECTORY }}/macos run: | diff --git a/.github/workflows/example-tvos-build-check.yml b/.github/workflows/example-tvos-build-check.yml index 3e2e42082b6c..e322f45b58b9 100644 --- a/.github/workflows/example-tvos-build-check.yml +++ b/.github/workflows/example-tvos-build-check.yml @@ -66,11 +66,6 @@ jobs: working-directory: ${{ env.COMMON_APP_DIR }} run: yarn add react-native-worklets@workspace:"*" - - name: Use external worklets - if: ${{ inputs.use-external-worklets == 'true' }} - working-directory: ${{ env.COMMON_APP_DIR }} - run: yarn add react-native-worklets@workspace:"*" - - name: Install Pods working-directory: ${{ env.WORKING_DIRECTORY }}/ios run: | diff --git a/apps/common-app/index.ts b/apps/common-app/index.ts index ac4f682e3200..942e3319daff 100644 --- a/apps/common-app/index.ts +++ b/apps/common-app/index.ts @@ -1,3 +1,34 @@ -import App from '@/App'; +/* eslint-disable no-var */ +/* eslint-disable no-inner-declarations */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable no-underscore-dangle */ + +// This detection mechanism is necessary for the time being for dynamic +// resolution of worklets. `react-native-worklets` is present in monorepo's +// node_modules therefore allowed to be imported though it's not a dependency. +let hasExternalWorklets = false; +try { + const packageJson = require('./package.json') as Record< + string, + Record + >; + hasExternalWorklets = + packageJson?.dependencies?.['react-native-worklets'] !== undefined || + packageJson?.devDependencies?.['react-native-worklets'] !== undefined; +} catch (_e) { + // Do nothing. +} + +declare global { + var __DISALLOW_WORKLETS_IMPORT: boolean | undefined; +} + +globalThis.__DISALLOW_WORKLETS_IMPORT = !hasExternalWorklets; + +// Has to be imported dynamically to inject __DISALLOW_WORKLETS_IMPORT before +// any other code is executed. +const App = require('@/App').default; export default App; diff --git a/packages/eslint-plugin-reanimated/index.js b/packages/eslint-plugin-reanimated/index.js index de7a94df7dc6..790774ed964c 100644 --- a/packages/eslint-plugin-reanimated/index.js +++ b/packages/eslint-plugin-reanimated/index.js @@ -215,6 +215,51 @@ var require_useReanimatedError = __commonJS({ }, }); +// public/useWorkletsResolver.js +var require_useWorkletsResolver = __commonJS({ + 'public/useWorkletsResolver.js'(exports2) { + 'use strict'; + Object.defineProperty(exports2, '__esModule', { value: true }); + var rule = { + create: function (context) { + return { + ImportDeclaration(node) { + const workletsRegex = /\/worklets[/.*]?/; + if (node.source.value.match(workletsRegex)) { + const replacement = node.source.value.replace( + workletsRegex, + '/WorkletsResolver' + ); + context.report({ + node, + messageId: 'useWorkletsResolver', + fix: function (fixer) { + return fixer.replaceText(node.source, `'${replacement}'`); + }, + }); + } + }, + }; + }, + meta: { + docs: { + recommended: 'recommended', + description: + "Warns when `react-native-reanimated` doesn't use `WorkletsResolver` to import worklets' files`.", + }, + messages: { + useWorkletsResolver: "Import worklets' files via WorkletsResolver.", + }, + type: 'suggestion', + schema: [], + fixable: 'code', + }, + defaultOptions: [], + }; + exports2.default = rule; + }, +}); + // public/index.js var __importDefault = (exports && exports.__importDefault) || @@ -227,8 +272,10 @@ var noAnimatedStyleToNonAnimatedComponent_1 = __importDefault( require_noAnimatedStyleToNonAnimatedComponent() ); var useReanimatedError_1 = __importDefault(require_useReanimatedError()); +var useWorkletsResolver_1 = __importDefault(require_useWorkletsResolver()); exports.rules = { 'animated-style-non-animated-component': noAnimatedStyleToNonAnimatedComponent_1.default, 'use-reanimated-error': useReanimatedError_1.default, + 'use-worklets-resolver': useWorkletsResolver_1.default, }; diff --git a/packages/eslint-plugin-reanimated/src/index.ts b/packages/eslint-plugin-reanimated/src/index.ts index aa2c987f35b0..b504826bf781 100644 --- a/packages/eslint-plugin-reanimated/src/index.ts +++ b/packages/eslint-plugin-reanimated/src/index.ts @@ -1,9 +1,11 @@ import type { TSESLint } from '@typescript-eslint/utils'; import noAnimatedStyleToNonAnimatedComponent from './noAnimatedStyleToNonAnimatedComponent'; import useReanimatedError from './useReanimatedError'; +import useWorkletsResolver from './useWorkletsResolver'; export const rules = { 'animated-style-non-animated-component': noAnimatedStyleToNonAnimatedComponent, 'use-reanimated-error': useReanimatedError, + 'use-worklets-resolver': useWorkletsResolver, } satisfies Record>>; diff --git a/packages/eslint-plugin-reanimated/src/useWorkletsResolver.ts b/packages/eslint-plugin-reanimated/src/useWorkletsResolver.ts new file mode 100644 index 000000000000..53879b5cb0bd --- /dev/null +++ b/packages/eslint-plugin-reanimated/src/useWorkletsResolver.ts @@ -0,0 +1,41 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +const rule: TSESLint.RuleModule<'useWorkletsResolver', []> = { + create: function (context) { + return { + ImportDeclaration(node: TSESTree.ImportDeclaration) { + const workletsRegex = /\/worklets[/.*]?/; + if (node.source.value.match(workletsRegex)) { + const replacement = node.source.value.replace( + workletsRegex, + '/WorkletsResolver' + ); + + context.report({ + node, + messageId: 'useWorkletsResolver', + fix: function (fixer) { + return fixer.replaceText(node.source, `'${replacement}'`); + }, + }); + } + }, + }; + }, + meta: { + docs: { + recommended: 'recommended', + description: + "Warns when `react-native-reanimated` doesn't use `WorkletsResolver` to import worklets' files`.", + }, + messages: { + useWorkletsResolver: "Import worklets' files via WorkletsResolver.", + }, + type: 'suggestion', + schema: [], + fixable: 'code', + }, + defaultOptions: [], +}; + +export default rule; diff --git a/packages/eslint-plugin-reanimated/types/index.d.ts b/packages/eslint-plugin-reanimated/types/index.d.ts index 03d482e6e7eb..97bbe348ddf0 100644 --- a/packages/eslint-plugin-reanimated/types/index.d.ts +++ b/packages/eslint-plugin-reanimated/types/index.d.ts @@ -10,4 +10,9 @@ export declare const rules: { [], TSESLint.RuleListener >; + 'use-worklets-resolver': TSESLint.RuleModule< + 'useWorkletsResolver', + [], + TSESLint.RuleListener + >; }; diff --git a/packages/eslint-plugin-reanimated/types/useWorkletsResolver.d.ts b/packages/eslint-plugin-reanimated/types/useWorkletsResolver.d.ts new file mode 100644 index 000000000000..609999649122 --- /dev/null +++ b/packages/eslint-plugin-reanimated/types/useWorkletsResolver.d.ts @@ -0,0 +1,3 @@ +import type { TSESLint } from '@typescript-eslint/utils'; +declare const rule: TSESLint.RuleModule<'useWorkletsResolver', []>; +export default rule; diff --git a/packages/react-native-reanimated/.eslintrc.js b/packages/react-native-reanimated/.eslintrc.js index c1ba8416bae1..40a7e8548382 100644 --- a/packages/react-native-reanimated/.eslintrc.js +++ b/packages/react-native-reanimated/.eslintrc.js @@ -4,9 +4,16 @@ module.exports = { overrides: [ { files: ['./src/**/*.ts', './src/**/*.tsx'], + excludedFiles: [ + './src/worklets/**/*.ts', + './src/worklets/**/*.tsx', + './src/WorkletsResolver/**/*.ts', + './src/WorkletsResolver/**/*.tsx', + ], plugins: ['reanimated'], rules: { 'reanimated/use-reanimated-error': 'error', + 'reanimated/use-worklets-resolver': 'error', }, }, ], diff --git a/packages/react-native-reanimated/.gitignore b/packages/react-native-reanimated/.gitignore index 9e966e6e2098..1359ca14d19a 100644 --- a/packages/react-native-reanimated/.gitignore +++ b/packages/react-native-reanimated/.gitignore @@ -77,3 +77,4 @@ apple/worklets Common/cpp/worklets android/src/worklets android/src/main/cpp/worklets +src/worklets diff --git a/packages/react-native-reanimated/package.json b/packages/react-native-reanimated/package.json index 93c67385a2ce..5e7c2757038c 100644 --- a/packages/react-native-reanimated/package.json +++ b/packages/react-native-reanimated/package.json @@ -19,7 +19,7 @@ "format:android:cpp": "find android/src -iname \"*.h\" -o -iname \"*.cpp\" | xargs clang-format -i", "format:android:cmake": "find ./android -type d \\( -name build -o -name .cxx \\) -prune -o -type f -name 'CMakeLists.txt' -print | xargs ./scripts/format-cmake.sh", "format:common": "find Common -iname \"*.h\" -o -iname \"*.cpp\" | xargs clang-format -i", - "find-unused-code:js": "yarn ts-prune --ignore \"index|.web.\" --error", + "find-unused-code:js": "yarn ts-prune --ignore \"index|.web.|src/worklets\" --error", "type:check": "yarn type:check:src && yarn type:check:plugin && yarn type:check:app && ./scripts/test-ts.sh __typetests__/common", "type:check:src": "yarn tsc --noEmit", "type:check:plugin": "cd plugin && yarn type:check:src", diff --git a/packages/react-native-reanimated/scripts/duplicate-worklets-code.sh b/packages/react-native-reanimated/scripts/duplicate-worklets-code.sh index 198e46c78eaf..fe35d26e8b01 100755 --- a/packages/react-native-reanimated/scripts/duplicate-worklets-code.sh +++ b/packages/react-native-reanimated/scripts/duplicate-worklets-code.sh @@ -18,17 +18,28 @@ copy_files_recursively() { done } +WORKLETS_DIR="../react-native-worklets" + +if [ ! -d "$WORKLETS_DIR" ]; then + echo "Please run this script from react-native-reanimated package" + exit 1 +fi + +# Worklets TypeScript files +# Note that we only duplicate "worklets" subdirectory +copy_files_recursively "src/worklets" "$WORKLETS_DIR/src/worklets" + # Worklets iOS files -copy_files_recursively "apple/worklets" "../react-native-worklets/apple/worklets" +copy_files_recursively "apple/worklets" "$WORKLETS_DIR/apple/worklets" # Worklets Android files # Note that we don't duplicate "android/src/main" -copy_files_recursively "android/src/worklets" "../react-native-worklets/android/src/worklets" +copy_files_recursively "android/src/worklets" "$WORKLETS_DIR/android/src/worklets" # Worklets Android-specific cpp files -copy_files_recursively "android/src/main/cpp/worklets" "../react-native-worklets/android/src/main/cpp/worklets" +copy_files_recursively "android/src/main/cpp/worklets" "$WORKLETS_DIR/android/src/main/cpp/worklets" # Worklets Common cpp files -copy_files_recursively "Common/cpp/worklets" "../react-native-worklets/Common/cpp/worklets" +copy_files_recursively "Common/cpp/worklets" "$WORKLETS_DIR/Common/cpp/worklets" echo "Done duplicating Worklets in Reanimated" diff --git a/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts b/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts index c680247d1a04..24d7644199d8 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/NativeReanimated.ts @@ -1,31 +1,35 @@ 'use strict'; +import type React from 'react'; import type { - Value3D, - ValueRotation, - ShareableRef, LayoutAnimationBatchItem, - IReanimatedModule, - IWorkletsModule, - WorkletFunction, ShadowNodeWrapper, StyleProps, + Value3D, + ValueRotation, } from '../commonTypes'; -import { checkCppVersion } from '../platform-specific/checkCppVersion'; -import { jsVersion } from '../platform-specific/jsVersion'; -import { isFabric, isWeb } from '../PlatformChecker'; -import type React from 'react'; -import { getShadowNodeWrapperFromRef } from '../fabricUtils'; -import { ReanimatedTurboModule } from '../specs'; -import { ReanimatedError } from '../errors'; -import { WorkletsModule } from '../worklets'; -import type { ReanimatedModuleProxy } from './reanimatedModuleProxy'; import type { NormalizedCSSTransitionConfig, NormalizedSingleCSSAnimationConfig, NormalizedSingleCSSAnimationSettings, } from '../css/platform/native'; +import { ReanimatedError } from '../errors'; +import { getShadowNodeWrapperFromRef } from '../fabricUtils'; +import { checkCppVersion } from '../platform-specific/checkCppVersion'; +import { jsVersion } from '../platform-specific/jsVersion'; +import { isFabric, shouldBeUseWeb } from '../PlatformChecker'; +import { ReanimatedTurboModule } from '../specs'; +import type { + ShareableRef, + WorkletFunction, + IWorkletsModule, +} from '../WorkletsResolver'; +import { WorkletsModule } from '../WorkletsResolver'; +import type { + IReanimatedModule, + ReanimatedModuleProxy, +} from './reanimatedModuleProxy'; -const IS_WEB = isWeb(); +const IS_WEB = shouldBeUseWeb(); export function createNativeReanimatedModule(): IReanimatedModule { return new NativeReanimatedModule(); diff --git a/packages/react-native-reanimated/src/ReanimatedModule/index.ts b/packages/react-native-reanimated/src/ReanimatedModule/index.ts index 0a49b2efc7ec..405799df3f77 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/index.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/index.ts @@ -1,4 +1,7 @@ 'use strict'; export { ReanimatedModule } from './reanimatedModuleInstance'; -export type { ReanimatedModuleProxy } from './reanimatedModuleProxy'; +export type { + IReanimatedModule, + ReanimatedModuleProxy, +} from './reanimatedModuleProxy'; diff --git a/packages/react-native-reanimated/src/ReanimatedModule/index.web.ts b/packages/react-native-reanimated/src/ReanimatedModule/index.web.ts index 3a7231496a33..523c6ae3c3d8 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/index.web.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/index.web.ts @@ -2,3 +2,7 @@ // this file was created to prevent NativeReanimated from being included in the web bundle import { createJSReanimatedModule } from './js-reanimated'; export const ReanimatedModule = createJSReanimatedModule(); +export type { + IReanimatedModule, + ReanimatedModuleProxy, +} from './reanimatedModuleProxy'; diff --git a/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts b/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts index ebc832bcc292..c7da70dfdafc 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/js-reanimated/JSReanimated.ts @@ -1,30 +1,32 @@ 'use strict'; -import { - isChromeDebugger, - isJest, - isWeb, - isWindowAvailable, -} from '../../PlatformChecker'; -import { SensorType } from '../../commonTypes'; import type { - IReanimatedModule, - IWorkletsModule, ShadowNodeWrapper, - ShareableRef, StyleProps, Value3D, ValueRotation, - WorkletFunction, } from '../../commonTypes'; -import type { WebSensor } from './WebSensor'; -import { logger } from '../../logger'; -import { ReanimatedError } from '../../errors'; -import { WorkletsModule } from '../../worklets'; +import { SensorType } from '../../commonTypes'; import type { NormalizedCSSTransitionConfig, NormalizedSingleCSSAnimationConfig, NormalizedSingleCSSAnimationSettings, } from '../../css/platform/native'; +import { ReanimatedError } from '../../errors'; +import { logger } from '../../logger'; +import { + isChromeDebugger, + isJest, + isWeb, + isWindowAvailable, +} from '../../PlatformChecker'; +import type { + IWorkletsModule, + ShareableRef, + WorkletFunction, +} from '../../WorkletsResolver'; +import { WorkletsModule } from '../../WorkletsResolver'; +import type { IReanimatedModule } from '../reanimatedModuleProxy'; +import type { WebSensor } from './WebSensor'; export function createJSReanimatedModule(): IReanimatedModule { return new JSReanimated(); diff --git a/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts b/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts index 15ba93c94653..d5b2e94fccd7 100644 --- a/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts +++ b/packages/react-native-reanimated/src/ReanimatedModule/reanimatedModuleProxy.ts @@ -1,19 +1,18 @@ 'use strict'; import type { - ShareableRef, + LayoutAnimationBatchItem, ShadowNodeWrapper, + StyleProps, Value3D, ValueRotation, - LayoutAnimationBatchItem, - WorkletFunction, - StyleProps, } from '../commonTypes'; import type { NormalizedCSSTransitionConfig, NormalizedSingleCSSAnimationConfig, NormalizedSingleCSSAnimationSettings, } from '../css/platform/native'; +import type { ShareableRef, WorkletFunction } from '../WorkletsResolver'; /** Type of `__reanimatedModuleProxy` injected with JSI. */ export interface ReanimatedModuleProxy { @@ -89,3 +88,13 @@ export interface ReanimatedModuleProxy { unregisterCSSTransition(viewTag: number): void; } + +export interface IReanimatedModule + extends Omit { + getViewProp( + viewTag: number, + propName: string, + component: React.Component | undefined, + callback?: (result: TValue) => void + ): Promise; +} diff --git a/packages/react-native-reanimated/src/Sensor.ts b/packages/react-native-reanimated/src/Sensor.ts index 64dc9dd6e926..3820262af901 100644 --- a/packages/react-native-reanimated/src/Sensor.ts +++ b/packages/react-native-reanimated/src/Sensor.ts @@ -5,11 +5,10 @@ import type { SharedValue, Value3D, ValueRotation, - ShareableRef, - WorkletFunction, } from './commonTypes'; import { SensorType } from './commonTypes'; import { makeMutable } from './mutables'; +import type { ShareableRef, WorkletFunction } from './WorkletsResolver'; function initSensorData( sensorType: SensorType diff --git a/packages/react-native-reanimated/src/SensorContainer.ts b/packages/react-native-reanimated/src/SensorContainer.ts index d535d4884b03..f7bb27506fea 100644 --- a/packages/react-native-reanimated/src/SensorContainer.ts +++ b/packages/react-native-reanimated/src/SensorContainer.ts @@ -1,13 +1,13 @@ 'use strict'; import type { - SensorType, SensorConfig, + SensorType, + SharedValue, Value3D, ValueRotation, - ShareableRef, - SharedValue, } from './commonTypes'; import Sensor from './Sensor'; +import type { ShareableRef } from './WorkletsResolver'; export class SensorContainer { private nativeSensors: Map = new Map(); diff --git a/packages/react-native-reanimated/src/WorkletsResolver/WorkletsResolver.ts b/packages/react-native-reanimated/src/WorkletsResolver/WorkletsResolver.ts new file mode 100644 index 000000000000..a33af7ef6471 --- /dev/null +++ b/packages/react-native-reanimated/src/WorkletsResolver/WorkletsResolver.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable reanimated/use-reanimated-error */ +'use strict'; + +let worklets; + +try { + const resolvedWorklets = require('react-native-worklets'); + // `globalThis.__DISALLOW_WORKLETS_IMPORT` applies only to our monorepo. + if (resolvedWorklets && !globalThis.__DISALLOW_WORKLETS_IMPORT) { + worklets = resolvedWorklets; + } +} catch (_e) { +} finally { + if (!worklets) { + worklets = require('../worklets'); + } +} + +module.exports = { + WorkletsModule: worklets.WorkletsModule, + isWorkletFunction: worklets.isWorkletFunction, + mockedRequestAnimationFrame: worklets.mockedRequestAnimationFrame, +}; diff --git a/packages/react-native-reanimated/src/WorkletsResolver/index.ts b/packages/react-native-reanimated/src/WorkletsResolver/index.ts new file mode 100644 index 000000000000..c0a61340b0b5 --- /dev/null +++ b/packages/react-native-reanimated/src/WorkletsResolver/index.ts @@ -0,0 +1,36 @@ +'use strict'; + +// react-native-reanimated isn't allowed to import anything directly +// from `worklets` directory. Everything should be acquired through +// the `WorkletsResolver` module. + +import type { + isWorkletFunction as isWorkletFunctionType, + IWorkletsModule, + mockedRequestAnimationFrame as mockedRequestAnimationFrameType, +} from '../worklets'; + +import { + // @ts-expect-error - required for resolving the module + WorkletsModule as ResolvedWorkletsModule, + // @ts-expect-error - required for resolving the module + isWorkletFunction as ResolvedIsWorkletFunction, + // @ts-expect-error - required for resolving the module + mockedRequestAnimationFrame as ResolvedMockedRequestAnimationFrame, +} from './WorkletsResolver'; + +export const WorkletsModule = ResolvedWorkletsModule as IWorkletsModule; +export const isWorkletFunction = + ResolvedIsWorkletFunction as typeof isWorkletFunctionType; +export const mockedRequestAnimationFrame = + ResolvedMockedRequestAnimationFrame as typeof mockedRequestAnimationFrameType; + +export type { + IWorkletsModule, + WorkletsModuleProxy, + ShareableRef, + WorkletFunction, + WorkletRuntime, + WorkletStackDetails, + WorkletFunctionDev, +} from '../worklets'; diff --git a/packages/react-native-reanimated/src/animation/util.ts b/packages/react-native-reanimated/src/animation/util.ts index b0d838b2ce6c..6c439fc71d92 100644 --- a/packages/react-native-reanimated/src/animation/util.ts +++ b/packages/react-native-reanimated/src/animation/util.ts @@ -10,7 +10,7 @@ import { toLinearSpace, clampRGBA, } from '../Colors'; -import { ReduceMotion, isWorkletFunction } from '../commonTypes'; +import { ReduceMotion } from '../commonTypes'; import type { SharedValue, AnimatableValue, @@ -40,6 +40,7 @@ import { ReducedMotionManager } from '../ReducedMotion'; import { logger } from '../logger'; import { ReanimatedError } from '../errors'; import { runOnUI } from '../threads'; +import { isWorkletFunction } from '../WorkletsResolver'; let IN_STYLE_UPDATER = false; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); diff --git a/packages/react-native-reanimated/src/commonTypes.ts b/packages/react-native-reanimated/src/commonTypes.ts index 98830b84dace..1686088048ce 100644 --- a/packages/react-native-reanimated/src/commonTypes.ts +++ b/packages/react-native-reanimated/src/commonTypes.ts @@ -1,30 +1,14 @@ 'use strict'; + import type { - ViewStyle, + ImageStyle, TextStyle, TransformsStyle, - ImageStyle, + ViewStyle, } from 'react-native'; -import type { WorkletsModuleProxy } from './worklets'; -import type { ReanimatedModuleProxy } from './ReanimatedModule'; +import type { ShareableRef, WorkletFunction } from './WorkletsResolver'; import type { CSSAnimationProperties, CSSTransitionProperties } from './css'; -type DisallowKeysOf = { - [TKey in keyof TInterface]?: never; -}; - -export interface IWorkletsModule extends WorkletsModuleProxy {} -export interface IReanimatedModule - extends Omit, - DisallowKeysOf { - getViewProp( - viewTag: number, - propName: string, - component: React.Component | undefined, - callback?: (result: TValue) => void - ): Promise; -} - export type LayoutAnimationsOptions = | 'originX' | 'originY' @@ -269,17 +253,6 @@ export interface Mutable extends SharedValue { _value: Value; } -// The below type is used for HostObjects returned by the JSI API that don't have -// any accessible fields or methods but can carry data that is accessed from the -// c++ side. We add a field to the type to make it possible for typescript to recognize -// which JSI methods accept those types as arguments and to be able to correctly type -// check other methods that may use them. However, this field is not actually defined -// nor should be used for anything else as assigning any data to those objects will -// throw an error. -export type ShareableRef = { - __hostObjectShareableJSRef: T; -}; - // In case of objects with depth or arrays of objects or arrays of arrays etc. // we add this utility type that makes it a `SharaebleRef` of the outermost type. export type FlatShareableRef = @@ -299,98 +272,6 @@ export type MapperRegistry = { stop: (mapperID: number) => void; }; -export type WorkletStackDetails = [ - error: Error, - lineOffset: number, - columnOffset: number, -]; - -type WorkletClosure = Record; - -interface WorkletInitDataCommon { - code: string; -} - -type WorkletInitDataRelease = WorkletInitDataCommon; - -interface WorkletInitDataDev extends WorkletInitDataCommon { - location: string; - sourceMap: string; - version: string; -} - -interface WorkletBaseCommon { - __closure: WorkletClosure; - __workletHash: number; -} - -interface WorkletBaseRelease extends WorkletBaseCommon { - __initData: WorkletInitDataRelease; -} - -interface WorkletBaseDev extends WorkletBaseCommon { - __initData: WorkletInitDataDev; - /** `__stackDetails` is removed after parsing. */ - __stackDetails?: WorkletStackDetails; -} - -export type WorkletFunctionDev< - Args extends unknown[] = unknown[], - ReturnValue = unknown, -> = ((...args: Args) => ReturnValue) & WorkletBaseDev; - -type WorkletFunctionRelease< - Args extends unknown[] = unknown[], - ReturnValue = unknown, -> = ((...args: Args) => ReturnValue) & WorkletBaseRelease; - -export type WorkletFunction< - Args extends unknown[] = unknown[], - ReturnValue = unknown, -> = - | WorkletFunctionDev - | WorkletFunctionRelease; - -/** - * This function allows you to determine if a given function is a worklet. It - * only works with Reanimated Babel plugin enabled. Unless you are doing - * something with internals of Reanimated you shouldn't need to use this - * function. - * - * ### Note - * - * Do not call it before the worklet is declared, as it will always return false - * then. E.g.: - * - * ```ts - * isWorkletFunction(myWorklet); // Will always return false. - * - * function myWorklet() { - * 'worklet'; - * } - * ``` - * - * ### Maintainer note - * - * This function is supposed to be used only in the React Runtime. It always - * returns `false` in Worklet Runtimes. - */ -export function isWorkletFunction< - Args extends unknown[] = unknown[], - ReturnValue = unknown, - BuildType extends WorkletBaseDev | WorkletBaseRelease = WorkletBaseDev, ->(value: unknown): value is WorkletFunction & BuildType { - 'worklet'; - // Since host objects always return true for `in` operator, we have to use dot notation to check if the property exists. - // See https://github.com/facebook/hermes/blob/340726ef8cf666a7cce75bc60b02fa56b3e54560/lib/VM/JSObject.cpp#L1276. - - return ( - // `__workletHash` isn't extracted in Worklet Runtimes. - typeof value === 'function' && - !!(value as unknown as Record).__workletHash - ); -} - export type AnimatedPropsAdapterFunction = ( props: Record ) => void; diff --git a/packages/react-native-reanimated/src/core.ts b/packages/react-native-reanimated/src/core.ts index 2a66389018ae..0d64638f9839 100644 --- a/packages/react-native-reanimated/src/core.ts +++ b/packages/react-native-reanimated/src/core.ts @@ -9,17 +9,16 @@ import type { SharedValue, Value3D, ValueRotation, - WorkletFunction, } from './commonTypes'; import { makeShareableCloneRecursive } from './shareables'; import { initializeUIRuntime } from './initializers'; import { SensorContainer } from './SensorContainer'; import { ReanimatedError } from './errors'; +import type { WorkletFunction } from './WorkletsResolver'; export { startMapper, stopMapper } from './mappers'; export { runOnJS, runOnUI, executeOnUIRuntimeSync } from './threads'; export { createWorkletRuntime, runOnRuntime } from './runtimes'; -export type { WorkletRuntime } from './runtimes'; export { makeShareable, makeShareableCloneRecursive } from './shareables'; export { makeMutable } from './mutables'; diff --git a/packages/react-native-reanimated/src/errors.ts b/packages/react-native-reanimated/src/errors.ts index 53b6a826a25a..f2adc4082d2b 100644 --- a/packages/react-native-reanimated/src/errors.ts +++ b/packages/react-native-reanimated/src/errors.ts @@ -1,6 +1,7 @@ /* eslint-disable reanimated/use-reanimated-error */ 'use strict'; -import type { WorkletStackDetails } from './commonTypes'; + +import type { WorkletStackDetails } from './WorkletsResolver'; type ReanimatedError = Error & 'ReanimatedError'; // signed type diff --git a/packages/react-native-reanimated/src/hook/commonTypes.ts b/packages/react-native-reanimated/src/hook/commonTypes.ts index 7a2b55d2ecd0..0bb1b98599ca 100644 --- a/packages/react-native-reanimated/src/hook/commonTypes.ts +++ b/packages/react-native-reanimated/src/hook/commonTypes.ts @@ -4,7 +4,6 @@ import type { AnimatedPropsAdapterFunction, ShadowNodeWrapper, SharedValue, - WorkletFunction, AnimatedStyle, } from '../commonTypes'; import type { @@ -16,6 +15,7 @@ import type { } from 'react-native'; import type { ViewDescriptorsSet } from '../ViewDescriptorsSet'; import type { ReanimatedHTMLElement } from '../ReanimatedModule/js-reanimated'; +import type { WorkletFunction } from '../WorkletsResolver'; export type DependencyList = Array | undefined; diff --git a/packages/react-native-reanimated/src/hook/useAnimatedReaction.ts b/packages/react-native-reanimated/src/hook/useAnimatedReaction.ts index abbfc5c50bb5..b70cb3a996dd 100644 --- a/packages/react-native-reanimated/src/hook/useAnimatedReaction.ts +++ b/packages/react-native-reanimated/src/hook/useAnimatedReaction.ts @@ -1,10 +1,10 @@ 'use strict'; import { useEffect } from 'react'; -import type { WorkletFunction } from '../commonTypes'; import { startMapper, stopMapper } from '../core'; import type { DependencyList } from './commonTypes'; import { useSharedValue } from './useSharedValue'; import { shouldBeUseWeb } from '../PlatformChecker'; +import type { WorkletFunction } from '../WorkletsResolver'; /** * Lets you to respond to changes in a [shared diff --git a/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts b/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts index 7f4f188f2dfb..baa86fc7c6fb 100644 --- a/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts +++ b/packages/react-native-reanimated/src/hook/useAnimatedStyle.ts @@ -28,13 +28,13 @@ import type { NestedObjectValues, SharedValue, StyleProps, - WorkletFunction, AnimatedPropsAdapterFunction, AnimatedPropsAdapterWorklet, AnimatedStyle, } from '../commonTypes'; -import { isWorkletFunction } from '../commonTypes'; import { ReanimatedError } from '../errors'; +import { isWorkletFunction } from '../WorkletsResolver'; +import type { WorkletFunction } from '../WorkletsResolver'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); diff --git a/packages/react-native-reanimated/src/hook/useComposedEventHandler.ts b/packages/react-native-reanimated/src/hook/useComposedEventHandler.ts index fb000e8ccf72..8fda9c8a8560 100644 --- a/packages/react-native-reanimated/src/hook/useComposedEventHandler.ts +++ b/packages/react-native-reanimated/src/hook/useComposedEventHandler.ts @@ -3,8 +3,8 @@ import { useEvent } from './useEvent'; import { useHandler } from './useHandler'; import { WorkletEventHandler } from '../WorkletEventHandler'; import type { ReanimatedEvent } from './commonTypes'; -import type { WorkletFunction } from '../commonTypes'; import type { EventHandlerProcessed, EventHandlerInternal } from './useEvent'; +import type { WorkletFunction } from '../WorkletsResolver'; type ComposedHandlerProcessed< Event extends object, diff --git a/packages/react-native-reanimated/src/hook/useDerivedValue.ts b/packages/react-native-reanimated/src/hook/useDerivedValue.ts index a43ef41ee1bb..38c8f23b3055 100644 --- a/packages/react-native-reanimated/src/hook/useDerivedValue.ts +++ b/packages/react-native-reanimated/src/hook/useDerivedValue.ts @@ -1,10 +1,11 @@ 'use strict'; import { useEffect, useRef } from 'react'; import { initialUpdaterRun } from '../animation'; -import type { SharedValue, WorkletFunction } from '../commonTypes'; +import type { SharedValue } from '../commonTypes'; import { makeMutable, startMapper, stopMapper } from '../core'; import type { DependencyList } from './commonTypes'; import { shouldBeUseWeb } from '../PlatformChecker'; +import type { WorkletFunction } from '../WorkletsResolver'; export interface DerivedValue extends Readonly, 'set'>> { diff --git a/packages/react-native-reanimated/src/hook/useHandler.ts b/packages/react-native-reanimated/src/hook/useHandler.ts index 6d5039aa47e1..6294767da3c4 100644 --- a/packages/react-native-reanimated/src/hook/useHandler.ts +++ b/packages/react-native-reanimated/src/hook/useHandler.ts @@ -1,10 +1,10 @@ 'use strict'; import { useEffect, useRef } from 'react'; -import type { WorkletFunction } from '../commonTypes'; import { isWeb, isJest } from '../PlatformChecker'; import type { DependencyList, ReanimatedEvent } from './commonTypes'; import { areDependenciesEqual, buildDependencies } from './utils'; import { makeShareable } from '../shareables'; +import type { WorkletFunction } from '../WorkletsResolver'; interface GeneralHandler< Event extends object, diff --git a/packages/react-native-reanimated/src/hook/utils.ts b/packages/react-native-reanimated/src/hook/utils.ts index 113e62b72891..0093c419a0ed 100644 --- a/packages/react-native-reanimated/src/hook/utils.ts +++ b/packages/react-native-reanimated/src/hook/utils.ts @@ -1,6 +1,6 @@ 'use strict'; -import type { WorkletFunction } from '../commonTypes'; import { ReanimatedError } from '../errors'; +import type { WorkletFunction } from '../WorkletsResolver'; import type { DependencyList } from './commonTypes'; // Builds one big hash from multiple worklets' hashes. diff --git a/packages/react-native-reanimated/src/index.ts b/packages/react-native-reanimated/src/index.ts index 144e01f314c9..e046ddfeb2dd 100644 --- a/packages/react-native-reanimated/src/index.ts +++ b/packages/react-native-reanimated/src/index.ts @@ -7,7 +7,6 @@ export default Animated; export { configureReanimatedLogger } from './ConfigHelper'; export { LogLevel as ReanimatedLogLevel } from './logger'; -export type { WorkletRuntime } from './core'; export { runOnJS, runOnUI, @@ -255,9 +254,10 @@ export { InterfaceOrientation, KeyboardState, ReduceMotion, - isWorkletFunction, SharedTransitionType, } from './commonTypes'; +export { isWorkletFunction } from './WorkletsResolver'; +export type { WorkletRuntime } from './WorkletsResolver'; export type { FrameInfo } from './frameCallback'; export { getUseOfValueInStyleWarning } from './pluginUtils'; export { diff --git a/packages/react-native-reanimated/src/initializers.ts b/packages/react-native-reanimated/src/initializers.ts index f3327642cda8..b05754ed25c5 100644 --- a/packages/react-native-reanimated/src/initializers.ts +++ b/packages/react-native-reanimated/src/initializers.ts @@ -8,14 +8,14 @@ import { runOnUIImmediately, executeOnUIRuntimeSync, } from './threads'; -import { mockedRequestAnimationFrame } from './mockedRequestAnimationFrame'; +import { mockedRequestAnimationFrame } from './WorkletsResolver'; import { DEFAULT_LOGGER_CONFIG, logToLogBoxAndConsole, registerLoggerConfig, replaceLoggerImplementation, } from './logger'; -import type { IReanimatedModule } from './commonTypes'; +import type { IReanimatedModule } from './ReanimatedModule'; const IS_JEST = isJest(); const SHOULD_BE_USE_WEB = shouldBeUseWeb(); diff --git a/packages/react-native-reanimated/src/privateGlobals.d.ts b/packages/react-native-reanimated/src/privateGlobals.d.ts index 932aed27f08b..49e88f2b398e 100644 --- a/packages/react-native-reanimated/src/privateGlobals.d.ts +++ b/packages/react-native-reanimated/src/privateGlobals.d.ts @@ -6,40 +6,34 @@ // If it ever breaks, we should address it so we'd not pollute the user's global namespace. import type { - StyleProps, - MeasuredDimensions, + FlatShareableRef, MapperRegistry, - ShareableRef, + MeasuredDimensions, ShadowNodeWrapper, - FlatShareableRef, + ShareableRef, + StyleProps, } from './commonTypes'; -import type { AnimatedStyle } from './helperTypes'; import type { FrameCallbackRegistryUI } from './frameCallback/FrameCallbackRegistryUI'; -import type { ReanimatedModuleProxy } from './ReanimatedModule'; -import type { WorkletsModuleProxy } from './worklets'; -import type { SensorContainer } from './SensorContainer'; +import type { AnimatedStyle } from './helperTypes'; +import type { callGuardDEV } from './initializers'; import type { LayoutAnimationsManager } from './layoutReanimation/animationsManager'; import type { ProgressTransitionRegister } from './layoutReanimation/sharedTransitions'; -import type { UpdatePropsManager } from './UpdateProps'; -import type { callGuardDEV } from './initializers'; +import type { LoggerConfigInternal } from './logger'; +import type { ReanimatedModuleProxy } from './ReanimatedModule'; import type { WorkletRuntime } from './runtimes'; import type { RNScreensTurboModuleType } from './screenTransition/commonTypes'; -import type { LoggerConfigInternal } from './logger'; +import type { SensorContainer } from './SensorContainer'; +import type { UpdatePropsManager } from './UpdateProps'; declare global { + var __DISALLOW_WORKLETS_IMPORT: boolean | undefined; var _REANIMATED_IS_REDUCED_MOTION: boolean | undefined; var _IS_FABRIC: boolean | undefined; var _REANIMATED_VERSION_CPP: string | undefined; var _REANIMATED_VERSION_JS: string | undefined; - var __workletsModuleProxy: WorkletsModuleProxy | undefined; var __reanimatedModuleProxy: ReanimatedModuleProxy | undefined; var __callGuardDEV: typeof callGuardDEV | undefined; - var evalWithSourceMap: - | ((js: string, sourceURL: string, sourceMap: string) => any) - | undefined; - var evalWithSourceUrl: ((js: string, sourceURL: string) => any) | undefined; var _log: (value: unknown) => void; - var _toString: (value: unknown) => string; var _notifyAboutProgress: ( tag: number, value: Record, @@ -104,8 +98,6 @@ declare global { var console: Console; var __frameTimestamp: number | undefined; var __flushAnimationFrame: (timestamp: number) => void; - var __workletsCache: Map; - var __handleCache: WeakMap; var __callMicrotasks: () => void; var __mapperRegistry: MapperRegistry; var __sensorContainer: SensorContainer; diff --git a/packages/react-native-reanimated/src/runtimes.ts b/packages/react-native-reanimated/src/runtimes.ts index 3237acc0e67b..bc48623fa9d5 100644 --- a/packages/react-native-reanimated/src/runtimes.ts +++ b/packages/react-native-reanimated/src/runtimes.ts @@ -1,6 +1,4 @@ 'use strict'; -import { isWorkletFunction } from './commonTypes'; -import type { WorkletFunction } from './commonTypes'; import { ReanimatedError, registerReanimatedError } from './errors'; import { setupCallGuard, setupConsole } from './initializers'; import { registerLoggerConfig } from './logger'; @@ -9,15 +7,11 @@ import { makeShareableCloneOnUIRecursive, makeShareableCloneRecursive, } from './shareables'; -import { WorkletsModule } from './worklets'; +import type { WorkletRuntime, WorkletFunction } from './WorkletsResolver'; +import { WorkletsModule, isWorkletFunction } from './WorkletsResolver'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); -export type WorkletRuntime = { - __hostObjectWorkletRuntime: never; - readonly name: string; -}; - /** * Lets you create a new JS runtime which can be used to run worklets possibly * on different threads than JS or UI thread. diff --git a/packages/react-native-reanimated/src/shareableMappingCache.ts b/packages/react-native-reanimated/src/shareableMappingCache.ts index b451d0103424..124a0446dcc4 100644 --- a/packages/react-native-reanimated/src/shareableMappingCache.ts +++ b/packages/react-native-reanimated/src/shareableMappingCache.ts @@ -1,6 +1,6 @@ 'use strict'; import { shouldBeUseWeb } from './PlatformChecker'; -import type { ShareableRef } from './commonTypes'; +import type { ShareableRef } from './WorkletsResolver'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); diff --git a/packages/react-native-reanimated/src/shareables.ts b/packages/react-native-reanimated/src/shareables.ts index ec4bdea2b331..ba2d01f647af 100644 --- a/packages/react-native-reanimated/src/shareables.ts +++ b/packages/react-native-reanimated/src/shareables.ts @@ -1,20 +1,19 @@ 'use strict'; -import { isWorkletFunction } from './commonTypes'; -import type { - ShareableRef, - FlatShareableRef, - WorkletFunction, - WorkletFunctionDev, -} from './commonTypes'; -import { shouldBeUseWeb } from './PlatformChecker'; +import type { FlatShareableRef } from './commonTypes'; import { ReanimatedError, registerWorkletStackDetails } from './errors'; +import { logger } from './logger'; import { jsVersion } from './platform-specific/jsVersion'; +import { shouldBeUseWeb } from './PlatformChecker'; import { shareableMappingCache, shareableMappingFlag, } from './shareableMappingCache'; -import { logger } from './logger'; -import { WorkletsModule } from './worklets'; +import type { + ShareableRef, + WorkletFunction, + WorkletFunctionDev, +} from './WorkletsResolver'; +import { isWorkletFunction, WorkletsModule } from './WorkletsResolver'; // for web/chrome debugger/jest environments this file provides a stub implementation // where no shareable references are used. Instead, the objects themselves are used diff --git a/packages/react-native-reanimated/src/threads.ts b/packages/react-native-reanimated/src/threads.ts index 291798b82ded..434a0931968b 100644 --- a/packages/react-native-reanimated/src/threads.ts +++ b/packages/react-native-reanimated/src/threads.ts @@ -1,13 +1,12 @@ 'use strict'; import { isJest, shouldBeUseWeb } from './PlatformChecker'; -import type { WorkletFunction } from './commonTypes'; import { makeShareableCloneOnUIRecursive, makeShareableCloneRecursive, } from './shareables'; -import { isWorkletFunction } from './commonTypes'; import { ReanimatedError } from './errors'; -import { WorkletsModule } from './worklets'; +import type { WorkletFunction } from './WorkletsResolver'; +import { WorkletsModule, isWorkletFunction } from './WorkletsResolver'; const IS_JEST = isJest(); const SHOULD_BE_USE_WEB = shouldBeUseWeb(); diff --git a/packages/react-native-reanimated/src/worklets/WorkletsModule/index.ts b/packages/react-native-reanimated/src/worklets/WorkletsModule/index.ts deleted file mode 100644 index d72476a7c167..000000000000 --- a/packages/react-native-reanimated/src/worklets/WorkletsModule/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -export { WorkletsModule } from './workletsModuleInstance'; -export type { WorkletsModuleProxy } from './workletsModuleProxy'; diff --git a/packages/react-native-reanimated/src/worklets/index.ts b/packages/react-native-reanimated/src/worklets/index.ts deleted file mode 100644 index 54bac2152fd3..000000000000 --- a/packages/react-native-reanimated/src/worklets/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -export { WorkletsModule } from './WorkletsModule'; -export type { WorkletsModuleProxy } from './WorkletsModule'; diff --git a/packages/react-native-worklets/.eslintrc.js b/packages/react-native-worklets/.eslintrc.js index 17799e759208..0d2a7e508124 100644 --- a/packages/react-native-worklets/.eslintrc.js +++ b/packages/react-native-worklets/.eslintrc.js @@ -1,4 +1,14 @@ /** @type {import('eslint').ESLint.ConfigData} */ module.exports = { extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['./src/**/*.ts', './src/**/*.tsx'], + plugins: ['reanimated'], + rules: { + // TODO: Add `use-worklets-error` rule. + }, + }, + ], + ignorePatterns: ['lib'], }; diff --git a/packages/react-native-worklets/package.json b/packages/react-native-worklets/package.json index fe84f8eb1adc..9231743647a9 100644 --- a/packages/react-native-worklets/package.json +++ b/packages/react-native-worklets/package.json @@ -34,6 +34,7 @@ "@types/react": "^18.2.44", "@typescript-eslint/eslint-plugin": "^6.19.0", "eslint": "^8.57.0", + "eslint-plugin-reanimated": "workspace:*", "madge": "^5.0.1", "prettier": "^3.3.3", "react": "18.3.1", diff --git a/packages/react-native-worklets/src/index.ts b/packages/react-native-worklets/src/index.ts index f0c2f62f3c97..b0c1251c597e 100644 --- a/packages/react-native-worklets/src/index.ts +++ b/packages/react-native-worklets/src/index.ts @@ -1,3 +1,16 @@ 'use strict'; -export { WorkletsTurboModule } from './specs'; +export { + isWorkletFunction, + mockedRequestAnimationFrame, + WorkletsModule, +} from './worklets'; +export type { + IWorkletsModule, + ShareableRef, + WorkletFunction, + WorkletFunctionDev, + WorkletRuntime, + WorkletsModuleProxy, + WorkletStackDetails, +} from './worklets'; diff --git a/packages/react-native-worklets/src/worklets/PlatformChecker.ts b/packages/react-native-worklets/src/worklets/PlatformChecker.ts new file mode 100644 index 000000000000..3dd8d6e6cd2e --- /dev/null +++ b/packages/react-native-worklets/src/worklets/PlatformChecker.ts @@ -0,0 +1,47 @@ +'use strict'; +import { Platform } from 'react-native'; + +// This type is necessary since some libraries tend to do a lib check +// and this file causes type errors on `global` access. +type localGlobal = typeof global & Record; + +export function isJest(): boolean { + return !!process.env.JEST_WORKER_ID; +} + +// `isChromeDebugger` also returns true in Jest environment, so `isJest()` check should always be performed first +export function isChromeDebugger(): boolean { + return ( + (!(global as localGlobal).nativeCallSyncHook || + !!(global as localGlobal).__REMOTEDEV__) && + !(global as localGlobal).RN$Bridgeless + ); +} + +export function isWeb(): boolean { + return Platform.OS === 'web'; +} + +export function isAndroid(): boolean { + return Platform.OS === 'android'; +} + +function isWindows(): boolean { + return Platform.OS === 'windows'; +} + +export function shouldBeUseWeb() { + return isJest() || isChromeDebugger() || isWeb() || isWindows(); +} + +export function isFabric() { + return !!(global as localGlobal)._IS_FABRIC; +} + +export function isWindowAvailable() { + // the window object is unavailable when building the server portion of a site that uses SSG + // this function shouldn't be used to conditionally render components + // https://www.joshwcomeau.com/react/the-perils-of-rehydration/ + // @ts-ignore Fallback if `window` is undefined. + return typeof window !== 'undefined'; +} diff --git a/packages/react-native-reanimated/src/worklets/WorkletsModule/JSWorklets.ts b/packages/react-native-worklets/src/worklets/WorkletsModule/JSWorklets.ts similarity index 63% rename from packages/react-native-reanimated/src/worklets/WorkletsModule/JSWorklets.ts rename to packages/react-native-worklets/src/worklets/WorkletsModule/JSWorklets.ts index 3b535f81286a..d8a6b1cd5ffd 100644 --- a/packages/react-native-reanimated/src/worklets/WorkletsModule/JSWorklets.ts +++ b/packages/react-native-worklets/src/worklets/WorkletsModule/JSWorklets.ts @@ -1,10 +1,10 @@ +/* eslint-disable reanimated/use-reanimated-error */ 'use strict'; -import type { IWorkletsModule, ShareableRef } from '../../commonTypes'; -import { ReanimatedError } from '../../errors'; -import { mockedRequestAnimationFrame } from '../../mockedRequestAnimationFrame'; -import { isJest } from '../../PlatformChecker'; -import type { WorkletRuntime } from '../../runtimes'; +import type { ShareableRef, WorkletRuntime } from '../workletTypes'; +import type { IWorkletsModule } from './workletsModuleProxy'; +import { mockedRequestAnimationFrame } from '../mockedRequestAnimationFrame'; +import { isJest } from '../PlatformChecker'; export function createJSWorkletsModule(): IWorkletsModule { return new JSWorklets(); @@ -20,19 +20,21 @@ const requestAnimationFrameImpl = class JSWorklets implements IWorkletsModule { makeShareableClone(): ShareableRef { - throw new ReanimatedError( - 'makeShareableClone should never be called in JSWorklets.' + throw new Error( + '[Worklets] makeShareableClone should never be called in JSWorklets.' ); } scheduleOnUI(worklet: ShareableRef) { + // TODO: `requestAnimationFrame` should be used exclusively in Reanimated + // @ts-ignore web implementation has still not been updated after the rewrite, // this will be addressed once the web implementation updates are ready requestAnimationFrameImpl(worklet); } executeOnUIRuntimeSync(_shareable: ShareableRef): R { - throw new ReanimatedError( + throw new Error( '`executeOnUIRuntimeSync` is not available in JSReanimated.' ); } @@ -41,14 +43,10 @@ class JSWorklets implements IWorkletsModule { _name: string, _initializer: ShareableRef<() => void> ): WorkletRuntime { - throw new ReanimatedError( - 'createWorkletRuntime is not available in JSReanimated.' - ); + throw new Error('createWorkletRuntime is not available in JSReanimated.'); } scheduleOnRuntime() { - throw new ReanimatedError( - 'scheduleOnRuntime is not available in JSReanimated.' - ); + throw new Error('scheduleOnRuntime is not available in JSReanimated.'); } } diff --git a/packages/react-native-reanimated/src/worklets/WorkletsModule/NativeWorklets.ts b/packages/react-native-worklets/src/worklets/WorkletsModule/NativeWorklets.ts similarity index 86% rename from packages/react-native-reanimated/src/worklets/WorkletsModule/NativeWorklets.ts rename to packages/react-native-worklets/src/worklets/WorkletsModule/NativeWorklets.ts index 16243cefe2e6..549b76971b8a 100644 --- a/packages/react-native-reanimated/src/worklets/WorkletsModule/NativeWorklets.ts +++ b/packages/react-native-worklets/src/worklets/WorkletsModule/NativeWorklets.ts @@ -1,10 +1,12 @@ +/* eslint-disable reanimated/use-reanimated-error */ 'use strict'; -import { getValueUnpackerCode } from '../valueUnpacker'; + import { WorkletsTurboModule } from '../../specs'; -import { ReanimatedError } from '../../errors'; -import type { IWorkletsModule, ShareableRef } from '../../commonTypes'; +import { getValueUnpackerCode } from '../valueUnpacker'; import type { WorkletsModuleProxy } from './workletsModuleProxy'; -import type { WorkletRuntime } from '../../runtimes'; +import type { ShareableRef, WorkletRuntime } from '../workletTypes'; + +export interface IWorkletsModule extends WorkletsModuleProxy {} export function createNativeWorkletsModule(): IWorkletsModule { return new NativeWorklets(); @@ -19,8 +21,8 @@ class NativeWorklets { WorkletsTurboModule?.installTurboModule(valueUnpackerCode); } if (global.__workletsModuleProxy === undefined) { - throw new ReanimatedError( - `Native part of Reanimated doesn't seem to be initialized (Worklets). + throw new Error( + `[Worklets] Native part of Worklets doesn't seem to be initialized. See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#native-part-of-reanimated-doesnt-seem-to-be-initialized for more details.` ); } diff --git a/packages/react-native-worklets/src/worklets/WorkletsModule/index.ts b/packages/react-native-worklets/src/worklets/WorkletsModule/index.ts new file mode 100644 index 000000000000..afde45c7ea7a --- /dev/null +++ b/packages/react-native-worklets/src/worklets/WorkletsModule/index.ts @@ -0,0 +1,7 @@ +'use strict'; + +export { WorkletsModule } from './workletsModuleInstance'; +export type { + IWorkletsModule, + WorkletsModuleProxy, +} from './workletsModuleProxy'; diff --git a/packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleInstance.ts b/packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleInstance.ts similarity index 81% rename from packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleInstance.ts rename to packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleInstance.ts index 483059436a08..52e0573bfb5a 100644 --- a/packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleInstance.ts +++ b/packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleInstance.ts @@ -1,7 +1,7 @@ 'use strict'; import { createNativeWorkletsModule } from './NativeWorklets'; -import { shouldBeUseWeb } from '../../PlatformChecker'; +import { shouldBeUseWeb } from '../PlatformChecker'; import { createJSWorkletsModule } from './JSWorklets'; export const WorkletsModule = shouldBeUseWeb() diff --git a/packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleInstance.web.ts b/packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleInstance.web.ts similarity index 100% rename from packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleInstance.web.ts rename to packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleInstance.web.ts diff --git a/packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleProxy.ts b/packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleProxy.ts similarity index 82% rename from packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleProxy.ts rename to packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleProxy.ts index df96918633a2..a760108bc0f4 100644 --- a/packages/react-native-reanimated/src/worklets/WorkletsModule/workletsModuleProxy.ts +++ b/packages/react-native-worklets/src/worklets/WorkletsModule/workletsModuleProxy.ts @@ -1,7 +1,6 @@ 'use strict'; -import type { ShareableRef } from '../../commonTypes'; -import type { WorkletRuntime } from '../../runtimes'; +import type { ShareableRef, WorkletRuntime } from '../workletTypes'; /** Type of `__workletsModuleProxy` injected with JSI. */ export interface WorkletsModuleProxy { @@ -27,3 +26,5 @@ export interface WorkletsModuleProxy { worklet: ShareableRef ): void; } + +export interface IWorkletsModule extends WorkletsModuleProxy {} diff --git a/packages/react-native-worklets/src/worklets/index.ts b/packages/react-native-worklets/src/worklets/index.ts new file mode 100644 index 000000000000..6f38586d4a22 --- /dev/null +++ b/packages/react-native-worklets/src/worklets/index.ts @@ -0,0 +1,13 @@ +'use strict'; + +export { WorkletsModule } from './WorkletsModule'; +export { isWorkletFunction } from './workletFunction'; +export { mockedRequestAnimationFrame } from './mockedRequestAnimationFrame'; +export type { IWorkletsModule, WorkletsModuleProxy } from './WorkletsModule'; +export type { + ShareableRef, + WorkletFunction, + WorkletRuntime, + WorkletStackDetails, + WorkletFunctionDev, +} from './workletTypes'; diff --git a/packages/react-native-reanimated/src/mockedRequestAnimationFrame.ts b/packages/react-native-worklets/src/worklets/mockedRequestAnimationFrame.ts similarity index 66% rename from packages/react-native-reanimated/src/mockedRequestAnimationFrame.ts rename to packages/react-native-worklets/src/worklets/mockedRequestAnimationFrame.ts index b8cba83d4029..380d91efc952 100644 --- a/packages/react-native-reanimated/src/mockedRequestAnimationFrame.ts +++ b/packages/react-native-worklets/src/worklets/mockedRequestAnimationFrame.ts @@ -1,5 +1,7 @@ 'use strict'; +// This file should be exclusively used in `react-native-reanimated`, but until we get an actual API in `react-native-worklets` we need to keep it here. + // This is Jest implementation of `requestAnimationFrame` that is required // by React Native for test purposes. export function mockedRequestAnimationFrame( diff --git a/packages/react-native-worklets/src/worklets/privateGlobals.d.ts b/packages/react-native-worklets/src/worklets/privateGlobals.d.ts new file mode 100644 index 000000000000..99311799045f --- /dev/null +++ b/packages/react-native-worklets/src/worklets/privateGlobals.d.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-var */ +'use strict'; + +// This file works by accident - currently Builder Bob doesn't move `.d.ts` files to output types. +// If it ever breaks, we should address it so we'd not pollute the user's global namespace. +import type { WorkletsModuleProxy } from './WorkletsModule'; + +declare global { + var __workletsCache: Map; + var __handleCache: WeakMap; + var evalWithSourceMap: + | ((js: string, sourceURL: string, sourceMap: string) => any) + | undefined; + var evalWithSourceUrl: ((js: string, sourceURL: string) => any) | undefined; + var _toString: (value: unknown) => string; + var __workletsModuleProxy: WorkletsModuleProxy | undefined; +} diff --git a/packages/react-native-reanimated/src/worklets/valueUnpacker.ts b/packages/react-native-worklets/src/worklets/valueUnpacker.ts similarity index 96% rename from packages/react-native-reanimated/src/worklets/valueUnpacker.ts rename to packages/react-native-worklets/src/worklets/valueUnpacker.ts index 3945f3fb3788..71f2598ed1af 100644 --- a/packages/react-native-reanimated/src/worklets/valueUnpacker.ts +++ b/packages/react-native-worklets/src/worklets/valueUnpacker.ts @@ -1,8 +1,8 @@ /* eslint-disable reanimated/use-reanimated-error */ 'use strict'; -import { shouldBeUseWeb } from '../PlatformChecker'; -import { isWorkletFunction } from '../commonTypes'; -import type { WorkletFunction } from '../commonTypes'; +import { shouldBeUseWeb } from './PlatformChecker'; +import { isWorkletFunction } from './workletFunction'; +import type { WorkletFunction } from './workletTypes'; function valueUnpacker( objectToUnpack: any, diff --git a/packages/react-native-worklets/src/worklets/workletFunction.ts b/packages/react-native-worklets/src/worklets/workletFunction.ts new file mode 100644 index 000000000000..54aa71c2bfd3 --- /dev/null +++ b/packages/react-native-worklets/src/worklets/workletFunction.ts @@ -0,0 +1,47 @@ +'use strict'; + +import type { + WorkletFunction, + WorkletBaseDev, + WorkletBaseRelease, +} from './workletTypes'; + +/** + * This function allows you to determine if a given function is a worklet. It + * only works with Reanimated Babel plugin enabled. Unless you are doing + * something with internals of Reanimated you shouldn't need to use this + * function. + * + * ### Note + * + * Do not call it before the worklet is declared, as it will always return false + * then. E.g.: + * + * ```ts + * isWorkletFunction(myWorklet); // Will always return false. + * + * function myWorklet() { + * 'worklet'; + * } + * ``` + * + * ### Maintainer note + * + * This function is supposed to be used only in the React Runtime. It always + * returns `false` in Worklet Runtimes. + */ +export function isWorkletFunction< + Args extends unknown[] = unknown[], + ReturnValue = unknown, + BuildType extends WorkletBaseDev | WorkletBaseRelease = WorkletBaseDev, +>(value: unknown): value is WorkletFunction & BuildType { + 'worklet'; + // Since host objects always return true for `in` operator, we have to use dot notation to check if the property exists. + // See https://github.com/facebook/hermes/blob/340726ef8cf666a7cce75bc60b02fa56b3e54560/lib/VM/JSObject.cpp#L1276. + + return ( + // `__workletHash` isn't extracted in Worklet Runtimes. + typeof value === 'function' && + !!(value as unknown as Record).__workletHash + ); +} diff --git a/packages/react-native-worklets/src/worklets/workletTypes.ts b/packages/react-native-worklets/src/worklets/workletTypes.ts new file mode 100644 index 000000000000..98544d47b837 --- /dev/null +++ b/packages/react-native-worklets/src/worklets/workletTypes.ts @@ -0,0 +1,71 @@ +'use strict'; + +/** + * The below type is used for HostObjects returned by the JSI API that don't + * have any accessible fields or methods but can carry data that is accessed + * from the c++ side. We add a field to the type to make it possible for + * typescript to recognize which JSI methods accept those types as arguments and + * to be able to correctly type check other methods that may use them. However, + * this field is not actually defined nor should be used for anything else as + * assigning any data to those objects will throw an error. + */ +export type ShareableRef = { + __hostObjectShareableJSRef: T; +}; + +export type WorkletRuntime = { + __hostObjectWorkletRuntime: never; + readonly name: string; +}; + +export type WorkletStackDetails = [ + error: Error, + lineOffset: number, + columnOffset: number, +]; + +type WorkletClosure = Record; + +interface WorkletInitDataCommon { + code: string; +} + +type WorkletInitDataRelease = WorkletInitDataCommon; + +interface WorkletInitDataDev extends WorkletInitDataCommon { + location: string; + sourceMap: string; + version: string; +} + +interface WorkletBaseCommon { + __closure: WorkletClosure; + __workletHash: number; +} + +export interface WorkletBaseRelease extends WorkletBaseCommon { + __initData: WorkletInitDataRelease; +} + +export interface WorkletBaseDev extends WorkletBaseCommon { + __initData: WorkletInitDataDev; + /** `__stackDetails` is removed after parsing. */ + __stackDetails?: WorkletStackDetails; +} + +export type WorkletFunctionDev< + Args extends unknown[] = unknown[], + ReturnValue = unknown, +> = ((...args: Args) => ReturnValue) & WorkletBaseDev; + +type WorkletFunctionRelease< + Args extends unknown[] = unknown[], + ReturnValue = unknown, +> = ((...args: Args) => ReturnValue) & WorkletBaseRelease; + +export type WorkletFunction< + Args extends unknown[] = unknown[], + ReturnValue = unknown, +> = + | WorkletFunctionDev + | WorkletFunctionRelease; diff --git a/yarn.lock b/yarn.lock index f0a1ca93c076..9fee73274b2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18192,6 +18192,7 @@ __metadata: "@types/react": "npm:^18.2.44" "@typescript-eslint/eslint-plugin": "npm:^6.19.0" eslint: "npm:^8.57.0" + eslint-plugin-reanimated: "workspace:*" madge: "npm:^5.0.1" prettier: "npm:^3.3.3" react: "npm:18.3.1"