diff --git a/packages/react-native-codegen/e2e/deep_imports/__test_fixtures__/modules/NativeImportedEnumTurboModule.js b/packages/react-native-codegen/e2e/deep_imports/__test_fixtures__/modules/NativeImportedEnumTurboModule.js new file mode 100644 index 000000000000..73ab3cf53d20 --- /dev/null +++ b/packages/react-native-codegen/e2e/deep_imports/__test_fixtures__/modules/NativeImportedEnumTurboModule.js @@ -0,0 +1,28 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import type { + SharedNumEnum, + SharedStateType, + SharedStatusEnum, +} from './SharedEnumTypes'; +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getStatus: (statusProp: SharedStateType) => SharedStatusEnum; + +getNum: () => SharedNumEnum; + +setStatus: (status: SharedStatusEnum) => void; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeImportedEnumTurboModule', +): Spec); diff --git a/packages/react-native-codegen/e2e/deep_imports/__test_fixtures__/modules/SharedEnumTypes.js b/packages/react-native-codegen/e2e/deep_imports/__test_fixtures__/modules/SharedEnumTypes.js new file mode 100644 index 000000000000..fc20c9a5a021 --- /dev/null +++ b/packages/react-native-codegen/e2e/deep_imports/__test_fixtures__/modules/SharedEnumTypes.js @@ -0,0 +1,26 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +export enum SharedStatusEnum { + Active = 'active', + Paused = 'paused', + Off = 'off', +} + +export enum SharedNumEnum { + One = 1, + Two = 2, + Three = 3, +} + +export type SharedStateType = { + status: SharedStatusEnum, + count: number, +}; diff --git a/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap b/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap index 12b33f60c4df..70ed4e4251d1 100644 --- a/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap +++ b/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap @@ -419,6 +419,153 @@ private: }; +#pragma mark - SharedEnumTypesSharedNumEnum + +enum class SharedEnumTypesSharedNumEnum { One, Two, Three }; + +template <> +struct Bridging { + static SharedEnumTypesSharedNumEnum fromJs(jsi::Runtime &rt, const jsi::Value &rawValue) { + double value = (double)rawValue.asNumber(); + if (value == 1) { + return SharedEnumTypesSharedNumEnum::One; + } else if (value == 2) { + return SharedEnumTypesSharedNumEnum::Two; + } else if (value == 3) { + return SharedEnumTypesSharedNumEnum::Three; + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for value in SharedEnumTypesSharedNumEnum\\"); + } + } + + static jsi::Value toJs(jsi::Runtime &rt, SharedEnumTypesSharedNumEnum value) { + if (value == SharedEnumTypesSharedNumEnum::One) { + return bridging::toJs(rt, 1); + } else if (value == SharedEnumTypesSharedNumEnum::Two) { + return bridging::toJs(rt, 2); + } else if (value == SharedEnumTypesSharedNumEnum::Three) { + return bridging::toJs(rt, 3); + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for enum value in SharedEnumTypesSharedNumEnum\\"); + } + } +}; + +#pragma mark - SharedEnumTypesSharedStatusEnum + +enum class SharedEnumTypesSharedStatusEnum { Active, Paused, Off }; + +template <> +struct Bridging { + static SharedEnumTypesSharedStatusEnum fromJs(jsi::Runtime &rt, const jsi::String &rawValue) { + std::string value = rawValue.utf8(rt); + if (value == \\"active\\") { + return SharedEnumTypesSharedStatusEnum::Active; + } else if (value == \\"paused\\") { + return SharedEnumTypesSharedStatusEnum::Paused; + } else if (value == \\"off\\") { + return SharedEnumTypesSharedStatusEnum::Off; + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for value in SharedEnumTypesSharedStatusEnum\\"); + } + } + + static jsi::String toJs(jsi::Runtime &rt, SharedEnumTypesSharedStatusEnum value) { + if (value == SharedEnumTypesSharedStatusEnum::Active) { + return bridging::toJs(rt, \\"active\\"); + } else if (value == SharedEnumTypesSharedStatusEnum::Paused) { + return bridging::toJs(rt, \\"paused\\"); + } else if (value == SharedEnumTypesSharedStatusEnum::Off) { + return bridging::toJs(rt, \\"off\\"); + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for enum value in SharedEnumTypesSharedStatusEnum\\"); + } + } +}; +#pragma mark - SharedEnumTypesSharedStateType + +template +struct SharedEnumTypesSharedStateType { + P0 status{}; + P1 count; + bool operator==(const SharedEnumTypesSharedStateType &other) const { + return status == other.status && count == other.count; + } +}; + +template +struct SharedEnumTypesSharedStateTypeBridging { + static T types; + + static T fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + T result{ + bridging::fromJs(rt, value.getProperty(rt, \\"status\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"count\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String statusToJs(jsi::Runtime &rt, decltype(types.status) value) { + return bridging::toJs(rt, value); + } + static double countToJs(jsi::Runtime &rt, decltype(types.count) value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const T &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"status\\", bridging::toJs(rt, value.status, jsInvoker)); + result.setProperty(rt, \\"count\\", bridging::toJs(rt, value.count, jsInvoker)); + return result; + } +}; + + +template +class JSI_EXPORT NativeImportedEnumTurboModuleCxxSpec : public TurboModule { +public: + static constexpr std::string_view kModuleName = \\"NativeImportedEnumTurboModule\\"; + +protected: + NativeImportedEnumTurboModuleCxxSpec(std::shared_ptr jsInvoker) : TurboModule(std::string{NativeImportedEnumTurboModuleCxxSpec::kModuleName}, jsInvoker) { + methodMap_[\\"getStatus\\"] = MethodMetadata {.argCount = 1, .invoker = __getStatus}; + methodMap_[\\"getNum\\"] = MethodMetadata {.argCount = 0, .invoker = __getNum}; + methodMap_[\\"setStatus\\"] = MethodMetadata {.argCount = 1, .invoker = __setStatus}; + } + +private: + static jsi::Value __getStatus(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_assert( + bridging::getParameterCount(&T::getStatus) == 2, + \\"Expected getStatus(...) to have 2 parameters\\"); + return bridging::callFromJs(rt, &T::getStatus, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule), + count <= 0 ? throw jsi::JSError(rt, \\"Expected argument in position 0 to be passed\\") : args[0].asObject(rt)); + } + + static jsi::Value __getNum(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) { + static_assert( + bridging::getParameterCount(&T::getNum) == 1, + \\"Expected getNum(...) to have 1 parameters\\"); + return bridging::callFromJs(rt, &T::getNum, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule)); + } + + static jsi::Value __setStatus(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_assert( + bridging::getParameterCount(&T::setStatus) == 2, + \\"Expected setStatus(...) to have 2 parameters\\"); + bridging::callFromJs(rt, &T::setStatus, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule), + count <= 0 ? throw jsi::JSError(rt, \\"Expected argument in position 0 to be passed\\") : args[0].asString(rt));return jsi::Value::undefined(); + } +}; + + template class JSI_EXPORT NativeNullableTurboModuleCxxSpec : public TurboModule { public: @@ -2016,6 +2163,153 @@ private: }; +#pragma mark - SharedEnumTypesSharedNumEnum + +enum class SharedEnumTypesSharedNumEnum { One, Two, Three }; + +template <> +struct Bridging { + static SharedEnumTypesSharedNumEnum fromJs(jsi::Runtime &rt, const jsi::Value &rawValue) { + double value = (double)rawValue.asNumber(); + if (value == 1) { + return SharedEnumTypesSharedNumEnum::One; + } else if (value == 2) { + return SharedEnumTypesSharedNumEnum::Two; + } else if (value == 3) { + return SharedEnumTypesSharedNumEnum::Three; + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for value in SharedEnumTypesSharedNumEnum\\"); + } + } + + static jsi::Value toJs(jsi::Runtime &rt, SharedEnumTypesSharedNumEnum value) { + if (value == SharedEnumTypesSharedNumEnum::One) { + return bridging::toJs(rt, 1); + } else if (value == SharedEnumTypesSharedNumEnum::Two) { + return bridging::toJs(rt, 2); + } else if (value == SharedEnumTypesSharedNumEnum::Three) { + return bridging::toJs(rt, 3); + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for enum value in SharedEnumTypesSharedNumEnum\\"); + } + } +}; + +#pragma mark - SharedEnumTypesSharedStatusEnum + +enum class SharedEnumTypesSharedStatusEnum { Active, Paused, Off }; + +template <> +struct Bridging { + static SharedEnumTypesSharedStatusEnum fromJs(jsi::Runtime &rt, const jsi::String &rawValue) { + std::string value = rawValue.utf8(rt); + if (value == \\"active\\") { + return SharedEnumTypesSharedStatusEnum::Active; + } else if (value == \\"paused\\") { + return SharedEnumTypesSharedStatusEnum::Paused; + } else if (value == \\"off\\") { + return SharedEnumTypesSharedStatusEnum::Off; + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for value in SharedEnumTypesSharedStatusEnum\\"); + } + } + + static jsi::String toJs(jsi::Runtime &rt, SharedEnumTypesSharedStatusEnum value) { + if (value == SharedEnumTypesSharedStatusEnum::Active) { + return bridging::toJs(rt, \\"active\\"); + } else if (value == SharedEnumTypesSharedStatusEnum::Paused) { + return bridging::toJs(rt, \\"paused\\"); + } else if (value == SharedEnumTypesSharedStatusEnum::Off) { + return bridging::toJs(rt, \\"off\\"); + } else { + throw jsi::JSError(rt, \\"No appropriate enum member found for enum value in SharedEnumTypesSharedStatusEnum\\"); + } + } +}; +#pragma mark - SharedEnumTypesSharedStateType + +template +struct SharedEnumTypesSharedStateType { + P0 status{}; + P1 count; + bool operator==(const SharedEnumTypesSharedStateType &other) const { + return status == other.status && count == other.count; + } +}; + +template +struct SharedEnumTypesSharedStateTypeBridging { + static T types; + + static T fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + T result{ + bridging::fromJs(rt, value.getProperty(rt, \\"status\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"count\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String statusToJs(jsi::Runtime &rt, decltype(types.status) value) { + return bridging::toJs(rt, value); + } + static double countToJs(jsi::Runtime &rt, decltype(types.count) value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const T &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"status\\", bridging::toJs(rt, value.status, jsInvoker)); + result.setProperty(rt, \\"count\\", bridging::toJs(rt, value.count, jsInvoker)); + return result; + } +}; + + +template +class JSI_EXPORT NativeImportedEnumTurboModuleCxxSpec : public TurboModule { +public: + static constexpr std::string_view kModuleName = \\"NativeImportedEnumTurboModule\\"; + +protected: + NativeImportedEnumTurboModuleCxxSpec(std::shared_ptr jsInvoker) : TurboModule(std::string{NativeImportedEnumTurboModuleCxxSpec::kModuleName}, jsInvoker) { + methodMap_[\\"getStatus\\"] = MethodMetadata {.argCount = 1, .invoker = __getStatus}; + methodMap_[\\"getNum\\"] = MethodMetadata {.argCount = 0, .invoker = __getNum}; + methodMap_[\\"setStatus\\"] = MethodMetadata {.argCount = 1, .invoker = __setStatus}; + } + +private: + static jsi::Value __getStatus(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_assert( + bridging::getParameterCount(&T::getStatus) == 2, + \\"Expected getStatus(...) to have 2 parameters\\"); + return bridging::callFromJs(rt, &T::getStatus, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule), + count <= 0 ? throw jsi::JSError(rt, \\"Expected argument in position 0 to be passed\\") : args[0].asObject(rt)); + } + + static jsi::Value __getNum(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) { + static_assert( + bridging::getParameterCount(&T::getNum) == 1, + \\"Expected getNum(...) to have 1 parameters\\"); + return bridging::callFromJs(rt, &T::getNum, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule)); + } + + static jsi::Value __setStatus(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_assert( + bridging::getParameterCount(&T::setStatus) == 2, + \\"Expected setStatus(...) to have 2 parameters\\"); + bridging::callFromJs(rt, &T::setStatus, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule), + count <= 0 ? throw jsi::JSError(rt, \\"Expected argument in position 0 to be passed\\") : args[0].asString(rt));return jsi::Value::undefined(); + } +}; + + template class JSI_EXPORT NativeNullableTurboModuleCxxSpec : public TurboModule { public: diff --git a/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap b/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap index 9bf2025a3a4f..d11e7a9f9724 100644 --- a/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap +++ b/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap @@ -179,6 +179,48 @@ namespace facebook::react { NativeEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms); }; } // namespace facebook::react +namespace JS { + namespace NativeImportedEnumTurboModule { + struct SharedStateType { + NSString *status() const; + double count() const; + + SharedStateType(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeImportedEnumTurboModule_SharedStateType) ++ (RCTManagedPointer *)JS_NativeImportedEnumTurboModule_SharedStateType:(id)json; +@end +@protocol NativeImportedEnumTurboModuleSpec + +- (NSString *)getStatus:(JS::NativeImportedEnumTurboModule::SharedStateType &)statusProp; +- (NSNumber *)getNum; +- (void)setStatus:(NSString *)status; + +@end + +@interface NativeImportedEnumTurboModuleSpecBase : NSObject { +@protected +facebook::react::EventEmitterCallback _eventEmitterCallback; +} +- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper; + + +@end + +namespace facebook::react { + /** + * ObjC++ class for module 'NativeImportedEnumTurboModule' + */ + class JSI_EXPORT NativeImportedEnumTurboModuleSpecJSI : public ObjCTurboModule { + public: + NativeImportedEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms); + }; +} // namespace facebook::react @protocol NativeNullableTurboModuleSpec @@ -1262,6 +1304,16 @@ inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::lowerCase() cons id const p = _v[@\\"lowerCase\\"]; return RCTBridgingToString(p); } +inline NSString *JS::NativeImportedEnumTurboModule::SharedStateType::status() const +{ + id const p = _v[@\\"status\\"]; + return RCTBridgingToString(p); +} +inline double JS::NativeImportedEnumTurboModule::SharedStateType::count() const +{ + id const p = _v[@\\"count\\"]; + return RCTBridgingToDouble(p); +} inline bool JS::NativeObjectTurboModule::SpecDifficultObjectAE::D() const @@ -1733,6 +1785,48 @@ namespace facebook::react { NativeEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms); }; } // namespace facebook::react +namespace JS { + namespace NativeImportedEnumTurboModule { + struct SharedStateType { + NSString *status() const; + double count() const; + + SharedStateType(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeImportedEnumTurboModule_SharedStateType) ++ (RCTManagedPointer *)JS_NativeImportedEnumTurboModule_SharedStateType:(id)json; +@end +@protocol NativeImportedEnumTurboModuleSpec + +- (NSString *)getStatus:(JS::NativeImportedEnumTurboModule::SharedStateType &)statusProp; +- (NSNumber *)getNum; +- (void)setStatus:(NSString *)status; + +@end + +@interface NativeImportedEnumTurboModuleSpecBase : NSObject { +@protected +facebook::react::EventEmitterCallback _eventEmitterCallback; +} +- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper; + + +@end + +namespace facebook::react { + /** + * ObjC++ class for module 'NativeImportedEnumTurboModule' + */ + class JSI_EXPORT NativeImportedEnumTurboModuleSpecJSI : public ObjCTurboModule { + public: + NativeImportedEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms); + }; +} // namespace facebook::react @protocol NativeNullableTurboModuleSpec @@ -2816,6 +2910,16 @@ inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::lowerCase() cons id const p = _v[@\\"lowerCase\\"]; return RCTBridgingToString(p); } +inline NSString *JS::NativeImportedEnumTurboModule::SharedStateType::status() const +{ + id const p = _v[@\\"status\\"]; + return RCTBridgingToString(p); +} +inline double JS::NativeImportedEnumTurboModule::SharedStateType::count() const +{ + id const p = _v[@\\"count\\"]; + return RCTBridgingToDouble(p); +} inline bool JS::NativeObjectTurboModule::SpecDifficultObjectAE::D() const @@ -3294,6 +3398,49 @@ namespace facebook::react { } } // namespace facebook::react +@implementation NativeImportedEnumTurboModuleSpecBase + + +- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper +{ + _eventEmitterCallback = std::move(eventEmitterCallbackWrapper->_eventEmitterCallback); +} +@end + +@implementation RCTCxxConvert (NativeImportedEnumTurboModule_SharedStateType) ++ (RCTManagedPointer *)JS_NativeImportedEnumTurboModule_SharedStateType:(id)json +{ + return facebook::react::managedPointer(json); +} +@end +namespace facebook::react { + + static facebook::jsi::Value __hostFunction_NativeImportedEnumTurboModuleSpecJSI_getStatus(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, StringKind, \\"getStatus\\", @selector(getStatus:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeImportedEnumTurboModuleSpecJSI_getNum(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, NumberKind, \\"getNum\\", @selector(getNum), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeImportedEnumTurboModuleSpecJSI_setStatus(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, \\"setStatus\\", @selector(setStatus:), args, count); + } + + NativeImportedEnumTurboModuleSpecJSI::NativeImportedEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params) { + + methodMap_[\\"getStatus\\"] = MethodMetadata {1, __hostFunction_NativeImportedEnumTurboModuleSpecJSI_getStatus}; + setMethodArgConversionSelector(@\\"getStatus\\", 0, @\\"JS_NativeImportedEnumTurboModule_SharedStateType:\\"); + + methodMap_[\\"getNum\\"] = MethodMetadata {0, __hostFunction_NativeImportedEnumTurboModuleSpecJSI_getNum}; + + + methodMap_[\\"setStatus\\"] = MethodMetadata {1, __hostFunction_NativeImportedEnumTurboModuleSpecJSI_setStatus}; + + } +} // namespace facebook::react + @implementation NativeNullableTurboModuleSpecBase diff --git a/packages/react-native-codegen/src/CodegenSchema.js b/packages/react-native-codegen/src/CodegenSchema.js index 90043816eeb5..939c64b7f55f 100644 --- a/packages/react-native-codegen/src/CodegenSchema.js +++ b/packages/react-native-codegen/src/CodegenSchema.js @@ -285,6 +285,12 @@ export type NativeModuleSchema = Readonly<{ // TODO: It's clearer to define `restrictedToPlatforms` instead, but // `excludedPlatforms` is used here to be consistent with ComponentSchema. excludedPlatforms?: ReadonlyArray, + // Maps imported type names to their source module haste name. + // Used by code generators to prefix these types with the source module + // name instead of the consuming module name, enabling shared types + // across multiple TurboModule specs. + importedAliasNames?: Readonly<{[aliasName: string]: string}>, + importedEnumNames?: Readonly<{[enumName: string]: string}>, }>; type NativeModuleSpec = Readonly<{ diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js index dddf253913ac..bc21303beca8 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js @@ -319,6 +319,8 @@ function createStructsString( aliasMap: NativeModuleAliasMap, resolveAlias: AliasResolver, enumMap: NativeModuleEnumMap, + importedAliasNames?: $FlowFixMe, + emittedSharedStructs?: Set, ): string { const getCppType = ( parentObjectAlias: string, @@ -340,7 +342,19 @@ function createStructsString( if (value.properties.length === 0) { return ''; } - const structName = `${hasteModuleName}${alias}`; + const sourceModule: string | void = + importedAliasNames != null ? importedAliasNames[alias] : undefined; + // Skip shared types that have already been emitted by another module + if (sourceModule != null && emittedSharedStructs != null) { + if (emittedSharedStructs.has(alias)) { + return ''; + } + emittedSharedStructs.add(alias); + } + const structName = + sourceModule != null + ? `${sourceModule}${alias}` + : `${hasteModuleName}${alias}`; const templateParameter = value.properties.filter( v => !isDirectRecursiveMember(alias, v.typeAnnotation) && @@ -549,11 +563,22 @@ function createEnums( hasteModuleName: string, enumMap: NativeModuleEnumMap, resolveAlias: AliasResolver, + importedEnumNames?: $FlowFixMe, + emittedSharedEnums?: Set, ): string { return Object.entries(enumMap) .map(([enumName, enumNode]) => { + const sourceModule: string | void = + importedEnumNames != null ? importedEnumNames[enumName] : undefined; + // Skip shared enums that have already been emitted by another module + if (sourceModule != null && emittedSharedEnums != null) { + if (emittedSharedEnums.has(enumName)) { + return ''; + } + emittedSharedEnums.add(enumName); + } return generateEnum( - hasteModuleName, + sourceModule != null ? sourceModule : hasteModuleName, enumName, enumNode.members, enumNode.memberType, @@ -687,6 +712,10 @@ module.exports = { ): FilesOutput { const nativeModules = getModules(schema); + // Track emitted shared types across modules to avoid duplicates + const emittedSharedStructs: Set = new Set(); + const emittedSharedEnums: Set = new Set(); + const modules = Object.keys(nativeModules).flatMap(hasteModuleName => { const nativeModule = nativeModules[hasteModuleName]; const { @@ -695,6 +724,8 @@ module.exports = { spec: {methods}, spec, moduleName, + importedAliasNames, + importedEnumNames, } = nativeModule; const resolveAlias = createAliasResolver(aliasMap); const structs = createStructsString( @@ -702,8 +733,16 @@ module.exports = { aliasMap, resolveAlias, enumMap, + importedAliasNames, + emittedSharedStructs, + ); + const enums = createEnums( + hasteModuleName, + enumMap, + resolveAlias, + importedEnumNames, + emittedSharedEnums, ); - const enums = createEnums(hasteModuleName, enumMap, resolveAlias); return [ ModuleSpecClassDeclarationTemplate({ hasteModuleName, diff --git a/packages/react-native-codegen/src/parsers/__tests__/cross-file-imports-test.js b/packages/react-native-codegen/src/parsers/__tests__/cross-file-imports-test.js new file mode 100644 index 000000000000..806ef8c2e02f --- /dev/null +++ b/packages/react-native-codegen/src/parsers/__tests__/cross-file-imports-test.js @@ -0,0 +1,416 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +const {FlowParser} = require('../flow/parser'); +const {TypeScriptParser} = require('../typescript/parser'); + +const SHARED_ENUM_TYPES_FLOW = ` +/** + * @flow strict-local + * @format + */ + +export enum SharedStatusEnum { + Active = 'active', + Paused = 'paused', + Off = 'off', +} + +export enum SharedNumEnum { + One = 1, + Two = 2, + Three = 3, +} + +export type SharedStateType = { + status: SharedStatusEnum, + count: number, +}; +`; + +const SPEC_WITH_IMPORTS_FLOW = ` +/** + * @flow strict-local + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {SharedStatusEnum, SharedNumEnum, SharedStateType} from './SharedEnumTypes'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getStatus: (statusProp: SharedStateType) => SharedStatusEnum; + +getNum: () => SharedNumEnum; + +setStatus: (status: SharedStatusEnum) => void; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeImportedEnumTurboModule', +): Spec); +`; + +const SHARED_ENUM_TYPES_TS = ` +export enum SharedStatusEnum { + Active = 'active', + Paused = 'paused', + Off = 'off', +} + +export enum SharedNumEnum { + One = 1, + Two = 2, + Three = 3, +} + +export type SharedStateType = { + status: SharedStatusEnum; + count: number; +}; +`; + +const SPEC_WITH_IMPORTS_TS = ` +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {SharedStatusEnum, SharedNumEnum, SharedStateType} from './SharedEnumTypes'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getStatus(statusProp: SharedStateType): SharedStatusEnum; + getNum(): SharedNumEnum; + setStatus(status: SharedStatusEnum): void; +} + +export default TurboModuleRegistry.getEnforcing( + 'NativeImportedEnumTurboModule', +); +`; + +function getNativeModule(schema: $FlowFixMe, moduleName: string): $FlowFixMe { + const module = schema.modules[moduleName]; + if (module == null) { + throw new Error(`Module ${moduleName} not found in schema`); + } + return module; +} + +describe('Cross-file type imports', () => { + describe('FlowParser', () => { + const parser = new FlowParser(); + + it('should parse a spec with imported enums via parseString with importedTypes', () => { + // First, parse the shared types file to get its TypeDeclarationMap + const sharedAst = parser.getAst(SHARED_ENUM_TYPES_FLOW); + const sharedTypes = parser.getTypes(sharedAst); + + // Verify we got the shared types + expect(sharedTypes.SharedStatusEnum).toBeDefined(); + expect(sharedTypes.SharedNumEnum).toBeDefined(); + expect(sharedTypes.SharedStateType).toBeDefined(); + + // Now parse the spec file, passing the shared types as importedTypes + const importedTypeSourceMap = { + SharedStatusEnum: 'SharedEnumTypes', + SharedNumEnum: 'SharedEnumTypes', + SharedStateType: 'SharedEnumTypes', + }; + const schema = parser.parseString( + SPEC_WITH_IMPORTS_FLOW, + 'NativeImportedEnumTurboModule.js', + sharedTypes, + importedTypeSourceMap, + ); + + // Verify the schema was generated correctly + const module = getNativeModule(schema, 'NativeImportedEnumTurboModule'); + expect(module.type).toBe('NativeModule'); + + // Verify enum map contains the imported enums + expect(module.enumMap).toBeDefined(); + expect(module.enumMap.SharedStatusEnum).toBeDefined(); + expect(module.enumMap.SharedNumEnum).toBeDefined(); + + // Verify enum member types + expect(module.enumMap.SharedStatusEnum.type).toBe( + 'EnumDeclarationWithMembers', + ); + expect(module.enumMap.SharedStatusEnum.memberType).toBe( + 'StringTypeAnnotation', + ); + expect(module.enumMap.SharedNumEnum.memberType).toBe( + 'NumberTypeAnnotation', + ); + + // Verify methods exist + expect(module.spec.methods.length).toBe(3); + + // Verify imported type tracking + expect(module.importedEnumNames).toEqual( + expect.objectContaining({ + SharedNumEnum: 'SharedEnumTypes', + SharedStatusEnum: 'SharedEnumTypes', + }), + ); + }); + + it('should not break when importedTypes is undefined', () => { + // Existing behavior: parse a self-contained spec + const selfContainedSpec = ` +/** + * @flow strict-local + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum LocalEnum { + A = 'a', + B = 'b', +} + +export interface Spec extends TurboModule { + +getEnum: () => LocalEnum; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeSelfContainedModule', +): Spec); +`; + const schema = parser.parseString( + selfContainedSpec, + 'NativeSelfContainedModule.js', + ); + + const module = getNativeModule(schema, 'NativeSelfContainedModule'); + expect(module.enumMap.LocalEnum).toBeDefined(); + }); + + it('should give local types precedence over imported types', () => { + // Define a shared type + const sharedAst = parser.getAst(SHARED_ENUM_TYPES_FLOW); + const sharedTypes = parser.getTypes(sharedAst); + + // Spec that defines a local type with the same name as an imported one + const specWithLocalOverride = ` +/** + * @flow strict-local + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SharedStatusEnum { + LocalA = 'local_a', + LocalB = 'local_b', +} + +export interface Spec extends TurboModule { + +getStatus: () => SharedStatusEnum; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeLocalOverrideModule', +): Spec); +`; + const schema = parser.parseString( + specWithLocalOverride, + 'NativeLocalOverrideModule.js', + sharedTypes, + ); + + const module = getNativeModule(schema, 'NativeLocalOverrideModule'); + expect(module.enumMap.SharedStatusEnum).toBeDefined(); + + // Local definition should win - it has LocalA and LocalB members + const members = module.enumMap.SharedStatusEnum.members; + expect(members).toEqual( + expect.arrayContaining([ + expect.objectContaining({name: 'LocalA'}), + expect.objectContaining({name: 'LocalB'}), + ]), + ); + + // SharedStatusEnum is defined locally, so should NOT be in importedEnumNames + expect(module.importedEnumNames ?? []).not.toContain('SharedStatusEnum'); + }); + }); + + describe('TypeScriptParser', () => { + const parser = new TypeScriptParser(); + + it('should parse a spec with imported enums via parseString with importedTypes', () => { + // First, parse the shared types file to get its TypeDeclarationMap + const sharedAst = parser.getAst(SHARED_ENUM_TYPES_TS); + const sharedTypes = parser.getTypes(sharedAst); + + // Verify we got the shared types + expect(sharedTypes.SharedStatusEnum).toBeDefined(); + expect(sharedTypes.SharedNumEnum).toBeDefined(); + expect(sharedTypes.SharedStateType).toBeDefined(); + + // Now parse the spec file, passing the shared types as importedTypes + const importedTypeSourceMap = { + SharedStatusEnum: 'SharedEnumTypes', + SharedNumEnum: 'SharedEnumTypes', + SharedStateType: 'SharedEnumTypes', + }; + const schema = parser.parseString( + SPEC_WITH_IMPORTS_TS, + 'NativeImportedEnumTurboModule.ts', + sharedTypes, + importedTypeSourceMap, + ); + + // Verify the schema was generated correctly + const module = getNativeModule(schema, 'NativeImportedEnumTurboModule'); + expect(module.type).toBe('NativeModule'); + + // Verify enum map contains the imported enums + expect(module.enumMap).toBeDefined(); + expect(module.enumMap.SharedStatusEnum).toBeDefined(); + expect(module.enumMap.SharedNumEnum).toBeDefined(); + + // Verify enum member types + expect(module.enumMap.SharedStatusEnum.type).toBe( + 'EnumDeclarationWithMembers', + ); + expect(module.enumMap.SharedStatusEnum.memberType).toBe( + 'StringTypeAnnotation', + ); + expect(module.enumMap.SharedNumEnum.memberType).toBe( + 'NumberTypeAnnotation', + ); + + // Verify methods exist + expect(module.spec.methods.length).toBe(3); + + // Verify imported type tracking + expect(module.importedEnumNames).toEqual( + expect.objectContaining({ + SharedNumEnum: 'SharedEnumTypes', + SharedStatusEnum: 'SharedEnumTypes', + }), + ); + }); + + it('should give local types precedence over imported types', () => { + // Define a shared type + const sharedAst = parser.getAst(SHARED_ENUM_TYPES_TS); + const sharedTypes = parser.getTypes(sharedAst); + + // Spec that defines a local type with the same name as an imported one + const specWithLocalOverride = ` +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SharedStatusEnum { + LocalA = 'local_a', + LocalB = 'local_b', +} + +export interface Spec extends TurboModule { + getStatus(): SharedStatusEnum; +} + +export default TurboModuleRegistry.getEnforcing( + 'NativeLocalOverrideModule', +); +`; + const schema = parser.parseString( + specWithLocalOverride, + 'NativeLocalOverrideModule.ts', + sharedTypes, + ); + + const module = getNativeModule(schema, 'NativeLocalOverrideModule'); + expect(module.enumMap.SharedStatusEnum).toBeDefined(); + + // Local definition should win - it has LocalA and LocalB members + const members = module.enumMap.SharedStatusEnum.members; + expect(members).toEqual( + expect.arrayContaining([ + expect.objectContaining({name: 'LocalA'}), + expect.objectContaining({name: 'LocalB'}), + ]), + ); + + // SharedStatusEnum is defined locally, so should NOT be in importedEnumNames + expect(module.importedEnumNames ?? []).not.toContain('SharedStatusEnum'); + }); + }); + + describe('getImportsFromAST', () => { + const {getImportsFromAST} = require('../utils'); + const flowParser = new FlowParser(); + const tsParser = new TypeScriptParser(); + + it('should extract type-only named imports from Flow import declarations', () => { + const ast = flowParser.getAst(` + import type {Foo, Bar} from './MyTypes'; + import type {Baz} from 'some-module'; + `); + + const imports = getImportsFromAST(ast); + expect(imports).toEqual({ + Foo: './MyTypes', + Bar: './MyTypes', + Baz: 'some-module', + }); + }); + + it('should skip value-only imports', () => { + const ast = flowParser.getAst(` + import {someFunction} from './utils'; + import type {Foo} from './MyTypes'; + `); + + const imports = getImportsFromAST(ast); + expect(imports).toEqual({ + Foo: './MyTypes', + }); + }); + + it('should ignore namespace imports', () => { + const ast = flowParser.getAst(` + import * as React from 'react'; + import type {Foo} from './MyTypes'; + `); + + const imports = getImportsFromAST(ast); + expect(imports).toEqual({ + Foo: './MyTypes', + }); + }); + + it('should return empty object for files with no imports', () => { + const ast = flowParser.getAst(` + const x = 1; + `); + + const imports = getImportsFromAST(ast); + expect(imports).toEqual({}); + }); + + it('should work with TypeScript ASTs', () => { + const ast = tsParser.getAst(` + import type {Foo, Bar} from './MyTypes'; + import {someValue} from './values'; + `); + + const imports = getImportsFromAST(ast); + expect(imports).toEqual({ + Foo: './MyTypes', + Bar: './MyTypes', + }); + }); + }); +}); diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js index 013cfce5b9ae..5d28da9e057f 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js @@ -409,7 +409,7 @@ describe('buildSchemaFromConfigType', () => { (_ast, _parser) => componentSchemaMock, ); const buildModuleSchemaMock = jest.fn( - (_0, _1, _2, _3, _4) => moduleSchemaMock, + (_0, _1, _2, _3, _4, _5, _6) => moduleSchemaMock, ); const buildSchemaFromConfigTypeHelper = ( @@ -422,8 +422,8 @@ describe('buildSchemaFromConfigType', () => { astMock, wrapComponentSchemaMock, buildComponentSchemaMock, - /* $FlowFixMe[incompatible-type] Natural Inference rollout. See - * https://fburl.com/workplace/6291gfvu */ + // $FlowFixMe[incompatible-type] + // $FlowFixMe[invalid-tuple-arity] buildModuleSchemaMock, parser, flowTranslateTypeAnnotation, @@ -509,6 +509,8 @@ describe('buildSchemaFromConfigType', () => { expect.any(Function), parser, flowTranslateTypeAnnotation, + undefined, + undefined, ); expect(buildComponentSchemaMock).not.toHaveBeenCalled(); diff --git a/packages/react-native-codegen/src/parsers/flow/parser.js b/packages/react-native-codegen/src/parsers/flow/parser.js index ac2c351c9ad3..91dba932cba2 100644 --- a/packages/react-native-codegen/src/parsers/flow/parser.js +++ b/packages/react-native-codegen/src/parsers/flow/parser.js @@ -48,6 +48,11 @@ const { } = require('../parsers-commons'); const {Visitor} = require('../parsers-primitives'); const {wrapComponentSchema} = require('../schema.js'); +const { + buildImportedTypeSourceMap, + getImportsFromAST, + resolveImportedTypes, +} = require('../utils'); const {buildComponentSchema} = require('./components'); const { flattenProperties, @@ -123,11 +128,33 @@ class FlowParser implements Parser { parseFile(filename: string): SchemaType { const contents = fs.readFileSync(filename, 'utf8'); - - return this.parseString(contents, filename); + const ast = this.getAst(contents, filename); + const imports = getImportsFromAST(ast); + const importedTypes = resolveImportedTypes( + imports, + filename, + ['.js', '.js.flow', '.jsx'], + (c, f) => this.getAst(c, f), + a => this.getTypes(a), + ); + // Build source map: typeName → source haste module name + const importedTypeSourceMap: {[string]: string} | void = importedTypes + ? buildImportedTypeSourceMap(imports) + : undefined; + return this.parseString( + contents, + filename, + importedTypes, + importedTypeSourceMap, + ); } - parseString(contents: string, filename: ?string): SchemaType { + parseString( + contents: string, + filename: ?string, + importedTypes?: TypeDeclarationMap, + importedTypeSourceMap?: {[string]: string}, + ): SchemaType { return buildSchema( contents, filename, @@ -137,6 +164,8 @@ class FlowParser implements Parser { Visitor, this, flowTranslateTypeAnnotation, + importedTypes, + importedTypeSourceMap, ); } diff --git a/packages/react-native-codegen/src/parsers/parser.js b/packages/react-native-codegen/src/parsers/parser.js index a7e10d8ea9b3..5d10e5226f96 100644 --- a/packages/react-native-codegen/src/parsers/parser.js +++ b/packages/react-native-codegen/src/parsers/parser.js @@ -157,9 +157,14 @@ export interface Parser { * Given the content of a file, it returns a Schema. * @parameter contents: the content of the file. * @parameter filename: the name of the file. + * @parameter importedTypes: optional map of type declarations imported from other files. * @returns: the Schema of the file. */ - parseString(contents: string, filename: ?string): SchemaType; + parseString( + contents: string, + filename: ?string, + importedTypes?: TypeDeclarationMap, + ): SchemaType; /** * Given the name of a file, it returns a Schema. * @parameter filename: the name of the file. diff --git a/packages/react-native-codegen/src/parsers/parsers-commons.js b/packages/react-native-codegen/src/parsers/parsers-commons.js index 20f5ec396855..bd6d685ecd96 100644 --- a/packages/react-native-codegen/src/parsers/parsers-commons.js +++ b/packages/react-native-codegen/src/parsers/parsers-commons.js @@ -181,8 +181,14 @@ function getObjectTypeAnnotations( tryParse: ParserErrorCapturer, translateTypeAnnotation: $FlowFixMe, parser: Parser, -): {...NativeModuleAliasMap} { + importedTypeNames?: $FlowFixMe, + importedTypeSourceMap?: {[string]: string}, +): { + aliasMap: {...NativeModuleAliasMap}, + importedAliasNames: {[string]: string}, +} { const aliasMap: {...NativeModuleAliasMap} = {}; + const importedAliasNames: {[string]: string} = {}; Object.entries(types).forEach(([key, value]) => { const isTypeAlias = value.type === 'TypeAlias' || value.type === 'TSTypeAliasDeclaration'; @@ -229,8 +235,15 @@ function getObjectTypeAnnotations( type: 'ObjectTypeAnnotation', properties: typeProperties, }; + if (importedTypeNames != null && importedTypeNames.has(key)) { + const sourceModule = + importedTypeSourceMap != null ? importedTypeSourceMap[key] : undefined; + if (sourceModule != null) { + importedAliasNames[key] = sourceModule; + } + } }); - return aliasMap; + return {aliasMap, importedAliasNames}; } function parseObjectProperty( @@ -575,9 +588,13 @@ function buildSchemaFromConfigType( tryParse: ParserErrorCapturer, parser: Parser, translateTypeAnnotation: $FlowFixMe, + importedTypes?: TypeDeclarationMap, + importedTypeSourceMap?: {[string]: string}, ) => NativeModuleSchema, parser: Parser, translateTypeAnnotation: $FlowFixMe, + importedTypes?: TypeDeclarationMap, + importedTypeSourceMap?: {[string]: string}, ): SchemaType { switch (configType) { case 'component': { @@ -598,6 +615,8 @@ function buildSchemaFromConfigType( tryParse, parser, translateTypeAnnotation, + importedTypes, + importedTypeSourceMap, ), ); @@ -639,12 +658,16 @@ function buildSchema( tryParse: ParserErrorCapturer, parser: Parser, translateTypeAnnotation: $FlowFixMe, + importedTypes?: TypeDeclarationMap, + importedTypeSourceMap?: {[string]: string}, ) => NativeModuleSchema, Visitor: ({isComponent: boolean, isModule: boolean}) => { [type: string]: (node: $FlowFixMe) => void, }, parser: Parser, translateTypeAnnotation: $FlowFixMe, + importedTypes?: TypeDeclarationMap, + importedTypeSourceMap?: {[string]: string}, ): SchemaType { // Early return for non-Spec JavaScript files if ( @@ -666,6 +689,8 @@ function buildSchema( buildModuleSchema, parser, translateTypeAnnotation, + importedTypes, + importedTypeSourceMap, ); } @@ -760,9 +785,24 @@ const buildModuleSchema = ( tryParse: ParserErrorCapturer, parser: Parser, translateTypeAnnotation: $FlowFixMe, + importedTypes?: TypeDeclarationMap, + importedTypeSourceMap?: {[string]: string}, ): NativeModuleSchema => { const language = parser.language(); - const types = parser.getTypes(ast); + const localTypes = parser.getTypes(ast); + const types = importedTypes ? {...importedTypes, ...localTypes} : localTypes; + + // Track which type names are imported (present in importedTypes but not + // overridden by a local definition). Used to populate importedAliasNames + // and importedEnumNames in the schema so generators can skip prefixing. + const importedTypeNames: Set = new Set(); + if (importedTypes) { + for (const name of Object.keys(importedTypes)) { + if (!(name in localTypes)) { + importedTypeNames.add(name); + } + } + } const moduleSpecs = (Object.values(types): ReadonlyArray<$FlowFixMe>).filter( t => parser.isModuleInterface(t), ); @@ -796,15 +836,23 @@ const buildModuleSchema = ( moduleName, ); - const aliasMap: {...NativeModuleAliasMap} = cxxOnly + const { + aliasMap, + importedAliasNames, + }: { + aliasMap: {...NativeModuleAliasMap}, + importedAliasNames: {[string]: string}, + } = cxxOnly ? getObjectTypeAnnotations( hasteModuleName, types, tryParse, translateTypeAnnotation, parser, + importedTypeNames.size > 0 ? importedTypeNames : undefined, + importedTypeSourceMap, ) - : {}; + : {aliasMap: {}, importedAliasNames: {}}; const properties: ReadonlyArray<$FlowFixMe> = language === 'Flow' ? moduleSpec.body.properties : moduleSpec.body.body; @@ -896,6 +944,35 @@ const buildModuleSchema = ( }, ); + // Determine which enum names are imported and their source modules + const importedEnumNames: {[string]: string} = {}; + for (const name of Object.keys(nativeModuleSchema.enumMap)) { + if ( + importedTypeNames.has(name) && + importedTypeSourceMap != null && + importedTypeSourceMap[name] != null + ) { + importedEnumNames[name] = importedTypeSourceMap[name]; + } + } + + // For non-cxxOnly modules, aliasMap is populated via side effects during + // property parsing. Scan it for imported types too. + if (!cxxOnly) { + for (const name of Object.keys(nativeModuleSchema.aliasMap)) { + if ( + importedTypeNames.has(name) && + importedTypeSourceMap != null && + importedTypeSourceMap[name] != null + ) { + importedAliasNames[name] = importedTypeSourceMap[name]; + } + } + } + + const hasImportedAliases = Object.keys(importedAliasNames).length > 0; + const hasImportedEnums = Object.keys(importedEnumNames).length > 0; + return { type: 'NativeModule', aliasMap: getSortedObject(nativeModuleSchema.aliasMap), @@ -906,6 +983,8 @@ const buildModuleSchema = ( }, moduleName, excludedPlatforms: nativeModuleSchema.excludedPlatforms, + importedAliasNames: hasImportedAliases ? importedAliasNames : undefined, + importedEnumNames: hasImportedEnums ? importedEnumNames : undefined, }; }; diff --git a/packages/react-native-codegen/src/parsers/typescript/parser.js b/packages/react-native-codegen/src/parsers/typescript/parser.js index b3f4d603b4d3..1080edc1e4ff 100644 --- a/packages/react-native-codegen/src/parsers/typescript/parser.js +++ b/packages/react-native-codegen/src/parsers/typescript/parser.js @@ -49,6 +49,11 @@ const { } = require('../parsers-commons.js'); const {Visitor} = require('../parsers-primitives'); const {wrapComponentSchema} = require('../schema.js'); +const { + buildImportedTypeSourceMap, + getImportsFromAST, + resolveImportedTypes, +} = require('../utils'); const {buildComponentSchema} = require('./components'); const { flattenProperties, @@ -125,11 +130,32 @@ class TypeScriptParser implements Parser { parseFile(filename: string): SchemaType { const contents = fs.readFileSync(filename, 'utf8'); - - return this.parseString(contents, filename); + const ast = this.getAst(contents, filename); + const imports = getImportsFromAST(ast); + const importedTypes = resolveImportedTypes( + imports, + filename, + ['.ts', '.tsx', '.d.ts'], + (c, f) => this.getAst(c, f), + a => this.getTypes(a), + ); + const importedTypeSourceMap: {[string]: string} | void = importedTypes + ? buildImportedTypeSourceMap(imports) + : undefined; + return this.parseString( + contents, + filename, + importedTypes, + importedTypeSourceMap, + ); } - parseString(contents: string, filename: ?string): SchemaType { + parseString( + contents: string, + filename: ?string, + importedTypes?: TypeDeclarationMap, + importedTypeSourceMap?: {[string]: string}, + ): SchemaType { return buildSchema( contents, filename, @@ -139,6 +165,8 @@ class TypeScriptParser implements Parser { Visitor, this, typeScriptTranslateTypeAnnotation, + importedTypes, + importedTypeSourceMap, ); } diff --git a/packages/react-native-codegen/src/parsers/utils.js b/packages/react-native-codegen/src/parsers/utils.js index 851450b0b181..3743cb1ac1e4 100644 --- a/packages/react-native-codegen/src/parsers/utils.js +++ b/packages/react-native-codegen/src/parsers/utils.js @@ -11,6 +11,7 @@ 'use strict'; const {ParserError} = require('./errors'); +const fs = require('fs'); const path = require('path'); export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe}; @@ -214,6 +215,131 @@ function getSortedObject(unsortedObject: {[key: string]: T}): { }, {}); } +export type ImportMap = {[importedName: string]: string}; + +/** + * Extracts a map of imported type names to their source module names + * from ImportDeclaration nodes in the AST. Only captures type imports + * (import type {...} or inline type specifiers) to avoid unnecessary + * filesystem I/O when resolving imported types. + * + * For example: + * import type {EnumInt2} from 'MyTypes'; + * produces: + * {EnumInt2: 'MyTypes'} + */ +function getImportsFromAST(ast: $FlowFixMe): ImportMap { + const imports: ImportMap = {}; + for (const node of ast.body) { + if (node.type !== 'ImportDeclaration') { + continue; + } + // Skip pure value imports (importKind === 'value' with no type specifiers) + const isTypeImport = + node.importKind === 'type' || node.importKind === 'typeof'; + const source = node.source.value; + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportSpecifier') { + // Include if the whole declaration is a type import, + // or if this specific specifier is a type import (TS inline type imports) + if (isTypeImport || specifier.importKind === 'type') { + const importedName = specifier.imported?.name ?? specifier.local.name; + imports[importedName] = source; + } + } + } + } + return imports; +} + +/** + * Resolves imported types from other files by reading the source files + * and extracting their type declarations. Only resolves relative path imports; + * framework imports (non-relative paths) are skipped. + * + * When a type is found in a source file, all types from that file are included + * to handle transitive type dependencies (e.g., importing ProjectDetails which + * references Package, both defined in the same source file). + * + * Note: This only follows one level of file imports. Transitive file imports + * (A imports from B which imports from C) are not followed. + */ +function resolveImportedTypes( + imports: ImportMap, + currentFilename: string, + extensions: Array, + getAst: (contents: string, filename?: ?string) => $FlowFixMe, + getTypes: (ast: $FlowFixMe) => TypeDeclarationMap, +): TypeDeclarationMap | void { + let importedTypes: TypeDeclarationMap = {}; + const currentDir = path.dirname(currentFilename); + let found = false; + + for (const typeName of Object.keys(imports)) { + const sourceModule = imports[typeName]; + + // Skip framework imports (react-native, react, etc.) + if (!sourceModule.startsWith('.') && !sourceModule.startsWith('/')) { + continue; + } + + // Resolve relative path + let resolvedPath = null; + const basePath = path.resolve(currentDir, sourceModule); + + for (const ext of extensions) { + const candidate = basePath + ext; + if (fs.existsSync(candidate)) { + resolvedPath = candidate; + break; + } + } + + // Try without extension (file may already have it) + if (resolvedPath == null && fs.existsSync(basePath)) { + resolvedPath = basePath; + } + + if (resolvedPath == null) { + continue; + } + + try { + const sourceContents = fs.readFileSync(resolvedPath, 'utf8'); + const sourceAst = getAst(sourceContents, resolvedPath); + const sourceTypes = getTypes(sourceAst); + if (sourceTypes[typeName] != null) { + // Include all types from the source file, not just the directly + // imported one. This handles transitive type dependencies within + // the same file (e.g., importing ProjectDetails which references + // Package, both defined in the same source file). + importedTypes = {...importedTypes, ...sourceTypes}; + found = true; + } + } catch { + // Skip files that fail to parse + continue; + } + } + + return found ? importedTypes : undefined; +} + +/** + * Builds a map from imported type names to their source haste module names. + * Uses extractNativeModuleName on the source path to derive the haste name. + */ +function buildImportedTypeSourceMap(imports: ImportMap): {[string]: string} { + const sourceMap: {[string]: string} = {}; + for (const typeName of Object.keys(imports)) { + const sourcePath = imports[typeName]; + // Extract the haste module name from the source path + const hasteName = extractNativeModuleName(sourcePath); + sourceMap[typeName] = hasteName; + } + return sourceMap; +} + module.exports = { getConfigType, extractNativeModuleName, @@ -222,4 +348,7 @@ module.exports = { visit, isModuleRegistryCall, getSortedObject, + getImportsFromAST, + resolveImportedTypes, + buildImportedTypeSourceMap, }; diff --git a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp index 492c29a33b66..a55a699ac174 100644 --- a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp +++ b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.cpp @@ -95,15 +95,15 @@ GraphNode NativeCxxModuleExample::getGraphNode( return arg; } -NativeCxxModuleExampleEnumInt NativeCxxModuleExample::getNumEnum( +SharedTypeEnumInt NativeCxxModuleExample::getNumEnum( jsi::Runtime& /*rt*/, - NativeCxxModuleExampleEnumInt arg) { + SharedTypeEnumInt arg) { return arg; } NativeCxxModuleExampleEnumStr NativeCxxModuleExample::getStrEnum( jsi::Runtime& /*rt*/, - NativeCxxModuleExampleEnumNone /*arg*/) { + SharedTypeEnumNone /*arg*/) { return NativeCxxModuleExampleEnumStr::SB; } @@ -191,7 +191,7 @@ void NativeCxxModuleExample::voidFunc(jsi::Runtime& /*rt*/) { ObjectStruct{1, "two", std::nullopt}, ObjectStruct{3, "four", std::nullopt}, ObjectStruct{5, "six", std::nullopt}}); - emitOnEvent(NativeCxxModuleExampleEnumNone::NA); + emitOnEvent(SharedTypeEnumNone::NA); } AsyncPromise<> NativeCxxModuleExample::voidPromise(jsi::Runtime& rt) { diff --git a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.h b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.h index 9c2ce01677b1..ce1b73154899 100644 --- a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.h +++ b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.h @@ -23,10 +23,10 @@ namespace facebook::react { #pragma mark - Structs -using ConstantsStruct = NativeCxxModuleExampleConstantsStruct; +using ConstantsStruct = SharedTypeConstantsStruct; template <> -struct Bridging : NativeCxxModuleExampleConstantsStructBridging {}; +struct Bridging : SharedTypeConstantsStructBridging {}; using ObjectStruct = NativeCxxModuleExampleObjectStruct>; @@ -138,9 +138,9 @@ class NativeCxxModuleExample : public NativeCxxModuleExampleCxxSpec> getMap( jsi::Runtime &rt, diff --git a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.js b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.js index ad01c7549045..6bb8c66f4ad5 100644 --- a/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.js +++ b/packages/rn-tester/NativeCxxModuleExample/NativeCxxModuleExample.js @@ -8,20 +8,11 @@ * @format */ +import type {ConstantsStruct, EnumInt, EnumNone} from './SharedType'; import type {CodegenTypes, TurboModule} from 'react-native'; import {TurboModuleRegistry} from 'react-native'; -export enum EnumInt { - IA = 23, - IB = 42, -} - -export enum EnumNone { - NA, - NB, -} - export enum EnumStr { SA = 's---a', SB = 's---b', diff --git a/packages/rn-tester/NativeCxxModuleExample/SharedType.js b/packages/rn-tester/NativeCxxModuleExample/SharedType.js new file mode 100644 index 000000000000..154e6bb56a83 --- /dev/null +++ b/packages/rn-tester/NativeCxxModuleExample/SharedType.js @@ -0,0 +1,25 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +export enum EnumInt { + IA = 23, + IB = 42, +} + +export enum EnumNone { + NA, + NB, +} + +export type ConstantsStruct = { + const1: boolean, + const2: number, + const3: string, +}; diff --git a/packages/rn-tester/NativeCxxModuleExample/tests/NativeCxxModuleExampleTests.cpp b/packages/rn-tester/NativeCxxModuleExample/tests/NativeCxxModuleExampleTests.cpp index 1de45d805f87..bf42ded02324 100644 --- a/packages/rn-tester/NativeCxxModuleExample/tests/NativeCxxModuleExampleTests.cpp +++ b/packages/rn-tester/NativeCxxModuleExample/tests/NativeCxxModuleExampleTests.cpp @@ -86,19 +86,19 @@ TEST_F(NativeCxxModuleExampleTests, GetGraphNodeReturnsCorrectValues) { TEST_F(NativeCxxModuleExampleTests, GetNumEnumReturnsCorrectValues) { EXPECT_EQ( - module_->getNumEnum(*runtime_, NativeCxxModuleExampleEnumInt::IA), - NativeCxxModuleExampleEnumInt::IA); + module_->getNumEnum(*runtime_, SharedTypeEnumInt::IA), + SharedTypeEnumInt::IA); EXPECT_EQ( - module_->getNumEnum(*runtime_, NativeCxxModuleExampleEnumInt::IB), - NativeCxxModuleExampleEnumInt::IB); + module_->getNumEnum(*runtime_, SharedTypeEnumInt::IB), + SharedTypeEnumInt::IB); } TEST_F(NativeCxxModuleExampleTests, GetStrEnumReturnsCorrectValues) { EXPECT_EQ( - module_->getStrEnum(*runtime_, NativeCxxModuleExampleEnumNone::NA), + module_->getStrEnum(*runtime_, SharedTypeEnumNone::NA), NativeCxxModuleExampleEnumStr::SB); EXPECT_EQ( - module_->getStrEnum(*runtime_, NativeCxxModuleExampleEnumNone::NB), + module_->getStrEnum(*runtime_, SharedTypeEnumNone::NB), NativeCxxModuleExampleEnumStr::SB); } diff --git a/packages/rn-tester/js/examples/TurboModule/NativeCxxModuleExampleExample.js b/packages/rn-tester/js/examples/TurboModule/NativeCxxModuleExampleExample.js index dcee38ad6560..64a2053e8b7a 100644 --- a/packages/rn-tester/js/examples/TurboModule/NativeCxxModuleExampleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/NativeCxxModuleExampleExample.js @@ -10,10 +10,8 @@ import type {EventSubscription, RootTag} from 'react-native'; -import NativeCxxModuleExample, { - EnumInt, - EnumNone, -} from '../../../NativeCxxModuleExample/NativeCxxModuleExample'; +import NativeCxxModuleExample from '../../../NativeCxxModuleExample/NativeCxxModuleExample'; +import {EnumInt, EnumNone} from '../../../NativeCxxModuleExample/SharedType'; import RNTesterText from '../../components/RNTesterText'; import styles from './TurboModuleExampleCommon'; import * as React from 'react'; diff --git a/private/react-native-codegen-typescript-test/src/__tests__/simple-scenario-frontend-test.ts b/private/react-native-codegen-typescript-test/src/__tests__/simple-scenario-frontend-test.ts index ebfcbb62b5f1..d4218f672f9c 100644 --- a/private/react-native-codegen-typescript-test/src/__tests__/simple-scenario-frontend-test.ts +++ b/private/react-native-codegen-typescript-test/src/__tests__/simple-scenario-frontend-test.ts @@ -27,6 +27,8 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); aliasMap: {}, enumMap: {}, excludedPlatforms: undefined, + importedAliasNames: undefined, + importedEnumNames: undefined, moduleName: 'SampleTurboModule', spec: { eventEmitters: [],