diff --git a/packages/react-native-codegen/src/parsers/typescript/components/commands.js b/packages/react-native-codegen/src/parsers/typescript/components/commands.js index 48756da897a..10d3ca79255 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/commands.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/commands.js @@ -22,17 +22,20 @@ type EventTypeAST = Object; function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) { const name = property.key.name; - const optional = property.optional; - const value = getValueFromTypes(property.value, types); + const optional = property.optional || false; + const value = getValueFromTypes( + property.typeAnnotation.typeAnnotation, + types, + ); - const firstParam = value.params[0].typeAnnotation; + const firstParam = value.parameters[0].typeAnnotation; if ( !( - firstParam.id != null && - firstParam.id.type === 'QualifiedTypeIdentifier' && - firstParam.id.qualification.name === 'React' && - firstParam.id.id.name === 'ElementRef' + firstParam.typeAnnotation != null && + firstParam.typeAnnotation.type === 'TSTypeReference' && + firstParam.typeAnnotation.typeName.left?.name === 'React' && + firstParam.typeAnnotation.typeName.right?.name === 'ElementRef' ) ) { throw new Error( @@ -40,12 +43,16 @@ function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) { ); } - const params = value.params.slice(1).map(param => { - const paramName = param.name.name; - const paramValue = getValueFromTypes(param.typeAnnotation, types); + const params = value.parameters.slice(1).map(param => { + const paramName = param.name; + const paramValue = getValueFromTypes( + param.typeAnnotation.typeAnnotation, + types, + ); + const type = - paramValue.type === 'GenericTypeAnnotation' - ? paramValue.id.name + paramValue.type === 'TSTypeReference' + ? paramValue.typeName.name : paramValue.type; let returnType; @@ -56,7 +63,7 @@ function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) { name: 'RootTag', }; break; - case 'BooleanTypeAnnotation': + case 'TSBooleanKeyword': returnType = { type: 'BooleanTypeAnnotation', }; @@ -76,7 +83,7 @@ function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) { type: 'FloatTypeAnnotation', }; break; - case 'StringTypeAnnotation': + case 'TSStringKeyword': returnType = { type: 'StringTypeAnnotation', }; @@ -112,7 +119,7 @@ function getCommands( types: TypeDeclarationMap, ): $ReadOnlyArray> { return commandTypeAST - .filter(property => property.type === 'ObjectTypeProperty') + .filter(property => property.type === 'TSPropertySignature') .map(property => buildCommandSchema(property, types)) .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 f6c68c80fcd..a5830504e16 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/events.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/events.js @@ -22,12 +22,12 @@ function getPropertyType( typeAnnotation, ): NamedShape { const type = - typeAnnotation.type === 'GenericTypeAnnotation' - ? typeAnnotation.id.name + typeAnnotation.type === 'TSTypeReference' + ? typeAnnotation.typeName.name : typeAnnotation.type; switch (type) { - case 'BooleanTypeAnnotation': + case 'TSBooleanKeyword': return { name, optional, @@ -35,7 +35,7 @@ function getPropertyType( type: 'BooleanTypeAnnotation', }, }; - case 'StringTypeAnnotation': + case 'TSStringKeyword': return { name, optional, @@ -67,28 +67,48 @@ function getPropertyType( type: 'FloatTypeAnnotation', }, }; - case '$ReadOnly': + case 'Readonly': return getPropertyType( name, optional, typeAnnotation.typeParameters.params[0], ); - case 'ObjectTypeAnnotation': + + case 'TSTypeLiteral': return { name, optional, typeAnnotation: { type: 'ObjectTypeAnnotation', - properties: typeAnnotation.properties.map(buildPropertiesForEvent), + properties: typeAnnotation.members.map(buildPropertiesForEvent), }, }; - case 'UnionTypeAnnotation': + + case 'TSUnionType': + // Check for + if ( + typeAnnotation.types.some( + t => t.type === 'TSNullKeyword' || t.type === 'TSVoidKeyword', + ) + ) { + const optionalType = typeAnnotation.types.filter( + t => t.type !== 'TSNullKeyword' && t.type !== 'TSVoidKeyword', + )[0]; + + // Check for <(T | T2) | null | void> + if (optionalType.type === 'TSParenthesizedType') { + return getPropertyType(name, true, optionalType.typeAnnotation); + } + + return getPropertyType(name, true, optionalType); + } + return { name, optional, typeAnnotation: { type: 'StringEnumTypeAnnotation', - options: typeAnnotation.types.map(option => option.value), + options: typeAnnotation.types.map(option => option.literal.value), }, }; default: @@ -103,13 +123,13 @@ function findEventArgumentsAndType( bubblingType, paperName, ) { - if (!typeAnnotation.id) { + if (!typeAnnotation.typeName) { throw new Error("typeAnnotation of event doesn't have a name"); } - const name = typeAnnotation.id.name; - if (name === '$ReadOnly') { + const name = typeAnnotation.typeName.name; + if (name === 'Readonly') { return { - argumentProps: typeAnnotation.typeParameters.params[0].properties, + argumentProps: typeAnnotation.typeParameters.params[0].members, paperTopLevelNameDeprecated: paperName, bubblingType, }; @@ -117,12 +137,10 @@ function findEventArgumentsAndType( const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct'; const paperTopLevelNameDeprecated = typeAnnotation.typeParameters.params.length > 1 - ? typeAnnotation.typeParameters.params[1].value + ? typeAnnotation.typeParameters.params[1].literal.value : null; - if ( - typeAnnotation.typeParameters.params[0].type === - 'NullLiteralTypeAnnotation' - ) { + + if (typeAnnotation.typeParameters.params[0].type === 'TSNullKeyword') { return { argumentProps: [], bubblingType: eventType, @@ -137,7 +155,7 @@ function findEventArgumentsAndType( ); } else if (types[name]) { return findEventArgumentsAndType( - types[name].right, + types[name].typeAnnotation, types, bubblingType, paperName, @@ -153,12 +171,8 @@ function findEventArgumentsAndType( function buildPropertiesForEvent(property): NamedShape { const name = property.key.name; - const optional = - property.value.type === 'NullableTypeAnnotation' || property.optional; - let typeAnnotation = - property.value.type === 'NullableTypeAnnotation' - ? property.value.typeAnnotation - : property.value; + const optional = property.optional || false; + let typeAnnotation = property.typeAnnotation.typeAnnotation; return getPropertyType(name, optional, typeAnnotation); } @@ -175,18 +189,27 @@ function buildEventSchema( property: EventTypeAST, ): ?EventTypeShape { const name = property.key.name; - const optional = - property.optional || property.value.type === 'NullableTypeAnnotation'; - let typeAnnotation = - property.value.type === 'NullableTypeAnnotation' - ? property.value.typeAnnotation - : property.value; + let optional = property.optional || false; + let typeAnnotation = property.typeAnnotation.typeAnnotation; + + // Check for T | null | void + if ( + typeAnnotation.type === 'TSUnionType' && + typeAnnotation.types.some( + t => t.type === 'TSNullKeyword' || t.type === 'TSVoidKeyword', + ) + ) { + typeAnnotation = typeAnnotation.types.filter( + t => t.type !== 'TSNullKeyword' && t.type !== 'TSVoidKeyword', + )[0]; + optional = true; + } if ( - typeAnnotation.type !== 'GenericTypeAnnotation' || - (typeAnnotation.id.name !== 'BubblingEventHandler' && - typeAnnotation.id.name !== 'DirectEventHandler') + typeAnnotation.type !== 'TSTypeReference' || + (typeAnnotation.typeName.name !== 'BubblingEventHandler' && + typeAnnotation.typeName.name !== 'DirectEventHandler') ) { return null; } @@ -228,11 +251,11 @@ function buildEventSchema( } } -// $FlowFixMe[unclear-type] there's no flowtype for ASTs +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser type EventTypeAST = Object; type TypeMap = { - // $FlowFixMe[unclear-type] there's no flowtype for ASTs + // $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser [string]: Object, ... }; @@ -242,7 +265,7 @@ function getEvents( types: TypeMap, ): $ReadOnlyArray { return eventTypeAST - .filter(property => property.type === 'ObjectTypeProperty') + .filter(property => property.type === 'TSPropertySignature') .map(property => buildEventSchema(types, property)) .filter(Boolean); } diff --git a/packages/react-native-codegen/src/parsers/typescript/components/extends.js b/packages/react-native-codegen/src/parsers/typescript/components/extends.js index 890329400a6..b4f708723bd 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/extends.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/extends.js @@ -14,10 +14,10 @@ import type {ExtendsPropsShape} from '../../../CodegenSchema.js'; import type {TypeDeclarationMap} from '../utils.js'; function extendsForProp(prop: PropsAST, types: TypeDeclarationMap) { - if (!prop.argument) { + if (!prop.expression) { console.log('null', prop); } - const name = prop.argument.id.name; + const name = prop.expression.name; if (types[name] != null) { // This type is locally defined in the file @@ -42,12 +42,12 @@ function removeKnownExtends( ): $ReadOnlyArray { return typeDefinition.filter( prop => - prop.type !== 'ObjectTypeSpreadProperty' || + prop.type !== 'TSExpressionWithTypeArguments' || extendsForProp(prop, types) === null, ); } -// $FlowFixMe[unclear-type] there's no flowtype for ASTs +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser type PropsAST = Object; function getExtendsProps( @@ -55,7 +55,7 @@ function getExtendsProps( types: TypeDeclarationMap, ): $ReadOnlyArray { return typeDefinition - .filter(prop => prop.type === 'ObjectTypeSpreadProperty') + .filter(prop => prop.type === 'TSExpressionWithTypeArguments') .map(prop => extendsForProp(prop, types)) .filter(Boolean); } diff --git a/packages/react-native-codegen/src/parsers/typescript/components/index.js b/packages/react-native-codegen/src/parsers/typescript/components/index.js index e49d40b9846..b16812fb4a5 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/index.js @@ -32,17 +32,17 @@ function findComponentConfig(ast) { // codegenNativeComponent can be nested inside a cast // expression so we need to go one level deeper - if (declaration.type === 'TypeCastExpression') { + if (declaration.type === 'TSAsExpression') { declaration = declaration.expression; } try { if (declaration.callee.name === 'codegenNativeComponent') { - const typeArgumentParams = declaration.typeArguments.params; + const typeArgumentParams = declaration.typeParameters.params; const funcArgumentParams = declaration.arguments; const nativeComponentType = {}; - nativeComponentType.propsTypeName = typeArgumentParams[0].id.name; + nativeComponentType.propsTypeName = typeArgumentParams[0].typeName.name; nativeComponentType.componentName = funcArgumentParams[0].value; if (funcArgumentParams.length > 1) { nativeComponentType.optionsExpression = funcArgumentParams[1]; @@ -89,16 +89,16 @@ function findComponentConfig(ast) { ); } - const typeArgumentParam = callExpression.typeArguments.params[0]; + const typeArgumentParam = callExpression.typeParameters.params[0]; - if (typeArgumentParam.type !== 'GenericTypeAnnotation') { + if (typeArgumentParam.type !== 'TSTypeReference') { throw new Error( "codegenNativeCommands doesn't support inline definitions. Specify a file local type alias", ); } return { - commandTypeName: typeArgumentParam.id.name, + commandTypeName: typeArgumentParam.typeName.name, commandOptionsExpression: callExpression.arguments[0], }; }) @@ -132,7 +132,7 @@ function getCommandProperties( const typeAlias = types[commandTypeName]; - if (typeAlias.type !== 'InterfaceDeclaration') { + if (typeAlias.type !== 'TSInterfaceDeclaration') { throw new Error( `The type argument for codegenNativeCommands must be an interface, received ${typeAlias.type}`, ); @@ -140,14 +140,14 @@ function getCommandProperties( let properties; try { - properties = typeAlias.body.properties; + properties = typeAlias.body.body; } catch (e) { throw new Error( - `Failed to find type definition for "${commandTypeName}", please check that you have a valid codegen flow file`, + `Failed to find type definition for "${commandTypeName}", please check that you have a valid codegen typescript file`, ); } - const flowPropertyNames = properties + const typeScriptPropertyNames = properties .map(property => property && property.key && property.key.name) .filter(Boolean); @@ -158,13 +158,14 @@ function getCommandProperties( } if ( - commandOptions.supportedCommands.length !== flowPropertyNames.length || + commandOptions.supportedCommands.length !== + typeScriptPropertyNames.length || !commandOptions.supportedCommands.every(supportedCommand => - flowPropertyNames.includes(supportedCommand), + typeScriptPropertyNames.includes(supportedCommand), ) ) { throw new Error( - `codegenNativeCommands expected the same supportedCommands specified in the ${commandTypeName} interface: ${flowPropertyNames.join( + `codegenNativeCommands expected the same supportedCommands specified in the ${commandTypeName} interface: ${typeScriptPropertyNames.join( ', ', )}`, ); @@ -173,7 +174,7 @@ function getCommandProperties( return properties; } -// $FlowFixMe[signature-verification-failure] there's no flowtype for AST +// $FlowFixMe[signature-verification-failure] TODO(T108222691): Use flow-types for @babel/parser function buildComponentSchema(ast): ComponentSchemaBuilderConfig { const { componentName, diff --git a/packages/react-native-codegen/src/parsers/typescript/components/options.js b/packages/react-native-codegen/src/parsers/typescript/components/options.js index 8a137c5cd7c..b82e6e50986 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/options.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/options.js @@ -12,7 +12,7 @@ import type {OptionsShape} from '../../../CodegenSchema.js'; -// $FlowFixMe[unclear-type] there's no flowtype for ASTs +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser type OptionsAST = Object; export type CommandOptions = $ReadOnly<{ diff --git a/packages/react-native-codegen/src/parsers/typescript/components/props.js b/packages/react-native-codegen/src/parsers/typescript/components/props.js index ab8d9caa41a..537f38e2fa6 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/props.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/props.js @@ -19,12 +19,23 @@ function getPropProperties( propsTypeName: string, types: TypeDeclarationMap, ): $FlowFixMe { - const typeAlias = types[propsTypeName]; + const alias = types[propsTypeName]; + const aliasKind = + alias.type === 'TSInterfaceDeclaration' ? 'interface' : 'type'; + try { - return typeAlias.right.typeParameters.params[0].properties; + if (aliasKind === 'interface') { + return [...(alias.extends ?? []), ...alias.body.body]; + } + + return ( + alias.typeAnnotation.members || + alias.typeAnnotation.typeParameters.params[0].members || + alias.typeAnnotation.typeParameters.params + ); } catch (e) { throw new Error( - `Failed to find type definition for "${propsTypeName}", please check that you have a valid codegen flow file`, + `Failed to find ${aliasKind} definition for "${propsTypeName}", please check that you have a valid codegen typescript file`, ); } } @@ -36,30 +47,37 @@ function getTypeAnnotationForArray( types: TypeDeclarationMap, ) { const extractedTypeAnnotation = getValueFromTypes(typeAnnotation, types); - if (extractedTypeAnnotation.type === 'NullableTypeAnnotation') { + + if ( + extractedTypeAnnotation.type === 'TSUnionType' && + extractedTypeAnnotation.types.some( + t => t.type === 'TSNullKeyword' || t.type === 'TSVoidKeyword', + ) + ) { throw new Error( - 'Nested optionals such as "$ReadOnlyArray" are not supported, please declare optionals at the top level of value definitions as in "?$ReadOnlyArray"', + 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | null | void"', ); } if ( - extractedTypeAnnotation.type === 'GenericTypeAnnotation' && - extractedTypeAnnotation.id.name === 'WithDefault' + extractedTypeAnnotation.type === 'TSTypeReference' && + extractedTypeAnnotation.typeName.name === 'WithDefault' ) { throw new Error( - 'Nested defaults such as "$ReadOnlyArray>" are not supported, please declare defaults at the top level of value definitions as in "WithDefault<$ReadOnlyArray, false>"', + 'Nested defaults such as "ReadonlyArray>" are not supported, please declare defaults at the top level of value definitions as in "WithDefault, false>"', ); } - if (extractedTypeAnnotation.type === 'GenericTypeAnnotation') { + if (extractedTypeAnnotation.type === 'TSTypeReference') { // Resolve the type alias if it's not defined inline const objectType = getValueFromTypes(extractedTypeAnnotation, types); - if (objectType.id.name === '$ReadOnly') { + if (objectType.typeName.name === 'Readonly') { return { type: 'ObjectTypeAnnotation', properties: flattenProperties( - objectType.typeParameters.params[0].properties, + objectType.typeParameters.params[0].members || + objectType.typeParameters.params, types, ) .map(prop => buildPropSchema(prop, types)) @@ -67,7 +85,7 @@ function getTypeAnnotationForArray( }; } - if (objectType.id.name === '$ReadOnlyArray') { + if (objectType.typeName.name === 'ReadonlyArray') { // We need to go yet another level deeper to resolve // types that may be defined in a type alias const nestedObjectType = getValueFromTypes( @@ -80,7 +98,8 @@ function getTypeAnnotationForArray( elementType: { type: 'ObjectTypeAnnotation', properties: flattenProperties( - nestedObjectType.typeParameters.params[0].properties, + nestedObjectType.typeParameters.params[0].members || + nestedObjectType.typeParameters.params, types, ) .map(prop => buildPropSchema(prop, types)) @@ -91,11 +110,17 @@ function getTypeAnnotationForArray( } const type = - extractedTypeAnnotation.type === 'GenericTypeAnnotation' - ? extractedTypeAnnotation.id.name - : extractedTypeAnnotation.type; + extractedTypeAnnotation.elementType === 'TSTypeReference' + ? extractedTypeAnnotation.elementType.typeName.name + : extractedTypeAnnotation.elementType?.type || + extractedTypeAnnotation.typeName?.name || + extractedTypeAnnotation.type; switch (type) { + case 'TSNumberKeyword': + return { + type: 'FloatTypeAnnotation', + }; case 'ImageSource': return { type: 'ReservedPropTypeAnnotation', @@ -133,17 +158,26 @@ function getTypeAnnotationForArray( return { type: 'FloatTypeAnnotation', }; - case 'BooleanTypeAnnotation': + case 'TSBooleanKeyword': return { type: 'BooleanTypeAnnotation', }; - case 'StringTypeAnnotation': + case 'TSStringKeyword': return { type: 'StringTypeAnnotation', }; - case 'UnionTypeAnnotation': + case 'TSUnionType': typeAnnotation.types.reduce((lastType, currType) => { - if (lastType && currType.type !== lastType.type) { + const lastFlattenedType = + lastType && lastType.type === 'TSLiteralType' + ? lastType.literal.type + : lastType.type; + const currFlattenedType = + currType.type === 'TSLiteralType' + ? currType.literal.type + : currType.type; + + if (lastFlattenedType && currFlattenedType !== lastFlattenedType) { throw new Error(`Mixed types are not supported (see "${name}")`); } return currType; @@ -154,19 +188,29 @@ function getTypeAnnotationForArray( } const unionType = typeAnnotation.types[0].type; - if (unionType === 'StringLiteralTypeAnnotation') { + if ( + unionType === 'TSLiteralType' && + typeAnnotation.types[0].literal?.type === 'StringLiteral' + ) { return { type: 'StringEnumTypeAnnotation', default: (defaultValue: string), - options: typeAnnotation.types.map(option => option.value), + options: typeAnnotation.types.map(option => option.literal.value), }; - } else if (unionType === 'NumberLiteralTypeAnnotation') { + } else if ( + unionType === 'TSLiteralType' && + typeAnnotation.types[0].literal?.type === 'NumericLiteral' + ) { throw new Error( `Arrays of int enums are not supported (see: "${name}")`, ); } else { throw new Error( - `Unsupported union type for "${name}", received "${unionType}"`, + `Unsupported union type for "${name}", received "${ + unionType === 'TSLiteralType' + ? typeAnnotation.types[0].literal?.type + : unionType + }"`, ); } default: @@ -184,9 +228,45 @@ function getTypeAnnotation( ) { const typeAnnotation = getValueFromTypes(annotation, types); + // Covers: readonly T[] if ( - typeAnnotation.type === 'GenericTypeAnnotation' && - typeAnnotation.id.name === '$ReadOnlyArray' + typeAnnotation.type === 'TSTypeOperator' && + typeAnnotation.operator === 'readonly' && + typeAnnotation.typeAnnotation.type === 'TSArrayType' + ) { + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( + name, + typeAnnotation.typeAnnotation, + defaultValue, + types, + ), + }; + } + + // Covers: ReadonlyArray + if ( + typeAnnotation.type === 'TSTypeReference' && + typeAnnotation.typeName.name === 'ReadonlyArray' + ) { + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( + name, + typeAnnotation.typeParameters.params[0], + defaultValue, + types, + ), + }; + } + + // Covers: Readonly + if ( + typeAnnotation.type === 'TSTypeReference' && + typeAnnotation.typeName?.name === 'Readonly' && + typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation' && + typeAnnotation.typeParameters.params[0].type === 'TSArrayType' ) { return { type: 'ArrayTypeAnnotation', @@ -200,23 +280,32 @@ function getTypeAnnotation( } if ( - typeAnnotation.type === 'GenericTypeAnnotation' && - typeAnnotation.id.name === '$ReadOnly' + (typeAnnotation.type === 'TSTypeReference' || + typeAnnotation.type === 'TSTypeLiteral') && + typeAnnotation.typeName?.name === 'Readonly' ) { + 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 => buildPropSchema(prop, types)) + .filter(Boolean); + return { type: 'ObjectTypeAnnotation', - properties: flattenProperties( - typeAnnotation.typeParameters.params[0].properties, - types, - ) - .map(prop => buildPropSchema(prop, types)) - .filter(Boolean), + properties, }; } const type = - typeAnnotation.type === 'GenericTypeAnnotation' - ? typeAnnotation.id.name + typeAnnotation.type === 'TSTypeReference' || + typeAnnotation.type === 'TSTypeAliasDeclaration' + ? typeAnnotation.typeName.name : typeAnnotation.type; switch (type) { @@ -266,14 +355,14 @@ function getTypeAnnotation( ? (defaultValue: number | null) : ((defaultValue ? defaultValue : 0): number), }; - case 'BooleanTypeAnnotation': + case 'TSBooleanKeyword': return { type: 'BooleanTypeAnnotation', default: withNullDefault ? (defaultValue: boolean | null) : ((defaultValue == null ? false : defaultValue): boolean), }; - case 'StringTypeAnnotation': + case 'TSStringKeyword': if (typeof defaultValue !== 'undefined') { return { type: 'StringTypeAnnotation', @@ -289,9 +378,18 @@ function getTypeAnnotation( }; } throw new Error(`A default string (or null) is required for "${name}"`); - case 'UnionTypeAnnotation': + case 'TSUnionType': typeAnnotation.types.reduce((lastType, currType) => { - if (lastType && currType.type !== lastType.type) { + const lastFlattenedType = + lastType && lastType.type === 'TSLiteralType' + ? lastType.literal.type + : lastType.type; + const currFlattenedType = + currType.type === 'TSLiteralType' + ? currType.literal.type + : currType.type; + + if (lastFlattenedType && currFlattenedType !== lastFlattenedType) { throw new Error(`Mixed types are not supported (see "${name}")`); } return currType; @@ -302,24 +400,34 @@ function getTypeAnnotation( } const unionType = typeAnnotation.types[0].type; - if (unionType === 'StringLiteralTypeAnnotation') { + if ( + unionType === 'TSLiteralType' && + typeAnnotation.types[0].literal?.type === 'StringLiteral' + ) { return { type: 'StringEnumTypeAnnotation', default: (defaultValue: string), - options: typeAnnotation.types.map(option => option.value), + options: typeAnnotation.types.map(option => option.literal.value), }; - } else if (unionType === 'NumberLiteralTypeAnnotation') { + } else if ( + unionType === 'TSLiteralType' && + typeAnnotation.types[0].literal?.type === 'NumericLiteral' + ) { return { type: 'Int32EnumTypeAnnotation', default: (defaultValue: number), - options: typeAnnotation.types.map(option => option.value), + options: typeAnnotation.types.map(option => option.literal.value), }; } else { throw new Error( - `Unsupported union type for "${name}", received "${unionType}"`, + `Unsupported union type for "${name}", received "${ + unionType === 'TSLiteralType' + ? typeAnnotation.types[0].literal?.type + : unionType + }"`, ); } - case 'NumberTypeAnnotation': + case 'TSNumberKeyword': throw new Error( `Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`, ); @@ -335,40 +443,72 @@ function buildPropSchema( ): ?NamedShape { const name = property.key.name; - const value = getValueFromTypes(property.value, types); - let typeAnnotation = - value.type === 'NullableTypeAnnotation' ? value.typeAnnotation : value; + const value = getValueFromTypes( + property.typeAnnotation.typeAnnotation, + types, + ); - const optional = - value.type === 'NullableTypeAnnotation' || - property.optional || - (value.type === 'GenericTypeAnnotation' && - typeAnnotation.id.name === 'WithDefault'); + let typeAnnotation = value; + let optional = property.optional || false; + + // Check for optional type in union e.g. T | null | void + if ( + typeAnnotation.type === 'TSUnionType' && + typeAnnotation.types.some( + t => t.type === 'TSNullKeyword' || t.type === 'TSVoidKeyword', + ) + ) { + typeAnnotation = typeAnnotation.types.filter( + t => t.type !== 'TSNullKeyword' && t.type !== 'TSVoidKeyword', + )[0]; + optional = true; + + // Check against optional type inside `WithDefault` + if ( + typeAnnotation.type === 'TSTypeReference' && + typeAnnotation.typeName.name === 'WithDefault' + ) { + throw new Error( + 'WithDefault<> is optional and does not need to be marked as optional. Please remove the union of void and/or null', + ); + } + } + + // example: WithDefault; + if ( + value.type === 'TSTypeReference' && + typeAnnotation.typeName.name === 'WithDefault' + ) { + optional = true; + } + + // example: Readonly<{prop: string} | null | void>; + if ( + value.type === 'TSTypeReference' && + typeAnnotation.typeParameters?.params[0].type === 'TSUnionType' && + typeAnnotation.typeParameters?.params[0].types.some( + element => + element.type === 'TSNullKeyword' || element.type === 'TSVoidKeyword', + ) + ) { + optional = true; + } if ( !property.optional && - value.type === 'GenericTypeAnnotation' && - typeAnnotation.id.name === 'WithDefault' + value.type === 'TSTypeReference' && + typeAnnotation.typeName.name === 'WithDefault' ) { throw new Error( `key ${name} must be optional if used with WithDefault<> annotation`, ); } - if ( - value.type === 'NullableTypeAnnotation' && - typeAnnotation.type === 'GenericTypeAnnotation' && - typeAnnotation.id.name === 'WithDefault' - ) { - throw new Error( - 'WithDefault<> is optional and does not need to be marked as optional. Please remove the ? annotation in front of it.', - ); - } let type = typeAnnotation.type; if ( - type === 'GenericTypeAnnotation' && - (typeAnnotation.id.name === 'DirectEventHandler' || - typeAnnotation.id.name === 'BubblingEventHandler') + type === 'TSTypeReference' && + (typeAnnotation.typeName.name === 'DirectEventHandler' || + typeAnnotation.typeName.name === 'BubblingEventHandler') ) { return null; } @@ -376,7 +516,7 @@ function buildPropSchema( if ( name === 'style' && type === 'GenericTypeAnnotation' && - typeAnnotation.id.name === 'ViewStyleProp' + typeAnnotation.typeName.name === 'ViewStyleProp' ) { return null; } @@ -384,8 +524,8 @@ function buildPropSchema( let defaultValue = null; let withNullDefault = false; if ( - type === 'GenericTypeAnnotation' && - typeAnnotation.id.name === 'WithDefault' + type === 'TSTypeReference' && + typeAnnotation.typeName.name === 'WithDefault' ) { if (typeAnnotation.typeParameters.params.length === 1) { throw new Error( @@ -393,19 +533,24 @@ function buildPropSchema( ); } + let defaultValueType = typeAnnotation.typeParameters.params[1].type; defaultValue = typeAnnotation.typeParameters.params[1].value; - const defaultValueType = typeAnnotation.typeParameters.params[1].type; - typeAnnotation = typeAnnotation.typeParameters.params[0]; - type = - typeAnnotation.type === 'GenericTypeAnnotation' - ? typeAnnotation.id.name - : typeAnnotation.type; + if (defaultValueType === 'TSLiteralType') { + defaultValueType = typeAnnotation.typeParameters.params[1].literal.type; + defaultValue = typeAnnotation.typeParameters.params[1].literal.value; + } - if (defaultValueType === 'NullLiteralTypeAnnotation') { + if (defaultValueType === 'TSNullKeyword') { defaultValue = null; withNullDefault = true; } + + typeAnnotation = typeAnnotation.typeParameters.params[0]; + type = + typeAnnotation.type === 'TSTypeReference' + ? typeAnnotation.typeName.name + : typeAnnotation.type; } return { @@ -421,7 +566,7 @@ function buildPropSchema( }; } -// $FlowFixMe[unclear-type] there's no flowtype for ASTs +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser type PropAST = Object; function verifyPropNotAlreadyDefined( @@ -441,11 +586,16 @@ function flattenProperties( ) { return typeDefinition .map(property => { - if (property.type === 'ObjectTypeProperty') { + if (property.type === 'TSPropertySignature') { return property; - } else if (property.type === 'ObjectTypeSpreadProperty') { + } else if (property.type === 'TSTypeReference') { return flattenProperties( - getPropProperties(property.argument.id.name, types), + getPropProperties(property.typeName.name, types), + types, + ); + } else if (property.type === 'TSExpressionWithTypeArguments') { + return flattenProperties( + getPropProperties(property.expression.name, types), types, ); } @@ -470,7 +620,9 @@ function getProps( types: TypeDeclarationMap, ): $ReadOnlyArray> { return flattenProperties(typeDefinition, types) - .map(property => buildPropSchema(property, types)) + .map(property => { + return buildPropSchema(property, types); + }) .filter(Boolean); }