diff --git a/apps/common-app/src/apps/reanimated/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/apps/reanimated/examples/RuntimeTests/RuntimeTestsExample.tsx index 8090d21d861a..d724989b809c 100644 --- a/apps/common-app/src/apps/reanimated/examples/RuntimeTests/RuntimeTestsExample.tsx +++ b/apps/common-app/src/apps/reanimated/examples/RuntimeTests/RuntimeTestsExample.tsx @@ -76,6 +76,14 @@ export default function RuntimeTestsExample() { require('./tests/runtimes/scheduleOnRuntimeWithId.test'); }, }, + { + testSuiteName: 'bundle mode core', + importTest: () => { + require('./tests/runtimes/reactNativeImportShim.test'); + require('./tests/runtimes/turboModuleRegistryShim.test'); + }, + disabled: !globalThis._WORKLETS_BUNDLE_MODE_ENABLED, + }, { testSuiteName: 'run loop', importTest: () => { diff --git a/apps/common-app/src/apps/reanimated/examples/RuntimeTests/tests/runtimes/reactNativeImportShim.test.ts b/apps/common-app/src/apps/reanimated/examples/RuntimeTests/tests/runtimes/reactNativeImportShim.test.ts new file mode 100644 index 000000000000..518d855b32f9 --- /dev/null +++ b/apps/common-app/src/apps/reanimated/examples/RuntimeTests/tests/runtimes/reactNativeImportShim.test.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { runOnUISync, runOnRuntimeSync } from 'react-native-worklets'; +import { + describe, + expect, + getWorkletRuntimeFromPool, + test, +} from '../../ReJest/RuntimeTestsApi'; + +describe("importing 'react-native' on Worklet Runtimes", () => { + const workerRuntime = getWorkletRuntimeFromPool('test'); + + const testFn = globalThis._WORKLETS_BUNDLE_MODE_ENABLED ? test : test.skip; + + const targets = [ + { + targetRuntime: 'UI', + runOnTarget: (worklet: () => TReturn) => runOnUISync(worklet), + }, + { + targetRuntime: 'Worker', + runOnTarget: (worklet: () => TReturn) => + runOnRuntimeSync(workerRuntime, worklet), + }, + ]; + + targets.forEach(({ targetRuntime, runOnTarget }) => { + describe(`on ${targetRuntime} Runtime`, () => { + testFn('works without access', () => { + const status = runOnTarget(() => { + 'worklet'; + const reactNative = require('react-native'); + return typeof reactNative === 'object'; + }); + + expect(status).toBe(true); + }); + + testFn('throws when accessing unsafe APIs', async () => { + await expect(() => { + runOnTarget(() => { + 'worklet'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const platform = require('react-native').Platform; + console.log(platform); + }); + }).toThrow(); + }); + }); + }); +}); diff --git a/apps/common-app/src/apps/reanimated/examples/RuntimeTests/tests/runtimes/turboModuleRegistryShim.test.ts b/apps/common-app/src/apps/reanimated/examples/RuntimeTests/tests/runtimes/turboModuleRegistryShim.test.ts new file mode 100644 index 000000000000..772e68c864e9 --- /dev/null +++ b/apps/common-app/src/apps/reanimated/examples/RuntimeTests/tests/runtimes/turboModuleRegistryShim.test.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { runOnUISync, runOnRuntimeSync } from 'react-native-worklets'; +import { + describe, + expect, + getWorkletRuntimeFromPool, + test, +} from '../../ReJest/RuntimeTestsApi'; + +const expectedErrorMessage = + '[Worklets] Accessing TurboModules is not allowed on Worklet Runtimes.'; + +const bundleModeEnabled = !!globalThis._WORKLETS_BUNDLE_MODE_ENABLED; +const proxy = globalThis.__workletsModuleProxy as + | { getStaticFeatureFlag: (name: string) => boolean } + | undefined; +const fetchPreviewEnabled = + bundleModeEnabled && !!proxy?.getStaticFeatureFlag('FETCH_PREVIEW_ENABLED'); + +describe('accessing Turbo Modules on Worklet Runtimes', () => { + const workerRuntime = getWorkletRuntimeFromPool('test'); + + const testFn = bundleModeEnabled ? test : test.skip; + const previewOnFn = fetchPreviewEnabled ? test : test.skip; + const previewOffFn = + bundleModeEnabled && !fetchPreviewEnabled ? test : test.skip; + + const targets = [ + { + targetRuntime: 'UI', + runOnTarget: (worklet: () => T) => runOnUISync(worklet), + }, + { + targetRuntime: 'Worker', + runOnTarget: (worklet: () => T) => + runOnRuntimeSync(workerRuntime, worklet), + }, + ]; + + targets.forEach(({ targetRuntime, runOnTarget }) => { + describe(`on ${targetRuntime} Runtime`, () => { + testFn('throws for non-polyfilled modules', () => { + const errorMessage = runOnTarget(() => { + 'worklet'; + try { + ( + require('react-native/Libraries/TurboModule/TurboModuleRegistry') as TurboModuleRegistryShape + ).get('NonExistentTurboModule'); + return null; + } catch (e) { + return (e as Error).message; + } + }); + + expect(errorMessage).toInclude(expectedErrorMessage); + }); + + previewOffFn( + 'throws for Networking when `FETCH_PREVIEW_ENABLED` is off', + () => { + const errorMessage = runOnTarget(() => { + 'worklet'; + try { + ( + require('react-native/Libraries/TurboModule/TurboModuleRegistry') as TurboModuleRegistryShape + ).get('Networking'); + return null; + } catch (e) { + return (e as Error).message; + } + }); + + expect(errorMessage).toInclude(expectedErrorMessage); + } + ); + + previewOnFn( + 'returns the Networking polyfill when `FETCH_PREVIEW_ENABLED` is on', + () => { + const outcome = runOnTarget(() => { + 'worklet'; + try { + const value = ( + require('react-native/Libraries/TurboModule/TurboModuleRegistry') as TurboModuleRegistryShape + ).get('Networking'); + return value !== undefined ? 'ok' : 'undefined'; + } catch (e) { + return (e as Error).message; + } + }); + + expect(outcome).toInclude('ok'); + } + ); + }); + }); +}); + +type TurboModuleRegistryShape = { get: (name: string) => unknown }; diff --git a/packages/react-native-worklets/bundleMode/index.js b/packages/react-native-worklets/bundleMode/index.js index 4fc79a11abbe..3f96067f6d89 100644 --- a/packages/react-native-worklets/bundleMode/index.js +++ b/packages/react-native-worklets/bundleMode/index.js @@ -1,6 +1,28 @@ const path = require('path'); const workletsPackageParentDir = path.resolve(__dirname, '../..'); +const reactNativeShimPath = path.join(__dirname, 'shims', 'reactNativeShim.js'); +const turboModuleRegistryShimPath = path.join( + __dirname, + 'shims', + 'turboModuleRegistryShim.js' +); +const turboModuleRegistryModuleName = + 'react-native/Libraries/TurboModule/TurboModuleRegistry'; +const turboModuleRegistryFileSuffix = path.join( + 'react-native', + 'Libraries', + 'TurboModule', + 'TurboModuleRegistry.js' +); + +function isResolvedTurboModuleRegistry(/** @type {any} */ result) { + return ( + result?.type === 'sourceFile' && + typeof result.filePath === 'string' && + result.filePath.endsWith(turboModuleRegistryFileSuffix) + ); +} const workletsPackageName = 'react-native-worklets'; const workletsDirPath = path.posix.join(workletsPackageName, '.worklets'); @@ -26,11 +48,30 @@ function bundleModeResolveRequest( const fullModuleName = path.join(workletsPackageParentDir, moduleName); return { type: 'sourceFile', filePath: fullModuleName }; } - return (userConfigResolveRequest || context.resolveRequest)( + if ( + moduleName === 'react-native' && + context.originModulePath !== reactNativeShimPath + ) { + return { type: 'sourceFile', filePath: reactNativeShimPath }; + } + if ( + moduleName === turboModuleRegistryModuleName && + context.originModulePath !== turboModuleRegistryShimPath + ) { + return { type: 'sourceFile', filePath: turboModuleRegistryShimPath }; + } + const resolved = (userConfigResolveRequest || context.resolveRequest)( context, moduleName, platform ); + if ( + context.originModulePath !== turboModuleRegistryShimPath && + isResolvedTurboModuleRegistry(resolved) + ) { + return { type: 'sourceFile', filePath: turboModuleRegistryShimPath }; + } + return resolved; } /** Use in React Native Community projects. */ @@ -48,7 +89,26 @@ const bundleModeMetroConfig = { const fullModuleName = path.join(workletsPackageParentDir, moduleName); return { type: 'sourceFile', filePath: fullModuleName }; } - return context.resolveRequest(context, moduleName, platform); + if ( + moduleName === 'react-native' && + context.originModulePath !== reactNativeShimPath + ) { + return { type: 'sourceFile', filePath: reactNativeShimPath }; + } + if ( + moduleName === turboModuleRegistryModuleName && + context.originModulePath !== turboModuleRegistryShimPath + ) { + return { type: 'sourceFile', filePath: turboModuleRegistryShimPath }; + } + const resolved = context.resolveRequest(context, moduleName, platform); + if ( + context.originModulePath !== turboModuleRegistryShimPath && + isResolvedTurboModuleRegistry(resolved) + ) { + return { type: 'sourceFile', filePath: turboModuleRegistryShimPath }; + } + return resolved; }, }, }; diff --git a/packages/react-native-worklets/bundleMode/shims/reactNativeShim.js b/packages/react-native-worklets/bundleMode/shims/reactNativeShim.js new file mode 100644 index 000000000000..c711083ce542 --- /dev/null +++ b/packages/react-native-worklets/bundleMode/shims/reactNativeShim.js @@ -0,0 +1,21 @@ +'use strict'; + +if ( + globalThis.__RUNTIME_KIND === undefined || + globalThis.__RUNTIME_KIND === 1 +) { + globalThis.__RUNTIME_KIND = 1; +} else if (__DEV__) { + globalThis.__fbBatchedBridgeConfig = new Proxy( + {}, + { + get() { + throw new Error( + '[Worklets] Accessing __fbBatchedBridgeConfig is not allowed on Worklet Runtimes. Perhaps you tried to access a Turbo Module?' + ); + }, + } + ); +} + +module.exports = require('react-native'); diff --git a/packages/react-native-worklets/bundleMode/shims/tsconfig.json b/packages/react-native-worklets/bundleMode/shims/tsconfig.json new file mode 100644 index 000000000000..6d8e2c66af54 --- /dev/null +++ b/packages/react-native-worklets/bundleMode/shims/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "es6", + "module": "nodenext", + "skipLibCheck": true, + "moduleResolution": "nodenext", + "esModuleInterop": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "checkJs": true, + "noEmit": true + }, + "include": ["."] +} diff --git a/packages/react-native-worklets/bundleMode/shims/turboModuleRegistryShim.js b/packages/react-native-worklets/bundleMode/shims/turboModuleRegistryShim.js new file mode 100644 index 000000000000..172104b00b35 --- /dev/null +++ b/packages/react-native-worklets/bundleMode/shims/turboModuleRegistryShim.js @@ -0,0 +1,47 @@ +'use strict'; + +/** @type {((name: string) => unknown) | undefined} */ +let get; +/** @type {((name: string) => unknown) | undefined} */ +let getEnforcing; + +if ( + globalThis.__RUNTIME_KIND === undefined || + globalThis.__RUNTIME_KIND === 1 +) { + globalThis.__RUNTIME_KIND = 1; + + const TurboModuleRegistry = require('react-native/Libraries/TurboModule/TurboModuleRegistry'); + + get = TurboModuleRegistry.get; + getEnforcing = TurboModuleRegistry.getEnforcing; +} else { + /** @type {Map | undefined} */ + let TurboModulesPolyfill; + + if ( + globalThis.__workletsModuleProxy.getStaticFeatureFlag( + 'FETCH_PREVIEW_ENABLED' + ) + ) { + TurboModulesPolyfill = new Map(); + TurboModulesPolyfill.set('Networking', {}); + + globalThis.TurboModules = TurboModulesPolyfill; + } + + function getPolyfill(/** @type {string} */ name) { + if (__DEV__ && !TurboModulesPolyfill?.has(name)) { + throw new Error( + '[Worklets] Accessing TurboModules is not allowed on Worklet Runtimes.' + ); + } else { + return TurboModulesPolyfill?.get(name); + } + } + + get = getPolyfill; + getEnforcing = getPolyfill; +} + +export { get, getEnforcing }; diff --git a/packages/react-native-worklets/bundleMode/shims/types.ts b/packages/react-native-worklets/bundleMode/shims/types.ts new file mode 100644 index 000000000000..20da14aef8d2 --- /dev/null +++ b/packages/react-native-worklets/bundleMode/shims/types.ts @@ -0,0 +1,13 @@ +export {}; + +declare global { + var __workletsModuleProxy: { + getStaticFeatureFlag: (name: string) => boolean; + }; + + var __fbBatchedBridgeConfig: unknown; + + var __RUNTIME_KIND: 1 | 2 | 3 | undefined; + + var TurboModules: Map | undefined; +} diff --git a/packages/react-native-worklets/package.json b/packages/react-native-worklets/package.json index 9b2732955f8c..b183a72e5ccf 100644 --- a/packages/react-native-worklets/package.json +++ b/packages/react-native-worklets/package.json @@ -110,6 +110,7 @@ "compatibility.json", "bundleMode/index.js", "bundleMode/index.d.ts", + "bundleMode/shims/*.js", "scripts/worklets_utils.rb", "scripts/validate-react-native-version.js", "jest", diff --git a/packages/react-native-worklets/src/bundleMode/metroOverrides.native.ts b/packages/react-native-worklets/src/bundleMode/metroOverrides.native.ts index 5788b49c3697..c2d3839312e3 100644 --- a/packages/react-native-worklets/src/bundleMode/metroOverrides.native.ts +++ b/packages/react-native-worklets/src/bundleMode/metroOverrides.native.ts @@ -24,105 +24,6 @@ export function silenceHMRWarnings() { globalThis.__r.Refresh = Refresh; } -/** - * Importing `react-native` on Worklet Runtimes will result in a crash due to - * the fact that React Native will try to set itself up as on the React Native - * runtime. To provide better developer experience we override the main React - * Native module with a proxy that puts an actionable warning. - * - * Note that this doesn't affect deep imports. - * - * Use only in dev builds. - */ -export function disallowRNImports() { - assertWorkletRuntime('disallowRNImports'); - - const modules = require.getModules(); - const ReactNativeModuleId = require.resolveWeak('react-native'); - - const moduleFactory = makeModuleFactory((module) => { - module.exports = new Proxy( - {}, - { - get: function get(_target, prop) { - globalThis.console.warn( - `You tried to import '${String( - prop - )}' from 'react-native' module on a Worklet Runtime. Using 'react-native' module on a Worklet Runtime is not allowed.`, - new Error().stack - ); - return { - get() { - return undefined; - }, - }; - }, - } - ); - }); - - const mockModule = { - dependencyMap: [], - moduleFactory, - hasError: false, - importedAll: {}, - importedDefault: {}, - isInitialized: false, - publicModule: { - exports: {}, - }, - }; - - modules.set(ReactNativeModuleId, mockModule); -} - -/** - * To use code from React Native that obtains TurboModules we need to mock the - * registry even if the TurboModules aren't actually used. - * - * This is needed for example for the XHR setup code that is imported from React - * Native. - */ -export function mockTurboModuleRegistry() { - const modules = require.getModules(); - - const TurboModuleRegistryId = require.resolveWeak( - 'react-native/Libraries/TurboModule/TurboModuleRegistry' - ); - - const TurboModules = new Map(); - - TurboModules.set('Networking', {}); - - globalThis.TurboModules = TurboModules; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const moduleFactory = makeModuleFactory((module: any) => { - function get(name: string) { - return globalThis.TurboModules.get(name); - } - function getEnforcing(name: string) { - return globalThis.TurboModules.get(name); - } - module.exports.get = get; - module.exports.getEnforcing = getEnforcing; - }); - - const metroModule = { - dependencyMap: [], - factory: moduleFactory, - hasError: false, - importedAll: {}, - importedDefault: {}, - isInitialized: false, - publicModule: { - exports: {}, - }, - }; - - modules.set(TurboModuleRegistryId, metroModule); -} - function assertWorkletRuntime(functionName: string) { if (!isWorkletRuntime()) { throw new Error( @@ -130,20 +31,3 @@ function assertWorkletRuntime(functionName: string) { ); } } - -/** Module factory mimicking the one used by Metro bundler. */ -function makeModuleFactory( - moduleImpl: (moduleExports: Record) => void -) { - return function ( - _global: unknown, - _require: unknown, - _importDefault: unknown, - _importAll: unknown, - module: Record, - _exports: unknown, - _dependencyMap: unknown - ) { - moduleImpl(module); - }; -} diff --git a/packages/react-native-worklets/src/initializers/initializers.native.ts b/packages/react-native-worklets/src/initializers/initializers.native.ts index 9625b802047e..dbeaf3af7575 100644 --- a/packages/react-native-worklets/src/initializers/initializers.native.ts +++ b/packages/react-native-worklets/src/initializers/initializers.native.ts @@ -1,10 +1,6 @@ 'use strict'; -import { - disallowRNImports, - mockTurboModuleRegistry, - silenceHMRWarnings, -} from '../bundleMode/metroOverrides'; +import { silenceHMRWarnings } from '../bundleMode/metroOverrides'; import { initializeNetworking } from '../bundleMode/network'; import { registerReportFatalRemoteError } from '../debug/errors'; import { getStaticFeatureFlag } from '../featureFlags/featureFlags'; @@ -174,11 +170,9 @@ function initializeWorkletRuntime() { if (globalThis._WORKLETS_BUNDLE_MODE_ENABLED) { if (__DEV__) { silenceHMRWarnings(); - disallowRNImports(); } if (getStaticFeatureFlag('FETCH_PREVIEW_ENABLED')) { - mockTurboModuleRegistry(); initializeNetworking(); } }