From 7615bde023000bd8a71cfbb8f9f8796db04cd3b6 Mon Sep 17 00:00:00 2001 From: Charles Dudley Date: Mon, 20 Dec 2021 14:15:57 -0800 Subject: [PATCH] TypeScript Components Summary: This is the logic for parsing Native Components. The files were copied from the flow parser and updated for TypeScript specific types and differences in the shape of the AST. The logic and code path is almost identical to the flow parser. While there is considerable duplication to the flow parser, I decided there are enough subtle differences to warrant keeping this logic separate. Changelog: [General][Add] - Add WIP TypeScript support for Native Component Codegen spec parsing Reviewed By: RSNara Differential Revision: D33080623 fbshipit-source-id: a68c8d4c4570e65a88a97dcea3cd18a6976c53c7 --- .../parsers/typescript/components/commands.js | 37 +- .../parsers/typescript/components/events.js | 97 ++++-- .../parsers/typescript/components/extends.js | 10 +- .../parsers/typescript/components/index.js | 29 +- .../parsers/typescript/components/options.js | 2 +- .../parsers/typescript/components/props.js | 318 +++++++++++++----- 6 files changed, 338 insertions(+), 155 deletions(-) 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); }