@react-native/codegen
Version:
Code generation tools for React Native
488 lines (483 loc) • 13.3 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
'use strict';
const {verifyPropNotAlreadyDefined} = require('../../parsers-commons');
const {
flattenIntersectionType,
parseTopLevelType,
} = require('../parseTopLevelType');
function getUnionOfLiterals(name, forArray, elementTypes, defaultValue, types) {
var _elementTypes$0$liter, _elementTypes$0$liter2;
elementTypes.reduce((lastType, currType) => {
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;
});
if (defaultValue === undefined) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = elementTypes[0].type;
if (
unionType === 'TSLiteralType' &&
((_elementTypes$0$liter = elementTypes[0].literal) === null ||
_elementTypes$0$liter === void 0
? void 0
: _elementTypes$0$liter.type) === 'StringLiteral'
) {
return {
type: 'StringEnumTypeAnnotation',
default: defaultValue,
options: elementTypes.map(option => option.literal.value),
};
} else if (
unionType === 'TSLiteralType' &&
((_elementTypes$0$liter2 = elementTypes[0].literal) === null ||
_elementTypes$0$liter2 === void 0
? void 0
: _elementTypes$0$liter2.type) === 'NumericLiteral'
) {
if (forArray) {
throw new Error(`Arrays of int enums are not supported (see: "${name}")`);
} else {
return {
type: 'Int32EnumTypeAnnotation',
default: defaultValue,
options: elementTypes.map(option => option.literal.value),
};
}
} else {
var _elementTypes$0$liter3;
throw new Error(
`Unsupported union type for "${name}", received "${
unionType === 'TSLiteralType'
? (_elementTypes$0$liter3 = elementTypes[0].literal) === null ||
_elementTypes$0$liter3 === void 0
? void 0
: _elementTypes$0$liter3.type
: unionType
}"`,
);
}
}
function detectArrayType(
name,
typeAnnotation,
defaultValue,
types,
parser,
buildSchema,
) {
// Covers: readonly T[]
if (
typeAnnotation.type === 'TSTypeOperator' &&
typeAnnotation.operator === 'readonly' &&
typeAnnotation.typeAnnotation.type === 'TSArrayType'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeAnnotation.elementType,
defaultValue,
types,
parser,
buildSchema,
),
};
}
// Covers: T[]
if (typeAnnotation.type === 'TSArrayType') {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.elementType,
defaultValue,
types,
parser,
buildSchema,
),
};
}
// Covers: Array<T> and ReadonlyArray<T>
if (
typeAnnotation.type === 'TSTypeReference' &&
(parser.getTypeAnnotationName(typeAnnotation) === 'ReadonlyArray' ||
parser.getTypeAnnotationName(typeAnnotation) === 'Array')
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
parser,
buildSchema,
),
};
}
return null;
}
function buildObjectType(rawProperties, types, parser, buildSchema) {
const flattenedProperties = flattenProperties(rawProperties, types, parser);
const properties = flattenedProperties
.map(prop => buildSchema(prop, types, parser))
.filter(Boolean);
return {
type: 'ObjectTypeAnnotation',
properties,
};
}
function getPrimitiveTypeAnnotation(type) {
switch (type) {
case 'Int32':
return {
type: 'Int32TypeAnnotation',
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
};
case 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
};
case 'Stringish':
case 'TSStringKeyword':
return {
type: 'StringTypeAnnotation',
};
default:
throw new Error(`Unknown primitive type "${type}"`);
}
}
function getCommonTypeAnnotation(
name,
forArray,
type,
typeAnnotation,
defaultValue,
types,
parser,
buildSchema,
) {
switch (type) {
case 'TSTypeLiteral':
return buildObjectType(
typeAnnotation.members,
types,
parser,
buildSchema,
);
case 'TSInterfaceDeclaration':
return buildObjectType([typeAnnotation], types, parser, buildSchema);
case 'TSIntersectionType':
return buildObjectType(
flattenIntersectionType(typeAnnotation, parser, types),
types,
parser,
buildSchema,
);
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ImageRequest':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageRequestPrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'DimensionValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'DimensionPrimitive',
};
case 'TSUnionType':
return getUnionOfLiterals(
name,
forArray,
typeAnnotation.types,
defaultValue,
types,
);
case 'Int32':
case 'Double':
case 'Float':
case 'TSBooleanKeyword':
case 'Stringish':
case 'TSStringKeyword':
return getPrimitiveTypeAnnotation(type);
case 'UnsafeMixed':
return {
type: 'MixedTypeAnnotation',
};
default:
return undefined;
}
}
function getTypeAnnotationForArray(
name,
typeAnnotation,
defaultValue,
types,
parser,
buildSchema,
) {
var _extractedTypeAnnotat;
// unpack WithDefault, (T) or T|U
const topLevelType = parseTopLevelType(typeAnnotation, parser, types);
if (topLevelType.defaultValue !== undefined) {
throw new Error(
'Nested optionals such as "ReadonlyArray<boolean | null | undefined>" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray<boolean> | null | undefined"',
);
}
if (topLevelType.optional) {
throw new Error(
'Nested optionals such as "ReadonlyArray<boolean | null | undefined>" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray<boolean> | null | undefined"',
);
}
const extractedTypeAnnotation = topLevelType.type;
const arrayType = detectArrayType(
name,
extractedTypeAnnotation,
defaultValue,
types,
parser,
buildSchema,
);
if (arrayType) {
if (arrayType.elementType.type !== 'ObjectTypeAnnotation') {
throw new Error(
`Only array of array of object is supported for "${name}".`,
);
}
return arrayType;
}
const type =
extractedTypeAnnotation.elementType === 'TSTypeReference'
? parser.getTypeAnnotationName(extractedTypeAnnotation.elementType)
: ((_extractedTypeAnnotat = extractedTypeAnnotation.elementType) ===
null || _extractedTypeAnnotat === void 0
? void 0
: _extractedTypeAnnotat.type) ||
parser.getTypeAnnotationName(extractedTypeAnnotation) ||
extractedTypeAnnotation.type;
const common = getCommonTypeAnnotation(
name,
true,
type,
extractedTypeAnnotation,
defaultValue,
types,
parser,
buildSchema,
);
if (common) {
return common;
}
switch (type) {
case 'TSNumberKeyword':
return {
type: 'FloatTypeAnnotation',
};
default:
type;
throw new Error(`Unknown prop type for "${name}": ${type}`);
}
}
function setDefaultValue(common, defaultValue) {
switch (common.type) {
case 'Int32TypeAnnotation':
case 'DoubleTypeAnnotation':
common.default = defaultValue ? defaultValue : 0;
break;
case 'FloatTypeAnnotation':
common.default =
defaultValue === null ? null : defaultValue ? defaultValue : 0;
break;
case 'BooleanTypeAnnotation':
common.default = defaultValue === null ? null : !!defaultValue;
break;
case 'StringTypeAnnotation':
common.default = defaultValue === undefined ? null : defaultValue;
break;
}
}
function getTypeAnnotation(
name,
annotation,
defaultValue,
withNullDefault,
// Just to make `getTypeAnnotation` signature match with the one from Flow
types,
parser,
buildSchema,
) {
// unpack WithDefault, (T) or T|U
const topLevelType = parseTopLevelType(annotation, parser, types);
const typeAnnotation = topLevelType.type;
const arrayType = detectArrayType(
name,
typeAnnotation,
defaultValue,
types,
parser,
buildSchema,
);
if (arrayType) {
return arrayType;
}
const type =
typeAnnotation.type === 'TSTypeReference' ||
typeAnnotation.type === 'TSTypeAliasDeclaration'
? parser.getTypeAnnotationName(typeAnnotation)
: typeAnnotation.type;
const common = getCommonTypeAnnotation(
name,
false,
type,
typeAnnotation,
defaultValue,
types,
parser,
buildSchema,
);
if (common) {
setDefaultValue(common, defaultValue);
return common;
}
switch (type) {
case 'ColorArrayValue':
return {
type: 'ArrayTypeAnnotation',
elementType: {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
},
};
case 'TSNumberKeyword':
throw new Error(
`Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`,
);
case 'TSFunctionType':
throw new Error(
`Cannot use "${type}" type annotation for "${name}": must use a specific function type like BubblingEventHandler, or DirectEventHandler`,
);
default:
throw new Error(`Unknown prop type for "${name}": "${type}"`);
}
}
function getSchemaInfo(property, types, parser) {
// unpack WithDefault, (T) or T|U
const topLevelType = parseTopLevelType(
property.typeAnnotation.typeAnnotation,
parser,
types,
);
const name = property.key.name;
if (!property.optional && topLevelType.defaultValue !== undefined) {
throw new Error(
`key ${name} must be optional if used with WithDefault<> annotation`,
);
}
return {
name,
optional: property.optional || topLevelType.optional,
typeAnnotation: topLevelType.type,
defaultValue: topLevelType.defaultValue,
withNullDefault: false, // Just to make `getTypeAnnotation` signature match with the one from Flow
};
}
function flattenProperties(typeDefinition, types, parser) {
return typeDefinition
.map(property => {
if (property.type === 'TSPropertySignature') {
return property;
} else if (property.type === 'TSTypeReference') {
return flattenProperties(
parser.getProperties(property.typeName.name, types),
types,
parser,
);
} else if (
property.type === 'TSExpressionWithTypeArguments' ||
property.type === 'TSInterfaceHeritage'
) {
return flattenProperties(
parser.getProperties(property.expression.name, types),
types,
parser,
);
} else if (property.type === 'TSTypeLiteral') {
return flattenProperties(property.members, types, parser);
} else if (property.type === 'TSInterfaceDeclaration') {
return flattenProperties(
parser.getProperties(property.id.name, types),
types,
parser,
);
} else if (property.type === 'TSIntersectionType') {
return flattenProperties(property.types, types, parser);
} else {
throw new Error(
`${property.type} is not a supported object literal type.`,
);
}
})
.filter(Boolean)
.reduce((acc, item) => {
if (Array.isArray(item)) {
item.forEach(prop => {
verifyPropNotAlreadyDefined(acc, prop);
});
return acc.concat(item);
} else {
verifyPropNotAlreadyDefined(acc, item);
acc.push(item);
return acc;
}
}, [])
.filter(Boolean);
}
module.exports = {
getSchemaInfo,
getTypeAnnotation,
getPrimitiveTypeAnnotation,
flattenProperties,
};