diff --git a/packages/react-native-codegen/src/parsers/typescript/components/commands.js b/packages/react-native-codegen/src/parsers/typescript/components/commands.js new file mode 100644 index 00000000000..48756da897a --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/components/commands.js @@ -0,0 +1,122 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import type { + NamedShape, + CommandTypeAnnotation, +} from '../../../CodegenSchema.js'; +import type {TypeDeclarationMap} from '../utils.js'; + +const {getValueFromTypes} = require('../utils.js'); + +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 firstParam = value.params[0].typeAnnotation; + + if ( + !( + firstParam.id != null && + firstParam.id.type === 'QualifiedTypeIdentifier' && + firstParam.id.qualification.name === 'React' && + firstParam.id.id.name === 'ElementRef' + ) + ) { + throw new Error( + `The first argument of method ${name} must be of type React.ElementRef<>`, + ); + } + + const params = value.params.slice(1).map(param => { + const paramName = param.name.name; + const paramValue = getValueFromTypes(param.typeAnnotation, types); + const type = + paramValue.type === 'GenericTypeAnnotation' + ? paramValue.id.name + : paramValue.type; + let returnType; + + switch (type) { + case 'RootTag': + returnType = { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }; + break; + case 'BooleanTypeAnnotation': + returnType = { + type: 'BooleanTypeAnnotation', + }; + break; + case 'Int32': + returnType = { + type: 'Int32TypeAnnotation', + }; + break; + case 'Double': + returnType = { + type: 'DoubleTypeAnnotation', + }; + break; + case 'Float': + returnType = { + type: 'FloatTypeAnnotation', + }; + break; + case 'StringTypeAnnotation': + returnType = { + type: 'StringTypeAnnotation', + }; + break; + default: + (type: empty); + throw new Error( + `Unsupported param type for method "${name}", param "${paramName}". Found ${type}`, + ); + } + + return { + name: paramName, + typeAnnotation: returnType, + }; + }); + + return { + name, + optional, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params, + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }; +} + +function getCommands( + commandTypeAST: $ReadOnlyArray, + types: TypeDeclarationMap, +): $ReadOnlyArray> { + return commandTypeAST + .filter(property => property.type === 'ObjectTypeProperty') + .map(property => buildCommandSchema(property, types)) + .filter(Boolean); +} + +module.exports = { + getCommands, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/components/events.js b/packages/react-native-codegen/src/parsers/typescript/components/events.js new file mode 100644 index 00000000000..f6c68c80fcd --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/components/events.js @@ -0,0 +1,252 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type { + EventTypeShape, + NamedShape, + EventTypeAnnotation, +} from '../../../CodegenSchema.js'; + +function getPropertyType( + name, + optional, + typeAnnotation, +): NamedShape { + const type = + typeAnnotation.type === 'GenericTypeAnnotation' + ? typeAnnotation.id.name + : typeAnnotation.type; + + switch (type) { + case 'BooleanTypeAnnotation': + return { + name, + optional, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }; + case 'StringTypeAnnotation': + return { + name, + optional, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }; + case 'Int32': + return { + name, + optional, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }; + case 'Double': + return { + name, + optional, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }; + case 'Float': + return { + name, + optional, + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }; + case '$ReadOnly': + return getPropertyType( + name, + optional, + typeAnnotation.typeParameters.params[0], + ); + case 'ObjectTypeAnnotation': + return { + name, + optional, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: typeAnnotation.properties.map(buildPropertiesForEvent), + }, + }; + case 'UnionTypeAnnotation': + return { + name, + optional, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + options: typeAnnotation.types.map(option => option.value), + }, + }; + default: + (type: empty); + throw new Error(`Unable to determine event type for "${name}": ${type}`); + } +} + +function findEventArgumentsAndType( + typeAnnotation, + types, + bubblingType, + paperName, +) { + if (!typeAnnotation.id) { + throw new Error("typeAnnotation of event doesn't have a name"); + } + const name = typeAnnotation.id.name; + if (name === '$ReadOnly') { + return { + argumentProps: typeAnnotation.typeParameters.params[0].properties, + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') { + const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct'; + const paperTopLevelNameDeprecated = + typeAnnotation.typeParameters.params.length > 1 + ? typeAnnotation.typeParameters.params[1].value + : null; + if ( + typeAnnotation.typeParameters.params[0].type === + 'NullLiteralTypeAnnotation' + ) { + return { + argumentProps: [], + bubblingType: eventType, + paperTopLevelNameDeprecated, + }; + } + return findEventArgumentsAndType( + typeAnnotation.typeParameters.params[0], + types, + eventType, + paperTopLevelNameDeprecated, + ); + } else if (types[name]) { + return findEventArgumentsAndType( + types[name].right, + types, + bubblingType, + paperName, + ); + } else { + return { + argumentProps: null, + bubblingType: null, + paperTopLevelNameDeprecated: null, + }; + } +} + +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; + + return getPropertyType(name, optional, typeAnnotation); +} + +function getEventArgument(argumentProps, name) { + return { + type: 'ObjectTypeAnnotation', + properties: argumentProps.map(buildPropertiesForEvent), + }; +} + +function buildEventSchema( + types: TypeMap, + 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; + + if ( + typeAnnotation.type !== 'GenericTypeAnnotation' || + (typeAnnotation.id.name !== 'BubblingEventHandler' && + typeAnnotation.id.name !== 'DirectEventHandler') + ) { + return null; + } + + const {argumentProps, bubblingType, paperTopLevelNameDeprecated} = + findEventArgumentsAndType(typeAnnotation, types); + + if (bubblingType && argumentProps) { + if (paperTopLevelNameDeprecated != null) { + return { + name, + optional, + bubblingType, + paperTopLevelNameDeprecated, + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: getEventArgument(argumentProps, name), + }, + }; + } + + return { + name, + optional, + bubblingType, + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: getEventArgument(argumentProps, name), + }, + }; + } + + if (argumentProps === null) { + throw new Error(`Unable to determine event arguments for "${name}"`); + } + + if (bubblingType === null) { + throw new Error(`Unable to determine event arguments for "${name}"`); + } +} + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +type EventTypeAST = Object; + +type TypeMap = { + // $FlowFixMe[unclear-type] there's no flowtype for ASTs + [string]: Object, + ... +}; + +function getEvents( + eventTypeAST: $ReadOnlyArray, + types: TypeMap, +): $ReadOnlyArray { + return eventTypeAST + .filter(property => property.type === 'ObjectTypeProperty') + .map(property => buildEventSchema(types, property)) + .filter(Boolean); +} + +module.exports = { + getEvents, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/components/extends.js b/packages/react-native-codegen/src/parsers/typescript/components/extends.js new file mode 100644 index 00000000000..890329400a6 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/components/extends.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {ExtendsPropsShape} from '../../../CodegenSchema.js'; +import type {TypeDeclarationMap} from '../utils.js'; + +function extendsForProp(prop: PropsAST, types: TypeDeclarationMap) { + if (!prop.argument) { + console.log('null', prop); + } + const name = prop.argument.id.name; + + if (types[name] != null) { + // This type is locally defined in the file + return null; + } + + switch (name) { + case 'ViewProps': + return { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }; + default: { + throw new Error(`Unable to handle prop spread: ${name}`); + } + } +} + +function removeKnownExtends( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, +): $ReadOnlyArray { + return typeDefinition.filter( + prop => + prop.type !== 'ObjectTypeSpreadProperty' || + extendsForProp(prop, types) === null, + ); +} + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +type PropsAST = Object; + +function getExtendsProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, +): $ReadOnlyArray { + return typeDefinition + .filter(prop => prop.type === 'ObjectTypeSpreadProperty') + .map(prop => extendsForProp(prop, types)) + .filter(Boolean); +} + +module.exports = { + getExtendsProps, + removeKnownExtends, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/components/index.js b/packages/react-native-codegen/src/parsers/typescript/components/index.js new file mode 100644 index 00000000000..e49d40b9846 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/components/index.js @@ -0,0 +1,218 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; +import type {CommandOptions} from './options'; +import type {TypeDeclarationMap} from '../utils'; + +import type {ComponentSchemaBuilderConfig} from './schema.js'; +const {getCommands} = require('./commands'); +const {getEvents} = require('./events'); +const {getProps, getPropProperties} = require('./props'); +const {getCommandOptions, getOptions} = require('./options'); +const {getExtendsProps, removeKnownExtends} = require('./extends'); +const {getTypes} = require('../utils'); + +function findComponentConfig(ast) { + const foundConfigs = []; + + const defaultExports = ast.body.filter( + node => node.type === 'ExportDefaultDeclaration', + ); + + defaultExports.forEach(statement => { + let declaration = statement.declaration; + + // codegenNativeComponent can be nested inside a cast + // expression so we need to go one level deeper + if (declaration.type === 'TypeCastExpression') { + declaration = declaration.expression; + } + + try { + if (declaration.callee.name === 'codegenNativeComponent') { + const typeArgumentParams = declaration.typeArguments.params; + const funcArgumentParams = declaration.arguments; + + const nativeComponentType = {}; + nativeComponentType.propsTypeName = typeArgumentParams[0].id.name; + nativeComponentType.componentName = funcArgumentParams[0].value; + if (funcArgumentParams.length > 1) { + nativeComponentType.optionsExpression = funcArgumentParams[1]; + } + foundConfigs.push(nativeComponentType); + } + } catch (e) { + // ignore + } + }); + + if (foundConfigs.length === 0) { + throw new Error('Could not find component config for native component'); + } + if (foundConfigs.length > 1) { + throw new Error('Only one component is supported per file'); + } + + const foundConfig = foundConfigs[0]; + + const namedExports = ast.body.filter( + node => node.type === 'ExportNamedDeclaration', + ); + + const commandsTypeNames = namedExports + .map(statement => { + let callExpression; + let calleeName; + try { + callExpression = statement.declaration.declarations[0].init; + calleeName = callExpression.callee.name; + } catch (e) { + return; + } + + if (calleeName !== 'codegenNativeCommands') { + return; + } + + // const statement.declaration.declarations[0].init + if (callExpression.arguments.length !== 1) { + throw new Error( + 'codegenNativeCommands must be passed options including the supported commands', + ); + } + + const typeArgumentParam = callExpression.typeArguments.params[0]; + + if (typeArgumentParam.type !== 'GenericTypeAnnotation') { + throw new Error( + "codegenNativeCommands doesn't support inline definitions. Specify a file local type alias", + ); + } + + return { + commandTypeName: typeArgumentParam.id.name, + commandOptionsExpression: callExpression.arguments[0], + }; + }) + .filter(Boolean); + + if (commandsTypeNames.length > 1) { + throw new Error('codegenNativeCommands may only be called once in a file'); + } + + return { + ...foundConfig, + commandTypeName: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandTypeName, + commandOptionsExpression: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandOptionsExpression, + }; +} + +function getCommandProperties( + commandTypeName, + types: TypeDeclarationMap, + commandOptions: ?CommandOptions, +) { + if (commandTypeName == null) { + return []; + } + + const typeAlias = types[commandTypeName]; + + if (typeAlias.type !== 'InterfaceDeclaration') { + throw new Error( + `The type argument for codegenNativeCommands must be an interface, received ${typeAlias.type}`, + ); + } + + let properties; + try { + properties = typeAlias.body.properties; + } catch (e) { + throw new Error( + `Failed to find type definition for "${commandTypeName}", please check that you have a valid codegen flow file`, + ); + } + + const flowPropertyNames = properties + .map(property => property && property.key && property.key.name) + .filter(Boolean); + + if (commandOptions == null || commandOptions.supportedCommands == null) { + throw new Error( + 'codegenNativeCommands must be given an options object with supportedCommands array', + ); + } + + if ( + commandOptions.supportedCommands.length !== flowPropertyNames.length || + !commandOptions.supportedCommands.every(supportedCommand => + flowPropertyNames.includes(supportedCommand), + ) + ) { + throw new Error( + `codegenNativeCommands expected the same supportedCommands specified in the ${commandTypeName} interface: ${flowPropertyNames.join( + ', ', + )}`, + ); + } + + return properties; +} + +// $FlowFixMe[signature-verification-failure] there's no flowtype for AST +function buildComponentSchema(ast): ComponentSchemaBuilderConfig { + const { + componentName, + propsTypeName, + commandTypeName, + commandOptionsExpression, + optionsExpression, + } = findComponentConfig(ast); + + const types = getTypes(ast); + + const propProperties = getPropProperties(propsTypeName, types); + const commandOptions = getCommandOptions(commandOptionsExpression); + + const commandProperties = getCommandProperties( + commandTypeName, + types, + commandOptions, + ); + + const extendsProps = getExtendsProps(propProperties, types); + const options = getOptions(optionsExpression); + + const nonExtendsProps = removeKnownExtends(propProperties, types); + const props = getProps(nonExtendsProps, types); + const events = getEvents(propProperties, types); + const commands = getCommands(commandProperties, types); + + return { + filename: componentName, + componentName, + options, + extendsProps, + events, + props, + commands, + }; +} + +module.exports = { + buildComponentSchema, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/components/options.js b/packages/react-native-codegen/src/parsers/typescript/components/options.js new file mode 100644 index 00000000000..8a137c5cd7c --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/components/options.js @@ -0,0 +1,87 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {OptionsShape} from '../../../CodegenSchema.js'; + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +type OptionsAST = Object; + +export type CommandOptions = $ReadOnly<{ + supportedCommands: $ReadOnlyArray, +}>; + +function getCommandOptions( + commandOptionsExpression: OptionsAST, +): ?CommandOptions { + if (commandOptionsExpression == null) { + return null; + } + + let foundOptions; + try { + foundOptions = commandOptionsExpression.properties.reduce( + (options, prop) => { + options[prop.key.name] = ( + (prop && prop.value && prop.value.elements) || + [] + ).map(element => element && element.value); + return options; + }, + {}, + ); + } catch (e) { + throw new Error( + 'Failed to parse command options, please check that they are defined correctly', + ); + } + + return foundOptions; +} + +function getOptions(optionsExpression: OptionsAST): ?OptionsShape { + if (!optionsExpression) { + return null; + } + let foundOptions; + try { + foundOptions = optionsExpression.properties.reduce((options, prop) => { + if (prop.value.type === 'ArrayExpression') { + options[prop.key.name] = prop.value.elements.map( + element => element.value, + ); + } else { + options[prop.key.name] = prop.value.value; + } + return options; + }, {}); + } catch (e) { + throw new Error( + 'Failed to parse codegen options, please check that they are defined correctly', + ); + } + + if ( + foundOptions.paperComponentName && + foundOptions.paperComponentNameDeprecated + ) { + throw new Error( + 'Failed to parse codegen options, cannot use both paperComponentName and paperComponentNameDeprecated', + ); + } + + return foundOptions; +} + +module.exports = { + getCommandOptions, + getOptions, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/components/props.js b/packages/react-native-codegen/src/parsers/typescript/components/props.js new file mode 100644 index 00000000000..ab8d9caa41a --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/components/props.js @@ -0,0 +1,480 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +const {getValueFromTypes} = require('../utils.js'); + +import type {NamedShape, PropTypeAnnotation} from '../../../CodegenSchema.js'; +import type {TypeDeclarationMap} from '../utils.js'; + +function getPropProperties( + propsTypeName: string, + types: TypeDeclarationMap, +): $FlowFixMe { + const typeAlias = types[propsTypeName]; + try { + return typeAlias.right.typeParameters.params[0].properties; + } catch (e) { + throw new Error( + `Failed to find type definition for "${propsTypeName}", please check that you have a valid codegen flow file`, + ); + } +} + +function getTypeAnnotationForArray( + name: string, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe | null, + types: TypeDeclarationMap, +) { + const extractedTypeAnnotation = getValueFromTypes(typeAnnotation, types); + if (extractedTypeAnnotation.type === 'NullableTypeAnnotation') { + throw new Error( + 'Nested optionals such as "$ReadOnlyArray" are not supported, please declare optionals at the top level of value definitions as in "?$ReadOnlyArray"', + ); + } + + if ( + extractedTypeAnnotation.type === 'GenericTypeAnnotation' && + extractedTypeAnnotation.id.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>"', + ); + } + + if (extractedTypeAnnotation.type === 'GenericTypeAnnotation') { + // Resolve the type alias if it's not defined inline + const objectType = getValueFromTypes(extractedTypeAnnotation, types); + + if (objectType.id.name === '$ReadOnly') { + return { + type: 'ObjectTypeAnnotation', + properties: flattenProperties( + objectType.typeParameters.params[0].properties, + types, + ) + .map(prop => buildPropSchema(prop, types)) + .filter(Boolean), + }; + } + + if (objectType.id.name === '$ReadOnlyArray') { + // We need to go yet another level deeper to resolve + // types that may be defined in a type alias + const nestedObjectType = getValueFromTypes( + objectType.typeParameters.params[0], + types, + ); + + return { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: flattenProperties( + nestedObjectType.typeParameters.params[0].properties, + types, + ) + .map(prop => buildPropSchema(prop, types)) + .filter(Boolean), + }, + }; + } + } + + const type = + extractedTypeAnnotation.type === 'GenericTypeAnnotation' + ? extractedTypeAnnotation.id.name + : extractedTypeAnnotation.type; + + switch (type) { + case 'ImageSource': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }; + case 'ColorValue': + case 'ProcessedColorValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }; + case 'PointValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }; + case 'EdgeInsetsValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'EdgeInsetsPrimitive', + }; + case 'Stringish': + return { + type: 'StringTypeAnnotation', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + }; + case 'Double': + return { + type: 'DoubleTypeAnnotation', + }; + case 'Float': + return { + type: 'FloatTypeAnnotation', + }; + case 'BooleanTypeAnnotation': + return { + type: 'BooleanTypeAnnotation', + }; + case 'StringTypeAnnotation': + return { + type: 'StringTypeAnnotation', + }; + case 'UnionTypeAnnotation': + typeAnnotation.types.reduce((lastType, currType) => { + if (lastType && currType.type !== lastType.type) { + throw new Error(`Mixed types are not supported (see "${name}")`); + } + return currType; + }); + + if (defaultValue === null) { + throw new Error(`A default enum value is required for "${name}"`); + } + + const unionType = typeAnnotation.types[0].type; + if (unionType === 'StringLiteralTypeAnnotation') { + return { + type: 'StringEnumTypeAnnotation', + default: (defaultValue: string), + options: typeAnnotation.types.map(option => option.value), + }; + } else if (unionType === 'NumberLiteralTypeAnnotation') { + throw new Error( + `Arrays of int enums are not supported (see: "${name}")`, + ); + } else { + throw new Error( + `Unsupported union type for "${name}", received "${unionType}"`, + ); + } + default: + (type: empty); + throw new Error(`Unknown prop type for "${name}": ${type}`); + } +} + +function getTypeAnnotation( + name: string, + annotation, + defaultValue: $FlowFixMe | null, + withNullDefault: boolean, + types: TypeDeclarationMap, +) { + const typeAnnotation = getValueFromTypes(annotation, types); + + if ( + typeAnnotation.type === 'GenericTypeAnnotation' && + typeAnnotation.id.name === '$ReadOnlyArray' + ) { + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( + name, + typeAnnotation.typeParameters.params[0], + defaultValue, + types, + ), + }; + } + + if ( + typeAnnotation.type === 'GenericTypeAnnotation' && + typeAnnotation.id.name === '$ReadOnly' + ) { + return { + type: 'ObjectTypeAnnotation', + properties: flattenProperties( + typeAnnotation.typeParameters.params[0].properties, + types, + ) + .map(prop => buildPropSchema(prop, types)) + .filter(Boolean), + }; + } + + const type = + typeAnnotation.type === 'GenericTypeAnnotation' + ? typeAnnotation.id.name + : typeAnnotation.type; + + switch (type) { + case 'ImageSource': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }; + case 'ColorValue': + case 'ProcessedColorValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }; + case 'ColorArrayValue': + return { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }; + case 'PointValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }; + case 'EdgeInsetsValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'EdgeInsetsPrimitive', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + default: ((defaultValue ? defaultValue : 0): number), + }; + case 'Double': + return { + type: 'DoubleTypeAnnotation', + default: ((defaultValue ? defaultValue : 0): number), + }; + case 'Float': + return { + type: 'FloatTypeAnnotation', + default: withNullDefault + ? (defaultValue: number | null) + : ((defaultValue ? defaultValue : 0): number), + }; + case 'BooleanTypeAnnotation': + return { + type: 'BooleanTypeAnnotation', + default: withNullDefault + ? (defaultValue: boolean | null) + : ((defaultValue == null ? false : defaultValue): boolean), + }; + case 'StringTypeAnnotation': + if (typeof defaultValue !== 'undefined') { + return { + type: 'StringTypeAnnotation', + default: (defaultValue: string | null), + }; + } + throw new Error(`A default string (or null) is required for "${name}"`); + case 'Stringish': + if (typeof defaultValue !== 'undefined') { + return { + type: 'StringTypeAnnotation', + default: (defaultValue: string | null), + }; + } + throw new Error(`A default string (or null) is required for "${name}"`); + case 'UnionTypeAnnotation': + typeAnnotation.types.reduce((lastType, currType) => { + if (lastType && currType.type !== lastType.type) { + throw new Error(`Mixed types are not supported (see "${name}")`); + } + return currType; + }); + + if (defaultValue === null) { + throw new Error(`A default enum value is required for "${name}"`); + } + + const unionType = typeAnnotation.types[0].type; + if (unionType === 'StringLiteralTypeAnnotation') { + return { + type: 'StringEnumTypeAnnotation', + default: (defaultValue: string), + options: typeAnnotation.types.map(option => option.value), + }; + } else if (unionType === 'NumberLiteralTypeAnnotation') { + return { + type: 'Int32EnumTypeAnnotation', + default: (defaultValue: number), + options: typeAnnotation.types.map(option => option.value), + }; + } else { + throw new Error( + `Unsupported union type for "${name}", received "${unionType}"`, + ); + } + case 'NumberTypeAnnotation': + throw new Error( + `Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`, + ); + default: + (type: empty); + throw new Error(`Unknown prop type for "${name}": "${type}"`); + } +} + +function buildPropSchema( + property: PropAST, + types: TypeDeclarationMap, +): ?NamedShape { + const name = property.key.name; + + const value = getValueFromTypes(property.value, types); + let typeAnnotation = + value.type === 'NullableTypeAnnotation' ? value.typeAnnotation : value; + + const optional = + value.type === 'NullableTypeAnnotation' || + property.optional || + (value.type === 'GenericTypeAnnotation' && + typeAnnotation.id.name === 'WithDefault'); + + if ( + !property.optional && + value.type === 'GenericTypeAnnotation' && + typeAnnotation.id.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') + ) { + return null; + } + + if ( + name === 'style' && + type === 'GenericTypeAnnotation' && + typeAnnotation.id.name === 'ViewStyleProp' + ) { + return null; + } + + let defaultValue = null; + let withNullDefault = false; + if ( + type === 'GenericTypeAnnotation' && + typeAnnotation.id.name === 'WithDefault' + ) { + if (typeAnnotation.typeParameters.params.length === 1) { + throw new Error( + `WithDefault requires two parameters, did you forget to provide a default value for "${name}"?`, + ); + } + + 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 === 'NullLiteralTypeAnnotation') { + defaultValue = null; + withNullDefault = true; + } + } + + return { + name, + optional, + typeAnnotation: getTypeAnnotation( + name, + typeAnnotation, + defaultValue, + withNullDefault, + types, + ), + }; +} + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +type PropAST = Object; + +function verifyPropNotAlreadyDefined( + props: $ReadOnlyArray, + needleProp: PropAST, +) { + const propName = needleProp.key.name; + const foundProp = props.some(prop => prop.key.name === propName); + if (foundProp) { + throw new Error(`A prop was already defined with the name ${propName}`); + } +} + +function flattenProperties( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, +) { + return typeDefinition + .map(property => { + if (property.type === 'ObjectTypeProperty') { + return property; + } else if (property.type === 'ObjectTypeSpreadProperty') { + return flattenProperties( + getPropProperties(property.argument.id.name, types), + types, + ); + } + }) + .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); +} + +function getProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, +): $ReadOnlyArray> { + return flattenProperties(typeDefinition, types) + .map(property => buildPropSchema(property, types)) + .filter(Boolean); +} + +module.exports = { + getProps, + getPropProperties, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/components/schema.js b/packages/react-native-codegen/src/parsers/typescript/components/schema.js new file mode 100644 index 00000000000..21ce46d6856 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/components/schema.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import type { + EventTypeShape, + NamedShape, + CommandTypeAnnotation, + PropTypeAnnotation, + ExtendsPropsShape, + SchemaType, + OptionsShape, +} from '../../../CodegenSchema.js'; + +export type ComponentSchemaBuilderConfig = $ReadOnly<{ + filename: string, + componentName: string, + extendsProps: $ReadOnlyArray, + events: $ReadOnlyArray, + props: $ReadOnlyArray>, + commands: $ReadOnlyArray>, + options?: ?OptionsShape, +}>; + +function wrapComponentSchema({ + filename, + componentName, + extendsProps, + events, + props, + options, + commands, +}: ComponentSchemaBuilderConfig): SchemaType { + return { + modules: { + [filename]: { + type: 'Component', + components: { + [componentName]: { + ...(options || {}), + extendsProps, + events, + props, + commands, + }, + }, + }, + }, + }; +} + +module.exports = { + wrapComponentSchema, +};