diff --git a/packages/react-native-codegen/src/parsers/consistency/__tests__/checkComponentSnaps-test.js b/packages/react-native-codegen/src/parsers/consistency/__tests__/checkComponentSnaps-test.js index 0e3efb671d6..cf2f51b1780 100644 --- a/packages/react-native-codegen/src/parsers/consistency/__tests__/checkComponentSnaps-test.js +++ b/packages/react-native-codegen/src/parsers/consistency/__tests__/checkComponentSnaps-test.js @@ -17,7 +17,11 @@ const flowSnaps = require('../../../../src/parsers/flow/components/__tests__/__s const flowExtraCases = []; const tsFixtures = require('../../typescript/components/__test_fixtures__/fixtures.js'); const tsSnaps = require('../../../../src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap'); -const tsExtraCases = ['ARRAY2_PROP_TYPES_NO_EVENTS', 'ARRAY2_STATE_TYPES']; +const tsExtraCases = [ + 'ARRAY2_PROP_TYPES_NO_EVENTS', + 'ARRAY2_STATE_TYPES', + 'PROPS_AND_EVENTS_WITH_INTERFACES', +]; const ignoredCases = ['ARRAY_PROP_TYPES_NO_EVENTS', 'ARRAY_STATE_TYPES']; compareSnaps( diff --git a/packages/react-native-codegen/src/parsers/typescript/components/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/typescript/components/__test_fixtures__/fixtures.js index f3b0df5c46a..0d31512d373 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/__test_fixtures__/fixtures.js @@ -1095,6 +1095,49 @@ export default codegenNativeComponent( ) as NativeType; `; +const PROPS_AND_EVENTS_WITH_INTERFACES = ` +import type { + BubblingEventHandler, + DirectEventHandler, + Int32, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export interface Base1 { + readonly x: string; +} + +export interface Base2 { + readonly y: Int32; +} + +export interface Derived extends Base1, Base2 { + readonly z: boolean; +} + +export interface ModuleProps extends ViewProps { + // Props + ordinary_prop: Derived; + readonly_prop: Readonly; + ordinary_array_prop?: readonly Derived[]; + readonly_array_prop?: readonly Readonly[]; + ordinary_nested_array_prop?: readonly Derived[][]; + readonly_nested_array_prop?: readonly Readonly[][]; + + // Events + onDirect: DirectEventHandler; + onBubbling: BubblingEventHandler>; +} + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + paperComponentName: 'RCTModule', +}) as HostComponent; +`; + // === STATE === const ALL_STATE_TYPES = ` /** @@ -1630,6 +1673,7 @@ module.exports = { COMMANDS_DEFINED_WITH_ALL_TYPES, PROPS_AS_EXTERNAL_TYPES, COMMANDS_WITH_EXTERNAL_TYPES, + PROPS_AND_EVENTS_WITH_INTERFACES, ALL_STATE_TYPES, ARRAY_STATE_TYPES, ARRAY2_STATE_TYPES, diff --git a/packages/react-native-codegen/src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap index 1634f2bfe48..5d20c9daeae 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap @@ -12640,6 +12640,317 @@ exports[`RN Codegen TypeScript Parser can generate fixture PROPS_AND_EVENTS_TYPE }" `; +exports[`RN Codegen TypeScript Parser can generate fixture PROPS_AND_EVENTS_WITH_INTERFACES 1`] = ` +"{ + 'modules': { + 'Module': { + 'type': 'Component', + 'components': { + 'Module': { + 'interfaceOnly': true, + 'paperComponentName': 'RCTModule', + 'extendsProps': [ + { + 'type': 'ReactNativeBuiltInType', + 'knownTypeName': 'ReactNativeCoreViewProps' + } + ], + 'events': [ + { + 'name': 'onDirect', + 'optional': false, + 'bubblingType': 'direct', + 'typeAnnotation': { + 'type': 'EventTypeAnnotation', + 'argument': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation' + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation' + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation' + } + } + ] + } + } + }, + { + 'name': 'onBubbling', + 'optional': false, + 'bubblingType': 'bubble', + 'typeAnnotation': { + 'type': 'EventTypeAnnotation', + 'argument': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation' + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation' + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation' + } + } + ] + } + } + } + ], + 'props': [ + { + 'name': 'ordinary_prop', + 'optional': false, + 'typeAnnotation': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation', + 'default': null + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation', + 'default': 0 + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation', + 'default': false + } + } + ] + } + }, + { + 'name': 'readonly_prop', + 'optional': false, + 'typeAnnotation': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation', + 'default': null + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation', + 'default': 0 + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation', + 'default': false + } + } + ] + } + }, + { + 'name': 'ordinary_array_prop', + 'optional': true, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation', + 'default': null + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation', + 'default': 0 + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation', + 'default': false + } + } + ] + } + } + }, + { + 'name': 'readonly_array_prop', + 'optional': true, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation', + 'default': null + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation', + 'default': 0 + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation', + 'default': false + } + } + ] + } + } + }, + { + 'name': 'ordinary_nested_array_prop', + 'optional': true, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation', + 'default': null + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation', + 'default': 0 + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation', + 'default': false + } + } + ] + } + } + } + }, + { + 'name': 'readonly_nested_array_prop', + 'optional': true, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'StringTypeAnnotation', + 'default': null + } + }, + { + 'name': 'y', + 'optional': false, + 'typeAnnotation': { + 'type': 'Int32TypeAnnotation', + 'default': 0 + } + }, + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'BooleanTypeAnnotation', + 'default': false + } + } + ] + } + } + } + } + ], + 'commands': [] + } + } + } + } +}" +`; + exports[`RN Codegen TypeScript Parser can generate fixture PROPS_AS_EXTERNAL_TYPES 1`] = ` "{ 'modules': { diff --git a/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js b/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js index f8818eb77cb..bd046a068d0 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js @@ -45,38 +45,29 @@ function getProperties( } function getTypeAnnotationForObjectAsArrayElement( - objectType: $FlowFixMe, - types: TypeDeclarationMap, - buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape, -): $FlowFixMe { - return { - type: 'ObjectTypeAnnotation', - properties: flattenProperties( - objectType.typeParameters.params[0].members || - objectType.typeParameters.params, - types, - ) - .map(prop => buildSchema(prop, types)) - .filter(Boolean), - }; -} - -function getTypeAnnotationForArrayOfArrayOfObject( + name: string, typeAnnotation: $FlowFixMe, types: TypeDeclarationMap, buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape, ): $FlowFixMe { - // We need to go yet another level deeper to resolve - // types that may be defined in a type alias - const nestedObjectType = getValueFromTypes(typeAnnotation, types); + // for array of array of a type + // such type must be an object literal + const elementType = getTypeAnnotationForArray( + name, + typeAnnotation, + null, + types, + buildSchema, + ); + if (elementType.type !== 'ObjectTypeAnnotation') { + throw new Error( + `Only array of array of object is supported for "${name}".`, + ); + } return { type: 'ArrayTypeAnnotation', - elementType: getTypeAnnotationForObjectAsArrayElement( - nestedObjectType, - types, - buildSchema, - ), + elementType, }; } @@ -121,7 +112,8 @@ function getTypeAnnotationForArray( // Covers: T[] if (typeAnnotation.type === 'TSArrayType') { - return getTypeAnnotationForArrayOfArrayOfObject( + return getTypeAnnotationForObjectAsArrayElement( + name, typeAnnotation.elementType, types, buildSchema, @@ -133,8 +125,10 @@ function getTypeAnnotationForArray( const objectType = getValueFromTypes(extractedTypeAnnotation, types); if (objectType.typeName.name === 'Readonly') { - return getTypeAnnotationForObjectAsArrayElement( - objectType, + return getTypeAnnotationForArray( + name, + objectType.typeParameters.params[0], + defaultValue, types, buildSchema, ); @@ -142,7 +136,8 @@ function getTypeAnnotationForArray( // Covers: ReadonlyArray if (objectType.typeName.name === 'ReadonlyArray') { - return getTypeAnnotationForArrayOfArrayOfObject( + return getTypeAnnotationForObjectAsArrayElement( + name, objectType.typeParameters.params[0], types, buildSchema, @@ -158,6 +153,19 @@ function getTypeAnnotationForArray( extractedTypeAnnotation.type; switch (type) { + case 'TSTypeLiteral': + case 'TSInterfaceDeclaration': { + const rawProperties = + type === 'TSInterfaceDeclaration' + ? [typeAnnotation] + : typeAnnotation.members; + return { + type: 'ObjectTypeAnnotation', + properties: flattenProperties(rawProperties, types) + .map(prop => buildSchema(prop, types)) + .filter(Boolean), + }; + } case 'TSNumberKeyword': return { type: 'FloatTypeAnnotation', @@ -336,27 +344,28 @@ function getTypeAnnotation( }; } + // Covers: Readonly, Readonly<{ ... }>, Readonly if ( - (typeAnnotation.type === 'TSTypeReference' || - typeAnnotation.type === 'TSTypeLiteral') && - typeAnnotation.typeName?.name === 'Readonly' + typeAnnotation.type === 'TSTypeReference' && + typeAnnotation.typeName?.name === 'Readonly' && + typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation' ) { - const rawProperties = - typeAnnotation.typeParameters.params[0].members || - (typeAnnotation.typeParameters.params[0].types && - typeAnnotation.typeParameters.params[0].types[0].members) || - typeAnnotation.typeParameters.params; - - const flattenedProperties = flattenProperties(rawProperties, types); - - const properties = flattenedProperties - .map(prop => buildSchema(prop, types)) - .filter(Boolean); - - return { - type: 'ObjectTypeAnnotation', - properties, - }; + // TODO: + // the original implementation assume Readonly + // to be Readonly<{ ... } | null | undefined> + // without actually verifying it + let elementType = typeAnnotation.typeParameters.params[0]; + if (elementType.type === 'TSUnionType') { + elementType = elementType.types[0]; + } + return getTypeAnnotation( + name, + elementType, + defaultValue, + withNullDefault, + types, + buildSchema, + ); } const type = @@ -366,6 +375,22 @@ function getTypeAnnotation( : typeAnnotation.type; switch (type) { + case 'TSTypeLiteral': + case 'TSInterfaceDeclaration': { + const rawProperties = + type === 'TSInterfaceDeclaration' + ? [typeAnnotation] + : typeAnnotation.members; + const flattenedProperties = flattenProperties(rawProperties, types); + const properties = flattenedProperties + .map(prop => buildSchema(prop, types)) + .filter(Boolean); + + return { + type: 'ObjectTypeAnnotation', + properties, + }; + } case 'ImageSource': return { type: 'ReservedPropTypeAnnotation', @@ -662,11 +687,22 @@ function flattenProperties( getProperties(property.typeName.name, types), types, ); - } else if (property.type === 'TSExpressionWithTypeArguments') { + } else if ( + property.type === 'TSExpressionWithTypeArguments' || + property.type === 'TSInterfaceHeritage' + ) { return flattenProperties( getProperties(property.expression.name, types), types, ); + } else if (property.type === 'TSTypeLiteral') { + return flattenProperties(property.members, types); + } else if (property.type === 'TSInterfaceDeclaration') { + return flattenProperties(getProperties(property.id.name, types), types); + } else { + throw new Error( + `${property.type} is not a supported object literal type.`, + ); } }) .filter(Boolean) diff --git a/packages/react-native-codegen/src/parsers/typescript/components/events.js b/packages/react-native-codegen/src/parsers/typescript/components/events.js index 59eccaea461..a6256242865 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/events.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/events.js @@ -15,6 +15,7 @@ import type { NamedShape, EventTypeAnnotation, } from '../../../CodegenSchema.js'; +const {flattenProperties} = require('./componentsUtils'); function getPropertyType( /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's @@ -126,16 +127,33 @@ function findEventArgumentsAndType( bubblingType: void | 'direct' | 'bubble', paperName: ?$FlowFixMe, ) { + if (typeAnnotation.type === 'TSInterfaceDeclaration') { + return { + argumentProps: flattenProperties([typeAnnotation], types), + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } + + if (typeAnnotation.type === 'TSTypeLiteral') { + return { + argumentProps: typeAnnotation.members, + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } + if (!typeAnnotation.typeName) { throw new Error("typeAnnotation of event doesn't have a name"); } const name = typeAnnotation.typeName.name; if (name === 'Readonly') { - return { - argumentProps: typeAnnotation.typeParameters.params[0].members, - paperTopLevelNameDeprecated: paperName, + return findEventArgumentsAndType( + typeAnnotation.typeParameters.params[0], + types, bubblingType, - }; + paperName, + ); } else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') { const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct'; const paperTopLevelNameDeprecated = @@ -160,8 +178,12 @@ function findEventArgumentsAndType( ); } } else if (types[name]) { + let elementType = types[name]; + if (elementType.type === 'TSTypeAliasDeclaration') { + elementType = elementType.typeAnnotation; + } return findEventArgumentsAndType( - types[name].typeAnnotation, + elementType, types, bubblingType, paperName, diff --git a/packages/react-native-codegen/src/parsers/typescript/utils.js b/packages/react-native-codegen/src/parsers/typescript/utils.js index 3ee71dd69b9..51b1b8f9508 100644 --- a/packages/react-native-codegen/src/parsers/typescript/utils.js +++ b/packages/react-native-codegen/src/parsers/typescript/utils.js @@ -125,15 +125,18 @@ function resolveTypeAnnotation( } function getValueFromTypes(value: ASTNode, types: TypeDeclarationMap): ASTNode { - if (value.type === 'TSTypeReference' && types[value.typeName.name]) { - return getValueFromTypes(types[value.typeName.name], types); + switch (value.type) { + case 'TSTypeReference': + if (types[value.typeName.name]) { + return getValueFromTypes(types[value.typeName.name], types); + } else { + return value; + } + case 'TSTypeAliasDeclaration': + return getValueFromTypes(value.typeAnnotation, types); + default: + return value; } - - if (value.type === 'TSTypeAliasDeclaration') { - return value.typeAnnotation; - } - - return value; } export type ParserErrorCapturer = (fn: () => T) => ?T;