in packages/eui/scripts/babel/proptypes-from-ts-props/index.js [452:1034]
function getPropTypesForNode(node, optional, state) {
const types = state.get('types');
if (node.isAlreadyResolved === true) return node;
let propType;
switch (node.type) {
// a type value by identifier
case 'TSTypeReference':
propType = resolveIdentifierToPropTypes(node, state);
break;
// a type annotation on a node
// Array<Foo>
// ^^^ Foo
case 'TSTypeAnnotation':
propType = getPropTypesForNode(node.typeAnnotation, true, state);
if (
types.isLiteral(propType) ||
(types.isIdentifier(propType) && propType.name === 'undefined')
) {
// can't use a literal straight, wrap it with PropTypes.oneOf([ the_literal ])
propType = convertLiteralToOneOf(types, propType);
}
break;
// Foo['bar']
case 'TSIndexedAccessType':
// verify the type of index access
if (types.isTSLiteralType(node.indexType) === false) break;
const indexedName = node.indexType.literal.value;
const objectPropType = getPropTypesForNode(node.objectType, true, state);
// verify this came out as a PropTypes.shape(), which we can pick the indexed property off of
if (
types.isCallExpression(objectPropType) &&
types.isMemberExpression(objectPropType.callee) &&
types.isIdentifier(objectPropType.callee.object) &&
objectPropType.callee.object.name === 'PropTypes' &&
types.isIdentifier(objectPropType.callee.property) &&
objectPropType.callee.property.name === 'shape'
) {
const shapeProps = objectPropType.arguments[0].properties;
for (let i = 0; i < shapeProps.length; i++) {
const prop = shapeProps[i];
if (prop.key.name === indexedName) {
propType = makePropTypeOptional(types, prop.value);
break;
}
}
}
break;
// translates intersections (Foo & Bar & Baz) to a shape with the types' members (Foo, Bar, Baz) merged together
case 'TSIntersectionType':
const usableNodes = [...node.types].filter((node) => {
const nodePropTypes = getPropTypesForNode(node, true, state);
if (
types.isMemberExpression(nodePropTypes) &&
nodePropTypes.object.name === 'PropTypes' &&
nodePropTypes.property.name === 'any'
) {
return false;
}
// validate that this resulted in a shape or oneOfType, otherwise we don't know how to extract/merge the values
if (
!types.isCallExpression(nodePropTypes) ||
!types.isMemberExpression(nodePropTypes.callee) ||
nodePropTypes.callee.object.name !== 'PropTypes' ||
(nodePropTypes.callee.property.name !== 'shape' &&
nodePropTypes.callee.property.name !== 'oneOfType')
) {
return false;
}
return true;
});
// merge the resolved proptypes for each intersection member into one object, mergedProperties
const mergedProperties = usableNodes.reduce((mergedProperties, node) => {
let nodePropTypes = getPropTypesForNode(node, true, state);
// if this is a `oneOfType` extract those properties into a `shape`
if (
types.isCallExpression(nodePropTypes) &&
types.isMemberExpression(nodePropTypes.callee) &&
nodePropTypes.callee.object.name === 'PropTypes' &&
nodePropTypes.callee.property.name === 'oneOfType'
) {
const properties = nodePropTypes.arguments[0].elements
.map((propType) => {
// This exists on a oneOfType which must be expressed as an optional proptype
propType = makePropTypeOptional(types, propType);
// validate we're working with a shape, otherwise we can't merge properties
if (
!types.isCallExpression(propType) ||
!types.isMemberExpression(propType.callee) ||
propType.callee.object.name !== 'PropTypes' ||
propType.callee.property.name !== 'shape'
) {
return null;
}
// extract all of the properties from this group and make them optional
return propType.arguments[0].properties.map((property) => {
property.value = makePropTypeOptional(types, property.value);
return property;
});
})
.filter((x) => x !== null)
.reduce((allProperties, properties) => {
return [...allProperties, ...properties];
}, []);
nodePropTypes = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('shape')
),
[types.objectExpression(properties)]
);
}
// iterate over this type's members, adding them (and their comments) to `mergedProperties`
const typeProperties = nodePropTypes.arguments[0].properties; // properties on the ObjectExpression passed to PropTypes.shape()
for (let i = 0; i < typeProperties.length; i++) {
const typeProperty = typeProperties[i];
// this member may be duplicated between two shapes, e.g. Foo = { buzz: string } & Bar = { buzz: string }
// either or both may have leading comments and we want to forward all comments to the generated prop type
const leadingComments = [
...(typeProperty.leadingComments || []),
...((mergedProperties[typeProperty.key.name]
? mergedProperties[typeProperty.key.name].leadingComments
: null) || []),
];
let propTypeValue = typeProperty.value;
if (
types.isLiteral(propTypeValue) ||
(types.isIdentifier(propTypeValue) &&
propTypeValue.name === 'undefined')
) {
// can't use a literal straight, wrap it with PropTypes.oneOf([ the_literal ])
propTypeValue = convertLiteralToOneOf(types, propTypeValue);
}
// if this property has already been found, the only action is to potentially change it to optional
if (mergedProperties.hasOwnProperty(typeProperty.key.name)) {
const existing = mergedProperties[typeProperty.key.name];
if (!areExpressionsIdentical(existing, typeProperty.value)) {
mergedProperties[typeProperty.key.name] = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('oneOfType')
),
[types.arrayExpression([existing, propTypeValue])]
);
if (
isPropTypeRequired(types, existing) &&
isPropTypeRequired(types, typeProperty.value)
) {
mergedProperties[typeProperty.key.name] = makePropTypeRequired(
types,
mergedProperties[typeProperty.key.name]
);
}
}
} else {
// property hasn't been seen yet, add it
mergedProperties[typeProperty.key.name] = propTypeValue;
}
mergedProperties[typeProperty.key.name].leadingComments =
leadingComments;
}
return mergedProperties;
}, {});
const propertyKeys = Object.keys(mergedProperties);
// if there is one or more members on `mergedProperties` then use PropTypes.shape,
// otherwise none of the types were resolvable and fallback to PropTypes.any
if (propertyKeys.length > 0) {
// At least one type/interface was resolved to proptypes
propType = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('shape')
),
[
types.objectExpression(
propertyKeys.map((propKey) => {
const objectProperty = types.objectProperty(
types.identifier(propKey),
mergedProperties[propKey]
);
objectProperty.leadingComments =
mergedProperties[propKey].leadingComments;
mergedProperties[propKey].leadingComments = null;
return objectProperty;
})
),
]
);
} else {
// None of the types were resolveable, return with PropTypes.any
propType = types.memberExpression(
types.identifier('PropTypes'),
types.identifier('any')
);
}
break;
// translate an interface definition into a PropTypes.shape
case 'TSInterfaceBody':
propType = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('shape')
),
[
types.objectExpression(
node.body
// This helps filter out index signatures from interfaces,
// which don't translate to prop types.
.filter(
(property) =>
property.key != null &&
!types.isTSNeverKeyword(
property.typeAnnotation.typeAnnotation
)
)
.map((property) => {
let propertyPropType =
property.type === 'TSMethodSignature'
? getPropTypesForNode(
{ type: 'TSFunctionType' },
property.optional,
state
)
: getPropTypesForNode(
property.typeAnnotation,
property.optional,
state
);
if (
types.isLiteral(propertyPropType) ||
(types.isIdentifier(propertyPropType) &&
propertyPropType.name === 'undefined')
) {
propertyPropType = convertLiteralToOneOf(
types,
propertyPropType
);
if (!property.optional) {
propertyPropType = makePropTypeRequired(
types,
propertyPropType
);
}
}
const objectProperty = types.objectProperty(
types.identifier(
property.key.name || `"${property.key.value}"`
),
propertyPropType
);
if (property.leadingComments != null) {
objectProperty.leadingComments = property.leadingComments.map(
({ type, value }) => ({ type, value })
);
}
return objectProperty;
})
),
]
);
break;
// resolve a type operator (keyword) that operates on a value
// currently only supporting `keyof typeof [object variable]`
case 'TSTypeOperator':
if (
node.operator === 'keyof' &&
node.typeAnnotation.type === 'TSTypeQuery'
) {
const typeDefinitions = state.get('typeDefinitions');
const typeDefinition =
typeDefinitions[node.typeAnnotation.exprName.name];
if (typeDefinition != null) {
propType = getPropTypesForNode(typeDefinition, true, state);
}
}
break;
// invoked only by `keyof typeof` TSTypeOperator, safe to form PropTypes.oneOf(Object.keys(variable))
case 'ObjectExpression':
propType = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('oneOf')
),
[
types.arrayExpression(
node.properties.map((property) =>
types.stringLiteral(
property.key
? property.key.name || property.key.value
: property.argument.name
)
)
),
]
);
break;
// translate a type definition into a PropTypes.shape
case 'TSTypeLiteral':
propType = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('shape')
),
[
types.objectExpression(
node.members
.map((property) => {
// skip never keyword
if (
types.isTSNeverKeyword(property.typeAnnotation.typeAnnotation)
)
return null;
// skip TS index signatures
if (types.isTSIndexSignature(property)) return null;
const propertyPropType =
property.type === 'TSMethodSignature'
? getPropTypesForNode(
{ type: 'TSFunctionType' },
property.optional,
state
)
: getPropTypesForNode(
property.typeAnnotation,
property.optional,
state
);
const objectProperty = types.objectProperty(
types.identifier(
property.key.name || `"${property.key.value}"`
),
propertyPropType
);
if (property.leadingComments != null) {
objectProperty.leadingComments = property.leadingComments.map(
({ type, value }) => ({ type, value })
);
}
return objectProperty;
})
.filter((x) => x != null)
),
]
);
break;
// translate a union (Foo | Bar | Baz) into PropTypes.oneOf or PropTypes.oneOfType, depending on
// the kind of members in the union (no literal values, all literals, or mixed)
// literal values are extracted into a `oneOf`, if all members are literals this oneOf is used
// otherwise `oneOfType` is used - if there are any literal values it contains the literals' `oneOf`
case 'TSUnionType':
const tsUnionTypes = node.types.map((node) =>
getPropTypesForNode(node, false, state)
);
// `tsUnionTypes` could be:
// 1. all non-literal values (string | number)
// 2. all literal values ("foo" | "bar")
// 3. a mix of value types ("foo" | number)
// this reduce finds any literal values and groups them into a oneOf node
const reducedUnionTypes = tsUnionTypes.reduce(
(foundTypes, tsUnionType) => {
if (
types.isLiteral(tsUnionType) ||
(types.isIdentifier(tsUnionType) &&
tsUnionType.name === 'undefined')
) {
if (foundTypes.oneOfPropType == null) {
foundTypes.oneOfPropType = types.arrayExpression([]);
foundTypes.unionTypes.push(
types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('oneOf')
),
[foundTypes.oneOfPropType]
)
);
}
// this is a literal value, move to the oneOfPropType argument
foundTypes.oneOfPropType.elements.push(tsUnionType);
} else {
// this is a non-literal type
foundTypes.unionTypes.push(tsUnionType);
}
return foundTypes;
},
{
unionTypes: [],
oneOfPropType: null,
}
);
// if there is only one member on the reduced union types, _and_a oneOf proptype was created,
// then that oneOf proptype is the one member on union types, and can be be extracted out
// e.g.
// PropTypes.oneOf([PropTypes.oneOf(['red', 'blue'])])
// ->
// PropTypes.oneOf(['red', 'blue'])
if (
reducedUnionTypes.unionTypes.length === 1 &&
reducedUnionTypes.oneOfPropType != null
) {
// the only proptype is a `oneOf`, use only that
propType = reducedUnionTypes.unionTypes[0];
} else {
propType = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('oneOfType')
),
[types.arrayExpression(reducedUnionTypes.unionTypes)]
);
}
break;
// translate enum to PropTypes.oneOf
case 'TSEnumDeclaration':
const memberTypes = node.members.map((member) =>
getPropTypesForNode(member, true, state)
);
propType = types.callExpression(
types.memberExpression(
types.identifier('PropTypes'),
types.identifier('oneOf')
),
[types.arrayExpression(memberTypes)]
);
break;
// translate an interface to PropTypes
case 'TSInterfaceDeclaration':
const { body, extends: extensions } = node;
// if the interface doesn't extend anything use just the interface body
if (extensions == null) {
propType = getPropTypesForNode(body, true, state);
} else {
// fake a TSIntersectionType to merge everything together
propType = getPropTypesForNode(
{
type: 'TSIntersectionType',
types: [body, ...extensions],
},
true,
state
);
}
break;
// simple pass-through wrapper
case 'TSExpressionWithTypeArguments':
propType = resolveIdentifierToPropTypes(node.expression, state);
break;
// an enum member is a simple wrapper around a type definition
case 'TSEnumMember':
propType = getPropTypesForNode(node.initializer, optional, state);
break;
// translate `string` to `PropTypes.string`
case 'TSStringKeyword':
propType = buildPropTypePrimitiveExpression(types, 'string');
break;
// translate `number` to `PropTypes.number`
case 'TSNumberKeyword':
propType = buildPropTypePrimitiveExpression(types, 'number');
break;
// translate `boolean` to `PropTypes.bool`
case 'TSBooleanKeyword':
propType = buildPropTypePrimitiveExpression(types, 'bool');
break;
// translate any function type to `PropTypes.func`
case 'TSFunctionType':
propType = buildPropTypePrimitiveExpression(types, 'func');
break;
// translate an array type, e.g. Foo[]
case 'TSArrayType':
propType = resolveArrayTypeToPropTypes(node, state);
break;
// parenthesized type is a small wrapper around another type definition
// e.g. (() => void)[]
// ^^^^^^^^^^^^ wrapping the TSFunctionType `() => void`
case 'TSParenthesizedType':
propType = getPropTypesForNode(node.typeAnnotation, optional, state);
optional = true; // handling `optional` has been delegated to the above call
break;
// literal type wraps a literal value
case 'TSLiteralType':
propType = getPropTypesForNode(node.literal, true, state);
optional = true; // cannot call `.isRequired` on a literal
break;
case 'StringLiteral':
propType = types.stringLiteral(node.value);
optional = true; // cannot call `.isRequired` on a string literal
break;
case 'NumericLiteral':
propType = types.numericLiteral(node.value);
optional = true; // cannot call `.isRequired` on a number literal
break;
case 'BooleanLiteral':
propType = types.booleanLiteral(node.value);
optional = true; // cannot call `.isRequired` on a boolean literal
break;
case 'TSNullKeyword':
propType = types.nullLiteral();
optional = true; // cannot call `.isRequired` on a null literal
break;
case 'TSUndefinedKeyword':
propType = types.identifier('undefined');
optional = true; // cannot call `.isRequired` on an undefined literal
break;
// very helpful debugging code
// default:
// debugger;
// throw new Error(`Could not generate prop types for node type ${node.type}`);
}
// if the node was unable to be translated to a prop type, fallback to PropTypes.any
if (propType == null) {
propType = types.memberExpression(
types.identifier('PropTypes'),
types.identifier('any')
);
}
if (!optional) {
propType = makePropTypeRequired(types, propType);
}
if (isTSType(node)) {
propType.isTSType = true;
}
return propType;
}