From e57b560d54ab06c45e1c87ad7a5c4ac570c002a7 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Fri, 21 Aug 2026 09:28:25 -0700 Subject: [PATCH] Align TurboModule EventEmitter payload types across platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Codegen's `EventEmitter` support for TurboModules diverged between platforms in two ways: - The explicit number types `Double`, `Float` and `Int32` worked as emitter payloads on Android and C++, but iOS rejected them at codegen time (plain `number` worked). iOS now maps all of them to `NSNumber *_Nonnull`, matching how plain `number` is already emitted. - `ArrayBuffer` was rejected on Android and iOS, but the C++ generator silently accepted it and produced a `jsi::ArrayBuffer` emitter. `ArrayBuffer` is not emittable on any platform — Android emitters always carry a `folly::dynamic` payload, which cannot hold raw bytes — and the schema type `NativeModuleEventEmitterBaseTypeAnnotation` already excluded it. It is now rejected everywhere. `ArrayBuffer` payloads are rejected in the shared parser, so Flow and TypeScript specs produce the same error, and the three generators keep an equivalent guard so schemas that are constructed without going through the parser fail the same way. `ArrayBuffer` remains supported as a method argument and as a synchronous return value. Changelog: [iOS][Added] - Support `Double`, `Float` and `Int32` payloads for TurboModule `EventEmitter`s [General][Breaking] - Reject `ArrayBuffer` as a TurboModule `EventEmitter` payload on all platforms Differential Revision: D116952150 --- .../src/generators/modules/GenerateModuleH.js | 6 +++ .../modules/GenerateModuleJavaSpec.js | 10 ++--- .../serializeEventEmitter.js | 11 ++++-- .../src/generators/modules/Utils.js | 19 ++++++++++ .../modules/__test_fixtures__/fixtures.js | 30 +++++++++++++++ .../modules/__tests__/GenerateModuleH-test.js | 37 +++++++++++++++++++ .../__tests__/GenerateModuleHObjCpp-test.js | 36 ++++++++++++++++++ .../__tests__/GenerateModuleJavaSpec-test.js | 31 ++++++++++++++++ .../GenerateModuleH-test.js.snap | 24 ++++++++++++ .../GenerateModuleHObjCpp-test.js.snap | 3 ++ .../GenerateModuleJavaSpec-test.js.snap | 21 +++++++++++ .../GenerateModuleJniCpp-test.js.snap | 3 ++ .../GenerateModuleMm-test.js.snap | 24 ++++++++++++ .../src/parsers/__tests__/error-utils-test.js | 30 +++++++++++++++ .../src/parsers/error-utils.js | 21 +++++++++++ .../src/parsers/errors.js | 16 ++++++++ .../modules/__test_fixtures__/failures.js | 26 +++++++++++++ .../modules/__test_fixtures__/fixtures.js | 5 ++- .../module-parser-snapshot-test.js.snap | 32 ++++++++++++++++ .../src/parsers/parsers-commons.js | 8 ++++ .../modules/__test_fixtures__/failures.js | 22 +++++++++++ .../modules/__test_fixtures__/fixtures.js | 4 ++ ...script-module-parser-snapshot-test.js.snap | 32 ++++++++++++++++ 23 files changed, 441 insertions(+), 10 deletions(-) diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js index af7f00aa3008..d2395dd297cf 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js @@ -40,6 +40,7 @@ const { getModules, isArrayRecursiveMember, isDirectRecursiveMember, + throwIfUnsupportedEventEmitterPayload, } = require('./Utils'); type FilesOutput = Map; @@ -638,6 +639,11 @@ function translateEventEmitterToCpp( resolveAlias: AliasResolver, enumMap: NativeModuleEnumMap, ): EventEmitterCpp { + throwIfUnsupportedEventEmitterPayload( + eventEmitter.name, + eventEmitter.typeAnnotation.typeAnnotation, + ); + const isVoidTypeAnnotation = eventEmitter.typeAnnotation.typeAnnotation.type === 'VoidTypeAnnotation'; const templateName = `${toPascalCase(eventEmitter.name)}Type`; diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index 9dc0b42ce8f6..52e8aa994d39 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -28,6 +28,7 @@ const {parseValidUnionType, toPascalCase} = require('../Utils'); const { createAliasResolver, getModules, + throwIfUnsupportedEventEmitterPayload, throwIfUnsupportedPromiseArrayBuffer, } = require('./Utils'); @@ -141,6 +142,9 @@ function translateEventEmitterTypeToJavaType( imports: Set, ): string { const typeAnnotation = eventEmitter.typeAnnotation.typeAnnotation; + + throwIfUnsupportedEventEmitterPayload(eventEmitter.name, typeAnnotation); + switch (typeAnnotation.type) { case 'StringTypeAnnotation': return 'String'; @@ -179,12 +183,8 @@ function translateEventEmitterTypeToJavaType( case 'ArrayTypeAnnotation': imports.add('com.facebook.react.bridge.ReadableArray'); return 'ReadableArray'; - case 'DoubleTypeAnnotation': - case 'FloatTypeAnnotation': - case 'Int32TypeAnnotation': case 'VoidTypeAnnotation': - case 'ArrayBufferTypeAnnotation': - // TODO: Add support for these types + // Void emitters take no argument, so the caller never asks for a type. throw new Error( `Unsupported eventType for ${eventEmitter.name}. Found: ${eventEmitter.typeAnnotation.typeAnnotation.type}`, ); diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeEventEmitter.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeEventEmitter.js index b5ea25048fef..774c4afb4903 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeEventEmitter.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeEventEmitter.js @@ -11,12 +11,15 @@ import type {NativeModuleEventEmitterShape} from '../../../CodegenSchema'; const {parseValidUnionType, toPascalCase} = require('../../Utils'); +const {throwIfUnsupportedEventEmitterPayload} = require('../Utils'); function getEventEmitterTypeObjCType( eventEmitter: NativeModuleEventEmitterShape, ): string { const typeAnnotation = eventEmitter.typeAnnotation.typeAnnotation; + throwIfUnsupportedEventEmitterPayload(eventEmitter.name, typeAnnotation); + switch (typeAnnotation.type) { case 'StringTypeAnnotation': return 'NSString *_Nonnull'; @@ -39,6 +42,9 @@ function getEventEmitterTypeObjCType( } case 'NumberTypeAnnotation': case 'NumberLiteralTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'Int32TypeAnnotation': return 'NSNumber *_Nonnull'; case 'BooleanTypeAnnotation': case 'BooleanLiteralTypeAnnotation': @@ -49,11 +55,8 @@ function getEventEmitterTypeObjCType( return 'NSDictionary *'; case 'ArrayTypeAnnotation': return 'NSArray> *'; - case 'DoubleTypeAnnotation': - case 'FloatTypeAnnotation': - case 'Int32TypeAnnotation': case 'VoidTypeAnnotation': - // TODO: Add support for these types + // Void emitters take no argument, so both callers skip this function. throw new Error( `Unsupported eventType for ${eventEmitter.name}. Found: ${eventEmitter.typeAnnotation.typeAnnotation.type}`, ); diff --git a/packages/react-native-codegen/src/generators/modules/Utils.js b/packages/react-native-codegen/src/generators/modules/Utils.js index 8cd8d37ff096..9c35c3e5bea0 100644 --- a/packages/react-native-codegen/src/generators/modules/Utils.js +++ b/packages/react-native-codegen/src/generators/modules/Utils.js @@ -118,10 +118,29 @@ function throwIfUnsupportedPromiseArrayBuffer( } } +// ArrayBuffer is not emittable on any platform: Android emitters always carry a +// folly::dynamic payload, which cannot hold raw bytes, and neither the ObjC nor +// the C++ emitter contract can hand out a buffer that outlives the emit call. +// The parser rejects this too; the guard here also covers schemas built without +// going through the parser. +function throwIfUnsupportedEventEmitterPayload( + eventEmitterName: string, + typeAnnotation: NativeModuleTypeAnnotation, +): void { + if (typeAnnotation.type === 'ArrayBufferTypeAnnotation') { + throw new Error( + `Unsupported eventType for ${eventEmitterName}. Found: ${typeAnnotation.type}. ` + + 'ArrayBuffer is not supported as an EventEmitter payload on any platform. ' + + 'Pass the ArrayBuffer through a method instead.', + ); + } +} + module.exports = { createAliasResolver, getModules, isDirectRecursiveMember, isArrayRecursiveMember, + throwIfUnsupportedEventEmitterPayload, throwIfUnsupportedPromiseArrayBuffer, }; diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index 83cc98bef054..fca175285f76 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -130,6 +130,36 @@ const EVENT_EMITTER_MODULES: SchemaType = { }, }, }, + { + name: 'onEvent7', + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }, + }, + { + name: 'onEvent8', + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }, + }, + { + name: 'onEvent9', + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + }, ], methods: [ { diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleH-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleH-test.js index 3bc24589a19c..48e619215f0d 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleH-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleH-test.js @@ -10,9 +10,37 @@ 'use strict'; +import type {SchemaType} from '../../../CodegenSchema'; + const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleH.js'); +const ARRAY_BUFFER_EVENT_EMITTER_SCHEMA: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [ + { + name: 'onBuffer', + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: { + type: 'ArrayBufferTypeAnnotation', + }, + }, + }, + ], + methods: [], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; + describe('GenerateModuleH', () => { Object.keys(fixtures) .sort() @@ -29,4 +57,13 @@ describe('GenerateModuleH', () => { ).toMatchSnapshot(); }); }); + + it('throws for an EventEmitter with an ArrayBuffer payload', () => { + expect(() => + generator.generate( + 'array_buffer_event_emitter_throws', + ARRAY_BUFFER_EVENT_EMITTER_SCHEMA, + ), + ).toThrow(/ArrayBuffer is not supported as an EventEmitter payload/); + }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js index c151a4aa1cc1..5dd67931bc09 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js @@ -71,4 +71,40 @@ describe('GenerateModuleHObjCpp', () => { ), ).toThrow(/Promise is not supported/); }); + + it('throws for an EventEmitter with an ArrayBuffer payload', () => { + const schema: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [ + { + name: 'onBuffer', + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: { + type: 'ArrayBufferTypeAnnotation', + }, + }, + }, + ], + methods: [], + }, + moduleName: 'SampleTurboModule', + }, + }, + }; + expect(() => + generator.generate( + 'array_buffer_event_emitter_throws', + schema, + 'com.facebook.fbreact.specs', + false, + ), + ).toThrow(/ArrayBuffer is not supported as an EventEmitter payload/); + }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js index 3cbcf9747179..a413ae0c27a0 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js @@ -64,4 +64,35 @@ describe('GenerateModuleJavaSpec', () => { generator.generate('array_buffer_promise_throws', schema), ).toThrow(/Promise is not supported/); }); + + it('throws for an EventEmitter with an ArrayBuffer payload', () => { + const schema: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [ + { + name: 'onBuffer', + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: { + type: 'ArrayBufferTypeAnnotation', + }, + }, + }, + ], + methods: [], + }, + moduleName: 'SampleTurboModule', + }, + }, + }; + expect(() => + generator.generate('array_buffer_event_emitter_throws', schema), + ).toThrow(/ArrayBuffer is not supported as an EventEmitter payload/); + }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap index f2b1d6ad49ce..466e7e67626e 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap @@ -1083,6 +1083,9 @@ protected: eventEmitterMap_[\\"onEvent4\\"] = std::make_shared>(); eventEmitterMap_[\\"onEvent5\\"] = std::make_shared>(); eventEmitterMap_[\\"onEvent6\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent7\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent8\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent9\\"] = std::make_shared>(); } void emitOnEvent1() { @@ -1123,6 +1126,27 @@ protected: return bridging::toJs(rt, eventValue, jsInvoker); }); } + + template void emitOnEvent7(OnEvent7Type value) { + static_assert(bridging::supportsFromJs, \\"value cannnot be converted to double\\"); + static_cast&>(*eventEmitterMap_[\\"onEvent7\\"]).emit([jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value { + return bridging::toJs(rt, eventValue, jsInvoker); + }); + } + + template void emitOnEvent8(OnEvent8Type value) { + static_assert(bridging::supportsFromJs, \\"value cannnot be converted to double\\"); + static_cast&>(*eventEmitterMap_[\\"onEvent8\\"]).emit([jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value { + return bridging::toJs(rt, eventValue, jsInvoker); + }); + } + + template void emitOnEvent9(OnEvent9Type value) { + static_assert(bridging::supportsFromJs, \\"value cannnot be converted to int\\"); + static_cast&>(*eventEmitterMap_[\\"onEvent9\\"]).emit([jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value { + return bridging::toJs(rt, eventValue, jsInvoker); + }); + } private: static jsi::Value __voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) { static_assert( diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap index 4726cdcd0d96..b1ce7953d5f3 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap @@ -614,6 +614,9 @@ facebook::react::EventEmitterCallback _eventEmitterCallback; - (void)emitOnEvent4:(BOOL)value; - (void)emitOnEvent5:(NSDictionary *)value; - (void)emitOnEvent6:(NSArray> *)value; +- (void)emitOnEvent7:(NSNumber *_Nonnull)value; +- (void)emitOnEvent8:(NSNumber *_Nonnull)value; +- (void)emitOnEvent9:(NSNumber *_Nonnull)value; @end namespace facebook::react { diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap index 14722e2179f5..c9d66a175adf 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap @@ -292,6 +292,27 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo } } + protected final void emitOnEvent7(double value) { + CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback; + if (eventEmitterCallback != null) { + eventEmitterCallback.invoke(\\"onEvent7\\", value); + } + } + + protected final void emitOnEvent8(double value) { + CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback; + if (eventEmitterCallback != null) { + eventEmitterCallback.invoke(\\"onEvent8\\", value); + } + } + + protected final void emitOnEvent9(double value) { + CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback; + if (eventEmitterCallback != null) { + eventEmitterCallback.invoke(\\"onEvent9\\", value); + } + } + @ReactMethod @DoNotStrip public abstract void voidFunc(); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap index 547344fda403..9207d5d26a5d 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap @@ -280,6 +280,9 @@ NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboMo eventEmitterMap_[\\"onEvent4\\"] = std::make_shared>(); eventEmitterMap_[\\"onEvent5\\"] = std::make_shared>(); eventEmitterMap_[\\"onEvent6\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent7\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent8\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent9\\"] = std::make_shared>(); configureEventEmitterCallback(); } diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap index f70403ac89b6..0395bb0b6548 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap @@ -366,6 +366,27 @@ Map { eventEmitterCallback(\\"onEvent6\\", value); } } +- (void)emitOnEvent7:(NSNumber *_Nonnull)value +{ + auto eventEmitterCallback = _eventEmitterCallback; + if (eventEmitterCallback) { + eventEmitterCallback(\\"onEvent7\\", value); + } +} +- (void)emitOnEvent8:(NSNumber *_Nonnull)value +{ + auto eventEmitterCallback = _eventEmitterCallback; + if (eventEmitterCallback) { + eventEmitterCallback(\\"onEvent8\\", value); + } +} +- (void)emitOnEvent9:(NSNumber *_Nonnull)value +{ + auto eventEmitterCallback = _eventEmitterCallback; + if (eventEmitterCallback) { + eventEmitterCallback(\\"onEvent9\\", value); + } +} - (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper { @@ -391,6 +412,9 @@ namespace facebook::react { eventEmitterMap_[\\"onEvent4\\"] = std::make_shared>(); eventEmitterMap_[\\"onEvent5\\"] = std::make_shared>(); eventEmitterMap_[\\"onEvent6\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent7\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent8\\"] = std::make_shared>(); + eventEmitterMap_[\\"onEvent9\\"] = std::make_shared>(); setEventEmitterCallback([eventEmitterMap = eventEmitterMap_](const std::string &name, id value) { auto it = eventEmitterMap.find(name); if (it != eventEmitterMap.end() && it->second) { diff --git a/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js b/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js index 6e893f6e4197..ec101f4a0bd0 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js @@ -18,6 +18,7 @@ const { throwIfArgumentPropsAreNull, throwIfArrayElementTypeAnnotationIsUnsupported, throwIfBubblingTypeIsNull, + throwIfEventEmitterPayloadTypeIsUnsupported, throwIfEventHasNoName, throwIfIncorrectModuleRegistryCallArgument, throwIfIncorrectModuleRegistryCallTypeParameterParserError, @@ -825,6 +826,35 @@ describe('throwIfArrayElementTypeAnnotationIsUnsupported', () => { }); }); +describe('throwIfEventEmitterPayloadTypeIsUnsupported', () => { + const { + UnsupportedModuleEventEmitterPayloadTypeParserError, + } = require('../errors.js'); + const moduleName = 'moduleName'; + + it('throws the error if the payload is an ArrayBuffer', () => { + expect(() => { + throwIfEventEmitterPayloadTypeIsUnsupported( + moduleName, + undefined, + 'onBuffer', + {type: 'ArrayBufferTypeAnnotation'}, + ); + }).toThrow(UnsupportedModuleEventEmitterPayloadTypeParserError); + }); + + it('does not throw the error if the payload is a supported type', () => { + expect(() => { + throwIfEventEmitterPayloadTypeIsUnsupported( + moduleName, + undefined, + 'onCount', + {type: 'DoubleTypeAnnotation'}, + ); + }).not.toThrow(); + }); +}); + describe('throwIfPartialNotAnnotatingTypeParameter', () => { const flowParser = new FlowParser(); const typescriptParser = new TypeScriptParser(); diff --git a/packages/react-native-codegen/src/parsers/error-utils.js b/packages/react-native-codegen/src/parsers/error-utils.js index 348f688f9b7a..3eb3d5eef65d 100644 --- a/packages/react-native-codegen/src/parsers/error-utils.js +++ b/packages/react-native-codegen/src/parsers/error-utils.js @@ -27,6 +27,7 @@ const { UnsupportedArrayElementTypeAnnotationParserError, UnsupportedFunctionParamTypeAnnotationParserError, UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedModuleEventEmitterPayloadTypeParserError, UnsupportedModuleEventEmitterPropertyParserError, UnsupportedModuleEventEmitterTypePropertyParserError, UnsupportedModulePropertyParserError, @@ -196,6 +197,25 @@ function throwIfEventEmitterEventTypeIsUnsupported( } } +// ArrayBuffer is not emittable on any platform: Android emitters always carry a +// folly::dynamic payload, which cannot hold raw bytes, and neither the ObjC nor +// the C++ emitter contract can hand out a buffer that outlives the emit call. +function throwIfEventEmitterPayloadTypeIsUnsupported( + nativeModuleName: string, + propertyValue: $FlowFixMe, + propertyName: string, + payloadTypeAnnotation: NativeModuleTypeAnnotation, +) { + if (payloadTypeAnnotation.type === 'ArrayBufferTypeAnnotation') { + throw new UnsupportedModuleEventEmitterPayloadTypeParserError( + nativeModuleName, + propertyValue, + propertyName, + 'ArrayBuffer', + ); + } +} + function throwIfModuleTypeIsUnsupported( nativeModuleName: string, propertyValue: $FlowFixMe, @@ -418,6 +438,7 @@ module.exports = { throwIfUntypedModule, throwIfEventEmitterTypeIsUnsupported, throwIfEventEmitterEventTypeIsUnsupported, + throwIfEventEmitterPayloadTypeIsUnsupported, throwIfModuleTypeIsUnsupported, throwIfMoreThanOneModuleInterfaceParserError, throwIfUnsupportedFunctionParamTypeAnnotationParserError, diff --git a/packages/react-native-codegen/src/parsers/errors.js b/packages/react-native-codegen/src/parsers/errors.js index 7e0b2277930f..8cd8706eac56 100644 --- a/packages/react-native-codegen/src/parsers/errors.js +++ b/packages/react-native-codegen/src/parsers/errors.js @@ -111,6 +111,21 @@ class UnsupportedModuleEventEmitterPropertyParserError extends ParserError { } } +class UnsupportedModuleEventEmitterPayloadTypeParserError extends ParserError { + constructor( + nativeModuleName: string, + propertyValue: $FlowFixMe, + propertyName: string, + payloadType: string, + ) { + super( + nativeModuleName, + propertyValue, + `EventEmitter '${propertyName}' cannot have payload type '${payloadType}'.`, + ); + } +} + class UnsupportedModulePropertyParserError extends ParserError { constructor( nativeModuleName: string, @@ -456,6 +471,7 @@ module.exports = { UnsupportedEnumDeclarationParserError, UnsupportedModuleEventEmitterTypePropertyParserError, UnsupportedModuleEventEmitterPropertyParserError, + UnsupportedModuleEventEmitterPayloadTypeParserError, UnsupportedModulePropertyParserError, UnsupportedObjectPropertyTypeAnnotationParserError, UnsupportedObjectPropertyWithIndexerTypeAnnotationParserError, diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js index 515098be5462..9c55884ac0a7 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js @@ -350,6 +350,31 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_EVENT_EMITTER = ` +/** + * 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'; + +import type {TurboModule} from '../RCTExport'; +import type {EventEmitter} from '../CodegenTypes'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +onBuffer: EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + module.exports = { NATIVE_MODULES_WITH_READ_ONLY_OBJECT_NO_TYPE_FOR_CONTENT, NATIVE_MODULES_WITH_UNNAMED_PARAMS, @@ -364,4 +389,5 @@ module.exports = { NUMERIC_VALUES_ENUM_NATIVE_MODULE, MAP_WITH_EXTRA_KEYS_NATIVE_MODULE, NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY, + NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_EVENT_EMITTER, }; diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js index 53605cbe10ea..9cc97dbef580 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js @@ -667,7 +667,7 @@ const NATIVE_MODULE_WITH_EVENT_EMITTERS = ` 'use strict'; import type {TurboModule} from '../RCTExport'; -import type {EventEmitter} from '../CodegenTypes'; +import type {Double, EventEmitter, Float, Int32} from '../CodegenTypes'; import * as TurboModuleRegistry from '../TurboModuleRegistry'; export type ObjectStruct = { @@ -686,6 +686,9 @@ export interface Spec extends TurboModule { +onEvent5: EventEmitter; +onEvent6: EventEmitter; +onEvent7: EventEmitter; + +onEvent8: EventEmitter; + +onEvent9: EventEmitter; + +onEvent10: EventEmitter; } export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap index 5309c7d0c616..64d69f752c6f 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap @@ -6,6 +6,8 @@ exports[`RN Codegen Flow Parser Fails with error message MAP_WITH_EXTRA_KEYS_NAT exports[`RN Codegen Flow Parser Fails with error message MIXED_VALUES_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enums can not be mixed- they all must be either blank, number, or string values."`; +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_EVENT_EMITTER 1`] = `"Module NativeSampleTurboModule: EventEmitter 'onBuffer' cannot have payload type 'ArrayBuffer'."`; + exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY 1`] = `"Module NativeSampleTurboModule: Object property '[object Object]' cannot have type 'ArrayBuffer'."`; exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; @@ -2119,6 +2121,36 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_EVENT_EM } } } + }, + { + 'name': 'onEvent8', + 'optional': false, + 'typeAnnotation': { + 'type': 'EventEmitterTypeAnnotation', + 'typeAnnotation': { + 'type': 'DoubleTypeAnnotation' + } + } + }, + { + 'name': 'onEvent9', + 'optional': false, + 'typeAnnotation': { + 'type': 'EventEmitterTypeAnnotation', + 'typeAnnotation': { + 'type': 'FloatTypeAnnotation' + } + } + }, + { + 'name': 'onEvent10', + 'optional': false, + 'typeAnnotation': { + 'type': 'EventEmitterTypeAnnotation', + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation' + } + } } ], 'methods': [] diff --git a/packages/react-native-codegen/src/parsers/parsers-commons.js b/packages/react-native-codegen/src/parsers/parsers-commons.js index 330e52fe8c76..a8b64de42408 100644 --- a/packages/react-native-codegen/src/parsers/parsers-commons.js +++ b/packages/react-native-codegen/src/parsers/parsers-commons.js @@ -42,6 +42,7 @@ import type { const { throwIfConfigNotfound, throwIfEventEmitterEventTypeIsUnsupported, + throwIfEventEmitterPayloadTypeIsUnsupported, throwIfEventEmitterTypeIsUnsupported, throwIfIncorrectModuleRegistryCallArgument, throwIfIncorrectModuleRegistryCallTypeParameterParserError, @@ -551,6 +552,13 @@ function buildEventEmitterSchema( parser, ); + throwIfEventEmitterPayloadTypeIsUnsupported( + hasteModuleName, + typeAnnotation.typeParameters.params[0], + key.name, + eventTypeAnnotation, + ); + return { name: eventemitterName, optional: Boolean(property.optional), diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js index a59c138df1d8..11b7f2b08519 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js @@ -286,6 +286,27 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_EVENT_EMITTER = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly onBuffer: EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + module.exports = { NATIVE_MODULES_WITH_UNNAMED_PARAMS, NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE, @@ -299,4 +320,5 @@ module.exports = { NUMERIC_VALUES_ENUM_NATIVE_MODULE, MAP_WITH_EXTRA_KEYS_NATIVE_MODULE, NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY, + NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_EVENT_EMITTER, }; diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js index c405627b93e4..ddd35c961d9c 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js @@ -766,6 +766,7 @@ const NATIVE_MODULE_WITH_EVENT_EMITTERS = ` */ import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {Double, Float, Int32} from 'react-native/Libraries/Types/CodegenTypes'; import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; export type ObjectStruct = { @@ -784,6 +785,9 @@ export interface Spec extends TurboModule { readonly onEvent5: EventEmitter; readonly onEvent6: EventEmitter; readonly onEvent7: EventEmitter; + readonly onEvent8: EventEmitter; + readonly onEvent9: EventEmitter; + readonly onEvent10: EventEmitter; } export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap index 4b098b27af55..27c8ba733690 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap @@ -6,6 +6,8 @@ exports[`RN Codegen TypeScript Parser Fails with error message MAP_WITH_EXTRA_KE exports[`RN Codegen TypeScript Parser Fails with error message MIXED_VALUES_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enum values can not be mixed. They all must be either blank, number, or string values."`; +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_EVENT_EMITTER 1`] = `"Module NativeSampleTurboModule: EventEmitter 'onBuffer' cannot have payload type 'ArrayBuffer'."`; + exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY 1`] = `"Module NativeSampleTurboModule: Object property '[object Object]' cannot have type 'ArrayBuffer'."`; exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; @@ -2343,6 +2345,36 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_EV } } } + }, + { + 'name': 'onEvent8', + 'optional': false, + 'typeAnnotation': { + 'type': 'EventEmitterTypeAnnotation', + 'typeAnnotation': { + 'type': 'DoubleTypeAnnotation' + } + } + }, + { + 'name': 'onEvent9', + 'optional': false, + 'typeAnnotation': { + 'type': 'EventEmitterTypeAnnotation', + 'typeAnnotation': { + 'type': 'FloatTypeAnnotation' + } + } + }, + { + 'name': 'onEvent10', + 'optional': false, + 'typeAnnotation': { + 'type': 'EventEmitterTypeAnnotation', + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation' + } + } } ], 'methods': []