mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
c532fcff90
commit
7615bde023
+22
-15
@@ -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);
|
||||
}
|
||||
|
||||
+60
-37
@@ -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<{
|
||||
|
||||
+235
-83
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user