From a1b64b7f15c475415977d2f57afe55ca6c6e4881 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Wed, 21 Jun 2023 18:30:09 -0700 Subject: [PATCH] Introduce SampleLegacyModule example in RNTester (#38008) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38008 Introduce a legacy module (+ example) in RNTester. In the future, SampleLegacyModule will be used to: - Showcase the TurboModule interop layer in RNTester, once Bridgeless mode is ready - E2E Test the TurboModule interop layer. The TurboModule interop layer is just an extension to the TurboModule system that allows the system to create legacy modules. Unlike regular TurboModules, these legacy modules don't need codegen for JavaScript -> native method dispatch. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D46874160 fbshipit-source-id: f9810d0bdb3bd0c0a74099fcb6f74ca547977a53 --- .../ios/ReactCommon/RCTSampleLegacyModule.h | 16 ++ .../ios/ReactCommon/RCTSampleLegacyModule.mm | 225 ++++++++++++++++++ .../TurboModule/LegacyModuleExample.js | 29 +++ .../TurboModule/SampleLegacyModuleExample.js | 188 +++++++++++++++ .../rn-tester/js/utils/RNTesterList.ios.js | 4 + 5 files changed, 462 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h create mode 100644 packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm create mode 100644 packages/rn-tester/js/examples/TurboModule/LegacyModuleExample.js create mode 100644 packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h new file mode 100644 index 00000000000000..0f1e4ff55926e8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import +#import + +@interface RCTSampleLegacyModule : NSObject +@end diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm new file mode 100644 index 00000000000000..b69fa307973cf8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm @@ -0,0 +1,225 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTSampleLegacyModule.h" + +@implementation RCTSampleLegacyModule { + RCTBridge *_bridge; +} + +// Backward-compatible export +RCT_EXPORT_MODULE() + +// Backward-compatible queue configuration ++ (BOOL)requiresMainQueueSetup +{ + return YES; +} + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +// Backward compatible invalidation +- (void)invalidate +{ + // Actually do nothing here. + NSLog(@"Invalidating RCTSampleTurboModule..."); +} + +- (NSDictionary *)getConstants +{ + __block NSDictionary *constants; + RCTUnsafeExecuteOnMainQueueSync(^{ + UIScreen *mainScreen = UIScreen.mainScreen; + CGSize screenSize = mainScreen.bounds.size; + + constants = @{ + @"const1" : @YES, + @"const2" : @(screenSize.width), + @"const3" : @"something", + }; + }); + + return constants; +} + +// TODO: Remove once fully migrated to TurboModule. +- (NSDictionary *)constantsToExport +{ + return [self getConstants]; +} + +RCT_EXPORT_METHOD(voidFunc) +{ + // Nothing to do +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getBool : (BOOL)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getEnum : (double)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getNumber : (double)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getFloat : (float)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getInt : (int)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getLongLong : (int64_t)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getUnsignedLongLong : (uint64_t)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getNSInteger : (NSInteger)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getNSUInteger : (NSUInteger)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSArray> *, getArray : (NSArray *)arg) +{ + return arg; +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, getObject : (NSDictionary *)arg) +{ + return arg; +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getString : (NSString *)arg) +{ + return arg; +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getNSNumber : (nonnull NSNumber *)arg) +{ + return arg; +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, getUnsafeObject : (NSDictionary *)arg) +{ + return arg; +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, getRootTag : (double)arg) +{ + return @(arg); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, getValue : (double)x y : (NSString *)y z : (NSDictionary *)z) +{ + return @{ + @"x" : @(x), + @"y" : y ?: [NSNull null], + @"z" : z ?: [NSNull null], + }; +} + +RCT_EXPORT_METHOD(getValueWithCallback : (RCTResponseSenderBlock)callback) +{ + if (!callback) { + return; + } + callback(@[ @"value from callback!" ]); +} + +RCT_EXPORT_METHOD(getValueWithPromise + : (BOOL)error resolve + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) +{ + if (!resolve || !reject) { + return; + } + + if (error) { + reject( + @"code_1", + @"intentional promise rejection", + [NSError errorWithDomain:@"RCTSampleTurboModule" code:1 userInfo:nil]); + } else { + resolve(@"result!"); + } +} + +RCT_EXPORT_METHOD(voidFuncThrows) +{ + NSException *myException = [NSException exceptionWithName:@"Excepption" + reason:@"Intentional exception from ObjC voidFuncThrows" + userInfo:nil]; + @throw myException; +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, getObjectThrows : (NSDictionary *)arg) +{ + NSException *myException = [NSException exceptionWithName:@"Excepption" + reason:@"Intentional exception from ObjC getObjectThrows" + userInfo:nil]; + @throw myException; +} + +RCT_EXPORT_METHOD(promiseThrows + : (BOOL)error resolve + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) +{ + NSException *myException = [NSException exceptionWithName:@"Excepption" + reason:@"Intentional exception from ObjC promiseThrows" + userInfo:nil]; + @throw myException; +} + +RCT_EXPORT_METHOD(voidFuncAssert) +{ + RCTAssert(false, @"Intentional assert from ObjC voidFuncAssert"); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, getObjectAssert : (NSDictionary *)arg) +{ + RCTAssert(false, @"Intentional assert from ObjC getObjectAssert"); + return arg; +} + +RCT_EXPORT_METHOD(promiseAssert + : (BOOL)error resolve + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) +{ + RCTAssert(false, @"Intentional assert from ObjC promiseAssert"); +} + +@end diff --git a/packages/rn-tester/js/examples/TurboModule/LegacyModuleExample.js b/packages/rn-tester/js/examples/TurboModule/LegacyModuleExample.js new file mode 100644 index 00000000000000..65d269e4aa7014 --- /dev/null +++ b/packages/rn-tester/js/examples/TurboModule/LegacyModuleExample.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const React = require('react'); +const { + default: SampleLegacyModuleExample, +} = require('./SampleLegacyModuleExample'); + +exports.displayName = (undefined: ?string); +exports.title = 'Legacy Native Module'; +exports.category = 'Basic'; +exports.description = 'Usage of legacy Native Module'; +exports.examples = [ + { + title: 'SampleLegacyModule', + render: function (): React.Element { + return ; + }, + }, +]; diff --git a/packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js new file mode 100644 index 00000000000000..8d08c79e88e683 --- /dev/null +++ b/packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js @@ -0,0 +1,188 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import type {RootTag} from 'react-native/Libraries/ReactNative/RootTag'; + +import * as React from 'react'; +import { + FlatList, + NativeModules, + RootTagContext, + Text, + TouchableOpacity, + View, +} from 'react-native'; + +import styles from './TurboModuleExampleCommon'; + +type State = {| + testResults: { + [string]: { + type: string, + value: mixed, + ... + }, + ... + }, +|}; + +let triedLoadingModuleOnce = false; +let module = null; + +function getSampleLegacyModule() { + if (triedLoadingModuleOnce) { + return module; + } + triedLoadingModuleOnce = true; + try { + module = NativeModules.SampleLegacyModule; + } catch (ex) { + console.error('Failed to load SampleLegacyModule. Message: ' + ex.message); + } + return module; +} + +class SampleLegacyModuleExample extends React.Component<{||}, State> { + static contextType: React$Context = RootTagContext; + + state: State = { + testResults: {}, + }; + + // Add calls to methods in TurboModule here + // $FlowFixMe[missing-local-annot] + _tests = { + voidFunc: () => getSampleLegacyModule()?.voidFunc(), + getBool: () => getSampleLegacyModule()?.getBool(true), + getEnum: () => getSampleLegacyModule()?.getEnum(1.0), + getNumber: () => getSampleLegacyModule()?.getNumber(99.95), + getFloat: () => getSampleLegacyModule()?.getNumber(99.95), + getInt: () => getSampleLegacyModule()?.getInt(99), + getLongLong: () => getSampleLegacyModule()?.getLongLong(99), + getUnsignedLongLong: () => getSampleLegacyModule()?.getUnsignedLongLong(99), + getNSInteger: () => getSampleLegacyModule()?.getNSInteger(99), + getNSUInteger: () => getSampleLegacyModule()?.getNSUInteger(99), + getArray: () => + getSampleLegacyModule()?.getArray([ + {a: 1, b: 'foo'}, + {a: 2, b: 'bar'}, + null, + ]), + getObject: () => + getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), + getString: () => getSampleLegacyModule()?.getString('Hello'), + getNullString: () => getSampleLegacyModule()?.getString(null), + getNSNumber: () => getSampleLegacyModule()?.getNSNumber(20.0), + getUnsafeObject: () => + getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), + getRootTag: () => getSampleLegacyModule()?.getRootTag(this.context), + getValue: () => + getSampleLegacyModule()?.getValue(5, 'test', {a: 1, b: 'foo'}), + callback: () => + getSampleLegacyModule()?.getValueWithCallback(callbackValue => + this._setResult('callback', callbackValue), + ), + promise: () => + getSampleLegacyModule() + ?.getValueWithPromise(false) + .then(valuePromise => this._setResult('promise', valuePromise)), + rejectPromise: () => + getSampleLegacyModule() + ?.getValueWithPromise(true) + .then(() => {}) + .catch(e => this._setResult('rejectPromise', e.message)), + // voidFuncThrows: () => getSampleLegacyModule()?.voidFuncThrows(), + // getObjectThrows: () => getSampleLegacyModule()?.getObjectThrows({}), + // promiseThrows: () => getSampleLegacyModule()?.promiseThrows(true), + // voidFuncAssert: () => getSampleLegacyModule()?.voidFuncAssert(), + // getObjectAssert: () => getSampleLegacyModule()?.getObjectAssert({}), + // promiseAssert: () => getSampleLegacyModule()?.promiseAssert(true), + getConstants: () => getSampleLegacyModule()?.getConstants(), + getConst1: () => getSampleLegacyModule()?.const1, + getConst2: () => getSampleLegacyModule()?.const2, + getConst3: () => getSampleLegacyModule()?.const3, + }; + + _setResult(name: string, result: mixed) { + this.setState(({testResults}) => ({ + /* $FlowFixMe[cannot-spread-indexer] (>=0.122.0 site=react_native_fb) + * This comment suppresses an error found when Flow v0.122.0 was + * deployed. To see the error, delete this comment and run Flow. */ + testResults: { + ...testResults, + /* $FlowFixMe[invalid-computed-prop] (>=0.111.0 site=react_native_fb) + * This comment suppresses an error found when Flow v0.111 was + * deployed. To see the error, delete this comment and run Flow. */ + [name]: {value: result, type: typeof result}, + }, + })); + } + + _renderResult(name: string): React.Node { + const result = this.state.testResults[name] || {}; + return ( + + {JSON.stringify(result.value)} + {result.type} + + ); + } + + _getContent(): React.Node { + if (getSampleLegacyModule() == null) { + return null; + } + + return ( + <> + + + Object.keys(this._tests).forEach(item => { + try { + this._setResult(item, this._tests[item]()); + } catch (ex) { + this._setResult(item, 'Fail: ' + ex.message); + } + }) + }> + Run all tests + + this.setState({testResults: {}})} + style={[styles.column, styles.button]}> + Clear results + + + item} + renderItem={({item}) => ( + + this._setResult(item, this._tests[item]())}> + {item} + + {this._renderResult(item)} + + )} + /> + + ); + } + + render(): React.Node { + return {this._getContent()}; + } +} + +export default SampleLegacyModuleExample; diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index 019887eb9e08d4..63e31e4edd31ef 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -262,6 +262,10 @@ const APIs: Array = [ key: 'TurboModuleExample', module: require('../examples/TurboModule/TurboModuleExample'), }, + { + key: 'LegacyModuleExample', + module: require('../examples/TurboModule/LegacyModuleExample'), + }, { key: 'TurboCxxModuleExample', module: require('../examples/TurboModule/TurboCxxModuleExample'),