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
This commit is contained in:
Charles Dudley
2021-12-20 14:20:21 -08:00
committed by Facebook GitHub Bot
parent c532fcff90
commit 7615bde023
6 changed files with 338 additions and 155 deletions
@@ -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<NamedShape<CommandTypeAnnotation>> {
return commandTypeAST
.filter(property => property.type === 'ObjectTypeProperty')
.filter(property => property.type === 'TSPropertySignature')
.map(property => buildCommandSchema(property, types))
.filter(Boolean);
}
@@ -22,12 +22,12 @@ function getPropertyType(
typeAnnotation,
): NamedShape<EventTypeAnnotation> {
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 <T | null | void>
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<EventTypeAnnotation> {
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<EventTypeShape> {
return eventTypeAST
.filter(property => property.type === 'ObjectTypeProperty')
.filter(property => property.type === 'TSPropertySignature')
.map(property => buildEventSchema(types, property))
.filter(Boolean);
}
@@ -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<PropsAST> {
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<ExtendsPropsShape> {
return typeDefinition
.filter(prop => prop.type === 'ObjectTypeSpreadProperty')
.filter(prop => prop.type === 'TSExpressionWithTypeArguments')
.map(prop => extendsForProp(prop, types))
.filter(Boolean);
}
@@ -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,
@@ -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<{
@@ -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<?boolean>" are not supported, please declare optionals at the top level of value definitions as in "?$ReadOnlyArray<boolean>"',
'Nested optionals such as "ReadonlyArray<boolean | null | void>" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray<boolean> | 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<WithDefault<boolean, false>>" are not supported, please declare defaults at the top level of value definitions as in "WithDefault<$ReadOnlyArray<boolean>, false>"',
'Nested defaults such as "ReadonlyArray<WithDefault<boolean, false>>" are not supported, please declare defaults at the top level of value definitions as in "WithDefault<ReadonlyArray<boolean>, 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<T>
if (
typeAnnotation.type === 'TSTypeReference' &&
typeAnnotation.typeName.name === 'ReadonlyArray'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
),
};
}
// Covers: Readonly<T[]>
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<PropTypeAnnotation> {
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<string, ''>;
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<NamedShape<PropTypeAnnotation>> {
return flattenProperties(typeDefinition, types)
.map(property => buildPropSchema(property, types))
.map(property => {
return buildPropSchema(property, types);
})
.filter(Boolean);
}