/** * 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. * * @flow strict * @format */ 'use strict'; import type {ComponentShape} from '../../CodegenSchema'; const { getNativeTypeFromAnnotation, getLocalImports, } = require('./ComponentsGeneratorUtils.js'); const { convertDefaultTypeToString, getEnumMaskName, generateStructName, toIntEnumValueName, } = require('./CppHelpers.js'); const {getEnumName, toSafeCppString} = require('../Utils'); import type { ExtendsPropsShape, NamedShape, PropTypeAnnotation, SchemaType, } from '../../CodegenSchema'; // File path -> contents type FilesOutput = Map; type StructsMap = Map; const FileTemplate = ({ imports, componentClasses, }: { imports: string, componentClasses: string, }) => ` /** * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). * * Do not edit this file as changes may cause incorrect behavior and will be lost * once the code is regenerated. * * ${'@'}generated by codegen project: GeneratePropsH.js */ #pragma once ${imports} namespace facebook { namespace react { ${componentClasses} } // namespace react } // namespace facebook `; const ClassTemplate = ({ enums, structs, className, props, extendClasses, }: { enums: string, structs: string, className: string, props: string, extendClasses: string, }) => ` ${enums} ${structs} class JSI_EXPORT ${className} final${extendClasses} { public: ${className}() = default; ${className}(const PropsParserContext& context, const ${className} &sourceProps, const RawProps &rawProps); #pragma mark - Props ${props} }; `.trim(); const EnumTemplate = ({ enumName, values, fromCases, toCases, }: { enumName: string, values: string, fromCases: string, toCases: string, }) => ` enum class ${enumName} { ${values} }; static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, ${enumName} &result) { auto string = (std::string)value; ${fromCases} abort(); } static inline std::string toString(const ${enumName} &value) { switch (value) { ${toCases} } } `.trim(); const IntEnumTemplate = ({ enumName, values, fromCases, toCases, }: { enumName: string, values: string, fromCases: string, toCases: string, }) => ` enum class ${enumName} { ${values} }; static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, ${enumName} &result) { assert(value.hasType()); auto integerValue = (int)value; switch (integerValue) {${fromCases} } abort(); } static inline std::string toString(const ${enumName} &value) { switch (value) { ${toCases} } } `.trim(); const StructTemplate = ({ structName, fields, fromCases, }: { structName: string, fields: string, fromCases: string, }) => `struct ${structName} { ${fields} }; static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, ${structName} &result) { auto map = (butter::map)value; ${fromCases} } static inline std::string toString(const ${structName} &value) { return "[Object ${structName}]"; } `.trim(); const ArrayConversionFunctionTemplate = ({ structName, }: { structName: string, }) => `static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, std::vector<${structName}> &result) { auto items = (std::vector)value; for (const auto &item : items) { ${structName} newItem; fromRawValue(context, item, newItem); result.emplace_back(newItem); } } `; const DoubleArrayConversionFunctionTemplate = ({ structName, }: { structName: string, }) => `static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, std::vector> &result) { auto items = (std::vector>)value; for (const std::vector &item : items) { auto nestedArray = std::vector<${structName}>{}; for (const RawValue &nestedItem : item) { ${structName} newItem; fromRawValue(context, nestedItem, newItem); nestedArray.emplace_back(newItem); } result.emplace_back(nestedArray); } } `; const ArrayEnumTemplate = ({ enumName, enumMask, values, fromCases, toCases, }: { enumName: string, enumMask: string, values: string, fromCases: string, toCases: string, }) => ` using ${enumMask} = uint32_t; enum class ${enumName}: ${enumMask} { ${values} }; constexpr bool operator&( ${enumMask} const lhs, enum ${enumName} const rhs) { return lhs & static_cast<${enumMask}>(rhs); } constexpr ${enumMask} operator|( ${enumMask} const lhs, enum ${enumName} const rhs) { return lhs | static_cast<${enumMask}>(rhs); } constexpr void operator|=( ${enumMask} &lhs, enum ${enumName} const rhs) { lhs = lhs | static_cast<${enumMask}>(rhs); } static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, ${enumMask} &result) { auto items = std::vector{value}; for (const auto &item : items) { ${fromCases} abort(); } } static inline std::string toString(const ${enumMask} &value) { auto result = std::string{}; auto separator = std::string{", "}; ${toCases} if (!result.empty()) { result.erase(result.length() - separator.length()); } return result; } `.trim(); function getClassExtendString(component: ComponentShape): string { if (component.extendsProps.length === 0) { throw new Error('Invalid: component.extendsProps is empty'); } const extendString = ' : ' + component.extendsProps .map(extendProps => { switch (extendProps.type) { case 'ReactNativeBuiltInType': switch (extendProps.knownTypeName) { case 'ReactNativeCoreViewProps': return 'public ViewProps'; default: (extendProps.knownTypeName: empty); throw new Error('Invalid knownTypeName'); } default: (extendProps.type: empty); throw new Error('Invalid extended type'); } }) .join(' '); return extendString; } function convertValueToEnumOption(value: string): string { return toSafeCppString(value); } function generateArrayEnumString( componentName: string, name: string, options: $ReadOnlyArray, ): string { const enumName = getEnumName(componentName, name); const values = options .map((option, index) => `${toSafeCppString(option)} = 1 << ${index}`) .join(',\n '); const fromCases = options .map( option => `if (item == "${option}") { result |= ${enumName}::${toSafeCppString(option)}; continue; }`, ) .join('\n '); const toCases = options .map( option => `if (value & ${enumName}::${toSafeCppString(option)}) { result += "${option}" + separator; }`, ) .join('\n' + ' '); return ArrayEnumTemplate({ enumName, enumMask: getEnumMaskName(enumName), values, fromCases, toCases, }); } function generateStringEnum( componentName: string, prop: NamedShape, ) { const typeAnnotation = prop.typeAnnotation; if (typeAnnotation.type === 'StringEnumTypeAnnotation') { const values: $ReadOnlyArray = typeAnnotation.options; const enumName = getEnumName(componentName, prop.name); const fromCases = values .map( value => `if (string == "${value}") { result = ${enumName}::${convertValueToEnumOption( value, )}; return; }`, ) .join('\n' + ' '); const toCases = values .map( value => `case ${enumName}::${convertValueToEnumOption( value, )}: return "${value}";`, ) .join('\n' + ' '); return EnumTemplate({ enumName, values: values.map(toSafeCppString).join(', '), fromCases: fromCases, toCases: toCases, }); } return ''; } function generateIntEnum( componentName: string, prop: NamedShape, ) { const typeAnnotation = prop.typeAnnotation; if (typeAnnotation.type === 'Int32EnumTypeAnnotation') { const values: $ReadOnlyArray = typeAnnotation.options; const enumName = getEnumName(componentName, prop.name); const fromCases = values .map( value => ` case ${value}: result = ${enumName}::${toIntEnumValueName(prop.name, value)}; return;`, ) .join(''); const toCases = values .map( value => `case ${enumName}::${toIntEnumValueName( prop.name, value, )}: return "${value}";`, ) .join('\n' + ' '); const valueVariables = values .map(val => `${toIntEnumValueName(prop.name, val)} = ${val}`) .join(', '); return IntEnumTemplate({ enumName, values: valueVariables, fromCases, toCases, }); } return ''; } function generateEnumString( componentName: string, component: ComponentShape, ): string { return component.props .map(prop => { if ( prop.typeAnnotation.type === 'ArrayTypeAnnotation' && prop.typeAnnotation.elementType.type === 'StringEnumTypeAnnotation' ) { return generateArrayEnumString( componentName, prop.name, prop.typeAnnotation.elementType.options, ); } if (prop.typeAnnotation.type === 'StringEnumTypeAnnotation') { return generateStringEnum(componentName, prop); } if (prop.typeAnnotation.type === 'Int32EnumTypeAnnotation') { return generateIntEnum(componentName, prop); } if (prop.typeAnnotation.type === 'ObjectTypeAnnotation') { return prop.typeAnnotation.properties .map(property => { if (property.typeAnnotation.type === 'StringEnumTypeAnnotation') { return generateStringEnum(componentName, property); } else if ( property.typeAnnotation.type === 'Int32EnumTypeAnnotation' ) { return generateIntEnum(componentName, property); } return null; }) .filter(Boolean) .join('\n'); } }) .filter(Boolean) .join('\n'); } function generatePropsString( componentName: string, props: $ReadOnlyArray>, ) { return props .map(prop => { const nativeType = getNativeTypeFromAnnotation(componentName, prop, []); const defaultValue = convertDefaultTypeToString(componentName, prop); return `${nativeType} ${prop.name}{${defaultValue}};`; }) .join('\n' + ' '); } function getExtendsImports( extendsProps: $ReadOnlyArray, ): Set { const imports: Set = new Set(); imports.add('#include '); imports.add('#include '); extendsProps.forEach(extendProps => { switch (extendProps.type) { case 'ReactNativeBuiltInType': switch (extendProps.knownTypeName) { case 'ReactNativeCoreViewProps': imports.add( '#include ', ); return; default: (extendProps.knownTypeName: empty); throw new Error('Invalid knownTypeName'); } default: (extendProps.type: empty); throw new Error('Invalid extended type'); } }); return imports; } function generateStructsForComponent( componentName: string, component: ComponentShape, ): string { const structs = generateStructs(componentName, component.props, []); const structArray = Array.from(structs.values()); if (structArray.length < 1) { return ''; } return structArray.join('\n\n'); } function generateStructs( componentName: string, properties: $ReadOnlyArray>, nameParts: Array, ): StructsMap { const structs: StructsMap = new Map(); properties.forEach(prop => { const typeAnnotation = prop.typeAnnotation; if (typeAnnotation.type === 'ObjectTypeAnnotation') { // Recursively visit all of the object properties. // Note: this is depth first so that the nested structs are ordered first. const elementProperties = typeAnnotation.properties; const nestedStructs = generateStructs( componentName, elementProperties, nameParts.concat([prop.name]), ); nestedStructs.forEach(function (value, key) { structs.set(key, value); }); generateStruct( structs, componentName, nameParts.concat([prop.name]), typeAnnotation.properties, ); } if ( prop.typeAnnotation.type === 'ArrayTypeAnnotation' && prop.typeAnnotation.elementType.type === 'ObjectTypeAnnotation' ) { // Recursively visit all of the object properties. // Note: this is depth first so that the nested structs are ordered first. const elementProperties = prop.typeAnnotation.elementType.properties; const nestedStructs = generateStructs( componentName, elementProperties, nameParts.concat([prop.name]), ); nestedStructs.forEach(function (value, key) { structs.set(key, value); }); // Generate this struct and its conversion function. generateStruct( structs, componentName, nameParts.concat([prop.name]), elementProperties, ); // Generate the conversion function for std:vector. // Note: This needs to be at the end since it references the struct above. structs.set( `${[componentName, ...nameParts.concat([prop.name])].join( '', )}ArrayStruct`, ArrayConversionFunctionTemplate({ structName: generateStructName( componentName, nameParts.concat([prop.name]), ), }), ); } if ( prop.typeAnnotation.type === 'ArrayTypeAnnotation' && prop.typeAnnotation.elementType.type === 'ArrayTypeAnnotation' && prop.typeAnnotation.elementType.elementType.type === 'ObjectTypeAnnotation' ) { // Recursively visit all of the object properties. // Note: this is depth first so that the nested structs are ordered first. const elementProperties = prop.typeAnnotation.elementType.elementType.properties; const nestedStructs = generateStructs( componentName, elementProperties, nameParts.concat([prop.name]), ); nestedStructs.forEach(function (value, key) { structs.set(key, value); }); // Generate this struct and its conversion function. generateStruct( structs, componentName, nameParts.concat([prop.name]), elementProperties, ); // Generate the conversion function for std:vector. // Note: This needs to be at the end since it references the struct above. structs.set( `${[componentName, ...nameParts.concat([prop.name])].join( '', )}ArrayArrayStruct`, DoubleArrayConversionFunctionTemplate({ structName: generateStructName( componentName, nameParts.concat([prop.name]), ), }), ); } }); return structs; } function generateStruct( structs: StructsMap, componentName: string, nameParts: $ReadOnlyArray, properties: $ReadOnlyArray>, ): void { const structNameParts = nameParts; const structName = generateStructName(componentName, structNameParts); const fields = properties .map(property => { return `${getNativeTypeFromAnnotation( componentName, property, structNameParts, )} ${property.name};`; }) .join('\n' + ' '); properties.forEach((property: NamedShape) => { const name = property.name; switch (property.typeAnnotation.type) { case 'BooleanTypeAnnotation': return; case 'StringTypeAnnotation': return; case 'Int32TypeAnnotation': return; case 'DoubleTypeAnnotation': return; case 'FloatTypeAnnotation': return; case 'ReservedPropTypeAnnotation': return; case 'ArrayTypeAnnotation': return; case 'StringEnumTypeAnnotation': return; case 'Int32EnumTypeAnnotation': return; case 'ObjectTypeAnnotation': const props = property.typeAnnotation.properties; if (props == null) { throw new Error( `Properties are expected for ObjectTypeAnnotation (see ${name} in ${componentName})`, ); } generateStruct(structs, componentName, nameParts.concat([name]), props); return; case 'MixedTypeAnnotation': return; default: (property.typeAnnotation.type: empty); throw new Error( `Received invalid component property type ${property.typeAnnotation.type}`, ); } }); const fromCases = properties .map(property => { const variable = 'tmp_' + property.name; return `auto ${variable} = map.find("${property.name}"); if (${variable} != map.end()) { fromRawValue(context, ${variable}->second, result.${property.name}); }`; }) .join('\n '); structs.set( structName, StructTemplate({ structName, fields, fromCases, }), ); } module.exports = { generate( libraryName: string, schema: SchemaType, packageName?: string, assumeNonnull: boolean = false, ): FilesOutput { const fileName = 'Props.h'; const allImports: Set = new Set(); const componentClasses = Object.keys(schema.modules) .map(moduleName => { const module = schema.modules[moduleName]; if (module.type !== 'Component') { return; } const {components} = module; // No components in this module if (components == null) { return null; } return Object.keys(components) .map(componentName => { const component = components[componentName]; const newName = `${componentName}Props`; const structString = generateStructsForComponent( componentName, component, ); const enumString = generateEnumString(componentName, component); const propsString = generatePropsString( componentName, component.props, ); const extendString = getClassExtendString(component); const extendsImports = getExtendsImports(component.extendsProps); const imports = getLocalImports(component.props); // $FlowFixMe[method-unbinding] added when improving typing for this parameters extendsImports.forEach(allImports.add, allImports); // $FlowFixMe[method-unbinding] added when improving typing for this parameters imports.forEach(allImports.add, allImports); const replacedTemplate = ClassTemplate({ enums: enumString, structs: structString, className: newName, extendClasses: extendString, props: propsString, }); return replacedTemplate; }) .join('\n\n'); }) .filter(Boolean) .join('\n\n'); const replacedTemplate = FileTemplate({ componentClasses, imports: Array.from(allImports).sort().join('\n'), }); return new Map([[fileName, replacedTemplate]]); }, };