From f0c4c291e12a8e76f91d3841d65291f0f1f16714 Mon Sep 17 00:00:00 2001 From: "Zihan Chen (MSFT)" <53799235+ZihanChen-MSFT@users.noreply.github.com> Date: Wed, 13 Jul 2022 15:46:44 -0700 Subject: [PATCH] Support TypeScript array types for turbo module (module only) (#34183) Summary: Turbo module codegen supports arrays in both Flow and TypeScript, but it only recognize `Array` and `ReadonlyArray` in TypeScript. In this change, `T[]` and `readonly T[]` are made recognizable in codegen. ## Changelog [General] [Added] - Support TypeScript array types for turbo module (module only) Pull Request resolved: https://github.com/facebook/react-native/pull/34183 Test Plan: `yarn jest` passed in `packages/react-native-codegen` Reviewed By: lunaleaps Differential Revision: D37812638 Pulled By: cipolleschi fbshipit-source-id: d63b0585497a43c274d50e1877baab5d1cc3f8fa --- .../modules/__test_fixtures__/fixtures.js | 91 ++++++++ ...script-module-parser-snapshot-test.js.snap | 209 ++++++++++++++++++ .../src/parsers/typescript/modules/index.js | 182 +++++++++------ 3 files changed, 417 insertions(+), 65 deletions(-) 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 2d466d293d78b1..4530bfe08969a9 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 @@ -322,6 +322,27 @@ export interface Spec extends TurboModule { export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_BASIC_ARRAY2 = ` +/** + * 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 getArray: (arg: string[]) => string[]; + readonly getArray: (arg: readonly string[]) => readonly string[]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + const NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY = ` /** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -377,6 +398,28 @@ export interface Spec extends TurboModule { export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE = ` +/** + * 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 getArray: ( + arg: [string, string][], + ) => (string | number | boolean)[]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + const NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS = ` /** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -399,6 +442,28 @@ export interface Spec extends TurboModule { export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS = ` +/** + * 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 type SomeString = string; + +export interface Spec extends TurboModule { + readonly getArray: (arg: SomeString[]) => string[]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + const NATIVE_MODULE_WITH_COMPLEX_ARRAY = ` /** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -421,6 +486,28 @@ export interface Spec extends TurboModule { export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_COMPLEX_ARRAY2 = ` +/** + * 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 getArray: ( + arg: string[][][][][], + ) => string[][][]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + const NATIVE_MODULE_WITH_PROMISE = ` /** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -534,6 +621,7 @@ export default TurboModuleRegistry.getEnforcing( module.exports = { NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY, NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE, + NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE, NATIVE_MODULE_WITH_FLOAT_AND_INT32, NATIVE_MODULE_WITH_ALIASES, NATIVE_MODULE_WITH_NESTED_ALIASES, @@ -545,8 +633,11 @@ module.exports = { NATIVE_MODULE_WITH_ROOT_TAG, NATIVE_MODULE_WITH_NULLABLE_PARAM, NATIVE_MODULE_WITH_BASIC_ARRAY, + NATIVE_MODULE_WITH_BASIC_ARRAY2, NATIVE_MODULE_WITH_COMPLEX_ARRAY, + NATIVE_MODULE_WITH_COMPLEX_ARRAY2, NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS, + NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS, NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, NATIVE_MODULE_WITH_CALLBACK, EMPTY_NATIVE_MODULE, 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 8a49cb12a3d0e2..20420a83361108 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 @@ -369,6 +369,49 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR }" `; +exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [ + { + 'name': 'getArray', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + }, + 'params': [ + { + 'name': 'arg', + 'optional': false, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + } + } + ] + } + } + ] + }, + 'moduleNames': [ + 'SampleTurboModule' + ] + } + } +}" +`; + exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE 1`] = ` "{ 'modules': { @@ -406,6 +449,43 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR }" `; +exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [ + { + 'name': 'getArray', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'ArrayTypeAnnotation' + }, + 'params': [ + { + 'name': 'arg', + 'optional': false, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation' + } + } + ] + } + } + ] + }, + 'moduleNames': [ + 'SampleTurboModule' + ] + } + } +}" +`; + exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BASIC_ARRAY 1`] = ` "{ 'modules': { @@ -474,6 +554,74 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BA }" `; +exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BASIC_ARRAY2 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [ + { + 'name': 'getArray', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + }, + 'params': [ + { + 'name': 'arg', + 'optional': false, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + } + } + ] + } + }, + { + 'name': 'getArray', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + }, + 'params': [ + { + 'name': 'arg', + 'optional': false, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + } + } + ] + } + } + ] + }, + 'moduleNames': [ + 'SampleTurboModule' + ] + } + } +}" +`; + exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BASIC_PARAM_TYPES 1`] = ` "{ 'modules': { @@ -691,6 +839,67 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO }" `; +exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_ARRAY2 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [ + { + 'name': 'getArray', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + } + } + }, + 'params': [ + { + 'name': 'arg', + 'optional': false, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'StringTypeAnnotation' + } + } + } + } + } + } + } + ] + } + } + ] + }, + 'moduleNames': [ + 'SampleTurboModule' + ] + } + } +}" +`; + exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_OBJECTS 1`] = ` "{ 'modules': { diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index a2048bbdc822d3..d0e395e31ac007 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -61,6 +61,82 @@ function nullGuard(fn: () => T): ?T { return fn(); } +function translateArrayTypeAnnotation( + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + cxxOnly: boolean, + tsArrayType: 'Array' | 'ReadonlyArray', + tsElementType: $FlowFixMe, + nullable: $FlowFixMe, +): Nullable { + try { + /** + * TODO(T72031674): Migrate all our NativeModule specs to not use + * invalid Array ElementTypes. Then, make the elementType a required + * parameter. + */ + const [elementType, isElementTypeNullable] = unwrapNullable( + translateTypeAnnotation( + hasteModuleName, + tsElementType, + types, + aliasMap, + /** + * TODO(T72031674): Ensure that all ParsingErrors that are thrown + * while parsing the array element don't get captured and collected. + * Why? If we detect any parsing error while parsing the element, + * we should default it to null down the line, here. This is + * the correct behaviour until we migrate all our NativeModule specs + * to be parseable. + */ + nullGuard, + cxxOnly, + ), + ); + + if (elementType.type === 'VoidTypeAnnotation') { + throw new UnsupportedArrayElementTypeAnnotationParserError( + hasteModuleName, + tsElementType, + tsArrayType, + 'void', + ); + } + + if (elementType.type === 'PromiseTypeAnnotation') { + throw new UnsupportedArrayElementTypeAnnotationParserError( + hasteModuleName, + tsElementType, + tsArrayType, + 'Promise', + ); + } + + if (elementType.type === 'FunctionTypeAnnotation') { + throw new UnsupportedArrayElementTypeAnnotationParserError( + hasteModuleName, + tsElementType, + tsArrayType, + 'FunctionTypeAnnotation', + ); + } + + const finalTypeAnnotation: NativeModuleArrayTypeAnnotation< + Nullable, + > = { + type: 'ArrayTypeAnnotation', + elementType: wrapNullable(isElementTypeNullable, elementType), + }; + + return wrapNullable(nullable, finalTypeAnnotation); + } catch (ex) { + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + }); + } +} + function translateTypeAnnotation( hasteModuleName: string, /** @@ -76,6 +152,38 @@ function translateTypeAnnotation( resolveTypeAnnotation(typeScriptTypeAnnotation, types); switch (typeAnnotation.type) { + case 'TSArrayType': { + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + cxxOnly, + 'Array', + typeAnnotation.elementType, + nullable, + ); + } + case 'TSTypeOperator': { + if ( + typeAnnotation.operator === 'readonly' && + typeAnnotation.typeAnnotation.type === 'TSArrayType' + ) { + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + cxxOnly, + 'ReadonlyArray', + typeAnnotation.typeAnnotation.elementType, + nullable, + ); + } else { + throw new UnsupportedTypeScriptGenericParserError( + hasteModuleName, + typeAnnotation, + ); + } + } case 'TSTypeReference': { switch (typeAnnotation.typeName.name) { case 'RootTag': { @@ -101,71 +209,15 @@ function translateTypeAnnotation( typeAnnotation, ); - try { - /** - * TODO(T72031674): Migrate all our NativeModule specs to not use - * invalid Array ElementTypes. Then, make the elementType a required - * parameter. - */ - const [elementType, isElementTypeNullable] = unwrapNullable( - translateTypeAnnotation( - hasteModuleName, - typeAnnotation.typeParameters.params[0], - types, - aliasMap, - /** - * TODO(T72031674): Ensure that all ParsingErrors that are thrown - * while parsing the array element don't get captured and collected. - * Why? If we detect any parsing error while parsing the element, - * we should default it to null down the line, here. This is - * the correct behaviour until we migrate all our NativeModule specs - * to be parseable. - */ - nullGuard, - cxxOnly, - ), - ); - - if (elementType.type === 'VoidTypeAnnotation') { - throw new UnsupportedArrayElementTypeAnnotationParserError( - hasteModuleName, - typeAnnotation.typeParameters.params[0], - typeAnnotation.type, - 'void', - ); - } - - if (elementType.type === 'PromiseTypeAnnotation') { - throw new UnsupportedArrayElementTypeAnnotationParserError( - hasteModuleName, - typeAnnotation.typeParameters.params[0], - typeAnnotation.type, - 'Promise', - ); - } - - if (elementType.type === 'FunctionTypeAnnotation') { - throw new UnsupportedArrayElementTypeAnnotationParserError( - hasteModuleName, - typeAnnotation.typeParameters.params[0], - typeAnnotation.type, - 'FunctionTypeAnnotation', - ); - } - - const finalTypeAnnotation: NativeModuleArrayTypeAnnotation< - Nullable, - > = { - type: 'ArrayTypeAnnotation', - elementType: wrapNullable(isElementTypeNullable, elementType), - }; - - return wrapNullable(nullable, finalTypeAnnotation); - } catch (ex) { - return wrapNullable(nullable, { - type: 'ArrayTypeAnnotation', - }); - } + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + cxxOnly, + typeAnnotation.type, + typeAnnotation.typeParameters.params[0], + nullable, + ); } case 'Readonly': { assertGenericTypeAnnotationHasExactlyOneTypeParameter(